Repository navigation
feat(web-shell): present structured shell execution results - #12311
Conversation
E2E report / 真机验证报告Real local macOS daemon and real shell processes, captured on 2026-09-20 during implementation; screenshots are linked in the PR body. No mocked daemon was used. These captures precede the final hook compatibility fix and are not claimed as a fresh final-commit E2E run.
Limits: the initial success command was submitted from a separate real client and required browser reload, so it does not prove automatic live completion. Reconnection navigated home and required reopening the session. Intermediate output of the interrupted run was not recorded. Its restored step summary showed 3 seconds rather than the observed approximately 180 seconds, so that summary is not elapsed-time evidence. Existing export-template artifacts were reused for CLI startup because the full export renderer build exceeds its budget. A new screenshot attempt during PR creation was blocked by browser navigation policy. Final hook fix verification: a 40,022-character successful stdout retains its middle sentinel; PostToolUse receives all 40,103 display characters including the saved-output notice. History/recording still cap each text field at 32,000 characters and ACP rawOutput remains within 65,536 UTF-8 JSON bytes. Four focused checks passed using actual producer/handler code with mocked execution service, filesystem and hook runner; this is not an OS hook E2E test. The 599-test core suite passed. Workspace typechecks passed; full build and root integration typecheck retain the blockers listed in the PR. 中文2026-09-20 实现过程中使用 macOS 本地真实 daemon 和 Shell 进程验证,截图见 PR 正文,没有模拟 daemon。图片采集早于最终 hook 兼容修复,不代表最终提交的新一轮真机 E2E。
限制:最初成功命令由另一个真实客户端提交,浏览器需要刷新,因此不能证明自动实时完成。重连曾回到首页,需要重新打开会话。被中断命令的中间输出没有录制;恢复后的步骤摘要显示 3 秒,与约 180 秒实际观察不符,不能作为耗时证据。因完整导出渲染包超限,启动 CLI 时复用了已有导出模板产物。本次创建 PR 时的新截图尝试被浏览器导航策略阻塞。 最终 hook 修复验证:40,022 字符成功 stdout 的中间标记完整保留,PostToolUse 收到包含文件提示的全部 40,103 字符展示文本。历史/录制文本字段仍限制为 32,000 字符,ACP rawOutput 仍不超过 65,536 UTF-8 JSON 字节。4 项针对性检查使用真实生产端/handler,底层执行服务、文件系统和 hook runner 使用 mock,不是操作系统 hook E2E。599 项 core 测试通过;各 workspace 类型检查通过;完整构建及根集成类型检查仍有 PR 中列明的阻塞。 Update: rebased onto main as f492336; PR now contains only the 55 feature files and is mergeable. Reran 599 core + 132 Web + 38 ACP tests: all 769 passed. The independent 4-check hook verification and full-build/typecheck results above were collected before this rebase; they were not rerun. 更新:已迁移到 main,提交 f492336;PR 仅包含 55 个功能文件,无合并冲突。重新执行 599 项 core、132 项 Web 和 38 项 ACP 测试,合计 769 项通过。上面的 4 项独立 hook 验证及完整构建/类型检查结果采集于此次 rebase 前,未重新执行。 |
c874b5e to
f492336
Compare
|
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)为单个提交。 |
Maintainer verification — real local environment, PR #12311Verified Rig — everything is real except the model. Two production daemons, each serving its own built VerdictThe feature works as described on a real daemon, and I found no correctness regression in any consumer I could exercise. Two statements in the PR description are stale and should be corrected, one CI job is red for a reason that is not this PR, and I have one Minor finding worth a decision before merge (exported documents), plus four nits. 1. The red
|
| arm | result |
|---|---|
main be9ed41 |
1 failed / 9,120 passed (9,121) — live/messages.test.ts |
| PR f492336 | 2 failed / 9,142 passed (9,144) — same one, plus BranchPickerPopover.test.tsx |
BranchPickerPopover.test.tsx passes in isolation on both arms (141/141) and CI's run of this PR reported only the one failure, so it is full-suite load flake, not PR-induced. This PR adds no new test failures; main needs a separate fix (drop t('live.open') from AddMenu.tsx, or move the key out of the live. namespace).
2. Two stale claims in the PR description
| Claim in the description | Measured here |
|---|---|
| "Full build remains blocked by the export renderer budget (1,989,901 bytes versus 1,930,000)" | Root npm run build exits 0. Document export renderer JS = 1,926,710 B (main: 1,916,801 B, so this PR adds 9,909 B) against warning 1,970,000 and cap 2,030,000 — #12295 raised the cap and #12305 shrank the bundle. Lint & Static is green on this PR, which runs the same gate. |
| "Root integration typecheck still reports the existing process-registry string/buffer mismatch and a missing generated export template" | npm run typecheck (all workspaces + typecheck:integration) exits 0, zero error TS. |
Please update the description — as written it reads as if the PR is build-blocked, which it is not.
3. Reviewer Test Plan — all five steps, real daemon
| # | Step | Result |
|---|---|---|
| 1 | Foreground command that waits, then prints and succeeds | Pass. Running + live elapsed (6s → 14s), Command and Output expanded by default, Timeout / Use default in details, then Succeeded. Waiting for output… until completion, as documented. |
| 2 | stderr + exit 7 vs grep exit 1; empty output, cancel, timeout |
Pass. exit 7 → Exited with code 7, stderr in Output; grep exit 1 → Completed with Exit code 1 in details (negative result, not a failure); true → Succeeded / No output; cancel mid-run → Cancelled + partial output + Signal: 15; timeout → Timed out + red error line + partial output. No invented success anywhere. |
| 3 | Copy command and output independently; expand long content | Pass. Two icon-only buttons (Copy command, Copy output), clipboard read back byte-exact including embedded newlines. Content panes measured max-height: 200px; overflow-y: auto, scrollHeight 11,086 vs clientHeight 200. At 390×844 documentElement.scrollWidth == clientWidth == 390 (no horizontal page scroll) and cards are full width. |
| 4 | Reopen after daemon restart; interrupted execution | Pass. Killed and restarted the daemon on the same QWEN_HOME: all six cards re-rendered with identical text. A run interrupted before its result was recorded shows Failed + "Tool result missing from saved history; the previous run likely ended before this tool completed" — no fabricated result. |
| 5 | >32,000-char successful output | Pass. 204,000-char stdout: PostToolUse received the full 208,252-char display string (main: 208,254; the 2-byte delta is only the QWEN_HOME path inside the notice). ACP bounded it to 31,896 chars with [... truncated for ACP transport ...] spliced in the middle; card shows Output preview truncated and the persisted .output file under Output files. |
4. Compatibility A/B against main (the part I would most want proven)
| Surface | Method | Result |
|---|---|---|
| Model-facing ACP content | 12/12 tool_call / tool_call_update events from GET /session/:id/transcript, normalized for PGID/paths/uuids/timestamps |
Identical. Only rawOutput changes: string → {"type":"shell_result","version":1,...}. |
PostToolUse |
real hook script dumping stdin, 6 scenarios, both arms | tool_response.returnDisplay is a string on both arms, equal modulo PGID/path. |
PostToolBatch |
same | tool_calls[].tool_response.result_display is a string on both arms, equal modulo PGID. |
PostToolUseFailure |
same | Unchanged shape, error string equal modulo PGID. |
Non-interactive -o stream-json |
4 scenarios, both arms | tool_result blocks byte-identical (PGID only). |
| Terminal (Ink TUI) | real pty, 120×40, 5 scenarios, both arms, final frame diffed | Identical rendering. No [object Object]. |
5. Findings
F1 — Minor — exported HTML documents get a half-populated card. sanitizeResultPreview maps shell_result → { kind: 'text', text }, so the exported envelope carries no structured fields and ShellToolOutput runs with result === null. Measured on a real export rendered with this build's renderer:
Execution detailscontains onlyTimeout / Use default— no exit code, directory, signal, notices or output files.- A failed run puts the whole
Command: / Directory: / Output: / Error: / Exit Code: / Signal: / Process Group PGID:envelope inside the Output pane — re-creating exactly the output/metadata mixing the card removes in the live UI. - The same run is labelled
Succeededlive butCompletedin the export, becauseresult?.exitCode === 0cannot be evaluated.
docs/design/structured-shell-results.md does say the v1 document schema "uses its sanitized text fallback", so the mechanism is intentional — but the visible result is a card that looks empty/inconsistent. Either carry the structured result through the document schema, or suppress the Execution details disclosure (and the failure-envelope Output) when result is null. See figure 6.
F2 — Minor — directory changed meaning and the (root) branch is dead. shell.ts sets directory: cwd (resolved absolute path), but ShellToolOutput renders result.directory === '(root)' ? t('shell.result.defaultDirectory') : result.directory. The producer never emits '(root)' — that is the legacy text label, built from this.params.directory || '(root)' at shell.ts:2835 — so shell.result.defaultDirectory is unreachable and the card always shows an absolute host path where main showed Directory: (root) or the requested relative subdirectory. Verified in the transcript: "directory":"/private/tmp/.../rig/ws". Either emit the requested-directory semantics, or drop the dead comparison.
F3 — Minor — Exit code 0 is shown for timed-out and cancelled runs. Measured: timeout → {outcome:'timed_out', exitCode:0, signal:15}; user cancel → {outcome:'cancelled', exitCode:0, signal:15}. The card then prints Exit code 0 in Execution details right next to Timed out / Cancelled. main never surfaced an exit code in either case (its display text was Command timed out after 2000ms… / Command cancelled by user.), so this is newly misleading. Suggest suppressing Exit code when signal is set or outcome is timed_out / cancelled. See figure 7.
F4 — Nit — three test-coverage gaps, found by mutation. 19 mutants over the new logic, 15 killed. Survivors:
| Mutation | Survives because |
|---|---|
directory: cwd → directory: '' |
the toEqual assertion uses expect.any(String) |
outputFiles: persistedOutputFiles ?? [] → [] |
the only assertion expects [] |
drop !wasPromoteRefused from the cancelled branch |
promote-refused is never exercised; it would be labelled Cancelled |
drop result?.outcome === 'timed_out' from failed in ShellToolOutput |
equivalent — redundant with tool.status === 'failed' and result.error; no action needed |
Both directory and outputFiles are correct in the live rig, so these are pinning gaps rather than defects — but they are exactly the two fields F2 is about.
F5 — Nit — container-execution-environment.ts:702. The "Container cleanup failed after tool execution" notice is still appended only if (typeof result.returnDisplay === 'string'), so with a structured shell result it is now silently dropped from the display (it survives in llmContent and error.message). Code read only — I did not exercise a container runtime.
F6 — Nit — small cleanups. shell.result.elapsed is added to both locales but referenced nowhere (dead weight in a byte-budgeted renderer); transcriptToMessages.ts re-implements isShellToolName as an inline regex in two places instead of importing the existing helper (same four names today, easy to drift); the error <pre> (styles.shellFailure) has no max-height/scroll unlike Command and Output (bounded in practice by the ACP budget, so cosmetic).
6. Test runs on this head
| Suite | Result |
|---|---|
core: shell + shell-result + hookEventHandler + coreToolScheduler + toolResultDisplayCompaction |
1,026 passed |
cli: acp projection, ToolMessage, daemon-tui-adapter, event-adapter, export normalize/export-transcript-document, doctorCommand |
347 passed |
web-shell: ShellToolOutput, ToolGroup, transcriptToMessages |
348 passed |
sdk-typescript daemonUi / acp-bridge transcript-replay |
388 / 71 passed |
npm run typecheck (all workspaces + integration) |
exit 0 |
root npm run build |
exit 0 |
| i18n | all 26 new shell.result.* keys present in both en and zh-CN |
7. Evidence
All captures are from the real daemons above; the "before" images come from the same rig running origin/main.
Successful command — before / after

Running command — before / after. main renders the raw tool input JSON as the card body; the PR suppresses it and shows Running with live elapsed time.

Long-run notice — before / after. This is the clearest demonstration of the PR's premise: process metadata leaves the stdout blob.

Outcomes: exit 7, grep exit 1, timeout, cancellation

Truncation and restart recovery

F1 — exported HTML transcript, before / after

F3 — Exit code 0 on a timed-out run

Narrow viewport and copy feedback

8. Not verified
Windows and Linux (macOS only). Live PTY ansiOutput streaming into the card — the current ACP path does not deliver ordinary intermediate stdout, so parseShellLiveOutput was never reached in this rig. Container execution environment (F5). The model was a local scripted stub, so nothing here exercises real model behaviour. Real-world rendering of an exported document still pins the renderer to the published 0.24.1 asset on unpkg; I rendered the export against this build's renderer instead.
Recommendation
Merge-ready on correctness, with a decision needed on F1 (exported documents) and, ideally, one-line fixes for F2 and F3. The red Test (ubuntu-latest) job should not block this PR — it needs a separate fix on main. Please also refresh the "build blocked" and "typecheck fails" paragraphs in the description.
中文说明
维护者真机验证报告 — PR #12311
在本地搭建真实环境验证了 f492336b26(基线 be9ed41afd,未落后 main)。环境:macOS 15 / Node 24.18.1,两个全新工作树,各自 pnpm install --frozen-lockfile + 根 npm run build。
装置说明 —— 除模型外全部为真。 两个生产 daemon,各自托管自己构建出的 packages/web-shell/dist(不是 vite dev,也不是 mock daemon):node packages/cli/dist/index.js serve --port 5311|5312 --workspace <ws> --web,各带独立 QWEN_HOME。唯一的桩是 127.0.0.1:5399 上的本地 OpenAI 兼容服务,用于返回脚本化的 run_shell_command 调用,以保证场景确定。Shell 进程、ACP 子进程、SSE 传输、会话录制、导出链路和浏览器(Playwright/Chromium 直连 daemon 源站)都是真实的。A 臂 = 本 PR,B 臂 = origin/main,同样的命令、同样的脚本调用。
结论
功能在真实 daemon 上与描述一致,在我能实测到的所有消费者上未发现正确性回归。但 PR 描述中有两处说法已过期;一个 CI 任务为红,原因不在本 PR;另有一个建议合并前拍板的 Minor 问题(导出文档),以及四个小问题。
1. Test (ubuntu-latest) 变红是从 main 继承来的
packages/web-shell 的 client/live/messages.test.ts > "are defined and used nowhere outside client/live" 失败:
AssertionError: expected [ 'components/composer/AddMenu.tsx' ] to deeply equal []
我在 origin/main(be9ed41afd)上逐字复现了同样的报错。AddMenu.tsx:684 使用了 t('live.open')(来自 #12252),而这道守卫测试晚一个提交才由 #12305 合入,所以 main 目前本身就是红的。同一台机器上前后跑完整 packages/web-shell 套件:
| 臂 | 结果 |
|---|---|
main be9ed41 |
1 失败 / 9,120 通过(9,121) —— live/messages.test.ts |
| PR f492336 | 2 失败 / 9,142 通过(9,144) —— 同上,外加 BranchPickerPopover.test.tsx |
BranchPickerPopover.test.tsx 单独跑在两臂都通过(141/141),并且 CI 上本 PR 也只报了前一个失败,所以那是全量并发下的抖动,不是 PR 引入的。本 PR 没有新增任何测试失败;需要在 main 上单独修(把 t('live.open') 从 AddMenu.tsx 移走,或让该 key 离开 live. 命名空间)。
2. PR 描述中的两处过期说法
| 描述中的说法 | 实测 |
|---|---|
| 「完整构建仍被导出渲染包体积限制阻塞(1,989,901 字节,限额 1,930,000)」 | 根 npm run build 退出码 0。导出渲染器 JS = 1,926,710 字节(main 为 1,916,801,本 PR 增加 9,909),告警线 1,970,000、硬上限 2,030,000 —— #12295 已抬高上限,#12305 又缩小了包体。本 PR 的 Lint & Static 为绿,它跑的就是同一道门。 |
| 「根集成类型检查仍有既存的 process-registry 字符串/Buffer 类型不匹配及缺失生成模板错误」 | npm run typecheck(全部 workspace 加上 typecheck:integration)退出码 0,error TS 计数为 0。 |
建议更新描述 —— 按现在的写法,读者会以为这个 PR 被构建阻塞,实际并没有。
3. 评审验证计划五步,真实 daemon 逐条实测
| # | 步骤 | 结果 |
|---|---|---|
| 1 | 先等待、再输出并成功结束的前台命令 | 通过。Running + 实时耗时(6s → 14s),命令与输出默认展开,详情里是 Timeout / Use default,结束后 Succeeded。结束前显示 Waiting for output…,与文档所述一致。 |
| 2 | stderr + 退出 7 对比 grep 退出 1;空输出、取消、超时 |
通过。退出 7 → Exited with code 7,stderr 落在 Output;grep 退出 1 → Completed,详情里保留 Exit code 1(已完成但结果为否,不是执行失败);true → Succeeded / No output;运行中取消 → Cancelled + 部分输出 + Signal: 15;超时 → Timed out + 红色错误行 + 部分输出。没有任何一处虚构成功。 |
| 3 | 分别复制命令与输出;展开长内容 | 通过。两个纯图标按钮(Copy command、Copy output),剪贴板读回逐字节一致,含换行。内容区实测 max-height: 200px; overflow-y: auto,scrollHeight 11,086 对 clientHeight 200。390×844 下 documentElement.scrollWidth == clientWidth == 390(无横向滚动),卡片占满宽度。 |
| 4 | 重启 daemon 后重新打开;中断的执行 | 通过。用同一个 QWEN_HOME 杀掉并重启 daemon:六张卡片文本完全一致。结果尚未记录就被中断的执行显示 Failed + 「Tool result missing from saved history; the previous run likely ended before this tool completed」,没有虚构结果。 |
| 5 | 超过 32,000 字符的成功输出 | 通过。204,000 字符 stdout:PostToolUse 收到完整的 208,252 字符展示串(main 为 208,254,2 字节之差只是通知里的 QWEN_HOME 路径长度)。ACP 侧限到 31,896 字符,中部插入 [... truncated for ACP transport ...];卡片显示 Output preview truncated,落盘的 .output 文件出现在 Output files。 |
4. 与 main 的兼容性 A/B(我最想证明的部分)
| 面 | 方法 | 结果 |
|---|---|---|
| 面向模型的 ACP 内容 | GET /session/:id/transcript 的 12/12 个 tool_call / tool_call_update 事件,归一化 PGID/路径/uuid/时间戳后比对 |
完全一致。只有 rawOutput 从字符串变成 {"type":"shell_result","version":1,...}。 |
PostToolUse |
真实 hook 脚本转存 stdin,6 个场景,两臂 | 两臂的 tool_response.returnDisplay 都是字符串,除 PGID/路径外完全相同。 |
PostToolBatch |
同上 | 两臂的 tool_calls[].tool_response.result_display 都是字符串,除 PGID 外相同。 |
PostToolUseFailure |
同上 | 结构未变,error 字符串除 PGID 外相同。 |
非交互 -o stream-json |
4 个场景,两臂 | tool_result 块逐字节相同(仅 PGID 不同)。 |
| 终端(Ink TUI) | 真实 pty,120×40,5 个场景,两臂,比对最终帧 | 渲染完全一致,没有 [object Object]。 |
5. 发现
F1 — Minor — 导出的 HTML 文档里卡片只填了一半。 sanitizeResultPreview 把 shell_result 映射成 { kind: 'text', text },导出信封里没有结构化字段,于是 ShellToolOutput 在 result === null 下渲染。用本次构建的渲染器渲染真实导出后实测:
Execution details里只有Timeout / Use default—— 没有退出码、目录、信号、通知和输出文件。- 失败的运行会把整段
Command: / Directory: / Output: / Error: / Exit Code: / Signal: / Process Group PGID:外壳塞进 Output 区 —— 正好把卡片在实时界面里已经消除的「输出与元数据混杂」又复原了。 - 同一次运行在实时界面是
Succeeded,在导出里变成Completed,因为无法判断result?.exitCode === 0。
docs/design/structured-shell-results.md 确实写了 v1 文档 schema「使用其清洗后的文本回退」,所以机制是有意为之 —— 但呈现出来就是一张看起来空掉、且与实时界面不一致的卡片。建议要么让结构化结果进入文档 schema,要么在 result 为空时不渲染 Execution details(以及失败态的外壳 Output)。见图 6。
F2 — Minor — directory 语义变了,且 (root) 分支是死代码。 shell.ts 写的是 directory: cwd(解析后的绝对路径),而 ShellToolOutput 渲染的是 result.directory === '(root)' ? t('shell.result.defaultDirectory') : result.directory。生产端永远不会给出 '(root)' —— 那是旧文本的标签,由 shell.ts:2835 的 this.params.directory || '(root)' 生成 —— 所以 shell.result.defaultDirectory 不可达,卡片总是显示宿主机绝对路径,而 main 显示的是 Directory: (root) 或请求的相对子目录。transcript 中实测为 "directory":"/private/tmp/.../rig/ws"。建议要么补上请求目录的语义,要么删掉这个死比较。
F3 — Minor — 超时和取消的运行会显示 Exit code 0。 实测:超时 → {outcome:'timed_out', exitCode:0, signal:15};用户取消 → {outcome:'cancelled', exitCode:0, signal:15}。卡片于是在 Timed out / Cancelled 旁边的 Execution details 里打出 Exit code 0。main 在这两种情况下从不展示退出码(其展示文本分别是 Command timed out after 2000ms… 和 Command cancelled by user.),所以这是新引入的误导。建议在 signal 非空或 outcome 为 timed_out / cancelled 时不显示 Exit code。见图 7。
F4 — Nit — 变异测试发现三处测试未钉住。 对新增逻辑做了 19 个变异,杀掉 15 个。存活的:
| 变异 | 存活原因 |
|---|---|
directory: cwd → directory: '' |
toEqual 断言用的是 expect.any(String) |
outputFiles: persistedOutputFiles ?? [] → [] |
唯一的断言期望就是 [] |
去掉 cancelled 分支里的 !wasPromoteRefused |
promote-refused 这条路径没有用例,它会被标成 Cancelled |
去掉 ShellToolOutput 中 failed 的 result?.outcome === 'timed_out' |
等价变异 —— 与 tool.status === 'failed'、result.error 冗余,无需处理 |
directory 和 outputFiles 在真机上都是正确的,所以这是钉不住而非缺陷 —— 但恰好就是 F2 涉及的那两个字段。
F5 — Nit — container-execution-environment.ts:702。 「Container cleanup failed after tool execution」这条通知仍然只在 typeof result.returnDisplay === 'string' 时追加,因此结构化 shell 结果下它会从展示中被静默丢掉(在 llmContent 和 error.message 里仍然保留)。仅为读代码所得,我没有跑容器运行时。
F6 — Nit — 零碎清理。 shell.result.elapsed 两种语言都加了但没有任何引用(在有字节预算的渲染器里是白白占体积);transcriptToMessages.ts 用内联正则重复实现了两处 isShellToolName,而不是直接引用已有的 helper(今天四个名字一致,但容易漂移);错误块的 <pre>(styles.shellFailure)没有像命令和输出那样的 max-height / 滚动(实际受 ACP 限额约束,属于观感问题)。
6. 本 head 上的测试运行
| 套件 | 结果 |
|---|---|
core:shell + shell-result + hookEventHandler + coreToolScheduler + toolResultDisplayCompaction |
1,026 通过 |
cli:ACP 投影、ToolMessage、daemon-tui-adapter、event-adapter、导出 normalize / export-transcript-document、doctorCommand |
347 通过 |
web-shell:ShellToolOutput、ToolGroup、transcriptToMessages |
348 通过 |
sdk-typescript daemonUi / acp-bridge transcript-replay |
388 / 71 通过 |
npm run typecheck(全 workspace + 集成) |
退出码 0 |
根 npm run build |
退出码 0 |
| i18n | 新增的 26 个 shell.result.* key 在 en 与 zh-CN 中都齐全 |
7. 证据
所有截图都来自上述真实 daemon;「修改前」的图来自同一装置运行 origin/main。图片依次为:成功命令前后对比、运行中前后对比(main 把工具入参 JSON 当作卡片正文渲染,PR 将其屏蔽并显示 Running 与实时耗时)、长耗时通知前后对比(最能体现本 PR 的价值:进程元数据离开了 stdout 区块)、四种结局(退出 7 / grep 退出 1 / 超时 / 取消)、截断与重启恢复、F1 的导出文档前后对比、F3 的超时 Exit code 0、窄屏与复制反馈。图片见上方英文部分。
8. 未验证
Windows 与 Linux(仅 macOS)。PTY 实时 ansiOutput 流入卡片 —— 当前 ACP 链路不投递普通中间 stdout,本装置中 parseShellLiveOutput 始终未被触达。容器执行环境(F5)。模型是本地脚本化的桩,因此本报告不涉及真实模型行为。真实世界中导出文档仍会去 unpkg 拉取已发布的 0.24.1 渲染器;我是用本次构建的渲染器渲染的导出件。
建议
正确性上已可合并,但建议先就 F1(导出文档)拍板,并顺手修掉 F2 与 F3(都是一行改动)。红色的 Test (ubuntu-latest) 不应阻塞本 PR —— 那需要在 main 上单独修。另请更新描述中「构建被阻塞」和「类型检查失败」两段。
|
Review follow-up in
Validation: the new assertions first failed on the old implementation (4 UI + 1 container case), then passed. The complete affected runs passed 385 core + 136 Web = 521 tests, plus 5 independent focused checks. Formatting, lint and whitespace checks passed. Two post-fix source/test/call-path audit passes found no further issue in this correction. Root build now exits 0; all workspace typechecks pass. On local Node 22.14.0, root integration typecheck still fails at The PR body includes a new browser screenshot of fixed component fixtures, explicitly marked as fixed test data, not a daemon or real shell execution. Existing real-daemon screenshots remain labelled separately. Images are in the fork's independent asset branch, not the implementation branch. Known deferred compatibility difference: structured ACP text and output share 64KB. Daemon CLI long display text may truncate earlier than the former string payload (40,021 characters became 32,667 in the targeted projection check). Normal Ink rendering parity does not cover this boundary. We retain the existing transport limit in this PR and record CLI long-output budget compatibility as a follow-up, rather than claiming exact parity. 中文:追加提交 新增断言先在旧实现上失败(4 项 UI、1 项容器),修复后通过;完整相关测试 385 core + 136 Web = 521 项,另有 5 项独立验证通过。格式、Lint、空白检查通过;两轮修复后源码/测试/调用链复查未发现本次修正的新问题。容器验证使用 mock worker,没有声称真机容器测试。 完整构建已通过,各 workspace 类型检查通过。本机 Node 22.14.0 的根集成类型检查仍报 明确后续项:结构化 ACP 的 text/output 共用 64KB,daemon CLI 长文本可能提前截断(定向检查中 40,021 字符变为 32,667)。普通 Ink 显示一致性不覆盖此边界。本 PR 保留当前传输限额,将 CLI 长输出预算兼容记录为后续工作,不声称完全等价。 |
|
Fixed the two failing scripts tests from the Ubuntu Node 22 test job in 6c2ee93. The new Validation: reproduced the two failures before the fix; the same two test files now pass all 4 tests. Prettier, ESLint, diff check, and full build pass. All workspace typechecks pass. Local integration typecheck still reports the previously documented 已修复本轮 Ubuntu Node 22 Test 中的两项失败:新增 shellResult 导出时漏同步集成测试源码别名和 review capture loader 映射。仅补齐 4 行映射。修复前复现失败,修复后相同测试 4 项全部通过;格式、lint、构建通过。workspace 类型检查全部通过,本地集成类型检查仍存在此前记录的 process-registry.ts:773 类型错误。新提交的远端 CI 待确认。 |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
|
@qwen-code /resolve |
Both sides added a distinct `paths` entry at the same spot in integration-tests/tsconfig.json: this branch mapped `@qwen-code/qwen-code-core/shellResult` and main's QwenLM#12304 mapped `@qwen-code/qwen-code-core/telemetryConstants`. Keep both; the map has no ordering constraint. Every other overlapping file auto-merged as an exact union.
|
Qwen Code resolved the merge conflicts and pushed the branch update. Merge resolution — PR #12311 ← mainMerge commit Root cause. Main's Textual, not semantic — adjacent insertions, no shared logic. Union (both targets exist; JSONC parses): "@qwen-code/qwen-code-core/shellResult": ["../packages/core/src/utils/shell-result.ts"],
"@qwen-code/qwen-code-core/telemetryConstants": ["../packages/core/src/telemetry/constants.ts"],Load-bearing.
Not verified — no build/typecheck/lint/tests run. All three sit in NON-conflicted files, so any break is CI's:
中文说明合并提交 根因:main 的 文本冲突,非语义冲突:仅相邻、无共同逻辑,取并集(见上)。 关键约束:
未能验证:未运行构建/类型检查/lint/测试。三点均在未冲突文件,问题须由 CI 暴露:
|
Maintainer re-verification (round 2) — PR #12311 @
|
| # | Fix | Verified how | Result |
|---|---|---|---|
| F1 | document exports drop the card and render the intact text fallback | real daemon GET /session/:id/export, rendered offline with this build's export-transcript-document.js |
Fixed. No more empty Execution details, no Completed/Succeeded mismatch, no metadata envelope stuffed into an Output section. |
| F2 | (root) comparison removed, absolute directory kept |
live card, details expanded | Fixed. Directory renders the resolved absolute path; shell.result.defaultDirectory is gone from both locales. |
| F3 | exit code hidden for cancel / timeout / signal | live cards, all three states | Fixed. Timeout → Timeout / Directory / PGID (no Exit code); SIGTERM → same; user cancel → same. Signal: 15 is still shown in all three. |
| F4 | the three surviving mutants | re-ran them on this head | All three killed now (directory: '', outputFiles: [], dropping !wasPromoteRefused) — 780 core tests, 1 failure each. |
| F5 | container cleanup notice reaches structured results | code read + mutation | Fixed at unit level. Removing the new else if (isShellResultDisplay(...)) branch fails the container boundary suite. I did not run a real container runtime, same caveat as yours. |
I also mutated the three new fixes to confirm the new tests actually pin them, rather than just passing:
| Mutation | Result |
|---|---|
disable the documentMode && !result early return |
1 failed / 136 |
relax the exit-code guard back to result?.exitCode != null |
3 failed / 136 |
| delete the container structured-notice branch | 1 failed / 46 |
Two small nits left over from the F1 fix (export only)
N1 — the command is printed twice in an exported document, three times on a failure. The new documentMode && !result branch renders <pre>{command}</pre> above the text fallback, but the collapsed row header already shows the command, and on a failure the fallback text itself opens with Command: …. docs/design/shell-command-card.md opens by naming command repetition as the problem this PR fixes, so it is worth closing the loop — dropping the extra <pre> (the header already carries it) or restoring getToolDescription(tool) there, as the base build showed, would do it.
N2 — a successful command with no output now exports as a card containing only the command. true → base exported Command: true / Directory: (root) / Output: (empty) / Exit Code: 0 / …; this head exports just true and nothing else. The live card correctly says No output; the export says nothing at all, so a reader cannot tell "ran and produced nothing" from "we lost the result". grep exit 1 is fine (Command exited with code: 1 still carries the status).
Neither blocks merge; both are one-line changes in the same branch you just added.
The deferred ACP-budget note is much smaller than stated
The description says "the structured ACP text/output fields share the 64KB budget, so daemon CLI long display text can be truncated earlier than the former string result (40,021 characters became 32,667 in the targeted projection check)". Measured end-to-end on the real daemon, same 204,000-char stdout, both arms:
| display text reaching ACP | ACP rawOutput frame |
|
|---|---|---|
base b85264be79 |
32,000 chars (plain string) | 32,613 B |
PR ac01af9d1d |
31,900 chars (text) + 31,896 chars (output) |
65,536 B |
So the display-text loss is 100 characters (0.3 %), not ~18 %: an upstream 32,000-char display cap already binds before the ACP budget does, and raising tools.truncateToolOutputThreshold to 200,000 changed neither arm. The real cost is the other half of that table — because text and output are near-duplicates for a successful command (text is the output plus the "saved to" line), the ACP frame for a large result doubles, from 32,613 B to exactly the full 65,536 B budget. That is worth a sentence in the description in place of the current one; if you ever want it back, giving output priority in allocatePayloadBudgets rather than an equal share would be the lever.
The process-registry.ts:773 typecheck failure is a local tree artifact
It does not reproduce here and it does not reproduce in CI:
npm run typecheck(all workspaces +typecheck:integration) on Node 24.18.1: exit 0, zeroerror TS.tsc -p integration-tests/tsconfig.jsonon Node 22.23.2: exit 0.- CI runs
npm run typecheck:integrationinsideIntegration Tests (no-AK, No Sandbox)on a Node 22.x runner — pass on this head.
Line 773 is return result.stdout in a string-returning helper. spawnSync(...).stdout only widens to NonSharedBuffer when @types/node ≥ 22 is resolved at that import; this lockfile pins @types/node 20.19.1 at the root (TypeScript 5.8.3), which is what CI and both of my trees resolve. Your tree is almost certainly hoisting a newer @types/node. Nothing to fix in the PR — I'd just drop the caveat from the description.
Regression sweep on this head
| Surface | Method | Result |
|---|---|---|
| Model-facing ACP content | 20 tool_call / tool_call_update events across two sessions, normalized |
20/20 identical. The one nominal diff is the per-workspace hash inside the persisted output-file path (ws vs ws-main). |
PostToolUse / PostToolBatch |
real hook script, 6 scenarios, both arms | 4/4 and 6/6 identical strings. Still typeof === 'string' on both arms. |
-o stream-json |
6 scenarios, both arms | 6/6 equivalent. SCEN-BIG differs only by a 2-character head/tail splice offset, caused by the 2-character difference in the isolated QWEN_HOME path names. |
| Terminal (Ink TUI) | real pty, 120×40, 6 scenarios, final frame diffed | 6/6 identical. |
| Daemon restart / interrupted run | kill + restart on the same QWEN_HOME |
Cards re-render identically; an interrupted run still shows the missing-result fallback. |
| Build | root npm run build, both arms |
exit 0 both. Export renderer JS 1,927,689 B (base 1,917,548, +10,141) vs warning 1,970,000 and cap 2,030,000. |
| Tests | core scoped (incl. the container boundary suite) 1,086 · cli scoped 347 · sdk 402 · acp-bridge 113 | all passed |
Full packages/web-shell suite |
both arms, same machine | PR 1 failed / 9,197 (BranchPickerPopover), base 1 failed / 9,170 (ChatEditor) — different files, both pass in isolation on both arms (341/341). Load flake, not PR-induced; CI's Test (ubuntu) is green on this head. |
The live/messages.test.ts failure I reported last round is gone: #12316 and #12319 fixed it on main, and every CI job on this head now passes except the still-running review-pr.
Verdict
The corrections are real, minimal and well pinned, and nothing else moved. From my side this is ready to merge; N1/N2 are cosmetic and export-only, and the two description sentences above are worth a touch-up. One housekeeping note: the branch is now 5 commits behind main (dfdbbace1f) again — no conflicts expected, just re-run the gate after the next merge.
中文说明
维护者复验(第 2 轮)—— PR #12311 @ ac01af9d1d
用同一套真机装置针对更新后的 head(ac01af9d1d,merge-base b85264be79;上一轮是 f492336b26 / be9ed41afd)重新验证。两个工作树都按 lockfile 重装并从零重建;A 臂 = 本 head,B 臂 = b85264be79(即本 PR 自己的 merge-base,保证 A/B 公平)。
上一轮的五个发现全部已修复,且都被测试钉住。 未发现任何修复引入的新回归。F1 的修复遗留两个仅影响导出文档的小问题;另外描述里那条「已知兼容性差异」的量级被高估了 —— 我实测到的数字远小于文中引用的。
上一轮发现的逐条复验
| # | 修复 | 验证方式 | 结果 |
|---|---|---|---|
| F1 | 导出文档去掉卡片、改为完整文本回退 | 真实 daemon 的 GET /session/:id/export,用本次构建的 export-transcript-document.js 离线渲染 |
已修复。 不再有空的 Execution details,不再有 Completed/Succeeded 不一致,也不再把元数据外壳塞进 Output 区。 |
| F2 | 去掉 (root) 比较,保留绝对目录 |
实时卡片,展开详情 | 已修复。 Directory 显示解析后的绝对路径;两种语言中的 shell.result.defaultDirectory 均已删除。 |
| F3 | 取消 / 超时 / 信号终止时隐藏退出码 | 实时卡片,三种状态全测 | 已修复。 超时 → Timeout / Directory / PGID(无 Exit code);SIGTERM 同;用户取消同。三者都仍保留 Signal: 15。 |
| F4 | 三个存活变异 | 在本 head 上重跑 | 三个全部被杀(directory: ''、outputFiles: []、去掉 !wasPromoteRefused)—— 780 项 core 测试,每个变异各挂 1 项。 |
| F5 | 容器清理通知进入结构化结果 | 读代码 + 变异 | 单元层面已修复。 删掉新增的 else if (isShellResultDisplay(...)) 分支会让容器边界套件变红。我没有跑真实容器运行时,与你的说明一致。 |
另外对三处新修复做了变异,确认新增测试是真的钉住了行为,而不只是碰巧通过:
| 变异 | 结果 |
|---|---|
关闭 documentMode && !result 提前返回 |
1 失败 / 136 |
把退出码守卫放宽回 result?.exitCode != null |
3 失败 / 136 |
| 删除容器结构化通知分支 | 1 失败 / 46 |
F1 修复遗留的两个小问题(仅影响导出)
N1 —— 导出文档里命令被打印两次,失败时三次。 新增的 documentMode && !result 分支在文本回退上方渲染了 <pre>{command}</pre>,但折叠行的表头本来就显示命令,失败时回退文本自身又以 Command: … 开头。docs/design/shell-command-card.md 开篇就把「重复命令」列为本 PR 要解决的问题,所以值得顺手闭环 —— 去掉那个多余的 <pre>(表头已经有了),或者像基线那样改回渲染 getToolDescription(tool),都可以。
N2 —— 成功但无输出的命令,导出后只剩一行命令。 true 在基线上导出的是 Command: true / Directory: (root) / Output: (empty) / Exit Code: 0 / …;本 head 只导出 true,再无其它。实时卡片正确显示 No output,而导出里什么都没有,读者无法区分「跑过且没有输出」和「结果丢了」。grep 退出 1 没问题(Command exited with code: 1 仍带出状态)。
两个都不阻塞合并,且都是你刚动过的同一个分支里的一行改动。
描述中那条「已知兼容性差异」的量级被高估了
描述写的是「结构化 ACP text/output 共享 64KB 预算,daemon CLI 的长展示文本可能比原先的字符串更早被截断(定向投影检查中 40,021 字符变成 32,667)」。在真实 daemon 上端到端实测,同样的 204,000 字符 stdout,两臂对比:
| 到达 ACP 的展示文本 | ACP rawOutput 帧大小 |
|
|---|---|---|
基线 b85264be79 |
32,000 字符(纯字符串) | 32,613 B |
PR ac01af9d1d |
31,900 字符(text)+ 31,896 字符(output) |
65,536 B |
也就是说展示文本只少了 100 个字符(0.3%),而不是约 18%:上游本来就有一道 32,000 字符的展示上限先生效,而且把 tools.truncateToolOutputThreshold 调到 200,000 后两臂都没有变化。真正的代价在表格的另一半 —— 成功命令下 text 与 output 近乎重复(text 就是输出再加一行「saved to」),于是大结果的 ACP 帧翻了一倍,从 32,613 B 涨到正好用满 65,536 B 预算。建议用这句话替换描述中现有的那句;若将来想把文本额度要回来,在 allocatePayloadBudgets 里让 output 优先于均分就是那个杠杆。
process-registry.ts:773 的类型错误是本地环境产物
它在我这里和 CI 里都复现不了:
- Node 24.18.1 上
npm run typecheck(全 workspace 加typecheck:integration):退出码 0,error TS计数为 0。 - Node 22.23.2 上
tsc -p integration-tests/tsconfig.json:退出码 0。 - CI 在
Integration Tests (no-AK, No Sandbox)里用 Node 22.x runner 跑npm run typecheck:integration—— 本 head 上通过。
第 773 行是一个返回 string 的辅助函数里的 return result.stdout。只有当该处解析到 @types/node ≥ 22 时,spawnSync(...).stdout 才会放宽为 NonSharedBuffer;而本 lockfile 在根上钉的是 @types/node 20.19.1(TypeScript 5.8.3),CI 和我的两个树解析到的都是它。你的树几乎可以确定提升到了更新版本的 @types/node。PR 里没有需要改的东西 —— 建议直接把这条注意事项从描述里去掉。
本 head 上的回归扫查
| 面 | 方法 | 结果 |
|---|---|---|
| 面向模型的 ACP 内容 | 两个会话共 20 个 tool_call / tool_call_update 事件,归一化后比对 |
20/20 一致。 唯一名义上的差异是持久化输出文件路径里按 workspace 生成的哈希(ws 对 ws-main)。 |
PostToolUse / PostToolBatch |
真实 hook 脚本,6 个场景,两臂 | 分别 4/4 与 6/6 完全相同,两臂都仍是 typeof === 'string'。 |
-o stream-json |
6 个场景,两臂 | 6/6 等价。SCEN-BIG 只差 2 个字符的头尾拼接偏移,源自两个隔离 QWEN_HOME 路径名长度相差 2 个字符。 |
| 终端(Ink TUI) | 真实 pty,120×40,6 个场景,比对最终帧 | 6/6 完全一致。 |
| daemon 重启 / 中断的执行 | 同一 QWEN_HOME 上杀掉并重启 |
卡片重渲染一致;被中断的执行仍显示缺失结果回退。 |
| 构建 | 两臂根 npm run build |
均退出 0。导出渲染器 JS 1,927,689 B(基线 1,917,548,+10,141),告警线 1,970,000、上限 2,030,000。 |
| 测试 | core 定向(含容器边界套件)1,086 · cli 定向 347 · sdk 402 · acp-bridge 113 | 全部通过 |
packages/web-shell 全量套件 |
两臂,同一台机器 | PR 1 失败 / 9,197(BranchPickerPopover),基线 1 失败 / 9,170(ChatEditor)—— 是不同的文件,且两臂单独跑都通过(341/341)。属于并发负载抖动,不是 PR 引入;CI 的 Test (ubuntu) 在本 head 上是绿的。 |
上一轮我报告的 live/messages.test.ts 失败已经消失:#12316 与 #12319 在 main 上修掉了它,本 head 上除仍在运行的 review-pr 外,全部 CI 任务通过。
结论
这几处修正真实、克制且钉得住,其它面没有移动。从我这边看可以合并;N1/N2 属于仅影响导出的观感问题,上面提到的两句描述值得顺手更新。另有一条事务性提醒:分支现在又落后 main 5 个提交(dfdbbace1f),预期无冲突,下次合并后重跑门禁即可。
Round-2 evidence (same rig; "before the fix" images are this PR at f492336b26):
F3 — exit code suppressed for timeout and cancellation

F1 — exported document, before and after the fix (N1 visible: the command appears twice)

N2 — grep exit 1 and a no-output success in the export, base vs this head

|
🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 1/100) — it will retry on the next scan. What I found before stopping: See the Qwen Autofix agent step logs for model/tool output. 中文说明🤖 AutoFix 在完成前耗尽了时间(timeout (7200000ms))(第 1/100 次尝试)—— 将在下次扫描时重试。 Run log: https://github.com/QwenLM/qwen-code/actions/runs/35520279908 🧠 Handled by Qwen Code · model/模型 |
) - Documents: stop duplicating the command above the fallback text and label an empty legacy result as 'No output' (maintainer N1/N2) - Escape control and bidi characters at every shell-card render site while keeping clipboard copies byte-exact - Render a string rawOutput display instead of model-facing content so background-promotion and refusal messages stay visible - Show a finished command's elapsed time from startTime/endTime - Suppress the Command section when the legacy envelope already leads with the same command, keeping it once per expanded card - Pin the wasCancelled status, the background timeout gates, the synthetic exit-zero headline and the status icons; freeze the running-elapsed fixture's clock - Tag the card smoke test and wait for the SSE connection before driving live frames - Drop the dead .expandedBash selector and align both design docs with the undisclosed live-frame transport for line/byte counts Co-authored-by: Qwen-Coder <[email protected]>
The deterministic verification gate ran the core suite on a persistent runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the three gitPush tests that resolve the push remote failed with 'fatal: gd does not appear to be a git repository'. gitEnv strips only the GIT_CONFIG_* env overrides, so the code under test still honors the host HOME config — the exact gap the file's hermeticEnv() helper documents and every gitPull test already guards against. Pass hermeticEnv() at all six gitPush call sites so host config can no longer steer push resolution. Reproduced with a poisoned HOME (remote.pushDefault=gd): the three gate failures appear on the pre-fix tree and the file is 107/107 green after, in both clean and poisoned environments. Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[email protected]>
…LM#12311) The deterministic verification gate rebuilds core with a scoped npm run build --workspace packages/core, but the review-address job installs with QWEN_SKIP_PREPARE=1 and restores only the root and core dist artifacts, so packages/browser-use/dist is absent and the core build dies in copyBrowserUseAssets. The dependency on the built browser-use runtime was only expressed through the root build's ordering, leaving every scoped core build (the gate's, or a developer's on a prepare-skipped install) broken. Add a prebuild that stages the browser-use runtime only when its dist/index.js sentinel is missing. The root build keeps building browser-use first, so the prebuild is a no-op skip there and the fallback fires exactly when the runtime is absent. Probe matrix: scoped core build fails without the runtime pre-fix (the gate rejection, reproduced locally), passes with the runtime absent (fallback stages it), and passes unchanged with the runtime present (skip path). Co-authored-by: Qwen-Coder <[email protected]>
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Address-review round — PR #12311Feedback triage
Root cause of the deterministic rejectionThe rejection was environmental, not caused by the rejected commit (a test-only change): the Fix (commit d23096e)
Mutation probe (build wiring — no unit-test surface applies)
Verification
中文说明Address-review 轮次 — PR #12311反馈分类
确定性拒绝的根因该拒绝是环境导致的,并非由被拒绝的提交(一个纯测试改动)引起: 修复(提交 d23096e)
变异探针(构建装配 —— 无适用的单元测试面)
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
@qwen-code /resolve |
|
Qwen Code did not run conflict resolution for this request. PR #12311 does not currently have merge conflicts with main. |
Maintainer verification (round 3) — PR #12311 @
|
| runtime present | guard | command | result |
|---|---|---|---|
| absent | pre-fix | npm run build in packages/core |
fail — Browser-use runtime not found, exit 1 |
| absent | fix | npm run build in packages/core |
pass, stages packages/browser-use/dist |
| absent | fix | pnpm --filter @qwen-code/qwen-code-core run build |
pass, stages it (pnpm 11.24.0 does run prebuild) |
| present | fix | npm run build in packages/core |
pass, skip path — browser-use not rebuilt |
Root npm run build also confirms the skip path: browser-use builds first, then core's prebuild no-ops.
2. New findings
F1 (Minor) — a pre-PR record whose display string is empty falls back to the model-facing envelope.
ShellToolOutput.tsx:43 gates the new string branch on typeof raw === 'string' && raw, so an empty display string ('', which is what a silent command persists) is treated as "no string" and the card falls back to extractText(tool) — the envelope. The new legacyOutputRepeatsCommand guard then also suppresses the Command panel, because the envelope starts with Command: <cmd>. Net effect: in one transcript, sibling cards render clean output while the silent one renders a raw envelope with no Command section and no copy button.
Verified candidate patch — one word, A/B/A on the running daemon:
- typeof raw === 'string' && raw
+ typeof raw === 'string'The focused suites (145 tests) stay green both with and without the patch, so neither behaviour is pinned.
F2 (Minor) — the new finished-command elapsed is live-only; it disappears on reload.
Same session, same command: while the page stays open the card reads Succeeded 3s; after F5 it reads Succeeded. Cause is not in the component — the daemon's compacted replay emits one tool_call event per tool, already in its terminal status (a 6-tool session replays as exactly 6 events, zero in_progress), so the client has no start timestamp and formatElapsed returns ''. Not a regression (main never showed elapsed at all), but R1-39 is only half delivered, and the displayed duration is inconsistent between a live and a reloaded view of the same run.
F3 (informational, not introduced here) — the trojan-source hardening covers the expanded card only.
The collapsed tool summary — the line a user sees before expanding anything, and the header line in exported documents — still renders model-supplied text with the raw override. Byte-identical on both arms, so this PR did not introduce it; flagging it because it is the surface the escaping was meant to protect.
F4 (Minor, coverage) — 4 of the 6 new sanitizeControlChars call sites are unpinned.
12 targeted mutants against the round-3 code, 8 killed / 4 survived:
| mutant | result |
|---|---|
legacyOutputRepeatsCommand → false |
killed |
| drop the finished-elapsed branch | killed |
drop the document No output label |
killed |
drop the string-rawOutput branch |
killed |
| Command panel renders raw | killed |
| Output segments render raw | killed |
| copy handlers copy sanitized text (×2) | killed |
| document fallback renders raw | survived |
| notices render raw | survived |
result.directory renders raw |
survived |
result.outputFiles renders raw |
survived |
3. Regression & gate status on this head
- Full
packages/web-shellsuite, same machine, both arms: head 349 files / 9207 tests, 0 failed; mainb85264be79348 / 9171, 0 failed. Round 2's 2 failures and main's ownlive/messages.test.tsred are both gone. npm run build(root) exit 0;npm run typecheck(all workspaces +typecheck:integration) exit 0 on Node 24.18.1 — the description's remainingprocess-registry.ts:773note still does not reproduce here.eslint+prettier --checkclean on all five files changed sinceac01af9d1d.- All PR checks green;
mergeStateStatusisBLOCKEDonly onREVIEW_REQUIRED. - Cancel/timeout headlines stay correct: a SIGTERM-killed run reads Failed / Signal 15 with no synthetic exit code, where main shows
Exit Code: 0next toSignal: 15.
4. Checked and not reproduced
- R1-30 (an empty shell display no longer collapsing to
{type:'none'}in the TUI): drove the real TUI through@lydell/node-ptyon both arms with a silent command. Final frames are the same —✓ Shell true (Produce no output), one line, no empty box. I could not produce the symptom.
5. Still open, for the record
The autofix bot's round-2 comment defers 21 of its own review findings as "real and scoped". Two of them are visible in this run and worth a follow-up rather than a block:
- R1-1 — exported documents of failed runs still render the model-facing envelope (
Command:/Directory:/Exit Code:), while the live card for the same run shows clean output. A live/export inconsistency, not a correctness bug. - R1-31 — measured end to end in round 2: the display-text cost is ~0.3 %, not the 40,021→32,667 the description implies; the real cost is the ACP frame going to the full 64 KiB because
textandoutputare near-duplicates.
Recommendation: merge. F1 is a one-word change worth taking first since it is already verified; F2, F3 and F4 are follow-ups.
中文说明
维护者复验(第 3 轮)—— PR #12311 @ d23096e857
针对第 2 轮之后新增的三个提交(69a6f7cd4a → af4e1217f1 → d23096e857)重新验证,合并基线 b85264be79(与第 2 轮相同)。macOS 15 / Node 24.18.1 / pnpm 11.24.0。
装置。 三个 git 工作树——main b85264be79、上一轮 head ac01af9d1d、当前 head d23096e857——各自完整构建,各自跑一个生产 daemon 自托管本树的 packages/web-shell/dist,配独立 QWEN_HOME 和一个按场景标记返回确定性 run_shell_command 的自写 fake OpenAI。真实 shell 进程、真实 ACP/SSE 链路、真实 Playwright 截图、真实导出端点并用各臂自己的 export-transcript-document.{js,css} 离线渲染。"旧格式"记录由 main 的 daemon 产生,再用同一份 QWEN_HOME 的副本交给 PR 的 daemon 回放。
结论:可以合并。 三个新提交声称的效果都独立复现。新发现两个 Minor,都属于观感/完成度问题而非正确性问题,其中一个已给出实测过的一词修复。无阻塞项。
1. 第 3 轮修复——已复现
控制字符/双向字符转义(R1-8 / R1-38)。 真实命令的参数里带 U+202E … U+202C 和 BEL。在 ac01af9d1d 上卡片按双向覆盖渲染,gnp.exe 被显示成 exe.png,是典型的 trojan-source 伪装;在 d23096e857 上渲染为惰性文本 \u202e…\u202c(图 01)。
剪贴板保持逐字节原样。 把任一 copy 处理器改成复制"已转义"文本,两个变异体都被测试杀掉——"显示转义、复制原字节"的契约是钉住的。
N1 / N2(我第 2 轮提的两个 nit)均已修复,并在真实渲染的导出文档中确认。 左:ac01af9d1d 在回退文本之上重复打印命令(每个回合共 3 次:标题行、<pre>、信封自带的 Command: 行),且把"成功但无输出"的运行导出成只有一行命令的空块。右:d23096e857 只出现 2 次(标题 + 信封),空输出导出为 No output(图 03)。
字符串 rawOutput 显示(R1-50)。 后台启动卡片现在显示人类可读的显示字符串,而不是带 id / pid / 输出文件 / 状态文件路径的面向模型文本(图 07)。
未被宣传的收益:历史记录渲染变好了。 把 main 产生的会话交给 PR 的 daemon 回放,同一条持久化记录不再把整段 Command:/Directory:/Output:/Exit Code:/PGID 信封塞进 Output,而是命令出现一次 + 干净输出。这是对存量会话最大的可见改善,PR 描述里没有提(图 04)。
hermeticEnv()(af4e1217f1)。 没有采信门禁日志,而是自己复现:把 HOME 用 remote.pushDefault=gd 污染后,修复前的树恰好 3/107 失败,报错签名完全一致(fatal: 'gd' does not appear to be a git repository);修复后的树在污染与干净两种环境下都是 107/107。
prebuild(d23096e857)。 完整矩阵本地重跑,并补上提交说明没覆盖的那一格——仓库声明的包管理器:
| 运行时 | 守卫 | 命令 | 结果 |
|---|---|---|---|
| 缺失 | 修复前 | packages/core 里 npm run build |
失败 —— Browser-use runtime not found,退出 1 |
| 缺失 | 修复后 | packages/core 里 npm run build |
通过,装配出 packages/browser-use/dist |
| 缺失 | 修复后 | pnpm --filter @qwen-code/qwen-code-core run build |
通过并装配(实测 pnpm 11.24.0 会执行 prebuild) |
| 存在 | 修复后 | packages/core 里 npm run build |
通过,走跳过路径,不重建 browser-use |
根构建也确认了跳过路径:先构建 browser-use,随后 core 的 prebuild 空转。
2. 新发现
F1(Minor)—— 显示字符串为空的旧记录会回退到面向模型的信封。
ShellToolOutput.tsx:43 把新的字符串分支写成 typeof raw === 'string' && raw,于是空显示字符串('',正是静默命令持久化下来的值)被当成"没有字符串",卡片回退到 extractText(tool),也就是信封。而新加的 legacyOutputRepeatsCommand 守卫又会同时隐藏 Command 面板(因为信封以 Command: <cmd> 开头)。结果是:同一份会话里,兄弟卡片渲染干净输出,静默那张却是一段没有 Command 段、也没有复制按钮的裸信封。
实测过的候选修复(一个词,在运行中的 daemon 上做了 A/B/A):
- typeof raw === 'string' && raw
+ typeof raw === 'string'见图 05。聚焦套件(145 项)在打与不打这个补丁时都是全绿——两种行为都没有被测试钉住。
F2(Minor)—— 新增的"完成后耗时"只在实时视图生效,刷新即消失。
同一会话、同一条命令:页面保持打开时卡片显示 Succeeded 3s,按 F5 之后变成 Succeeded。根因不在组件里——daemon 的压缩回放对每个工具只发一条 tool_call 事件、且已经是终态(6 个工具的会话回放出的正好是 6 条,零条 in_progress),客户端拿不到起始时间戳,formatElapsed 返回 ''。这不是回归(main 从来就不显示耗时),但 R1-39 只完成了一半,而且同一次运行在实时视图与刷新后视图里显示的耗时不一致(图 06)。
F3(信息性,非本 PR 引入)—— trojan-source 加固只覆盖了展开后的卡片。
折叠态的工具摘要行——用户在展开任何东西之前先看到的那一行,以及导出文档里的标题行——仍然以原样渲染模型提供的文本。两臂逐字相同,所以不是本 PR 引入;提出来是因为它恰恰是转义想要保护的那个界面(图 02)。
F4(Minor,覆盖率)—— 新增的 6 个 sanitizeControlChars 调用点里有 4 个没被钉住。
针对第 3 轮代码做了 12 个定向变异,杀掉 8 个 / 存活 4 个:存活的是"文档回退分支不转义""notices 不转义""result.directory 不转义""result.outputFiles 不转义"。被杀掉的包括:legacyOutputRepeatsCommand 恒假、去掉完成耗时分支、去掉文档 No output 标签、去掉字符串 rawOutput 分支、Command 面板不转义、Output 分段不转义,以及两个"复制按钮改成复制转义后文本"的变异。
3. 本 head 的回归与门禁状态
- 同机两臂跑完整
packages/web-shell套件: head 349 个文件 / 9207 项,0 失败;mainb85264be79是 348 / 9171,0 失败。第 2 轮的 2 个失败、以及 main 自身live/messages.test.ts的红,都已消失。 - 根
npm run build退出 0;npm run typecheck(全部 workspace +typecheck:integration)在 Node 24.18.1 上退出 0——描述里残留的process-registry.ts:773说明在这里仍然复现不出来。 - 自
ac01af9d1d以来改动的 5 个文件,eslint与prettier --check均干净。 - 所有 PR 检查为绿;
mergeStateStatus显示BLOCKED仅因为REVIEW_REQUIRED。 - 取消/超时的标题依旧正确:被 SIGTERM 杀掉的运行显示 Failed / Signal 15,没有合成的退出码;而 main 会在
Signal: 15旁边显示Exit Code: 0(图 08)。
4. 查了但没有复现的
- R1-30(空的 shell display 不再折叠成
{type:'none'},会在 TUI 里留下空框):用@lydell/node-pty在两臂上驱动真实 TUI 跑静默命令,最终帧一致——都是✓ Shell true (Produce no output)一行,没有空框。这个症状我复现不出来。
5. 仍然未决,供记录
autofix bot 在第 2 轮评论里把它自己的 21 条评审发现标记为"属实且范围明确"后顺延。其中两条在本轮实测中可见,建议作为后续项而非阻塞项:
- R1-1 —— 失败运行的导出文档仍然渲染面向模型的信封(
Command:/Directory:/Exit Code:),而同一次运行的实时卡片显示的是干净输出。属于实时/导出不一致,不是正确性缺陷。 - R1-31 —— 第 2 轮已端到端量过:展示文本的代价约 0.3%,而不是描述里暗示的 40,021→32,667;真正的代价是 ACP 帧涨到满额 64 KiB,因为
text与output近乎重复。
建议:合并。 F1 是一个词的改动且已实测,值得先带上;F2、F3、F4 作为后续项。
Evidence: figures at wenshao/qwen-code@236dbbb06c524add571be5a6740a2aceb71277af under pr12311-r3/. Rounds 1–2 reports: #issuecomment-5748505797, #issuecomment-5750340118.
|
@qwen-code /triage |
…enLM#12311) A persisted empty display string from a silent command is falsy, so the card fell back to the model-facing envelope in content and the legacyOutputRepeatsCommand guard then suppressed the Command panel, rendering a bare envelope with no copy action. Gate the string branch on the type alone so an empty string renders as empty output next to the command.
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Autofix round — PR #12311 @
|
|
@qwen-code /triage |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix round: no actionable feedbackNo code changes were made in this round. What was reviewed
OutcomeWith no review feedback, no inline findings, and no failing checks, there is no in-scope work for this round. The branch is left untouched and nothing was committed. 中文说明Autofix 本轮:无可处理的反馈本轮未做任何代码修改。 已检查的内容
结论由于没有评审反馈、没有行内发现、也没有失败的检查,本轮没有需要处理的工作。分支保持原样,未提交任何内容。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
chiga0
left a comment
There was a problem hiding this comment.
Round 1 — COMMENT, 2 blockers remain (head 9b470cc2, base 1cc63cfe)
Progress since last analysis
| Commit | What it fixed |
|---|---|
22f6db30 |
ShellToolOutput: empty-string display now treated as authoritative ✅ |
69a6f7cd |
Web-shell UI: control/bidi escaping, elapsed time, string-fallback render, smoke tag ✅ |
| Various | coreToolScheduler and chatRecordingService input side migrated to shellResultText() ✅ |
coreToolScheduler:6651 |
Error artifact now updates text: errorMessage ✅ (partial — see R1-34 below) |
Blocker 1 — R1-1: text field carries LLM-facing content on exit-error / signal-termination path
File: packages/core/src/tools/shell.ts line 3157
Anchor: executionError.error && !timeoutSummary
executionError is built three ways:
- timeout →
error.message = timeoutSummary - spawn error (
result.error) →error.message = result.error.message(raw ENOENT / EPERM etc.) - exit-error / signal-termination →
error.message = typeof llmContent === "string" ? llmContent : returnDisplayMessage— the code comment explicitly labels this "model-facing response"
ShellResultDisplay.text is then:
executionError.error && !timeoutSummary
? executionError.error.message // ← on exit-error path this IS llmContent
: returnDisplayMessageOn the exit-error / signal path, text therefore carries the LLM-formatted output — truncation markers, "Model Context: …" prefixes, advisory text — directly to every web-shell card and shellResultText() caller. Fix: use returnDisplayMessage unconditionally.
Blocker 2 — R1-4: chatRecordingService output boundary still uses typeof … === "string"
File: packages/core/src/services/chatRecordingService.ts
Anchor: shellResultText(inputDisplay) !== undefined (line 2387, input side correctly migrated)
Unfixed line: 2440
The recorder_input boundary was correctly migrated. The recorder_output boundary at line 2440 was not:
// line 2440 — UNCHANGED, never matches ShellResultDisplay
...(typeof outputDisplay === 'string'
? [{ representation: 'display', value: outputDisplay }]
: []),Every shell output-stage observation silently produces an empty values array. Fix mirrors the input-side change at 2387: replace typeof outputDisplay === 'string' with shellResultText(outputDisplay) !== undefined.
Non-blocking (deferred per dev-bot, confirming open)
| Id | Note |
|---|---|
| R1-34 | coreToolScheduler:6651 — text: errorMessage added ✅; outcome still shows pre-error value (e.g. completed) while text describes a failure. Cosmetic, not a crash. |
| R1-3 | acp-tool-result-text-projection.ts:316 — allocatePayloadBudgets(…)! non-null assertion safe in practice (64 KB budget, skeleton ≈ 200 B), but should be guarded. |
The 19 other CI-bot round-1 findings that dev-bot deferred are not re-listed; they stand from that review.
| type: 'shell_result', | ||
| version: 1, | ||
| text: | ||
| executionError.error && !timeoutSummary |
There was a problem hiding this comment.
Blocker R1-1: On the exit-error/signal-termination branch, executionError.error.message was set above to typeof llmContent === 'string' ? llmContent : returnDisplayMessage (the comment reads 'Schedulers use error.message as the model-facing response'). So ShellResultDisplay.text here receives the LLM-formatted output — truncation markers, Model-Context prefixes, advisory text — rather than the user-facing returnDisplayMessage.
Fix: set text: returnDisplayMessage unconditionally (the error field already holds executionError.error.message for scheduler use).
There was a problem hiding this comment.
Fixed the producer in aacd8e5: text now always uses returnDisplayMessage, pinned for exit errors with/without output and signal termination. The scheduler override is intentionally retained to match the legacy createErrorResponse fallback (resultDisplay ?? error.message), including failure-hook context. Tests cover both legacy and structured scheduler results and failed batch hook normalization; both design languages explain the distinction.
| const inputValues = () => [ | ||
| ...toolResultPartDiagnosticValues(message), | ||
| ...(typeof inputDisplay === 'string' | ||
| ...(shellResultText(inputDisplay) !== undefined |
There was a problem hiding this comment.
Blocker R1-4 (sibling at line 2440 not migrated): This recorder_input boundary was correctly updated to shellResultText(). The recorder_output boundary at line ~2440 still reads typeof outputDisplay === 'string' and silently emits an empty values array for every ShellResultDisplay. All shell output-stage observations in downstream pipelines now lose their display content.
Fix: apply the same change — shellResultText(outputDisplay) !== undefined / shellResultText(outputDisplay)!.
There was a problem hiding this comment.
Fixed in aacd8e5. recorder_output now uses shellResultText exactly as recorder_input does. Regression coverage checks empty, small and compacted 40,000-character results against the actual saved display. All three cases fail when restoring the old typeof-string check and pass with the fix. Persisted results were intact; this repairs output-boundary diagnostic visibility.
|
Addressed the latest review's two code findings and verified the failure-path compatibility end to end through the scheduler and hook handler.
Validation: 1,156 tests passed across the four affected core suites; 4 independent boundary checks passed. Temporarily restoring both old production implementations made all 6 targeted regression cases fail; restoring the fixes made all six pass. Full build and bundle passed; the bundled CLI starts and reports 0.24.2. All workspace typechecks passed. Root integration typecheck still reports the previously documented Browser evidence: 1 Chromium command-card E2E passed, using the existing mock daemon. Success/failure screenshots were visually inspected, uploaded to the separate asset branch, and replace the earlier supplemental fixtures in the PR description. These are UI checks, not real shell/OS hook E2E or proof of the recorder fix. The independent boundary checks use mocked execution/filesystem/hook runner; the scheduler and hook handler under observation are actual implementation code. 中文:已修复生产端失败展示文本混入模型外壳,以及录制输出边界漏掉结构化展示文本的问题。调度器保留旧有错误详情与失败 hook 上下文。沙箱报告关于失败 PostToolBatch 从缺省变为字符串的结论漏看了基线中的 验证:1,156 项测试、4 项独立边界检查、1 项使用模拟 daemon 的 Chromium E2E 通过;撤回修复时 6 项回归测试全部失败,恢复后全部通过。构建、bundle、workspace 类型检查、lint/格式通过;根集成类型检查仍有已记录的本地 process-registry:773 类型错误。成功/失败截图已上传独立素材分支并更新 PR,明确仅作为模拟 daemon 的 UI 证据。 |
chiga0
left a comment
There was a problem hiding this comment.
Round 2 — APPROVE (head cb50a750, base 0f2be9a4)
Round-1 blockers resolved
| Id | Fix | Verified |
|---|---|---|
| R1-1 | shell.ts: text field now uses returnDisplayMessage unconditionally — the conditional that routed exit-error / signal paths through executionError.error.message (LLM content) is removed |
✅ Confirmed at line 3152: text: returnDisplayMessage |
| R1-4 | chatRecordingService.ts: output boundary (recorder_output stage, ~2440) migrated to shellResultText() — mirrors the input-side fix |
✅ Confirmed live at line 2443: shellResultText(outputDisplay) !== undefined |
New test coverage (commit aacd8e52):
chatRecordingService.test.ts:it.each(['', 'small display', 'x'.repeat(40_000)])drives bothrecorder_inputandrecorder_outputobservations and asserts display values present — covers the R1-4 regression directlyshell.test.ts: non-zero exit test parameterised over empty and non-empty output; assertsoutcome: 'failed',text, and the fullreturnDisplayshape
Remaining open items (non-blocking, no change since round 1)
| Id | Status |
|---|---|
| R1-3 | allocatePayloadBudgets(…)! non-null assertion still present; safe at 64 KB budget (skeleton ≈ 200 B) — deferred per maintainer measurement |
| R1-34 | Error artifact keeps original outcome while text carries errorMessage; cosmetic only, deferred per dev-bot |
| 19 CI-bot findings | All remain in "deferred to next round" state; no new blockers in that set |
yiliang114
left a comment
There was a problem hiding this comment.
LGTM — second maintainer vote, with the one open question independently re-checked.
The only reason the 04:14 sandbox round was findings rather than merge-ready was Finding A (failed-PostToolBatch seeing undefined → string). The author's rebuttal checks out on the code: the scheduler's failure-path fallback resultDisplay: resultDisplay ?? error.message exists identically at base 0f2be9a4 (:1101) and head cb50a750 (:1105), so the complete path produces the same error string on both arms — the report's payload construction bypassed that fallback. The added legacy/structured scheduler and failed-batch normalization coverage in aacd8e52 pins it.
Everything else on the gate: CI green (review-pr queued is the daytime runner window), 12k+ targeted tests green at verify time, mutation matrix 0 survivors, and the remaining review-ledger items are all Suggestion-level under the autofix loop. Approving the code as read at cb50a750.
* feat(web-shell): present structured shell execution results * fix(shell): preserve cleanup notices and clarify result fallbacks * fix(test): sync shell result source mappings * fix(web-shell): address shell command card review findings (QwenLM#12311) - Documents: stop duplicating the command above the fallback text and label an empty legacy result as 'No output' (maintainer N1/N2) - Escape control and bidi characters at every shell-card render site while keeping clipboard copies byte-exact - Render a string rawOutput display instead of model-facing content so background-promotion and refusal messages stay visible - Show a finished command's elapsed time from startTime/endTime - Suppress the Command section when the legacy envelope already leads with the same command, keeping it once per expanded card - Pin the wasCancelled status, the background timeout gates, the synthetic exit-zero headline and the status icons; freeze the running-elapsed fixture's clock - Tag the card smoke test and wait for the SSE connection before driving live frames - Drop the dead .expandedBash selector and align both design docs with the undisclosed live-frame transport for line/byte counts Co-authored-by: Qwen-Coder <[email protected]> * test(core): isolate gitPush tests from host git config (QwenLM#12311) The deterministic verification gate ran the core suite on a persistent runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the three gitPush tests that resolve the push remote failed with 'fatal: gd does not appear to be a git repository'. gitEnv strips only the GIT_CONFIG_* env overrides, so the code under test still honors the host HOME config — the exact gap the file's hermeticEnv() helper documents and every gitPull test already guards against. Pass hermeticEnv() at all six gitPush call sites so host config can no longer steer push resolution. Reproduced with a poisoned HOME (remote.pushDefault=gd): the three gate failures appear on the pre-fix tree and the file is 107/107 green after, in both clean and poisoned environments. Co-authored-by: Qwen-Coder <[email protected]> Co-authored-by: Qwen-Coder <[email protected]> * fix(core): stage browser-use runtime on demand in core prebuild (QwenLM#12311) The deterministic verification gate rebuilds core with a scoped npm run build --workspace packages/core, but the review-address job installs with QWEN_SKIP_PREPARE=1 and restores only the root and core dist artifacts, so packages/browser-use/dist is absent and the core build dies in copyBrowserUseAssets. The dependency on the built browser-use runtime was only expressed through the root build's ordering, leaving every scoped core build (the gate's, or a developer's on a prepare-skipped install) broken. Add a prebuild that stages the browser-use runtime only when its dist/index.js sentinel is missing. The root build keeps building browser-use first, so the prebuild is a no-op skip there and the fallback fires exactly when the runtime is absent. Probe matrix: scoped core build fails without the runtime pre-fix (the gate rejection, reproduced locally), passes with the runtime absent (fallback stages it), and passes unchanged with the runtime present (skip path). Co-authored-by: Qwen-Coder <[email protected]> * fix(web-shell): treat empty string shell display as authoritative (QwenLM#12311) A persisted empty display string from a silent command is falsy, so the card fell back to the model-facing envelope in content and the legacyOutputRepeatsCommand guard then suppressed the Command panel, rendering a bare envelope with no copy action. Gate the string branch on the type alone so an empty string renders as empty output next to the command. * fix(core): preserve shell display and recording diagnostics * feat(web-shell): add session tool calls panel with prompt selection * fix(web-shell): address tool calls review findings * fix(web-shell): keep prompt text out of persisted dock state * fix(web-shell): restore sender tool call identity and review fixes * docs(web-shell): record real daemon sender verification --------- Co-authored-by: 钉萁 <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: Qwen Autofix <[email protected]> Co-authored-by: Qwen-Coder <[email protected]>
Keep both additive test blocks in normalize.test.ts: this PR's usage-metadata identity tests and main's structured shell output test (QwenLM#12311) were inserted at the same anchor inside describe('normalizeSessionData').








What this PR does
Organizes shell execution into Command, Output and Execution details, with command/output expanded by default, icon-only copy actions, explicit execution states, and 200px scrollable content regions. Running commands show elapsed time beside their status; supplied timeouts remain available in details.
Carries versioned structured shell results from execution through recording, ACP, SDK previews and Web Shell, so output and metadata are displayed without parsing a human-readable result envelope. Preserves model-facing content, terminal/text consumers, and string hook payloads, including full successful display text before PostToolUse. Applies bounded previews at downstream boundaries. Historical text and unknown versions retain a compatible fallback. Foreground completed results are structured; background acknowledgments and early returns such as pre-execution cancellation keep their existing string fallback. Producer failure text preserves the original human display; the scheduler keeps its legacy error-detail fallback for terminal and batch-hook consumers.
Why it's needed
The previous card repeated the command and mixed output with exit codes, empty errors and process metadata. Structured results make execution outcomes explicit without mistaking user output for metadata, while retaining the existing policy that grep/diff/test exit 1 can be a completed negative result.
Reviewer Test Plan
How to verify
Latest review fix verification (2026-09-21): 1,156 tests passed across the four affected core suites; 4 independent producer/recorder/scheduler/hook checks passed with mocked execution service/filesystem/hook runner, plus 1 Chromium command-card E2E using a mock daemon. Full build, bundle, workspace typechecks, formatting and lint passed. Root integration typecheck retains the local environment limitation below. These checks are not a fresh real-daemon or OS hook E2E run.
Latest follow-up on the updated main base: root
npm run buildexits 0; all workspace typechecks pass. On this local Node 22.14.0 environment, root integration typecheck still reports the process-registry string/NonSharedBuffer mismatch at line 773; the missing generated-template error is gone. The maintainer independently reported a clean full build and full typecheck on Node 24.18.1. This is not complete cross-platform validation.Evidence (Before & After)
These are actual browser captures from the earlier local real-daemon validation, visually inspected again for this PR, not mocked-daemon screenshots or a fresh run of the final commit. Before uses the original frontend and global CLI 0.22.0; after uses this worktree's structured-result implementation. Commands and themes differ. The earlier running screenshot places elapsed time in details; the final UI moves it beside status. The earlier real-daemon recapture attempt was blocked by browser navigation policy; refreshed mock-daemon captures are separately labelled below. Screenshots demonstrate UI states, not Git backend operations.
Restart evidence: restored success, restored failure, interrupted execution fallback. Completed structured fields matched after restart. Recovery required reopening the session; intermediate output for the interrupted command was not recorded. The separate E2E comment details these limits.
Tested on
Environment (optional)
Node.js 22.14.0; CLI/core from the isolated worktree; real daemon on 5328 and frontend on 5201. Existing export-template build artifacts were reused for the earlier real run because of the export bundle blocker. The earlier real-daemon screenshots used no mock; the supplemental 2026-09-21 captures below use a mock daemon.
Risk & Scope
Design: command card (English), 命令卡片(中文), structured results (English), 结构化结果(中文).
Follow-up review fixes: text-only exported documents render the complete fallback without empty category disclosures; terminated/cancelled/timed-out results do not show a synthetic exit code; resolved working-directory semantics are explicit; container cleanup notices remain visible in both compatible text and structured notices. Added assertions for the resolved directory, persisted output files and refused-promotion outcome. 385 core + 136 Web tests and 5 independent focused checks passed; formatting and lint passed. Container verification mocks the worker, not a real container runtime.
Supplemental browser captures below were refreshed on 2026-09-21 using Chromium and the existing command-card E2E with a mock daemon (1 test passed). They were visually inspected and show success/failure UI only; they do not prove real shell execution or the recorder/hook fixes. The earlier real-daemon evidence above remains separate.
Known deferred items: the structured ACP text/output fields share the 64KB budget, so daemon CLI long display text can be truncated earlier than the former string result. This is not full long-output display parity. F6 cosmetic cleanups and adding this UI to the dedicated visual-preview workflow remain follow-ups; existing browser regression tests are a separate harness.
Linked Issues
None.
中文说明
变更内容
将 Shell 执行整理为命令、输出和执行详情。命令及输出默认展开,使用纯图标复制按钮、明确的执行状态,以及最高 200px 的内部滚动内容区。运行耗时显示在状态旁,传入的超时设置可在详情中查看。
将版本化的结构化结果从执行端贯穿录制、ACP、SDK 预览和 Web Shell,直接展示输出与元数据,不解析人类可读的结果外壳。保留面向模型的内容、终端/纯文本消费者及字符串 hook 载荷,包括 PostToolUse 前完整的成功展示文本。在下游边界限制预览大小。历史文本及未知版本使用兼容回退。前台完成结果使用结构化格式;后台确认及执行前取消等提前返回保留既有字符串回退。生产端失败文本保持原有人类可读展示,调度器继续为终端和批次 hook 保留旧有错误详情回退。
为什么需要
原有卡片重复命令,并将输出、退出码、空错误和进程元数据混在一起。结构化结果能明确表达执行结果,避免将用户输出误识别为元数据,并沿用 grep/diff/test 退出码 1 可以表示已完成但结果为否的现有策略。
评审验证计划
如何验证
最新评审修复验证(2026-09-21):受影响的 4 个 core 测试文件共 1,156 项通过;4 项独立生产端、录制、调度器与 hook 验证通过,底层执行服务、文件系统和 hook runner 使用 mock;另有 1 项 Chromium 命令卡 E2E 使用模拟 daemon 通过。完整构建、bundle、workspace 类型检查、格式和 lint 通过。根集成类型检查仍有下述本地环境限制。这些结果不代表新一轮真实 daemon 或操作系统 hook E2E。
基于更新后 main 的本轮结果:根
npm run build退出码 0,各 workspace 类型检查通过。本机 Node 22.14.0 下根集成类型检查仍报告 process-registry 第 773 行 string/NonSharedBuffer 类型不匹配,缺失生成模板错误已消失。维护者另在 Node 24.18.1 下报告完整构建及完整类型检查通过。尚未完成跨平台验证。前后证据
以上图片来自此前本地真实 daemon 的浏览器验证,本次再次目视检查;不是模拟 daemon 截图,也不是最终提交的新一轮真机验证。修改前使用原前端与全局 CLI 0.22.0,修改后使用本 worktree 的结构化实现;命令及主题不同。早期运行态截图将耗时放在详情中,最终界面已移到状态旁。此前的真实 daemon 补拍被浏览器导航策略阻塞;本轮模拟 daemon 截图在下方单独标注。截图证明界面状态,不代表 Git 后端操作验证。
上方两组图片分别展示修改前后的真实健康检查,以及运行中等待输出和退出码 7 的失败状态。重启证据:恢复成功结果、恢复失败结果、中断执行回退。完成态结构化字段在重启前后保持一致;恢复需要重新打开会话,中断命令的中间输出未录制。单独的 E2E 评论详述这些限制。
测试平台
macOS:已进行本地真实 daemon 验证及针对性测试。Windows、Linux:未测试。
环境
Node.js 22.14.0;CLI/core 来自隔离 worktree,真实 daemon 使用 5328 端口,前端使用 5201。此前真机验证因导出包体积阻塞而复用了已有导出模板构建产物。截图没有使用模拟 daemon。
风险与范围
设计文档包含同步的中英文版本:命令卡片英文、命令卡片中文、结构化结果英文、结构化结果中文。
本轮评审修复:仅有文本的导出文档展示完整回退,不渲染空分类;信号终止、取消和超时不显示可能由执行层补出的退出码;明确解析后的工作目录语义;容器清理提示同时保留于兼容文本及结构化 notices。补充了实际目录、输出文件和拒绝后台切换结果的断言。385 项 core、136 项 Web 和 5 项独立定向检查通过,格式和 Lint 通过。容器验证使用模拟 worker,未运行真实容器。
以下补充浏览器截图于 2026-09-21 使用 Chromium 和现有命令卡 E2E 重新采集,使用模拟 daemon,1 项测试通过并已目视检查。图片仅证明成功/失败 UI 展示,不证明真实 Shell 执行或本轮录制/hook 修复;与前述真机证据分开标注。
明确后续处理项:结构化 ACP 的 text/output 共用 64KB 预算,daemon CLI 长展示文本可能比原字符串结果更早截断,尚未做到长输出展示完全等价。F6 外观清理及接入专用视觉预览工作流留待后续;已有浏览器回归测试使用另一套入口。
关联问题
无。