Repository navigation
feat(web-shell): inspect session tool calls by prompt - #12466
Conversation
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.
) - 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]>
…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.
…to feat/web-shell-turn-calls-panel
…alls-panel # Conflicts: # packages/web-shell/client/components/artifacts/ArtifactPanel.tsx # packages/web-shell/client/components/messages/ToolGroup.tsx # packages/web-shell/client/i18n.tsx # packages/web-shell/client/main.tsx
Post-merge E2E reportCommit:
Build and unit validation
中文验证说明合并提交
构建与单元验证
|
Maintainer verification: real daemon, real tool execution, Linux (
|
| Area | Evidence | Result |
|---|---|---|
| Completeness and ownership | 9 turns, 217 calls, compared against the raw chats/<id>.jsonl. Includes a 90-call turn and a 110-call turn stopped by the loop cap, the "57 shown as 13" class. |
Count, order and ids match exactly per turn. 0 missing, 0 extra, 0 duplicated. Identical after a daemon restart. sessions/live-state stays [], so the read never attaches the session. |
| Recorded timing | Each fake-model response logs its send time and the arrival time of the next request. | Every one of the 206 timed calls has [startedAt, startedAt+durationMs] inside its real wall-clock window, and equals the raw ui_telemetry started_at/duration_ms (206/206). The tooltip is exact to the millisecond (19:59:19.428 → .452 for 24 ms). The approval wait is included (3548 ms for a 2 s command with a 1.5 s approval), as the design doc states. |
| Legacy records | A session recorded by the base daemon, read by head. | 11 durations and 0 startedAt. No tooltip or aria-description: nothing is fabricated. |
| Statuses | Tool error, non-zero exit, duplicate provider id, loop-cap rejection, cancel after 1.5 s, and kill -9 of the daemon mid-call. |
All show Failed with the right reason. Cancel shows 2s Cancelled: the recorded cancel overrides the generic failed replay. After the crash and a restart, the dangling call becomes Failed: "Tool result missing from saved history…". |
| Post-approval ACP frame | SSE capture. | tool_call_update(in_progress) arrives 3 ms after permission_resolved, carrying rawInput and startedAt. |
| Live prompt | A real 8 s shell call sent from the composer. | The clock ticks 948 ms → 8 s with 0 /tool-calls reads while running. A mid-turn message injected at +5 s does not split the turn: both calls stay under the prompt, and there is no selector entry for the injection. |
| Read economy | page.on('request'). |
Selecting a prompt = 1 GET. Refresh = 1 GET. Reload restore = 1 GET. Viewing an older prompt while another runs = 1 GET, with no repeats over 4 s of streaming. Switching back to the running prompt = 0 GETs while it runs and 1 at settle, and the rows don't flash. |
| UI | EN/zh-CN, light/dark. | Localized names. MCP badge and filter (the count stays 11). Shell Arguments = command, Result = output, Other collapsed. Edit shows the recorded diff. JSON is formatted. |
| API contract | curl. | 401 without a token or with a wrong one. 400 invalid_turn_anchor for a missing, blank, 201-char or repeated turnId, an unknown uuid, a non-prompt record, or another session's record. 404 when the session is read through a different registered workspace (no cross-workspace read). The route is a 404 on base. |
| Tests (Linux) | Local vitest. | core 586, acp-bridge 136, cli 1231, server.test.ts full file 1322/1322 on the first run (it was flaky on macOS for the author), sdk 878, web-shell 1470 (10 touched files). All green. CI is green, including Test. |
Defect: a tab opened from your own message has no durable identity
Repro: send a prompt from the composer, then click View tool calls on that message, either while it runs or after.
| PR head | With the fix below | |
|---|---|---|
| The prompt in the selector | listed twice | once |
turn_calls tab in localStorage |
none (dropped by serializeArtifactPanelTabs) |
{promptId} |
/tool-calls read after settle |
0 | 1 |
| Panel after a page reload | closed | restored, same prompt |
A second client watching the same session behaves correctly on head: it gets promptId from the bridge echo. So this only affects the sender, which is the common case.
Root cause. The sender's own user echo is suppressed (suppressOwnUserEcho). As DaemonSessionProvider.tsx:362-364 notes, that local block never gets a recordId, and the tab that App.openTurnCalls builds from it ends up with neither recordId nor promptId. The persisted state proves this: serializeArtifactPanelTabs drops exactly such tabs. The backfill effect in App.tsx waits for sourceRecordIds that never arrive on this block. In TurnCallsPanel, the selector falls back to a synthetic block:<turnId> entry next to the provisional prompt:<id> one, and the history effect is gated on the recordId/promptId props. The provisional navigation turn already knows blockId → promptId (from recordPromptAdmitted), so the panel can adopt it.
Fix (12 lines) + 2 tests: RED on head, GREEN with the fix
--- a/packages/web-shell/client/components/artifacts/TurnCallsPanel.tsx
+++ b/packages/web-shell/client/components/artifacts/TurnCallsPanel.tsx
@@ -492,6 +492,18 @@ export function TurnCallsPanel({
const idle = usePromptStatus() === 'idle';
const navigation = useTurnNavigationState();
const blocks = useTranscriptBlocks();
+ // The sender's own live user block never carries a record or prompt id: its
+ // daemon echo is suppressed. Adopt the prompt id from the provisional turn
+ // bound to this block so the tab can persist, resolve and dedupe.
+ const provisionalPromptId =
+ !recordId && !promptId
+ ? navigation.provisionalTurns.find((turn) => turn.blockId === turnId)
+ ?.promptId
+ : undefined;
+ useEffect(() => {
+ if (provisionalPromptId)
+ onSelectPrompt?.(turnId, undefined, provisionalPromptId, promptLabel);
+ }, [provisionalPromptId, onSelectPrompt, turnId, promptLabel]);The tests are appended to TurnCallsPanel.test.tsx. One checks that a sender-local block with only a provisional turn adopts prompt-live. The other checks that a tab which already has a promptId is not retargeted. On head the first fails, and with the fix both pass. TurnCallsPanel + App + loadTurnCalls tests: 1104/1104. ESLint, Prettier and web-shell tsc --noEmit are clean. The full patch is fix-sender-identity.patch.
Non-blocking notes
- The wrapped MCP call (
tool_search→tool_call): the row name resolves tomcp__inventory__lookup_sku, but the description line readstool_calland Arguments show the{name, arguments}envelope. The design doc says wrappers resolve their actual name and arguments. See the right pane of figure 3. - English count grammar:
1 tool calls. - The tooltip's "Start time" is when scheduling started, so it includes the approval wait (design-stated). On an approved call it reads earlier than the command actually began. A wording hint could help.
- Test coverage of
session-tool-calls.ts: 18 targeted mutants run against its own test file, 11 killed. The one real gap is "never finalize dangling calls of a settled turn". The unit tests don't pin it, but the real-daemon crash case above shows the behaviour is correct. The other survivors are defense in depth: the reader already rejects bad anchors (400 on the real daemon), ownership closes at the next navigation record anyway, and the pagination loop ends through another bound. - The triage bot left two questions for a maintainer. The
Testresult is now green, andserver.test.tspasses locally as a full file. For the unvirtualized selector question, I measured a real long session: on a 4,227-turn session, opening the selector issues 17turn-indexpage reads and renders all 4,227 options: about 18.7k DOM nodes and a ~104 MB page heap, usable after ~1.3 s. So it is reachable and still tolerable at that size, but it grows linearly. The 25k ceiling would be about 6× this.
Not covered: Windows/macOS, the agent detail tab contents, MCP auth, and an embedded host with showToolCalls=false restoring a persisted tab (triage finding 4).
Evidence (harness, raw ground-truth diffs, logs, figures): wenshao/qwen-code@823ef47/pr-12466
中文版
维护者验证:真实 daemon、真实工具执行、Linux(d10c768)
结论:建议补上一处客户端小修复后合并,或合并后立即跟进。 新的服务端读取接口端到端正确。在真实 qwen serve daemon 上,217 次真实工具调用的读取结果与原始 JSONL 逐条一致,daemon 重启后也一样。记录的耗时与墙钟时间吻合。面板在生产构建的 Web Shell 里工作正常。我发现了一处缺陷,出在用户最常走的路径上:发送 prompt 的那个客户端。在自己刚发的消息上打开“工具调用”时,tab 没有持久身份。该 prompt 在选择器里出现两次;tab 不会被持久化,刷新页面后面板关闭;该轮结束后也不会读取历史。下文给出根因、12 行修复和 2 个测试,修复已在同一套浏览器环境里验证。
测试方式(环境)
- Linux x86_64,Node 22.22.2。对 head
d10c768与 basec83265ff36分别执行pnpm install --frozen-lockfile、完整npm run build与npm run bundle。 - 真实
qwen servedaemon 提供它自己打包的 Web Shell(生产dist/web-shell,不是 vite dev)。浏览器为 Playwright 的 Chromium 1228。没有page.route,没有 mock daemon。 - 脚本化的 OpenAI 兼容模型发出真实工具调用,由 daemon 实际执行:shell、读/编辑/写文件、grep/glob、失败的读取、非零退出码、10 个一批的并行调用、子代理、重复的 provider id。两个真实 stdio MCP 服务器:一个是延迟加载的(经
tool_search→tool_call调用),一个配置了alwaysLoadTools。 - 审批走真实的权限请求,daemon 在等待 1.5 秒后投票。取消通过
POST /session/:id/cancel。崩溃场景是在调用执行中对 daemon 发送kill -9。 - 生成数据和浏览之间重启了 daemon,因此读取走的是从磁盘冷启动的路径。
已通过实际执行验证
| 方面 | 证据 | 结果 |
|---|---|---|
| 完整性与归属 | 9 轮、217 次调用,与原始 chats/<id>.jsonl 比对。包括一轮 90 次调用的,和一轮被 loop cap 截停的 110 次调用(即“57 次只显示 13 次”那一类)。 |
每轮的数量、顺序、id 完全一致。缺失 0,多余 0,重复 0。daemon 重启后结果相同。sessions/live-state 始终为 [],说明读取不会挂载会话。 |
| 记录的耗时 | 假模型记录每次响应的发送时间和下一次请求的到达时间。 | 206 次带计时的调用,每一次的 [startedAt, startedAt+durationMs] 都落在它的真实墙钟区间内,且与原始 ui_telemetry 的 started_at/duration_ms 一致(206/206)。tooltip 精确到毫秒(24ms:19:59:19.428 → .452)。耗时包含审批等待(2 秒命令 + 1.5 秒审批 = 3548ms),与设计文档所述一致。 |
| 旧记录 | 由 base daemon 录制的会话,用 head 读取。 | 11 个耗时,0 个 startedAt。没有 tooltip,也没有 aria-description,没有伪造任何数据。 |
| 状态 | 工具报错、非零退出、重复 provider id、loop cap 拒绝、1.5 秒后取消、执行中 kill -9 daemon。 |
都显示为失败,且原因正确。取消显示 2s 已取消:记录的取消状态覆盖了通用的失败回放。崩溃并重启后,悬空调用被收尾为失败:“Tool result missing from saved history…”。 |
| 审批后的 ACP 帧 | 抓取 SSE。 | tool_call_update(in_progress) 在 permission_resolved 之后 3ms 到达,带有 rawInput 与 startedAt。 |
| 运行中的 prompt | 从输入框发出的一次真实 8 秒 shell 调用。 | 计时从 948ms 走到 8s,运行期间 /tool-calls 读取为 0 次。+5 秒时注入的 mid-turn 消息没有把这一轮切开:两次调用仍归在该 prompt 下,选择器里也没有为注入消息单列条目。 |
| 读取次数 | page.on('request')。 |
选中一个 prompt = 1 次 GET;Refresh = 1 次;刷新页面恢复 = 1 次。另一轮运行时查看旧 prompt = 1 次,在 4 秒的流式输出期间没有重复读取。切回运行中的 prompt:运行期间 0 次,结束时 1 次,列表不闪空。 |
| 界面 | 中英文,浅色/深色主题。 | 工具名已本地化。MCP 标签和筛选正常(总数保持 11)。Shell 的“参数”是命令、“结果”是输出、“其他”默认折叠。编辑显示记录的 diff。JSON 已格式化。 |
| 接口约定 | curl。 | 不带 token 或 token 错误都返回 401。turnId 缺失、空白、201 字符、重复传参、未知 uuid、非 prompt 记录、其他会话的记录,都返回 400 invalid_turn_anchor。通过另一个已注册的工作区读取该会话返回 404(不能跨工作区读取)。base 上该路由为 404。 |
| 测试(Linux) | 本地 vitest。 | core 586、acp-bridge 136、cli 1231、server.test.ts 整文件首次运行 1322/1322(作者在 macOS 上该文件不稳定)、sdk 878、web-shell 1470(10 个相关文件),全部通过。CI 全绿,包括 Test。 |
(图见上方英文部分)
缺陷:从自己发出的消息打开的 tab 没有持久身份
复现: 在输入框发送一个 prompt,然后在这条消息上点击查看工具调用(运行中或结束后都一样)。
| PR head | 应用下方修复后 | |
|---|---|---|
| 该 prompt 在选择器中 | 出现两次 | 一次 |
localStorage 中的 turn_calls tab |
无(被 serializeArtifactPanelTabs 丢弃) |
{promptId} |
结束后的 /tool-calls 读取 |
0 次 | 1 次 |
| 刷新页面后的面板 | 关闭 | 恢复,且选中同一 prompt |
在 head 上,同一会话的另一个旁观客户端表现正常(它从 bridge echo 拿到了 promptId)。所以问题只出在发送端,而这恰恰是最常见的情况。
根因。 发送端自己的用户回显被压制了(suppressOwnUserEcho)。正如 DaemonSessionProvider.tsx:362-364 的注释所说,这个本地块永远拿不到 recordId;App.openTurnCalls 据此创建的 tab 既没有 recordId 也没有 promptId。持久化状态可以证明这一点:serializeArtifactPanelTabs 丢弃的恰好就是这种 tab。App.tsx 里的回填 effect 在等 sourceRecordIds,而这个块永远不会有。在 TurnCallsPanel 中,选择器退回到一个合成的 block:<turnId> 条目,与临时条目 prompt:<id> 并列显示;历史读取的 effect 又以 props 上的 recordId/promptId 为前提。导航状态里的临时轮次已经知道 blockId → promptId(来自 recordPromptAdmitted),面板可以直接采用。
修复:TurnCallsPanel.tsx 增加 12 行(diff 见英文部分),并在 TurnCallsPanel.test.tsx 追加 2 个测试。一个验证只有临时轮次的发送端本地块会采用 prompt-live;另一个验证已有 promptId 的 tab 不会被重定向。第一个测试在 head 上失败,修复后两个都通过。TurnCallsPanel + App + loadTurnCalls 测试 1104/1104。ESLint、Prettier、web-shell 的 tsc --noEmit 均无问题。
非阻塞问题
- 经包装的 MCP 调用(
tool_search→tool_call):行名称已解析为mcp__inventory__lookup_sku,但描述行显示tool_call,“参数”里是{name, arguments}外层信封。设计文档写的是包装调用会解析出实际名称和参数(见图 3 右栏)。 - 英文计数的单复数:显示为
1 tool calls。 - tooltip 的“开始时间”是调度开始的时间,包含审批等待(设计文档已说明)。对需要审批的调用,它会早于命令实际开始执行的时间,可以考虑在文案上提示。
session-tool-calls.ts的测试覆盖:用它自己的测试文件跑 18 个定向变异,杀掉 11 个。唯一真实的缺口是“已结束轮次的悬空调用从不收尾”:单测没有钉住,但上面真实 daemon 的崩溃场景证明行为是正确的。其余存活的变异属于纵深防御:reader 本身会拒绝错误锚点(真实 daemon 上返回 400);归属在下一个导航记录处本来就会关闭;分页循环也会被另一个边界终止。- Triage 机器人给维护者留了两个问题。
Test现在已经绿了,server.test.ts在本地整文件通过。关于选择器不做虚拟化的问题,我在真实长会话上测了:在 4,227 轮的会话上,打开选择器会发出 17 次turn-index分页读取,并渲染全部 4,227 个选项:页面约 1.87 万个 DOM 节点、JS 堆约 104 MB,约 1.3 秒后可用。所以这个规模是可达的,目前仍可接受,但开销线性增长,到 25k 上限时约为现在的 6 倍。
未覆盖: Windows/macOS、子代理详情页内容、MCP 认证,以及 showToolCalls=false 的嵌入宿主恢复已持久化 tab 的情况(triage 的第 4 条)。
证据(harness、原始对账数据、日志、图片):wenshao/qwen-code@823ef47/pr-12466
🤖 Generated with Claude Code — Claude Opus 5 (1M context)
Review follow-upFixes pushed in 0ad95657ea and 6f68391f00. Verified the findings against
R1-4 is not established as a regression of this PR. The actual Linux test job for the reviewed SHA passed (360 Web Shell files / 9,536 tests, no unhandled exception in that log). A strict local TrajectoryPanel run also passed 21/21. An isolated probe confirms an existing virtual-core debounce cleanup gap, but that dependency, virtualizer and triggering test are unchanged from the base. No unhandled-error suppression was added; the dependency cleanup issue should be tracked separately. R1-25’s proposed early stop is not safe: records after the navigation ownership boundary can contain late results/timing for calls that started before it. The replay must continue to the next ordinary prompt boundary, within the existing explicit scan limits. Fixed discussions are replied to and resolved; partially addressed and deferred discussions remain open. Remaining suggestions are deferred to avoid further broadening this already extensively reviewed feature: nested-emitter timing coverage/wiring (R1-16/18), SDK cancellation/timeout options (R1-28), additional coverage-only cases (R1-13/17/19–24/26/27/29–32/38–41/50/56), and cleanup/polish (R1-34/35/37/49/55/57). In particular, the file action remains available on collapsed rows as explicitly requested; per-file availability-check coalescing is a follow-up. Fresh real-daemon verification of the originally reported large sessions is still pending, not claimed here. Validation this round: 1,663 scoped unit tests passed across the most recent focused runs (Web Shell/App 1,491; ACP replay 138; persisted-reader/SSH tests 34). Independent reproductions retain failing snapshots of the original head and passing probes of the fixes. Full build/typecheck/bundle passed during the fixes; final Web Shell build/typecheck and changed-file ESLint/Prettier checks passed. Chromium mock-daemon E2E passed in light/dark themes (2/2); screenshots were visually checked and updated on the separate assets branch. These screenshots do not claim real daemon execution or Git backend validation. Updated E2E report. The final storage-only follow-up also passed its red/green App regression, Web Shell build/typecheck and commit hooks. 中文:已修复确认的功能、计时、数据保真、归属和性能问题,并补充回归验证。未证实的 CI 归因没有用忽略异常绕过;必须读取的跨边界迟到结果仍然保留。其余覆盖率、重构、嵌套计时扩展与界面微调建议按上表留作后续,避免继续扩大本 PR。截图使用模拟 daemon,真实大会话的新一轮执行验证仍未宣称完成。 |
ytahdn
left a comment
There was a problem hiding this comment.
Ran an AI-assisted review over the diff and manually verified each finding against the code at this commit. Three issues worth addressing before merge — one functional bug, one behavioural defect in the ACP frame emission, and one paging-size mismatch that will desync the turn-index consumers. Details inline.
Maintainer verification, round 2: real daemon, Linux (
|
| Area | Method | Result |
|---|---|---|
| Server read unchanged | GET /tool-calls for every turn of 5 sessions, d10c768 vs 6f68391 daemons on the same data. |
4,262 turns / 5,432 events byte-identical. Ground truth re-run on 6f68391: every turn equals the raw JSONL (125/125, 241/241, 20/20, 12/12 calls; order equal, 0 missing / extra / dup). All 362 timed calls fall inside their fake-model wall-clock window and equal ui_telemetry. The legacy base-recorded session still has 0 fabricated startedAt. |
| Unit tests (changed files) | vitest. | web-shell 1,377/1,377 (TurnCallsPanel, App, MessageItem, buildTrajectory, transcriptToMessages); cli 34/34 (session-tool-calls, ssh-workspace); acp-bridge 138/138 (transcript-replay). |
| Selector on 4,227 turns (R1-9/36/42–45) | Open the selector, then Home, mid-list scroll, and choose. | See the table below. On-demand paging works: scrolling to turn ~2,000 loaded only pages start=1800 and start=2000, rendered in 63 ms. Choosing by mouse at #2005 and by Home→Enter at #1 both retarget the panel and issue one /tool-calls read (200). |
| Prompt text out of dock state (R1-33) | localStorage after opening a tab. |
Tabs carry turnId / recordId / promptId only: promptLabel is absent. |
| Wrapped MCP row (R1-3; my round-1 note 1) | Expand mcp__inventory__lookup_sku reached via tool_search → tool_call. |
Fixed: the description no longer reads tool_call. Still open: Arguments show the {name, arguments} envelope (fig. 3). |
| Failed agent diagnostics (R1-2) | New scenario: a foreground subagent whose model call returns HTTP 400. | See note 1 below. |
The remaining defect: the sender's tab has no identity
Repro: send a prompt from the composer, then click View tool calls on that message, either while it runs or after it finishes.
PR head 6f68391 |
With the patch below | |
|---|---|---|
Opened while running (SCN:slow, 9 s) |
not persisted, no check mark | persisted {promptId}, checked |
| After the turn settles | 0 /tool-calls reads (live rows only) |
1 read (200) |
| Page reload | panel closed | panel restored, 2 rows |
Opened shortly after sending (SCN:after) |
not persisted, 0 reads, closed on reload | persisted {recordId}, 1 read, restored |
Why round 1's root cause still applies: the sender's own user echo is suppressed, so that local block never gets sourceRecordIds or promptId. App.openTurnCalls therefore creates a tab with neither id. serializeArtifactPanelTabs drops it, the history effect is gated on selectedRecordId || promptId, and TurnCallPromptSelect receives recordId/promptId both undefined, so it never matches its selected row. The navigation store already knows this block's identity. While the turn runs, it is provisionalTurns[].blockId → promptId. After the turn settles, it is a live locations entry, blockId → turnId. The fix adopts that identity through the existing onSelectPrompt:
const selectedPromptId = promptId ?? user?.promptId ?? indexedTurn?.promptId;
+ // The sender's own live user block never carries a record or prompt id: its
+ // daemon echo is suppressed. Adopt the identity navigation already tracks
+ // for that block so the tab can persist and read history once settled.
+ const adoptedPromptId =
+ recordId || promptId
+ ? undefined
+ : navigation.provisionalTurns.find((turn) => turn.blockId === turnId)
+ ?.promptId;
+ const adoptedRecordId =
+ recordId || promptId || adoptedPromptId
+ ? undefined
+ : [...navigation.locations.values()].find(
+ (location) => location.view === 'live' && location.blockId === turnId,
+ )?.turnId;
+ useEffect(() => {
+ if (adoptedPromptId || adoptedRecordId)
+ onSelectPrompt?.(turnId, adoptedRecordId, adoptedPromptId, promptLabel);
+ }, [adoptedPromptId, adoptedRecordId, onSelectPrompt, turnId, promptLabel]);There are three tests: the running block adopts promptId, the settled block adopts recordId, and a tab that already has an identity is not retargeted. The test mock also gains locations. On 6f68391 the first two fail. With the fix all pass. TurnCallsPanel + App: 1,107/1,107. ESLint --max-warnings 0, Prettier, and web-shell tsc --noEmit are clean. Full patch: fix-sender-identity-r2.patch.
Selector on the real 4,227-turn session
d10c768 |
6f68391 |
|
|---|---|---|
| Options in the DOM on open | 4,227 | 10 (window of ≤ 12, aria-setsize=4227) |
/turn-index requests on open |
17 | 0 |
| Page DOM nodes / JS heap | 18,536 / 104 MB | 1,642 / 38 MB |
| Open → stable | 1,304 ms | 306 ms |
| Home → first prompt rendered | (all options already in the DOM) | 20 ms |
Non-blocking notes
- R1-2 doesn't reach the real failure path. A real foreground subagent failure (the model returns 400) is recorded as a successful tool result carrying
resultDisplay.status: "failed"andterminateReason: "Failed to run subagent: 400 …". On the wire it istool_call_updatewithstatus: "completed", so the newstatus === 'completed'guard still strips its content. Both heads return identical bytes here. No diagnostic is lost:terminateReasonsurvives inrawOutput. But the panel shows a green Completed with no reason (fig. 3). The chat transcript doesn't flag it either, so this is pre-existing ACP status behaviour. The new panel makes it look more authoritative, though. ReadingrawOutput.status === 'failed'fortask_executionrows would fix the badge. - The selector re-requests a page while it is still in flight. In 2/2 runs, the mid-list scroll fetched
start=2000twice.missingPagesgoes from"1800,2000"to"2000"when the first page lands, the effect reruns, andloadOrdinaldoesn't dedupe in-flight loads. - Keyboard focus doesn't follow wheel scrolling. After scrolling the list with the mouse,
aria-activedescendantis null and PageDown→Enter re-selects the old off-screen item (Upgrade @agentclientprotocol/sdk from 0.14.1 to 0.21.0 to unlock 3 session lifecycle methods (resumeSession / closeSession / unstable_forkSession) #4227). Clicking works. - Still open from round 1 (the author deferred these as polish): the wrapped-MCP Arguments envelope and
1 tool calls.
Not covered: the SSH-workspace GET allowlist (R1-1) and goal-runtime replay marking (R1-7) were checked by unit tests only, and so was the diff truncation (R1-51/52). Windows/macOS were not covered.
Evidence (harness, per-turn diffs, API dumps, images): wenshao/qwen-code@d7b8cc9/pr-12466-round2
中文版
维护者验证第二轮:真实 daemon,Linux(6f68391)
这是对第一轮报告(d10c768,评论)的跟进,只覆盖增量部分。
结论与第一轮相同:建议补上一个客户端小修复后合并,或者合并后立即跟进这个修复。 在真实 daemon 上,这次的跟进提交都站得住。服务端读取结果在 4,262 轮上与第一轮逐字节一致。新的提示词选择器在 4,227 轮会话上把开销从 18.5k 个 DOM 节点、104 MB 降到 1.6k 个、38 MB。持久化的面板状态里也不再保存提示词原文。但第一轮报告的问题只修了一半。选择器里的重复条目已经没有了,因为条目现在按序号作键。发送端在自己的消息上打开"查看工具调用"时,这个 tab 仍然没有持久身份。它不会被持久化,刷新页面后面板就关闭了。这一轮结束后也不会读取历史,选择器里当前项也没有对勾。作者的跟进表对应的是 /review 的编号,没有包含这一条,可能只是漏看了。下面针对新代码给出一个 18 行的修复和 3 个测试,已在同一套浏览器环境里验证。
测试环境
- Linux x86_64,Node 22。
d10c768之后依赖锁文件没有变化,所以用硬链接复用了第一轮安装好的node_modules。对6f68391完整执行npm run build和npm run bundle,均为 exit 0。 - 真实
qwen servedaemon 提供自身打包的 Web Shell(生产版dist/web-shell),浏览器是 Playwright 驱动的 Chromium。没有page.route,也没有 mock daemon。 沿用第一轮持久化的会话:脚本化的 OpenAI 兼容模型驱动真实工具执行(shell、文件工具、两个真实 stdio MCP 服务器、子代理、10 个一批的并行调用、循环上限、取消、kill -9),另有预置的 4,227 轮会话。 - 对照组:
d10c768(第一轮 head)、6f68391(当前 head),以及6f68391加下方补丁(只重建了 SPA)。
检查项
| 方面 | 方法 | 结果 |
|---|---|---|
| 服务端读取不变 | 在同一份数据上,分别用 d10c768 和 6f68391 的 daemon 对 5 个会话的每一轮调用 GET /tool-calls。 |
4,262 轮、5,432 个事件逐字节一致。 在 6f68391 上重跑真值比对,每一轮都与原始 JSONL 一致(125/125、241/241、20/20、12/12 次调用;顺序一致,缺失、多余、重复均为 0)。362 个带计时的调用都落在假模型记录的真实时间窗口内,并且与 ui_telemetry 相等。base 录制的旧会话仍然没有伪造任何 startedAt。 |
| 单测(改动文件) | vitest。 | web-shell 1,377/1,377;cli 34/34;acp-bridge 138/138。 |
| 4,227 轮上的选择器(R1-9/36/42–45) | 打开选择器,再测试 Home 键、滚动到列表中部和选择操作。 | 见下表。按需分页有效:滚到第 ~2,000 轮时只加载了 start=1800 和 start=2000 两页,63 ms 渲染完成。用鼠标选 #2005、用 Home→Enter 选 #1,面板都会切换过去,并各读取一次 /tool-calls(200)。 |
| 面板状态不存提示词原文(R1-33) | 打开 tab 后检查 localStorage。 |
tab 只保存 turnId、recordId、promptId,没有 promptLabel。 |
| 包装的 MCP 行(R1-3,即第一轮非阻塞项 1) | 展开经 tool_search → tool_call 调用的 mcp__inventory__lookup_sku。 |
描述行不再显示 tool_call,已修复。"参数"里仍是 {name, arguments} 外层信封(图 3),尚未解决。 |
| 失败 agent 的诊断信息(R1-2) | 新增场景:一个前台子代理,它的模型调用返回 HTTP 400。 | 见下方非阻塞项 1。 |
遗留缺陷:发送端 tab 没有身份
复现: 在输入框发送一条提示词,然后在这条消息上点"查看工具调用"。运行中或结束后点都会出现问题。
PR head 6f68391 |
加补丁后 | |
|---|---|---|
运行中打开(SCN:slow,9 秒) |
未持久化,没有对勾 | 持久化了 {promptId},有对勾 |
| 本轮结束后 | 读取 /tool-calls 0 次(只有实时行) |
读取 1 次(200) |
| 刷新页面 | 面板关闭 | 面板恢复,显示 2 行 |
发送约 1 秒后打开(SCN:after) |
未持久化,读取 0 次,刷新后关闭 | 持久化了 {recordId},读取 1 次,刷新后恢复 |
根因与第一轮相同: 发送端自己的用户回显被压制,这个本地块永远拿不到 sourceRecordIds 或 promptId。因此 App.openTurnCalls 创建的 tab 两个 id 都没有。结果是 serializeArtifactPanelTabs 把它丢弃,历史读取的 effect 因为 selectedRecordId || promptId 都为空而不执行,TurnCallPromptSelect 收到的 recordId 和 promptId 都是 undefined,所以匹配不到当前选中项。其实导航 store 已经知道这个块的身份。运行中时,可以从 provisionalTurns[].blockId 找到 promptId。结束后,可以从 live 的 locations 条目里由 blockId 找到 turnId。补丁通过现有的 onSelectPrompt 把这个身份写回 tab,代码见英文部分。补丁附 3 个测试:运行中的块能拿到 promptId、已结束的块能拿到 recordId、已有身份的 tab 不会被改写。测试 mock 也补上了 locations。在 6f68391 上前两个测试失败,加修复后全部通过。TurnCallsPanel + App 共 1,107/1,107。 ESLint、Prettier、web-shell tsc --noEmit 均无报错。
4,227 轮会话上的选择器
d10c768 |
6f68391 |
|
|---|---|---|
| 打开时 DOM 中的选项数 | 4,227 | 10(窗口最多 12 个,aria-setsize=4227) |
打开时的 /turn-index 请求数 |
17 | 0 |
| 页面 DOM 节点数 / JS 堆 | 18,536 / 104 MB | 1,642 / 38 MB |
| 从打开到稳定 | 1,304 ms | 306 ms |
| 按 Home 到第一条提示词渲染出来 | (所有选项已在 DOM 中) | 20 ms |
非阻塞项
- R1-2 的修复碰不到真实的失败路径。 真实的前台子代理失败(模型返回 400)被记录成一次成功的工具结果,其中带着
resultDisplay.status: "failed"和terminateReason: "Failed to run subagent: 400 …"。线上的tool_call_update是status: "completed",所以新加的status === 'completed'判断仍然会清空它的 content。两个 head 在这里返回的字节完全相同。诊断信息并没有丢,terminateReason还保留在rawOutput里。但面板显示的是绿色 Completed,也不显示失败原因(图 3)。聊天记录里同样没有标出失败,所以这是 ACP 状态层早已存在的行为,只是新面板让它看起来更权威。对task_execution行读取rawOutput.status === 'failed',就能把这个徽标改对。 - 选择器会对还在请求中的页面重复请求。两次运行里,滚到列表中部时
start=2000都被请求了两次。原因是第一页返回后missingPages从"1800,2000"变成"2000",effect 重新执行,而loadOrdinal不会对进行中的请求去重。 - 键盘焦点不跟随滚轮滚动。用鼠标滚动列表后,
aria-activedescendant为 null,按 PageDown→Enter 选中的仍是屏幕外的旧项(Upgrade @agentclientprotocol/sdk from 0.14.1 to 0.21.0 to unlock 3 session lifecycle methods (resumeSession / closeSession / unstable_forkSession) #4227)。鼠标点击则没有问题。 - 第一轮遗留、作者作为细节打磨延后处理的两项:包装 MCP 行"参数"里的外层信封,以及英文单复数
1 tool calls。
未覆盖: SSH 工作区的 GET 放行(R1-1)、goal-runtime 回放标记(R1-7)、diff 截断(R1-51/52)只由单测覆盖。Windows 和 macOS 没有测试。
证据(harness、逐轮比对、API 转储、图片):wenshao/qwen-code@d7b8cc9/pr-12466-round2
🤖 Generated with Claude Code — Claude Opus 5.5 (1M context)
Sender identity and new inline review follow-upFixed in 5aad27e6db. The sender-path defect was missed in the previous follow-up. A locally sent user message has no echoed The three new inline comments were also addressed:
Regression checks failed before the fixes. Sender App tests cover both running and settled opening, stable identity persistence, checked selection, one history read at settlement, and restored content after reload. Panel tests also cover retained live rows, identity/owner guards, loss of client, and index page boundaries. Session tests exercise prepared/unprepared approved MCP metadata before execution and preserve notification-failure execution behavior. Browser E2EVerified the actual sender path in Chromium against the locally built real daemon and bundled Web Shell, with a local scripted OpenAI-compatible model and real shell/glob execution in an isolated temporary workspace. No intercepted browser responses or mock daemon. Running: persisted prompt ID, checked selector, no history polling. After settlement: one tool-calls request returning 200 and two rows. Reload: panel and both rows restored. Screenshots were visually checked. This does not claim a production-model run or repeat the maintainer's large-session survey. Validation: 2,234 scoped tests passed (App 1,035; panel 77; Session 1,052; emitter 70); full build/typecheck/bundle, lint/format and commit hooks passed. Real-daemon browser report and screenshots. Observed separately: a new empty session returned index 404s before prompt admission. The notice recovered after a successful index read; the light screenshot follows a manual index-only refresh while running, with history reads still zero. This initialization behavior is not claimed fixed. Before reload we also closed/reopened from the original sender message and verified record-ID persistence. 中文:上一轮漏掉了发送端回显被压制后的身份衔接,本次已从导航 store 补齐,并验证运行中/结束后打开、持久化、选中、结算读取和刷新恢复。新增三条评论均已修复:清除卡住的加载状态、统一审批后的起始通知、复用索引分页常量。浏览器使用真实 daemon、打包页面和真实工具执行,模型为本地脚本模型;没有用模拟 daemon 或拦截响应替代发送链路。维护者报告中的其他非阻塞建议不列为本次已修复。 |
|
Merged origin/main Validation: 3,395 scoped tests passed; full build, typecheck and bundle passed; commit hooks passed. Two automatically merged stale timing assertions were corrected and the 51-test trajectory suite passed on rerun. Chromium against the real built daemon and a local scripted model verified running selection, one historical read on settlement, canonical recorded timing, reopening the original sender and restoring the panel after reload. Updated screenshots and bilingual evidence are in the PR summary. The initial empty-session index notice is documented as pre-existing, not fixed here. 已在 |
|
Merged origin/main Validation: 1,210 scoped Web Shell tests, full build/typecheck/bundle and commit hooks passed. Real-daemon Chromium verification passed for sender selection, timing replay, reopening and reload restoration, plus rendering/selecting the upstream tool timeline spans. Screenshots and bilingual evidence are updated in the summary. The browser harness needed one correction to reopen the right panel before the new overview check; no product change was needed. 已合并最新主分支,唯一冲突保留上游时间轴实现,GitHub 确认无冲突。1,210 项相关单测及完整构建/类型检查/打包通过;真实 daemon 浏览器验证覆盖工具面板恢复和时间轴点击,截图证据已更新。 |
|
@qwen-code /resolve |
|
Qwen Code attempted to resolve merge conflicts but the run did not complete successfully. Check the workflow run for full logs. |
|
Merged origin/main Validation: 1,379 scoped Web Shell tests passed; full build, typecheck, bundle and commit hooks passed. Real-daemon Chromium verification passed for sender identity, timing replay, reopening, reload restoration and trajectory tool-span selection. Updated screenshots and bilingual evidence are in the summary. Search behavior is covered by unit tests, not claimed as browser-tested in this run. 已合并最新主分支并解决三个文件冲突,保留会话搜索、历史工具调用定位和双方测试。1,379 项相关测试、完整构建/类型检查/打包及提交钩子通过;真实 daemon 浏览器验证也通过,截图证据已更新。 |
Maintainer verification, round 3: real daemon, macOS (
|
| Area | Method | Result |
|---|---|---|
| Sender identity (R2 blocker) | Send from the composer, then open View tool calls on that message at three timings: at ~0 ms (instant), at 1.5 s while running, and after the turn settles. Record localStorage, the selector, the /tool-calls requests, and a page reload. |
Fixed in all three. instant persists {promptId}. At 1.5 s and after settling, the tab persists {recordId}. The selector check mark is correct. There are 0 reads while live and exactly 1 read (200) at settlement. After reload, the panel is restored with 2 rows (fig. 1). |
| Mutation check on the fix | Six single-point mutations of the adoption code in TurnCallsPanel.tsx, each run against TurnCallsPanel.test.tsx + App.test.tsx (1,148 tests). |
5/6 killed. Removing the effect: 4 tests fail. Dropping promptId adoption: 2. Dropping recordId adoption: 2. Dropping the owner guard: 1. Retargeting an existing identity: 1. Survivor: removing location.view === 'live' (note 3). |
| Timing after the merges | Ground truth: for every turn, compare GET …/tool-calls with the raw JSONL. That covers the call IDs between turn anchors and the ui_telemetry started_at_ms/duration_ms. |
PR-recorded sessions: 14 turns, 23 calls. Order matches in 14/14 turns, with 0 missing and 0 extra. All 22 timed calls match exactly. Main-recorded sessions, read by the PR daemon: 16 turns, 26 calls, order 16/16, timing 26/26. This includes a copy with every started_at_ms stripped: 0 fabricated startedAt. |
Approved-call start frame (new shared behaviour in 5aad27e) |
Wire A/B test. A default-mode session gets one shell call that needs approval, answered through POST /session/:id/permission/:requestId. I recorded the SSE stream on the main daemon and on the PR daemon. |
See the table below. The PR adds one tool_call_update in_progress after approval and no second creating tool_call, so no duplicate card. |
| Duplicate prompt text | One session has two identical SCN:parallel prompts. Open the panel from the last one. |
The panel checks option #5 (not #0) and reads that prompt's own turnId. The tooltip times belong to that prompt (fig. 2). |
Search coexistence (merge 2e424ef) |
Diff the resolved TranscriptViewport.tsx against main, then in the browser: Search this conversation → SCN:slow → Enter → View tool calls on the hit. |
Compared with main, the resolution adds only the TurnCallsProvider wrapper and its opener. Main's MessageList props, including the historical overrides, are identical after whitespace normalisation. In the browser, the panel opens the searched prompt (20f3425a) with its 2 rows (fig. 2). |
| Empty-session index notice (documented by the author) | Open the panel on a brand-new session 0 ms and 300 ms after sending. | Not reproduced in 2/2 attempts. Every turn-index request returned 200 and no alert was rendered. |
| Unit tests / static | Every PR-changed test file, per package, plus typecheck, ESLint and Prettier. | web-shell 1,553/1,553, acp-bridge 144/144, core 588/588, sdk 879/879. cli serve + acp-integration: 3,998/3,999; the one failure (acp-output.test.ts "ACP EOF output") fails identically on main. ssh-workspace.test.ts: the same 12 local-environment Git failures on main and on the PR; the PR's tool-calls allowlist case passes. Typecheck exits 0 for all 5 packages. eslint --max-warnings 0 and prettier --check on changed files: clean. CI on 2e424ef is green. |
Approved-call wire A/B
5aad27e removes the didRequestPermission gate. Every non-todo call now goes through emitStart after approval, so the change reaches every ACP client, not just the Web Shell. What each daemon sent for the same call:
| Vote | main 906418a |
PR 2e424ef |
|---|---|---|
| Allow once | tool_call pending → permission → tool_call_update completed |
tool_call pending → permission → tool_call_update in_progress (kind execute, full title, startedAt) → tool_call_update completed (+ startedAt, durationMs) |
| Reject | tool_call pending → permission → tool_call_update failed |
same sequence, plus startedAt and durationMs: 1513 on the failed frame |
The streamed call was prepared, so the start goes out as an update, not as a second creating frame. The Web Shell renders a single card. ChannelBase consumers now see an in_progress transition after approval. Before, they saw only pending → completed. That is a real status change. I think it's correct, and I'm flagging it only because it affects the channels as well. I did not exercise the unprepared-call path (a provider that doesn't stream tool calls).
Non-blocking notes
- A rejected call shows its approval wait as elapsed time. The row reads Cancelled · 2s, and its tooltip gives start/end times for a command that never ran (fig. 2, last panel). The data is honest: the JSONL
ui_telemetryrecord already stores this duration on main (1,523 ms for the same rejection on the main arm; 1,513 ms on the PR arm, equal to its live frame), and the PR's risk section documents that approval wait is part of the scope. Hiding the elapsed badge for rejected or never-executed calls would avoid implying that the command ran for 2 s. - Carried over from round 2, unchanged:
TurnCallPromptSelect.tsxand the server read path are byte-identical to6f68391. So the in-flight page re-fetch, the missing focus follow after wheel scrolling, the green Completed badge on a failed foreground subagent, and the wrapped-MCP{name, arguments}envelope all still apply. So does1 tool calls: the panel header still says it, while the message area already says "1 tool call" (visible in both figures). - Small test gap: no test fails when
location.view === 'live'is removed from the recordId adoption. A test with a historical location sharing the sameblockIdwould pin it.
Not covered this round: Windows and Linux (rounds 1–2 were Linux), a production model, SSH workspaces, and the large-session selector survey (unchanged since round 2).
中文版
维护者验证第三轮:真实 daemon,macOS(2e424ef)
这是第一轮(d10c768)和第二轮(6f68391)的跟进,只覆盖 6f68391 之后的变化:发送端身份修复(5aad27e)、三次合并 origin/main(8fce4a3、20a5809、2e424ef),以及随修复一起进来的一处共享代码改动。前两轮在 Linux 上测,这一轮在 macOS 上测。
结论:第二轮的阻塞问题已修复,我建议合并。 发送端在自己的消息上点「查看工具调用」时,tab 现在有了持久身份。我测了三个时机:刚发送、运行中、结束后。三种情况下身份都会持久化,选择器也都有对勾。运行中读取历史 0 次,结束时恰好 1 次,刷新页面后面板恢复。合并之后,记录的计时仍与原始 JSONL 逐条一致。这对本 PR 录制的会话、main 录制的会话,以及一份去掉了开始时间戳的旧格式副本都成立(伪造的时间戳为 0 个)。下面各项都不阻塞合并。
测试环境
- macOS 26(arm64),Node 24.18.1,pnpm 依赖树(
scripts/setup-worktree.js)。两个隔离 worktree:PR head2e424ef和它的合并基点906418a(即当前origin/main)。两边npm run build && npm run bundle均 exit 0。 - 真实
qwen servedaemon(dist/cli.js)提供它自己打包的 Web Shell,浏览器是 Playwright 驱动的 Chromium。没有page.route,也没有 mock daemon。 模型是本地脚本化的 OpenAI 兼容服务,工具真实执行:shell(串行、并行、失败)、glob,以及通过真实权限接口回应的审批。每个臂有独立的QWEN_HOME/QWEN_RUNTIME_DIR。 - 三个臂:
2e424ef的 daemon;906418a(main)的 daemon;第三个是2e424ef的 daemon 指向 main 录制的数据目录,用来测跨版本读取。 - 装置脚本:
wenshao/qwen-code@109dbe6/pr12466/r3/rig。
检查项
| 方面 | 方法 | 结果 |
|---|---|---|
| 发送端身份(第二轮阻塞项) | 从输入框发送,然后在三个时机点这条消息的「查看工具调用」:约 0 ms(instant)、运行中 1.5 秒、结束后。记录 localStorage、选择器、/tool-calls 请求,以及刷新页面后的状态。 |
三种时机都已修复。 instant 持久化 {promptId};1.5 秒和结束后打开时持久化 {recordId}。选择器对勾正确。运行中读取 0 次,结束时恰好 1 次(200)。刷新后面板恢复,显示 2 行(图 1)。 |
| 修复的变异检验 | 对 TurnCallsPanel.tsx 的身份采纳代码做 6 个单点变异,每个都跑 TurnCallsPanel.test.tsx + App.test.tsx(1,148 个测试)。 |
6 个杀掉 5 个。 去掉 effect:4 个测试失败。去掉 promptId 采纳:2 个。去掉 recordId 采纳:2 个。去掉 owner 守卫:1 个。改写已有身份:1 个。存活的一个是去掉 location.view === 'live'(非阻塞项 3)。 |
| 合并后的计时 | 真值比对:逐轮比较 GET …/tool-calls 与原始 JSONL,包括轮次锚点之间的调用 ID,以及 ui_telemetry 的 started_at_ms/duration_ms。 |
PR 录制的会话: 14 轮、23 次调用,14/14 轮顺序一致,缺失 0、多余 0,22 个计时全部相等。main 录制、由 PR daemon 读取的会话: 16 轮、26 次调用,顺序 16/16,计时 26/26。其中包括一份删掉全部 started_at_ms 的副本:伪造的 startedAt 为 0 个。 |
审批后的起始帧(5aad27e 新增的共享行为) |
线上 A/B:default 模式会话里发一个需要审批的 shell 调用,通过 POST /session/:id/permission/:requestId 投票,分别在 main 和 PR 的 daemon 上录制 SSE。 |
见下表。PR 在批准后多发一帧 tool_call_update in_progress,不会再发一个新建的 tool_call,所以不会出现重复卡片。 |
| 提示词文本重复 | 同一会话里有两条完全相同的 SCN:parallel,从最后一条打开面板。 |
选中的是第 5 项(不是第 0 项),读取用的是这一轮自己的 turnId,悬浮提示里的时间也属于这一轮(图 2)。 |
与会话搜索共存(合并 2e424ef) |
把解完冲突的 TranscriptViewport.tsx 与 main 对比;再在浏览器里操作:「搜索此会话」→ SCN:slow → 回车 → 在命中消息上点「查看工具调用」。 |
与 main 相比,冲突解法只多了 TurnCallsProvider 包裹和打开回调;main 的 MessageList props(包括历史视图的覆盖项)在归一化空白后完全一致。浏览器里,面板打开的正是搜到的那一轮(20f3425a),显示它的 2 行(图 2)。 |
| 空会话索引提示(作者自述) | 在全新会话里,发送后 0 ms 和 300 ms 各打开一次面板。 | 2/2 次都没有复现:所有 turn-index 请求都返回 200,也没有出现错误提示。 |
| 单测 / 静态检查 | PR 改动的所有测试文件(按包跑),以及类型检查、ESLint、Prettier。 | web-shell 1,553/1,553,acp-bridge 144/144,core 588/588,sdk 879/879。cli 的 serve + acp-integration:3,998/3,999,唯一失败的(acp-output.test.ts「ACP EOF output」)在 main 上同样失败。ssh-workspace.test.ts:main 和 PR 上是同样 12 个本机环境导致的 Git 失败,PR 新加的 tool-calls 放行用例通过。5 个包的类型检查都 exit 0。改动文件的 eslint --max-warnings 0 和 prettier --check 都通过。2e424ef 上的 CI 全绿。 |
审批路径的线上 A/B
5aad27e 去掉了 didRequestPermission 这个门,所有非 todo 调用在批准后都会走 emitStart。因此这个改动影响所有 ACP 客户端,不只是 Web Shell。同一个调用,两边 daemon 发出的帧如下:
| 投票 | main 906418a |
PR 2e424ef |
|---|---|---|
| 允许一次 | tool_call pending → 权限请求 → tool_call_update completed |
tool_call pending → 权限请求 → tool_call_update in_progress(kind execute、完整标题、startedAt)→ tool_call_update completed(带 startedAt、durationMs) |
| 拒绝 | tool_call pending → 权限请求 → tool_call_update failed |
同样的序列,failed 帧多带 startedAt 和 durationMs: 1513 |
这个流式调用在批准前已经预先发过帧,所以起始帧以更新形式发出,而不是第二个新建帧;Web Shell 只渲染一张卡片。ChannelBase 的消费方现在会在批准后看到一次 in_progress 状态变化,以前只有 pending → completed。这是真实的状态变化,我认为是对的,这里提出来只是因为它也影响渠道。没有预先发帧的路径(不流式输出工具调用的 provider)这一轮没有测。
非阻塞项
- 被拒绝的调用把审批等待时间显示为耗时。 这一行显示 已取消 · 2s,悬浮提示给出了开始/结束时间,但这个命令从来没有执行过(图 2 最后一栏)。数据本身没问题:main 的 JSONL
ui_telemetry记录里本来就有这个耗时(main 臂同样的拒绝是 1,523 ms;PR 臂是 1,513 ms,与实时帧相等),PR 的风险说明也写明了耗时包含审批等待。如果对被拒绝或从未执行的调用隐藏耗时标记,就不会让人以为命令跑了 2 秒。 - 第二轮遗留,没有变化:
TurnCallPromptSelect.tsx和服务端读取路径与6f68391逐字节相同。因此以下几项仍然存在:进行中的分页被重复请求、滚轮滚动后键盘焦点不跟随、前台子代理失败时仍显示绿色「已完成」、包装的 MCP 行「参数」里仍是{name, arguments}外层信封。英文单复数问题也还在:面板标题仍写1 tool calls,而消息区已经写的是「1 tool call」(两张图里都能看到)。 - 小的测试缺口: 从 recordId 采纳逻辑里去掉
location.view === 'live'后,没有任何测试失败。补一个「历史位置与 live 位置blockId相同」的用例就能钉住它。
本轮未覆盖: Windows 和 Linux(前两轮是 Linux)、生产模型、SSH 工作区,以及大会话选择器的测量(自第二轮以来代码没有变化)。
|
@qwen-code /triage |
qqqys
left a comment
There was a problem hiding this comment.
Independent Critical-only review — head 2e424efa
Not approving, on budget and on one unresolved Critical rather than on a new finding. This is a 7,706-line, 64-file change whose first review round filed 21 Criticals, and I could not independently verify the fixes or scan the feature surface inside the budget.
The one Critical still open: R1-4, disputed and not closed by either side
R1-4 claims the PR reproducibly makes npm test --workspace=packages/web-shell exit non-zero on Linux — base c83265ff exits 0 — via a single unhandled ReferenceError: window is not defined from a @tanstack/virtual-core debounce setTimeout, which vitest attributes to the untouched TrajectoryPanel.test.tsx.
The thread is still unresolved and the two sides have not converged:
- The reporter disclosed that the exact culprit file could not be isolated — no single file and no candidate+victim pair under
--no-file-parallelismreproduces it; it needs the full-suite worker context. The anchor onApp.test.tsxwas named as the most plausible leak source, not a demonstrated one. - The author's rebuttal is that the Linux job for the reviewed SHA passed (360 files / 9,536 tests), a strict local
TrajectoryPanelrun passed 21/21, an isolated probe confirms a pre-existing virtual-core debounce cleanup gap, and the dependency, both virtualizers (MessageList.tsx,TrajectoryPanel.tsx) and the cited victim test are all unchanged from base, with no exception suppression added. It explicitly leaves the finding open pending a reproducible failing full-suite environment or an isolated culprit.
What I can state from my own read at this head: Test (ubuntu-latest, Node 22.x) is completed/success, as are web-shell E2E Smoke (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox) and Real daemon E2E / Java 11, with Test (macos) and Test (windows) skipped rather than failing. So the symptom as stated — a red web-shell suite on Linux CI — is not present at 2e424efa, and the mechanism cited lives in code this diff does not touch.
I am nonetheless not treating R1-4 as confirmed fixed. Nothing was changed to close it, the reporter has not retracted, and I did not run the suite myself to test the local-reproduction claim. Under a Critical-only standard, an unresolved and genuinely disputed blocker that I cannot confirm either way is not something I can approve past.
Twenty resolved Criticals I did not verify in code
Round 1's other Criticals are all on resolved threads with concrete fix replies pinned to 6f68391f00 — the SSH-workspace GET boundary now admitting the new route as a GET-only passthrough with write methods still rejected; wrapper-generated titles resolving to the real tool name instead of the literal tool_call; live elapsed estimates using the browser receipt clock only rather than subtracting a server-clock timestamp from a client-clock Date.now(); and timing-pairing guards accepting either the recorded wrapper name or the resolved tool name while retaining the identity and collision checks.
Those replies are specific and plausible, and the green Linux test job is real evidence for the parts that were test-visible. But a resolved flag plus an author reply is not verification, and I read none of them against the code at head. I am reporting them as unconfirmed rather than as fixed.
Surfaces I did not scan
I performed no independent Critical-only pass over the feature this PR adds. Unread at head: the new daemon tier — packages/cli/src/serve/session-tool-calls.ts and the GET /workspaces/:workspace/session/:id/tool-calls route, including its ownership classification, the 100-page × 250-record scan budget, the two 32 MiB byte budgets, the tool_calls_replay_incomplete code, and its archive-coordination and runtime-resolution wrapper; the SSH-workspace passthrough change itself, which is a trust-boundary widening and deserves a read of its own rather than acceptance on a reply; transcriptToMessages.ts and buildTrajectory.ts wrapper-name resolution; and TurnCallsPanel clock handling. Given that round 1 found 21 Criticals across exactly these areas, I would not want to approve them on someone else's audit.
Also outstanding and disclosed by the author: R1-13, a Suggestion noting that fresh real-daemon verification of the original 57-call/139-call sessions and a many-call server fixture are still open, and that the current browser evidence uses a mock daemon so it does not establish live persistence correctness. I did not gate on it — it is a Suggestion, and the author left it open rather than overstating the evidence, which is the right call — but it means the endpoint's live behaviour is unverified by anyone so far.
CI
Green at head as itemised above; no failure attributable to this diff. CI is not the basis for this verdict, and R1-4 turns on a local full-suite reproduction that CI does not settle either way.
Verdict: COMMENT — One Critical (R1-4) is unresolved and disputed: the claimed red Linux web-shell suite is not reproduced by CI at this head and the cited mechanism sits in unchanged code, but nothing was changed to close it and I could not confirm it either way. The remaining gate is independent verification, at head, of the twenty resolved Criticals and of the new daemon route tier — its ownership scope, scan and byte budgets, and the SSH passthrough widening. Next step: re-request review at this head so a fresh pass can read those surfaces, and for R1-4 either supply the reproducible full-suite environment the reporter asked for or record the pre-existing virtual-core cleanup gap as a separate issue so this thread can be closed on evidence rather than left contested.
|
@qqqys Follow-up to review 5301167259, at head R1-4: The current Linux test job, Web Shell E2E Smoke, and Lint & Static pass. Passing CI does not rule out an intermittent timer leak, but no PR-specific cause has been isolated and the reported failure is not demonstrated by CI at this head. I have replied on the original thread requesting closure as an unconfirmed PR regression, rather than retaining it as a Critical blocker. This is not a claim that the dependency cleanup gap was fixed; a reproducible current-head environment and complete logs would justify reopening it. Correction to the verification summary: The statement that current browser evidence uses only a mock daemon is outdated. Subsequent runs, including this exact head, used the built real local daemon and bundled Web Shell in Chromium, with a local scripted OpenAI-compatible model and actual shell/glob execution, without browser response interception. They verified persistence of the sender prompt identity while running, no tool-history polling during execution, a historical read on settlement, recorded tool timing, reopening from the original sender message, and restoration after reload. The current run also verified trajectory tool-span selection. Bilingual report and limitations, restored panel screenshot. These are real-daemon persistence checks, not production-model or large-session validation. Still outstanding: Fresh verification of the original 57-call/139-call turns and the proposed many-call, multi-page server-reader fixture remain outstanding. The two-call browser scenario does not replace them. Independent verification of the twenty resolved findings and the daemon route/SSH ownership boundaries also remains useful; a resolved thread or author reply is not a substitute for that review. Your review explicitly did not perform that pass, so I understand this as remaining review work rather than a new demonstrated defect. Please update the assessment to reflect the newer real-daemon evidence and reconsider R1-4's blocker classification on the current-head evidence. We are not asking that missing verification be treated as completed, or that an approval be based solely on green CI. 中文补充:R1-4 已在原讨论附上当前提交的绿色 CI,并请求按“尚未证实为本 PR 回归”关闭阻塞项,不声称依赖清理缺口已修复。整体 review 中“目前只有 mock daemon 验证”的信息已过时:当前提交已通过真实本地 daemon、实际 shell/glob 执行的发送端身份、历史计时、重新打开及刷新恢复验证,证据见上方报告。原始 57/139 次调用轮次、多调用跨页服务端用例,以及已修复项和接口归属边界的独立复核仍明确保留为待完成项,不以两次调用的浏览器场景替代。请据此更新验证描述并重新考虑 R1-4 的阻塞级别。 |
chiga0
left a comment
There was a problem hiding this comment.
Scope: Standard tier — new API endpoint, SDK surface expansion, React UI panel, core pipeline fixes. Reviewed: session-tool-calls.ts, the new route in session.ts, loadTurnCalls.ts, turnCallsContext.tsx, TurnCallsPanel.tsx (first half + key state logic), App.tsx, TranscriptViewport.tsx, ArtifactPanel.tsx, MessageItem.tsx/MessageTimestamp.tsx, DaemonClient.ts, types.ts, ui/types.ts, normalizer.ts, transcript.ts, coreToolScheduler.ts, session-transcript-reader.ts, transcript-replay.ts, history-replayer.ts, history-replay-page.ts, toolClassification.ts, transcriptToMessages.ts, buildTrajectory.ts. Not reviewed: TurnCallsPanel.test.tsx (2472 lines, test validity only), TurnCallPromptSelect.tsx (prompt selector UI), SSH-workspace route passthrough in full context, all docs.
No blocking findings.
Non-blocking observations
Session.ts — didRequestPermission guard removed (Minor)
emitStart is now emitted for all non-todo tools, including permission-requested non-agent tools. Previously those tools did not receive a tool.start frame at this call site. Behaviorally this changes when the tool block first appears in the transcript (before vs. after approval). upsertToolBlock handles a duplicate start idempotently via existingId lookup, so this does not corrupt state, but callers relying on "no block before approval" would see a change. Looks intentional given the panel needs in-flight calls to appear immediately.
session-transcript-reader.ts — ??= change (confirm R1-36 as correct)
currentPromptTurn.promptId ??= entry.turnResultPromptId correctly prevents turnResultPromptId from overwriting a daemonPromptId-set value. The new daemonPromptId field is now assigned to turn.promptId first; the ??= ensures the first writer wins rather than the last. No concern.
buildTrajectory.ts + transcript-replay.ts — goal_runtime/goal_control as injected sources
Both INJECTED_USER_SOURCES and isTurnCallsPrompt now exclude goal_runtime/goal_control. The old comment called this a "known gap — costs a spurious turn header." The PR closes it consistently server-side and client-side.
transcriptToMessages.ts — tool_call wrapper title resolution
title: block.title === block.toolName ? toolName : block.title — when the title was auto-generated from the raw tool name, it upgrades to the resolved inner name. When the title was set to something other than the tool name, it's preserved. Correct.
TurnCallsPanel — promptId/promptLabel not forwarded from TranscriptViewport
openViewportTurnCalls has signature (turnId: string) => void and calls openTurnCalls(turnId, recordId) without promptId or promptLabel. App.tsx's openTurnCalls handles this gracefully: it falls back to user?.promptId and user.text from the store. UX-only: the label won't be available until the store lookup resolves.
Cross-check against existing reviews
R1-4/R1-5 (web-shell suite non-zero on Linux): CI passes at 2e424efa on Ubuntu Node 22 (360 files / 9,536 tests). The cited mechanism — @tanstack/virtual-core debounce setTimeout — is in code this diff does not touch. I did not run the suite locally. Cannot confirm or refute the local-reproduction claim; recording as unverified. The CI green is real evidence, the local claim is unresolved.
R1-6 (title stays tool_call for wrapper calls): Fixed by the block.title === block.toolName ? toolName : block.title guard. Confirmed at head.
R1-20/R1-21 (timing-pairing guards): Both buildTrajectory.ts and transcript-replay.ts guards now accept timing.toolName matching either the raw tool name or the resolved inner name (&& logic, correct). Confirmed.
R1-35 (scan continues reading pages after selectionClosed): Records past selectionClosed are intentionally retained — the replay comment says "late interleaved results" need the broader span for result pairing. The byte budget (2 × SESSION_TRANSCRIPT_MAX_EXPANDED_PAGE_BYTES) caps the exposure. I read this as a design choice, not a stop-condition bug.
R1-36 (??= precedence): Confirmed as correct (see note above).
R1-2 / SSH-workspace GET passthrough (not in my read scope): Not independently verified. Existing review thread shows a specific fix was applied at 6f68391f; CI green is supporting evidence but not a substitute for reading that change.
No blockers. No approvalBlockers. Approving.
Reviewed with AI assistance.
qqqys
left a comment
There was a problem hiding this comment.
Correction to my review of head 2e424efa — verdict unchanged (COMMENT)
The head has not moved since my review, so this is not a re-review. It corrects one statement in it that is wrong, records a reclassification I now accept, and restates why the verdict does not change.
I was wrong about the live-daemon evidence
I wrote that current browser evidence uses a mock daemon and drew the conclusion that "the endpoint's live behaviour is unverified by anyone so far." That conclusion is false. I sourced it from the author's earlier disclosure on the R1-13 thread and presented it as the state at this head without checking whether it still held. It does not: runs at this exact head used the built real local daemon and the bundled Web Shell in Chromium, with a local scripted OpenAI-compatible model and actual shell/glob execution and no browser response interception, and they verified sender prompt identity persisting while running, no tool-history polling during execution, a historical read on settlement, recorded tool timing, reopening from the original sender message, restoration after reload, and trajectory tool-span selection.
The author states the limits of that evidence themselves — it is real-daemon persistence checking, not production-model or large-session validation — which is the right framing, and it is narrower than the many-call cases still outstanding. But "real-daemon verified for these behaviours" and "unverified by anyone" are different claims, and mine was the wrong one.
R1-4: I accept the reclassification, and it does not by itself change the verdict
The author asks that R1-4 be closed as an unconfirmed PR regression rather than retained as a Critical blocker, and is explicit that this is not a claim the virtual-core cleanup gap was fixed — a reproducible current-head environment with complete logs would justify reopening it.
That position is consistent with what I found independently: Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke and Lint & Static are green at this head; the reporter disclosed that no single file and no candidate/victim pair under --no-file-parallelism reproduces the failure; and the dependency, both virtualizers and the cited victim test are unchanged from base. I called it "unresolved and disputed" and said I could not confirm it either way. I still cannot confirm a PR-caused regression, and on that evidence I no longer treat it as a blocker I am holding this PR against.
Two things stay true and are worth keeping separate: the thread is still unresolved and the reporter has not retracted, so this is a reclassification on the available evidence rather than a demonstrated closure; and the underlying timer-cleanup gap in the dependency is real and unfixed, which belongs in its own issue rather than in this PR's verdict.
Why the verdict is still COMMENT
Setting R1-4 aside entirely, the gates I named remain open, and the author agrees they do:
- The twenty Criticals from round 1 sit on resolved threads with specific fix replies pinned to
6f68391f00, but I read none of them against the code at head. A resolved flag plus an author reply is not verification. - I performed no independent Critical-only pass over the new daemon tier —
session-tool-calls.tsandGET /workspaces/:workspace/session/:id/tool-calls, its ownership scope, the 100-page × 250-record scan budget, the two 32 MiB byte budgets,tool_calls_replay_incomplete, and the archive-coordination and runtime-resolution wrapper. - The SSH-workspace passthrough change is a trust-boundary widening that deserves its own read rather than acceptance on a reply.
- Fresh verification of the original 57-call/139-call turns and a many-call, multi-page server-reader fixture is still outstanding, as the author states; the two-call browser scenario does not substitute for it.
The author's framing is the same as mine: this is remaining review work, not a newly demonstrated defect, and neither green CI nor a missing pass should be treated as a completed one.
Verdict: COMMENT (unchanged) — I approve of nothing being merged on the strength of this note; it corrects my record and narrows the open gates from three to two. What stands between this PR and an approval from me is an independent read, at head, of the twenty resolved fixes and of the new daemon route tier including the SSH passthrough boundary. Re-request review when that pass can be funded, or point a reviewer at those two surfaces directly.











What this PR does
Adds a session-scoped Tool calls panel opened from a user message. The selected prompt is preselected, with a prompt selector, refresh action and tool-type filter. Rows show localized tool names, descriptions, status, MCP badges and recorded timing. Expanded rows render JSON arguments/results, structured shell output and file diffs, with links to file and agent detail tabs. Panel selection survives reloads.
Running prompts use the existing message stream without polling; historical prompts use a workspace-scoped, complete-turn read API. Tool start timestamps are retained through recording and replay instead of inferring execution times from browser receipt or batch logging. Embedded hosts opt in with
showToolCalls; the standalone page enables it.Why it's needed
Inspecting a prompt currently requires finding tool calls scattered through the conversation. Reading only the live message window also misses historical calls and can associate a selected prompt with the wrong projection. This panel keeps stable prompt/call identities and reports unavailable or incomplete history explicitly.
Reviewer Test Plan
How to verify
showToolCalls.Evidence (Before & After)
Before: no consolidated session tool-call inspector. The initial local panel relied on live blocks and could omit historical calls or show receipt times as execution times. No controlled screenshot of the pre-feature baseline is claimed.
After: the sender path runs in Chromium against the real local daemon and bundled Web Shell, with a local scripted OpenAI-compatible model and real shell/glob execution. Running selection persists, settlement reads history once, reopening the original sender message adopts its record ID, and page reload restores the panel. No mock daemon or browser response interception; no production-model or Git-operation claim. The running screenshot follows a manual index refresh that clears the new-empty-session notice documented in the report.
Latest merge verification (
2e424efa8e): 1,379 scoped Web Shell tests passed across App, the standalone entry, transcript viewport, conversation search, Tool calls and artifact/trajectory panels. Full build/typecheck/bundle and commit hooks passed. Chromium with a real daemon and local scripted model verified sender selection, historical timing, panel restoration and tool-span selection in the overview. Conversation search is covered by unit tests; no browser search verification is claimed. The initial empty-session index notice remains documented, not claimed fixed. Browser evidence and limits. Earlier cross-package validation remains in the PR comments.Tested on
Environment (optional)
macOS, Node.js 22.14.0, pnpm 11.24.0, bundled Web Shell and Chromium with an isolated real daemon and local scripted model. Based on
origin/mainat906418aa9b; structured shell results from #12311 are already in the base branch.Risk & Scope
Design: English · 简体中文. Both versions describe the same behavior, constraints and acceptance criteria.
Linked Issues
No linked issue. Builds on the structured shell result contract from #12311.
中文说明
本 PR 的改动
新增从用户消息打开的会话级“工具调用”面板。默认选中入口所属提示词,提供提示词选择、刷新及工具类型筛选。每行展示国际化工具名、描述、状态、MCP 标签和记录的耗时;展开后展示 JSON 参数与结果、结构化命令输出及文件 diff,并可打开文件和智能体详情页签。刷新页面后保留面板选择。
运行中的提示词复用消息流,不轮询历史;历史提示词通过工作区限定的接口读取完整轮次。工具开始时间贯穿记录与回放,不再用浏览器接收时间或批量写日志时间推测。嵌入宿主通过
showToolCalls显式开启入口,独立页面默认开启。为什么需要
检查一个提示词的工具调用,目前需要在对话中逐条查找。仅使用实时消息窗口还会漏掉历史调用,或将选中的提示词匹配到错误的投影。本面板使用稳定的提示词及调用标识,并明确报告无法获取或不完整的历史。
评审验证计划
如何验证
showToolCalls时不展示入口。证据(前后对比)
之前:没有统一的会话工具调用检查面板。最初的本地面板依赖实时块,会漏掉历史调用或把接收时间当作执行时间;不声称提供受控的功能开发前截图。
之后:在 Chromium 中使用真实本地 daemon 与打包 Web Shell验证发送端链路,模型为本地脚本化 OpenAI 兼容服务,shell/glob 工具真实执行。运行中保存选择,结算后读取一次历史,从原发送消息重新打开会获取 record ID,刷新页面恢复面板。未使用模拟 daemon 或浏览器响应拦截,不代表生产模型或 Git 操作验证。运行中截图拍摄于手动刷新索引清除空会话初始提示后,详情见报告。
最新合并验证(
2e424efa8e):1,379 项 Web Shell 相关测试通过,覆盖 App、独立入口、历史消息视口、会话搜索、工具调用及扩展区/轨迹面板;完整构建、类型检查、打包及提交钩子通过。Chromium 使用真实 daemon 和本地脚本模型,验证发送端选择、历史计时、面板恢复及时间轴工具区间选中。会话搜索由单测覆盖,不声称完成浏览器搜索验证。空会话初始索引提示仍单独记录,不声称已修复。浏览器证据与验证边界。此前跨包验证保留在 PR 评论中。测试平台
环境
macOS、Node.js 22.14.0、pnpm 11.24.0、打包 Web Shell、Chromium、隔离的真实 daemon 及本地脚本模型。已合并
origin/main的906418aa9b;#12311 的结构化命令结果已在基分支中。风险与范围
设计文档:English · 简体中文。两份文档的行为、约束和验收标准保持一致。
关联问题
无关联 issue。基于 #12311 已提供的结构化命令结果约定。