Skip to content

feat(web-shell): per-task token & time detail on completed todos#5118

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
wenshao:feat/todo-task-resource-detail
Jun 15, 2026
Merged

feat(web-shell): per-task token & time detail on completed todos#5118
wenshao merged 5 commits into
QwenLM:mainfrom
wenshao:feat/todo-task-resource-detail

Conversation

@wenshao

@wenshao wenshao commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Expanding a completed task in the web-shell todo list now reveals, on click, when it ran and what it spent: a Time group (the start time, and the end time with the elapsed duration trailing it) plus resource rows for tokens (input / output / cached), API time, and tool time. The data is derived entirely from the transcript — no polling. The agent (cli) keeps a per-session cumulative-usage accumulator and PlanEmitter stamps a snapshot of it onto every todo/plan update's _meta.stats; the SDK's normalizePlanUpdate carries that snapshot into the TodoWrite tool call's rawOutput; the web-shell diffs consecutive todo snapshots for tokens and API time, and sums transcript tool-call durations within each task's window for tool time. The earlier client-side approach (polling getStats() at each todo boundary) was racy — it frequently read identical cumulative totals and showed zeros — and is removed.

Why it's needed

The web-shell todo list previously showed only task status — there was no way to see how long a completed task took, or how many tokens and how much API / tool time it consumed. Per-task visibility makes the cost and timing of each step of a multi-step task legible, both while the agent is running and after the session is resumed.

Reviewer Test Plan

How to verify

Run the web-shell against a daemon, send a prompt that drives a multi-step todo_write plan, and let the agent work through it. Expand the Update Todos tool card, then click a completed task: it expands into a Time group (start, and end with (duration) trailing it) and, when measured, Tokens and Time spent groups. Resume the session with /resume and expand a completed task: start / end / duration and tokens and tool time still show (reconstructed from the transcript), while API time is omitted (it is live-only). A task whose token diff measured nothing shows a "not captured" hint rather than a misleading row of zeros.

Behaviour summary:

Tokens Tool time API time
Live (new session) exact (no race) yes yes
/resume (including older sessions) reconstructed from persisted usage metadata from transcript omitted (live-only)

Evidence (Before & After)

This is a user-visible web-shell change. Schematic of the expanded detail panel (illustrative values; verified by the render test below rather than a screenshot):

Before — the completed row was not expandable, no per-task detail:
  ● Implement parser

After — click a completed task to expand:
  ● Implement parser  ▾
    Time
      Start   12:30:01
      End     12:34:15 (4m 14s)
    Tokens
      Input   12,403
      Output  1,287
      Cached  9,800
    Time spent
      API     18.2s
      Tool    3.4s

A new jsdom render test (TodoView.test.tsx) drives the click-to-expand interaction and asserts the section headers and values; data-layer unit tests cover the snapshot diff, the tool-time windowing, and the /resume path (API time omitted, tokens reconstructed). Real screenshots were not captured in this PR.

Tested on

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

Environment (optional)

Local unit + render tests (vitest) and the web-shell vite build, across cli, sdk-typescript, and web-shell. No live-provider run.

Risk & Scope

  • Main risk or tradeoff: API time is live-only — per-turn durations are not persisted or replayed, so on /resume the API-time row is intentionally omitted (tokens and tool time still reconstruct). The cumulative-usage accumulator is mutated by MessageEmitter and read by PlanEmitter on the same SessionContext; the _meta.stats it stamps on plan updates is additive and ignored by clients that don't read it (e.g. Zed).
  • Not validated / out of scope: no end-to-end run against a live provider in this PR (the logic is covered by unit / render tests); real screenshots are not attached; persisting per-turn telemetry so API time survives /resume is deliberately out of scope.
  • Breaking changes / migration notes: none — the change is additive. It takes effect only after rebuilding the daemon (cli + sdk-typescript) and web-shell, since the snapshot is produced by the agent and consumed by the web-shell. Sessions whose agent never stamped a snapshot degrade gracefully to start / end + tool time.

Linked Issues

None.

中文说明

这个 PR 做了什么

在 web-shell 的待办列表里展开一个已完成的任务后,点击它即可查看该任务何时运行、消耗了什么:一个时间分组(开始时间,以及结束时间后面紧跟的耗时时长),外加 token(输入 / 输出 / 缓存)、API 时间工具时间 三类资源行。数据完全从 transcript 推导,无轮询。Agent(cli)维护一个 per-session 的累计用量累加器,PlanEmitter 在每个 todo/plan 更新的 _meta.stats 上盖一份它的快照;SDK 的 normalizePlanUpdate 把该快照透传进 TodoWrite 工具调用的 rawOutput;web-shell 对相邻的 todo 快照做差分得到 token 与 API 时间,并按每个任务的时间窗口对 transcript 里的工具调用时长求和得到工具时间。之前的客户端做法(在每个 todo 边界轮询 getStats())存在竞态——经常读到相同的累计值、显示为 0——已被移除。

为什么需要

web-shell 的待办列表此前只显示任务状态,无法看到一个已完成任务花了多久、用了多少 token、消耗了多少 API / 工具时间。逐任务的可见性让多步任务里每一步的成本与耗时变得清晰,无论是 agent 运行中还是会话恢复后。

评审验证计划

如何验证

让 web-shell 连上 daemon,发送一个会驱动多步 todo_write 计划的提示,让 agent 跑完。展开 Update Todos 工具卡片,再点击一个已完成的任务:它会展开成时间分组(开始,以及结束后紧跟 (时长)),并在有数据时展示Token耗时分组。用 /resume 恢复会话后展开一个已完成任务:开始 / 结束 / 时长 与 token工具时间 仍会显示(从 transcript 重建),而 API 时间被省略(它是 live-only)。差分为零的任务会显示"未采集"提示,而不是一排误导性的 0。

行为总览:

Token 工具时间 API 时间
Live(新会话) 精确(无竞态)
/resume(含旧会话) 从持久化的 usage 元数据重建 从 transcript 省略(live-only)

证据(前后对比)

这是一个用户可见的 web-shell 改动。展开后明细面板的示意(数值为示意,实际由下面的 render 测试验证,而非截图):

改动前——已完成行不可展开,无逐任务明细:
  ● Implement parser

改动后——点击已完成任务即可展开:
  ● Implement parser  ▾
    时间
      开始   12:30:01
      结束   12:34:15 (4m 14s)
    Token
      输入   12,403
      输出   1,287
      缓存   9,800
    耗时
      API    18.2s
      工具   3.4s

新增的 jsdom render 测试(TodoView.test.tsx)驱动点击展开交互并断言分组标题与数值;数据层单测覆盖快照差分、工具时间窗口、以及 /resume 路径(API 时间省略、token 重建)。本 PR 未附真实截图。

测试平台

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

运行环境(可选)

本地单测 + render 测试(vitest),以及 web-shellvite 构建,覆盖 clisdk-typescriptweb-shell 三个包。未做真实 provider 的运行。

风险与范围

  • 主要风险 / 权衡:API 时间是 live-only——per-turn 时长不持久化、也不回放,因此 /resume 时 API 时间行被有意省略(token 与工具时间仍可重建)。累计用量累加器由 MessageEmitter 修改、由 PlanEmitter 读取,二者共享同一个 SessionContext;它盖在 plan 更新上的 _meta.stats 是增量字段,不读取它的客户端(如 Zed)会忽略。
  • 未验证 / 范围外:本 PR 未做针对真实 provider 的端到端运行(逻辑由单测 / render 测试覆盖);未附真实截图;持久化 per-turn 遥测以让 API 时间在 /resume 后存活,属有意的范围外。
  • 破坏性变更 / 迁移说明:无——改动是增量的。需要重建 daemon(cli + sdk-typescript)与 web-shell 后才生效,因为快照由 agent 产生、由 web-shell 消费。agent 从未盖过快照的会话会优雅降级为只显示 开始 / 结束 + 工具时间。

关联 Issue

无。

image

Expanding a completed task in the todo list now reveals when it ran (start /
end / duration) and what it spent: input / output / cached tokens, API time,
and tool time.

The agent stamps a cumulative-usage snapshot onto each todo (plan) update via
`_meta.stats`; the SDK normalizer carries it into the TodoWrite tool call's
rawOutput, and the web-shell diffs consecutive snapshots for tokens and API
time while summing transcript tool durations for tool time.

Works live (no polling race) and on /resume: tokens and tool time are
reconstructed from persisted usage metadata, while API time is live-only since
per-turn durations are not replayed. Sessions whose agent never stamped a
snapshot degrade gracefully to start/end + tool time.
@wenshao

wenshao commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hey @wenshao, thanks for the PR — the feature itself looks interesting!

However, the PR body doesn't follow the PR template. Several required sections are missing:

  • What this PR does — you have "What" but the heading doesn't match
  • Why it's needed — motivation / user-facing benefit
  • Reviewer Test Plan — how to verify, before/after evidence, tested-on OS table
  • Risk & Scope — tradeoffs, out-of-scope items, breaking changes
  • Linked Issues — related issue references

Could you update the PR body to match the template? It helps reviewers (and future-you) understand the change quickly. Happy to re-triage once updated.

中文说明

你好 @wenshao,感谢这个 PR!

不过 PR 描述没有按照 PR 模板 来写,缺少以下必填部分:

  • What this PR does — 标题不完全匹配
  • Why it's needed — 动机和用户价值
  • Reviewer Test Plan — 验证步骤、前后对比、测试平台
  • Risk & Scope — 权衡、范围外内容、破坏性变更
  • Linked Issues — 关联的 issue

请按模板更新 PR 描述,更新后我会重新 triage。

Qwen Code · qwen3.7-max

Trail the elapsed duration after the end time as a dimmed parenthetical
("12:34:15 (4m 14s)") instead of a separate row, since it's derived from the
start/end pair.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@wenshao

wenshao commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Updated the PR body to follow the template: added What this PR does, Why it's needed, a full Reviewer Test Plan (how-to-verify, before/after schematic, tested-on table, environment), Risk & Scope, and Linked Issues, with a complete paragraph-by-paragraph Chinese translation. Thanks for the pointer — re-triage when you get a chance.

@qwen-code /triage

中文

已按模板更新 PR 描述:补齐了 What this PR doesWhy it's needed、完整的 Reviewer Test Plan(如何验证、前后示意、测试平台表、运行环境)、Risk & ScopeLinked Issues,并附逐段对应的完整中文翻译。感谢提醒,麻烦重新 triage。

Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/components/messages/TodoView.tsx
Comment thread packages/web-shell/client/utils/todos.ts Outdated
…ollow-ups

- computeTodoDetails: when a completed id+content key restarts as in_progress (positional plan-N ids repeat across plans), reset the window so the new task diffs its own start instead of the prior task's far-earlier boundary — which rendered a cross-plan window with wildly inflated token/time numbers. Correct the todoStateKey JSDoc accordingly.
- Tool time: sort spans once and binary-search the task window instead of an O(todos x spans) scan per completed task.
- Tests: add the reuse-reset case, a windowed tool-time case, an SDK-normalizer -> extractTodoStats contract test (locks the stats passthrough so a field rename fails loudly), and a stopPropagation test (expander click must not bubble to the tool-row header).
…ust direct

Track keys that have ever reached 'completed' instead of checking prev === 'completed': a reopened task can pass through 'pending' (completed → pending → in_progress), where prev at the re-activation is 'pending' and the direct check missed it, leaving a stale baseline that diffs across both runs. A pause/resume that never completed (in_progress → pending → in_progress) still keeps its first baseline, so its diff captures the whole task.
@wenshao

wenshao commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR @wenshao!

Template looks good ✓ — all required sections present, bilingual, thorough.

Direction: Per-task cost visibility in the todo list fills a real gap — the web-shell showed task status but nothing about what each step consumed. This is exactly the kind of observability that makes multi-step agent runs legible. Fits web-shell's role well. No direct Claude Code CHANGELOG reference, but the area (usage visibility, session stats) is squarely in scope.

Approach: The data pipeline is clean and deterministic: cumulative accumulator → snapshot stamped on plan update → SDK pass-through → client-side diff. The previous polling approach was clearly racy, and replacing it with transcript-derived data is the right call. The scope touches exactly the layers it needs to (cli emitter, sdk normalizer, web-shell UI), each change proportional.

One bundled change worth noting: the i18n terminology shift (令牌 → Token) rides along with the feature. It's minor and probably correct (Token is more standard in developer-facing Chinese), but it's a separate concern. Not a blocker.

Moving on to code review. 🔍

中文说明

感谢贡献 @wenshao

模板完整 ✓——所有必填章节齐全,中英双语,内容充实。

方向: 逐任务成本可见性填补了 web-shell 的一个实际缺口——此前只能看到任务状态,无法知道每一步消耗了什么。这正是让多步 agent 运行变得可读的那种可观测性。符合 web-shell 的定位。Claude Code CHANGELOG 中没有完全对应的功能,但用量可见性 / 会话统计方向完全在范围内。

方案: 数据管线干净且确定性:累加器 → plan 更新时打快照 → SDK 透传 → 客户端差分。之前的轮询方式明显存在竞态,改用 transcript 推导是正确选择。范围恰好覆盖了需要改动的层(cli emitter、sdk normalizer、web-shell UI),每处改动比例得当。

一个附带改动值得记录:i18n 术语变更(令牌 → Token)随主功能一起进来。改动不大且可能更准确,但和主功能是独立的关注点。不阻塞。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Code review: clean — no correctness bugs, security issues, or convention violations found.

The data pipeline is well-structured across three layers:

  • CLI: MessageEmitter accumulates usage with NaN/Infinity guards (the addFinite helper prevents a single bad provider response from corrupting the running total forever). PlanEmitter snapshots a copy (not a reference) of the accumulator onto _meta.stats. The ordering invariant — usage must advance before the plan snapshots it — is documented in comments at both call sites.
  • SDK: normalizePlanUpdate is a minimal, safe pass-through: extract _meta.stats if present, fold it into rawOutput. Two lines of logic, tested.
  • Web-shell: computeTodoDetails is the most complex piece — a state machine that tracks task windows, diffs cumulative snapshots, and sums tool-time within each window using binary search. It handles the tricky cases correctly: ID reuse after completion (resets the window), task reopening via pending (resets via completedKeys set), stats-less start baselines (upgrades to a later snapshot that has stats).

The TodoFullList component correctly uses stopPropagation on the expander button so clicking a task detail never collapses the parent tool row. The todoDetailSignature memoization keeps the detail map referentially stable across unrelated streaming ticks.

One observation (not a blocker): the expanded state in TodoFullList is keyed by todo.id || String(index). If todos were reordered or removed, an expanded state could attach to the wrong row. In practice, todos are append-only during a session so this won't bite — but worth being aware of.

Testing: this PR modifies the web-shell (a browser-based UI), so tmux-based real-user testing isn't applicable — you can't drive a browser UI from a terminal session. Verification was done via unit/render tests across all three affected packages.

Unit test results (verified on PR branch):

packages/cli — MessageEmitter.test.ts + PlanEmitter.test.ts
  Test Files  2 passed (2)
  Tests       39 passed (39)

packages/sdk-typescript — daemonUi.test.ts
  Test Files  1 passed (1)
  Tests       230 passed (230)

packages/web-shell — todos.test.ts + TodoView.test.tsx + PlanMessage.test.tsx + ToolGroup.test.tsx
  Test Files  4 passed (4)
  Tests       94 passed (94)

Total: 363 passed, 0 failed

The new tests cover the full data path: accumulator correctness (including NaN/Infinity rejection), snapshot stamping and copy semantics, SDK round-trip, snapshot diffing, tool-time windowing with binary search, the resume path (API time omitted, tokens reconstructed), ID reuse after completion, task reopening, the not-captured hint, click-to-expand interaction, and stopPropagation.

中文说明

代码审查: 无问题——未发现正确性 bug、安全隐患或违反项目规范的情况。

数据管线在三层间结构清晰:

  • CLI: MessageEmitter 使用 addFinite 累加用量,防止 NaN/Infinity 污染累计值。PlanEmitter 对累加器做拷贝(而非引用)打快照。排序不变量——用量必须在 plan 打快照之前推进——在两处调用点均有注释记录。
  • SDK: normalizePlanUpdate 是最小化的安全透传:提取 _meta.stats 并折入 rawOutput。两行逻辑,有测试覆盖。
  • Web-shell: computeTodoDetails 是最复杂的部分——一个状态机,追踪任务窗口、差分累计快照、并用二分查找对窗口内的工具时间求和。正确处理了各种边界情况:完成后 ID 复用(重置窗口)、经 pending 重开任务(通过 completedKeys 重置)、无统计的起始基线(升级到后续有统计的快照)。

TodoFullList 组件正确地在展开按钮上使用 stopPropagation,防止点击任务明细时折叠父级工具行。todoDetailSignature 的记忆化保持 detail map 在不相关的流式更新中引用稳定。

一个观察(不阻塞): TodoFullList 中的 expanded 状态以 todo.id || String(index) 为键。如果 todos 被重新排序或删除,展开状态可能附着到错误的行。实际中 todos 在会话期间是只增的,所以不会出问题——但值得注意。

测试: 该 PR 改动的是 web-shell(浏览器 UI),无法通过 tmux 做真实用户测试。验证方式为对三个受影响包的单测 / render 测试。

单测结果:363 项全部通过,0 项失败。新增测试覆盖了完整数据路径:累加器正确性(含 NaN/Infinity 拒绝)、快照打戳与拷贝语义、SDK 往返、快照差分、二分查找工具时间窗口、resume 路径、完成后 ID 复用、任务重开、"未采集"提示、点击展开交互、以及 stopPropagation。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Stepping back to look at the whole picture: this PR solves a genuine gap in the web-shell — the todo list showed task status but nothing about cost or timing, which is exactly the kind of visibility you need when reviewing a multi-step agent run. The approach is the right one: stamp cumulative usage on plan updates, diff on the client, derive everything from the transcript. No polling, no races, works on resume.

The implementation is thorough without being overengineered. The state machine for task windows handles the tricky cases (ID reuse after completion, reopened tasks, resume without API time) and the tests prove it — 363 tests across the three touched packages, all passing. The binary search for tool-time windowing is a nice efficiency touch. The NaN/Infinity guard in the accumulator is the kind of defensive code that actually matters — one bad provider response shouldn't corrupt the entire session's stats.

Resume caveat: a prior real-browser E2E validation (by a separate reviewer) confirmed that on cold resume, tokens reconstruct correctly and API time is correctly omitted — but duration and tool time do not reconstruct because the daemon replay re-stamps every todo update at replay time, collapsing task windows to ~0 width. The PR description's behaviour table overstates this: it says duration and tool time "still show" on resume, but in practice they degenerate. This is a documentation accuracy issue, not a code bug — the feature's primary value (per-task token/cost visibility) does work on resume. Worth either correcting the table or preserving original transcript timestamps on replay, but not a merge blocker.

The bundled i18n terminology change (令牌 → Token) is minor and doesn't affect the feature. The code is additive and backward-compatible — sessions from older agents degrade gracefully.

LGTM, approving. ✅

中文说明

整体回顾:该 PR 填补了 web-shell 的一个实际缺口——待办列表只显示任务状态,无法看到成本和耗时,而这正是审查多步 agent 运行时最需要的可见性。方案正确:在 plan 更新时打累计用量快照,在客户端做差分,一切从 transcript 推导。无轮询、无竞态、resume 也能工作。

实现充分而不过度工程化。任务窗口的状态机处理了各种边界情况(完成后 ID 复用、任务重开、resume 时无 API 时间),测试充分验证了这些——三个包共 363 项测试全部通过。工具时间窗口的二分查找是个不错的效率优化。累加器中的 NaN/Infinity 防护是真正重要的防御性代码——一次 provider 异常不应污染整个会话的统计。

Resume 注意事项: 此前的真实浏览器端到端验证(由独立审查者完成)确认冷恢复时 token 正确重建、API 时间正确省略——但时长和工具时间不会重建,因为 daemon 回放按回放时刻重新打戳,导致任务窗口塌缩到 ~0 宽。PR 描述的行为表高估了这一点:它称时长和工具时间在 resume 后"仍会显示",但实际上会退化。这是文档准确性问题,不是代码 bug——功能的主要价值(逐任务 token/成本可见性)在 resume 下确实可用。建议更正表格或在回放时保留原始 transcript 时间戳,但不阻塞合并。

附带的 i18n 术语变更(令牌 → Token)影响很小,不影响主功能。改动是增量的、向后兼容——旧 agent 的会话会优雅降级。

LGTM,通过。✅

Qwen Code · qwen3.7-max

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@wenshao

wenshao commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Verification report — real browser E2E (the live run this PR skipped) + data-layer reverse-audit

I validated this PR end-to-end by building the full daemon + web-shell from the branch and driving it in a real Chromium browser against a live qwen serve daemon + vite web-shell over tmux, with glm-4.7 working through multi-step todo_write plans. This covers the gap the PR notes ("no end-to-end run against a live provider … real screenshots are not attached"). TL;DR: the feature works — per-task token & time detail renders correctly end-to-end. I recommend merge. One minor resume-path observation and two negligible heuristic notes below.

How it was validated

Area Method Result
Unit + render tests vitest across the 3 changed packages 359/359 green — web-shell 91 (todos 61 + TodoView 4 + PlanMessage 6 + ToolGroup 20), cli emitters 38, sdk daemonUi 230
Data layer independent read + adversarial test of todos.ts correct; 2 negligible tool-time edge cases (below)
Live browser E2E built core+acp-bridge+sdk+cli+web-shell; qwen serve + vite + Playwright/Chromium; glm-4.7 ran real multi-step todo plans ALL-PASS — Time / Tokens / Time spent render with real per-task numbers
Resume path restarted the daemon (cold reload from transcript) + reattached in-browser tokens reconstruct ✓, API time omitted ✓ (by design); duration + tool-time do not reconstruct (Finding 1)

Live E2E evidence (real rendered detail panels, glm-4.7 via the daemon)

After the agent completed a plan, expanding a completed task renders exactly the designed layout. Real captured output:

Read package json  ▾
  Time         Start 13:07:07   End 13:07:10 (3.3s)
  Tokens       Input 57,485     Output 132    Cached 57,216
  Time spent   API 3.3s         Tool 16ms
Read readme  ▾
  Time         Start 13:07:10   End 13:07:13 (3.3s)
  Tokens       Input 58,146     Output 133    Cached 57,856
  Time spent   API 3.3s         Tool 23ms
Count deps  ▾
  Time         Start 13:07:13   End 13:07:15 (1.7s)
  Tokens       Input 29,347     Output 127    Cached 29,184
  Time spent   API 1.7s                       ← no Tool row (this step ran no tool)

The numbers are real and correctly differentiated per task (the no-file "Count deps" step has lower tokens and no Tool row). I also instrumented computeTodoDetails in the running app and confirmed it derives these from the daemon's stamped _meta.stats snapshots: every TodoWrite message arrives with hasStats:true + a timestamp, and consecutive snapshots diff into the per-task {inputTokens, cachedTokens, outputTokens, apiTimeMs, toolTimeMs}. The full data path (cli MessageEmitter/PlanEmitter → ACP _meta.stats → sdk normalizePlanUpdate → web-shell todos.ts/TodoView) works as described, with no polling.

The "not captured" hint and compact-mode/expand wiring also behave as designed (a completed task with no measured tokens shows the hint, not a row of zeros).

Resume path

Cold resume (daemon restarted so it reloads from the persisted transcript, then reattached in-browser): tokens reconstruct and the API-time row is correctly omitted (verdict: API-OMITTED-AS-EXPECTED, 0 API rows across the sampled completed tasks, token rows present). This matches the design — the fresh accumulator in acpAgent.ts advances tokens from replayed usage metadata while apiTimeMs only moves on a live per-turn durationMs.

🟠 Finding 1 — on cold resume, duration and tool time do not reconstruct (only tokens do) · Low–Medium

The PR's behaviour table says that on /resume "tokens … from persisted usage metadata" and "tool time … from transcript" reconstruct, and that "start / end / duration … still show". I measured a cold resume (daemon restarted → reloads from the persisted transcript → reattached in-browser) across all 83 completed tasks:

  • ✅ tokens reconstruct, ✅ API row omitted (both correct, by design)
  • duration collapses — every task renders Start 13:10:52 End 13:10:52 (0s) (a few (1ms)): start/end show but at replay-time, not the original wall-clock, so durations are degenerate
  • tool time is absent0 of 83 blocks show a Tool row, even for the stable-title tasks that showed Tool 16ms / 23ms in the live run

Root cause: the daemon replay re-stamps every todo update (and tool call) at replay time, so the per-task windows collapse to ~0 width; tokens survive because they're cumulative (the diff is timing-independent), but duration and the transcript tool-time windowing both depend on wall-clock timestamps that the replay overwrites. The feature's primary value (per-task token/cost visibility) does work on resume — but the PR description overstates it: on a cold resume, duration and tool time do not reconstruct. Worth either preserving the transcript's original timestamps on replay, or correcting the behaviour table to "tokens only" for resume.

⚪ Finding 2 — tool-time attribution heuristic (negligible) · Info

sumToolTimeInWindow attributes a tool span to the task whose window contains the span's start, and counts the span's full duration. Two consequences, both confirmed with a focused test: (a) a span that starts inside a task but ends after the task completed is counted in full, so tool time can exceed the task's wall-clock duration; (b) a span starting exactly on a shared end==start boundary between two adjacent tasks is counted in both. Both require timing coincidences that don't arise in normal operation (the agent marks a task complete only after its tool returns), so this is a non-issue — noting only for completeness. The documented "reworded-todo / reused id+content" trade-offs in todoStateKey are honest and held up in testing (stable titles key and render correctly).

Recommendation

Approve / mergeable. The core feature is correct and now has a real live-provider E2E behind it (Time + Tokens + API + Tool, with proper graceful degradation). Finding 1 is a small resume-only timing caveat; Finding 2 is informational.

Validated at 54b9654e2. Stack: qwen serve daemon (glm-4.7/GLM_API_KEY) + vite web-shell + Playwright Chromium over tmux. Note: ui.compactMode must be off (or toggled via Ctrl+O) for the TodoWrite card body to render, and each card is expanded by clicking its header. Local screenshots captured but not attached.

🇨🇳 中文版验证报告(点击展开)

✅ 验证报告 —— 真实浏览器端到端(补上本 PR 未做的 live 运行)+ 数据层反向审计

我从分支构建了完整的 daemon + web-shell,并在 真实 Chromium 浏览器 中、通过 tmux 连上 live 的 qwen serve daemon + vite web-shell,用 glm-4.7 跑多步 todo_write 计划做了端到端验证。这正好补上 PR 自述的缺口("未做针对真实 provider 的端到端运行……未附真实截图")。结论:功能可用 —— 逐任务的 Token 与耗时明细端到端渲染正确,建议合并。 下面有一个 resume 路径的小观察和两个可忽略的启发式说明。

验证方式

范围 方法 结果
单测 + render 测试 对 3 个改动包跑 vitest 359/359 通过 —— web-shell 91(todos 61 + TodoView 4 + PlanMessage 6 + ToolGroup 20)、cli emitters 38、sdk daemonUi 230
数据层 独立通读 + 对 todos.ts 的对抗测试 正确;2 个可忽略的工具时间边角(见下)
真实浏览器 E2E 构建 core+acp-bridge+sdk+cli+web-shell;qwen serve + vite + Playwright/Chromium;glm-4.7 跑真实多步 todo 计划 全部通过 —— 时间 / Token / 耗时 都带真实逐任务数值渲染
Resume 路径 重启 daemon(从 transcript 冷加载)后在浏览器重新接入 Token 重建 ✓、API 时间被省略 ✓(符合设计);时长有下述注意点

Live E2E 证据(真实渲染的明细面板,经 daemon 的 glm-4.7

Agent 跑完计划后,展开一个已完成任务即渲染出设计中的布局。真实抓取的输出:

Read package json  ▾
  时间    开始 13:07:07   结束 13:07:10 (3.3s)
  Token   输入 57,485     输出 132    缓存 57,216
  耗时    API 3.3s        工具 16ms
Read readme  ▾
  时间    开始 13:07:10   结束 13:07:13 (3.3s)
  Token   输入 58,146     输出 133    缓存 57,856
  耗时    API 3.3s        工具 23ms
Count deps  ▾
  时间    开始 13:07:13   结束 13:07:15 (1.7s)
  Token   输入 29,347     输出 127    缓存 29,184
  耗时    API 1.7s                    ← 无"工具"行(该步未调用工具)

数值真实且逐任务正确区分(不读文件的 "Count deps" 步 token 更少、且没有"工具"行)。我还在运行中的应用里给 computeTodoDetails 加了探针,确认这些数据确实来自 daemon 盖的 _meta.stats 快照:每个 TodoWrite 消息都带 hasStats:true + 时间戳,相邻快照差分得到逐任务的 {inputTokens, cachedTokens, outputTokens, apiTimeMs, toolTimeMs}。整条数据链(cli MessageEmitter/PlanEmitter → ACP _meta.stats → sdk normalizePlanUpdate → web-shell todos.ts/TodoView)如描述般工作,无轮询。"未采集"提示与 compact 模式/展开的接线也都符合设计(无可测 token 的已完成任务显示提示,而非一排 0)。

Resume 路径

冷恢复(重启 daemon 使其从持久化 transcript 重载,再在浏览器重新接入):Token 成功重建,且 API 时间行被正确省略(结论 API-OMITTED-AS-EXPECTED,抽样的已完成任务里 0 个 API 行、Token 行存在)。这与设计一致 —— acpAgent.ts 里全新的累加器从回放的 usage 元数据推进 token,而 apiTimeMs 只在有 live 的 per-turn durationMs 时才前进。

🟠 发现 1 —— 冷恢复时"时长"与"工具时间"都不会重建(只有 token 重建)· 低–中

PR 的行为表称 /resume 后 "token(从持久化 usage 元数据)" "工具时间(从 transcript)" 都会重建,且 "开始 / 结束 / 时长……仍会显示"。我对一次冷恢复(重启 daemon → 从持久化 transcript 重载 → 浏览器重新接入)在全部 83 个已完成任务上做了测量:

  • ✅ token 重建、✅ API 行省略(均正确,符合设计)
  • 时长塌缩 —— 每个任务都渲染成 开始 13:10:52 结束 13:10:52 (0s)(少数 (1ms)):开始/结束确实显示,但是回放时刻而非原始墙钟时间,时长因此退化
  • 工具时间缺失 —— 83 个明细块里 0 个有"工具"行,即便是 live 运行中显示过 工具 16ms / 23ms 的稳定标题任务

根因:daemon 回放把每个 todo 更新(及工具调用)都按回放时刻重新打戳,于是逐任务窗口塌缩到 ~0 宽;token 因为是累计量(差分与时间无关)得以幸存,而时长与 transcript 工具时间窗口都依赖被回放覆盖掉的墙钟时间戳。功能的主要价值(逐任务 token/成本 可见性)在 resume 下确实可用 —— 但 PR 描述高估了:冷恢复时时长与工具时间并不会重建。建议要么在回放时保留 transcript 原始时间戳,要么把行为表的 resume 一栏更正为"仅 token"。

⚪ 发现 2 —— 工具时间归属启发式(可忽略)· 提示

sumToolTimeInWindow 把一个工具 span 归到其起始落入窗口的那个任务,并计入 span 的完整时长。两个后果(均用定向测试确认):(a) 起始在任务内、但结束在任务完成之后的 span 被全额计入,于是工具时间可能超过任务墙钟时长;(b) 起始恰好落在两个相邻任务 end==start 边界上的 span 会被双方各计一次。两者都需要正常运行中不会出现的时间巧合(agent 只会在工具返回后才标记任务完成),故为非问题,仅作完整性记录。todoStateKey 中已记录的"改写 todo / 复用 id+content"权衡是诚实的,且在测试中成立(标题稳定时键与渲染都正确)。

建议

通过 / 可合并。 核心功能正确,且现在有了真实 provider 的 live E2E 背书(时间 + Token + API + 工具,并有恰当的优雅降级)。发现 1 是 resume 专属的小时间问题;发现 2 是提示性的。

基于 54b9654e2 验证。栈:qwen serve daemon(glm-4.7/GLM_API_KEY)+ vite web-shell + Playwright Chromium,经 tmux。注意:ui.compactMode 需为关闭(或用 Ctrl+O 切换)才能渲染 TodoWrite 卡片主体,且每张卡片需点其标题展开。本地已截图但未附上。

Comment thread packages/cli/src/acp-integration/session/emitters/MessageEmitter.ts Outdated
Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/utils/todos.ts Outdated
Comment thread packages/cli/src/acp-integration/session/emitters/PlanEmitter.ts
…al snapshots

- MessageEmitter: only fold finite usage/duration values into the cumulative accumulator. A NaN/Infinity (incl. a NaN that survives `?? 0`) would poison the running total forever, making every later snapshot fail extractTodoStats and silently show 'not captured' for the rest of the session.
- extractTodoStats: require the token fields but default the live-only apiTimeMs to 0 when absent/non-finite, so a snapshot that omits it keeps its valid token counts instead of being dropped whole.
- computeTodoDetails: gate the start baseline on the stored value, not Map.has — a stats-less start (e.g. a plain plan message) recorded undefined, which Map.has treated as already-set, blocking a later stats-bearing snapshot from upgrading the baseline.
- Document the MessageEmitter-before-PlanEmitter ordering invariant in both emitters.
@wenshao

wenshao commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No review findings. LGTM! ✅ — qwen3.7-plus via Qwen Code /review

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No review findings. LGTM! ✅ — qwen3.7-plus via Qwen Code /review

@wenshao
wenshao merged commit 6677ca1 into QwenLM:main Jun 15, 2026
67 of 68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants