Skip to content

feat(cli): OpenTUI migration foundation batch — theme, a11y, clipboard, keys, dialogs scaffolding - #10146

Open
chiga0 wants to merge 13 commits into
QwenLM:mainfrom
chiga0:feat/tui-foundation-batch
Open

feat(cli): OpenTUI migration foundation batch — theme, a11y, clipboard, keys, dialogs scaffolding#10146
chiga0 wants to merge 13 commits into
QwenLM:mainfrom
chiga0:feat/tui-foundation-batch

Conversation

@chiga0

@chiga0 chiga0 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Foundation batch of the OpenTUI migration tracked in #8662. Adds the renderer foundation modules under a new opentui UI directory: the theme family, accessibility layers (plain-text and screen-reader), clipboard, key mapping, mouse hit-testing and caret placement, link clicking with OSC 8 parity, early input handling, exit guard and lifecycle, kitty graphics negotiation, the stream event adapter, history item projection, slash dispatch with command parsing, command context/output plumbing, help content, input history, and the dialog scaffolding primitives plus the theme dialog. All files are new; the single touch on existing code is a one-line export addition in the shared OSC 8 module.

Stacked PR: base is the infra batch branch (see #10134), because this batch consumes the framework-neutral streaming model and the locked OpenTUI dependencies. Once #10134 merges, this PR gets retargeted to main and only its own commits remain.

Why it's needed

The migration lands in dependency-ordered batches. These modules are the shared foundation every later batch (live session, dialogs, backend composition) builds on, and landing them first keeps each follow-up batch small and reviewable. Two command/dialog helpers land inside the opentui directory instead of the leaf utils layer because main's new utils dependency-direction rule (#9737) forbids utils importing upward — the helpers are OpenTUI-specific anyway.

Reviewer Test Plan

How to verify

  1. cd packages/cli && npx vitest run src/ui/opentui/ — 349 tests pass across 26 files.
  2. cd packages/cli && npx vitest run src/ui/utils/osc8.test.ts — 99 tests pass (the one-line export change breaks nothing).
  3. npm run build clean; ESLint clean including the architecture/no-utils-upward-import rule; node scripts/check-tui-dep-direction.mjs passes.
  4. Default path unchanged: everything new lives under the opentui directory, which no reachable code imports yet.

Evidence (Before & After)

N/A (no user-visible change; modules gain consumers in later batches)

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Node 22.23.1; stacked on feat/tui-infra-batch (#10134).

Risk & Scope

Linked Issues

Part of #8662 (Foundation modules batch). Stacked on #10134.

中文说明

本 PR 做了什么

#8662 跟踪的 OpenTUI 迁移的 Foundation 批次。在新的 opentui UI 目录下加入渲染器基础模块:theme 家族、无障碍层(纯文本与读屏)、剪贴板、键位映射、鼠标命中与光标定位、OSC 8 对等的链接点击、早期输入处理、退出守卫与生命周期、kitty 图形协商、流事件适配器、历史条目投影、斜杠分发与命令解析、命令上下文/输出管道、帮助内容、输入历史,以及对话框脚手架基元和主题对话框。全部是新文件;对既有代码的唯一改动是共享 OSC 8 模块加一行 export。

叠层 PR:base 是 Infra 批分支(见 #10134),因为本批消费框架无关的 streaming model 与锁版 OpenTUI 依赖。#10134 合入后本 PR 改回 main,只剩自身提交。

为什么需要

迁移按依赖序分批落地。这些模块是后续所有批次(live session、dialogs、backend 组合根)共享的地基,先落地让后续每批保持小而可审。两个命令/对话框 helper 落在 opentui 目录而非 utils 叶子层,因为 main 新的 utils 依赖方向规则(#9737)禁止 utils 向上 import——这两个 helper 本来就是 OpenTUI 专用的。

评审测试计划

如何验证

  1. cd packages/cli && npx vitest run src/ui/opentui/——26 个文件 349 个测试通过。
  2. cd packages/cli && npx vitest run src/ui/utils/osc8.test.ts——99 个测试通过(一行 export 改动无破坏)。
  3. npm run build 干净;ESLint 干净(含 architecture/no-utils-upward-import 规则);node scripts/check-tui-dep-direction.mjs 通过。
  4. 默认路径不变:新代码全在 opentui 目录下,尚无任何可达代码 import 它。

前后对比证据

N/A(无用户可见变化;模块在后续批次获得消费者)

测试环境

操作系统 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

Node 22.23.1;叠在 feat/tui-infra-batch(#10134)之上。

风险与范围

关联 Issue

属于 #8662(Foundation modules 批次)。叠在 #10134 之上。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Re-run on the retargeted diff — gate re-checked against the current head.

Template looks good ✓

Problem: unchanged from the prior pass — this is the Foundation-modules batch of the OpenTUI migration tracked in #8662. The motivation is observed, not theoretical (flicker/mouse/renderer-maintenance reports against the patched ink build: #6137, #8580, #8659), and the maintainer explicitly approved the Phase 1 batched landing in #8662.

Direction: aligned and endorsed. Since the last pass the stack resolved: #10134 merged and this PR retargeted to main via a clean merge (zero conflicts), so this review judges the final diff against main rather than a stacked base.

Size: 54 files, +11,165 / −4. Breakdown: 5,785 production lines (5,781 additions — 26 all-new modules plus four one-line export additions on existing files — and 4 replaced declaration lines) vs 5,384 test lines across 26 colocated test files; no generated/schema files. No core-module paths are touched (everything under packages/cli/src/ui/), so the two-tier core gate does not apply. The 1000+-line large-PR advisory applies factually, but the size matches the batch cut approved in #8662, so no split is requested.

Approach: the shape is right — additive leaf modules, colocated tests, no reachable consumer yet, so the default ink path stays untouched by construction. Both Stage 1 observations from the prior pass are resolved: the tracking table in #8662 now reflects the pulled-forward dialog scaffolding, and the #10134 base landed. The four touches on existing code are each a one-line export addition feeding reuse (redactProxy, toHex, OSC11_TIMEOUT_MS, BARE_URL_BREAK_CHARACTERS) — exactly the direction this migration should go.

Risk: no elevated risk signals (no high-risk path matches).

Moving on to code review. 🔍

中文说明

感谢贡献!本次为改向(retarget)后 diff 的复跑——针对当前 head 重新过门禁。

模板完整 ✓

问题:与上一轮一致——这是 #8662 跟踪的 OpenTUI 迁移中 Foundation modules 批次。动机是已观测到的问题而非理论假设(打补丁的 ink 构建上的闪烁/鼠标/渲染器维护报告:#6137#8580#8659),且维护者已在 #8662 明确批准 Phase 1 分批落地。

方向:对齐且已获背书。上轮之后叠层已解开:#10134 合入,本 PR 经干净合并(零冲突)改回 main,本次评审直接对 main 上的最终 diff 作判断。

规模:54 个文件,+11,165 / −4。拆分:5,785 行生产代码(5,781 行新增——26 个全新模块加四个既有文件上各一行的 export——外加 4 行被替换的声明行)对 5,384 行测试(26 个就近测试文件);无生成/schema 文件。未触及核心模块路径(全部位于 packages/cli/src/ui/ 下),两层核心门禁不适用。1000+ 行大 PR 提示在事实上成立,但规模与 #8662 批准的批次切分一致,故不要求拆分。

方案:形态正确——纯增量叶子模块、就近测试、尚无任何可达消费者,默认 ink 路径在构造上不受影响。上轮的两点观察均已解决:#8662 跟踪表已反映提前落地的 dialog 脚手架;#10134 base 已合入。对既有代码的四处改动均为各一行的 export 追加以服务复用(redactProxytoHexOSC11_TIMEOUT_MSBARE_URL_BREAK_CHARACTERS)——正是这场迁移该有的方向。

风险:无升级风险信号(未命中高风险路径)。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at e0c4f14d628ee74a38f342ae9fe89b8d6526a6cf · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Code review — re-run at e0c4f14d

Read the full 11.2k-line diff against main. The two blockers from my prior pass are both fixed and verified in source:

  • Slash parser: slash-command-parse.ts (+ its 465-line test) is gone; slash-dispatch.ts now imports parseSlashCommand / parseStackedSlashCommands from the original ../commands/commands.js, and its doc comment matches reality.
  • Dialog scope utils: dialog-scope-utils.ts is gone; dialogs-theme.tsx imports getScopeItems / getScopeMessageForSetting from ../../config/dialogScopeUtils.js — the same module ink's ThemeDialog uses.

The hardening round since then (d09231cb11, per the maintainer's four findings) is also in place and coherent: the kitty reply regex now requires a flags parameter so an echoed bare query can't fake kitty support, the probe keeps a bounded 256-byte tail instead of an unbounded re-scanned buffer, the no-op late-reply drain is gone, and the screen-reader writer enforces its plain-text contract by sanitizing (ANSI-strip + C0 drop) inside appendStatic/updateDynamic.

My own pass over every production module found no new Criticals. What stands out: the security-sensitive surfaces keep ink's constraints intact rather than approximating them (OSC 8 scheme allowlist + label-deception suffix reused from utils/osc8.js, unsafe-scheme refusal in link hit-testing, TTY gate + 75KB cap on OSC 52, proxy-credential redaction reused via the new redactProxy export), and the reuse direction is consistently right — keyMatchers, commandMetadata, input-mouse (re-exported, not forked), the exit-cleanup chain, sanitizeSensitiveText, cost/stats helpers all come from the originals. Tests are substantive (496 cases, no skips, asserting observable behavior — including the R1/R2 fix pins like UTF-16 cell mapping for non-BMP cells and the smuggled-OSC-52 writer test). The batch remains inert by construction: nothing outside opentui/ imports it, and the four existing-file touches are pure export additions.

Non-blocking, recorded for the backend batch that wires these modules:

  • Quit payload contract (also raised by the independent static review): the quit effect drops QuitActionReturn.messages — the /quit echo and session-duration summary ink renders. Either carry the projected text on the effect (the existing notice pattern fits) or make backend re-synthesis explicit before wiring.
  • A handful of parity gaps worth folding in then: no retry-countdown-clear on the error/finished branches, /context detail and /mcp schema projections drop the detail/schema sections ink renders, SkillsList name truncation is off by one column, model_fallback model names skip sanitizeDisplayText, and singleColumn in the help layout is dead (the ≥72 clamp makes it always false).
  • Hygiene only: dialogs-shared.tsx / dialogs-theme.tsx are the first kebab-case .tsx files in packages/cli/src/ui (AGENTS.md spells component files PascalCase.tsx). Nothing enforces it for .tsx and the directory is internally consistent, so not a blocker — just noting it.

Test evidence — the PR's own CI

CI ran on this head; quoting real checks. Everything the PR claims is substantiated except one red check that this PR did not cause: Test (ubuntu-latest, Node 22.x) failed on exactly one test — web-shell's MessageList.dom.test.tsx > drops the anchor instead of re-expanding when the user collapsed the anchored turn (AssertionError: expected "spy" to be called 2 times, but got 1 times). That is a timing-sensitive DOM assertion in a package this PR doesn't touch at all: zero web-shell files in the diff, web-shell has no dependency on packages/cli, and the test file is unchanged from the base. Classified as a pre-existing flake, not a PR defect. Everything else in the job passed — all linters (ESLint incl. the architecture rule, Prettier, actionlint, shellcheck, yamllint, sensitive keywords), lockfile/dep-direction checks, build, and the full suites: core 25,913 passed, cli 22,168 passed (including the batch's 26 opentui files). Windows/macOS test lanes are skipped by design (their PR trigger is off per ci.yml pending standing Windows failures); integration tests are not in the PR lane. web-shell E2E Smoke was still running at review time; the table below is a snapshot the finalize job will refresh once CI settles. A maintainer re-run of the failed job (or any re-trigger) settles the flake.

Final CI results for e0c4f14 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Classify PR ✅ success
Dependency CVE audit ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Secret scan (TruffleHog) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification lanes: nothing to exercise yet — the batch has no reachable consumer, so /tmux has no TUI surface to drive, and the suite/build claims are now settled by the PR's own CI above rather than author self-report. When the backend batch wires these modules, /verify becomes the lane that matters.

中文说明

代码审查(复跑于 e0c4f14d):上一轮的两个阻塞项均已修复并在源码中核验——slash-command-parse.ts(及其 465 行测试)已删除,slash-dispatch.ts 改为从原模块 ../commands/commands.js 导入 parseSlashCommand/parseStackedSlashCommands,注释与事实一致;dialog-scope-utils.ts 已删除,dialogs-theme.tsx 改为从 ../../config/dialogScopeUtils.js 导入(与 ink ThemeDialog 同源)。此后按维护者四条意见的加固(d09231cb11)也已就位且自洽:kitty 应答正则要求 flags 参数(回显的裸查询无法伪造 kitty 支持)、探针只保留 256 字节有界尾部缓冲、no-op 的迟到应答 drain 已移除、读屏 writer 在 appendStatic/updateDynamic 内部强制纯文本契约(剥离 ANSI + 去除 C0)。

本人对全部生产模块的复查未发现新的 Critical。亮点:安全敏感面完整保留而非近似 ink 的约束(OSC 8 scheme 白名单与标签欺骗后缀直接复用 utils/osc8.js、链接命中拒绝不安全 scheme、OSC 52 有 TTY 门控与 75KB 上限、代理凭据脱敏经新增 redactProxy 导出复用),复用方向一致正确——keyMatcherscommandMetadatainput-mouse(re-export 而非 fork)、退出清理链、sanitizeSensitiveText、成本/统计工具均取自原模块。测试扎实(496 个用例、无 skip、断言可观察行为——含 R1/R2 修复锚点如非 BMP 字符的 UTF-16 单元映射、读屏 writer 的 OSC 52 注入测试)。批次在构造上仍然不可达:opentui/ 之外无任何代码 import 它,四处既有文件改动均为纯 export 追加。

非阻塞、留给接线批次的记录:

  • quit 载荷契约(独立静态复审同样提出):quit 效果丢弃了 QuitActionReturn.messages(ink 渲染的 /quit 回显与会话时长摘要)。接线前要么让效果携带投影文本(可复用现有 notice 模式),要么明确由 backend 重新合成。
  • 若干 parity 缺口届时一并合入:error/finished 分支未发 retry-countdown-clear/context detail/mcp schema 投影丢失 ink 会渲染的明细/schema 段、SkillsList 名字截断差一列、model_fallback 的模型名未过 sanitizeDisplayText、help 布局的 singleColumn 是死代码(≥72 钳制后恒为 false)。
  • 仅卫生问题:dialogs-shared.tsx/dialogs-theme.tsxpackages/cli/src/ui 下首批 kebab-case .tsx(AGENTS.md 约定组件文件为 PascalCase.tsx)。.tsx 并无强制规则、目录内部自洽,不构成阻塞——仅记录。

测试证据:CI 已在该 head 上运行。PR 的主张均被证实,除一项与本 PR 无关的红检查Test (ubuntu-latest) 恰好失败于一个测试——web-shell 的 MessageList.dom.test.tsx(时序敏感的 DOM 断言)。该包与本 PR 完全无关:diff 中零 web-shell 文件、web-shell 不依赖 packages/cli、该测试文件相对 base 未改动。判定为既有 flake 而非 PR 缺陷。同 job 其余全部通过——所有 linter(含架构规则的 ESLint、Prettier、actionlint、shellcheck、yamllint、敏感词)、lockfile/依赖方向检查、构建,以及完整套件:core 25,913 通过、cli 22,168 通过(含本批 26 个 opentui 文件)。Windows/macOS 测试通道按 ci.yml 设计跳过(PR 触发器因既有 Windows 问题关闭);集成测试不在 PR 通道。评审时 web-shell E2E Smoke 仍在运行;下表为快照,CI 落定后 finalize 任务会刷新。维护者重跑失败任务即可排除该 flake。

沙盒验证通道:目前无用户可见面——批次尚无可达消费者,/tmux 无可驱动的 TUI 面,测试/构建主张已由上述 CI 证实(不再是作者自述)。待 backend 批次接线后,/verify 才是关键通道。

Qwen Code · qwen3.8-max

Reviewed at e0c4f14d628ee74a38f342ae9fe89b8d6526a6cf · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean across every stage; the only reservations are non-blocking follow-ups for the wiring batch and one pre-existing CI flake that is not this PR's.

Stepping back: the prior pass scored this 2/5 over two verbatim forks of modules the PR could import, plus zero CI evidence on that head. Both are resolved. The forks are gone — the batch now dispatches through the original parseSlashCommand and builds the theme dialog on the original dialogScopeUtils, which is exactly the shape my independent proposal for this batch would have taken: additive leaf modules that import ink originals wherever the dependency direction allows, and only re-derive what is genuinely renderer-specific. Since then the PR also absorbed two bot review rounds (35 findings), an independent full-tree static pass (zero new Criticals), and the maintainer's four hardening findings — all verified fixed at this head, and the maintainer has approved it.

What I still carry as reservations, none blocking a foundation batch with no reachable consumer:

  • The quit payload contract (the /quit echo and session summary currently dropped by the bare quit effect) and the small parity gaps listed in my Stage 2 note should be pinned down before the backend batch wires these effects — recorded there, not re-litigated here.
  • CI on this head is red, but not because of this PR. The single failing test is a timing flake in web-shell's MessageList.dom.test.tsx — a package with zero files in this diff and no dependency on packages/cli, the test file unchanged from base. Everything this PR actually owns is green: all linters, the build, and the full core/cli suites including the batch's 26 test files. A maintainer re-run of the failed job settles it.

Verdict: approve. One pull_request CI run is still in flight on this head, so approval is deferred until CI lands green on the reviewed commit rather than posted now.

(One note for the thread: a bot /review run the author requested is also in flight on this head; if it raises anything Critical it will post as a request-changes review and supersedes this signal.)

中文说明

置信度:4/5 —— 各阶段均干净;仅有的保留是留给接线批次的非阻塞跟进项,以及一项与本 PR 无关的既有 CI flake。

整体看:上一轮因两处"本可直接 import 却逐字 fork"以及该 head 上零 CI 证据而给 2/5,两者都已解决。fork 已删净——批次现在走原 parseSlashCommand 分发、主题对话框基于原 dialogScopeUtils 构建,这正是我独立方案的形态:纯增量叶子模块,依赖方向允许处一律 import ink 原模块,只在确属渲染器特有处重新实现。此后 PR 还吸收了两轮 bot 审查(35 条)、一次独立全树静态复审(零新 Critical)以及维护者的四条加固意见——均已在该 head 上核验修复,维护者已 approve。

仍然保留的顾虑(均不阻塞一个尚无可达消费者的 foundation 批次):

  • quit 载荷契约/quit 回显与会话摘要目前被裸 quit 效果丢弃)以及 Stage 2 列出的少量 parity 缺口,应在 backend 批次接线前钉死——已记录在 Stage 2,不再赘述。
  • 该 head 的 CI 是红的,但责任不在本 PR。 唯一失败的测试是 web-shell MessageList.dom.test.tsx 的时序 flake——该包在本 diff 中零文件、不依赖 packages/cli、测试文件相对 base 未改动。本 PR 真正拥有的一切都是绿的:全部 linter、构建、含本批 26 个测试文件在内的完整 core/cli 套件。维护者重跑失败任务即可排除。

结论:批准。该 head 上还有一个 pull_request CI 运行未完成,因此批准暂缓至 CI 在受审提交上全绿后由 finalize 落章,而非现在直接提交。

(线程说明:作者请求的一次 bot /review 也在该 head 上运行中;若其提出任何 Critical,将以 request-changes 形式发出并优先于本信号。)

Qwen Code · qwen3.8-max

Reviewed at e0c4f14d628ee74a38f342ae9fe89b8d6526a6cf · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two of the new modules are verbatim copies of existing, importable code — slash-command-parse.ts copies ui/commands/commands.ts and dialog-scope-utils.ts copies config/dialogScopeUtils.ts (~736 lines including the duplicated tests). For a parity migration the forked parser is a silent-drift hazard; both originals are already reachable from opentui (this PR's own imports prove it). Please delete the copies and import the originals. Also note: no CI run exists on this head yet, so the green-suite claim is unverified, and the base (#10134) still carries CHANGES_REQUESTED. Details in my stage-2/stage-3 comments. 🙏

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

@chiga0

chiga0 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Both Stage-1 observations addressed:

  1. Dialog scaffolding pulled forward — intentional: the later "Dialogs & commands" batch builds its dialogs on this scaffolding (frame, tab bar, selection-list machine), so it lands with its first consumer (the theme dialog) instead of after it. The tracking table in Migrate TUI rendering layer from ink to OpenTUI (tracking) #8662 now reflects the re-cut — the Foundation-modules row carries the pulled-forward items with a note pointing here, matching the format of the other scoping records.
  2. Stacked on feat(cli): OpenTUI migration infra batch — locked deps, dependency-direction arch check, framework-neutral streaming model #10134 — acknowledged; this PR retargets to main after the infra batch merges, and the infra batch's review round (now addressed in feat(cli): OpenTUI migration infra batch — locked deps, dependency-direction arch check, framework-neutral streaming model #10134 @ 3d37d90) has been re-based against here as needed.

@chiga0

chiga0 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Both duplication findings fixed in a8e3a50 — as the Stage-3 ask summarizes:

  • slash-command-parse.ts (+its 465-line test) deleted; slash-dispatch.ts now imports parseSlashCommand from ../commands/commands.js — the same-layer edge the review identified (the original imports only the leaf ./types.js, and this PR already reaches ../commands/types.js from four files). The doc comment claiming resolution "through the shared parseSlashCommand" is now literally true.
  • dialog-scope-utils.ts deleted; dialogs-theme.tsx now imports getScopeItems / getScopeMessageForSetting from ../../config/dialogScopeUtils.js — the identical relative path ink's ThemeDialog uses, and the same direction commands-context.ts already takes into ../../config/.

Net: −738 lines (736 duplicated + 2 import rewires), one source of truth for both. The originals keep their existing coverage, so no parity test was lost — the duplicated suites went with the copies. The two non-blocking notes (dialog re-cut, CommandContext stub fold-in) are recorded: the re-cut is now reflected in #8662, and the stub fold-in stays planned for the backend batch.

Local verification at a8e3a50: npm run build ✅, npm run typecheck ✅, ESLint + Prettier clean ✅, ui/opentui + ui/commands + config/dialogScopeUtils suites 1433/1433 ✅. CI on this head follows (maintainer approval if the fork workflows require it).

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not explored to full depth (tool budget reached): chunk 26: none — parity of mouse-caret.ts against the original utils/input-mouse.ts#visualClickToOffset it claims to port was not diffed line-by-line, but that source….

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Test Plan (not a blocker): 349 tests pass — this review observed 25097 passed; 99 tests pass — this review observed 25097 passed.

中文说明

仅完成部分审查,审查缺口已披露。

未探索到全部深度(达到工具调用预算):chunk 26:none — parity of mouse-caret.ts against the original utils/input-mouse.ts#visualClickToOffset it claims to port was not diffed line-by-line, but that source…

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

Test Plan(非阻断):349 tests pass — this review observed 25097 passed; 99 tests pass — this review observed 25097 passed

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/cli/src/ui/opentui/a11y-plain-text.ts Outdated
Comment thread packages/cli/src/ui/opentui/a11y-plain-text.ts Outdated
Comment thread packages/cli/src/ui/opentui/dialogs-theme.tsx
Comment thread packages/cli/src/ui/opentui/dialogs-theme.tsx
Comment thread packages/cli/src/ui/opentui/early-input.ts Outdated
Comment thread packages/cli/src/ui/opentui/event-adapter.ts
Comment thread packages/cli/src/ui/opentui/event-adapter.ts
Comment thread packages/cli/src/ui/opentui/item-projection.ts
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts Outdated
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts Outdated
@chiga0

chiga0 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

R1 review round: fixes pushed (54edee0)

All 19 Critical findings and the low-cost Suggestions are addressed in 54edee0cb4 on top of this branch; each finding has a per-thread reply with the concrete fix.

Highlights

  • a11y: CommonMark-correct fence tracking (per-character), blockquote-aware block passes, code spans extracted before inline passes (R1-1/47/48)
  • security/hygiene: proxy credential redaction fallback (R1-6), OSC 52 payload cap (R1-19), unescapeMarkdownDollars + nextCharacter parity (R1-9/93)
  • forked copies replaced by originals: copyToClipboard, SLASH_COMMANDS_SKIP_RECORDING, levelLabel, visualClickToOffset, shouldDisplayGoalStateCause, OSC11_TIMEOUT_MS (R1-14 ×5, R1-52)
  • slash dispatch: real /clear effect, no fabricated sessionStartTime, stacked-skill deferral, abort-result drop, comment-style guard, ui.addItem projection (R1-96/100/101/17/18/102)
  • projections: compression + stats history items, savings-tip placement, MCP line spellings, resolvedSettings array shape (R1-7/8/76/86)
  • event adapter: showCitations gate + structured displays (plan_summary, mcp_tool_progress, mcp_app, team_result/task_list) (R1-20/66/68 partial)
  • theme: accent/purple wiring swap, preview heights, list re-sync on scope change (R1-108/3/4/58)
  • input: full ECMA-48 CSI strip incl. truncated tails (R1-5); exit guard per-key windows (R1-71); click mapping per UTF-16 unit (R1-88); license headers (R1-34 ×2); hoisted mocks (R1-27)

Verification: packages/cli opentui suite 25 files / 339 tests green (27 new witness tests), npm run typecheck clean, ESLint + Prettier clean on all touched files.

Deferred (threads left open, replies explain scope)

  • R1-16 — interactive submit_prompt UserPromptExpansion pipeline
  • R1-103 — ink telemetry/recording side-effects in dispatch
  • R1-68 (partial) — findings_list / task_execution structured payloads still hit the stringify fallthrough

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Test Plan (not a blocker): 349 tests pass — this review observed 25127 passed; 99 tests pass — this review observed 25127 passed.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/opentui/a11y-screen-reader.test.ts:244 — [review] The clearDynamic test never re-writes the pre-clear…
  • packages/cli/src/ui/opentui/clipboard.ts:28 — [review] Hand-rolled tmux/screen DCS wrapping duplicates core's…
  • packages/cli/src/ui/opentui/commands-output.ts:31 — [review] Added optional parameter now on messageToHistoryItem is…
  • packages/cli/src/ui/opentui/dialogs-core.ts:166 — [review] applyNumberSelectKey's exact-boundary comparator…
  • packages/cli/src/ui/opentui/dialogs-shared.test.tsx:60 — [review] No test moves the highlight between digit and flush, so…
  • packages/cli/src/ui/opentui/dialogs-shared.test.tsx:61 — [review] The flush test never types after the timeout commit, so…
  • packages/cli/src/ui/opentui/dialogs-shared.tsx:187 — [review] The resync path resets activeIndex/scrollOffset but leaves…
  • packages/cli/src/ui/opentui/dialogs-shared.tsx:227 — [review] selectIndex fires onSelect but never onHighlight,…
  • packages/cli/src/ui/opentui/dialogs-shared.tsx:243 — [review] The focused:false gate — the only protection against two…
  • packages/cli/src/ui/opentui/dialogs-shared.tsx:369 — [review] DialogSelect gates visual selection on focused but not…
  • packages/cli/src/ui/opentui/dialogs-theme.tsx:135 — [review] The Math.max(1,…) pane clamps plus the theme-mode header's…
  • packages/cli/src/ui/opentui/early-input.test.ts:102 — [review] The successful-injection test never asserts polling stops…
  • packages/cli/src/ui/opentui/early-input.test.ts:124 — [review] The 'stops retrying after maxAttempts' test asserts…
  • packages/cli/src/ui/opentui/early-input.ts:48 — [review] The truncated-tail guard covers only CSI (ESC [); a…
  • packages/cli/src/ui/opentui/event-adapter.test.ts:49 — [review] The tool-args and confirm channels have zero assertions —…
  • packages/cli/src/ui/opentui/event-adapter.ts:70 — [review] The confirm event carries only {id, tool, title};…
  • packages/cli/src/ui/opentui/event-adapter.ts:74 — [review] The compaction union member is producer-less and…
  • packages/cli/src/ui/opentui/event-adapter.ts:110 — [review] The goal-legacy union member has no producer and no…
  • packages/cli/src/ui/opentui/event-adapter.ts:330 — [review] The image channel skips ink's guards (non-image MIME, size…
  • packages/cli/src/ui/opentui/event-adapter.ts:435 — [review] The 'Press Ctrl+Y to retry' hint is emitted…
  • …and 37 more (see the run report)
中文说明

仅完成部分审查,审查缺口已披露。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

Test Plan(非阻断):349 tests pass — this review observed 25127 passed; 99 tests pass — this review observed 25127 passed

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 57 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/cli/src/ui/opentui/dialogs-shared.tsx Outdated
Comment thread packages/cli/src/ui/opentui/dialogs-shared.tsx
Comment thread packages/cli/src/ui/opentui/event-adapter.ts Outdated
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts Outdated
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts Outdated
Comment thread packages/cli/src/ui/opentui/exit-guard.ts Outdated
Comment thread packages/cli/src/ui/opentui/item-projection.ts Outdated
Comment thread packages/cli/src/ui/opentui/event-adapter.ts
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts Outdated
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts
chiga0 added 4 commits August 28, 2026 10:50
… dialogs scaffolding

Foundation batch of the OpenTUI migration tracked in QwenLM#8662. Adds the renderer-neutral foundation modules under ui/opentui: theme family, a11y (plain-text, screen-reader), clipboard, key-map, mouse hit/caret, link-click + osc8 parity, early-input, exit guard/lifecycle, kitty negotiation, event-adapter, item-projection, slash dispatch (+ command parsing), commands context/output, help content, input history, and the dialog scaffolding primitives (core/shared) with the theme dialog. Two helpers land inside ui/opentui rather than utils/ to respect the utils leaf-layer rule (QwenLM#9737). Stacked on the infra batch: consumes ui/model streaming model and @OpenTui deps. No reachable ink code changes beyond a one-line export addition in the shared osc8 module.
Upstream renamed setGeminiMdFileCount to setMemoryFileCount in the command
UI contract; the rebase onto main surfaced the mismatch at build. Rename
the host interface member, the bridge wiring, the dispatch stub, and the
test mock to match.
@github-actions

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@chiga0
chiga0 changed the base branch from feat/tui-infra-batch to main August 28, 2026 03:03
@chiga0

chiga0 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main now that the infra batch has merged (#10134b91c281e): the three foundation commits replayed cleanly (no textual conflicts), plus one adaptation commit for the upstream setGeminiMdFileCountsetMemoryFileCount rename in the command UI contract (host interface member, bridge wiring, dispatch stub, test mock).

Verified on the rebased head 8e9a6c7295: full workspace build clean · tsc --noEmit clean across workspaces · lockfile check passes · dependency-direction gate passes · opentui suite 339/339 green. The diff against main is now exactly this batch (52 files, ~10.4k including tests).

All 38 round-1 findings remain addressed at this head; the review threads were anchored to pre-rebase commits and will read as outdated.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

@chiga0

chiga0 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code-ci-bot /review

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent pass at 8e9a6c7, scoped to new ground — not repeating the 28 open threads. Local verification on this head (tarball checkout, deps from the unchanged lockfile): full src/ui/opentui/ suite 339/339 across 25 files, tsc --noEmit clean in packages/cli. Four new findings inline, concentrated in kitty-negotiation.ts (reply regex matches its own query; the late-reply drain is a no-op listener; unbounded buffer re-scan) and the screen-reader writer (plain-text contract not enforced — stripAnsi has zero callers in this batch). The two edits to existing files are export-only and safe; clipboard OSC 52 payloads are base64-wrapped and the OSC 8/link paths reuse the existing scheme allowlist, so those surfaces look fine.

Comment thread packages/cli/src/ui/opentui/kitty-negotiation.ts Outdated
Comment thread packages/cli/src/ui/opentui/kitty-negotiation.ts Outdated
Comment thread packages/cli/src/ui/opentui/kitty-negotiation.ts Outdated
Comment thread packages/cli/src/ui/opentui/a11y-screen-reader.ts Outdated
Round-2 review fixes (17 Critical + 10 Suggestion resolved in code):

- dialogs-shared: move number-select flush out of the setState updater
  (StrictMode double-fires onSelect); split setActiveIndex (ink
  SET_ACTIVE_INDEX, lands on any in-range row) from highlightIndex
  (arrow keys skip disabled rows) so wheel navigation never sticks
- event-adapter: chat_compressed notice mirrors ink formatCount ('~'
  prefix for estimated counts); vision_bridge_notice renders
  summary\nnotice; explicit projections for task_execution /
  findings_list / terminal_image keep multi-MB payloads off the
  transcript; retry-countdown-clear forwards isContinuation
- slash-dispatch: isSlashCommandInput drops the '?' branch (ink gate
  routes ? input to the model); executeSlashCommand races the action
  against the abort signal; dialog effects carry the
  OpenDialogActionReturn payload; projected added-item text surfaces
  alongside non-handled effects (notice); message-shaped items project
  to their text; ui.history comes from env; absent sessionStats stamp
  now, not epoch; telemetry parity (recordSkillInvocation /
  recordAutoSkillCommandUsage / makeSlashCommandEvent)
- item-projection: model stats render per-(model,source) sections with
  N/A for unpriced entries; Tool Calls line uses ASCII x like ink;
  redactProxy deduplicated via systemInfoFields export
- theme: palette/syntax colors resolve through color-utils toHex before
  parseColor (ink CSS names / *bright names no longer degrade to
  magenta); unresolvable values stay unset
- key-map: kitty 'kpenter' normalizes to 'return'; resolveCommands
  exposes ink's key fan-out (Ctrl+C fires QUIT + CLEAR_INPUT)
- a11y: hardWrap delegates to wrap-ansi (word-boundary parity with
  ink's screen-reader path); markdown reducer tracks fence length,
  keeps fence-like lines literal inside fences and inner backticks in
  multi-backtick spans; stripAnsi delegates to strip-ansi plus a
  private-parameter CSI pass (SGR mouse, DEC save/restore)
- clipboard: OSC 52 write gated on a TTY (stderr preferred), tests spy
  the stream instead of writing real sequences to the runner's terminal
- exit-guard: independent per-key arm windows like ink
- dialogs-theme: diff preview pane receives syntaxStyle/filetype
…review

- kitty-negotiation: KITTY_REPLY_RE requires at least one flag digit
  (\d+), so an echoed bare query \x1b[?u in PTY/CI environments no
  longer resolves true and locks the renderer into kitty mode on a
  terminal that never answers queries; the accumulation buffer keeps
  only a 256-byte tail (bounded memory, bounded rescan under byte
  floods); the settle-window drain is removed — an EventEmitter data
  listener cannot consume chunks from other listeners, so late replies
  flow to the renderer's input parser like any other terminal noise
- a11y-screen-reader: ScreenReaderOutputWriter sanitizes written
  content (stripAnsi + drop bare C0/C1 controls, keep newlines) so the
  plain-text-only contract is enforced at the writer instead of
  trusting every future caller — smuggled OSC 52 clipboard writes or
  title/cursor sequences cannot execute on the main screen
@chiga0
chiga0 enabled auto-merge August 28, 2026 09:59
yiliang114
yiliang114 previously approved these changes Aug 28, 2026

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on re-review at e0c4f14. All four of my earlier findings are fixed in d09231c and verified in source:

  • Kitty reply regex now requires at least one flag digit, so an echoed bare query can no longer fake kitty support (echo + split-chunk tests added).
  • The no-op late-reply drain and settle window are removed; the comment now states the actual contract (late replies reach the renderer's input parser).
  • The probe buffer keeps a 256-byte tail — bounded memory and bounded rescans, with a byte-flood test; worst case fails safe toward legacy keys.
  • The screen-reader writer enforces its own plain-text contract via sanitize() (stripAnsi + bare C0/DEL drop) in both appendStatic and updateDynamic, with a smuggled-OSC-52 test.

Also spot-checked the R2 batch (theme colors resolved through toHex before parseColor; kpenter aliased to return) — both match their findings. Local verification on this head: full opentui suite 375/375 across 25 files (up from 339 — the fixes carry their tests), tsc --noEmit clean in packages/cli.

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Independent static review @ head e0c4f14 / 独立静态复审 @ head e0c4f14

Pure static review (no tests executed). Four module batches (a11y + theme / input & terminal-negotiation security / stream-event adapter & history projections / slash dispatch & dialogs) walked all 55 files. The exclusion context was extracted up front from the two bot review rounds (35 findings), the 57 deferred convergence-posture items, and the maintainer's four findings from earlier today — none are re-reported here. The head is a clean merge of main (zero combined hunks), and every prior-round fix was re-verified in the head tree.

纯静态审查(未运行测试),4 个模块批次(a11y+theme / 输入与终端协商安全批 / 流事件适配与历史投影 / slash 分发与 dialogs)走查全部 55 个文件。排除集事先提取自两轮 bot 审查(35 条)、57 条延后项以及今天早些时候维护者的 4 条发现——本文不重复这些条目。head 是干净的 main 合并(零 combined hunks),历轮修复均在 head tree 上逐条复验。

Prior fixes re-verified / 既有修复核验

The forked copies are genuinely gone: slash-dispatch.ts:45 now imports parseSlashCommand from ../commands/commands.js and dialogs-theme.tsx imports from ../../config/dialogScopeUtils.js, with no residue tree-wide. The maintainer's four findings (kitty reply-regex self-match, no-op drain listener, unbounded probe buffer, screen-reader writer sanitization) are all fixed and coherent at this head. The security surfaces were exercised adversarially and hold: clipboard platform fallbacks pipe text via stdin.write with fixed argv (no shell interpolation), OSC 52 payloads are base64-wrapped with the 75KB cap and TTY gate, and the link-click scheme gate rejects javascript:/file:/case-folding/full-width disguises. All 20 stream-event members and all 14 ToolResultDisplay members were checked against core's actual shapes with no silent drops.

fork 副本确实删净:slash-dispatch.ts:45 现从 ../commands/commands.js 导入 parseSlashCommand,dialogs-theme.tsx 从 ../../config/dialogScopeUtils.js 导入,全树无残留。维护者的 4 条(kitty 应答正则自匹配、no-op drain、无界探针缓冲、读屏 writer 净化)均已修复且自洽。安全面经对抗性推演成立:剪贴板平台回退全部经 stdin.write 管道、固定参数数组(无 shell 拼接),OSC 52 载荷 base64 包裹、75KB 上限、TTY 门控,link-click 的 scheme 门禁拒绝 javascript:/file:/大小写伪装/全角伪装。20 个流事件成员与 14 个 ToolResultDisplay 成员均与 core 实际形状核对,无静默丢弃。

New findings (no Criticals) / 新发现(无 Critical)

🟡 Important

  1. slash-dispatch.ts:296-297 — the quit arm returns bare { kind: 'quit' } and drops QuitActionReturn.messages, where quitCommand computes the /quit echo item and the session-duration summary that ink renders via setQuittingMessages. The SlashEffect quit variant carries no payload and exit-lifecycle only runs cleanup, so this output is permanently lost under the new renderer unless the backend is contractually expected to re-synthesize it — nothing in the contract says so. Suggest carrying the projected messages on the effect (the existing notice field pattern fits) or documenting backend responsibility explicitly. Confidence: medium (may be intentionally deferred to the backend batch).

    quit 分支返回裸 { kind: 'quit' },丢弃 QuitActionReturn.messages——quitCommand 在其中计算 /quit 回显条目与会话时长摘要,ink 经 setQuittingMessages 渲染。SlashEffect 的 quit 变体无载荷、exit-lifecycle 只做清理,除非契约约定后端重新合成(当前无任何承载),该输出在新渲染器永久丢失。建议随 effect 携带投影后的文本(可复用 notice 字段模式)或显式注明后端职责。置信度:中(可能是有意留给 backend 批次)。

🟢 Nits

  1. event-adapter.ts — the error (~468-485) and finished (~665-678) branches never emit retry-countdown-clear, while the other four sites do; ink clears unconditionally in handleErrorEvent (use-llm-stream.ts:2110-2111) and in handleFinishedEvent (:2228). A terminal event inside the live countdown window leaves the stale row. Fix: emit the clear in both branches.

    errorfinished 分支不发 retry-countdown-clear,其余四处都发;ink 在 handleErrorEvent(use-llm-stream.ts:2110-2111)无条件清、handleFinishedEvent(:2228)也清。倒计时窗口内到达终态会残留过期 行。两个分支补发 clear 即可。

  2. item-projection.ts:414/462projectContextUsage uses showDetails only to toggle the hint line; ink's ContextUsage (views/ContextUsage.tsx:472) renders the per-item detail sections (built-in/MCP tools, memory files, skills) plus the compaction-thresholds block. /context detail transcripts end up with strictly less information. Fix: project the detail arrays and breakdown.thresholds.

    projectContextUsage 只用 showDetails 切换提示语;ink ContextUsage 在该标志下渲染逐条明细(内置/MCP 工具、记忆文件、技能)与压缩阈值块。/context detail 的转录反而信息更少。建议投影明细数组与 breakdown.thresholds

  3. item-projection.ts (projectMcpStatus) — ignores the showSchema/showTips fields carried by HistoryItemMcpStatus (ui/types.ts:439); ink's McpStatus.tsx:191 prints parameter schemas and tips. /mcp schema transcripts are indistinguishable from /mcp.

    projectMcpStatus 忽略 HistoryItemMcpStatus(ui/types.ts:439)携带的 showSchema/showTips;ink McpStatus.tsx:191 会打印参数 schema 与提示。/mcp schema 转录与 /mcp 无差别。

  4. slash-dispatch.ts:428settings: env.settings ?? null contradicts the real CommandContext.services.settings type (non-null LoadedSettings, commands/types.ts:63) and is bridged with as unknown as. If the backend omits settings, commands reading .merged throw a TypeError surfaced as a generic command failure. Suggest making env.settings required.

    settings: env.settings ?? null 与真实类型矛盾(CommandContext.services.settings 为非空 LoadedSettings,commands/types.ts:63),靠 as unknown as 绕过。后端漏传时读 .merged 的命令抛 TypeError,表现为泛化失败。建议 env.settings 设为必填。

  5. help-content.ts:185singleColumn is dead: safeWidth is clamped to ≥72, so descWidth < 4 is always false (the test itself asserts computeHelpWidthLayout(1).singleColumn === false), and ink's original has no single-column mode. Drop the field or evaluate it before the clamp.

    singleColumn 永假:safeWidth 钳到 ≥72 后 descWidth < 4 恒为 false(测试自身断言 computeHelpWidthLayout(1).singleColumn === false),且 ink 原版无单列模式。删除该字段或在钳制前判断。

  6. early-input.ts:45 — truncated SS3 leaks a bare O: the SS3 strip requires the final byte and the truncated-tail fallback covers only CSI (ESC [). If startup capture stops at the second byte of a 3-byte F1-F4, one O lands in the composer. Add a /\u001BO$/ fallback symmetric with line 48.

    截尾 SS3 泄漏单个 O:SS3 剥离要求 final 字节,截尾兜底只覆盖 CSI(ESC [)。启动捕获恰在 F1-F4(3 字节)第二字节停止时,composer 会多出一个 O。补一条与 48 行对称的 /\u001BO$/

  7. link-click.ts:135/174readBufferRow trims trailing whitespace from text but not from cellColumns, so a URL ending at end-of-row with a full-width final character misses clicks on the right half of that cell (fallback bound is lastColumn + 1, covering only the left half). UX-only; keep the two arrays length-aligned or record per-character next-column.

    readBufferRow 裁掉 text 尾部空白但 cellColumns 不同步,行尾以全角字符结尾的 URL 点击其右半格不命中(回退边界 lastColumn + 1 只覆盖左半)。纯 UX;保持两数组等长或记录每字符的下一列起点。

  8. kitty-negotiation.ts:134-136 (Confidence: low) — the two probe stdout.write calls are unguarded; if one throws synchronously (e.g. ERR_STREAM_DESTROYED while isTTY still reports true), finish() never runs: raw mode is not restored and the data listener leaks. Wrap the writes in try/catch calling finish(false).

    探针的两次 stdout.write 无保护;若同步抛错(如 isTTY 仍为 true 时 ERR_STREAM_DESTROYED),finish() 永不执行:raw 模式不恢复、监听器泄漏。用 try/catch 包裹并走 finish(false)

💡 Suggestions

  1. a11y-screen-reader.ts:137-150 — the hand-rolled eraseLines is byte-identical to ansi-escapes' eraseLines, which the repo already imports elsewhere (ui/utils/terminal-resize-reflow.ts:299). Reuse it.

    手写 eraseLinesansi-escapes 同名助手字节级等价,仓库已在 ui/utils/terminal-resize-reflow.ts:299 引入该库,可直接复用。

  2. help-content.ts:138truncateText is a zero-value passthrough wrapper of the exported truncateHelpText; call the original directly.

    truncateText 是对已导出 truncateHelpText 的零价值透传包装,调用点直接用原函数。

  3. item-projection.ts:179 — the SkillsList truncate is off by one (projection total length n+1 vs ink's n via slice(0, max-1) + '…'), shifting the description column by one cell whenever a name truncates.

    SkillsList 截断差一字符(投影总长 n+1,ink 为 slice(0, max-1) + '…' 总长 n),名字截断时描述列整体右移一格。

  4. event-adapter.ts:601model_fallback model names skip ink's sanitizeDisplayText (use-llm-stream.ts:2800-2801); low injection probability, but it is a sanitization gap vs the claimed parity.

    model_fallback 的模型名未过 ink 的 sanitizeDisplayText;注入概率低,但与声明的 parity 相比是消毒缺口。

  5. dialogs-shared.tsx:242selectIndex (mouse click) fires onSelect without onHighlight; ink dispatches SET_ACTIVE_INDEX + SELECT_CURRENT so the highlight callback precedes select. Harmless for the current close-on-select consumer; a future stay-open dialog driven by highlight would desync on mouse input.

    selectIndex(鼠标点击)只触发 onSelect 不触发 onHighlight;ink 依次 dispatch SET_ACTIVE_INDEX + SELECT_CURRENT,highlight 回调先于 select。当前"选中即关"消费者无影响,后续依赖 highlight 驱动预览且保持打开的对话框会在鼠标输入下失同步。

  6. Two low-confidence edges: event-adapter.ts mcp_app falls through to JSON.stringify if fallbackText is ever absent (currently unreachable; contradicts the "HTML never enters output" comment), and item-projection.ts projectAbout shows the Base URL line when selectedAuthType is the empty string where ink hides it.

    两条低置信边界:mcp_appfallbackText 缺省时落入 JSON.stringify(当前不可达,但与"HTML 绝不进输出"注释矛盾);projectAboutselectedAuthType 为空串时多打一行 Base URL(ink 隐藏)。

Positives / 值得肯定

The fix round is genuinely high quality: numeric quick-select now runs onSelect from a ref outside setState updaters with a StrictMode single-fire test, setActiveIndex/highlightIndex are correctly split, the abort race cleans up without dangling rejections, and telemetry ordering on throw matches ink's catch path. Every module documents the ink behavior it reproduces, and the tests assert observable behavior rather than mirroring implementations.

修复轮质量很高:数字快选经 ref 在 setState updater 之外执行 onSelect 并配 StrictMode 单次触发测试,setActiveIndex/highlightIndex 拆分正确,abort race 清理无悬挂 rejection,抛错路径的遥测顺序与 ink catch 一致。每个模块都注明其复刻的 ink 行为,测试断言可观察行为而非镜像实现。

CI

Test (ubuntu) is still in progress at this head; everything else completed is green or skipped.

head 上 Test (ubuntu) 仍在运行,其余已完成项为绿或跳过。

Verdict / 结论

💬 Comment — zero new Criticals from this pass. The one Important (quit payload contract) and the parity gaps are worth pinning down before the backend batch wires these effects, but they do not block a foundation batch that has no reachable consumer yet.

💬 评论——本轮零新增 Critical。1 条 Important(quit 载荷契约)与若干 parity 缺口值得在 backend 批次接线前钉死,但不阻塞这个尚无可达消费者的 foundation 批次。

chiga0 added 2 commits August 28, 2026 19:18
…ation

All 15 findings from the independent static review verified in source and
fixed (no false positives; none deferred):

- quit effect carries QuitActionReturn.messages projected to text on a
  notice field — ink renders them via QuittingDisplay and the payload was
  permanently lost (Important #1)
- error and finished branches emit retry-countdown-clear like ink's
  handleErrorEvent/handleFinishedEvent, so a terminal event inside the
  countdown window no longer leaves a stale retry row (#2)
- projectContextUsage renders the compaction-threshold ladder and the
  per-item detail sections (tools/memory/skills, ink's sort order) when
  showDetails is on — /context detail transcripts no longer show strictly
  less than the compact view (#3)
- projectMcpStatus honors showSchema (parameter JSON under each tool) and
  showTips, so /mcp schema is distinguishable from /mcp (QwenLM#4)
- SlashDispatchEnv.settings is required: the real
  CommandContext.services.settings is non-null and a null surfaced as a
  generic command failure on first .merged read (QwenLM#5)
- dead singleColumn flag removed from the help width layout (clamp makes
  it always false; ink has no single-column mode) (QwenLM#6)
- truncated SS3 tail (bare ESC O) is stripped like the truncated CSI
  tail, so a captured half F1-F4 no longer leaks 'O' into the composer (QwenLM#7)
- readBufferRow trims cellColumns alongside text, so URLs ending at
  end-of-row on a wide character hit-test on both halves of the cell (QwenLM#8)
- kitty probe writes guarded: a synchronous stream throw settles the
  probe (restores raw mode, removes the listener) instead of leaking (QwenLM#9)
- eraseLines reuses the ansi-escapes helper (already a repo dependency)
  instead of a byte-identical hand-rolled copy (QwenLM#10)
- truncateText passthrough wrapper dropped; callers use the exported
  truncateHelpText directly (QwenLM#11)
- SkillsList truncate keeps total length n like ink, so the description
  column no longer shifts by one cell when a name truncates (QwenLM#12)
- model_fallback names pass through sanitizeDisplayText like ink (QwenLM#13)
- selectIndex fires onHighlight before onSelect (ink dispatches
  SET_ACTIVE_INDEX then SELECT_CURRENT), keeping highlight-driven
  stay-open dialogs synced on mouse input (QwenLM#14)
- mcp_app without fallbackText renders empty instead of JSON-dumping the
  embedded HTML; projectAbout hides Base URL when selectedAuthType is
  empty, matching ink's formatBaseUrl (QwenLM#15)
@chiga0

chiga0 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Thank you @yiliang114 for the approval and the verified re-review, and @ytahdn for the independent static pass — all 15 findings were checked against the source (every one held; none deferred) and fixed in ff19f4f, pushed on top of the head you reviewed:

378/378 opentui tests, typecheck, and lint are green on the pushed head; regression tests added for the quit notice, both countdown clears, the context detail sections, and the mcp schema/tips paths.

@chiga0

chiga0 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up @yiliang114: pushing the review-response fixes (ff19f4f, merged into ac1278f) auto-dismissed your approval, so the PR is back to review-required. The head diff since your approval is exactly the 15 ytahdn-finding fixes described in my comment above — no other changes. Full CI is green on ac1278f (Test/Desktop Shell/web-shell E2E/CVE/secret scan). Could you re-approve when convenient?

yiliang114
yiliang114 previously approved these changes Aug 28, 2026

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approved at ac1278f after the ytahdn-review delta (ff19f4f). Reviewed every changed module in the delta: the eraseLines swap to the shared ansi-escapes helper, the mouse-select highlight ordering fix, the ESC-O partial-tail strip in early-input (also closes the residual I noted in my first pass), the event-adapter parity fixes (mcp_app empty-fallback no longer dumps raw HTML, retry-countdown clears on error/finished, sanitized fallback model names), the help-content cleanup, the item-projection fixes plus the compaction-ladder/detail/schema projections with Number() coercion, the kitty probe's fail-settled write guard, the wide-char cellColumns trim in link-click, and the slash-dispatch settings-required + quit-notice projection changes. Local verification on this head: full opentui suite 378/378 across 25 files (up from 375), tsc --noEmit clean in packages/cli. Prior findings remain fixed; no new issues in the delta.

@chiga0
chiga0 requested a review from ytahdn August 28, 2026 13:41

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This run could not certify that any of this diff was reviewed. Suggestions are inline.

Not reviewed: the executable-script lint — the report is stale or its diff could not be verified; re-run qwen review script-lint.

Not reviewed: 35 of the diff's 36 sections, the linked-issue fidelity pass, the whole-diff test-coverage check, the removed-behavior audit, the cross-file consistency pass, the build-and-test check — its prompt was built, but no agent on record was launched with it.

Not reviewed: 1 of the diff's 36 sections — no record shows its brief reaching an agent, so this dimension was reviewed, if at all, from a prompt the run wrote for itself.

Not reviewed: verification and reverse audit — both prompts were built, but no agent was launched with either — the posted findings cannot be counted as verified, and the pass that hunts what the rest of the review missed cannot be certified.

Convergence: round 3 posted 5 inline comment(s), 5 of them reported for the first time; the previous round posted 28 (24 new). Findings keep coming back to the same files: packages/cli/src/ui/opentui/slash-dispatch.ts (findings in rounds 1, 2; 2 more now); packages/cli/src/ui/opentui/a11y-plain-text.ts (findings in round 2; 1 more now); packages/cli/src/ui/opentui/a11y-screen-reader.ts (findings in round 2; 1 more now), and 1 more file(s). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

⚠️ 本次运行无法证明这个 diff 的任何部分经过了审查。 建议见行内评论。

未审查:the executable-script lint — the report is stale or its diff could not be verified; re-run qwen review script-lint

未审查:diff 36 个片段中的 35 个、关联 issue 一致性检查、全 diff 测试覆盖检查、删除行为审计、跨文件一致性检查、构建与测试验证——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。

未审查:diff 36 个片段中的 1 个——没有记录表明它的 brief 到达过任何 agent,这个维度即便被审查过,也是基于这次 run 自行编写的 prompt。

未审查:验证与反向审计——两份 prompt 都已构建,但都没有 agent 用它们启动——发布的发现不能算作已验证,搜寻评审遗漏问题的工序也无法作证。

收敛情况:第 3 轮发布了 5 条行内评论,其中 5 条是首次提出;上一轮发布了 28 条(其中 24 条首次提出)。发现反复回到同一批文件:packages/cli/src/ui/opentui/slash-dispatch.ts(第 1、2 轮已出过发现,本轮又有 2 条);packages/cli/src/ui/opentui/a11y-plain-text.ts(第 2 轮已出过发现,本轮又有 1 条);packages/cli/src/ui/opentui/a11y-screen-reader.ts(第 2 轮已出过发现,本轮又有 1 条),另有 1 个文件。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts
Comment thread packages/cli/src/ui/opentui/slash-dispatch.ts
Comment thread packages/cli/src/ui/opentui/a11y-plain-text.ts Outdated
Comment thread packages/cli/src/ui/opentui/item-projection.ts Outdated
Comment thread packages/cli/src/ui/opentui/a11y-screen-reader.ts Outdated
- executeSlashCommand catch checks the abort signal first: an ESC-
  cancelled command (action rejects AbortError) returns handled with no
  failure telemetry or error message, mirroring ink's processor — the
  race promise never resolves when the signal is already aborted at
  addEventListener time
- submit effect carries the full SubmitPromptActionReturn contract
  (modelOverride, onComplete, refreshContextFilesOnWrite) so the
  backend can honor /model <id> <prompt>, /dream's manual-run record,
  and /remember's context refresh like ink instead of silently
  degrading them
- closing fences cannot carry info text (CommonMark): a ```js line
  inside an open block is literal body, not an early close that drops
  the block and inverts parse state for the rest of the document
- info items append their linkUrl/linkText footer (ink's InfoMessage
  renders it; headless/SSH users need the printed URL, e.g. /bug)
- the screen-reader sanitize keeps TAB: it separates words in
  tool/model output and deleting it fused adjacent tokens

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): chunk 20: executing packages/cli/src/ui/opentui/item-projection.test.ts (no installed dependencies in the shared review worktree; installing/building the monorepo excee…; "agent reverse-audit (round 2)": did not locate core's AgentEvent→ServerGeminiStreamEvent bridge to verify whether live per-tool progress updates (subagent task_execution 'running' displays, …; chunk 9: verifying e.scroll?.direction , MouseButton.LEFT , and the onMouseScroll / onMouseOver / onMouseUp prop typings against the installed @opentui/core@0.5.8 …; chunk 8: could not execute dialogs-shared.test.tsx (no node_modules in the review worktree); the numeric-flush finding was verified by reading the code paths at HEAD…; chunk 31: execute slash-dispatch.test.ts under vitest to confirm green (blocked: worktree lacks node_modules and built dist; install+build exceeds tool budget), and 1 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

Test Plan (not a blocker): 349 tests pass — this review observed 25927 passed; 99 tests pass — this review observed 25927 passed.

Convergence: round 4 posted 71 inline comment(s), 71 of them reported for the first time; the previous round posted 5 (5 new). Findings keep coming back to the same files: packages/cli/src/ui/opentui/item-projection.ts (findings in round 3; 13 more now); packages/cli/src/ui/opentui/slash-dispatch.ts (findings in round 3; 10 more now); packages/cli/src/ui/opentui/a11y-plain-text.ts (findings in round 3; 4 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):chunk 20:executing packages/cli/src/ui/opentui/item-projection.test.ts (no installed dependencies in the shared review worktree; installing/building the monorepo excee…"agent reverse-audit (round 2)"did not locate core's AgentEvent→ServerGeminiStreamEvent bridge to verify whether live per-tool progress updates (subagent task_execution 'running' displays, …;chunk 9:verifying e.scroll?.direction , MouseButton.LEFT , and the onMouseScroll / onMouseOver / onMouseUp prop typings against the installed @opentui/core@0.5.8 …;chunk 8:could not execute dialogs-shared.test.tsx (no node_modules in the review worktree); the numeric-flush finding was verified by reading the code paths at HEAD…;chunk 31:execute slash-dispatch.test.ts under vitest to confirm green (blocked: worktree lacks node_modules and built dist; install+build exceeds tool budget),另有 1 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

Test Plan(非阻断):349 tests pass — this review observed 25927 passed; 99 tests pass — this review observed 25927 passed

收敛情况:第 4 轮发布了 71 条行内评论,其中 71 条是首次提出;上一轮发布了 5 条(其中 5 条首次提出)。发现反复回到同一批文件:packages/cli/src/ui/opentui/item-projection.ts(第 3 轮已出过发现,本轮又有 13 条);packages/cli/src/ui/opentui/slash-dispatch.ts(第 3 轮已出过发现,本轮又有 10 条);packages/cli/src/ui/opentui/a11y-plain-text.ts(第 3 轮已出过发现,本轮又有 4 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment thread packages/cli/src/ui/opentui/a11y-plain-text.ts
Comment thread packages/cli/src/ui/opentui/dialogs-shared.tsx
Comment thread packages/cli/src/ui/opentui/event-adapter.ts
Comment thread packages/cli/src/ui/opentui/item-projection.ts
Comment thread packages/cli/src/ui/opentui/key-map.ts
Comment on lines +482 to +483
loadHistory: () => {},
refreshStatic: () => {},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Commands that rebuild history imperatively via ui.loadHistory are silently swallowed by the no-op stub with no deferral notice — unlike the equivalent load_history result type, which gets an explicit 'not yet available' message.

User with collapsed resume history runs /history expand-now: expandNowCommand detects suppressed items, computes the expanded list, calls context.ui.loadHistory(updated) — the stub drops it — and returns void, so executeSlashCommand yields {kind:'handled'}. In ink the transcript expands; through this dispatcher nothing happens and nothing tells the user the feature is unported, breaking the module's own deferral-message convention (the same stub is why /restore's transcript restore is lost).

Suggested fix: Route loadHistory/refreshStatic through SlashDispatchEnv callbacks the backend can wire, or have the stub record the call and surface the same explicit parity-deferral message the load_history branch produces.

Fix witness: A command whose action calls ctx.ui.loadHistory([...]) must produce the deferral message (or the env callback must be invoked); deleting the notice/guard turns it red.

中文说明

经 ui.loadHistory 命令式重建历史的命令被空操作桩静默吞掉且无延迟提示——等价的 load_history 结果类型却有显式 'not yet available' 消息。带折叠恢复历史的用户运行 /history expand-now:expandNowCommand 检测被抑制条目、计算展开列表、调用 context.ui.loadHistory(updated)——桩丢弃它——并返回 void,于是 executeSlashCommand 得到 {kind:'handled'}。ink 中转录展开;经本分发器什么都不发生也没人告知用户该功能未移植,违反模块自身的延迟消息约定(同一个桩也是 /restore 转录恢复丢失的原因)。修复:把 loadHistory/refreshStatic 路由到后端可接线的 SlashDispatchEnv 回调;或让桩记录调用并上呈与 load_history 分支相同的显式延迟消息。修复见证:动作调用 ctx.ui.loadHistory([...]) 的命令必须产生延迟消息(或 env 回调被调用);删除提示/守卫会变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +540 to +542
const notice = projectAddedItems(addedItems, env);
if (effect.kind === 'handled') {
return { kind: 'message', messageType: 'info', content: notice };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] History items added via ui.addItem with type error/warning are always projected onto a messageType:'info' effect, dropping the item's severity that ink renders.

The suite's own R2-5 case is the trigger: a command adds { type:'error', text:'Unknown extensions source: bogus.' } and returns no result (/extensions explore bogus). Ink renders the item with error styling; the OpenTUI effect is { kind:'message', messageType:'info' } — a user-visible error surfaces as a neutral info message. The effect type already carries 'warning'|'error' and the projection knows the item type; only the mapping discards it.

Observed:

probe: replaying the real extensionsCommand trigger through dispatch → {kind:'message', messageType:'info', content:'Unknown extensions source: bogus.'}.

Suggested fix: Derive the effect's messageType from the projected item type (single message-type item → its type; otherwise the first message-type item's type) instead of hardcoding 'info'.

Fix witness: Extend the R2-5 test to assert effect.messageType === 'error' for the error item (and 'warning' for a warning item); the hardcoded 'info' fails it.

中文说明

经 ui.addItem 添加的 error/warning 类型历史条目总是被投影为 messageType:'info' 的 effect,丢掉了 ink 会渲染的条目严重级。套件自身的 R2-5 用例就是触发器:命令添加 { type:'error', text:'Unknown extensions source: bogus.' } 且不返回结果(/extensions explore bogus)。ink 以错误样式渲染该条目;OpenTUI 的 effect 是 { kind:'message', messageType:'info' }——用户可见的错误以中性 info 呈现。effect 类型本已携带 'warning'|'error',投影也知道条目类型;只有映射丢弃了它。探针:经分发重放真实 extensionsCommand 触发 → messageType 为 'info'。修复:从投影条目类型推导 effect 的 messageType(单个消息型条目 → 其类型;否则首个消息型条目的类型),而不是硬编码 'info'。修复见证:扩展 R2-5 测试,对 error 条目断言 effect.messageType === 'error'(warning 同理);硬编码 'info' 会失败。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +125 to +128
* The currently active theme mapped for OpenTUI — includes the ink parity
* rule that `NO_COLOR` forces the NoColor theme.
*/
export function getActiveOpenTuiTheme(): OpenTuiThemeDefinition {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The NO_COLOR parity the docstring promises is mapped but then discarded by the apply path: NoColorTheme maps to an all-empty palette, and applyOpenTuiTheme drops every empty value and falls back to the full-color built-in dark surface — under NO_COLOR the OpenTUI renderer shows full color where ink renders colorless.

With NO_COLOR=1, themeManager.getActiveTheme() returns NoColorTheme and getActiveOpenTuiTheme() yields its all-empty palette (pinned by theme-parity.test.ts). The documented apply path then executes its value === '' ? undefined : toHex(value) filter, drops every palette key, and Object.assign(C, DARK, {}) leaves the full-color dark palette active — theme.test.ts explicitly asserts C.text stays '#CDD6F4'. Trigger: any user who sets NO_COLOR once the renderer is wired; the override the module header claims to carry has no visual effect.

Suggested fix: Either honor NoColor on the apply path where opentui tolerates it, or correct the theme-parity docstring to state that NO_COLOR cannot be honored on the OpenTUI renderer and let the renderer-selection layer act on that.

Fix witness: theme.test.ts's NoColor case currently pins the divergent fallback; the chosen fix must change/extend that test to pin the honored behavior (or the documented fallback).

中文说明

docstring 承诺的 NO_COLOR 对等被映射后又被应用路径丢弃:NoColorTheme 映射为全空调色板,而 applyOpenTuiTheme 丢弃每个空值并回退到全彩内置深色表面——NO_COLOR 下 OpenTUI 渲染器显示全彩,而 ink 渲染无色。NO_COLOR=1 时 themeManager.getActiveTheme() 返回 NoColorTheme,getActiveOpenTuiTheme() 产出全空调色板(theme-parity.test.ts 钉住)。随后文档化的应用路径执行 value === '' ? undefined : toHex(value) 过滤,丢弃每个调色板键,Object.assign(C, DARK, {}) 留下全彩深色调色板——theme.test.ts 显式断言 C.text 保持 '#CDD6F4'。触发:任何设置 NO_COLOR 的用户,一旦渲染器接线;模块头声称携带的覆盖没有视觉效果。修复:或在 opentui 容忍处兑现 NoColor,或修正 theme-parity 的 docstring 声明 OpenTUI 渲染器无法兑现 NO_COLOR、由渲染器选择层据此处理。修复见证:theme.test.ts 的 NoColor 用例当前钉住相异的回退;所选修复必须改动/扩展该测试以钉住被兑现的行为(或被文档化的回退)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +133 to +137
export function createSyntaxStyle(
definition: OpenTuiThemeDefinition,
): SyntaxStyle {
return SyntaxStyle.fromStyles(definition.syntaxStyles);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] createSyntaxStyle hands raw ink color values to SyntaxStyle.fromStyles without the toHex normalization its sibling path applyOpenTuiTheme performs, so for ink-name colors the two construction paths produce different opentui colors for the same theme definition; unrecognized names degrade to magenta.

Verified against @opentui/core 0.5.8: parseColor accepts only hex plus a 28-entry CSS table; anything else defaults to magenta with a console.warn. ANSI theme: syntaxStylesFromInkTheme emits {fg:'green'} for number/comment; via createSyntaxStyle opentui parses CSS green = #8000 while applyOpenTuiTheme resolves ink green to #00ff00 — the same theme renders different greens depending on which API wiring uses. Custom themes: 'bluebright' passes through unresolved (opentui knows 'brightblue', reversed) → keywords render magenta, while applyOpenTuiTheme resolves it to #8080ff. theme.ts's own comment documents this hazard.

Observed:

probe (real parseColor + repo toHex): green → #008000 vs #00ff00; bluebright → #ff00ff (warn 'Invalid hex color: bluebright, defaulting to magenta') vs #8080ff; coral → #ff00ff (warn) vs #ff7f50.

Suggested fix: Normalize in createSyntaxStyle the same way applyOpenTuiTheme does — resolve each style's fg/bg through toHex, delete unresolvable ones, then call fromStyles — ideally via a shared helper.

Fix witness: Assert createSyntaxStyle(getOpenTuiTheme('ANSI')!).styles['number'].fg === '#00ff00' (reds today — it is 'green'), and/or that every fg matches /^#[0-9a-f]{6}$/ for all built-ins.

中文说明

createSyntaxStyle 把原始 ink 颜色值直接交给 SyntaxStyle.fromStyles,没有做兄弟路径 applyOpenTuiTheme 所执行的 toHex 规范化,因此对 ink 名称颜色,两条构造路径对同一主题定义产出不同的 opentui 颜色;无法识别的名称退化为品红。对照 @opentui/core 0.5.8 验证:parseColor 只接受 hex 加 28 项 CSS 表;其余默认品红并 console.warn。ANSI 主题:syntaxStylesFromInkTheme 对 number/comment 发出 {fg:'green'};经 createSyntaxStyle,opentui 把 CSS green 解析为 #8000,而 applyOpenTuiTheme 把 ink green 解析为 #00ff00——同一主题因接线使用哪个 API 而渲染不同绿色。自定义主题:'bluebright' 原样通过(opentui 只认 'brightblue',方向相反)→ 关键字渲染品红,而 applyOpenTuiTheme 解析为 #8080ff。theme.ts 的注释本就记录了这个危害。探针(真实 parseColor + 仓库 toHex):green → #8000 对 #00ff00;bluebright → #ff00ff(warn 'Invalid hex color: bluebright, defaulting to magenta')对 #8080ff;coral → #ff00ff(warn)对 #ff7f50。修复:在 createSyntaxStyle 中做与 applyOpenTuiTheme 相同的规范化——每个样式的 fg/bg 经 toHex 解析、删除不可解析者,再调 fromStyles——理想做法是抽出共享助手。修复见证:断言 createSyntaxStyle(getOpenTuiTheme('ANSI')!).styles['number'].fg === '#00ff00'(当前为 'green',红),并/或所有内置主题的每个 fg 匹配 /^#[0-9a-f]{6}$/。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +144 to +145
'markup.raw': { fg: inlineCode },
'markup.italic': { italic: true },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] markdownMarkupTokens defines markup.raw (the markdown_inline code_span capture) but omits markup.raw.block, which is the capture @opentui/core's markdown highlights.scm actually uses for fenced and indented code blocks — and the style fallback cannot bridge the gap.

Verified against lockfile-pinned @opentui/core 0.5.8: assets/markdown/highlights.scm captures (fenced_code_block) @markup.raw.block and (indented_code_block) @markup.raw.block; markup.raw only exists in markdown_inline. Style resolution for an unmatched group falls back to the FIRST dot-segment only, so markup.raw.block falls back to markup — which theme.ts also does not define — never to markup.raw. The bundled grammars are only javascript/typescript/zig/markdown, so any fenced block with another language (python, bash, json — the common case in model output) is styled by markup.raw.block alone: fenced-code bodies render in the default foreground instead of the code color once tree-sitter markdown is enabled, while inline code keeps it.

Suggested fix: Add the block capture alongside the inline one: 'markup.raw.block': { fg: inlineCode },

Fix witness: theme.test.ts's markdownMarkupTokens capture-name key list: adding 'markup.raw.block' to it reds without the token.

中文说明

markdownMarkupTokens 定义了 markup.raw(markdown_inline 的 code_span 捕获)但遗漏了 markup.raw.block——@opentui/core 的 markdown highlights.scm 对围栏与缩进代码块实际使用的捕获——且样式回退无法弥合。对照锁定的 @opentui/core 0.5.8 验证:assets/markdown/highlights.scm 捕获 (fenced_code_block) @markup.raw.block 与 (indented_code_block) @markup.raw.block;markup.raw 只在 markdown_inline 中。未匹配组的样式解析只回退到首个点分段,因此 markup.raw.block 回退到 markup——theme.ts 同样未定义——永远不会到 markup.raw。捆绑语法只有 javascript/typescript/zig/markdown,因此其他语言(python、bash、json——模型输出的常见情形)的围栏块只由 markup.raw.block 定样式:一旦启用 tree-sitter markdown,围栏代码主体以默认前景色渲染而非代码色,行内代码却保留。修复:在行内捕获旁加上块捕获:'markup.raw.block': { fg: inlineCode }。修复见证:theme.test.ts 的 markdownMarkupTokens 捕获名键列表:未加该键时,把 'markup.raw.block' 加入列表会变红。

— qwen3.8-max via Qwen Code /review (v0.22.2)

- a11y-plain-text: split on all CommonMark line endings so CRLF
  markdown opens/closes fences correctly; private-param CSI regex
  covers ECMA-48 intermediate bytes; DCS/SOS/PM/APC and unterminated
  OSC sequences consumed before strip-ansi; code-span pattern mirrors
  ink's INLINE_CODE_SPAN_PATTERN (non-empty content, closing-run
  lookbehind)
- dialogs-shared: clearNumberBuffer called from setActiveIndex,
  selectIndex, and resyncKey block so wheel/hover/click/resync can't
  commit a stale numeric-flush selection the user never made
- event-adapter: tool_call_response carries visionBridgeNotice on the
  tool-result event (ink ToolMessage renders the egress disclosure)
- item-projection: projectContextUsage reads memoryFiles as { path,
  tokens } (ContextMemoryDetail), not { name, tokens }
- key-map: 10 kp* keypad-navigation aliases (kpleft→left, …) and
  super flag folded into meta (ink Cmd+Enter = newline, not submit)
- link-click: cellColumns no longer truncated to trimmed text length
  (preserves the wide-glyph right-half boundary); findUrlAtRow end
  boundary is width-aware (stringWidth of the last glyph)
- slash-dispatch: submit effect carries PartListUnion content + a
  textContent string for text-only consumers (image parts survive);
  toggleVimEnabled and startNewSession seams wired from env; abort
  race resolves immediately for an already-aborted signal
- clipboard: OSC 52 self-write removed — copyToClipboard's existing
  fallback (writeOsc52 / wrapForMultiplexer) is the single source
- a11y-screen-reader: appendStatic skips clean === '\n' (ink's
  hasStaticOutput guard)

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R4-32 single-byte escape sequences (ESC + one final byte) leak into the composer — already reported (comment 3884061149)
  • R4-33 decodeCapturedInput strips editing keys without applying their effect — already reported (comment 3884061154)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: reverse audit — stopped before round 2 by the review time budget.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/opentui/dialogs-core.ts:16 — [review] Module header points at dialogs-core.tsx, which does not exi
  • packages/cli/src/ui/opentui/dialogs-shared.test.tsx:8 — [review] The suite never presses Enter, so useDialogSelect's two sele
  • packages/cli/src/ui/opentui/dialogs-theme.test.ts:8 — [review] OpenTuiThemeDialog — the sole consumer wiring preview height
  • packages/cli/src/ui/opentui/early-input.test.ts:73 — [review] drainCapturedInputAsText is tested only for the empty-buffer
  • packages/cli/src/ui/opentui/event-adapter.ts:274 — [review] The task_execution flattening drops taskDescription, though
  • packages/cli/src/ui/opentui/event-adapter.ts:286 — [review] The findings_list projection collapses ink's FindingsDisplay
  • packages/cli/src/ui/opentui/help-content.ts:252 — [review] formatHelpText never truncates shortcut descriptions, but in
  • packages/cli/src/ui/opentui/help-content.ts:284 — [review] formatHelpText pushes 'Browse built-in commands:' unconditio
  • packages/cli/src/ui/opentui/help-content.ts:290 — [review] When there are no custom commands, formatHelpText drops the
  • packages/cli/src/ui/opentui/item-projection.ts:164 — [review] List projections indent sub-rows with a 1-space base where i
  • packages/cli/src/ui/opentui/item-projection.ts:277 — [review] projectModelStats gates the Cached/Thoughts rows per entry,
  • packages/cli/src/ui/opentui/item-projection.ts:353 — [review] projectSkillStats sorts by count only; ink breaks count ties
  • packages/cli/src/ui/opentui/item-projection.ts:447 — [review] projectContextUsage emits the 'Usage by category' header unc
  • packages/cli/src/ui/opentui/item-projection.ts:560 — [review] The skills-detail block emits the body-tokens suffix for eve
  • packages/cli/src/ui/opentui/kitty-negotiation.ts:139 — [review] The write-throw guard (stdout.write throw -> finish(false))
  • packages/cli/src/ui/opentui/link-click.ts:157 — [review] mailto: is in the reused osc8 scheme allowlist but unreachab
  • packages/cli/src/ui/opentui/mouse-hit.ts:45 — [review] mouse-hit.ts re-implements frameAnchor/terminalRowToLayoutRo
  • packages/cli/src/ui/opentui/mouse-hit.ts:147 — [review] The container.width > 0 bypass in resolveListIndex is exerci
  • packages/cli/src/ui/opentui/osc8-parity.ts:80 — [review] The legacy fallback's byte-identity invariant (label and URL
  • packages/cli/src/ui/opentui/osc8-parity.ts:110 — [review] renderBareUrl's defense-in-depth scheme-allowlist check is p
  • …and 6 more (see the run report)

Convergence: round 5 posted 27 inline comment(s), 13 of them reported for the first time. Findings keep coming back to the same files: packages/cli/src/ui/opentui/a11y-plain-text.ts (findings in round 4; 3 more now); packages/cli/src/ui/opentui/key-map.ts (findings in round 4; 2 more now); packages/cli/src/ui/opentui/link-click.ts (findings in round 4; 2 more now), and 5 more file(s). (Evidence: the previous round's work list was truncated to fit the marker, so the rounds named above may be an undercount, and a new finding written under an earlier round's id cannot be told from a re-post over a partial list, so the new-finding count may be understated.) A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:反向审计——评审时间预算不足,未能开始第 2 轮。

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 26 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 5 轮发布了 27 条行内评论,其中 13 条是首次提出。发现反复回到同一批文件:packages/cli/src/ui/opentui/a11y-plain-text.ts(第 4 轮已出过发现,本轮又有 3 条);packages/cli/src/ui/opentui/key-map.ts(第 4 轮已出过发现,本轮又有 2 条);packages/cli/src/ui/opentui/link-click.ts(第 4 轮已出过发现,本轮又有 2 条),另有 5 个文件。(证据说明:上一轮的工作清单为放进标记而被截断,上述轮次可能少计;在不完整的清单上,冠以早先轮次 id 的新发现无法与重发区分,首次提出的条数可能少计。)一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.2)

// CommonMark line endings: \r\n, \n, and lone \r all terminate a line;
// splitting on \n alone leaves \r on the line, which `.` excludes and
// `$` cannot see past, deadening fence detection for CRLF markdown.
for (const rawLine of markdown.split(/\r\n|\n|\r/)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R5-1: [certifies-falsely] [new-surface] markdownToPlainText hand-rolls a regex mirror of ink's inline/block markdown semantics over unbounded model output instead of deriving text from the renderer's tokenizer or a real CommonMark parser; the corner space does not converge — rounds 1, 3, 4 and 5 have each found new corners. Round-5 demonstrated corners: (a) italic pass diverges from ink's INLINE_MARKDOWN_REGEX ('foo * bar' -> 'foo * bar' vs ink's 'foo bar*'); (b) NUL-digit-NUL placeholder restore collides with literal text ('real \u00000\u0000 end' gets the code-span content substituted); (c) the fence opener accepts a backtick fence whose info string contains a backtick, which CommonMark/ink reject, inverting parse state for the rest of the document; (d) spaced thematic breaks ('* * *') slip past the hr regex while ink's hrRegex matches them. Any model output containing one of these corners is announced to screen-reader users differently from what ink renders; each round patches corners without closing the class.

Witness:

probe: markdownToPlainText('*foo * bar*')='foo * bar' vs ink tokenizer squashed='foo bar*'; 'real `code` here \u00000\u0000 end' -> 'real code here code end'; '```a`b\n# Not a heading' -> '# Not a heading...' while ink CODE_FENCE_RE.exec('```a`b')=null; '* * *' -> '* * *' while ink hrRegex.test('* * *')=true

Suggested fix: Derive the reduction from the renderer's authoritative tokenization (getPlainTextLength in InlineMarkdownRenderer.tsx already walks INLINE_MARKDOWN_REGEX and strips markers — extract and share it), or from a real markdown parser's AST, instead of parallel regexes.

Fix witness: Pin the divergence cases above in a11y-plain-text.test.ts; they are red today and only go green once the reduction is derived from ink's tokenizer. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

markdownToPlainText 用手工正则镜像 ink 的内联/块级 markdown 语义,而不是从渲染器的 tokenizer 或真正的 CommonMark 解析器推导纯文本;corner 空间无法收敛——第 1、3、4、5 轮各发现新 corner。本轮实证:(a) 斜体处理与 ink 的 INLINE_MARKDOWN_REGEX 分歧('foo * bar' -> 'foo * bar',ink 为 'foo bar*');(b) NUL-数字-NUL 占位符还原与字面文本冲突,用户文本被代码段内容替换;(c) 围栏开启正则接受 info 串含反引号的反引号围栏(CommonMark/ink 均拒绝),反转文档其余部分的解析状态;(d) 带空格的分割线 '* * *' 逃过 hr 正则。建议改为从渲染器的权威 token 流推导(InlineMarkdownRenderer.tsx 的 getPlainTextLength 已遍历 INLINE_MARKDOWN_REGEX 并去标记,可抽出共享),或从真正的 markdown 解析器 AST 推导。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +35 to +36
const OTHER_ESCAPE_SEQUENCE =
/\x1b[PX^_][\s\S]*?(?:\x1b\\|\x07|$)|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R4-14: [certifies-falsely] [new-surface] (fix-induced) The round-4 fix of R4-14 added OTHER_ESCAPE_SEQUENCE, but an OSC sequence followed by another escape without BEL/ST is not consumed, so its body leaks into screen-reader text despite the regex's stated purpose. A truncated stream or tool output whose title/hyperlink OSC lost its BEL, followed by colored output, leaks the OSC body into the announced text via ScreenReaderOutputWriter.sanitize.

Witness:

probe: stripAnsi('\x1b]0;secret\x1b[31mred\x1b[0m') = 'ecretred' (expected 'red'); leak reaches the writer: appendStatic emits 'ecretred\n'

Suggested fix: Make the OSC alternative stop (without consuming) at any bare ESC that does not start ST: \x1b][^\x07\x1b]*(?:\x07|\x1b\|(?=\x1b)|$) — an ESC inside an OSC body is invalid, so truncating there leaves the following sequence intact for strip-ansi.

Fix witness: Add expect(stripAnsi('\x1b]0;secret\x1b[31mred\x1b[0m')).toBe('red') to a11y-plain-text.test.ts; removing the (?=\x1b) alternative makes it red again. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(fix-induced)第 4 轮对 R4-14 的修复新增了 OTHER_ESCAPE_SEQUENCE 预处理,但未带 BEL/ST 终止、后随另一个转义序列的 OSC 不会被消费,其正文仍会泄漏进读屏文本(实测泄漏到 ScreenReaderOutputWriter 的播报输出)。建议 OSC 分支在遇到不构成 ST 的裸 ESC 时停止且不消费:改为 \x1b][^\x07\x1b]*(?:\x07|\x1b\|(?=\x1b)|$)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +183 to +185
const lastChar = row.text[hit.end - 1] ?? '';
const lastColumn = row.cellColumns[row.cellColumns.length - 1] ?? 0;
endCellExclusive = lastColumn + (stringWidth(lastChar) || 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R4-8: [certifies-falsely] [new-surface] (fix-induced) The round-4 fix of R4-8 made the end-boundary fallback width-aware via stringWidth(lastChar), but lastChar is the last UTF-16 UNIT, not the last code point: for a URL ending in a non-BMP character (emoji), it is the lone low surrogate, stringWidth of it is 0, and the ||1 fallback yields width 1 instead of 2 — the right-half cell of the final emoji misses the hit. A rendered row 'https://x.co/🎉' with the URL at the row's content edge: clicking the right half of the final emoji does not open the link.

Witness:

probe: left-half of final emoji -> HIT 'https://x.co/🎉'; right-half -> null; BMP-wide control 'https://x.co/文' hits both halves; stringWidth(lone low surrogate)=0, stringWidth('🎉')=2

Suggested fix: Take the last code point instead of the last UTF-16 unit: const lastChar = Array.from(row.text).at(-1) ?? ''.

Fix witness: Add a link-click.test.ts row 'https://a.dev/🎉' asserting findUrlAtRow hits on both the last and last+1 cells and null at last+2; reverting to row.text[hit.end - 1] makes the right-cell assertion fail. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(fix-induced)第 4 轮对 R4-8 的修复让行末边界回退感知宽度(stringWidth(lastChar)),但 lastChar 取的是最后一个 UTF-16 单元而非最后一个码点:URL 以 emoji(非 BMP)结尾时,它是孤立低代理位,stringWidth 为 0,||1 回退得到宽度 1 而非 2——末尾 emoji 右半格点击无法命中链接(已执行验证:左半命中、右半 miss,BMP 宽字符对照两边均命中)。建议用 Array.from(row.text).at(-1) 取最后一个码点。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +222 to +224
return (
<DialogFrame>
{mode === 'theme' ? (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R4-30: (still stands from round 4, covers R4-29, severity escalated) The tight-height (includePadding=false) branch of the height budget is only half-applied: DialogFrame hardcodes padding={1} with no vertical-padding prop while ink's ThemeDialog sets paddingTop/Bottom={includePadding ? 1 : 0}, and theme mode adds a marginBottom={1} header margin and a marginTop={1} preview-pane margin that ink renders neither. 24-row terminal with 13+ themes: budget 19 (getDialogMaxHeight(24,3)), internal 15 < 16 -> includePadding=false; ink renders 19 rows (fits exactly), the port renders 22 rows — exceeding the computed budget by 3, clipping the bottom border/footer, in the exact scenario the budget function exists for.

Witness:

probe: computeThemePreviewLayout(19, 13) = {includePadding: false, codeBlockHeight: 4, diffHeight: 2}; row count from quoted JSX: ink 19 vs port 22 (16 content + 2 unconditional frame padding + 2 borders + 2 added margins)

Suggested fix: Give DialogFrame a paddingY prop and pass layout.includePadding ? 1 : 0 from OpenTuiThemeDialog; drop the theme-mode marginBottom/marginTop rows to match ink's snapshot geometry.

Fix witness: A render test mounting OpenTuiThemeDialog with availableTerminalHeight=19 and 13 themes asserting total height <= 19 and zero frame vertical padding when includePadding=false; removing the propagation turns it red. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(第 4 轮 R4-30 仍成立,合并 R4-29,严重度升级)紧凑高度(includePadding=false)分支只做了一半:DialogFrame 硬编码 padding={1} 且没有垂直 padding 参数,而 ink 的 ThemeDialog 用 paddingTop/Bottom={includePadding ? 1 : 0};theme 模式还额外加了 ink 并不渲染的 marginBottom={1}(标题下)与 marginTop={1}(预览区上)。24 行终端、13 个主题:预算 19(getDialogMaxHeight(24,3)),内部 15 < 16 -> includePadding=false;ink 渲染 19 行恰好放下,本实现渲染 22 行——在预算函数存在的场景里超出预算 3 行并裁掉底边框。建议给 DialogFrame 增加 paddingY 参数并由对话框传入 layout.includePadding ? 1 : 0,同时去掉两个多余 margin。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +102 to +104
for (let x = 0; x < grid.width; x++) {
const codePoint = chars[base + x];
cells[x] = isContinuationCell(codePoint) ? '' : (lineChars[ci++] ?? ' ');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R5-2: [certifies-falsely] [new-surface] The native-grid decode consumes exactly one code point per non-continuation cell; if a cell resolves to a multi-code-point grapheme cluster (combining mark, ZWJ/skin-tone emoji), every subsequent cell's character and cellColumns entry shifts, corrupting hit-test mapping and even truncating the row text. The shipped libopentui.so exposes CHAR_FLAG_GRAPHEME/drawGrapheme/GraphemeTracker, but link-click.ts handles only CHAR_FLAG_CONTINUATION. 'Café https://a.dev' with é as e+U+0301 occupying one terminal cell: cellColumns shift right by the surplus code points, trailing code points of the row are dropped (URL 'a.dev' truncated to 'a.de' observed), clicking the visible URL start misses, and a click one cell off opens the truncated URL.

Witness:

probe (native-path input modeling a grapheme cell): decoded cells leak the combining mark into the next cell; row.text='Café https://a.de' (final 'v' dropped); findUrlAtRow(x=5)=null while findUrlAtRow(x=6) hits — off by one

Suggested fix: Consume one grapheme cluster per non-continuation cell (Intl.Segmenter granularity:'grapheme'), or verify and document @opentui/core's per-cell invariant at the seam.

Fix witness: A native-grid test whose getRealCharBytes returns 'e\u0301x' for two non-continuation cells asserting cellColumns 0,0,1 — red today (returns 0,1,2). Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

原生网格解码对每个非续接单元恰好消费一个码点;若单元解析为多码点字位簇(组合标记、ZWJ/肤色 emoji),其后所有单元的字符与 cellColumns 条目整体右移,命中映射被破坏,行尾码点还会被截断(实测 'a.dev' 被截成 'a.de',可见 URL 起点点击 miss、偏一格反而打开截断 URL)。libopentui.so 暴露 CHAR_FLAG_GRAPHEME/drawGrapheme/GraphemeTracker,但 link-click.ts 只处理 CHAR_FLAG_CONTINUATION。建议按字位簇消费(Intl.Segmenter, granularity:'grapheme'),或在接缝处验证并记录库的每单元不变量。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +720 to +721
default:
break;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-40: (still stands from round 4) The mapper's top-level switch over ServerGeminiStreamEvent ends in a bare default:break with no exhaustiveness guard, so a new core event type silently produces nothing. Core adds an event type; the OpenTUI mapper drops it without a compile error or a log, and the divergence is discovered only by user reports.

Witness:

round-5 re-check at HEAD: unchanged from round 4

Suggested fix: Add a satisfy/exhaustiveness check or a debug log on unhandled event types.

Fix witness: Adding a new member to the event union must fail compilation or produce the logged fallback. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(第 4 轮仍成立)映射器顶层 switch 以裸 default:break 结尾、无穷尽性守卫;核心新增事件类型会被静默丢弃且无编译错误、无日志。建议加 satisfies/穷尽性检查,或对未处理事件类型输出调试日志。

— qwen3.8-max via Qwen Code /review (v0.22.2)

Comment on lines +743 to +744
): Promise<void> {
const map = createEventMapper();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-41: (still stands from round 4; re-verified this round) pumpServerStream — the documented drain for real agent streams and the only non-test consumer of createEventMapper — constructs the mapper with no EventMapperContext and accepts no parameter to forward one, making all four context knobs (showCitations, formatError, getModelName, getMaxSessionTurns) dead switches on the sole production path. Once wired: ui.showCitations:false still shows citations; errors bypass parseAndFormatApiError (no 429/auth hints, empty-message errors silently dropped); compaction and max-turns notices print 'the model' / 'the configured limit' instead of real values.

Witness:

round-5 probe at HEAD: pumpServerStream.length === 2; draining max_session_turns emits 'the configured limit'; a citation event passes ungated

Suggested fix: Add a third parameter and forward it: pumpServerStream(stream, sink, context?: EventMapperContext) -> createEventMapper(context).

Fix witness: A test pumping a citation event with { showCitations: () => false } asserting no info event reaches the sink goes red if the parameter is dropped. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(第 4 轮仍成立,本轮复核)pumpServerStream——文档声明用于真实代理流的唯一非测试消费者——构造 createEventMapper() 时不接受也不转发 EventMapperContext,四个上下文旋钮(showCitations、formatError、getModelName、getMaxSessionTurns)在唯一生产路径上全是死开关。探针验证:max_session_turns 通知打印 'the configured limit',citation 不被门控。建议增加第三参数并转发:createEventMapper(context)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

*/
onWindowExpired?: (key: ExitGuardKey) => void;
/** Injectable timer for tests. */
setTimeoutFn?: (fn: () => void, ms: number) => unknown;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-42: (still stands from round 4) The injectable timer seam (setTimeoutFn/clearTimeoutFn) is a dead switch: declared and read but never set by any caller in this commit. An unexercised injection seam can rot silently; the guard's timing behavior is only ever tested through real timers.

Witness:

round-5 re-check at HEAD: unchanged from round 4; tests use vi.useFakeTimers() instead of the seam

Suggested fix: Either exercise the seam in a test or drop it until a consumer needs it.

中文说明

(第 4 轮仍成立)可注入定时器接缝(setTimeoutFn/clearTimeoutFn)被声明、被读取,但无任何调用方设置——死开关(测试用的是 vi.useFakeTimers())。建议用测试行使该接缝,或在有消费者之前删除。

— qwen3.8-max via Qwen Code /review (v0.22.2)

}

/** Footer hint text for an armed exit (ink Footer.tsx / ExitWarning parity). */
export function exitGuardHint(key: ExitGuardKey): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-43: (still stands from round 4) exitGuardHint hardcodes English hint strings, but the ink footer renders both hints through t() and all nine locale files translate both keys. zh locale, Ctrl+C once: ink shows 再次按 Ctrl+C 退出; the OpenTUI footer shows English — breaking the parity the module docstring and the 'matches the ink footer wording' test claim (the test pins the English strings, hiding the divergence).

Witness:

round-5 probe at HEAD: exitGuardHint returns the English literals under any locale; zh.js:1809 carries the translation

Suggested fix: Return t(key === 'ctrl-d' ? 'Press Ctrl+D again to exit.' : 'Press Ctrl+C again to exit.') — both keys already exist in every locale file.

Fix witness: A test switching the active language to zh and asserting the translated hint goes red if the t() wrapper is removed. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(第 4 轮仍成立)exitGuardHint 硬编码英文提示,而 ink 的 footer 两处提示都经 t() 渲染,且九个语言文件均已翻译(zh.js:1809 '再次按 Ctrl+C 退出')。zh 语言下按一次 Ctrl+C:ink 显示中文,OpenTUI 显示英文——现有测试固定英文字符串,反而掩盖了分歧。建议包 t()(两个键在所有语言中已存在,无需新增翻译)。

— qwen3.8-max via Qwen Code /review (v0.22.2)

out.push({
type: 'error',
text: message,
hint: 'Press Ctrl+Y to retry',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-44: (still stands from round 4; scope corrected this round) The hardcoded-English divergence holds for the error retry hint: ink wraps it in t() and all nine locales translate '按 Ctrl+Y 重试。', while the adapter hardcodes it. The round-4 sibling examples (compression/max-turns/cancel notices) are retracted — ink hardcodes those too, and the adapter is faithful parity there. Non-English locale: every error notice from the adapter ships the English retry hint while ink localizes it.

Witness:

round-5 probe at HEAD: error events carry the hardcoded hint; locale grep shows nine translations against ink's t(...) call sites; compression/max-turns texts verified byte-identical to ink's hardcoded originals

Suggested fix: Wrap the retry hint in t() (the key already exists in every locale).

Fix witness: A locale-switched test asserting the translated hint goes red if t() is removed. Please add this test and confirm the mutation — remove the fix/guard and verify the test goes red.

中文说明

(第 4 轮仍成立,本轮修正范围)错误重试提示硬编码英文:ink 用 t() 包裹 'Press Ctrl+Y to retry',九个语言均已翻译(中文 '按 Ctrl+Y 重试。')。第 4 轮列举的姊妹例子(压缩/最大轮次/取消通知)予以撤回——ink 同样硬编码这些字符串,适配器在那些位置是忠实对等。建议只给重试提示包 t()。

— qwen3.8-max via Qwen Code /review (v0.22.2)

- event-adapter: finished branch emits retry-countdown-clear BEFORE the
  info notice so the countdown row is actually cleared (the fold only
  pops when the last item is the retry row)
- item-projection: /mcp tips now include all 5 lines ink renders (added
  OAuth auth tip and Ctrl+T toggle tip)
- link-click: wide-glyph end boundary uses the last code point (not
  UTF-16 code unit) so non-BMP emoji are measured correctly by
  stringWidth
- a11y-plain-text: CSI_SEQUENCE replaces PRIVATE_PARAM_CSI — drops the
  marker requirement so any CSI (with or without private parameter
  marker, with or without intermediate bytes) is fully consumed

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not explored to full depth (tool budget reached): "agent 1b": none — no line needed.; chunk 12: could not execute npx vitest run src/ui/opentui/event-adapter.test.ts — the review worktree has no installed node_modules and no built workspace dist; static …; chunk 19: none — wait, per instructions: no checks were cut short, so no Budget gap line..

Not reviewed: reverse audit — stopped before round 2 by the review time budget.

Not reviewed: "agent reverse-audit (round 1)" — the agent made no tool call: it read nothing.

Not reviewed: "agent verify", "agent verify (round 1)" — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/opentui/a11y-plain-text.test.ts:22 — [probe] The end-of-input ($) branch of OTHER_ESCAPE_SEQUENCE is…
  • packages/cli/src/ui/opentui/dialogs-shared.test.tsx:135 — [probe] The resync branch's disarm of an armed numeric flush has…
  • packages/cli/src/ui/opentui/dialogs-shared.tsx:108 — [probe] DialogTabBar hardcodes the active-tab foreground '#000000'…
  • packages/cli/src/ui/opentui/dialogs-shared.tsx:370 — [probe] DialogSelect's row-render logic (indicator/number color…
  • packages/cli/src/ui/opentui/early-input.test.ts:123 — [probe] The 'stops retrying after maxAttempts' test contains zero…
  • packages/cli/src/ui/opentui/event-adapter.test.ts:57 — [probe] visionBridgeNotice propagation has zero coverage even on…
  • packages/cli/src/ui/opentui/event-adapter.test.ts:77 — [probe] Every tool_call_response mapper test asserts display:''…
  • packages/cli/src/ui/opentui/event-adapter.ts:481 — [probe] The failed/cancelled branch of the tool-end mapping has no…
  • packages/cli/src/ui/opentui/exit-lifecycle.ts:46 — [probe] exitSession/isExitInProgress/EXIT_CODE_* have zero…
  • packages/cli/src/ui/opentui/item-projection.test.ts:46 — [review] The vi.mock override hardcodes getMCPServerStatus to…
  • packages/cli/src/ui/opentui/item-projection.ts:73 — [probe] projectAbout emits a 'Qwen Code' line when cliVersion is…
  • packages/cli/src/ui/opentui/item-projection.ts:261 — [review] ~30 user-facing strings are hardcoded English where ink…
  • packages/cli/src/ui/opentui/item-projection.ts:276 — [probe] The Cached/Thoughts rows in projectModelStats are gated…
  • packages/cli/src/ui/opentui/item-projection.ts:353 — [probe] projectSkillStats sorts only by call count, omitting ink's…
  • packages/cli/src/ui/opentui/item-projection.ts:408 — [probe] projectInsightProgress dropped ink's negative-side clamp…
  • packages/cli/src/ui/opentui/item-projection.ts:441 — [probe] projectContextUsage never emits the over-limit warning ink…
  • packages/cli/src/ui/opentui/item-projection.ts:447 — [probe] 'Usage by category' is pushed unconditionally; ink renders…
  • packages/cli/src/ui/opentui/item-projection.ts:559 — [probe] The skill body-cost suffix prints for every loaded skill;…
  • packages/cli/src/ui/opentui/item-projection.ts:562 — [probe] Skill names are truncated at 30 chars but padded to only…
  • packages/cli/src/ui/opentui/item-projection.ts:731 — [probe] projectMcpStatus drops MCP prompt descriptions that ink…
  • …and 23 more (see the run report)

Convergence: round 6 posted 7 inline comment(s), 3 of them reported for the first time; the previous round posted 27 (13 new). Findings keep coming back to the same files: packages/cli/src/ui/opentui/event-adapter.ts (findings in round 4; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。

未探索到全部深度(达到工具调用预算):"agent 1b"none — no line needed.;chunk 12:could not execute npx vitest run src/ui/opentui/event-adapter.test.ts — the review worktree has no installed node_modules and no built workspace dist; static …;chunk 19:none — wait, per instructions: no checks were cut short, so no Budget gap line.

未审查:反向审计——评审时间预算不足,未能开始第 2 轮。

未审查:"agent reverse-audit (round 1)"——该 agent 未发起任何工具调用:它什么都没读。

未审查:"agent verify""agent verify (round 1)"——启动 prompt 为它指定了 diff 中的行,但它从未打开:有工具调用,却没有一次读取 diff。

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 43 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 6 轮发布了 7 条行内评论,其中 3 条是首次提出;上一轮发布了 27 条(其中 13 条首次提出)。发现反复回到同一批文件:packages/cli/src/ui/opentui/event-adapter.ts(第 4 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.3)

// passthroughs never reach the screen reader as announced garbage.
/* eslint-disable no-control-regex */
const OTHER_ESCAPE_SEQUENCE =
/\x1b[PX^_][\s\S]*?(?:\x1b\\|\x07|$)|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R4-14: [certifies-falsely] [new-surface] An OSC sequence followed by another escape without BEL/ST still violates the strip contract — the visible text after it is swallowed from the screen-reader announcement entirely.

The round-4 fix added OTHER_ESCAPE_SEQUENCE and this round's broadened CSI_SEQUENCE changed the symptom without closing it: for '\x1b]0;secret\x1b[31mred\x1b[0m' (a title OSC that lost its BEL, followed by colored output), the CSI pass strips '\x1b[31m'/'\x1b[0m' first, leaving '\x1b]0;secretred'; the OSC alternative then takes its end-of-input branch and consumes everything, so stripAnsi returns '' and ScreenReaderOutputWriter.appendStatic writes nothing — the legitimate text 'red' never reaches the announcement. Any truncated stream or tool output whose OSC lost its BEL, followed by real content, goes silent.

Witness:

probe at HEAD: stripAnsi('\x1b]0;secret\x1b[31mred\x1b[0m') = ""   (expected "red")
writer.appendStatic(same) -> writes: []                            (expected ["red\n"])
mutant ($ branch removed): "ecretred" — round-5 leak shape, suite still 19/19 green

Make the OSC alternative stop (without consuming) at any bare ESC that does not start ST, so the following sequence survives for the CSI pass and the text after it is kept — add a (?=\x1b) alternative to the OSC half. The header comment's "consume through ST/BEL/end-of-input" rationale answers dropping the garbage body itself; it does not answer swallowing the legitimate trailing text.

Suggested change
/\x1b[PX^_][\s\S]*?(?:\x1b\\|\x07|$)|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g;
/\x1b[PX^_][\s\S]*?(?:\x1b\\|\x07|$)|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|(?=\x1b)|$)/g;

Please add expect(stripAnsi('\x1b]0;secret\x1b[31mred\x1b[0m')).toBe('red') to a11y-plain-text.test.ts, and confirm the mutation — remove the lookahead alternative and verify the new test goes red.

中文说明

R4-14(仍成立):未带 BEL/ST 终止、后随另一个转义序列的 OSC 仍然违反剥离契约——其后的可见文本会被整体吞掉,无法进入读屏播报。第 4 轮新增的 OTHER_ESCAPE_SEQUENCE 与本轮放宽的 CSI_SEQUENCE 改变了症状但未关闭问题:对 '\x1b]0;secret\x1b[31mred\x1b[0m'(丢了 BEL 的标题 OSC + 彩色输出),CSI 通道先剥掉 '\x1b[31m'/'\x1b[0m',OSC 分支随后走到"直到输入结束"分支,把剩余文本全部消费——stripAnsi 返回 '',读屏写入器什么都不写,合法文本 'red' 丢失。建议把 OSC 分支改为在遇到不构成 ST 的裸 ESC 时停止(前瞻 (?=\x1b)),让后续序列留给 CSI 通道、其后的文本保留。请补充上述断言并做变异验证(移除前瞻分支后新测试应变红)。

— qwen3.8-max via Qwen Code /review (v0.22.3)

* and horizontal rules are dropped. Bullet markers stay — they are readable
* content in the ink parity path too.
*/
export function markdownToPlainText(markdown: string): string {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R5-1: [certifies-falsely] [new-surface] markdownToPlainText hand-rolls a regex mirror of ink's inline/block markdown semantics over unbounded model output instead of deriving text from the renderer's tokenizer or a real CommonMark parser; the corner space does not converge — rounds 1, 3, 4, 5 and now 6 have each found new corners.

Round 6 re-executed the round-5 corners at this commit (all still reproduce: italic divergence on 'foo * bar', the NUL-placeholder collision, a backtick fence whose info string contains a backtick inverting parse state, spaced thematic breaks '* * *' slipping past the hr regex) and found two more: the ... underline construct is missing entirely — markdownToPlainText('hi') announces the literal tags where ink renders 'hi' — and the unescapeMarkdownDollars prose pass is missing, so 'costs $5' reaches the reader with the backslash where ink says '$'. Any model output containing one of these corners is announced to screen-reader users differently from what ink renders.

Witness:

probe at HEAD vs ink authority:
'*foo * bar*' -> 'foo * bar' (ink tokenizes 'foo bar*')
'> ```\n> code\n> ```' -> '```\ncode\n```' (fence detection runs before de-quote)
'``` `weird\nstill?\n```' -> 'still?' (ink CODE_FENCE_RE rejects the opener; two lines dropped)
'* * *' unmatched, ink hrRegex.test('* * *') = true
inline-pass diff vs InlineMarkdownRenderer.tsx: no <u> branch, no unescapeMarkdownDollars

Derive the reduction from the renderer's authoritative tokenization (getPlainTextLength in InlineMarkdownRenderer.tsx already walks INLINE_MARKDOWN_REGEX and strips markers — extract and share it) or from a real CommonMark parser AST, instead of parallel regexes; if the hand-rolled reducer is deliberately kept for this foundation PR, at minimum add the pass and the unescapeMarkdownDollars parity pass before the placeholder restore. Note the module's own pinned invariant: ink guards underscore emphasis at word boundaries so identifiers like init survive (test 'leaves dunders and snake_case identifiers untouched') — any replacement must preserve that.

Please pin the divergence cases above in a11y-plain-text.test.ts, and confirm the mutation — they are red today and only go green once the reduction is derived from ink's tokenizer or the missing passes are added.

中文说明

R5-1(仍成立):markdownToPlainText 用手工正则镜像 ink 的内联/块级 markdown 语义,作用于无界的模型输出,而不是从渲染器的 tokenizer 或真正的 CommonMark 解析器推导纯文本;corner 空间无法收敛——第 1、3、4、5、6 轮各发现新 corner。本轮在该提交上复测第 5 轮 corner 全部仍可复现('foo * bar' 斜体分歧、NUL 占位符冲突、info 串含反引号的围栏反转解析状态、带空格的 '* * *' 逃过 hr 正则),并新发现两处:... 下划线结构完全缺失('hi' 会把字面标签读出来,ink 渲染为 'hi');缺少 unescapeMarkdownDollars 通道('costs $5' 会带着反斜杠播报)。建议改为从渲染器权威 token 流(InlineMarkdownRenderer.tsx 的 getPlainTextLength)或真正的 markdown 解析器 AST 推导;若本基础 PR 刻意保留手工归约器,至少补上 通道与 $ 反转义通道。注意模块自身已固定的约束:ink 在词边界保护下划线强调,init 这类标识符必须原样保留。请补充上述分歧用例并做变异验证。

— qwen3.8-max via Qwen Code /review (v0.22.3)

borderColor={C.dim}
paddingX={1}
paddingY={layout.includePadding ? 1 : 0}
marginTop={1}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R4-30: [certifies-falsely] [new-surface] The tight-height (includePadding=false) branch of the theme dialog's height budget is still only half-applied — the port renders 22 rows into a 19-row budget and clips the bottom border/footer, in the exact scenario the budget function exists for.

DialogFrame hardcodes padding={1} with no vertical-padding prop, while ink's ThemeDialog sets paddingTop/Bottom={includePadding ? 1 : 0}; theme mode additionally renders a marginBottom={1} header margin (line 227) and a marginTop={1} preview-pane margin (line 268) that ink renders neither of. With a 24-row terminal and 16+ themes the budget is 19 (getDialogMaxHeight(24,3)) and computeThemePreviewLayout(19, 17) picks includePadding=false — ink renders exactly 19 rows, the port renders 22 (16 content + 2 unconditional frame padding + 2 borders + 2 added margins), 3 rows over.

Witness:

probe at HEAD: computeThemePreviewLayout(19, 17) = {includePadding:false, codeBlockHeight:6, diffHeight:3}
row sums from quoted JSX constants: ink = 1+0+15+2+0+0+1 = 19 (fits) | port = 1+1+16+2+1+1 = 22
DialogFrame (dialogs-shared.tsx:70) hardcodes padding={1}; margins quoted at dialogs-theme.tsx:227 and :268

Give DialogFrame a paddingY prop and pass layout.includePadding ? 1 : 0 from OpenTuiThemeDialog, and drop the two theme-mode margin rows to match ink's snapshot geometry:

// dialogs-shared.tsx — DialogFrame gains a vertical-padding prop
padding={1} paddingY={props.paddingY ?? 1}
// dialogs-theme.tsx — remove the margins ink does not render
<box flexDirection="row">              {/* was marginBottom={1} */}
<box>                                  {/* was marginTop={1} */}

The fix must keep PREVIEW_PANE_FIXED_VERTICAL_SPACE = 8 consistent with the rendered markup (its comment enumerates what the 8 rows count). Please add a render test mounting OpenTuiThemeDialog with availableTerminalHeight=19 and 13+ themes asserting total height <= 19 and zero frame vertical padding when includePadding=false — removing the propagation must turn it red.

中文说明

R4-30(仍成立,覆盖 R4-29):主题对话框紧凑高度(includePadding=false)分支仍然只做了一半——本实现在 19 行预算里渲染 22 行,裁掉底边框/页脚,正是预算函数要解决的场景。DialogFrame 硬编码 padding={1} 且没有垂直 padding 参数(ink 的 ThemeDialog 用 paddingTop/Bottom={includePadding ? 1 : 0});theme 模式还多渲染了 ink 没有的 marginBottom={1}(第 227 行)与 marginTop={1}(第 268 行)。24 行终端、16+ 主题时预算为 19,ink 恰好渲染 19 行,本实现渲染 22 行,超出 3 行。建议给 DialogFrame 增加 paddingY 参数并由对话框传入 layout.includePadding ? 1 : 0,同时去掉两个多余 margin;保持 PREVIEW_PANE_FIXED_VERTICAL_SPACE = 8 与渲染结构一致。请补充渲染测试(19 行预算、13+ 主题,断言总高 <= 19 且紧凑分支下框架垂直 padding 为 0),并做变异验证。

— qwen3.8-max via Qwen Code /review (v0.22.3)

// Full ECMA-48 CSI production (parameter bytes incl. ':' for kitty
// CSI-u, intermediate bytes, any final @-~): arrows, editing keys
// like Delete/Home/PgDn ('~' final), function keys, modifier forms.
.replace(/\u001B\[[0-9:;<=>?]*[ -/]*[@-~]/g, '')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R6-54: [certifies-falsely] [new-surface] Double-bracket F-key sequences (ESC [ [ A..E, the TERM=linux F1-F5 encoding) survive the capture filter, and the generic CSI strip eats only ESC [ [ — the second '[' is a valid final byte in [@-~] — leaking the trailing letter into the composer.

A user on a Linux virtual console (TERM=linux; terminfo kf1=\E[[A .. kf5=\E[[E) who presses F1-F5 while the CLI boots gets stray 'A'-'E' text injected into their first prompt: classifyEscapeSequence keeps the sequence (the third byte '[' is none of ?/>/<, so it is user input), and decodeCapturedInput's CSI regex then matches '\x1b[[' alone and leaves the payload letter. That is exactly the corruption this module exists to prevent, on the drain path that is its sole documented purpose.

Witness:

probe at HEAD (full capture pipeline):
decode('\x1b[[A') => 'A'
decode('a\x1b[[Ab\x1b[[Ec') => 'aAbEc'
F1..F5 interleaved => 'aAbBcCdDeEf'
flip: adding .replace(/\u001B\[\[[A-E]/g, '') before the CSI pass -> '', 'abc', 'abcdef'

Strip the double-bracket form whole, before the generic CSI rule (order matters — the generic regex matches '\x1b[[' by itself, so it must run second):

      // Linux-console double-bracket F1-F5 (ESC [ [ A..E): not valid ECMA-48;
      // strip whole before the generic CSI rule, which would match only ESC [ [
      // and leak the trailing letter.
      .replace(/\u001B\[\[[A-E]/g, '')
Suggested change
.replace(/\u001B\[[0-9:;<=>?]*[ -/]*[@-~]/g, '')
// Linux-console double-bracket F1-F5 (ESC [ [ A..E): not valid ECMA-48;
// strip whole before the generic CSI rule, which matches only ESC [ [
.replace(/\u001B\[\[[A-E]/g, '')
.replace(/\u001B\[[0-9:;<=>?]*[ -/]*[@-~]/g, '')

Please add expect(decodeCapturedInput(Buffer.from('a\u001B[[Ab\u001B[[Ec'))).toBe('abc') to early-input.test.ts — removing the new replace yields 'aAbEc', so the test goes red.

中文说明

R6-54:双括号功能键序列(ESC [ [ A..E,TERM=linux 的 F1-F5 编码)能通过捕获过滤,而通用 CSI 剥离只吃掉 ESC [ [——第二个 '[' 是 [@-~] 中的合法终止字节——导致尾部字母泄漏进输入框。Linux 虚拟控制台用户在启动期间按 F1-F5,会把 'A'-'E' 注入第一条提示词:classifyEscapeSequence 保留该序列(第三字节 '[' 不是 ?/>/<,判为用户输入),decodeCapturedInput 的 CSI 正则只匹配 '\x1b[['。这正是本模块要防止的损坏,且发生在它唯一职责所在的排空路径上。建议在通用 CSI 规则之前整体剥离双括号形式(顺序重要)。请补充上述测试并做变异验证。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +470 to +471
const display = renderResultDisplay(v.resultDisplay);
if (display)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R6-1: [certifies-falsely] [new-surface] The vision-bridge egress disclosure (visionBridgeNotice) is silently dropped whenever the tool response has no displayable result content — exactly the case core produces for cancelled-after-completion responses.

Core's createCancelledResponse hardcodes resultDisplay: undefined while deliberately preserving visionBridgeNotice ("bridge notices survive cancellation", coreToolScheduler.ts:959-984), and cancelAfterPostProcessing forwards it. In the mapper, renderResultDisplay(undefined) returns '', so the if (display) gate emits no tool-result — the notice rides only inside that event — and only tool-end {summary:'cancelled'} reaches the UI: the user is never told their image left the machine via the vision model. Ink renders the notice regardless of display content (ToolMessage.tsx:954-960), and the diff's own ink-parity comment states the notice should ride the card.

Witness:

probe at HEAD: mapper on {type:'tool_call_response', value:{callId:'c1',
executionStatus:'cancelled', visionBridgeNotice:'Converted 1 image via qwen3-vl-plus.'}}
-> [{"type":"tool-end","id":"c1","success":false,"summary":"cancelled"}]
r6_1_any_event_carries_notice: false
flip: `if (display || visionBridgeNotice)` -> tool-result carries the notice
Suggested change
const display = renderResultDisplay(v.resultDisplay);
if (display)
const display = renderResultDisplay(v.resultDisplay);
if (display || visionBridgeNotice)

Emit the result event when there is either display content or a notice — the display:'' shape is already used by the diff/todos/ansi branches. Please add a mapper test for a cancelled response carrying visionBridgeNotice and assert the emitted event carries it — removing the || visionBridgeNotice guard must make it red.

中文说明

R6-1:只要工具响应没有可显示的结果内容,视觉桥接出口披露(visionBridgeNotice)就会被静默丢弃——而这正是 core 在"完成后取消"场景产生的情况。core 的 createCancelledResponse 硬编码 resultDisplay: undefined 但刻意保留 visionBridgeNotice(coreToolScheduler.ts:959-984 注释:桥接通知在取消后保留);映射器中 renderResultDisplay(undefined) 返回 '',if (display) 门控不再发出 tool-result,而通知只挂在该事件上——UI 只收到 tool-end {summary:'cancelled'},用户完全不知道自己的图片已经发送给视觉模型。ink 无论有无显示内容都会渲染该通知(ToolMessage.tsx),且本 diff 自己的 ink 对等注释也写明通知应随卡片显示。建议改为 if (display || visionBridgeNotice)。请补充取消响应携带 visionBridgeNotice 的映射测试,并做变异验证。

— qwen3.8-max via Qwen Code /review (v0.22.3)

}`,
);
} else {
lines.push(` - ${skill.name}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R6-2: [certifies-falsely] [new-surface] projectSkillsList drops the skill level suffix when a skill has no description, breaking parity with ink's SkillsList — the transcript silently loses the skill's scope (Bundled/Project/User/Extension) exactly for description-less skills.

Ink renders the level independently of the description: SkillsList.tsx applies {skill.level && ...levelLabel...} outside the description conditional, and skillsCommand.ts:101-102 appends the level regardless of description. Here the else branch pushes only - ${skill.name}, so projectSkillsList([{name:'x', level:'bundled'}]) projects ' - x' where ink renders '- x (Bundled)'. description is optional in SkillDefinition, and no projectSkillsList test exists to catch this.

Witness:

probe at HEAD:
projectSkillsList([{name:'x', level:'bundled'}]) -> "Available skills:\n\n - x"
control (with description)                      -> "... - x d (Bundled)"
scratch-tree fix                                -> "Available skills:\n\n - x (Bundled)"

Append the same suffix in the else branch. Ink renders the bare name unmodified in the no-description case (no truncate/padEnd — SkillsList.tsx:40-44), so the fix must not pad or truncate there.

Suggested change
lines.push(` - ${skill.name}`);
lines.push(
` - ${skill.name}${skill.level ? ` (${levelLabel(skill.level)})` : ''}`,
);

Please add a projectSkillsList case asserting the output for [{name:'x', level:'bundled'}] contains '(Bundled)' — removing the suffix from the else branch must make it red.

中文说明

R6-2:projectSkillsList 在技能没有描述时丢弃技能级别后缀,与 ink 的 SkillsList 不一致——恰好对无描述技能,转录文本静默丢失技能作用域(Bundled/Project/User/Extension)。ink 的级别渲染独立于描述条件(SkillsList.tsx 在描述条件之外渲染 {skill.level && ...},skillsCommand.ts:101-102 同样无条件追加级别);此处 else 分支只 push - ${skill.name},于是 projectSkillsList([{name:'x', level:'bundled'}]) 输出 ' - x',而 ink 渲染 '- x (Bundled)'。SkillDefinition 中 description 为可选,且不存在 projectSkillsList 测试。建议在 else 分支追加相同后缀;注意 ink 在无描述时原样输出名称(不截断/补齐)。请补充测试并做变异验证。

— qwen3.8-max via Qwen Code /review (v0.22.3)

let ci = 0;
for (let x = 0; x < grid.width; x++) {
const codePoint = chars[base + x];
cells[x] = isContinuationCell(codePoint) ? '' : (lineChars[ci++] ?? ' ');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R5-2: [certifies-falsely] [new-surface] The native-grid decode consumes exactly one code point per non-continuation cell; a cell that resolves to a multi-code-point grapheme cluster (combining mark, ZWJ/skin-tone emoji) shifts every subsequent cell's character and cellColumns entry — corrupting hit-test mapping and truncating row text.

Verified against the real library this round: @opentui/core 0.5.8 stores 'Café' (é as e+U+0301) with the cluster in ONE cell (char flag 800200fe) while getRealCharBytes resolves it as TWO code points (bytes 65 cc 81). Feeding that real grid through this decoder shifts cellColumns right by the surplus code points and drops trailing code points — the URL truncates and every click maps one cell off.

Witness:

probe against real OptimizedBuffer via bun:ffi (all three widthMethods):
drawText('Cafe\u0301 https://a.dev') -> é in one cell, getRealCharBytes = 2 code points
PR-DECODE row0: findUrlAtRow(5) = null   findUrlAtRow(6) = "https://a.dev"
                (column 5 is the true display column of 'h')
full-width variant: row.text = "Café https://a.de" (final 'v' lost)

Consume one grapheme cluster per non-continuation cell instead of one code point — e.g. segment each realLine with new Intl.Segmenter(undefined, {granularity: 'grapheme'}) and advance the cell cursor by cluster, or verify and document @opentui/core's per-cell invariant at this seam if the library guarantees one cluster per cell in another shape. The shipped libopentui.so exposes CHAR_FLAG_GRAPHEME/drawGrapheme/GraphemeTracker but link-click.ts handles only CHAR_FLAG_CONTINUATION.

Please add a native-grid test whose getRealCharBytes returns 'e\u0301x' for two non-continuation cells asserting cellColumns 0,0,1 — it is red today (returns 0,1,2) — and confirm the mutation.

中文说明

R5-2(仍成立):原生网格解码对每个非续接单元恰好消费一个码点;若单元解析为多码点字位簇(组合标记、ZWJ/肤色 emoji),其后所有单元的字符与 cellColumns 条目整体右移——命中映射被破坏、行尾码点被截断。本轮用真实库验证:@opentui/core 0.5.8 把 'Café'(é = e+U+0301)存为一个单元(char flag 800200fe),但 getRealCharBytes 解析出两个码点。把真实网格喂给该解码器,cellColumns 右移、行尾码点丢失('a.dev' 截成 'a.de'),点击整体偏一格。建议按字位簇消费(Intl.Segmenter granularity:'grapheme'),或在该接缝验证并记录 @opentui/core 的每单元不变量(libopentui.so 暴露 CHAR_FLAG_GRAPHEME/drawGrapheme/GraphemeTracker,但 link-click.ts 只处理 CHAR_FLAG_CONTINUATION)。请补充原生网格测试(getRealCharBytes 返回 'e\u0301x' 两个非续接单元,断言 cellColumns 0,0,1——当前为红),并做变异验证。

— qwen3.8-max via Qwen Code /review (v0.22.3)

- abort race: already-aborted signal now skips command.action entirely
  (result = undefined) instead of eagerly evaluating it as a Promise.race
  argument — the action's side effects (clear, persist, addItem) must not
  run on a cancelled submission
- parent command telemetry: logEvent (slash_command SUCCESS) is now
  called before the early return for parent commands with subCommands
  (help listing) and bare handled — matching ink's finally-block logging
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants