feat(web-shell): per-task token & time detail on completed todos#5118
Conversation
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.
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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 review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
|
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 does、Why it's needed、完整的 Reviewer Test Plan(如何验证、前后示意、测试平台表、运行环境)、Risk & Scope、Linked Issues,并附逐段对应的完整中文翻译。感谢提醒,麻烦重新 triage。 |
…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.
|
@qwen-code /triage |
|
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 |
|
Code review: clean — no correctness bugs, security issues, or convention violations found. The data pipeline is well-structured across three layers:
The One observation (not a blocker): the 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): 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、安全隐患或违反项目规范的情况。 数据管线在三层间结构清晰:
一个观察(不阻塞): 测试: 该 PR 改动的是 web-shell(浏览器 UI),无法通过 tmux 做真实用户测试。验证方式为对三个受影响包的单测 / render 测试。 单测结果:363 项全部通过,0 项失败。新增测试覆盖了完整数据路径:累加器正确性(含 NaN/Infinity 拒绝)、快照打戳与拷贝语义、SDK 往返、快照差分、二分查找工具时间窗口、resume 路径、完成后 ID 复用、任务重开、"未采集"提示、点击展开交互、以及 stopPropagation。 — Qwen Code · qwen3.7-max |
|
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
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
✅ Verification report — real browser E2E (the live run this PR skipped) + data-layer reverse-auditI 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 How it was validated
Live E2E evidence (real rendered detail panels,
|
| 范围 | 方法 | 结果 |
|---|---|---|
| 单测 + 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 卡片主体,且每张卡片需点其标题展开。本地已截图但未附上。
…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.
|
@qwen-code /triage |
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
ytahdn
left a comment
There was a problem hiding this comment.
No review findings. LGTM! ✅ — qwen3.7-plus via Qwen Code /review
ytahdn
left a comment
There was a problem hiding this comment.
No review findings. LGTM! ✅ — qwen3.7-plus via Qwen Code /review
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
PlanEmitterstamps a snapshot of it onto every todo/plan update's_meta.stats; the SDK'snormalizePlanUpdatecarries that snapshot into theTodoWritetool call'srawOutput; 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 (pollinggetStats()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_writeplan, and let the agent work through it. Expand theUpdate Todostool 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/resumeand 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:
/resume(including older sessions)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):
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/resumepath (API time omitted, tokens reconstructed). Real screenshots were not captured in this PR.Tested on
Environment (optional)
Local unit + render tests (
vitest) and theweb-shellvitebuild, acrosscli,sdk-typescript, andweb-shell. No live-provider run.Risk & Scope
/resumethe API-time row is intentionally omitted (tokens and tool time still reconstruct). The cumulative-usage accumulator is mutated byMessageEmitterand read byPlanEmitteron the sameSessionContext; the_meta.statsit stamps on plan updates is additive and ignored by clients that don't read it (e.g. Zed)./resumeis deliberately out of scope.cli+sdk-typescript) andweb-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。行为总览:
/resume(含旧会话)证据(前后对比)
这是一个用户可见的 web-shell 改动。展开后明细面板的示意(数值为示意,实际由下面的 render 测试验证,而非截图):
新增的 jsdom render 测试(
TodoView.test.tsx)驱动点击展开交互并断言分组标题与数值;数据层单测覆盖快照差分、工具时间窗口、以及/resume路径(API 时间省略、token 重建)。本 PR 未附真实截图。测试平台
运行环境(可选)
本地单测 + render 测试(
vitest),以及web-shell的vite构建,覆盖cli、sdk-typescript、web-shell三个包。未做真实 provider 的运行。风险与范围
/resume时 API 时间行被有意省略(token 与工具时间仍可重建)。累计用量累加器由MessageEmitter修改、由PlanEmitter读取,二者共享同一个SessionContext;它盖在 plan 更新上的_meta.stats是增量字段,不读取它的客户端(如 Zed)会忽略。/resume后存活,属有意的范围外。cli+sdk-typescript)与web-shell后才生效,因为快照由 agent 产生、由 web-shell 消费。agent 从未盖过快照的会话会优雅降级为只显示 开始 / 结束 + 工具时间。关联 Issue
无。