Skip to content

feat(web-shell): collapsible TodoWrite history with status diff#5109

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-todo-collapse
Jun 14, 2026
Merged

feat(web-shell): collapsible TodoWrite history with status diff#5109
wenshao merged 4 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-todo-collapse

Conversation

@wenshao

@wenshao wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Gives todo_write updates a dedicated, collapsible rendering in the web-shell transcript. Each update is now its own standalone block, collapsed by default to the current change — the items that just completed (●) or just started (◐), or the current step — with a completed/total count in the header, and expands on click to the full checklist. Todo detection now matches the tool by name (todo_write / todowrite) instead of an exact todowrite string, and status glyphs (● done · ◐ in progress · ○ pending) are consistent between the collapsed and expanded views via a shared TodoView component reused by both the tool renderer and the ACP plan renderer.

Why it's needed

In the web-shell daemon path, todo updates arrive as todo_write tool calls with kind: "think", but the inline renderer keyed on the literal string todowrite, so the todo-specific UI never matched. Todos therefore rendered as a generic, non-collapsible tool row crammed into the same group box as the surrounding tool calls — hard to scan and impossible to fold. Fixing detection by name revives the rich rendering and, as a side effect, populates the floating todo panel that was previously always empty on the daemon path.

Reviewer Test Plan

How to verify

Run the web-shell against a session that uses the todo tool: npm run dev:daemon, open the browser, then resume or start a session where the model calls todo_write. Expected: each todo update appears as its own TodoWrite N/M block, collapsed to the latest change / current step and separated from other tool calls; clicking it expands the full numbered checklist; the collapsed and expanded views use the same ● / ◐ / ○ glyphs. Automated: npm test in packages/web-shell (273 passing), including a new component test that renders a real todo_write tool (wire name + think kind) and a new transcript test asserting each todo_write forms its own group instead of merging.

Evidence (Before & After)

Before — a flat, non-collapsible row merged in with other tools:

[read] [edit] ✓ TodoWrite  Todos have been modified successfully...

After — a standalone block, collapsed by default to the current change:

▸ ✓ TodoWrite  3/7
     ● Implement parser
     ◐ Add unit tests

After — expanded on click to the full checklist:

▾ ✓ TodoWrite  3/7
     1. ● Implement parser
     2. ◐ Add unit tests
     3. ○ Wire up CLI
     ...

Tested on

OS Status
🍏 macOS ✅ unit tests + build
🪟 Windows ⚠️ not tested (CI)
🐧 Linux ⚠️ not tested (CI)

Environment (optional)

Local vitest unit/component tests + production build (npm run build, app + lib) on macOS; browser behavior covered by component tests that render the real tool, plus npm run dev:daemon for manual confirmation.

Risk & Scope

  • Main risk or tradeoff: changes how todo_write tool calls render and group in the web-shell transcript (each update is now standalone rather than merged). Covered by unit + component + transcript tests; no data or API changes.
  • Not validated / out of scope: no per-step time is shown — the only available signal is the wall-clock gap between two todo updates, which includes model thinking and idle time (observed up to ~36 min for an otherwise quick step), so it is not a meaningful step duration. Windows and Linux were not exercised locally (CI covers them).
  • Breaking changes / migration notes: none. This is a web-shell rendering/grouping change only; no protocol, storage, or public-API changes.

Linked Issues

N/A — no associated issue.

中文说明

这个 PR 做了什么

为 web-shell 会话记录里的 todo_write 更新提供专门的、可折叠的渲染。每次更新现在是一个独立的块,默认折叠为本次变化——刚完成(●)或刚开始(◐)的条目,或当前步骤——表头显示 已完成/总数,点击展开为完整清单。todo 的识别现在按名字匹配(todo_write / todowrite),不再依赖精确字符串 todowrite;状态图标(● 完成 · ◐ 进行中 · ○ 待办)在折叠态与展开态保持一致,通过工具渲染器与 ACP plan 渲染器共用的 TodoView 组件实现。

为什么需要

在 web-shell 的 daemon 路径下,todo 更新是以 todo_write 工具调用(kind: "think")进来的,但内联渲染判定写死了字面量 todowrite,所以 todo 专属 UI 从未命中。结果 todo 只渲染成一行普通工具,和相邻工具挤在同一个分组盒子里——既难扫读也无法折叠。按名字修正识别后,恢复了富渲染,并顺带让此前在 daemon 路径一直为空的浮动 todo 面板也能显示。

评审验证计划

如何验证

用使用了 todo 工具的会话运行 web-shell:npm run dev:daemon,打开浏览器,然后 resume 或新开一个模型会调用 todo_write 的会话。预期:每次 todo 更新显示为独立的 TodoWrite N/M 块,默认折叠到最新变化 / 当前步骤,并与其它工具调用分开;点击展开为完整编号清单;折叠态与展开态使用相同的 ● / ◐ / ○ 图标。自动化:在 packages/web-shellnpm test(273 通过),包含一个用真实 todo_write(线上名 + think kind)渲染的新组件测试,以及一个断言 todo_write 独立成组、不并入相邻组的新 transcript 测试。

前后对比证据

之前——一行扁平、不可折叠、与其它工具混在一组:

[read] [edit] ✓ TodoWrite  Todos have been modified successfully...

之后——独立的块,默认折叠到当前变化:

▸ ✓ TodoWrite  3/7
     ● Implement parser
     ◐ Add unit tests

之后——点击展开为完整清单:

▾ ✓ TodoWrite  3/7
     1. ● Implement parser
     2. ◐ Add unit tests
     3. ○ Wire up CLI
     ...

测试平台

系统 状态
🍏 macOS ✅ 单元测试 + 构建
🪟 Windows ⚠️ 未测试(CI)
🐧 Linux ⚠️ 未测试(CI)

环境(可选)

macOS 上本地 vitest 单元/组件测试 + 生产构建(npm run build,app + lib);浏览器行为由渲染真实工具的组件测试覆盖,并通过 npm run dev:daemon 手动确认。

风险与范围

  • 主要风险 / 取舍:改变了 web-shell 会话记录里 todo_write 工具调用的渲染与分组方式(每次更新现在独立成组而非合并)。已由单元 + 组件 + transcript 测试覆盖;无数据或 API 改动。
  • 未验证 / 不在范围:不显示单步时间——唯一能拿到的信号是两次 todo 更新之间的墙钟差,它包含模型思考与空闲时间(实测一个本应很快的步骤可达约 36 分钟),不是有意义的步骤时长。Windows 与 Linux 未在本地验证(由 CI 覆盖)。
  • 破坏性变更 / 迁移说明:无。仅为 web-shell 渲染/分组改动;无协议、存储或公共 API 变更。

关联 Issue

N/A —— 无关联 issue。

image image

Inline todo_write updates rendered as a generic, non-collapsible tool row
crammed in with surrounding tool calls — the todo-specific renderer was
effectively dead code because the detector matched the literal "todowrite"
while the wire name is "todo_write" (kind "think").

- Detect the todo tool by name (todo_write / todowrite) instead of relying
  on the unrelated tool kind, reviving the rich rendering and also
  populating the floating todo panel that was empty on the daemon path.
- Render each update as its own standalone group, collapsed by default to
  the per-snapshot diff (just-completed / just-started items) or the current
  step, expanding to the full checklist; the header shows completed/total.
- Use consistent status glyphs (●/◐/○) in both collapsed and expanded views
  via a shared TodoView component used by ToolGroup and PlanMessage.
@wenshao

wenshao commented Jun 14, 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 collapsible todo history looks like a solid improvement. 👍

However, the PR body doesn't follow our pull request template. Could you update it to use the required sections? Specifically:

  • What this PR does / Why it's needed — your "Summary" and "What changed" cover the same ground, just need the right headings.
  • Reviewer Test Plan — needs the structured subsections: "How to verify", "Evidence (Before & After)", and the "Tested on" OS table. Your current "Test plan" section has the spirit of this but is missing the OS table and the Before/After evidence format.
  • Risk & Scope — tradeoffs, what's out of scope, breaking changes.
  • Linked IssuesCloses #N or references.
  • <details> 中文说明 — full Chinese translation of the English body.

This is a housekeeping ask — once the template is filled out, the actual review can proceed.

中文说明

@wenshao 你好,感谢 PR!可折叠的 todo 历史看起来是个不错的改进。

不过 PR 正文没有按照 PR 模板 来写。能否按照模板更新一下?主要是:

  • What this PR does / Why it's needed — 现有的 "Summary" 和 "What changed" 内容够了,换一下标题即可。
  • Reviewer Test Plan — 需要结构化的子节:如何验证步骤、前后对比证据、以及 Tested on 的操作系统表格。
  • Risk & Scope — 权衡取舍、不在范围内的内容、破坏性变更。
  • Linked Issues — 关联 issue。
  • <details> 中文说明 — 英文正文的完整中文翻译。

纯粹是格式要求,更新后就可以继续 review 了。

Qwen Code · qwen3.7-max

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ Downgraded from Request changes to Comment: self-PR; CI still running. GitHub does not allow Request changes on your own PR — the substantive findings (one [Critical]) are in the inline comments below. — claude-opus-4-8[1m] via Qwen Code /qreview

Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/components/messages/TodoView.tsx Outdated
Comment thread packages/web-shell/client/components/messages/PlanMessage.tsx
@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 14, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the triage check — updated the PR description to follow the template: What this PR does / Why it's needed, a structured Reviewer Test Plan (How to verify · Before/After evidence · Tested-on OS table), Risk & Scope, Linked Issues, and the full <details> 中文说明.

@qwen-code /triage

中文

感谢 triage 检查——已按模板更新 PR 描述:补齐 What this PR does / Why it's needed、结构化的 Reviewer Test Plan(如何验证 · 前后对比证据 · Tested-on 系统表格)、Risk & Scope、Linked Issues,以及完整的 <details> 中文说明。

Comment thread packages/web-shell/client/components/messages/ToolGroup.tsx
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/components/messages/TodoView.tsx Outdated
Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/components/messages/PlanMessage.tsx
- [Critical] computeTodoTimeline keyed its running state on todo.id alone, but
  ids aren't globally unique (ACP assigns positional ids, models renumber per
  plan), so a later plan diffed against a previous plan's stale terminal status
  and silently dropped events in the collapsed view. Key on id+content so
  distinct tasks stay separate; add an id-reuse regression test.
- Stabilize the TodoTimelineContext value with a signature-cached Map so
  streaming ticks that don't touch a todo snapshot no longer re-render every
  todo/plan row.
- Drop the unused completed/total from TodoSnapshotDiff (consumers compute the
  count locally — single source of truth).
- Fall back to the raw result summary when a todo_write payload is unparseable.
- Add PlanMessage rendering tests and extractTodosFromToolCall coverage; remove
  stale "step time" comments left after dropping per-step timing.
@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the /qreview + /review findings in bada65bf55:

  • [Critical] id collision (todos.ts) — the running state is now keyed by id + content (todoStateKey), so a later plan's reused id (ACP positional ids / per-plan renumbering) no longer diffs against a previous plan's stale status and drops events. Added a regression test (two plans both using id 1 with different content).
  • Context re-render churn (App.tsx)todoTimeline is now a signature-cached Map (todoTimelineSignature + a ref), so streaming ticks that don't touch a todo snapshot keep the provider value referentially stable; todo/plan rows no longer re-render per token.
  • completed/total unused / two sources of truth — dropped from TodoSnapshotDiff entirely; consumers compute the count locally.
  • Unparseable payload (ToolGroup) — falls back to the raw result summary when extractTodosFromToolCall returns undefined (with a test).
  • Stale "step time" comments — removed from TodoView.tsx, TodoView.module.css, and the TodoTimelineContext JSDoc.
  • Test coverage — added PlanMessage.test.tsx (collapse/expand, progress header, plan-keyed diff, all-done / pending-focus / empty fallbacks), extractTodosFromToolCall tests (args / rawOutput.todos / rawOutput.entries / non-todo guard), and a mixed plan+tool computeTodoTimeline test.
  • Deferred (minor) — sharing a single message walk between getFloatingTodos and computeTodoTimeline: each is O(messages) and cheap, and the re-render cost (the real concern) is now fixed.

All 286 web-shell tests pass; eslint + typecheck clean; app + lib build green. Resolving the threads below.

中文

bada65bf55 中处理了 /qreview + /review 的意见:

  • [Critical] id 冲突(todos.ts):运行状态改为按 id + 内容 作 key(todoStateKey),后续 plan 复用的 id(ACP 位置 id / 每个 plan 重新编号)不再对着上一个 plan 的陈旧状态做 diff、丢事件。加了回归测试(两个 plan 都用 id 1 但内容不同)。
  • context 重渲染(App.tsx):todoTimeline 改为按签名缓存的 Map,不涉及 todo 的流式 tick 保持引用稳定,todo/plan 行不再每 token 重渲染。
  • completed/total 没人用 / 双数据源:从 TodoSnapshotDiff 移除,计数由消费方本地算(单一来源)。
  • payload 解析失败(ToolGroup):extractTodosFromToolCall 返回 undefined 时回退显示原始结果摘要(带测试)。
  • 过时的"步骤时间"注释:从 TodoView.tsxTodoView.module.cssTodoTimelineContext JSDoc 移除。
  • 测试覆盖:新增 PlanMessage.test.tsxextractTodosFromToolCall 提取路径测试、plan+tool 混合时间线测试。
  • 暂缓(次要):合并 getFloatingTodoscomputeTodoTimeline 的遍历——各自 O(messages) 很便宜,真正的问题(重渲染)已解决。

web-shell 286 测试全过;eslint + 类型检查干净;app + lib 构建通过。正在 resolve 下面的线程。

Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/utils/todos.test.ts
Follow-up to review on the id+content keying in computeTodoTimeline:
- Document the two rare trade-offs in the todoStateKey doc (a mid-task reword on
  a stable id, and unrelated plans reusing both id and content), both degrading
  to "the collapsed diff omits one event" while the expanded list stays correct.
- Pin behavior with tests: an item carried over and completed in a later turn
  (which id+content handles but a user-turn reset would drop), plus the two
  documented gaps.
- Add todoTimelineSignature tests: stable across non-todo edits; changes on any
  id/status/content change.
@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 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

⚠️ Downgraded from Request changes to Comment: self-PR; CI still running.

Round 2 incremental review (new SHA bada65bf5 since R1 8c580d586be). 14 files, +1032/-294. tsc 0 actionable (all pre-existing project-wide TS2835/TS4111), eslint 0, 286 tests pass. Reverse audit: no additional gaps.

3 high-confidence Suggestions + 3 low-confidence Suggestions posted inline. 2 low-confidence findings held for terminal only (cross-layer import adapters→utils, non-null assertions on todoItems).

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/utils/todos.ts Outdated
Comment thread packages/web-shell/client/utils/todos.ts
Comment thread packages/web-shell/client/components/messages/PlanMessage.tsx Outdated
Comment thread packages/web-shell/client/components/messages/ToolGroup.tsx
Comment thread packages/web-shell/client/components/messages/TodoView.tsx
@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification incl. real-browser render (merge reference)

Verified a1dee77a1e on Linux (Node 22.22.2): full web-shell suite, targeted mutation on the two highest-risk fixes, and — since this is a rendering change — a real headless-Chromium render (Playwright) of the actual ToolGroupTodoView components against a todo_write (wire name) tool call, not just jsdom.

Verdict: works end-to-end in a real browser, the review findings (incl. the [Critical] id-collision) are addressed at this head, and the diff-keying gaps are bounded and explicitly tested. Good to merge.

1. Real-browser render (headless Chromium via Playwright)

Mounted the real components with a 3-item todo_write snapshot (1 completed / 1 in-progress / 1 pending) + a timeline diff, served via Vite, driven in Chromium. No page/console errors.

Collapsed (default):

▸ ✓ TodoWrite 1/3
   ● Implement parser
   ◐ Add unit tests

→ N/M header 1/3, chevron, just-completed + just-started diff glyphs, current step shown, the pending item hidden.

Expanded (after a real click on the header):

▾ ✓ TodoWrite 1/3
   ● Implement parser     (struck-through)
   ◐ Add unit tests
   ○ Wire up CLI

chevron, full list incl. the pending item, all three ● ◐ ○ glyphs, completed item rendered with strikethrough (real CSS applied — beyond what jsdom shows). Matches the PR's Before/After exactly. Detection by the wire name todo_write (kind think) is what makes this rich block appear at all.

2. Test suite — green

vitest run over packages/web-shell: 291 passed (19 files). (A first run showed 5 build-artifact.test.ts failures purely because dist/ wasn't built yet; they pass once npm run build completes — app + lib build is green.)

3. Mutation — the two key fixes are non-vacuous

Reverted change Failing tests
detection back to exact 'todowrite' 3 ToolGroup todo_write rendering tests fail (block no longer matches)
todoStateKey back to id-only computeTodoTimeline › does not diff a reused id against a previous, unrelated plan fails (the [Critical] collision)

So the detection-by-name fix and the id+content keying (the [Critical] review finding) are each genuinely guarded.

4. Review findings + known limits

  • The /qreview + /review items are addressed at this head: the [Critical] id-collision (id+content keying), the context re-render churn (signature-cached timeline Map), the dropped completed/total duplicate, the unparseable-payload fallback, and the stale step-time comments — each with a test.
  • The two diff-keying gaps are honestly documented in todoStateKey's JSDoc and pinned by tests (…known gap): a mid-task reword on a stable id, and two unrelated plans reusing both id and exact content. Both degrade only to "the collapsed diff omits one event" while the expanded checklist stays correct — an acceptable trade for the daemon path, where ids are not globally unique.

Conclusion: end-to-end correct in a real browser, fully tested (incl. the regression that the [Critical] fix prevents), with bounded, documented edge cases. Good to merge.

中文版(Chinese version)

本地验证(含真实浏览器渲染,合并参考)

Linux(Node 22.22.2)上验证 a1dee77a1e:完整 web-shell 测试、对两处最高风险修复的定向变异测试,以及——因为这是渲染改动——用 真实 headless Chromium(Playwright)渲染真实的 ToolGroupTodoView 组件(对接 todo_write 线缆名工具调用),而非仅 jsdom。

结论:在真实浏览器中端到端可用;review 的意见(含 [Critical] id 冲突)在此 head 已处理;diff keying 的缺口是有界的且有专门测试。可以合并。

1. 真实浏览器渲染(Playwright + headless Chromium)

用一个 3 条目的 todo_write 快照(1 完成 / 1 进行中 / 1 待办)+ timeline diff 挂载真实组件,经 Vite 提供,在 Chromium 中驱动。无 page/console 错误。

折叠(默认):

▸ ✓ TodoWrite 1/3
   ● Implement parser
   ◐ Add unit tests

→ N/M 表头 1/3 折叠箭头、 刚完成 + 刚开始的 diff 图标、显示当前步骤、隐藏待办项。

展开(真实点击表头后):

▾ ✓ TodoWrite 1/3
   ● Implement parser     (删除线)
   ◐ Add unit tests
   ○ Wire up CLI

展开箭头、含 待办的完整清单、● ◐ ○ 三种图标齐全、完成项带删除线(真实 CSS 生效——超出 jsdom 能体现的)。与 PR 的 Before/After 完全一致。正是按线缆名 todo_write(kind think)识别,才让这个富渲染块得以出现。

2. 测试套件 —— 全绿

vitest run 覆盖 packages/web-shell:291 通过(19 文件)。(首跑有 5 个 build-artifact.test.ts 失败,纯粹因为 dist/ 尚未构建;npm run build 完成后即通过——app + lib 构建为绿。)

3. 变异测试 —— 两处关键修复非空壳

回退的改动 失败测试
识别改回精确 'todowrite' 3 个 ToolGroup todo_write 渲染测试失败(块不再命中)
todoStateKey 改回仅按 id computeTodoTimeline › does not diff a reused id against a previous, unrelated plan 失败([Critical] 冲突)

因此"按名识别"修复与 id+内容 keying([Critical] review 意见)都被真正守护。

4. Review 意见 + 已知限制

  • /qreview + /review 的意见在此 head 已处理:[Critical] id 冲突(id+内容 keying)、context 重渲染(按签名缓存的 timeline Map)、移除重复的 completed/total、payload 解析失败回退、过时的步骤时间注释——各有测试。
  • 两个 diff-keying 缺口在 todoStateKey 的 JSDoc 中如实记录并由测试固定(…known gap):稳定 id 上的中途改写、两个无关 plan 复用相同 id 且内容完全一致。两者只退化为"折叠态 diff 漏掉一个事件",展开清单仍正确——在 id 非全局唯一的 daemon 路径下是可接受的取舍。

结论:真实浏览器中端到端正确,测试充分(含 [Critical] 修复所阻止的回归),边界情况有界且有文档。可以合并。

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR @wenshao!

Template looks good ✓ — all required sections present, bilingual, with Before/After evidence and OS table.

On direction: This fixes a real bug — the web-shell daemon path sends todo_write as the wire name but the inline renderer matched the literal string todowrite, so the rich todo UI never activated and the floating panel stayed empty. That's a genuine regression in the daemon experience. The collapsible rendering on top is a natural UX companion, not a bolt-on. CHANGELOG shows steady web-shell and todo-related work (sticky todo panel, Option+Enter shortcuts, hooks), so this area is clearly active and aligned.

On approach: The scope feels right. The bug fix (isTodoWriteToolName matching both names) is minimal and necessary. The collapsible UI with status diffs is a reasonable companion — it ships in the same PR because it touches the same rendering path. The shared TodoView component reused by both ToolGroup and PlanMessage avoids duplication. The computeTodoTimeline with id+content keying addresses a real constraint (ids aren't globally unique across plans in the daemon path), and the documented edge cases (mid-task reword, content collision) are honest tradeoffs.

One thing worth noting: the TodoTimelineContext provider wraps the entire content area, but only todo/plan rows consume it. The signature-cached Map with useRef keeps this stable across streaming ticks — good call on preventing re-render churn.

Moving on to code review. 🔍

中文说明

感谢 PR @wenshao

模板完整 ✓ — 所有必需章节齐全,双语,有前后对比证据和系统表格。

方向: 这修了一个真实的 bug — web-shell daemon 路径发送 todo_write 作为线上名,但内联渲染器匹配的是字面量 todowrite,所以富渲染 UI 从未激活,浮动面板也一直为空。这是 daemon 体验的真实退化。在此之上的可折叠渲染是自然的 UX 增强,不是强加的。CHANGELOG 显示 web-shell 和 todo 相关工作持续推进(sticky todo 面板、Option+Enter 快捷键、hooks),这个方向明确且对齐。

方案: 范围合理。bug 修复(isTodoWriteToolName 匹配两个名字)精简且必要。带状态 diff 的可折叠 UI 是合理的伴侣 — 它在同一个 PR 中交付是因为触及了同一个渲染路径。共享的 TodoView 组件被 ToolGroupPlanMessage 复用,避免了重复。computeTodoTimeline 用 id+content 作 key 解决了真实约束(daemon 路径中 id 在不同 plan 间不全局唯一),文档化的边界情况(中途改写、内容碰撞)是诚实的取舍。

值得注意的一点:TodoTimelineContext provider 包裹了整个内容区域,但只有 todo/plan 行消费它。按签名缓存的 MapuseRef 在流式 tick 间保持稳定 — 防止重渲染的策略很好。

进入代码审查 🔍

Qwen Code · qwen3.7-max

…rage

Address follow-up review:
- Extract PlanEventSummary as the sole TodoTimelineContext consumer, mirroring
  ToolGroup's TodoToolBody so the memo-shielded PlanMessage stays stable when
  the timeline Map reference changes.
- Note the spurious-`started` axis of the reword trade-off in the todoStateKey
  doc (a reword can drop a completion or emit a stray start).
- Add direct isTodoWriteToolName tests (incl. the `todowrite` ACP variant) and
  a todoTimelineSignature empty-transcript test.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

2a. Code Review

Independent proposal (before reading the diff):
I'd fix the name mismatch by adding an isTodoWriteToolName() helper matching both todo_write and todowrite. Each todo_write call should get its own tool_group so it doesn't merge with adjacent tool calls. For the UX side, a collapsible component with a progress count (completed/total) that defaults to showing just the current step. For status diffs, walk the transcript in order and track transitions per item.

Comparison with the PR:
The PR's approach matches my proposal almost exactly — isTodoWriteToolName(), standalone groups via isStandalone(), a shared TodoView with TodoEventSummary (collapsed) and TodoFullList (expanded), and computeTodoTimeline for transitions. The id+content keying (todoStateKey) goes beyond my proposal by addressing the real problem of non-unique ids across plans — good call.

Findings:

No critical blockers. The code is well-structured and the previous review round's findings (id collision, context re-render, duplicate completed/total) are all addressed at this head with regression tests.

A few observations (non-blocking):

  • isTodoWriteToolName normalizes to lowercase internally, but in ToolGroup.tsx the caller already does tool.toolName.toLowerCase() before passing. Harmless (idempotent), just a minor redundancy.
  • The extractTodosFromToolCall guard !isTodoWriteToolName(tool.toolName) && tool.kind !== 'other' means a non-todo tool with kind: 'other' would still be checked for todo arrays. This is the original behavior — the PR correctly preserves it.
  • The todoStateKey using JSON.stringify([id, content]) is a pragmatic choice. The two documented edge cases (mid-task reword, content collision) degrade gracefully — collapsed diff omits one event, expanded list stays correct.

2b. Testing

Unit tests (applied PR diff to worktree, ran locally):

$ cd packages/web-shell && npx vitest run

 ✓ utils/todos.test.ts (33 tests)
 ✓ adapters/transcriptToMessages.test.ts (88 tests)
 ✓ components/messages/ToolGroup.test.tsx (16 tests)
 ✓ components/messages/PlanMessage.test.tsx (6 tests)
 ... (19 files total)

 Test Files  19 passed (19)
      Tests  292 passed (292)
   Duration  5.66s

Lint: Clean on all 6 changed source files.

CI (GitHub Actions): macOS ✅, Ubuntu ✅, Lint ✅, CodeQL ✅, Windows pending.

Real-browser testing: tmux-based testing doesn't apply here — the web-shell is a browser-based UI rendered by Vite, not a terminal TUI. The qwen CLI in tmux would test the Ink-based terminal renderer, not the web-shell components. The author's own Playwright/headless-Chromium verification (posted in this comment) covers real-browser rendering: collapsed view shows ▸ TodoWrite 1/3 with / glyphs, expanded view shows the full numbered list with strikethrough on completed items, matching the PR's Before/After exactly. Mutation testing confirms the two key fixes (name detection, id+content keying) are non-vacuous — reverting each causes specific test failures.

中文说明

2a. 代码审查

独立方案(读 diff 前):
我会加一个 isTodoWriteToolName() 辅助函数匹配 todo_writetodowrite。每个 todo_write 调用应该独占一个 tool_group,不与相邻工具合并。UX 方面,做一个可折叠组件,带进度计数(completed/total),默认只显示当前步骤。状态 diff 则按顺序遍历会话记录、跟踪每个条目的状态变化。

与 PR 对比:
PR 的方案与我的几乎完全一致 — isTodoWriteToolName()、通过 isStandalone() 独立成组、共享的 TodoViewTodoEventSummary 折叠态 + TodoFullList 展开态)、以及 computeTodoTimeline 处理状态转换。id+content 作 key(todoStateKey)超越了我的方案,解决了跨 plan id 不唯一的真实问题 — 好。

发现:

无关键阻塞项。代码结构良好,上一轮 review 的意见(id 冲突、context 重渲染、重复的 completed/total)都已在当前 head 处理并有回归测试。

几个观察(非阻塞):

  • isTodoWriteToolName 内部做了 lowercase 归一化,但 ToolGroup.tsx 的调用方已经先做了 tool.toolName.toLowerCase()。无害(幂等),只是小冗余。
  • extractTodosFromToolCall 的守卫条件保留了原始行为 — PR 正确地保持了它。

2b. 测试

单元测试(在 worktree 中应用 PR diff 后本地运行):

Test Files  19 passed (19)
     Tests  292 passed (292)
  Duration  5.66s

Lint: 6 个变更源文件全部干净。

CI: macOS ✅、Ubuntu ✅、Lint ✅、CodeQL ✅、Windows 待完成。

真实浏览器测试: tmux 测试不适用于此 — web-shell 是 Vite 渲染的浏览器 UI,不是终端 TUI。作者自己的 Playwright/headless-Chromium 验证覆盖了真实浏览器渲染:折叠态显示 ▸ TodoWrite 1/3/ 图标,展开态显示完整编号列表并已完成项带删除线。变异测试确认两个关键修复非空壳 — 回退任一修复都导致特定测试失败。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Stepping back: this PR fixes a genuine regression in the web-shell daemon path (todo rendering broken due to name mismatch) and pairs it with a well-designed collapsible UI. The implementation is straightforward — a shared TodoView component, a timeline computed once per snapshot change, and clean separation between collapsed (event diff) and expanded (full list) views.

The code reads like someone who thought about the problem, not someone who threw abstractions at it. The id+content keying for todoStateKey is the right call given the daemon path's non-unique ids, and the two documented edge cases (reword, content collision) are honestly traded. The signature-cached Map for TodoTimelineContext is a pragmatic fix for the streaming re-render problem without over-engineering it.

My independent proposal matched the PR's approach, and the PR went further in the right ways (id+content keying, mutation-tested key fixes). 292 tests pass, lint is clean, CI green on macOS + Ubuntu. The author's own Playwright verification covers real-browser rendering with mutation testing to prove the fixes aren't vacuous.

This is ready to ship. ✅

中文说明

退一步看:这个 PR 修复了 web-shell daemon 路径的真实回归(因名称不匹配导致 todo 渲染失效),并配上了设计良好的可折叠 UI。实现简洁 — 共享的 TodoView 组件、按快照变化计算一次的时间线、折叠态(事件 diff)和展开态(完整列表)的清晰分离。

代码读起来像是思考过问题的人写的,而不是拿抽象堆砌的人。todoStateKey 用 id+content 作 key 是 daemon 路径 id 不唯一下的正确选择,两个文档化的边界情况(改写、内容碰撞)是诚实的取舍。按签名缓存的 TodoTimelineContext Map 对流式重渲染问题的修复务实而不过度工程化。

我的独立方案与 PR 一致,PR 还在正确的方向上更进一步(id+content keying、变异测试验证关键修复)。292 测试通过,lint 干净,CI 在 macOS + Ubuntu 上绿。作者自己的 Playwright 验证覆盖了真实浏览器渲染并用变异测试证明修复非空壳。

可以合并了。✅

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.

@chiga0 chiga0 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.

Review Overview — PR #5109

PR: feat(web-shell): collapsible TodoWrite history with status diff
Author: wenshao | HEAD: 8e1f4bf (4 commits) | +1138/-292, 14 files
Base: main

Summary

Well-scoped UI enhancement giving todo_write updates a dedicated, collapsible rendering in the web-shell transcript. Core changes:

  1. Todo detection fix: isTodoWriteToolName() matches both todo_write (wire name) and todowrite (legacy/ACP), fixing the root cause where daemon-path todos were never matched.
  2. Standalone grouping: todo_write and sub-agent tool calls each get their own tool_group, preventing them from merging with adjacent tool calls.
  3. Shared TodoView component: TodoEventSummary (collapsed diff) + TodoFullList (expanded checklist) reused by both PlanMessage (ACP path) and ToolLine (daemon path).
  4. Timeline computation: computeTodoTimeline() walks all messages and derives per-snapshot status transitions (started/completed), with todoStateKey([id, content]) preventing cross-plan id collision.
  5. Referential stability: todoTimelineSignature() + useRef memoization keeps the TodoTimelineContext Map stable across unrelated streaming ticks.
  6. Context isolation: PlanEventSummary and TodoToolBody are the only useContext(TodoTimelineContext) consumers, preventing memo-shielded parents from re-rendering.

Independent Findings: 0 blocking

The code is well-structured with clear data flow, proper memoization, documented trade-offs (reword edge case, recurring-plan collision), and comprehensive test coverage (~30+ new test cases across 4 test files). i18n keys verified present in both EN and CN locales.

Cross-Validation

# wenshao (self-review) Severity My Assessment
W1 lastStatus keyed by todo.id — ids not globally unique Critical FIXED in commit a1dee77a1e — composite key [id, content] via todoStateKey(). Trade-offs well-documented.
W2 todoTimeline Map referential instability Suggestion FIXED in commit 8e1f4bftodoTimelineSignature + useRef memoization.
W3 Stale JSDoc referencing "step time" Suggestion FIXED at HEAD — no stale references found.
W4 PlanMessage has no rendering test Suggestion FIXED — PlanMessage.test.tsx added (6 tests: collapse, expand, timeline diff, all-done, fallback, empty).
W5 TodoToolBody blank when parse fails Suggestion FIXED — fallback to result summary when !hasTodoList.
W6 getFloatingTodos + computeTodoTimeline double iteration Suggestion NOTED — both iterate messages but serve different consumers (floating panel vs timeline context). Merging adds complexity for minimal gain.
W7-W8 extractTodosFromToolCall missing test coverage for rawOutput paths + isTodoWriteToolName Suggestion FIXED — extractTodosFromToolCall tests cover all 3 paths + non-todo rejection. isTodoWriteToolName tests cover 4 matches + 4 rejections.
W9 PlanMessage useContext bypasses memo Suggestion FIXED — extracted PlanEventSummary as isolated context consumer.
W10-W11 Blank row edge case + TodoFullList unbounded Low confidence ACCEPTABLE — row header always renders; lists are model-bounded.

Additional Audit Coverage

  • Tool detection fix verified: Old toolName !== 'todowrite' && kind !== 'other' returned undefined for todo_write (underscore) with kind: 'think' — the exact daemon wire format. New isTodoWriteToolName() correctly handles both names.
  • Standalone grouping logic: isStandalone predicate combining isSubAgentToolCall + isTodoWriteToolName is correct — bidirectional check prevents both directions of merging.
  • Status transition logic: computeTodoTimeline correctly suppresses events for first-seen-completed items (prev === undefined), handles pending → completed transitions (emits completed event), and produces empty events for unchanged re-emissions.
  • Context default safety: TodoTimelineContext defaults to new Map(), so components outside the provider gracefully fall back to current-step display.
  • CSS migration: Old todo-specific classes removed from ToolChrome.module.css and PlanMessage.module.css, replaced by shared TodoView.module.css — no orphaned styles.

Final Verdict — APPROVE

Clean, well-tested UI enhancement. The todo detection bug fix (todo_write vs todowrite) resolves a real regression in the daemon path. All self-review findings addressed across 4 commits. 0 independent blocking issues.


This review was generated by QoderWork AI

@wenshao
wenshao merged commit 9f2168f into QwenLM:main Jun 14, 2026
82 of 83 checks passed
@wenshao
wenshao deleted the feat/web-shell-todo-collapse branch June 14, 2026 17:23
doudouOUC pushed a commit that referenced this pull request Jun 15, 2026
* feat(web-shell): collapsible TodoWrite history with status diff

Inline todo_write updates rendered as a generic, non-collapsible tool row
crammed in with surrounding tool calls — the todo-specific renderer was
effectively dead code because the detector matched the literal "todowrite"
while the wire name is "todo_write" (kind "think").

- Detect the todo tool by name (todo_write / todowrite) instead of relying
  on the unrelated tool kind, reviving the rich rendering and also
  populating the floating todo panel that was empty on the daemon path.
- Render each update as its own standalone group, collapsed by default to
  the per-snapshot diff (just-completed / just-started items) or the current
  step, expanding to the full checklist; the header shows completed/total.
- Use consistent status glyphs (●/◐/○) in both collapsed and expanded views
  via a shared TodoView component used by ToolGroup and PlanMessage.

* fix(web-shell): scope todo diff per task identity; address review

- [Critical] computeTodoTimeline keyed its running state on todo.id alone, but
  ids aren't globally unique (ACP assigns positional ids, models renumber per
  plan), so a later plan diffed against a previous plan's stale terminal status
  and silently dropped events in the collapsed view. Key on id+content so
  distinct tasks stay separate; add an id-reuse regression test.
- Stabilize the TodoTimelineContext value with a signature-cached Map so
  streaming ticks that don't touch a todo snapshot no longer re-render every
  todo/plan row.
- Drop the unused completed/total from TodoSnapshotDiff (consumers compute the
  count locally — single source of truth).
- Fall back to the raw result summary when a todo_write payload is unparseable.
- Add PlanMessage rendering tests and extractTodosFromToolCall coverage; remove
  stale "step time" comments left after dropping per-step timing.

* test(web-shell): document todo-diff keying limits; cover signature

Follow-up to review on the id+content keying in computeTodoTimeline:
- Document the two rare trade-offs in the todoStateKey doc (a mid-task reword on
  a stable id, and unrelated plans reusing both id and content), both degrading
  to "the collapsed diff omits one event" while the expanded list stays correct.
- Pin behavior with tests: an item carried over and completed in a later turn
  (which id+content handles but a user-turn reset would drop), plus the two
  documented gaps.
- Add todoTimelineSignature tests: stable across non-todo edits; changes on any
  id/status/content change.

* refactor(web-shell): isolate plan context read; expand todo test coverage

Address follow-up review:
- Extract PlanEventSummary as the sole TodoTimelineContext consumer, mirroring
  ToolGroup's TodoToolBody so the memo-shielded PlanMessage stays stable when
  the timeline Map reference changes.
- Note the spurious-`started` axis of the reword trade-off in the todoStateKey
  doc (a reword can drop a completion or emit a stray start).
- Add direct isTodoWriteToolName tests (incl. the `todowrite` ACP variant) and
  a todoTimelineSignature empty-transcript test.
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