Skip to content

fix(cli): hide stale sticky todos from previous turns - #7900

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
chiga0:fix/hide-stale-sticky-todo
Jul 28, 2026
Merged

fix(cli): hide stale sticky todos from previous turns#7900
wenshao merged 2 commits into
QwenLM:mainfrom
chiga0:fix/hide-stale-sticky-todo

Conversation

@chiga0

@chiga0 chiga0 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Prevents the sticky todo panel from resurfacing with stale todos from a previous turn. The sticky panel now checks whether a new user message exists after the todo snapshot in history; if so, the snapshot belongs to an earlier turn and the panel stays hidden. This is a pure data-layer change — no layout components or streaming-state logic are modified.

Why it's needed

Fixes #7061 (reopened scenario not covered by #7062).

PR #7062 hid the sticky panel when streaming is idle, but the panel resurfaces with stale todos as soon as the user sends a new message (state → Responding). The user sees in-progress indicators (/) for work that already finished in an earlier turn — exactly the scenario in the #7831 screenshot: the model hit ECONNRESET, the turn ended, the user typed /stats, and the old todo list reappeared.

Reviewer Test Plan

How to verify

  1. Run the unit tests: cd packages/cli && npx vitest run src/ui/utils/todoSnapshot.test.ts — confirm the two new tests pass alongside all existing ones.
  2. Manual check: start a multi-step task so the sticky todo panel appears, let the turn finish, then send a new message — the stale todo panel should not reappear during the new turn's response.
  3. Confirm the normal case still works: while the model is actively working on a task with todos, the sticky panel should still appear and update as todos change.

Evidence (Before & After)

N/A (data-layer logic change; the visual behavior is a panel not appearing when it previously did)

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Unit tests only (npx vitest run).

Risk & Scope

  • Main risk or tradeoff: Enter to steer injects a user message mid-turn, which triggers the same turn-boundary check and temporarily hides the sticky panel. Impact is minimal — steer implies a direction change so old todos are likely stale anyway, and the model typically calls todo_write again shortly after, which creates a new snapshot after the steer message and the panel reappears. Worst case: the panel stays hidden for the remainder of that turn if the model doesn't update todos.
  • Not validated / out of scope: a 100% precise fix that distinguishes steer messages from real turn starts (e.g., tracking a stale flag in the message-submission path) was considered but would invade the message-submission logic for marginal benefit.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #7061

中文说明

本 PR 做了什么

防止 sticky todo 面板在新轮次中重新显示上一轮的过期 todo。sticky 面板现在会检查 history 中 todo 快照之后是否存在新的用户消息;如果存在,说明快照属于上一轮,面板保持隐藏。这是纯数据层的修改——不涉及布局组件或流式状态逻辑。

为什么需要

修复 #7061#7062 未覆盖的重现场景)。

PR #7062 在流式空闲时隐藏了 sticky 面板,但用户发送新消息后(状态 → Responding),面板会带着过期 todo 重新出现。用户看到的是上一轮已完成工作的进行中指示器(/)——正是 #7831 截图中的场景:模型遇到 ECONNRESET,轮次结束,用户输入 /stats,旧 todo 列表重新出现。

审阅者测试计划

如何验证

  1. 运行单元测试:cd packages/cli && npx vitest run src/ui/utils/todoSnapshot.test.ts——确认两个新测试与所有现有测试一起通过。
  2. 手动验证:启动一个多步骤任务使 sticky todo 面板出现,等轮次结束后发送新消息——过期的 todo 面板不应在新轮次的响应中重新出现。
  3. 确认正常场景仍然有效:模型正在处理含 todo 的任务时,sticky 面板应正常显示并随 todo 变化更新。

证据(前后对比)

N/A(数据层逻辑变更;视觉行为是面板在之前会显示时不再显示)

测试环境

操作系统 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

仅单元测试(npx vitest run)。

风险与范围

  • 主要风险或权衡:Enter to steer 在模型响应过程中注入用户消息,会触发相同的轮次边界检查,临时隐藏 sticky 面板。实际影响很小——steer 本身意味着用户改变了方向,旧 todo 大概率已不适用;且模型收到 steer 后通常会很快更新 todo,新快照在 steer 消息之后,面板自动恢复。最坏情况:steer 后模型没更新 todo,面板在该轮剩余时间不显示。
  • 未验证 / 不在范围内:曾考虑 100% 精确区分 steer 消息和真实轮次开始的方案(如在消息提交路径中跟踪 stale 标记),但这会侵入消息提交逻辑,收益有限。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

修复 #7061

PR QwenLM#7062 hid the sticky panel when streamingState is Idle, but the panel
resurfaces with stale todos from a previous turn as soon as the user sends
a new message (state → Responding). This is confusing — the user sees
in-progress indicators for work that already finished in an earlier turn.

Add a turn-boundary check in getStickyTodos: if a user message exists
after the todo snapshot in history, the snapshot belongs to a previous
turn and the sticky panel returns null.

Fixes QwenLM#7061
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen 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 @chiga0 — thanks for the well-researched fix! The root cause analysis and test coverage look solid.

One thing before we can move forward: the PR body doesn't follow the PR template. Could you restructure it using the template headings? Specifically:

  • What this PR does / Why it's needed — your Summary + Root cause content maps well here
  • Reviewer Test Plan — how to verify, and Before/After evidence (even a short description of the stale-panel scenario vs. the fixed behavior)
  • Risk & Scope — main tradeoff, what's not validated
  • Linked IssuesFixes #7061
  • A Chinese translation in a <details> block

The content you've written is great — it just needs to be reorganized into the template structure so reviewers can find what they need quickly. Once updated, re-trigger with @qwen-code /triage and we'll pick it right back up.

中文说明

感谢贡献!根因分析和测试覆盖都很扎实。

PR 描述需要按照 PR 模板 的格式重新组织:

  • What this PR does / Why it's needed — 你的 Summary 和 Root cause 内容可以放在这里
  • Reviewer Test Plan — 如何验证,以及 Before/After 证据
  • Risk & Scope — 主要权衡、未验证的部分
  • Linked IssuesFixes #7061
  • <details> 中附中文翻译

内容写得很好,只需要按模板结构重新排列即可。更新后用 @qwen-code /triage 重新触发审查。

Qwen Code · qwen3.8-max-preview

@chiga0

chiga0 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

已知边界情况:Steer 消息

Enter to steer 在模型响应过程中注入的 user 消息也会产生 type: 'user' 的 history item,这会触发 hasUserMessageAfter → 当前轮的 sticky panel 被临时隐藏。

实际影响很小:

  1. Steer 本身就意味着用户改变了方向,旧 todo 大概率已不适用
  2. 模型收到 steer 后通常会很快 todo_write 更新列表 → 新快照在 steer 消息之后 → hasUserMessageAfter 返回 false → 面板自动恢复
  3. 最坏情况:steer 后模型没更新 todo,面板在这一轮剩余时间不显示 — 可接受

如果需要 100% 精确,可以在 AppContainer 提交消息时,仅当 streamingState === Idle 时标记 snapshot stale(加一个 ref 跟踪),但这会侵入消息提交逻辑。当前纯数据层方案已经覆盖了绝大多数场景,steer 的 trade-off 可以接受。

Comment on lines +133 to +135
if (items[i].type === 'user') {
return true;
}

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.

[Critical] hasUserMessageAfter treats mid-turn steer messages as new user turns, hiding the sticky todo panel prematurely during an active turn.

Failure scenario: User asks model to work on todos → model calls todoWrite (snapshot at index N) → user sends a mid-turn steer ("also handle edge case Y") → steer pushed as { type: 'user', sentToModel: false } → model generates 2+ more items → hasUserMessageAfter finds the steer at type === 'user' → returns null. Panel disappears mid-turn even though no new turn started.

The established codebase pattern distinguishes real user turns from steers via sentToModel !== false — used by findLastUserItemIndex (historyUtils.ts:132), realUserPromptTexts (historyUtils.ts:143), isSyntheticHistoryItem (historyUtils.ts:65), and useAwaySummary.ts (lines 55, 63). This function omits that guard.

Suggested change
if (items[i].type === 'user') {
return true;
}
if (items[i].type === 'user' && (items[i] as HistoryItemUser).sentToModel !== false) {
return true;
}

— qwen3.7-max via Qwen Code /review

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.

Thanks for the careful read — declining this one, because the suggested guard would regress the exact case this PR fixes.

Verified at the reviewed commit: local slash commands keep sentToModel: false (slashCommandProcessor.ts:857-860 initializes it false and only flips it to true when the command yields a submit_prompt; UI-only commands like /stats, /about, /exit never do), while regular user messages leave it undefined (types.ts:124-131). Steers are also false (useGeminiStream.ts:2576-2582). So sentToModel !== false cannot separate a mid-turn steer from a local slash command — and applying it would stop /stats (the command in the #7061 screenshot) from hiding the stale panel, re-breaking #7061. This lines up with @wenshao's independent E2E on 090ad18, which confirmed /stats triggers the boundary today.

The sentToModel !== false pattern elsewhere answers "did this reach the model?" (rewind/telemetry/turn rendering); hasUserMessageAfter answers "did the user start any new interaction?", which intentionally includes local slash commands — so the omission is deliberate. The steer case is real but minor and self-healing (next todo_write re-snapshots), already accepted by the author and @wenshao; a precise fix needs streaming state in this pure function, out of scope here.

To make sure the naive guard can't be applied silently, I added a regression test (hides sticky todos when a local slash command starts after the snapshot) using a faithful sentToModel: false fixture — it passes now and fails if the guard is added.

中文说明

感谢细致的审查 —— 这一条予以拒绝,因为建议的判断条件会让本 PR 修复的场景重新回归。

已在被审查的 commit 上核实:本地斜杠命令保持 sentToModel: false(slashCommandProcessor.ts:857-860 初始化为 false,仅当命令产生 submit_prompt 时才改为 true;/stats/about/exit 等纯 UI 命令从不触发),而普通用户消息保持 undefined(types.ts:124-131)。Steer 同样是 false(useGeminiStream.ts:2576-2582)。因此 sentToModel !== false 无法区分轮内 steer 与本地斜杠命令 —— 应用它会导致 /stats(#7061 截图中的命令)不再隐藏过期面板,重新破坏 #7061。这与 @wenshao090ad18 上的独立 E2E 验证一致:他确认当前 /stats 能触发边界。

其他地方的 sentToModel !== false 模式回答的是"这条是否发给了模型?"(rewind/telemetry/turn 渲染);而 hasUserMessageAfter 回答的是"用户是否开启了任何新交互?",这有意包含本地斜杠命令 —— 所以这里的省略是刻意的。Steer 场景确实存在但影响很小且可自愈(下一次 todo_write 重建快照),作者与 @wenshao 均已接受;精确修复需要在这个纯函数中引入 streaming 状态,超出本次范围。

为防止该朴素判断被悄悄应用,我新增了一个回归测试(hides sticky todos when a local slash command starts after the snapshot),使用忠实的 sentToModel: false fixture —— 当前通过,加上该判断后会失败。

@chiga0 chiga0 changed the title fix(cli): hide sticky todo panel when a new turn starts fix(cli): hide stale sticky todos from previous turns Jul 28, 2026
@chiga0

chiga0 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Independent local verification of 090ad18 (merge reference)

Verdict: the fix works exactly as described — the stale panel no longer resurfaces on a new turn, and the panel still appears for the current turn's own todos. Recommend merge.

Verified the pushed head 090ad189 in an isolated detached worktree (fresh npm ci, macOS, Node 22.23.1), independent of the author's environment.

1. Unit A/B — the new test pins the fix

  • todoSnapshot.test.ts on the PR head: 21/21 pass (matches the PR body).
  • Same test file with todoSnapshot.ts swapped back to the merge-base (6a432ad): exactly one test fails — "hides sticky todos when a new user message starts after the snapshot" — receiving the stale todo array instead of null. The other 20 pass on both sources, i.e. they are regression guards and the new test is the one that actually pins the fix.
  • The other suites that exercise the sticky panel (DefaultAppLayout.test.tsx, ScreenReaderAppLayout.test.tsx, app-container-controls-dep.test.ts): 22/22 pass on the PR head.

unit A/B

2. Real-TUI E2E A/B — bug reproduces on base, gone on the PR build

I drove the real built CLI (packages/cli/dist/index.js) in a pty against a scripted OpenAI-compatible mock server, once built from the merge-base and once from the PR head, with an identical scenario:

A screen poll watched for the todo strings during each turn:

Oracle merge-base build PR head build
Idle after turn 1: panel hidden ✅ hidden ✅ hidden
Turn 2 while Responding: stale panel resurfaces ❌ resurfaces (~0.25 s after the turn starts) ✅ never appears (polled the entire turn)
Turn 3: panel shows for the current turn's todos ✅ shows ✅ shows, with updated statuses

E2E turn 2 before/after

E2E turn 3 regression control

3. Code-review notes — turn-boundary coverage

I checked every path that starts a turn to confirm it commits a type: 'user' history item (what hasUserMessageAfter keys on):

  • Normal submitsuseGeminiStream.ts adds the USER item synchronously at submit time, so there is no pending-window flash before the boundary is visible to getStickyTodos().
  • Slash commands (incl. the /stats from the issue screenshot) — slashCommandProcessor.ts adds a USER item with sentToModel: false; the type is still 'user', so the boundary triggers.
  • Steer — mid-turn USER item; matches the trade-off the author already documented in this thread (current-turn panel hides until the next todo_write re-snapshots; self-healing). Agreed it's acceptable.
  • One small gap, non-blocking: cron-submitted prompts intentionally skip the USER history item (submitType !== SendMessageType.Cron guard in useGeminiStream.ts), so a cron-driven turn could still resurface the previous turn's panel. Niche enough to leave as a follow-up.

Landscape: #7061 is closed (idle case fixed by #7062, merged 07-17); this PR covers the remaining Responding-resurface scenario; no competing open PR touches this area.

中文版本 (Chinese version)

090ad18 的独立本地验证(合并参考)

结论:修复效果与描述完全一致 —— 新一轮对话开始后过期 todo 面板不再复现,当前轮自己的 todo 面板仍正常显示。建议合并。

在隔离的 detached worktree 中(全新 npm ci,macOS,Node 22.23.1)对已推送的 head 090ad189 做了独立验证。

1. 单元测试 A/B —— 新测试确实钉住了修复

  • PR head 上运行 todoSnapshot.test.ts:21/21 通过(与 PR 描述一致)。
  • 保留 PR 的测试文件、把 todoSnapshot.ts 换回 merge-base(6a432ad)源码:恰好只有一个测试失败 —— "hides sticky todos when a new user message starts after the snapshot"(收到过期 todo 数组而非 null)。其余 20 个双向通过,属回归守护;新测试是真正钉住修复的判别性测试。
  • 其他涉及 sticky 面板的套件(DefaultAppLayoutScreenReaderAppLayoutapp-container-controls-dep):PR head 上 22/22 通过

2. 真实 TUI E2E A/B —— base 构建复现 bug,PR 构建修复

用 pty 驱动真实构建的 CLI(packages/cli/dist/index.js)对接脚本化的 OpenAI 兼容 mock server,分别以 merge-base 和 PR head 构建各跑一次完全相同的场景:

每轮期间轮询屏幕内容检测 todo 字符串:

判定点 merge-base 构建 PR head 构建
Turn 1 结束后 idle:面板隐藏 ✅ 隐藏 ✅ 隐藏
Turn 2 Responding 期间:过期面板复现 ❌ 复现(新一轮开始约 0.25 秒后) ✅ 全程轮询从未出现
Turn 3:当前轮 todo 面板正常显示 ✅ 显示 ✅ 显示,且状态已更新

(截图见上方英文部分)

3. 代码审查 —— turn 边界覆盖情况

逐一核对了所有开启新一轮的路径,确认都会提交 type: 'user' 的 history item(hasUserMessageAfter 的判定依据):

  • 普通消息提交 —— useGeminiStream.ts 在提交时同步添加 USER item,不存在边界对 getStickyTodos() 不可见的 pending 窗口。
  • 斜杠命令(包括 issue 截图中的 /stats)—— slashCommandProcessor.ts 会添加 sentToModel: false 的 USER item,type 仍为 'user',边界生效。
  • Steer —— mid-turn USER item;与作者在本线程中已说明的 trade-off 一致(当前轮面板隐藏至下次 todo_write 重建快照,可自愈)。同意可接受。
  • 一个不阻塞合并的小缺口:cron 提交的 prompt 有意跳过 USER history item(useGeminiStream.ts 中的 submitType !== SendMessageType.Cron 分支),因此 cron 驱动的新一轮理论上仍可能复现上一轮的面板。场景较偏,留作后续即可。

关联背景:#7061 已关闭(idle 场景由 #7062 修复,07-17 合并);本 PR 覆盖剩余的 Responding 复现场景;当前没有其他在开的竞争 PR 涉及此区域。

@chiga0

chiga0 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@chiga0 chiga0 added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 28, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔄 Takeover re-armed: the round counter starts a fresh window (previous rounds no longer count toward the cap); management continues.

中文说明

🔄 已重新武装:轮次计数开启新窗口(此前轮次不再计入上限),托管继续。

@qwen-code-dev-bot

qwen-code-dev-bot commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 2 finishedview run. See this round's report below.

中文说明

AutoFix 第 2 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #7900

Feedback point 1 — Inline Critical [rc:3662969985]: hasUserMessageAfter should guard on sentToModel !== false

Decision: Decline the suggested code change; added a regression test that pins the current (correct) behavior.

The suggested guard would re-introduce the very bug this PR fixes. Verified against the code at the reviewed commit:

  • Regular user messages leave sentToModel undefinedpackages/cli/src/ui/types.ts:124-131: "Regular user messages leave this undefined… visible slash-command invocations may be handled locally without entering API history."
  • Local slash commands keep sentToModel: falseslashCommandProcessor.ts:857-860 adds the USER item with sentToModel: false and only updates it to true when the command produces a submit_prompt. UI-only commands such as /stats, /about, /exit, /help, /clear never reach the model, so they stay false.
  • Steers are also sentToModel: falseuseGeminiStream.ts:2576-2582, with the comment "Intentionally false… steer is not a standalone user turn."

So sentToModel !== false cannot distinguish a mid-turn steer from a local slash command — both are { type: 'user', sentToModel: false }. Applying the guard would stop /stats (the exact command in the #7061 screenshot) from hiding the stale panel, regressing #7061. This matches @wenshao's independent E2E verification on 090ad18, which confirmed /stats correctly triggers the boundary with the current code and recommended merge.

The sentToModel !== false pattern in historyUtils.ts / useAwaySummary.ts / MainContent.tsx answers a different question — "is this a real prompt that reached the model?" (for rewind, telemetry, turn rendering). hasUserMessageAfter answers "did the user start any new interaction after the snapshot?", which deliberately includes local slash commands. The omission is intentional, not a missing guard.

The steer behavior the finding flags is real but minor and self-healing (the next todo_write re-snapshots after the steer and the panel returns), and was already analyzed and accepted by the author and independently by @wenshao. A precise fix would require threading streaming/submission state into this pure data-layer function — out of scope for this focused fix and disproportionate to the impact.

Change made: added hides sticky todos when a local slash command starts after the snapshot to todoSnapshot.test.ts, using a faithful sentToModel: false fixture. It passes on the current code and would fail if the suggested guard were applied, so the regression cannot be introduced silently. Also extended the makeUserHistoryItem helper to carry optional sentToModel and relabeled the existing regular-message fixture so the two cases are unambiguous. No production source changed.

Feedback point 2 — Top-level review: PR body does not follow the PR template

Decision: Not actioned here — non-code item that requires author/workflow action.

This review asks for the PR description to be restructured under the template headings (What this PR does / Why it's needed, Reviewer Test Plan, Risk & Scope, Linked Issues, Chinese <details>). It is a PR-metadata change, not a code change, and is directed at the PR author to apply and then re-trigger @qwen-code /triage. It is outside the code-change scope of this round and cannot be resolved from the checkout.

Conflict notes

--conflict false; no merge performed.

Verification

  • npx vitest run src/ui/utils/todoSnapshot.test.ts (packages/cli) — 22 passed (was 21; +1 new regression test)
  • npm run typecheck — passed
  • npm run build — passed
  • npm run lint — passed
  • npx eslint packages/cli/src/ui/utils/todoSnapshot.test.ts — clean
中文说明

Autofix 审查轮次总结 — PR #7900

反馈点 1 — 行内 Critical [rc:3662969985]:hasUserMessageAfter 应加上 sentToModel !== false 判断

决定:拒绝建议的代码改动;新增一个回归测试钉住当前(正确的)行为。

建议的判断条件会重新引入本 PR 要修复的 bug。已在被审查的 commit 上逐行核实:

  • 普通用户消息的 sentToModelundefined —— packages/cli/src/ui/types.ts:124-131:"Regular user messages leave this undefined… visible slash-command invocations may be handled locally without entering API history."
  • 本地斜杠命令保持 sentToModel: false —— slashCommandProcessor.ts:857-860 添加 USER item 时为 sentToModel: false,仅当命令产生 submit_prompt 时才更新为 true/stats/about/exit/help/clear 等纯 UI 命令不会发给模型,因此始终保持 false
  • Steer 同样是 sentToModel: false —— useGeminiStream.ts:2576-2582,注释为 "Intentionally false… steer is not a standalone user turn."

因此 sentToModel !== false 无法区分轮内 steer 与本地斜杠命令 —— 两者都是 { type: 'user', sentToModel: false }。应用该判断会导致 /stats(正是 #7061 截图中的命令)不再隐藏过期面板,从而让 #7061 回归。这与 @wenshao090ad18 上的独立 E2E 验证一致:他确认当前代码下 /stats 能正确触发边界,并建议合并。

historyUtils.ts / useAwaySummary.ts / MainContent.tsx 中的 sentToModel !== false 模式回答的是另一个问题 —— "这是不是真正发给模型的 prompt?"(用于 rewind、telemetry、turn 渲染)。而 hasUserMessageAfter 回答的是"快照之后用户是否开启了任何新交互?",这有意包含本地斜杠命令。这里的省略是刻意的,而非漏掉判断。

该 finding 指出的 steer 行为确实存在,但影响很小且可自愈(steer 之后下一次 todo_write 会重建快照,面板随之恢复),作者已分析并接受,@wenshao 也独立认可。若要精确修复,需要把 streaming/提交状态引入这个纯数据层函数 —— 超出本次聚焦修复的范围,且与影响不成比例。

所做改动:todoSnapshot.test.ts 中新增 hides sticky todos when a local slash command starts after the snapshot,使用忠实的 sentToModel: false fixture。它在当前代码下通过,若应用建议的判断则会失败,从而防止该回归被悄悄引入。同时扩展了 makeUserHistoryItem helper 以支持可选的 sentToModel,并把已有的普通消息 fixture 改名以消除歧义。未改动任何生产源码。

反馈点 2 — 顶层 review:PR 描述未遵循 PR 模板

决定:本轮不处理 —— 属于需要作者/工作流处理的非代码项。

该 review 要求把 PR 描述按模板标题重新组织(What this PR does / Why it's needed、Reviewer Test Plan、Risk & Scope、Linked Issues、中文 <details>)。这是 PR 元数据改动,而非代码改动,需要 PR 作者自行修改后重新触发 @qwen-code /triage。它超出本轮代码修改的范围,也无法从当前 checkout 解决。

冲突说明

--conflict false;未执行 merge。

验证

  • npx vitest run src/ui/utils/todoSnapshot.test.ts(packages/cli)—— 22 通过(原 21;新增 1 个回归测试)
  • npm run typecheck —— 通过
  • npm run build —— 通过
  • npm run lint —— 通过
  • npx eslint packages/cli/src/ui/utils/todoSnapshot.test.ts —— 无问题

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

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

⚠️ This run could not certify that any of this diff was reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.

— qwen3.7-max via Qwen Code /review

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action taken

The only feedback on this PR is a [COMMENTED] review from the automated reviewer (qwen-code-ci-bot via qwen3.7-max) noting that it could not certify diff coverage because no review plan was provided. This is an informational message about the review tooling's process — it does not identify any code defect, suggest any implementation change, or request any action on the diff.

There are no inline comments, no issue-level comments, no failed checks, and no still-red checks. No code changes are warranted.

中文说明

未采取任何操作

本 PR 上唯一的反馈来自自动审查机器人(qwen-code-ci-bot,通过 qwen3.7-max)的一条 [COMMENTED] 评论,指出由于未提供审查计划,它无法验证 diff 的覆盖率。这是一条关于审查工具流程的信息性消息——它没有指出任何代码缺陷、没有建议任何实现变更,也没有要求对 diff 采取任何操作。

没有行内评论、没有 issue 级别的评论、没有失败的检查、也没有持续红色的检查。无需进行代码变更。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge July 28, 2026 10:06
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — and for the thorough root-cause analysis linking #7061, #7062, and #7831.

Template looks good ✓ (the earlier template issue from the prior commit appears resolved)

Problem: observed bug with clear evidence. The #7831 screenshot shows the exact scenario — model hits ECONNRESET, turn ends, user types /stats, stale todo panel reappears with in-progress indicators for work that already finished. PR #7062 fixed the idle case but left the new-turn case open. This is a real regression users hit.

Direction: aligned — this is a straightforward correctness fix for the sticky todo panel, squarely within the CLI's UI layer. No auth/sandbox/model-selection/telemetry concerns.

Size: not applicable (no core paths touched; both files are in packages/cli/src/ui/utils/).

Approach: the scope feels right. One helper (hasUserMessageAfter), one guard in getStickyTodos, three focused tests. The "Enter to steer" edge case is honestly acknowledged and the tradeoff is reasonable — steer implies a direction change so stale todos are expected. I don't see a materially simpler path.

Risk: no elevated risk signals (no high-risk paths matched).

Moving on to code review. 🔍

中文说明

感谢贡献!也感谢你把 #7061#7062#7831 之间的因果关系梳理得很清楚。

模板完整 ✓(之前提交中的模板问题已修复)

问题:已观测到的 bug,有明确证据。#7831 截图展示了完整场景——模型遇到 ECONNRESET,轮次结束,用户输入 /stats,过期 todo 面板带着进行中指示器重新出现。PR #7062 修复了空闲时的情况,但新轮次的场景没有覆盖。这是用户实际遇到的回归。

方向:对齐——这是 sticky todo 面板的直接正确性修复,完全在 CLI UI 层内。无 auth/sandbox/模型选择/遥测相关顾虑。

规模:不适用(未触及核心路径;两个文件均在 packages/cli/src/ui/utils/)。

方案:范围合理。一个辅助函数(hasUserMessageAfter),一个 getStickyTodos 中的守卫,三个聚焦的测试。"Enter to steer" 边界情况被诚实地承认了,权衡也合理——steer 意味着方向改变,旧 todo 本来就该过期。没有看到更简路径。

风险:无升级风险信号(未匹配高风险路径)。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

Reviewed at 086fdb3d19cc02044b6605c8dbb75f3a173dc804 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: given "stale sticky todos resurface when a new user message starts a new turn", I would add a guard in getStickyTodos that scans history after the todo snapshot's position for any type === 'user' item — if found, the snapshot belongs to a previous turn, return null.

Comparison with the diff: the PR does exactly this. hasUserMessageAfter is a clean linear scan from afterIndex + 1 to end. The guard sits after the recency check and before the all-completed check — logical ordering. No correctness bugs, no security concerns, no regressions I can see.

The test coverage is solid: three new cases covering (1) a new user message after the snapshot → hidden, (2) a local slash command with sentToModel: false → still hidden (guards against a subtle future regression where someone might filter on sentToModel), and (3) no new user message → panel still shows. The second test is particularly thoughtful — it documents why the check must not filter on sentToModel.

No critical blockers. No convention violations. The change is minimal and every line earns its place.

Testing

CI evidence (fetched via API for commit 086fdb3d):

The ubuntu unit test suite passed. Windows/macOS tests and integration tests were skipped — standard for fork PRs where the secret-gated matrix legs don't run.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (windows-latest, Node 22.x) ⏭️ skipped (fork)
Test (macos-latest, Node 22.x) ⏭️ skipped (fork)
Integration Tests (CLI, No Sandbox) ⏭️ skipped (fork)
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
review-pr ✅ success

Real-scenario testing: not performed. This is a fork PR — the author lacks write access, so the sandboxed lanes (@qwen-code /tmux, @qwen-code /verify) are unavailable. The change is a pure data-layer guard (no rendering logic modified); the visual effect is the absence of a panel that previously appeared incorrectly. The unit tests cover the logic comprehensively. A maintainer can verify manually: start a multi-step task with todos, let the turn finish, send a new message — the stale panel should not reappear.

Not verified: the "Enter to steer" edge case (panel briefly hides during steer, reappears when model updates todos). The PR's reasoning that steer implies a direction change is sound, but this path is not covered by a unit test.

中文说明

代码审查

独立方案: 针对"新轮次开始时过期 sticky todo 重新出现"的问题,我会在 getStickyTodos 中添加一个守卫:从 todo 快照位置向后扫描 history,如果存在 type === 'user' 的条目,说明快照属于上一轮,返回 null。

与 diff 对比: PR 的实现完全一致。hasUserMessageAfter 是从 afterIndex + 1 到末尾的简洁线性扫描。守卫放在 recency 检查之后、all-completed 检查之前——逻辑顺序合理。未发现正确性 bug、安全问题或回归。

测试覆盖扎实:三个新用例分别覆盖 (1) 快照后有新用户消息 → 隐藏,(2) sentToModel: false 的本地斜杠命令 → 仍然隐藏(防止未来有人加 sentToModel 过滤导致回归),(3) 无新用户消息 → 面板正常显示。第二个测试尤其周到——它记录了为什么检查不能过滤 sentToModel

无关键阻塞项。无规范违反。改动极简,每一行都有其存在价值。

测试

CI 证据(通过 API 获取,commit 086fdb3d):

ubuntu 单元测试套件通过。Windows/macOS 测试和集成测试被跳过——fork PR 的标准行为(密钥隔离的矩阵分支不运行)。

真实场景测试: 未执行。这是 fork PR——作者没有写权限,沙箱通道(@qwen-code /tmux@qwen-code /verify)不可用。改动是纯数据层守卫(未修改渲染逻辑);视觉效果是之前错误出现的面板不再出现。单元测试已全面覆盖逻辑。维护者可手动验证:启动含 todo 的多步骤任务,等轮次结束,发送新消息——过期面板不应重新出现。

未验证:"Enter to steer" 边界情况(steer 时面板短暂隐藏,模型更新 todo 后恢复)。PR 关于 steer 意味着方向改变的推理是合理的,但该路径没有单元测试覆盖。

Qwen Code · qwen3.8-max-preview

Reviewed at 086fdb3d19cc02044b6605c8dbb75f3a173dc804 · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 5/5 — clean across every stage; minimal, correct fix with strong test coverage.

This is exactly the kind of PR I like to see. The author traced a real user-facing regression through three related issues, identified the precise gap left by the prior fix, and closed it with 17 lines of production code and 67 lines of tests. The implementation matches my independent proposal perfectly — a simple "is there a user message after the snapshot?" guard. No over-engineering, no scope creep, no unrelated changes.

The "Enter to steer" tradeoff is the only wrinkle, and the PR handles it honestly: steer implies a direction change, the model typically re-issues todo_write shortly after, and the worst case is a briefly hidden panel. That's acceptable.

The prior CHANGES_REQUESTED (template issue on an earlier commit) is resolved — the current PR body follows the template correctly.

CI is green on ubuntu; the skipped matrix legs are standard fork-PR behavior, not a concern for a change this localized.

中文说明

置信度:5/5 —— 每个阶段都很干净;极简、正确的修复,测试覆盖充分。

这正是我喜欢看到的 PR。作者通过三个相关 issue 追踪了一个真实的用户可见回归,精确定位了前一个修复留下的缺口,用 17 行生产代码和 67 行测试关闭了它。实现与我的独立方案完全一致——一个简单的"快照之后是否有用户消息?"守卫。没有过度工程,没有范围蔓延,没有无关改动。

"Enter to steer" 的权衡是唯一的褶皱,PR 诚实地处理了它:steer 意味着方向改变,模型通常很快会重新调用 todo_write,最坏情况是面板短暂隐藏。可以接受。

之前的 CHANGES_REQUESTED(早期提交的模板问题)已解决——当前 PR body 正确遵循了模板。

CI 在 ubuntu 上绿色通过;跳过的矩阵分支是 fork PR 的标准行为,对于如此局部的改动不构成顾虑。

Qwen Code · qwen3.8-max-preview

Reviewed at 086fdb3d19cc02044b6605c8dbb75f3a173dc804 · re-run with @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.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Jul 28, 2026
Merged via the queue into QwenLM:main with commit 6f739c7 Jul 28, 2026
82 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sticky task panel remains visible after all tasks finish and conversation ends

4 participants