Skip to content

feat(web-shell): run read-only info commands immediately mid-turn - #8496

Open
wenshao wants to merge 8 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-mid-turn-info-commands
Open

feat(web-shell): run read-only info commands immediately mid-turn#8496
wenshao wants to merge 8 commits into
QwenLM:mainfrom
wenshao:feat/web-shell-mid-turn-info-commands

Conversation

@wenshao

@wenshao wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

In the Web Shell, the read-only info commands /stats, /about (same handler as /status) and /context now run immediately while a turn is streaming. Previously they were silently swallowed mid-turn: the composer cleared and nothing happened until the user re-ran the command after the turn finished. The command echo (the /stats user row) is still only appended when no turn is in flight, because that user row acts as a turn boundary and would split the active turn; only the echo is skipped mid-turn, and the command output itself renders inline as before.

Why it's needed

Long turns are exactly when users want to check token usage or session info, and today typing /stats or clicking the status-bar context indicator mid-turn produces zero feedback — the command just vanishes. It is safe to run these commands during a turn: they are read-only queries, and their results render as status blocks, which are not turn boundaries in the transcript turn-collapse logic, are not counted in turn metrics (tool/thinking/token counters), and stay visible when the turn is collapsed — identical to their behavior when idle. Skipping the echo also avoids finalizing the in-flight assistant block mid-stream, which appending a local user message would do.

Reviewer Test Plan

How to verify

  1. Open the Web Shell against a daemon session and send a prompt that keeps the model busy for a while (e.g. a multi-step task).
  2. While the turn is still streaming, type /stats (or /about, /context, or click the context indicator in the status bar) and press Enter.
  3. Expected: the stats/about/context output appears inline in the transcript immediately; the active turn's tool/thinking/token counters are unaffected and the turn is not split into two. The /stats echo row is absent mid-turn (it only appears when running the command while idle).
  4. Run the same commands while idle: behavior is unchanged — the echo row appears followed by the output.
  5. Unit tests cover the new echo helper: cd packages/web-shell && npx vitest run client/utils/localCommandQueue.test.ts (7 tests pass), plus the full App suite npx vitest run client/App.test.tsx (303 tests pass), npm run typecheck, ESLint and Prettier.

Evidence (Before & After)

Before (mid-turn): typing /stats cleared the composer with no toast, no output, and no queueing — the command was dropped. After (mid-turn): the stats output renders inline immediately with no echo row; idle behavior unchanged. No screenshots captured; verified via the unit suites above.

Tested on

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

Environment (optional)

Local automated verification on macOS: vitest unit tests, TypeScript typecheck, ESLint, Prettier. No manual browser session was recorded.

Risk & Scope

  • Main risk or tradeoff: mid-turn output has no command echo row above it (the echo is client-side transient state and never enters session history, so this only affects the live transcript); status blocks appearing inside the active turn are kept visible on collapse, same as when idle.
  • Not validated / out of scope: other echo-style local commands keep the current behavior — /tools (bare listing), /bug, /model --voice, /extensions install usage errors are still suppressed mid-turn; daemon-forwarded commands still go through the queue/blocked paths. No Playwright e2e was added.
  • Breaking changes / migration notes: none.

Linked Issues

None.

中文说明

本 PR 做了什么

在 Web Shell 中,只读信息类命令 /stats/about(与 /status 同一处理分支)和 /context 现在可以在回合进行中立即执行。此前它们在回合中会被静默吞掉:输入框被清空,但什么都不发生,用户必须等回合结束后重新输入。命令回显(即 /stats 那行用户消息)仍然只在没有回合进行时才追加,因为该用户行会作为回合边界把正在进行的回合切成两段;回合中只跳过回显,命令输出本身照常内联渲染。

为什么需要

长回合进行时恰恰是用户最想查看 token 用量或会话信息的时机,而现在回合中输入 /stats 或点击状态栏的 context 指示器没有任何反馈——命令直接消失。这些命令在回合中执行是安全的:它们都是只读查询,其结果以 status 块渲染,而 status 块在 transcript 的回合折叠逻辑中不是回合边界、不计入回合统计(tool/thinking/token 计数),回合折叠后也保持可见——与空闲时的行为完全一致。跳过回显还避免了在流式中途收尾正在进行的 assistant 块(追加本地用户消息会触发这一行为)。

Reviewer Test Plan

如何验证

  1. 用 Web Shell 连接一个 daemon 会话,发送一个会让模型忙较长时间的提示(例如多步任务)。
  2. 回合仍在流式输出时,输入 /stats(或 /about/context,或点击状态栏的 context 指示器)并回车。
  3. 预期:stats/about/context 输出立即内联出现在 transcript 中;当前回合的 tool/thinking/token 计数不受影响,回合不会被切成两段。回合中不会出现 /stats 回显行(回显只在空闲时执行命令时出现)。
  4. 空闲时执行相同命令:行为不变——先出现回显行,再出现输出。
  5. 单元测试覆盖新的回显 helper:cd packages/web-shell && npx vitest run client/utils/localCommandQueue.test.ts(7 个测试通过),以及完整 App 套件 npx vitest run client/App.test.tsx(303 个测试通过)、npm run typecheck、ESLint、Prettier。

Evidence (Before & After)

改动前(回合中):输入 /stats 后输入框被清空,无 toast、无输出、不排队——命令被丢弃。改动后(回合中):stats 输出立即内联渲染,无回显行;空闲时行为不变。未截图,以上述单元测试套件验证为准。

Tested on

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

Environment (optional)

macOS 本地自动化验证:vitest 单元测试、TypeScript typecheck、ESLint、Prettier。未录制手动浏览器会话。

Risk & Scope

  • 主要风险或权衡:回合中的输出上方没有命令回显行(回显是客户端瞬态,不进会话历史,只影响当前 transcript 显示);出现在活跃回合内的 status 块在折叠后保持可见,与空闲时一致。
  • 未验证 / 不在范围内:其他回显型本地命令保持现状——/tools(无参列表)、/bug/model --voice/extensions install 用法错误仍在回合中被抑制;转发 daemon 的命令仍走排队/阻塞路径。未新增 Playwright e2e。
  • 破坏性变更 / 迁移说明:无。

Linked Issues

无。

/stats, /about (/status) and /context were silently swallowed while a
turn was streaming, because their local user echo would act as a turn
boundary in applyTurnCollapse and split the active turn. Their output
is a status block, which is not a turn boundary and is not counted in
turn metrics, so only the echo needs to be skipped mid-turn.

Add appendLocalUserEchoIfIdle, which echoes when idle and skips the
echo while streaming without blocking the command, and switch these
three commands to it so their results render inline immediately even
during an active turn.
@wenshao

wenshao commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head b1a935e. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:

  • packages/web-shell/client/App.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

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

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finished — CI landed green on 508e2b0 and the deferred approval was posted. finalize run

Qwen Triage 已完成 —— 508e2b0 的 CI 全绿,延迟审批已提交。查看 finalize 运行

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed, not theoretical. I confirmed it in the base code: appendOrDeferLocalUserMessage returns "suppressed" while streaming and every caller bails out, so mid-turn /stats (and /about, /context, the status-bar indicator) clears the composer and silently does nothing — exactly as described.

Direction: aligned. Long turns are precisely when users want token/session info, and dropping the command with zero feedback is a real UX papercut. Supporting signal from the claude-code CHANGELOG: Fixed desktop sessions getting stuck showing "running" after a slash command was sent mid-turn — mid-turn slash-command handling is a recognized problem area upstream.

Size: not applicable to the core gate — all changes are in packages/web-shell/client/ (no core paths). ~60 production lines (App.tsx 32, localCommandQueue.ts 28) + 21 test lines; no generated/schema changes.

Approach: scope feels right. One small pure helper (appendLocalUserEchoIfIdle — skip the echo mid-turn, never block the command) reusing the existing LocalEchoSink interface, wired into exactly the three read-only call sites; the other echo-style commands (/tools, /bug, /model, /extensions) are deliberately left on the old behavior, and the description is accurate about that. Skipping the echo is the correct minimal move — the user row is the turn boundary, and suppressing it also avoids finalizing the in-flight assistant block.

Risk: no high-risk path matches; no elevated risk signals.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的真实问题,不是理论假设。我在基线代码中确认了:回合进行中 appendOrDeferLocalUserMessage 返回"抑制",所有调用方随即退出,因此回合中输入 /stats(以及 /about/context、点击状态栏指示器)会清空输入框并静默无反应——与 PR 描述完全一致。

方向:对齐。长回合进行时恰恰是用户最想查看 token/会话信息的时机,命令毫无反馈地消失是真实的体验问题。claude-code CHANGELOG 中有旁证:Fixed desktop sessions getting stuck showing "running" after a slash command was sent mid-turn——回合中的 slash 命令处理在上游也是已知问题区域。

规模:不涉及核心模块门槛——全部改动都在 packages/web-shell/client/(未触及核心路径)。生产代码约 60 行(App.tsx 32 行、localCommandQueue.ts 28 行)+ 测试 21 行;无生成/schema 文件改动。

方案:范围合理。一个小的纯函数 helper(appendLocalUserEchoIfIdle——回合中跳过回显、绝不阻塞命令本身),复用现有 LocalEchoSink 接口,只接入三个只读命令的调用点;其他回显类命令(/tools/bug/model/extensions)有意保持原有行为,PR 描述对此的说明是准确的。跳过回显是正确的最小改动——用户消息行就是回合边界,跳过它同时也避免了在流式中途收尾正在进行的 assistant 块。

风险:未命中高风险路径;无升级风险信号。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 508e2b042cea03efd2c4d6d89a94c5d0e2b6581c · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Code review

Clean pass — I verified the load-bearing claims against the base code rather than taking the description at face value:

  • Turn splitting: turns start only at user / user_shell messages (isTurnStartMessage in MessageList.tsx). All three commands render via store.dispatch([{ type: 'status' }]), which lands as a system/info message — never a turn boundary, so the active turn stays intact.
  • Collapse visibility & metrics: a status message carries no source, so isHideableStep keeps it visible when the turn collapses, and it contributes nothing to the tool/thinking/token counters — same as idle behavior, as claimed.
  • Echo skip rationale: skipping the echo also avoids finalizing the in-flight assistant block mid-stream, which is exactly why the suppression existed in the first place. resumeChatBottomFollow('smooth') still fires after the async result, so the transcript follows the tail even without the echo row.
  • No silent behavior loss: the dropped images argument was already unused (_images) in the base helper, and the streaming condition (streamingStateRef.current !== 'idle') is identical to the existing one. The untouched /tools, /bug, /model, /extensions paths match the scope description.

The change is minimal and idiomatic: one small pure helper reusing LocalEchoSink, three one-line call-site swaps, two focused unit tests. No critical issues. One non-blocking note: the new behavior is tested at the helper level; there's no App-level test exercising "mid-turn /stats runs immediately" (the existing 303-test App suite passes, so no regression — just the new wiring isn't pinned by a component test).

Test evidence (PR's own CI — unattended run, no local execution)

The platform-matrix and CLI-integration jobs were skipped by CI's Classify PR gate, consistent with a web-shell-only diff; the remaining live job is the web-shell E2E smoke, still queued at review time. The author's self-reported local results (7 helper tests, 303 App tests) are their claim; the CI signal below is what I verified via the API.

Final CI results for 508e2b0 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
route ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle the remaining gap: @qwen-code /tmux — that a mid-turn /stats actually renders inline in a live session without splitting or finalizing the active turn is not pinned by the unit suite (which covers the helper, not the live flow), and the author records no manual browser session.

中文说明

代码审查

审查通过——我对照基线代码逐条验证了关键论断,而不是只看 PR 描述:

  • 回合切分:回合只从 user / user_shell 消息开始(MessageList.tsxisTurnStartMessage)。三个命令都通过 store.dispatch([{ type: 'status' }]) 渲染,落地为 system/info 消息——绝不是回合边界,活动回合保持完整。
  • 折叠可见性与统计:status 消息不带 sourceisHideableStep 在回合折叠时仍保留其可见,且不计入 tool/thinking/token 计数——与空闲时行为一致,与 PR 声明相符。
  • 跳过回显的理由:跳过回显同时避免了在流式中途收尾正在进行的 assistant 块——这正是当初加入抑制逻辑的原因。异步结果返回后仍会调用 resumeChatBottomFollow('smooth'),即使没有回显行 transcript 也会跟随到底部。
  • 无隐性行为丢失:被去掉的 images 参数在基线 helper 中本就未使用(_images);流式判断条件(streamingStateRef.current !== 'idle')与现有逻辑完全一致。未改动的 /tools/bug/model/extensions 路径与 PR 范围说明一致。

改动最小且符合惯例:一个复用 LocalEchoSink 的小纯函数 helper、三处一行调用点替换、两个聚焦的单元测试。无阻塞问题。一条非阻塞建议:新行为目前只在 helper 层有测试覆盖,缺少 App 层"回合中 /stats 立即执行"的组件测试(现有 303 个 App 测试全部通过、无回归,只是新接线没有被组件测试钉住)。

测试证据(PR 自身 CI——无人值守运行,不在本地执行 PR 代码)

平台矩阵与 CLI 集成 job 被 CI 的 Classify PR 门禁跳过,与纯 web-shell 改动一致;审查时唯一未完成的 job 是 web-shell E2E smoke(排队中)。作者自报的本地结果(7 个 helper 测试、303 个 App 测试)是其声明;下表是我通过 API 核实的 CI 信号。CI 表格见上方标记区域(finalize 任务会在 CI 结束后原地更新)。

沙箱验证可以补上剩余缺口:@qwen-code /tmux ——"回合中输入 /stats 会在真实会话中内联渲染、且不切断或提前收尾活动回合"这一点目前没有被单元测试钉住(单测覆盖的是 helper,不是真实流程),且作者声明未录制手工浏览器会话。

Qwen Code · qwen3.8-max

Reviewed at 508e2b042cea03efd2c4d6d89a94c5d0e2b6581c · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal, well-reasoned change with every mechanism verified against the base code; the only reservations are non-blocking (no App-level test for the new wiring, and live-browser behavior rests on the pending web-shell E2E smoke rather than a recorded session).

Stepping back: this is exactly the kind of PR the gate should wave through. The problem is real and reproduced in the base code — mid-turn info commands vanish with zero feedback, at the moment users most want them. My independent take before reading the diff was the same shape the PR landed on: run the read-only commands immediately, suppress only the echo (the turn boundary), touch nothing else. The implementation matches that — one small pure helper in the existing echo module, three one-line call-site swaps, and a deliberate, accurately-described scope boundary around the other echo-style commands.

Every load-bearing claim checked out against the code, not just the description: status blocks are never turn boundaries, stay visible on collapse, don't pollute turn metrics, and the tail-follow survives the missing echo. The diff carries no drive-by edits; in six months this reads as an obvious improvement, not something to curse at. The two reservations are genuine but small: a component test pinning "mid-turn /stats runs immediately" would be a nice follow-up, and live confirmation is one queued E2E job (or a maintainer-triggered sandboxed run) away.

Verdict: approve. One CI job (web-shell E2E Smoke) is still queued, so approval is deferred until CI lands green on 508e2b042cea03efd2c4d6d89a94c5d0e2b6581c — the approval will be posted automatically against that exact commit once all checks complete.

中文说明

置信度:4/5 —— 干净、最小、论证充分的改动,每个机制都对照基线代码验证过;唯一的保留意见是非阻塞的(新接线缺少 App 层测试,真实浏览器行为目前取决于排队中的 web-shell E2E smoke,而非录制的会话)。

整体来看:这正是门禁应该放行的 PR。问题真实存在,且我在基线代码中复现了它——回合进行中信息类命令毫无反馈地消失,而这恰恰是用户最需要它们的时刻。我在读 diff 之前的独立方案与 PR 最终形态一致:只读命令立即执行、只抑制回显(回合边界)、其他一概不动。实现与此吻合——现有回显模块中一个小的纯函数 helper、三处一行替换,以及对其他回显类命令刻意且描述准确的范围边界。

每个关键论断都经受了代码级验证,而不只是相信描述:status 块绝不是回合边界、折叠后保持可见、不污染回合统计,缺少回显行时底部跟随依然生效。diff 没有夹带任何无关改动;六个月后回头看,这是显而易见的改进,而不是让人咒骂的东西。两个保留意见真实但很小:用一个组件测试钉住"回合中 /stats 立即执行"会是不错的后续补充;而线上确认只差一个排队中的 E2E job(或维护者触发的沙箱运行)。

结论:批准。由于还有一个 CI job(web-shell E2E Smoke)在排队,批准将推迟到该提交的 CI 全绿后自动发布——届时会针对该确切提交贴上批准。

Qwen Code · qwen3.8-max

Reviewed at 508e2b042cea03efd2c4d6d89a94c5d0e2b6581c · 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 — CI landed green after the review. ✅

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 4, 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

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

Autofix review round — no code change required

The PR is already approved ("LGTM, looks ready to ship — CI landed green") with all checks green and no failing or still-red checks. The only new feedback item is the automated web-shell visual preview informational comment, which notes that this PR edits a render-shaping file (packages/web-shell/client/App.tsx) but produces no screenshot diff, and suggests adding a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts.

After inspecting the change and the visual harness, I'm declining that suggestion with evidence rather than adding a scenario, for the reasons below. No source change is warranted this round.

Why a visual screenshot scenario is not appropriate for this change

1. This is a timing/interaction change, not a static visual change. The PR makes the read-only info commands (/stats, /about, /context) run immediately mid-turn instead of being silently dropped, and skips only the user echo row while streaming. The status block those commands render is byte-for-byte the same component already rendered in the idle case — the PR does not change how the result looks, only when it appears and whether the echo row precedes it. A before/after static-frame comparison cannot express "the command now runs instead of vanishing mid-turn."

2. The only mid-turn visual state is byte-nondeterministic. To show the changed behavior, a scenario would have to hold the session mid-turn so StreamingStatus renders. That component is driven by:

  • a braille spinner advanced by a 250 ms setInterval (SPINNER_FRAMES / setDotFrame in StreamingStatus.tsx),
  • a loading phrase picked via Math.random() and rotated on a timer,
  • an elapsed-time counter ticking on wall clock.

The harness's freezeLoopingAnimations() only pauses WAAPI / CSS @keyframes animations found via document.getAnimations(); its own comment warns that a spinner driven by a hand-rolled JS loop "would NOT be caught, and the flake would silently return." A setInterval + React-state spinner, a Math.random() phrase, and a wall-clock timer are all outside its reach, so any mid-turn capture would differ between identical runs and surface false-positive "changed" previews unrelated to the PR.

3. This matches the existing codebase convention. The split view scenario in the same file deliberately asserts the restore path without capturing a screenshot for exactly this reason: "the capture is byte-nondeterministic between identical runs — a flaky, redundant view that surfaces false-positive 'changed' previews unrelated to the PR under review." The established pattern for a nondeterministic state is a behavioral assertion, not a screenshot.

4. The behavior is already covered. The new appendLocalUserEchoIfIdle logic (echo when idle, skip-but-don't-block while streaming) is unit-tested in packages/web-shell/client/utils/localCommandQueue.test.ts, and the independent triage verified each mechanism against the base code and approved at confidence 4/5, explicitly marking the remaining coverage reservations as non-blocking.

Adding a static screenshot scenario here would introduce a flaky, diff-noise-producing test and grow the diff of an already-approved PR without asserting the actual behavior — the opposite of what the visual suite is designed for. If mid-turn behavioral coverage is wanted later, a recorded flows.spec.ts flow (where motion is the point) would be the right vehicle, but that is net-new scope beyond this feedback and is left to a maintainer to request.

中文说明

Autofix 评审轮次 —— 无需修改代码

本 PR 已被批准("LGTM,看起来可以合入 —— 评审后 CI 全绿"),所有检查均为绿色,没有失败或持续失败的检查。唯一的新反馈条目是自动化的 web-shell 视觉预览信息性评论:它注意到本 PR 修改了一个影响渲染的文件(packages/web-shell/client/App.tsx),但没有产生任何截图差异,因此建议向 packages/web-shell/client/e2e/visuals/screenshots.spec.ts 添加一个场景。

在检查了改动内容与视觉测试脚手架(harness)之后,我基于如下证据婉拒该建议,而不是添加场景。本轮不值得修改源码。

为什么视觉截图场景不适合本改动

1. 这是一个时序/交互改动,而非静态视觉改动。 本 PR 让只读信息命令(/stats/about/context)在回合进行中立即执行,而不是被静默丢弃,并且仅在流式输出时跳过用户回显行。这些命令渲染的状态块(status block)与空闲(idle)情况下已渲染的组件逐字节相同 —— 本 PR 并未改变结果的外观,只改变了它何时出现以及回显行是否出现在其前面。静态帧的前后对比无法表达"该命令现在会执行而不是在回合中消失"。

2. 唯一的回合中视觉状态是字节级不确定的。 要展示被改变的行为,场景必须让会话停留在回合中,使 StreamingStatus 渲染出来。而该组件由以下因素驱动:

  • 一个由 250 毫秒 setInterval 推进的盲文点阵加载动画(StreamingStatus.tsx 中的 SPINNER_FRAMES / setDotFrame),
  • 一个通过 Math.random() 选取并定时轮换的加载短语,
  • 一个按实时时钟计时的已用时间计数器。

harness 的 freezeLoopingAnimations() 只会暂停通过 document.getAnimations() 找到的 WAAPI / CSS @keyframes 动画;它自己的注释就警告:由手写 JS 循环驱动的加载动画"不会被捕获,flake(不稳定)会悄悄回归"。一个由 setInterval + React 状态驱动的动画、一个 Math.random() 短语以及一个实时时钟计时器全都超出其能力范围,因此任何回合中的截图在完全相同的多次运行之间都会不同,从而产生与本 PR 无关的假阳性"已变化"预览。

3. 这与现有代码库约定一致。 同一文件中的 split view 场景刻意只断言恢复路径而截图,原因正是:"该截图在相同运行之间是字节级不确定的 —— 一个不稳定、冗余的视图,会产生与本 PR 无关的假阳性'已变化'预览。" 对于不确定状态的既定做法是行为断言,而非截图。

4. 该行为已有测试覆盖。 新增的 appendLocalUserEchoIfIdle 逻辑(空闲时回显、流式时跳过但不阻塞)已在 packages/web-shell/client/utils/localCommandQueue.test.ts 中做了单元测试;独立的 triage 也已对照基础代码逐一验证了各个机制,并以 4/5 的信心批准,明确将剩余的覆盖保留意见标记为非阻塞。

在此添加一个静态截图场景,只会引入一个不稳定、只会产生差异噪声的测试,并使一个已被批准的 PR 的 diff 膨胀,却无法断言实际行为 —— 这与视觉测试套件的设计初衷背道而驰。如果之后需要回合中的行为覆盖,录制式的 flows.spec.ts flow(其中"动态过程"本身就是重点)才是合适的载体,但那属于超出本反馈的全新范围,留给维护者决定是否提出。

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


🧠 Handled by Qwen Code · model/模型 qwen3.8-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.

Test Plan (not a blocker): 7 tests pass — this review observed 2764 passed; 303 tests pass — this review observed 2764 passed.

中文说明

Test Plan(非阻断):7 tests pass — this review observed 2764 passed; 303 tests pass — this review observed 2764 passed

— qwen3.8-max via Qwen Code /review (v0.21.5)

Comment thread packages/web-shell/client/utils/localCommandQueue.ts Outdated
Comment thread packages/web-shell/client/utils/localCommandQueue.ts
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx
wenshao and others added 4 commits August 4, 2026 20:12
…fo commands

Address PR review. A status dispatch finalizes the active assistant
block by default, so running /stats, /about or /context mid-turn would
fragment the streaming answer around the status card and drop later
usage frames. Add an optional clearActiveText flag to the status event
and pass false from these three command dispatches, covered by
reducer-level tests.

Also collapse the echo gate into a single body (the new helper now
delegates to appendOrDeferLocalUserMessage), add App-level wiring tests
for the responding/idle behavior of /stats and /about, and surface
failed getStats via reportError instead of swallowing them.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

No code change this round — every finding is already fixed at HEAD, re-verified with full checks

All four inline findings from the round-1 review run (anchored at 508e2b04) were implemented before this round ran, by the PR author's own commit 867249815 ("fix(web-shell): keep streaming assistant block intact for mid-turn info commands", committed 2026-08-04 12:12 UTC — three minutes before the reply comments below were posted). This round re-verified each fix against HEAD 7206e8e (which since merged main) and re-ran the trusted verification commands. Everything passes; there is nothing left to change, so this round commits nothing.

Point-by-point dispositions

1. rc:3709814016 — [Critical] mid-turn status dispatch finalizes the in-flight assistant block → already fixed in code, re-verified.

  • The exact suggested fix was taken: a clearActiveText opt-out on the status event (DaemonUiStatusEvent.clearActiveText in the SDK types), forwarded by the reducer's case 'status' / case 'debug' into appendStatusBlock, whose opts.clearActiveText !== false guard preserves the default finalize for daemon-emitted events.
  • All three converted entry points dispatch with clearActiveText: false: /context + status-bar indicator (showContextUsage, App.tsx:5899), /stats (App.tsx:7594), /status + /about (App.tsx:7667). No other dispatch site of these commands exists.
  • Reducer-level tests were added covering both the default finalize and the opt-out (including that a mid-stream assistant.usage frame now lands on the kept-active block): packages/sdk-typescript/test/daemon-ui-transcript.test.ts — 3/3 pass locally.

2. rc:3709814017 — [Suggestion] duplicated idle gate with inverted polarity → already fixed in code, re-verified.
appendLocalUserEchoIfIdle now delegates (return !appendOrDeferLocalUserMessage(isStreaming, text, undefined, sink)), so the idle gate lives in exactly one body; the doc comment states the inverted polarity explicitly. Helper suite 7/7 pass locally.

3. rc:3709814023 — [Suggestion] wiring untested at the App level → already fixed in code, re-verified.
New App read-only local commands mid-turn suite in App.test.tsx: for /stats and /about, a responding state asserts the command runs, the echo is NOT appended, and the dispatch carries clearActiveText: false; an idle state asserts the echo is appended. This kills the surviving-mutation scenario (inverted predicate) named in the finding. App suite 307/307 pass locally.

4. rc:3709814027 — [Suggestion] failed /stats fully silent → already fixed in code, re-verified.
.catch((error: unknown) => { reportError(error, 'Failed to load stats'); }) (App.tsx:7599-7601), matching showContextUsage's catch.

5. Review-level note "Test Plan (not a blocker)" — informational, no change warranted.
The PR body's numbers were the focused-suite counts at writing time: localCommandQueue.test.ts = 7 tests (still 7 — confirmed), App.test.tsx = 303 tests then (307 now, after the 4 wiring tests above — confirmed). The review's 2764 passed is a broader vitest scope, not a contradiction of the focused-suite claims.

6. Maintainer reply comments rc:3712312856 / rc:3712313081 / rc:3712313381 / rc:3712313647 — content not recoverable, no action taken from them.
Each of these four replies — one per finding thread — has a body that is only a reference to a file on the commenter's machine (@/tmp/autofix/reply-r1.mdreply-r4.md). The files do not exist on this runner or anywhere in the repository, so the intended text cannot be read by anyone viewing the PR. Given the same author's fix commit 867249815 landed three minutes earlier and covers all four findings point-for-point, these read as the reply artifacts of that local fix run posted by reference instead of by content. If any of them contained something beyond what that commit already implements, please repost the body text.

Thread-resolution note

Because this round commits nothing, the workflow's deterministic guard deliberately does not auto-resolve any review thread this round (threads are only resolved while the live PR head is the exact commit a round pushed and verified). The four finding threads therefore stay open even though all four findings are fixed at HEAD — a maintainer may resolve them directly, and any future round that pushes a commit can resolve them through the normal path.

Verification (commands actually run this round, at HEAD 7206e8e)

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/sdk-typescript test/daemon-ui-transcript.test.ts (reducer, finding 1) — 3 passed
  • vitest packages/web-shell client/utils/localCommandQueue.test.ts (finding 2) — 7 passed
  • vitest packages/web-shell client/App.test.tsx (findings 3-4) — 307 passed
  • No commit this round, so no bundle/integration re-run and no settings-schema regeneration were applicable (no settings source touched; CI on 7206e8e is green).
中文说明

本轮无代码改动 —— 所有 finding 均已在 HEAD 修复,并已重新完整验证

第 1 轮 review(锚定于 508e2b04)提出的 4 条 inline finding,在本轮运行之前已由 PR 作者本人的提交 867249815("fix(web-shell): keep streaming assistant block intact for mid-turn info commands",提交于 2026-08-04 12:12 UTC——比下方的回复评论早 3 分钟)全部实现。本轮在 HEAD 7206e8e(其后已合入 main)上逐条复核了这些修复,并重新运行了可信验证命令。全部通过;没有需要改动的内容,因此本轮不产生任何提交。

逐条处理结论

1. rc:3709814016 —— [Critical] 回合中 status dispatch 会收尾正在流式的 assistant 块 → 已在代码中修复,复核通过。

  • 采用的正是建议中的修复:为 status 事件增加 clearActiveText 选项(SDK 类型中的 DaemonUiStatusEvent.clearActiveText),reducer 的 case 'status' / case 'debug' 将其转发给 appendStatusBlock;其中 opts.clearActiveText !== false 的守卫保证 daemon 发出的事件仍保持默认的收尾行为。
  • 三个被改造的入口全部以 clearActiveText: false 派发:/context + 状态栏指示器(showContextUsage,App.tsx:5899)、/stats(App.tsx:7594)、/status + /about(App.tsx:7667)。这些命令不存在其它派发点。
  • 已补充 reducer 级测试,同时覆盖默认收尾与选项豁免(包括流式中途到达的 assistant.usage 帧如今能落在保持活跃的块上):packages/sdk-typescript/test/daemon-ui-transcript.test.ts —— 本地 3/3 通过。

2. rc:3709814017 —— [Suggestion] 重复的空闲闸门且布尔极性相反 → 已在代码中修复,复核通过。
appendLocalUserEchoIfIdle 现改为委托实现(return !appendOrDeferLocalUserMessage(isStreaming, text, undefined, sink)),空闲闸门只存在于一处;doc 注释明确说明了相反的极性。helper 测试本地 7/7 通过。

3. rc:3709814023 —— [Suggestion] 接线缺少 App 层测试 → 已在代码中修复,复核通过。
App.test.tsx 新增 App read-only local commands mid-turn 套件:对 /stats/aboutresponding 状态断言命令执行、回显未被追加、dispatch 携带 clearActiveText: falseidle 状态断言回显被追加。这恰好杀死了 finding 指出的可存活变异(谓词反转)。App 套件本地 307/307 通过。

4. rc:3709814027 —— [Suggestion] /stats 失败完全静默 → 已在代码中修复,复核通过。
.catch((error: unknown) => { reportError(error, 'Failed to load stats'); })(App.tsx:7599-7601),与 showContextUsage 的 catch 保持一致。

5. review 级备注 "Test Plan (not a blocker)" —— 信息性说明,无需改动。
PR 描述中的数字是撰写时聚焦套件的测试数:localCommandQueue.test.ts = 7 个(现仍为 7,已确认)、App.test.tsx 当时 = 303 个(在新增上述 4 个接线测试后为 307,已确认)。review 观察到的 2764 passed 是更大范围的 vitest 运行结果,与聚焦套件的数字并不矛盾。

6. 维护者回复评论 rc:3712312856 / rc:3712313081 / rc:3712313381 / rc:3712313647 —— 内容无法恢复,未据此采取任何行动。
这 4 条回复分别位于 4 个 finding 线程内,但每条的正文都只是一个指向评论者本机文件的引用(@/tmp/autofix/reply-r1.mdreply-r4.md)。这些文件既不在本运行器上,也不在仓库中,任何查看 PR 的人都无法读到其真实内容。鉴于同一作者的修复提交 867249815 早 3 分钟落地、且逐点覆盖了全部 4 条 finding,这些评论看起来是该次本地修复运行生成的回复文件被以引用而非正文的形式贴出。若其中任何一条包含超出该提交已实现内容之外的意见,请重新贴出正文。

关于线程 resolve

由于本轮没有任何提交,工作流的确定性守卫在本轮不会自动 resolve 任何 review 线程(只有当 PR 的实时 head 恰为本轮推送并通过验证的提交时才会 resolve)。因此这 4 个 finding 线程将保持打开状态——尽管它们都已在 HEAD 修复;维护者可以直接手动 resolve,未来任何产生提交的轮次也可以通过正常路径 resolve。

验证(本轮在 HEAD 7206e8e 实际运行的命令)

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/sdk-typescript test/daemon-ui-transcript.test.ts(reducer,对应 finding 1)— 3 通过
  • vitest packages/web-shell client/utils/localCommandQueue.test.ts(对应 finding 2)— 7 通过
  • vitest packages/web-shell client/App.test.tsx(对应 finding 3-4)— 307 通过
  • 本轮无提交,因此无需重跑 bundle/集成测试,也无需重新生成 settings schema(未触及 settings 源;7206e8e 的 CI 为绿色)。

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


🧠 Handled by Qwen Code · model/模型 qwen3.8-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.

Reviewed. Suggestions are inline. Not reviewed: verification and reverse audit — each ran and opened its brief, but neither was launched with the prompt the CLI built — the launches were written by hand, so the posted findings cannot be counted as verified, and what the agents were actually asked is not what this skill certifies. Test Plan (not a blocker): client/App.test.tsxno such file or directory; 7 tests pass — this review observed 1446, 2782 passed; 303 tests pass — this review observed 1446, 2782 passed.

中文说明

已审查。 建议见行内评论。 未审查:验证与反向审计——两者都运行并打开了各自的 brief,但都不是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,发布的发现不能算作已验证,agent 实际被要求做的也不是本 skill 所认证的内容。 Test Plan(非阻断):client/App.test.tsxno such file or directory; 7 tests pass — this review observed 1446, 2782 passed; 303 tests pass — this review observed 1446, 2782 passed

— qwen3.8-max via Qwen Code /review (v0.21.5)

Comment on lines +344 to +346
appendStatusBlock(next, event.type, event.text, event, {
clearActiveText: event.clearActiveText,
});

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] The clearActiveText: false opt-out skips the entire clearActiveText(state) call — including state.activeUserBlockId = undefined. All three new dispatch sites pass the flag unconditionally, also when idle. Pre-PR, an idle status dispatch reset activeUserBlockId; post-PR, running /stats, /about or /context (or clicking the status-bar context indicator) while idle leaves the local command echo block as the active user block indefinitely.

Failure scenario: Web Shell client B is idle; its user runs /stats → echo block E becomes activeUserBlockId; the result dispatch leaves the pointer at E. A peer client (TUI or a second web tab on the same daemon session) then submits a prompt: the bridge echo arrives as a mergeable user.text.delta (no sourceRecordIds, no qwenDiscreteMessage), canMergeTextDelta passes, and the peer's prompt text is appended onto E — rendering /stats<peer prompt> in one user block. Since applyTurnCollapse bounds turns by user messages, the peer's entire turn (assistant text, tool steps, token usage) groups under the /stats echo's turn, corrupting turn boundaries and per-turn metrics. Verified by a reducer probe at the reviewed commit: the PR arm merged into one user block (/statsfix the bug) where the pre-PR control arm produced separate user blocks.

Suggested fix — probe-verified (flips the repro back to separate blocks while keeping this PR's reducer tests green): in appendStatusBlock, keep the assistant/thought block but drop the user pointer on the opt-out path:

  appendBlock(state, block);
  if (opts.clearActiveText !== false) clearActiveText(state);
  else state.activeUserBlockId = undefined;

(Alternative: pass clearActiveText: false from App.tsx only while streaming — idle dispatches have no streaming block to protect.)

中文说明

clearActiveText: false 选项跳过了整个 clearActiveText(state) 调用——包括 state.activeUserBlockId = undefined。三处新的 dispatch 都无条件传入该标志,空闲时也是如此。本 PR 之前,空闲时的 status dispatch 会重置 activeUserBlockId;现在,空闲时运行 /stats/about/context(或点击状态栏的 context 指示器)会让本地命令回显块无限期地保持为活跃用户块。

失败场景:Web Shell 客户端 B 空闲时运行 /stats → 回显块 E 成为 activeUserBlockId;结果 dispatch 使指针一直停留在 E。此时同一 daemon 会话上的对端客户端(TUI 或第二个网页标签页)提交提示词:桥的回显以可合并的 user.text.delta 到达(无 sourceRecordIds、无 qwenDiscreteMessage),canMergeTextDelta 通过,对端的提示词文本被追加到 E 上——一个用户块渲染出 /stats<对端提示词>。由于 applyTurnCollapse 以用户消息为回合边界,对端的整个回合(assistant 文本、工具步骤、token 用量)都会归入 /stats 回显所在的回合,破坏回合边界与逐回合统计。已在被审提交上用 reducer 探针验证:PR 分支合并为一个用户块(/statsfix the bug),而 PR 前的对照组产生独立的用户块。

建议修复(已用探针验证——复现恢复为独立块,且本 PR 的 reducer 测试仍全绿):在 appendStatusBlock 中保留 assistant/thought 块,但在 opt-out 路径上清掉用户指针:

  appendBlock(state, block);
  if (opts.clearActiveText !== false) clearActiveText(state);
  else state.activeUserBlockId = undefined;

(备选方案:仅在流式时才从 App.tsx 传 clearActiveText: false——空闲 dispatch 没有需要保护的流式块。)

— qwen3.8-max via Qwen Code /review (v0.21.5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment on lines +5890 to 5892
echoLocalCommandIfIdle(commandText);
sessionActions
.getContextUsage({ detail })

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.

[Suggestion] /context — the third command this PR converts to mid-turn execution — has no test anywhere in web-shell for this new wiring (no references to getContextUsage/showContextUsage in any test file), while /stats and /about each got four new App-level tests. All three entry points (keyboard /context, the status-bar context indicator, and the in-chat context-detail click) funnel through showContextUsage.

Concrete cost: a future edit reverting showContextUsage to echoOrDeferLocalCommand(commandText) (suppressing /context mid-turn again) or dropping clearActiveText: false from its dispatch leaves the entire suite green — the exact regression this PR fixes would ship.

Suggested fix — mirror the new /stats tests in the App read-only local commands mid-turn describe:

// streamingState = 'responding': submit /context
//   -> expect getContextUsage called, appendLocalUserMessage NOT called,
//      dispatch receives objectContaining({ type: 'status', clearActiveText: false })
// streamingState = 'idle': submit /context -> expect the echo appended
中文说明

/context 是本 PR 改造为回合中执行的第三个命令,但 web-shell 中没有任何测试覆盖这段新接线(任何测试文件中都没有 getContextUsage/showContextUsage 的引用),而 /stats/about 各新增了四个 App 级测试。三个入口(键盘 /context、状态栏 context 指示器、聊天内 context 详情点击)都汇聚到 showContextUsage

具体代价:未来某次编辑把 showContextUsage 改回 echoOrDeferLocalCommand(commandText)(重新在回合中抑制 /context),或从其 dispatch 中删掉 clearActiveText: false,整个测试套件仍会全绿——本 PR 所修复的回归会原样合入。

建议修复——在 App read-only local commands mid-turn describe 中仿照新增的 /stats 测试:

// streamingState = 'responding':提交 /context
//   -> 断言 getContextUsage 被调用、appendLocalUserMessage 未被调用、
//      dispatch 收到 objectContaining({ type: 'status', clearActiveText: false })
// streamingState = 'idle':提交 /context -> 断言回显被追加

— qwen3.8-max via Qwen Code /review (v0.21.5)

Comment on lines +7599 to +7601
.catch((error: unknown) => {
reportError(error, 'Failed to load stats');
});

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.

[Suggestion] This failure path changed from swallowed (.catch(() => {})) to reported via reportError, but no test pins the new reporting behaviour — there is no 'Failed to load stats' assertion and no getStats rejection mock in the suite.

Failure scenario: a revert to .catch(() => {}) passes the suite and load failures become invisible again (no toast, no console) — the very regression the previous review round fixed on this line. With the command no longer waiting for idle, getStats() rejection is now a realistic mid-turn race. The suite already establishes this guard pattern for /goal ("reports a failure to open a goal's session instead of swallowing it").

Suggested fix:

mockSessionActions.getStats.mockRejectedValueOnce(new Error('stats unavailable'));
// submit /stats, then assert the error reaches reportError (console.error/toast spy),
// matching the existing goal-failure test's style
中文说明

这条失败路径从被吞掉(.catch(() => {}))改为通过 reportError 上报,但没有任何测试固定新的上报行为——套件中既没有对 'Failed to load stats' 的断言,也没有 getStats 的 rejection mock。

失败场景:把代码改回 .catch(() => {}) 也能通过套件,加载失败会再次变得不可见(无 toast、无 console)——这正是上一轮评审在这行代码上修复的回归。由于命令不再等待空闲,getStats() reject 现在是现实的回合中竞态。套件中已有同样的守卫模式(/goal 的 "reports a failure to open a goal's session instead of swallowing it")。

建议修复:

mockSessionActions.getStats.mockRejectedValueOnce(new Error('stats unavailable'));
// 提交 /stats,然后断言错误到达了 reportError(console.error/toast spy),
// 与现有 goal 失败测试的写法保持一致

— qwen3.8-max via Qwen Code /review (v0.21.5)

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

Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): client/App.test.tsxno such file or directory.

中文说明

已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):client/App.test.tsxno such file or directory

— claude-opus-4-6 via Qwen Code /review (v0.21.5)

Comment on lines +7605 to 7607
echoLocalCommandIfIdle(text);
Promise.all([
workspaceActions.loadPreflight().catch(() => null),

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.

[Suggestion] The /about handler's Promise.all(...).then(...) chain has no .catch(), while the adjacent /stats handler was explicitly updated in this same diff to use reportError. — Concrete cost: if serializeStatusMessage, store.dispatch, or resumeChatBottomFollow throws synchronously inside the .then() callback, the rejection is unhandled — no error toast, no console diagnostic, silent failure. The /about path is the lone outlier among the three read-only display commands after this diff.

Append .catch((error: unknown) => { reportError(error, 'Failed to load status info'); }) to the Promise chain, matching /stats and /context.

中文说明

/about 处理器的 Promise.all(...).then(...) 链缺少 .catch(),而相邻的 /stats 处理器在本 diff 中已显式更新为使用 reportError。— 具体代价:如果 .then() 回调内的 serializeStatusMessagestore.dispatchresumeChatBottomFollow 同步抛出异常,该 rejection 将无人处理——没有错误 toast、没有控制台诊断信息,静默失败。本 diff 后 /about 是三个只读显示命令中唯一的遗漏。

在 Promise 链末尾追加 .catch((error: unknown) => { reportError(error, 'Failed to load status info'); }),与 /stats/context 保持一致。

— claude-opus-4-6 via Qwen Code /review (v0.21.5)

…aming (QwenLM#8496)

The clearActiveText: false opt-out skipped the whole clearActiveText call,
leaving the local command echo as the active user block indefinitely. A
peer client's prompt echo then merged into it, corrupting turn boundaries.
Keep the streaming assistant/thought block open on the opt-out path but
still drop the user pointer.

Also report /about load failures like /stats and /context already do, and
pin the new /context wiring plus the /stats failure path with tests.
@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 轮)。改动内容与我反驳保留之处如下:

Round summary

Addressed all four inline findings from this review round: one Critical and three Suggestions. Committed as 8782aad1f on top of the PR head.

Feedback and dispositions

  1. [Critical] clearActiveText: false opt-out leaves the command echo as the active user block (rc:3714828018)Fixed. Verified against the reviewed code: the opt-out skipped the entire clearActiveText(state) call, including state.activeUserBlockId = undefined, so an idle /stats, /about or /context dispatch left the local echo block mergeable, and a peer client's prompt echo (a mergeable user.text.delta with no sourceRecordIds) would append onto it — /stats<peer prompt> in one user block, corrupting applyTurnCollapse turn boundaries. Applied the reviewer's suggested reducer fix: on the opt-out path appendStatusBlock still keeps the streaming assistant/thought block open but now resets state.activeUserBlockId. This also applies to the pre-existing trimmed-tool-notification opt-out, matching the flag's documented intent (protect the assistant/thought block only). Added a regression test reproducing the probe scenario: after a clearActiveText: false status dispatch the user pointer is cleared, and a following mergeable user.text.delta opens its own user block instead of merging into the echo.
  2. [Suggestion] no test covers the new /context wiring (rc:3714828024)Fixed. Added getContextUsage to the mocked session actions (mock factory plus the beforeEach reset) and two tests in the App read-only local commands mid-turn describe mirroring the /stats pair: mid-turn /context runs immediately (getContextUsage called, echo skipped, dispatch carries { type: 'status', clearActiveText: false }), and idle /context appends the echo.
  3. [Suggestion] /stats failure reporting is unpinned (rc:3714828033)Fixed. Added a test that makes getStats reject and asserts the failure reaches reportError (console.error('[web-shell]', ...) with the error message), following the existing goal-failure guard test's style. Reverting to .catch(() => {}) now fails the suite.
  4. [Suggestion] /about Promise chain has no .catch() (rc:3714914972)Fixed. Appended .catch((error: unknown) => { reportError(error, 'Failed to load status info'); }) to the chain, matching /stats and /context; reportError was already a dependency of the enclosing callback. Adding the chain tail made Prettier re-indent the /about .then body (member-chain style) — a formatting-only change.

The review-level notes ("verification and reverse audit were not launched with the CLI-built prompt"; Test Plan client/App.test.tsxno such file or directory) are process observations, not code findings: the file lives at packages/web-shell/client/App.test.tsx and passes when run from the package directory. No code action.

Conflict

None (--conflict false; no merge performed).

Verification

  • cd packages/sdk-typescript && npx vitest run test/daemon-ui-transcript.test.ts — 4 passed (includes the new regression test)
  • cd packages/sdk-typescript && npx vitest run test/unit/daemonUi.test.ts — 279 passed (covers the pre-existing trimmed-tool opt-out path)
  • cd packages/sdk-typescript && npx vitest run — 31 files, 1447 passed (reviewer-observed baseline 1446; +1 new test)
  • cd packages/web-shell && npx vitest run client/App.test.tsx -t "read-only local commands mid-turn" — 7 passed (4 existing + 3 new)
  • cd packages/web-shell && npx vitest run client/App.test.tsx — 310 passed
  • cd packages/web-shell && npx vitest run — 166 files, 2785 passed (reviewer-observed baseline 2782; +3 new tests)
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check on the four touched files — clean
  • Integration tests: not run — the touched behavior is exercised by unit tests in both packages, not only through the bundled CLI or the integration harness.
中文说明

本轮总结

本轮处理了全部四条行内评审发现:一条 Critical、三条 Suggestion。已以 8782aad1f 提交到 PR head 之上。

反馈与处理

  1. [Critical] clearActiveText: false 选项使命令回显块残留为活跃用户块(rc:3714828018)——已修复。 已在被审代码上核实:该选项跳过了整个 clearActiveText(state) 调用(包括 state.activeUserBlockId = undefined),导致空闲时运行 /stats/about/context 后本地回显块仍可被合并;此时对端客户端的提示词回显(无 sourceRecordIds 的可合并 user.text.delta)会追加到该块上——一个用户块渲染出 /stats<对端提示词>,破坏 applyTurnCollapse 的回合边界。采用评审建议的 reducer 修复:opt-out 路径上 appendStatusBlock 仍保持流式 assistant/thought 块打开,但现在会重置 state.activeUserBlockId。该修复同样适用于既有的"工具输出被裁剪"通知 opt-out 路径,符合该标志的文档语义(仅保护 assistant/thought 块)。新增回归测试复现探针场景:clearActiveText: false 的 status dispatch 之后用户指针被清空,随后的可合并 user.text.delta 会新建自己的用户块,而不是并入回显块。
  2. [Suggestion] 新 /context 接线没有任何测试覆盖(rc:3714828024)——已修复。 在 mock 的 session actions 中补充 getContextUsage(mock 工厂 + beforeEach 重置各一处),并在 App read-only local commands mid-turn describe 中仿照 /stats 新增两个测试:回合中提交 /context 立即执行(getContextUsage 被调用、跳过回显、dispatch 携带 { type: 'status', clearActiveText: false });空闲时提交 /context 追加回显。
  3. [Suggestion] /stats 失败上报未被测试固定(rc:3714828033)——已修复。 新增测试:让 getStats reject,断言失败到达 reportErrorconsole.error('[web-shell]', ...) 且包含错误信息),写法与既有的 goal 失败守卫测试一致。若改回 .catch(() => {}),套件将失败。
  4. [Suggestion] /about 的 Promise 链缺少 .catch()(rc:3714914972)——已修复。 在链尾追加 .catch((error: unknown) => { reportError(error, 'Failed to load status info'); }),与 /stats/context 保持一致;reportError 本就在外层回调的依赖数组中。链尾的新增使 Prettier 按成员链风格对 /about.then 主体重新缩进——纯格式变更。

评审层面的备注("verification 与 reverse audit 不是用 CLI 构建的 prompt 启动的";Test Plan 中 client/App.test.tsxno such file or directory)属于流程观察,不是代码发现:该文件位于 packages/web-shell/client/App.test.tsx,在包目录下运行全部通过。无需代码改动。

冲突

无(--conflict false;未执行合并)。

验证

  • cd packages/sdk-typescript && npx vitest run test/daemon-ui-transcript.test.ts — 4 通过(含新增回归测试)
  • cd packages/sdk-typescript && npx vitest run test/unit/daemonUi.test.ts — 279 通过(覆盖既有的"工具输出被裁剪" opt-out 路径)
  • cd packages/sdk-typescript && npx vitest run — 31 个文件、1447 通过(评审观察到的基线为 1446;新增 1 个测试)
  • cd packages/web-shell && npx vitest run client/App.test.tsx -t "read-only local commands mid-turn" — 7 通过(4 个既有 + 3 个新增)
  • cd packages/web-shell && npx vitest run client/App.test.tsx — 310 通过
  • cd packages/web-shell && npx vitest run — 166 个文件、2785 通过(评审观察到的基线为 2782;新增 3 个测试)
  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0)
  • 对四个改动文件运行 npx prettier --check — 无格式问题
  • 集成测试:未运行——本次改动的行为由两个包的单元测试覆盖,并非只能通过打包后的 CLI 或集成测试框架验证。

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

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

Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): client/App.test.tsxno such file or directory; 7 tests pass — this review observed 1448, 2785 passed; 303 tests pass — this review observed 1448, 2785 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):client/App.test.tsxno such file or directory; 7 tests pass — this review observed 1448, 2785 passed; 303 tests pass — this review observed 1448, 2785 passed

— qwen3.8-max via Qwen Code /review (v0.21.5)

Comment thread packages/web-shell/client/App.tsx Outdated
Comment on lines +7593 to +7594
// Mid-turn runs must not finalize the streaming block.
clearActiveText: false,

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.

[Suggestion] The read-only command result dispatch — store.dispatch([{ type: 'status', text, ..., clearActiveText: false }]) followed by resumeChatBottomFollow('smooth') — is duplicated verbatim at the three sites this diff adds (showContextUsage ~5899, /stats ~7594, /status|about ~7669), each copy carrying the load-bearing flag. — Concrete cost: the invariant "a read-only result dispatch must pass clearActiveText: false" is enforced only by convention at three sites. When a fourth read-only command gains mid-turn support, or one of the copies is edited and drops the flag, the in-flight assistant/thought block silently finalizes mid-stream — the exact regression this PR exists to fix (the streaming answer splits at the status block; subsequent assistant.usage frames no longer attach to the active block). Suggested fix — centralize the flag + follow-resume pair in one callback next to echoLocalCommandIfIdle, called from all three .then(...) bodies (serializers stay at the call sites):

const dispatchReadOnlyStatus = useCallback(
  (text: string) => {
    store.dispatch([{ type: 'status', text, clearActiveText: false }]);
    resumeChatBottomFollow('smooth');
  },
  [store, resumeChatBottomFollow],
);
中文说明

只读命令的结果派发——store.dispatch([{ type: 'status', text, ..., clearActiveText: false }]) 后跟 resumeChatBottomFollow('smooth')——在本 diff 新增的三处调用点(showContextUsage ~5899、/stats ~7594、/status|about ~7669)被逐字复制,每处都携带这个关键标志。具体代价:「只读结果派发必须传 clearActiveText: false」这一不变量仅靠三处调用点的约定维持。当第四个只读命令获得回合中执行支持、或某一处副本被编辑而漏掉该标志时,会在流式中途悄悄收尾正在进行的 assistant/thought 块——正是本 PR 要修复的回归(流式回答在 status 块处被切断,后续 assistant.usage 帧不再挂到活跃块上)。建议修复——把「标志 + 恢复底部跟随」收进 echoLocalCommandIfIdle 旁的一个回调中(见英文部分代码),三个 .then(...) 主体统一调用它,序列化仍留在各调用点。

— qwen3.8-max via Qwen Code /review (v0.21.5)

Comment on lines +7674 to +7676
.catch((error: unknown) => {
reportError(error, 'Failed to load status info');
});

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.

[Suggestion] This new /about (/status) error-handling branch has no test, while the sibling new /stats catch is pinned by 'reports /stats load failures instead of swallowing them'. — Failure scenario: if a follow-up change drops this catch or replaces reportError with a swallow, the suite stays green and a failing /about regresses to an unhandled promise rejection with zero user feedback — the exact pre-PR behaviour this diff fixes for /stats only. Note: each Promise.all member catches to null, so this catch is reachable only via a throw inside the .then body — mirror the /stats failure test by making collectSystemInfo/serialization throw once, then assert console.error is called with '[web-shell]' and a message containing 'Failed to load status info'.

中文说明

新增的 /about/status)错误捕获没有测试覆盖,而同期新增的 /stats 捕获已有 'reports /stats load failures instead of swallowing them' 测试固定。失败场景:后续改动若删除此 catch 或将 reportError 换回静默吞掉,测试套件仍为绿色,/about 失败会退化为无任何用户反馈的未处理 promise rejection——正是本 diff 仅为 /stats 修复的改动前行为。注意:Promise.all 的每个成员都已 .catch(() => null),因此这个 catch 只能通过 .then 主体内部抛错来触达——测试应仿照 /stats 失败用例,让 collectSystemInfo/序列化过程抛错一次,然后断言 console.error'[web-shell]' 和包含 'Failed to load status info' 的消息被调用。

— qwen3.8-max via Qwen Code /review (v0.21.5)

QwenLM#8496)

Address the round-3 review feedback on the mid-turn read-only commands:

- The read-only result dispatch (status block with clearActiveText:
  false plus the follow-resume) was copied verbatim at the /context,
  /stats, and /about sites, leaving the load-bearing flag enforced by
  convention at three places. Centralize it in one
  dispatchReadOnlyStatus callback next to echoLocalCommandIfIdle; the
  three .then bodies now call it with their serialized text.
- Pin the /about catch the way the sibling /stats catch is pinned:
  make collectSystemInfo throw once and assert the failure surfaces
  through console.error instead of becoming an unhandled rejection
  with zero user feedback.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round summary

Round 3 on PR #8496 raised two inline Suggestions from the automated reviewer; both are resolved in commit 548879c3a on branch feat/web-shell-mid-turn-info-commands. No base conflict (--conflict false, no merge performed).

Feedback points and decisions

  1. [Suggestion] rc:3715931383 — duplicated read-only result dispatch (App.tsx ~7594) — RESOLVED.
    Verified: the dispatch of a status block carrying the load-bearing clearActiveText: false flag followed by resumeChatBottomFollow('smooth') was copied verbatim at the three sites this PR added (showContextUsage for /context, /stats, /status|about). Centralized the flag + follow-resume pair in one dispatchReadOnlyStatus callback placed next to echoLocalCommandIfIdle; the three .then(...) bodies now call it with their serialized text (serializers stay at the call sites, as suggested). The flag's "why" comment now lives once on that callback instead of three times at the call sites, and the change is a net 12-line shrink. Dependency arrays updated accordingly: showContextUsage drops store/resumeChatBottomFollow (no longer referenced in its body), and handleSubmit gains dispatchReadOnlyStatus while keeping store/resumeChatBottomFollow for its other, non-read-only dispatches (/tools, /bug, etc.). Existing tests pin the dispatched shape ({ type: 'status', clearActiveText: false }) and still pass unchanged.

  2. [Suggestion] rc:3715931394 — no test for the new /about (/status) error branch (App.tsx ~7676) — RESOLVED.
    Added 'reports /about load failures instead of swallowing them', mirroring the sibling 'reports /stats load failures instead of swallowing them' pin. As the finding notes, each Promise.all member catches to null, so the catch is reachable only via a throw inside the .then body: the test mocks ./utils/systemInfo so collectSystemInfo throws once, then asserts console.error is called with '[web-shell]' and the failure message. One deliberate deviation from the suggested assertion text: formatError prefers error.message over the fallback whenever the thrown value is an Error instance, so the assertion pins the thrown message ('status unavailable') rather than the 'Failed to load status info' fallback — the same shape as the /stats test, and it pins the identical catch → reportError wiring.
    The mock follows this test file's conventions: a hoisted vi.fn(), a full module replacement (App.tsx imports only collectSystemInfo from that module), and a default re-pinned in beforeEach (an all-empty SystemInfo, exactly what the real function returns for the default null preflight/env) — required because afterEach runs vi.restoreAllMocks(), which wipes any factory-set implementation.

No finding was declined, deferred, or escalated.

Verification

  • npx prettier --check packages/web-shell/client/App.tsx packages/web-shell/client/App.test.tsx — passed
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint (eslint . --ext .ts,.tsx && eslint integration-tests) — passed
  • vitest packages/web-shell (client/App.test.tsx + client/utils/localCommandQueue.test.ts) — 318 passed, 2 files; re-run on the committed tree after the pre-commit hook — 318 passed
  • vitest packages/web-shell (full package suite) — 2786 passed, 166 files
  • git commit pre-commit hook (lint-staged) — passed

Integration tests after npm run bundle: not run — the touched behavior is web-shell client dispatch logic, exercised by the package's unit harness, not only through the bundled CLI or integration harness. npm run generate:settings-schema: not needed — no settings source changed.

中文说明

本轮概要

PR #8496 的第 3 轮审查中,自动审查器提出了两条行内建议,均已在分支 feat/web-shell-mid-turn-info-commands 的提交 548879c3a 中解决。无基线冲突(--conflict false,未执行合并)。

反馈点与处理决定

  1. [建议] rc:3715931383 — 重复的只读结果派发(App.tsx ~7594) — 已解决。
    已核实:携带关键标志 clearActiveText: falsestatus 块派发、以及随后的 resumeChatBottomFollow('smooth'),在本 PR 新增的三处调用点(/contextshowContextUsage/stats/status|about)被逐字复制。现将「标志 + 恢复底部跟随」收进一个 dispatchReadOnlyStatus 回调,置于 echoLocalCommandIfIdle 旁;三个 .then(...) 主体改为以各自的序列化文本调用它(按建议,序列化仍留在各调用点)。该标志的 "why" 注释如今只存在于这个回调上,不再在三处调用点重复,整体净减少 12 行。依赖数组同步更新:showContextUsage 移除 store/resumeChatBottomFollow(其函数体不再引用它们),handleSubmit 新增 dispatchReadOnlyStatus,同时保留 store/resumeChatBottomFollow(其余非只读派发如 /tools/bug 仍在使用)。现有测试固定了派发形状({ type: 'status', clearActiveText: false }),未改动仍全部通过。

  2. [建议] rc:3715931394 — 新增 /about/status)错误分支缺少测试(App.tsx ~7676) — 已解决。
    新增 'reports /about load failures instead of swallowing them',仿照同期的 'reports /stats load failures instead of swallowing them' 固定用例。正如该发现所述,Promise.all 的每个成员都已 .catch(() => null),因此这个 catch 只能通过 .then 主体内部抛错来触达:测试 mock 了 ./utils/systemInfo,让 collectSystemInfo 抛错一次,然后断言 console.error'[web-shell]' 和失败消息被调用。相对建议中的断言文本有一处刻意偏差:只要抛出值是 Error 实例,formatError 就会优先使用 error.message 而非兜底文案,因此断言固定的是抛出的消息('status unavailable')而不是 'Failed to load status info' 兜底文案——与 /stats 测试同形,且固定的是同一条 catch → reportError 链路。
    该 mock 遵循本测试文件的惯例:一个 hoisted 的 vi.fn()、整体模块替换(App.tsx 仅从该模块导入 collectSystemInfo),并在 beforeEach 中重新固定默认实现(全空的 SystemInfo,与真实函数在默认 null preflight/env 下的返回完全一致)——这是必须的,因为 afterEach 会执行 vi.restoreAllMocks(),清除工厂中设置的任何实现。

没有发现被拒绝、推迟或升级处理。

验证

  • npx prettier --check packages/web-shell/client/App.tsx packages/web-shell/client/App.test.tsx — 通过
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run linteslint . --ext .ts,.tsx && eslint integration-tests)— 通过
  • vitest packages/web-shellclient/App.test.tsx + client/utils/localCommandQueue.test.ts)— 318 通过,2 个文件;pre-commit 钩子后在已提交树上重跑 — 318 通过
  • vitest packages/web-shell(整包测试套件)— 2786 通过,166 个文件
  • git commit pre-commit 钩子(lint-staged)— 通过

npm run bundle 后的集成测试:未运行——所触及的行为是 web-shell 客户端派发逻辑,由该包的单元测试覆盖,并非只能通过打包后的 CLI 或集成测试框架验证。npm run generate:settings-schema:无需运行——未改动任何 settings 源。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

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

Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): client/App.test.tsxno such file or directory; 7 tests pass — this review observed 1447, 2794 passed; 303 tests pass — this review observed 1447, 2794 passed.

中文说明

已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):client/App.test.tsxno such file or directory; 7 tests pass — this review observed 1447, 2794 passed; 303 tests pass — this review observed 1447, 2794 passed

— qwen3.8-max via Qwen Code /review (v0.21.5)

]);
});

it('keeps the assistant block active when clearActiveText is false', () => {

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.

[Suggestion] R4-3: The clearActiveText: false opt-out is only tested with an active assistant block; the thought-block half of the documented contract (types.ts: "without finalizing the active assistant/thought block") is unpinned — all three new reducer tests use assistant.text.delta only. — Failure scenario: named surviving mutation — adding clearActiveThought(state); to the reducer's opt-out else-branch keeps the entire sdk-typescript suite green (verified by applying the mutation: 1449 tests still pass), while a mid-turn status insertion would then finalize a streaming thought block (a thinking model still streaming thought when the user runs /stats), splitting it around the command output — the exact regression the flag exists to prevent. Suggested fix: add a mirror test with thought events.

it('keeps the thought block active when clearActiveText is false', () => {
  // user.text.delta → thought.text.delta → { type: 'status', clearActiveText: false }
  // → thought.text.delta → assistant.done
  // assert: a single thought block with merged text, still active after the status event
});
中文说明

clearActiveText: false 选项目前只用「活跃的 assistant 块」场景测试过;文档约定(types.ts:"不会收尾活跃的 assistant/thought 块")中 thought 块那一半没有任何测试固定——三条新 reducer 测试全部只使用 assistant.text.delta。失败场景:已点名的可存活变异——在 reducer 的 opt-out else 分支中加入 clearActiveThought(state);,整个 sdk-typescript 套件仍然全绿(已实际施加该变异验证:1449 个测试全部通过),而回合中插入 status 块此时会收尾正在流式的 thought 块(思考模型仍在流式输出 thought 时用户运行 /stats),把思考内容切成围绕命令输出的碎片——这正是该标志要避免的回归。建议修复:补充一个 thought 事件的镜像测试。

— qwen3.8-max via Qwen Code /review (v0.21.5)

});

describe('App read-only local commands mid-turn', () => {
it('runs /stats immediately while streaming and skips the echo', async () => {

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.

[Suggestion] R4-4: All three mid-turn tests pin only the dispatch envelope (type + clearActiveText), never the text payload — and nothing else pins it: serializeStatsMessage / serializeStatusMessage / serializeContextUsageMessage have zero unit tests, and the fully mocked store means no render-level assertion sees the text either. — Failure scenario: named surviving mutation — replacing dispatchReadOnlyStatus(serializeStatsMessage(result, statsView)) (and the /about / /context equivalents) with dispatchReadOnlyStatus('') keeps all eight new tests green while /stats//about//context render empty status blocks mid-turn — the user-visible output of this feature ships untested. Suggested fix: pin the serialized payload in at least one test per command, e.g.

expect(mockStore.dispatch).toHaveBeenCalledWith([
  expect.objectContaining({
    type: 'status',
    clearActiveText: false,
    text: serializeStatsMessage(statsFixture, 'summary'),
  }),
]);
中文说明

三条回合中测试都只固定了 dispatch 的外壳(type + clearActiveText),从未固定 text 载荷——也没有其它测试固定它:serializeStatsMessage / serializeStatusMessage / serializeContextUsageMessage 没有任何单元测试,且 store 被完全 mock,渲染层断言同样看不到这段文本。失败场景:已点名的可存活变异——把 dispatchReadOnlyStatus(serializeStatsMessage(result, statsView))(以及 /about/context 的对应调用)替换为 dispatchReadOnlyStatus(''),全部 8 个新测试仍然通过,而回合中 /stats//about//context 会渲染出空的 status 块——本功能的用户可见输出在无人测试的情况下上线。建议修复:每条命令至少在一个测试中固定序列化后的载荷,例如断言 text 等于对已知 fixture 调用序列化函数的结果(或至少包含某个已知字段)。

— qwen3.8-max via Qwen Code /review (v0.21.5)

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.

5 participants