Skip to content

fix(cli): improve slash command history feedback - #8365

Merged
DragonnZhang merged 25 commits into
QwenLM:mainfrom
DragonnZhang:dragon/slash-command-feedback
Aug 12, 2026
Merged

fix(cli): improve slash command history feedback#8365
DragonnZhang merged 25 commits into
QwenLM:mainfrom
DragonnZhang:dragon/slash-command-feedback

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

What this PR does

This PR keeps transient slash-command navigation out of visible TUI history. Authentication, settings, status, help, theme, editor, and diff commands no longer leave invocation-only rows, while the bare effort, stats, and statusline pickers receive the same treatment. Commands with direct execution semantics continue to show their invocation, aliases inherit their canonical command's behavior, and custom commands that override a built-in name are unaffected.

Closing the primary model picker without making a selection now adds Kept model as <current model>, so /model always has an explicit outcome. Successful model changes keep their existing detailed response.

Why it's needed

Interactive slash commands were appended to TUI history before their action was resolved. Commands that only opened a transient panel therefore left a bare command row after the panel closed, which looked like a missing response. The model picker had the same ambiguity when it was dismissed without changing the model.

Reviewer Test Plan

How to verify

  1. Start Qwen Code interactively, run /help, /theme, /editor, /diff, /effort, /stats, and /statusline, then close each panel with Escape. Confirm the panel opens normally and no invocation-only row remains.
  2. Run the aliases /?, /about, /connect, /login, and /usage. Confirm each behaves like its canonical command and does not leave an invocation row.
  3. Run /model, close it with Escape, and confirm the current model is reported as unchanged. Select a model and confirm the existing detailed selection response still appears.
  4. Run /effort high and confirm both the invocation and the success response remain visible. Confirm direct-action forms such as /statusline <prompt> and /stats export ... also retain their invocation.
  5. Define a custom /status command and confirm its invocation remains visible rather than inheriting the built-in suppression rule.

Evidence (Before & After)

Before, closing a transient panel left a bare row:

> /help

After, the panel closes without adding an invocation row. Cancelling the primary model picker now has an explicit result:

> /model
● Kept model as test-model

Real PTY testing also confirmed that /connect opens the provider dialog without an invocation row, while management commands such as /mcp retain their invocation as intended.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested yet
🐧 Linux ⚠️ not tested yet

Environment (optional)

Local TypeScript development runtime with an isolated QWEN_HOME; verification used a real PTY TUI session.

Risk & Scope

  • Main risk or tradeoff: Invocation suppression is intentionally limited to selected built-in command paths; new transient commands must opt into the same classification explicitly.
  • Not validated / out of scope: Auxiliary model pickers keep their existing cancellation behavior. Windows and Linux TUI behavior was not tested locally. CI remains responsible for cross-platform coverage.
  • Breaking changes / migration notes: None.

Linked Issues

None.

中文说明

本 PR 的改动

本 PR 不再让临时 slash command 导航进入可见的 TUI 历史。认证、设置、状态、帮助、主题、编辑器和 diff 命令不会再留下只有 invocation 的记录,无参数的 effort、stats 和 statusline 选择器也采用相同行为。具有直接执行语义的命令仍会显示 invocation;别名继承 canonical command 的行为;覆盖内置名称的自定义命令不受影响。

关闭主模型选择器且未选择新模型时,现在会新增 Kept model as <current model>,因此 /model 始终会得到明确结果。成功切换模型时继续使用原有的详细响应。

为什么需要这个改动

交互式 slash command 过去会在解析 action 之前先追加到 TUI 历史。因此,只打开临时面板的命令在面板关闭后会留下一个裸命令,看起来像缺少响应。模型选择器在未切换模型就关闭时也存在同样的歧义。

Reviewer 测试计划

验证方法

  1. 交互式启动 Qwen Code,依次运行 /help/theme/editor/diff/effort/stats/statusline,然后用 Escape 关闭面板。确认面板正常打开,关闭后没有只包含 invocation 的记录。
  2. 运行别名 /?/about/connect/login/usage。确认它们与 canonical command 行为一致,并且不会留下 invocation。
  3. 运行 /model,用 Escape 关闭,确认当前模型被报告为保持不变。再选择一个模型,确认原有的详细选择结果仍然出现。
  4. 运行 /effort high,确认 invocation 和成功响应均继续显示。确认 /statusline <prompt>/stats export ... 等直接操作形式也保留 invocation。
  5. 定义一个自定义 /status 命令,确认它的 invocation 仍然可见,不会继承内置命令的隐藏规则。

证据(改动前后)

改动前,关闭临时面板会留下裸命令:

> /help

改动后,面板关闭时不会新增 invocation。取消主模型选择器时现在会显示明确结果:

> /model
● Kept model as test-model

真实 PTY 测试还确认了 /connect 会打开 provider 对话框且不显示 invocation;/mcp 等管理命令则按预期继续保留 invocation。

测试平台

操作系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 尚未本地测试
🐧 Linux ⚠️ 尚未本地测试

环境(可选)

使用隔离 QWEN_HOME 的本地 TypeScript 开发运行时;通过真实 PTY TUI 会话完成验证。

风险与范围

  • 主要风险或权衡:invocation 隐藏有意限制在选定的内置命令路径;未来新增临时命令时需要显式加入相同分类。
  • 未验证或不在范围内:辅助模型选择器保持现有取消行为。Windows 和 Linux 的 TUI 行为未在本地测试,跨平台覆盖由 CI 负责。
  • 破坏性变更或迁移说明:无。

关联 Issue

无。

@DragonnZhang

Copy link
Copy Markdown
Collaborator Author

E2E Test Report

Tested the worktree build in a real PTY TUI on macOS with an isolated QWEN_HOME.

  • /help and /? opened Help; Escape left no invocation row.
  • /theme, /editor, and /diff opened their picker or panel; Escape left no invocation row.
  • Bare /effort, /stats, and /statusline opened their UI; Escape left no invocation row.
  • /connect opened Connect a Provider and left no invocation row.
  • /effort high retained its invocation and displayed Reasoning effort: high.
  • /statusline <prompt> retained its invocation; cancelling the resulting request displayed Request cancelled.
  • /mcp auth server retained its invocation and displayed the expected OAuth migration warning.
  • Bare /mcp retained its invocation after Escape, confirming management commands remain outside the suppression set.
  • /stats export was covered by a focused unit test instead of E2E execution to avoid writing an export file into the worktree.

Focused E2E-related unit runs passed: 17 passed | 89 skipped and 3 passed | 103 skipped. The complete related test run passed all 142 tests.

@DragonnZhang
DragonnZhang marked this pull request as ready for review August 2, 2026 06:44
@DragonnZhang
DragonnZhang enabled auto-merge August 2, 2026 06:44
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 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 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Gate re-run against the current head.

Template ✓ — all required sections present, bilingual body included.

Problem: observed, not theoretical — with unusually strong evidence. Any interactive session shows it: run /help or /theme, close the panel, and a bare > /help row is left behind with no outcome. The PR carries before/after traces, and a maintainer A/B-verified the behavior on a real PTY stack against the previous head (see the verification report in this thread).

Direction: aligned. Slash-command UX is an actively invested area here (e.g. #8130 safe slash commands during streaming — same author, #7818 /model --compaction). This removes a long-standing "command with no visible response" artifact from TUI history and makes /model dismissal unambiguous.

Size: 2175 changed lines = 1528 test / 647 production (incl. a 40-line design doc and 9 one-line i18n additions) / 0 generated. Two core paths are touched: packages/core/src/services/chatRecordingService.ts (+5/−0, additive optional payload field) and packages/cli/src/ui/auth/useAuth.ts (+23/−11, dialog-open latch). Production lines are over 500, so per the core-module policy this is flagged for maintainer awareness — not blocked on size, but this gate will not auto-approve; a human sign-off is required first.

Approach: the scope matches the problem. Hiding invocations necessarily extends to the reconstruction paths (/resume, desktop transcript merge) — otherwise resume would bring back exactly the rows the live session hid — and the PR carries that through consistently. The reveal-on-rejection path (/model --project --global shows its invocation when the args are refused) and the NO_COLOR /theme exemption are exactly the edge cases I'd have asked about; both are covered. Every edit traces to the stated goal — no drive-by changes spotted. The diff since the maintainer-verified head is a single test-only commit (isPickerOnlyModelInvocation regex boundaries).

Risk: no high-risk path matches in the revert-correlation scan.

Moving on to code review. 🔍

中文说明

针对当前 head 重新执行准入门检查。

模板 ✓ —— 必填章节齐全,包含中文说明。

问题: 已观测到、非理论问题——且证据异常充分。任何交互会话都能看到:运行 /help/theme 后关闭面板,历史里会留下一行没有结果的裸 > /help。PR 提供了 before/after 记录,维护者还在真实 PTY 环境对上一个 head 做过 A/B 验证(见本 thread 中的验证报告)。

方向: 对齐。slash command UX 是本仓库持续投入的领域(例如 #8130 流式响应期间允许安全 slash command——同一作者,#7818 /model --compaction)。本 PR 移除了 TUI 历史中长期存在的"命令没有可见响应"残留,并让 /model 的关闭结果不再含糊。

规模: 共 2175 行变更 = 1528 行测试 / 647 行生产代码(含 40 行设计文档和 9 处单行 i18n 词条)/ 0 行生成代码。触及两个核心路径:packages/core/src/services/chatRecordingService.ts(+5/−0,纯新增的可选字段)和 packages/cli/src/ui/auth/useAuth.ts(+23/−11,对话框打开来源锁)。生产行数超过 500,按核心模块策略标记为需维护者知悉——不因规模阻塞,但本关卡不会自动批准,需要先有人工确认。

方案: 范围与问题匹配。隐藏 invocation 必然要同步覆盖重建路径(/resume、desktop transcript 合并)——否则 resume 会把实时会话里隐藏掉的行重新带回来——PR 一致地做到了。参数被拒时重新显示 invocation(/model --project --global)以及 NO_COLOR/theme 的例外,正是我会追问的边界情况,两者都已覆盖。每处改动都能追溯到既定目标,未发现顺手改动。维护者验证之后新增的 diff 只有一个纯测试提交(isPickerOnlyModelInvocation 正则边界)。

风险: revert 相关性扫描未命中任何高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I formed an independent proposal before reading the diff (defer invocation recording until the action is resolved; classify built-ins only; persist a hide marker that resume/transcript reconstruction honors; give a cancelled /model an explicit outcome; aliases inherit the canonical command's class, custom overrides don't). The PR does all of that, plus the two edge cases I would have pushed on: reveal on rejection (/model --project --global gets its invocation row back when the args are refused) and the NO_COLOR /theme exemption. No critical blockers found.

Things I verified against the base code, not just the diff:

  • The old case 'message' handler went through addMessage, which bypasses recordedItems — so rejection/info messages never landed in the result record before. Switching to addItemWithRecording is what makes those errors survive /resume; that's an improvement, and the maintainer's A/B confirms it (rejected /model --project --global error survives resume on head, was lost on base).
  • The reveal path is correctly guarded: model-only, only while hidden, only when no invocation item exists — no double-add.
  • Old transcripts are unaffected: hiddenInvocation absent ⇒ treated as visible by both resumeHistoryUtils and the desktop merge. The desktop ancestor-walk keeps a user-turn boundary and a consumed-invocation dedup so a later same-name orphan result can't re-emit a paired invocation.
  • ModelDialog's three refs (close latch, selection-in-flight, selection-committed) cover double-Escape, double-selection, and Escape mid-switch; after a persistence failure the dialog stays open and closes silently — consistent behavior, covered by tests.
  • The picker-only regex is linear (no ambiguous quantifiers) and fails closed: unknown flags like --fastx keep the invocation visible.
  • The Kept model as {{model}} string is present in all 9 locales (resolves the earlier R12-2 finding), and the newest commit adds direct boundary tests for isPickerOnlyModelInvocation, the one classifier that previously had no direct coverage.

Non-blocking items already documented in this thread — no need to relitigate them here: the desktop-side hiddenInvocation patch check still has no test coverage (per the maintainer's mutation pass), auxiliary pickers close silently by documented design, and /insight counting picks up the extra dialog-side result records (pre-existing double-count, slightly amplified).

Files changed (27 of 34 shown)
File What changed
packages/cli/src/ui/hooks/slashCommandProcessor.ts Core of the change: hide classification resolved before the invocation row is added, reveal-on-rejection, messages now recorded, hide flag in the invocation record
packages/cli/src/ui/components/ModelDialog.tsx Kept-model feedback on dismissal, close latch and in-flight guards, result recording for every selection path, persistence-failure error state
packages/desktop/packages/shared/src/agent/qwen-agent.ts Transcript merge honors the hide flag; ancestor-walk pairing with dedup and user-turn boundary
packages/core/src/services/chatRecordingService.ts New optional hiddenInvocation field on the slash-command payload
packages/cli/src/ui/utils/resumeHistoryUtils.ts Resume reconstruction skips hidden invocation rows, still replays results
packages/cli/src/ui/commands/modelCommand.ts Picker-only args classifier for /model (empty or known flags only)
packages/cli/src/ui/commands/themeCommand.ts NO_COLOR rejects with a message before the picker opens
packages/cli/src/ui/auth/useAuth.ts Records /auth result only for a command-opened dialog; latch cleared on close
packages/cli/src/ui/components/StatusLineDialog.tsx /statusline save feedback recorded for reconstruction
packages/cli/src/ui/hooks/use-effort-command.ts /effort picker result recorded
packages/cli/src/ui/hooks/useEditorSettings.ts /editor selection feedback recorded
packages/cli/src/ui/hooks/useThemeCommand.ts NO_COLOR /theme feedback recorded
packages/cli/src/ui/AppContainer.tsx Threads config into the theme and editor hooks
docs/design/slash-command-feedback.md Design doc for the hide/reveal rules and the model-picker exception
packages/cli/src/i18n/locales/en.js Adds the Kept model as {{model}} string
packages/cli/src/ui/hooks/slashCommandProcessor.test.ts Classification matrix: hidden roots, bare pickers, aliases, subcommands, custom override, reveal, NO_COLOR
packages/cli/src/ui/components/ModelDialog.test.tsx Dismissal feedback, latches, in-flight guards, auxiliary-picker silence
packages/desktop/packages/shared/src/agent/tests/qwen-agent-slash-history.test.ts Hidden-invocation fixtures, orphan-result pairing, turn-boundary tests
packages/cli/src/ui/utils/resumeHistoryUtils.test.ts Hidden invocation skipped, result replayed on resume
packages/cli/src/ui/commands/modelCommand.test.ts Direct regex-boundary tests for the picker-only classifier (newest commit)
packages/cli/src/ui/auth/useAuth.test.ts Auto-open vs command-open recording latch
packages/cli/src/ui/commands/themeCommand.test.ts NO_COLOR message path
packages/cli/src/ui/hooks/use-effort-command.test.ts /effort result recording
packages/cli/src/ui/hooks/useEditorSettings.test.ts /editor result recording
packages/cli/src/ui/hooks/useThemeCommand.test.ts NO_COLOR feedback recording
packages/cli/src/ui/components/StatusLineDialog.test.tsx /statusline result recording
…and 8 more locale files Same one-line Kept-model string added (ca, de, fr, ja, pt, ru, zh-TW, zh)

Testing

CI signal for the reviewed commit, fetched via the API (this unattended run never executes PR code):

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (fork PR)
Test (windows-latest, Node 22.x) ⏭️ skipped (fork PR)
Integration Tests (CLI, No Sandbox) ⏭️ skipped (merge_group-gated by design)
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Classify PR / label / authorize / precheck-pr ✅ success

The Linux unit suite — including every test this PR adds — is green. The macOS/Windows legs are skipped as usual for fork PRs; the desktop bun suite (qwen-agent-slash-history.test.ts) is not executed by any gating CI leg. The maintainer covered both gaps locally at the previous head (9 affected CLI suites 285/285 on macOS; desktop bun 45/45, shared 424/424) — that is the maintainer's own attestation, quoted for context. The only commit since then is test-only.

Sandboxed verification would settle the remainder: @qwen-code /verify — the TUI suppression and resume-reconstruction claims are behavioural, and on CI they rest on the Linux unit suite alone while the macOS/Windows legs and the desktop bun suite stay ungated. This is a fork PR, so that is a sponsored run: a maintainer's @qwen-code /verify comment approves the head it was written against, and the run carries a pre-execution risk screen plus a full workspace wipe — read its report with the same skepticism as the fork's own CI logs.

  • not verified: live TUI behaviour in this run — unattended triage never builds or drives PR code; the maintainer's real-stack A/B (previous head) and the lane above are the behavioural evidence.
  • not verified: Windows and Linux TUI by the author (marked untested in the PR body); Linux unit coverage is green in CI.
中文说明

代码审查: 在读 diff 之前我先独立给出了方案(action 解析前暂缓记录 invocation、仅对内置命令分类、用隐藏标记让 resume/transcript 重建遵守、取消的 /model 给出明确结果、别名继承 canonical 命令而自定义覆盖不继承)。PR 全部做到了,还额外覆盖了我本来会追问的两个边界:参数被拒时重新显示 invocation、NO_COLOR/theme 的例外。未发现关键阻塞项。

几处对照基线代码(而非只看 diff)确认的点:旧的 case 'message'addMessage、不进 recordedItems,所以改用 addItemWithRecording 正是让报错能在 resume 后保留的原因(维护者 A/B 已证实);reveal 路径有三重守卫不会重复添加;旧 transcript 不受影响(无该字段即视为可见);desktop 侧祖先链配对带去重与 user-turn 边界;ModelDialog 的三个 ref 覆盖双击/中途 Escape/持久化失败;picker-only 正则线性且对未知 flag 保守地保留 invocation;新字符串已进全部 9 个语言包(解决 R12-2),最新提交补上了此前无直接覆盖的正则边界测试。

线程中已记录的非阻塞项不再重复:desktop 侧 hiddenInvocation 补丁检查仍无测试覆盖、辅助选择器按设计静默关闭、/insight 计数受既有重复计数问题轻微放大。

测试: 以上 CI 表格来自 API。Linux 单元套件(含本 PR 全部新增测试)通过;macOS/Windows 腿按 fork PR 惯例跳过,desktop bun 套件不在任何 gating CI 中运行(维护者曾在上一 head 本地覆盖,此处仅引用其自述)。其后唯一新提交是纯测试。行为类验证可用 @qwen-code /verify 托管运行补齐(fork PR,需维护者触发,运行前有筛查与工作区清理,报告应以其 CI 日志同样的怀疑眼光阅读)。未验证:本次运行未驱动真实 TUI(无人值守 triage 不执行 PR 代码);作者未测试 Windows/Linux TUI(Linux 单元由 CI 覆盖)。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean review, verified behaviour; the one thing that blocked the last run (the Stage 0 size escalation) is now resolved by the maintainer's own commit-pinned approval. The remaining items are documented non-blocking follow-ups.

The previous pass ended in a defer over policy, not substance: 647 production lines with two small core touches crossed the threshold where this gate hands the decision to a human instead of voting alone. That escalation has now been answered — @wenshao approved this exact commit (full SHA in the footer) with a real-stack A/B report attached: 12 transient commands leave zero bare rows on head, /model always has an explicit outcome, resume reconstruction reproduces the live history exactly, doubled-keystroke latches hold, and mutation probes show every CLI guard is load-bearing. Escalation means handing the decision to a maintainer, not vetoing the PR — with that decision made on the commit under review, deferring again would loop forever and gate nothing.

My own read is unchanged from the staged review above, re-verified against this head: the classifier hides only bare built-in root/picker forms, aliases inherit via the canonical path, custom overrides are exempt by the CommandKind.BUILT_IN check, argument rejection reveals the invocation behind a triple guard, and the two core touches are additive — an optional payload field in the recording service and a command-open latch in the auth hook. CI is green on this head for every leg that runs for fork PRs, and the only commit since the maintainer's verified head is a test-only one.

Non-blocking follow-ups, named for the record: the desktop-side hide-flag patch check (isPatchableQwenUserRecord) still has no covering test — the maintainer's mutation probe deletes it and 45/45 still pass; auxiliary pickers close silently by documented design; /insight counting inherits a slightly amplified pre-existing double-count.

Approving, pinned to the reviewed commit. ✅

中文说明

置信度:4/5 —— 审查干净、行为已验证;上一轮唯一的阻碍(Stage 0 规模升级)现已由维护者对同一提交的亲自批准解决。剩余事项均为已记录的非阻塞后续项。

上一轮以政策原因 defer,而非实质问题:647 行生产代码加两处小的核心改动越过了关卡自行投票前须交人工决定的阈值。该升级现已得到回应——@wenshao 在这同一个提交上批准(完整 SHA 见页脚),并附上真实环境 A/B 报告:12 个临时命令在 head 上残留零裸行、/model 始终有明确结果、resume 重建与实时历史完全一致、双击键锁有效、突变探针证明 CLI 侧每个守卫都起实际作用。升级的含义是把决定权交给维护者,而不是否决 PR——决定已在被审查的提交上作出,再次 defer 只会无限循环、毫无把关意义。

我本人的判断与上方各阶段审查一致,并已对当前 head 重新核验:分类器只隐藏裸内置 root/picker 形态,别名经由 canonical path 继承,自定义覆盖因 CommandKind.BUILT_IN 检查而豁免,参数被拒时在三重守卫下重新显示 invocation,两处核心改动均为纯增量——记录服务中一个可选负载字段与 auth hook 中一个命令打开锁。该 head 上 fork PR 可运行的全部 CI 腿通过;维护者验证的 head 之后仅新增一个纯测试提交。

非阻塞后续项记录在案:desktop 侧隐藏标记补丁检查(isPatchableQwenUserRecord)仍无测试覆盖——维护者的突变探针删除该检查后 45/45 依旧通过;辅助选择器按文档化设计静默关闭;/insight 计数继承了被轻微放大的既有重复计数。

批准,锚定到已审查的提交。✅

Qwen Code · qwen3.8-max

Reviewed at b6b0cc95379e15e9dbb522f600658a7d1a699e3a · 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-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: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

— qwen3.8-max-preview via Qwen Code /review

Comment thread packages/cli/src/ui/components/ModelDialog.test.tsx Outdated
Comment thread packages/cli/src/ui/components/ModelDialog.tsx Outdated

@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: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

— qwen3.8-max-preview via Qwen Code /review

Comment thread packages/cli/src/ui/components/ModelDialog.tsx

@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. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Overview

Two independent changes aimed at the same symptom — a slash command that leaves a bare > /cmd row with nothing after it:

  1. slashCommandProcessor.tsparseSlashCommand() is hoisted above the invocation-append so the command can be resolved first, and a new shouldHideSlashCommandInvocation() suppresses the MessageType.USER row for a hardcoded set of built-in commands (auth/diff/editor/help/settings/status/theme by root, plus bare effort/stats/statusline).
  2. ModelDialog.tsx — the repeated 5-way auxiliary-mode condition is extracted to isAuxiliaryModelMode, and dismissing the primary picker now appends Kept model as <model>.

The refactor of isAuxiliaryModelMode is a genuine readability win, the parseSlashCommand hoist is safe (the function is pure), chat-recording output is untouched (recordedItems.filter(item => item.type !== 'user') already dropped user rows), and the test suite is unusually thorough for this area — it.each over aliases, direct-action forms, and every auxiliary picker mode. Prettier passes on both source files.

The concerns below are mostly about the classification strategy, plus one behavior gap that undoes the fix.


1. Resumed / previewed sessions bring the bare invocation row back (main issue)

The invocation is still recorded unconditionally (recordSlashCommand({ phase: 'invocation', rawCommand })) — the new tests assert this deliberately. But resumeHistoryUtils.ts:315-336 rebuilds a visible user history item straight from that payload:

if (payload.phase === 'invocation' && payload.rawCommand) {
  items.push({ type: 'user', text: payload.rawCommand, ... });
}

buildResumedHistoryItems feeds /resume, /branch, /history, and SessionPreview. So /help is clean live, but after a resume the bare > /help row is back with nothing under it — the exact symptom the PR removes.

/model is worse, because the two halves of the PR don't compose: the invocation is recorded and restored, while Kept model as <model> goes through uiState.historyManager.addItem, and useHistoryManager.ts is pure React state with no recorder hook. A resumed session therefore shows > /model followed by nothing — reintroducing precisely the ambiguity this PR set out to fix. (The pre-existing Using model: … success message has the same display-only limitation, so that part isn't a regression — but the cancel message inherits it.)

Suggestion: carry the decision into the record rather than dropping it at the UI layer — e.g. a hiddenInvocation?: boolean on SlashCommandRecordPayload set from the same predicate, with resumeHistoryUtils skipping the user item when it's set. That keeps rawCommand for telemetry and /insight while making live and replayed history agree.

2. Classification is a hardcoded list living far from the commands

SLASH_COMMAND_ROOTS_HIDE_INVOCATION / BARE_SLASH_COMMANDS_HIDE_INVOCATION encode knowledge that belongs on the command definition. Practical consequences:

  • Whole-subtree hiding. The root rule hides /status paths too, which is not a picker — it prints session path text (aboutCommand.ts subCommands). That contradicts the design doc's own rule ("preserve invocations for commands that directly perform work"), and it's the only place where a text-producing subcommand loses its invocation. Was that intended, or should the match be on the full canonical path?
  • Drift. Renaming a command, or adding a new dialog-only one, silently changes behavior with nothing in the command file hinting that a list elsewhere must be updated. types.ts already carries per-command metadata (hidden, supportedModes, canRunDuringStreaming); a transientInvocation?: boolean (or a predicate for the argument-sensitive cases) would keep the rule next to the thing it describes and let extensions opt in.
  • Latent sentToModel bug. If a command under a hidden root ever returns submit_prompt, invocationItemId is undefined, so the updateItem(invocationItemId, { sentToModel: true }) branch is skipped and invocationSentToModel stays false — the recorded invocation is misclassified as never sent to the model. No current command hits this (/statusline <prompt> submits but is only hidden when bare), but nothing structurally prevents it.

3. Two opposite strategies for the same UX problem

/theme, /editor, /settings, /effort, /stats, /statusline: dismissing the picker leaves no trace at all. /model: keeps the invocation and adds an explicit "nothing changed" message. Both are "transient picker dismissed without a choice". As written the rule is hard for a user to predict, and the design doc doesn't justify the split — /model fits BARE_SLASH_COMMANDS_HIDE_INVOCATION cleanly (/model <id> stays visible, bare /model doesn't), which would also make issue #1 moot for it. Whichever way it lands, it'd be worth stating the rationale in docs/design/slash-command-feedback.md.

4. Missing translations

t('Kept model as {{model}}') isn't in any of packages/cli/src/i18n/locales/*.js, so non-English users see English. Its siblings in the same file are all translated ('Fast Model': '快速模型', 'Compaction Model': '压缩模型', 'Image Model': '图像模型' in zh.js). No test enforces this, so CI won't catch it.

5. closeWithoutSelection isn't idempotent

The old handler called onClose(), which was harmless when invoked twice. Now each call appends a history item, and onClose clears isModelDialogOpen through React state — asynchronously. Two ESC bytes in one stdin chunk (fast double-tap, or a pasted sequence) can append two Kept model as … rows. A useRef latch would settle it. The new test presses escape then a, so a real double-escape isn't covered.

6. Runtime-snapshot model naming (worth a check)

In primary mode with an active runtime snapshot, the highlighted row is keyed by activeRuntimeSnapshot.id, but the cancel message prints preferredModelId (config.getModel()). handleModelSwitchSuccess deliberately distinguishes Using runtime model: from Using model: — worth confirming the cancel message names the same thing the picker had highlighted.

7. Test coverage gaps

  • The FILE-override test calls setupProcessorHook([], [command]) with no built-in status registered, so it verifies the kind !== BUILT_IN check but not that a user command actually wins precedence over the built-in — which is what the PR description claims.
  • No coverage for subtree hiding (/status paths) or for a subcommand that should stay visible (/stats model).
  • No assertion that the cancel message is skipped when a selection already occurred.

Security / performance

No concerns. The new predicate is two Set lookups on an already-parsed path; the hoisted parseSlashCommand call replaces the old one rather than adding a second parse. Nothing touches auth material, and the new message interpolates a model id already rendered elsewhere in the same dialog.

Summary

The direction is right and the ModelDialog refactor is a clear improvement. Blocking-ish: #1, since resume/branch/preview restore exactly the row the PR hides — and for /model restore the invocation while dropping its explanation. #2 and #3 are structural and worth settling before this ossifies. #4 is a small omission.

Note: findings are from static review against pr/8365 (296961e) — I did not execute the test suites, as the local checkout is on an unrelated branch.

@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. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@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 7, 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-dev-bot

qwen-code-dev-bot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

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

中文说明

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

@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 74 passed · 0 failed · 74 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:74 通过 · 0 失败 · 74 总计

Verification report

PR 8365 — fix(cli): improve slash command history feedback

Verdict: merge-ready — 74/74 scripted assertions passed (68 A/B TUI cells, 5 mutation kills, 1 targeted gate), 0 unexpected failures. Verified head: 296961ece93305bf9b3703f9580d323e0d2bbb93 (base 2221ea03255f0c0d793ea13062728bcbdd2e85e0).

中文摘要
  • 结论: merge-ready。74/74 脚本化断言通过,0 个意外失败。
  • A/B 结论: 在真实 PTY TUI 会话中(node-pty + xterm headless,隔离 HOME,假 API key),PR 的核心行为全部翻转且对照稳定:base 上 /help/theme、裸 /stats、裸 /effort/connect/about 关闭后留下裸 invocation 行,head 上不再留下(7 个翻转格全部 FLIP);取消主模型选择器时 head 新增 ● Kept model as qwen3.7-max,base 无此行。对照格(/model/mcp/effort high、自定义 /status 的 invocation 保留;模型选择成功路径的详细响应)两臂完全一致。见 01-ab-head-vs-base.png04-tui-help-base-vs-head.png
  • 测试钉合: 5 个单点突变(整体关闭抑制、删除 kind 守卫、删除 Kept-model 消息、删除辅助选择器守卫、删除参数子句)分别使 13/1/1/10/2 个新增测试按预期变红,无幸存突变;定向门禁 152/152 通过。
  • Findings: 无阻塞项。3 条建议级观察:用户 FILE 命令覆盖内置名时其别名静默消失(既有行为,且使测试计划 step 2 与 step 5 无法在同一会话共存);settings 持久化后的命令注册表重载窗口内提交的命令会报 "Unknown command"(既有行为,两臂共有);Kept model as 在 runtime 模型激活时可能显示原始 $runtime|… ID(代码推断,未实测)。
  • 未覆盖: 每提交归因(shallow checkout 仅可达 merge commit);/editor/diff/settings 未实机驱动(同一 root 集合代码路径,由单测+突变钉住);辅助模型选择器的取消路径仅单测级验证;Windows/macOS 未测。

Central claim + A/B

Central claim: built-in transient slash commands stop leaving invocation-only rows in TUI history (roots auth|diff|editor|help|settings|status|theme always; bare effort|stats|statusline only without args), aliases inherit canonical behavior, direct-action forms and non-built-in commands keep their invocation; cancelling the primary /model picker adds an explicit Kept model as <model> row.

Environment per cell: real compiled dist/ CLI (head: CI build at 296961ec; base: tsc --build of packages/cli in a HEAD^1 worktree), driven over a real PTY by harness/tui-harness.mjs (@lydell/node-pty + @xterm/headless, the same stack as the repo's interactive integration tests), isolated HOME + workspace with security.auth.selectedType=openai, fake OPENAI_API_KEY, loopback OPENAI_BASE_URL. 5 sessions per arm; every cell asserts on the rendered terminal grid, with dialog open/close markers polled (not fixed sleeps).

Control purity: git diff HEAD^1..HEAD touches only packages/cli + docs/ (core/acp-bridge/audio-capture/sdk/channels/lockfile all empty). The base worktree resolves @qwen-code/qwen-code-core via the root node_modules symlink to the head tree (readlink -f asserted); since that source diff is empty, both arms run byte-identical non-cli code. The base worktree needed the root node_modules symlinked in because packages/core/tsconfig.json's paths shim for @lydell/node-pty is relative to the repo root — a worktree artifact, not a PR property.

cell (observable) base head verdict
/help + Esc — invocation row YES no FLIP
/theme + Esc — invocation row YES no FLIP
/stats (bare) + Esc — invocation row YES no FLIP
/effort (bare) + Esc — invocation row YES no FLIP
/connect alias + Esc — invocation row YES no FLIP
/about alias — invocation row (status output still shown) YES no FLIP
/model + Esc — Kept model as qwen3.7-max no YES FLIP
/model + Esc — invocation row kept YES YES same
/mcp — invocation row kept YES YES same
/effort high — invocation row kept + response YES YES same
custom /status (FILE override) — invocation row kept YES YES same
/model select — detailed authType/model/Base URL response YES YES same

Both arms: 34/34 assertions pass (raw per-cell logs in logs/{base,head}-*.txt + .ansi, summaries in logs/{base,head}-run.txt). Witness: evidence/01-ab-head-vs-base.png (cell table), evidence/04-tui-help-base-vs-head.png (the same real TUI session rendered side by side — base shows > /help, head shows nothing).

Sibling sweep of the suppression sets: all seven suppressed roots are dialog- or info-only in the real registry (auth/settings/editor/theme open dialogs; status renders an About/paths info item; diff renders output); arg-taking forms of the bare trio (/effort high, /statusline <prompt>, /stats export) are retained live. Chat recording is unchanged by construction: recordSlashCommand runs in the finally block regardless of suppression (phases invocation+result), pinned by the new unit tests.

Findings

F1 (suggestion) — a user command that overrides a built-in name silently drops that built-in's aliases. With ~/.qwen/commands/status.md present, the FILE command replaces the built-in status (loader order: later wins), and /about becomes Unknown command for the whole session — verified live on base. This is pre-existing precedence behavior, and the PR's own test-plan steps 2 (/about behaves like canonical) and 5 (custom /status keeps invocation) cannot both hold in one session. Not a PR defect; worth a line in the design doc.

F2 (suggestion) — transient "Unknown command" window after settings persistence. Selecting a model or /effort high writes settings.json; the resulting settings reload re-runs the command loader, and a command submitted inside that window resolves as unknown (observed on base during harness development; the code path is shared by both arms). Pre-existing, not introduced by this PR; the harness hit it only when a persisting command was followed immediately by another submit.

F3 (suggestion, unverified) — Kept model as may print a raw runtime-snapshot id. preferredModelId falls back to config?.getModel(), which for an active runtime model can be the $runtime|<authType>|<modelId> snapshot string; the cancel message would then show that raw id. Not exercised live (needs a runtime snapshot); the primary-picker path with a plain configured model prints the friendly id (qwen3.7-max observed).

Not covered

  • Per-commit attribution: the checkout is shallow (--is-shallow-repository=true); git rev-list HEAD^1..HEAD^2 reaches only the merge commit, while the snapshot lists 3 feature commits. Verified the aggregate HEAD^1..HEAD diff only.
  • /editor, /diff, /settings were not driven live; they share the root-set branch with the live-tested roots and are pinned by the unit tests + mutations M1/M2. Auxiliary model-picker cancel (fast/voice/vision/compaction/image) verified at unit level only (M4), not via live TUI.
  • The model-select cell selected against a loopback base URL; real provider validation paths untouched by this PR.
  • Repo-wide gates (full test suite, lint, typecheck of other workspaces) not re-run here; tsc --build of packages/cli at head passed (that is the head build used for the A/B), and the two affected suites ran green.
  • Windows/macOS TUI behavior (author-tested macOS only); container here is Linux.

Methodology

Environment: node:22-bookworm CI container, merge-ref checkout (HEAD = merge of 296961ec into 2221ea03), npm ci + npm run build pre-run at head. The A/B harness (harness/tui-harness.mjs) spawns the real CLI binary per cell under @lydell/node-pty, replays ANSI through @xterm/headless with allowProposedApi, and asserts on the reconstructed grid; readiness is gated on the /-popup command counter being stable (≥70 commands) to dodge the async command-registry load. Base arm = tsc --build of packages/cli (plus referenced projects) in a HEAD^1 worktree with the root node_modules symlinked (required by core's paths shim); control purity asserted via empty git diff on every non-cli workspace and readlink -f on internal package links. Mutation matrix applied as in-place edits with byte-identical restores (verified by diff against backups in mutation-backup/). Raw logs: logs/ (per-cell .txt grids + .ansi streams, per-arm assertion JSON, run transcripts); captures: evidence/01..04-*.png via scripts/verify-capture.mjs. Assertion tally: 68 A/B + 5 mutation-kills-as-expected + 1 targeted-gate-green = 74.

Evidence images

01-ab-head-vs-base

02-targeted-gate-152-pass

03-mutation-m1-kills-13

04-tui-help-base-vs-head

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on 296961ece93305bf9b3703f9580d323e0d2bbb93 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 296961ece93305bf9b3703f9580d323e0d2bbb93 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@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 轮)。改动内容与我反驳保留之处如下:

Address review summary — PR #8365

Round changes are in commit d7a14bf46 (fix(cli): persist slash command invocation hiding across resume). No base conflicts (--conflict false); origin/main was not merged.

Issue-level review (@wenshao)

1. Resumed/previewed sessions bring the bare invocation row back — FIXED

The hiding decision is now carried into the chat record instead of being dropped at the UI layer, as suggested:

  • SlashCommandRecordPayload gains hiddenInvocation?: boolean (packages/core).
  • The processor records hiddenInvocation from the same predicate that hides the TUI row.
  • buildResumedHistoryItems (feeds /resume, /branch, /history, and SessionPreview) skips the reconstructed user item when the flag is set. rawCommand stays in the record for telemetry.
  • /model no longer restores as a bare row either, because bare /model is now hidden (see 如何自定义密钥文件 .env可能与其他文件冲突 #3).
  • Tests: recording assertions on both hidden (hiddenInvocation: true) and visible (false) commands, plus a resume reconstruction test proving the hidden row is skipped.

Note: the desktop transcript reconstruction in packages/desktop/.../qwen-agent.ts also renders slash invocations as user messages. It is a separate subsystem and was intentionally left untouched; flagging it as a follow-up if the same consistency is wanted there.

2. Classification is a hardcoded list living far from the commands — CONCRETE BUGS FIXED, STRUCTURAL MOVE DECLINED

  • Whole-subtree hiding (bug): fixed. The root rule now matches the bare command only (canonicalPath.length === 1), so /status paths keeps its invocation. Verified all other roots (auth, diff, editor, help, settings, theme) have no subcommands, so this changes behavior only for real subcommands. Tests added for /status paths and /stats model.
  • Latent sentToModel bug: fixed. invocationSentToModel = true is now set unconditionally in the submit_prompt path; only the UI updateItem call is guarded by invocationItemId !== undefined (mirrors the stacked-skills path). A regression test drives a hidden built-in command that returns submit_prompt and asserts the record is classified sentToModel: true.
  • Drift / move metadata onto command definitions: DECLINED. Reason: the bare-vs-argument rule (/effort hidden, /effort high visible) is argument-sensitive, so a per-command boolean cannot express it and the rule would still live partly in the processor — splitting one rule across ~10 command files and the public SlashCommand type without removing the argument-dependent core. Non-built-in commands (user/project/extension/MCP) are already uniformly excluded via the kind !== BUILT_IN check, so extension opt-in would additionally require a policy change this PR should not make. Drift risk is covered by the per-command behavior tests and the design doc, which now states the rule. Happy to do the metadata move as a follow-up if maintainers prefer it.

3. Two opposite strategies for the same UX problem — RESOLVED per the reviewer's suggested landing

Bare /model is now in the bare-hide set, exactly as suggested: /model opens the picker with no invocation row, /model <id> keeps its invocation. The "Kept model as …" info message on dismissal is kept — the design doc now states the rationale: the active model is session-critical and otherwise invisible in history, so the model picker states the outcome explicitly while the other pickers leave no trace. Combined with #1, /model no longer restores as > /model followed by nothing. Design doc updated.

4. Missing translations — FIXED

'Kept model as {{model}}' added to all nine locales (en, zh, zh-TW, ja, de, fr, pt, ru, ca), appended following the files' recent-key convention.

5. closeWithoutSelection isn't idempotent — FIXED

Added a useRef latch so the close feedback and onClose fire only once even if two Escape bytes arrive in one stdin chunk. The primary-picker test now presses Escape twice and asserts a single history entry and a single onClose. Remaining known edge: an Escape racing an in-flight Enter selection is not guarded; the normal selection path is covered by the new test in #7c.

6. Runtime-snapshot model naming — FIXED

Confirmed the mismatch: the picker highlights activeRuntimeSnapshot.id, but the cancel message printed preferredModelId (config.getModel()). The message now names activeRuntimeSnapshot.modelId when a runtime snapshot is active. Test added: with a runtime snapshot and a different configured model, dismissal reports the runtime model.

7. Test coverage gaps — FIXED

  • FILE-override precedence: the override test now registers both a built-in status and a FILE status, and asserts the FILE command's action runs, the built-in's does not, and the invocation is kept (loader order: built-in first, file last, later wins in CommandService).
  • Subtree coverage: /status paths (kept) and /stats model (kept) tests added (see Where is the config saved? #2).
  • Selection skips cancel message: new test drives a real selection through onSelect and asserts no "Kept model as" entry is added.

Inline findings (automated reviewer)

  • rc:3698230302 (it.each over all five auxiliary flags) — RESOLVED in e7183bbd8, re-verified: the suite covers fast/voice/vision/compaction/image with both Escape and Left.
  • rc:3698230310 (extract isAuxiliaryModelMode) — RESOLVED in e7183bbd8, re-verified: the flag is computed once and reused for runtime-snapshot suppression, Escape/Left handling, and close-feedback suppression.
  • rc:3698426463 (left-key coverage) — RESOLVED in facb38f71, re-verified: primary-picker left-key no-op test plus auxiliary Left cases exist.

Verification

Commands actually run this round (after the final code state):

  • npm run build — passed
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0, no errors/warnings)
  • npx prettier --check on all changed files — passed
  • cd packages/cli && npx vitest run src/ui/hooks/slashCommandProcessor.test.ts — 111 passed
  • cd packages/cli && npx vitest run src/ui/hooks/slashCommandProcessor.test.ts src/ui/utils/resumeHistoryUtils.test.ts src/ui/components/ModelDialog.test.tsx — 201 passed (3 files)
  • cd packages/cli && npx vitest run src/ui/components/ModelDialog.test.tsx — 48 passed
  • cd packages/core && npx vitest run src/services/chatRecordingService.test.ts — 66 passed
  • cd packages/cli && npx vitest run src/ui/commands/arenaCommand.agentComplete.test.ts src/ui/hooks/useAwaySummary.test.ts src/ui/utils/restoreGoal.test.ts — 60 passed
  • cd packages/cli && npx vitest run src/ui/hooks/useGeminiStream.test.tsx src/acp-integration/session/Session.test.ts — 667 passed
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.worktree.test.ts src/acp-integration/session/Session.review-lease.test.ts — 12 passed
  • Integration tests — not run: the changed behavior (TUI history + recording payload) is exercised by the unit tests above, not only through the bundled CLI or integration harness.
  • npm run generate:settings-schema — not needed: no settings source changed.
中文说明

处理审查总结 — PR #8365

本轮修改位于提交 d7a14bf46fix(cli): persist slash command invocation hiding across resume)。无基线冲突(--conflict false),未合并 origin/main

议题级审查(@wenshao

1. 恢复/预览会话时,被隐藏的调用行又回来了 — 已修复

按建议,隐藏判定现在写入聊天记录,而不是只在 UI 层丢弃:

  • SlashCommandRecordPayload 新增 hiddenInvocation?: boolean(packages/core)。
  • processor 用与隐藏 TUI 行相同的谓词记录 hiddenInvocation
  • buildResumedHistoryItems(供 /resume/branch/historySessionPreview 使用)在该标记为真时跳过重建的 user 条目。rawCommand 仍保留在记录中供遥测使用。
  • /model 恢复时也不再出现空调用行,因为裸 /model 现在也被隐藏(见 如何自定义密钥文件 .env可能与其他文件冲突 #3)。
  • 测试:隐藏(hiddenInvocation: true)与可见(false)两个方向的记录断言,以及证明恢复时跳过隐藏行的重建测试。

说明:packages/desktop/.../qwen-agent.ts 中的 desktop 转录重建也会把 slash 调用渲染为用户消息。它是独立子系统,本次有意不动;如果那里也需要同样的一致性,作为后续跟进。

2. 分类是远离命令定义的硬编码列表 — 具体 bug 已修复,结构性迁移不做

  • 整棵子树被隐藏(bug):已修复。根规则现在只匹配裸命令(canonicalPath.length === 1),因此 /status paths 保留调用行。已核实其他根(authdiffeditorhelpsettingstheme)均无子命令,所以该改动只影响真正的子命令。新增 /status paths/stats model 测试。
  • 潜在 sentToModel bug:已修复。submit_prompt 路径现在无条件设置 invocationSentToModel = true,只有 UI 的 updateItem 调用受 invocationItemId !== undefined 保护(与 stacked-skills 路径一致)。新增回归测试:驱动一个返回 submit_prompt 的被隐藏内置命令,断言记录被分类为 sentToModel: true
  • 漂移/把元数据移到命令定义上:不做。原因:裸命令与带参数的区分规则(/effort 隐藏、/effort high 可见)依赖参数,单个布尔元数据无法表达,规则仍会一部分留在 processor 中——相当于把一条规则拆散到约 10 个命令文件和公共 SlashCommand 类型里,却没有消除参数依赖的核心。非内置命令(用户/项目/扩展/MCP)已经通过 kind !== BUILT_IN 检查统一排除,扩展可选加入还需要一个本 PR 不应做出的策略变更。漂移风险已由逐命令的行为测试和设计文档(现已写明规则)覆盖。如果维护者更倾向元数据方案,可以作为后续跟进实现。

3. 同一 UX 问题采用了两种相反策略 — 按审查者建议的方向解决

/model 现在加入裸隐藏集合,与建议一致:/model 打开选择器时不留调用行,/model <id> 保留调用行。关闭时的 "Kept model as …" 信息保留——设计文档现已写明理由:当前生效模型对会话至关重要、且在历史中本来不可见,所以模型选择器在关闭时明确说明结果,而其他选择器不留痕迹。结合 #1/model 恢复时不再出现 > /model 后跟空白的情况。设计文档已更新。

4. 缺少翻译 — 已修复

'Kept model as {{model}}' 已添加到全部九个语言文件(enzhzh-TWjadefrptruca),按这些文件追加新键的惯例添加。

5. closeWithoutSelection 不是幂等的 — 已修复

新增 useRef 闩锁,即使同一个 stdin 块中到达两个 Escape 字节,关闭反馈和 onClose 也只触发一次。主选择器测试现在按两次 Escape,断言只有一条历史记录和一次 onClose。遗留已知边界:Escape 与进行中的 Enter 选择之间的竞态未加防护;正常选择路径由 #7c 的新测试覆盖。

6. 运行时快照模型命名 — 已修复

确认了不一致:选择器高亮的是 activeRuntimeSnapshot.id,但取消信息打印的是 preferredModelIdconfig.getModel())。现在当运行时快照激活时,信息显示 activeRuntimeSnapshot.modelId。新增测试:存在运行时快照且配置模型不同时,关闭报告运行时模型。

7. 测试覆盖缺口 — 已修复

  • FILE 覆盖优先级:覆盖测试现在同时注册内置 status 和 FILE status,断言 FILE 命令的 action 被执行、内置的不被执行、调用行被保留(加载器顺序:内置在前、文件在后,CommandService 中后者优先)。
  • 子树覆盖:新增 /status paths(保留)和 /stats model(保留)测试(见 Where is the config saved? #2)。
  • 已选择时跳过取消信息:新测试通过 onSelect 驱动一次真实选择,断言没有添加 "Kept model as" 条目。

行内发现(自动审查器)

  • rc:3698230302(对全部五个辅助标志使用 it.each)— 已在 e7183bbd8 中解决,复核确认:测试套件覆盖 fast/voice/vision/compaction/image 五种模式,且 Escape 和 Left 均覆盖。
  • rc:3698230310(提取 isAuxiliaryModelMode)— 已在 e7183bbd8 中解决,复核确认:该标志只计算一次,并复用于运行时快照抑制、Escape/Left 处理和关闭反馈抑制。
  • rc:3698426463(left 键覆盖)— 已在 facb38f71 中解决,复核确认:存在主选择器 left 键无操作测试和辅助选择器 Left 用例。

验证

本轮实际运行的命令(针对最终代码状态):

  • npm run build — 通过
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0,无错误/警告)
  • 对全部改动文件运行 npx prettier --check — 通过
  • cd packages/cli && npx vitest run src/ui/hooks/slashCommandProcessor.test.ts — 111 通过
  • cd packages/cli && npx vitest run src/ui/hooks/slashCommandProcessor.test.ts src/ui/utils/resumeHistoryUtils.test.ts src/ui/components/ModelDialog.test.tsx — 201 通过(3 个文件)
  • cd packages/cli && npx vitest run src/ui/components/ModelDialog.test.tsx — 48 通过
  • cd packages/core && npx vitest run src/services/chatRecordingService.test.ts — 66 通过
  • cd packages/cli && npx vitest run src/ui/commands/arenaCommand.agentComplete.test.ts src/ui/hooks/useAwaySummary.test.ts src/ui/utils/restoreGoal.test.ts — 60 通过
  • cd packages/cli && npx vitest run src/ui/hooks/useGeminiStream.test.tsx src/acp-integration/session/Session.test.ts — 667 通过
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.worktree.test.ts src/acp-integration/session/Session.review-lease.test.ts — 12 通过
  • 集成测试 — 未运行:本次改动的行为(TUI 历史 + 记录载荷)由上述单元测试覆盖,并非只能通过打包 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

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification: real local stack, base vs head A/B

I built both sides of this PR from source and drove the real TUI in a PTY (tmux, 110×40) against a recording OpenAI-compatible mock provider, then compared visible history, the on-disk chat transcript, and the resumed session.

Arms — base = merge-base 6195895d27, head = 92d1dc3465. Each arm: isolated worktree, npm ci + npm run build + npm run bundle, run as node dist/cli.js with an isolated QWEN_HOME (3 picker models pointed at 127.0.0.1:8365). macOS (darwin arm64, Darwin 25.6.0), Node 24.18.1.

Verdict

Every behaviour claimed in the PR description reproduced on the real stack. No regression found. Two observations below are worth a look before merge, neither is a blocker.


1. Transient panels no longer leave bare rows

12 commands run and closed with Escape (/help /theme /editor /diff /effort /stats /statusline /settings /status /connect /? /usage) — counting invocation rows left in visible history:

arm invocation rows left
base 6195895d27 one per command (7 still on screen, 5 scrolled)
head 92d1dc3465 0

Aliases resolve to their canonical command: /connect, /?, /usage are suppressed exactly like /auth, /help, /stats.

BEFORE (base) AFTER (head)
before after

2. /model always has an explicit outcome

step base head
bare /model + Escape bare > /model, no result ● Kept model as mock-model-a
/model → select 2nd entry > /model + detail block detail block (no invocation row)
/model mock-model-c > /model mock-model-c + ● Model: mock-model-c identical
/model --project --global (rejected) invocation + error identical — invocation is revealed

3. Commands that must keep their invocation do keep it

/effort high, /stats model, /mcp, and a custom /status (project .qwen/commands/status.md overriding the built-in) render byte-identical panes on both arms. The transcript shows head deciding explicitly: hiddenInvocation: false for all four, and sentToModel: true for the custom command that submits a prompt — the built-in suppression does not leak to user/project commands.

kept

4. Resume reconstruction — the strongest result

Session: model turn → /model+Esc → /help+Esc → /editor+select → /effort high/quit, then qwen -c.

BEFORE (base) AFTER (head)
resume-before resume-after

Base resumes to four bare rows and loses every outcome (Editor preference cleared, Reasoning effort: high are gone — those messages were never recorded). Head reproduces the live history exactly. The addMessageaddItemWithRecording switch is what fixes this; it is a real improvement beyond the stated scope. Same effect for the rejected /model --project --global and for NO_COLOR /theme:

BEFORE — error lost on resume AFTER — error survives
reveal-before reveal-after

5. Doubled keystrokes in one PTY write

\e\e on the model picker and \r\r on the effort picker, each delivered as a single write to the pty:

rows added phase:'result' records
/model + doubled Escape 1 1
/effort + doubled Enter 1 1

No duplication observed, so the reviewer-flagged un-latched handlers (/effort, /editor, /statusline) did not misfire through a real terminal. Caveat: I cannot prove the runtime dispatched two key events from one chunk, so this corroborates the latch rather than closing those comments.

6. Test suites — closing two CI gaps

  • macOS leg (skipped for fork PRs): all 9 changed CLI suites pass — 285/285.
  • Desktop bun suite (qwen-agent-slash-history.test.ts), which the review bot reported runs in no gating CI and could not execute: 45/45 pass locally on bun 1.3.14; full desktop shared suite 424/424.

Mutation probes — does the suite actually catch a regression in each guard?

CLI (6/6 caught — every guard is load-bearing):

mutation result
hiddenInvocation not recorded 17 tests fail ✅
resume ignores hiddenInvocation 1 fail ✅
reveal-on-rejection removed 1 fail ✅
NO_COLOR /theme exception removed 1 fail ✅
model-picker close latch removed 4 fail ✅
every /model form treated as picker-only 1 fail ✅

Desktop (qwen-agent.ts, 45-test suite) — 3 of 5 mutations survive:

mutation result
hiddenInvocation ignored in the orphan walk (core behaviour) 1 fail ✅
consumed-invocation dedup removed 1 fail ✅
ancestor-walk command-name match guard removed 45 pass — not load-bearing
ancestor-walk user-turn stop removed 45 pass — not load-bearing
hiddenInvocation !== true removed from isPatchableQwenUserRecord 45 pass (and 424/424 desktop-wide) — untested

The first two confirm existing review comments R11-4 / R10-5. The third is new: that predicate change at qwen-agent.ts:3481 has no coverage anywhere in the repo — deleting the flag check breaks no test.


Two things to look at before merge

(a) Auxiliary pickers now leave nothing at all. /model --fast is picker-only, so its invocation is hidden, but auxiliary cancellation deliberately adds no feedback. Cancelling it leaves zero trace — which is the same "nothing happened?" ambiguity this PR fixes for the primary picker. Base at least left > /model --fast. docs/design/slash-command-feedback.md states this is intentional; flagging it because the PR's own rationale (a session-critical setting invisible in history) applies to --fast/--vision/--compaction too.

aux

(b) One extra slash_command record per dialog-completed command. Dialog-side phase:'result' records are additive, so the same 4-command session writes 8 records on base and 10 on head (4 invocation + 6 result). packages/cli/src/services/insight/generators/DataProcessor.ts counts every slash_command record — both phases — toward totalMessages, the heatmap and active-hours, so /insight numbers drift a little further from "user interactions". Pre-existing double counting, made 1.5× worse for /model and /editor; the same point as the ModelDialog.tsx:696 review comment, now with counts.

Not verified

  • Windows and Linux TUI behaviour (macOS only here; CI covers Linux).
  • The desktop Electron app itself — the desktop change was covered by its bun suite plus the mutation probe above, not by a live Electron run.
  • Headless qwen -p is unaffected by design (it goes through nonInteractiveCliCommands.ts, untouched).
中文版本

维护者验证:本地真实环境,base 与 head A/B 对比

我从源码分别构建了本 PR 的两侧,并在 真实 PTY 中驱动真实 TUI(tmux,110×40),provider 使用可记录请求的 OpenAI 兼容 mock,然后对比可见历史、磁盘上的会话 transcript 以及 resume 后的会话。

两侧 —— base = merge-base 6195895d27,head = 92d1dc3465。每侧:独立 worktree,npm ci + npm run build + npm run bundle,以 node dist/cli.js 启动,使用隔离的 QWEN_HOME(3 个 picker 模型指向 127.0.0.1:8365)。macOS(darwin arm64,Darwin 25.6.0),Node 24.18.1。

结论

PR 描述中声称的所有行为都在真实环境中复现。未发现回归。 下面两点建议在合并前看一下,都不是阻塞项。

1. 临时面板不再留下裸命令行

运行 12 个命令并用 Escape 关闭(/help /theme /editor /diff /effort /stats /statusline /settings /status /connect /? /usage),统计可见历史中残留的 invocation 行:

分支 残留 invocation 行
base 6195895d27 每个命令一行(屏幕上可见 7 行,5 行已滚出)
head 92d1dc3465 0

别名会解析为 canonical command:/connect/?/usage/auth/help/stats 行为完全一致。

2. /model 始终有明确结果

步骤 base head
无参 /model + Escape > /model,无结果 ● Kept model as mock-model-a
/model → 选中第 2 项 > /model + 详情块 详情块(无 invocation 行)
/model mock-model-c > /model mock-model-c + ● Model: mock-model-c 完全一致
/model --project --global(被拒) invocation + 错误 完全一致 —— invocation 被重新显示

3. 需要保留 invocation 的命令确实保留了

/effort high/stats model/mcp,以及自定义 /status(项目内 .qwen/commands/status.md 覆盖内置命令)在两侧渲染出完全相同的界面。transcript 显示 head 是显式判定的:四者均为 hiddenInvocation: false,提交 prompt 的自定义命令为 sentToModel: true —— 内置隐藏规则不会泄漏到 user/project 命令。

4. Resume 重建 —— 最有价值的结果

会话流程:模型对话 → /model+Esc → /help+Esc → /editor+选择 → /effort high/quit,然后 qwen -c

base resume 后是四行裸命令,并且丢掉了全部结果Editor preference clearedReasoning effort: high 都不见了 —— 这些消息从未被记录)。head 精确复现了实时历史。真正修好这一点的是 addMessageaddItemWithRecording 的改动,这是超出声称范围的实质改进。被拒的 /model --project --globalNO_COLOR 下的 /theme 同理。

5. 单次 PTY 写入中的重复按键

在模型选择器上写入 \e\e、在 effort 选择器上写入 \r\r,均为单次写入 pty:

新增行数 phase:'result' 记录数
/model + 双 Escape 1 1
/effort + 双 Enter 1 1

未观察到重复,因此 reviewer 指出的未加锁 handler(/effort/editor/statusline)在真实终端下没有误触发。注意:我无法证明运行时确实从同一 chunk 派发了两次按键事件,所以这只是佐证,不能视为关闭那些评论。

6. 测试套件 —— 补上两个 CI 缺口

  • macOS 腿(fork PR 会跳过):9 个受影响的 CLI 套件全部通过 —— 285/285
  • desktop bun 套件qwen-agent-slash-history.test.ts),review bot 报告它不被任何 gating CI 执行且当时无法运行:本地 bun 1.3.14 下 45/45 通过;desktop shared 全量 424/424

变异测试 —— 每个新增保护是否真的被测试覆盖?

CLI(6/6 被捕获,全部 load-bearing):不记录 hiddenInvocation(17 失败)、resume 忽略该标记(1 失败)、去掉拒绝时的 reveal(1 失败)、去掉 NO_COLOR /theme 例外(1 失败)、去掉模型选择器关闭锁(4 失败)、把所有 /model 形式都当作 picker-only(1 失败)。

Desktop(qwen-agent.ts,45 个测试):5 个变异中 3 个存活 ——

变异 结果
orphan walk 忽略 hiddenInvocation(核心行为) 1 失败 ✅
去掉 consumed-invocation 去重 1 失败 ✅
去掉 ancestor walk 的命令名匹配保护 45 通过 —— 非 load-bearing
去掉 ancestor walk 的 user-turn 终止 45 通过 —— 非 load-bearing
isPatchableQwenUserRecord 去掉 hiddenInvocation !== true 45 通过(desktop 全量 424/424)—— 无覆盖

前两项印证了已有评论 R11-4 / R10-5。第三项是新发现:qwen-agent.ts:3481 处的这个改动在整个仓库中没有任何测试覆盖,删掉标记检查不会让任何测试失败。

合并前建议看两点

(a) 辅助选择器现在完全没有任何反馈。 /model --fast 属于 picker-only,因此 invocation 被隐藏,但辅助选择器的取消刻意不给反馈。取消后没有任何痕迹 —— 这正是本 PR 为主选择器修复的那种"到底发生了什么"的歧义。base 至少留下了 > /model --fastdocs/design/slash-command-feedback.md 说明这是有意设计;之所以提出,是因为 PR 自己的理由(会话关键设置在历史中不可见)对 --fast/--vision/--compaction 同样成立。

(b) 每个由 dialog 完成的命令多出一条 slash_command 记录。 dialog 侧的 phase:'result' 记录是叠加的,同样 4 个命令的会话在 base 写 8 条,在 head 写 10 条(4 invocation + 6 result)。packages/cli/src/services/insight/generators/DataProcessor.ts 会把所有 slash_command 记录(两个 phase 都算)计入 totalMessages、热力图与活跃时段,因此 /insight 的数字会进一步偏离"用户交互次数"。这是既有的重复计数问题,对 /model/editor 变成 1.5 倍;与 ModelDialog.tsx:696 的评论同一件事,这里补上了具体数字。

未验证

  • Windows 与 Linux 的 TUI 行为(本次仅 macOS;Linux 由 CI 覆盖)。
  • desktop Electron 应用本身 —— desktop 侧改动通过其 bun 套件加上上述变异测试覆盖,未做真实 Electron 运行。
  • headless qwen -p 按设计不受影响(走未改动的 nonInteractiveCliCommands.ts)。

@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. Suggestions are inline.

Not explored to full depth (tool budget reached): This PR hides transient slash-command invocations from TU...: 无。; This PR hides transient slash-command invocations from TU...: None. All - lines were examined.; This PR hides transient slash-command invocations from TU...: None. All 19 pairings examined..

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):This PR hides transient slash-command invocations from TU...:无。;This PR hides transient slash-command invocations from TU...:None. All - lines were examined.;This PR hides transient slash-command invocations from TU...:None. All 19 pairings examined.

— deepseek-v4-flash via Qwen Code /review (v0.21.10)

Comment thread packages/cli/src/ui/commands/modelCommand.ts

@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: build-and-test — packages/desktop bun suite (qwen-agent-slash-history.test.ts) is negated from the npm workspace graph and executed by no gating CI; not runnable on this review host (no bun) — the desktop reconstruction changes were verified statically and by direct probes of the real loadSlashCommandInvocationMessages only..

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) CI legs are skipped for fork PRs; the unit suites ran on Linux only. Integration Tests (CLI, No Sandbox) is merge_group-gated by design and collects no file this diff changes..

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5..

Not explored to full depth (tool budget reached): PR #8365 (round 12): hides transient slash-command invoca...: none — all checks above completed within budget.; PR #8365 (round 12): hides transient slash-command invoca...: none — all checks above completed within budget; I did not run the vitest file since the finding is an assertion-absence verified textually at the reviewed comm…; PR #8365 (round 12): hides transient slash-command invoca...: none — all checks above completed within budget.; chunk 9: could not execute the three touched test files — the review worktree has no node_modules ( npx vitest fails with ERR_MODULE_NOT_FOUND) and installing depende…; PR #8365 (round 12): hides transient slash-command invoca...: none — all checks I intended completed within budget. Consumer-side logic in slashCommandProcessor.ts (invocation-phase recording, revealHiddenInvocation ) a…, and 4 more.

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — packages/desktop bun suite (qwen-agent-slash-history.test.ts) is negated from the npm workspace graph and executed by no gating CI; not runnable on this review host (no bun) — the desktop reconstruction changes were verified statically and by direct probes of the real loadSlashCommandInvocationMessages only.。

未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) CI legs are skipped for fork PRs; the unit suites ran on Linux only. Integration Tests (CLI, No Sandbox) is merge_group-gated by design and collects no file this diff changes.。

未审查:reverse audit — did not converge within the reverse-audit round cap of 5.。

未探索到全部深度(达到工具调用预算):PR #8365 (round 12): hides transient slash-command invoca...:none — all checks above completed within budget.;PR #8365 (round 12): hides transient slash-command invoca...:none — all checks above completed within budget; I did not run the vitest file since the finding is an assertion-absence verified textually at the reviewed comm…;PR #8365 (round 12): hides transient slash-command invoca...:none — all checks above completed within budget.;chunk 9:could not execute the three touched test files — the review worktree has no node_modules ( npx vitest fails with ERR_MODULE_NOT_FOUND) and installing depende…;PR #8365 (round 12): hides transient slash-command invoca...:none — all checks I intended completed within budget. Consumer-side logic in slashCommandProcessor.ts (invocation-phase recording, revealHiddenInvocation ) a…,另有 4 条。

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

Comment on lines +1047 to +1049
const errorMessage = e instanceof Error ? e.message : String(e);
setErrorMessage(
`${t('Model switched, but the selection could not be saved.')}\n\n${errorMessage}`,

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] After a post-switch persistence failure the dialog stays open but is selection-dead: selectionCommittedRef is already true, so handleSelect's guard silently discards every further selection (including a retry of the same row), and closeWithoutSelection skips the feedback entirely — a hidden /model exchange ends with zero live history items and zero recorded phase:'result'. — Failure scenario: settings file unwritable or disk full (EROFS/EACCES/ENOSPC) → switchModel succeeds and selectionCommittedRef.current = truepersistModelSelectionsettings.setValuesaveSettings re-throws → this catch shows the error and returns with the dialog still mounted → every later Enter/number-key selection is discarded by the guard (which runs before setErrorMessage(null)), and Escape exits with no history item and no /model result record, so the whole exchange vanishes from live and resumed history. Verified against the code at this commit; filed as a Suggestion rather than Critical because the pre-change behavior was worse (an unhandled promise rejection), Escape always exits, and the displayed error is accurate.

Two fix directions:

// Option A — treat the post-switch failure as terminal (the switch is
// already applied in memory; retrying would just re-fail the same disk):
      } catch (e) {
        const errorMessage = e instanceof Error ? e.message : String(e);
        setErrorMessage(/* ...unchanged... */);
        onClose();
        return;
      }

// Option B — keep the dialog usable: introduce a separate persistFailedRef,
// set it in this catch, and have handleSelect's guard consult
// `selectionCommittedRef.current && !persistFailedRef.current` instead.
中文说明

[Suggestion] 切换成功但持久化失败后,对话框仍然打开但已无法再进行任何选择:此时 selectionCommittedRef 已为 truehandleSelect 的守卫会静默丢弃之后所有的选择(包括重试同一行),且 closeWithoutSelection 会完全跳过反馈——一次被隐藏的 /model 交互最终在实时历史和持久化记录中都零痕迹。— 失败场景:设置文件不可写或磁盘满(EROFS/EACCES/ENOSPC)→ switchModel 成功且 selectionCommittedRef.current = truepersistModelSelectionsettings.setValuesaveSettings 重新抛出异常 → 此 catch 显示错误并在对话框仍然挂载时返回 → 之后每一次回车/数字键选择都被守卫丢弃(该守卫在 setErrorMessage(null) 之前执行),按 Escape 退出时既不产生历史条目也不产生 /model result 记录,整段交互在实时与 resume 历史中同时消失。已在本提交的代码上核实;定为 Suggestion 而非 Critical,因为改动前的行为更糟(未处理的 promise rejection)、Escape 始终可以退出、且错误文案准确描述了状态。

修复方向见英文部分代码块(A:失败即关闭对话框;B:新增 persistFailedRef,让 handleSelect 守卫在持久化失败后允许重新选择)。

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

Comment on lines +1049 to +1050
`${t('Model switched, but the selection could not be saved.')}\n\n${errorMessage}`,
);

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 user-facing string is wrapped in t() but exists in none of the 9 locale files — not even en.js — while the sibling string added by the same diff (Kept model as {{model}}) was translated in all 9. — Failure scenario: this catch block is the key's only use site (verified by grep: here and its test), and the repo's t() falls back to the raw key, so every non-English user hitting this persistence-failure path sees an untranslated English message. The gap is also invisible to the translation workflow: scripts/check-i18n.ts run at this commit prints "All checks passed!" because its parity checks anchor on en.js keys and cannot detect a used-but-unregistered key.

// en.js (identity):
'Model switched, but the selection could not be saved.':
  'Model switched, but the selection could not be saved.',
// + real translations in the other 8 locale files (ca, de, fr, ja, pt, ru,
// zh, zh-TW), mirroring how 'Kept model as {{model}}' was added
中文说明

[Suggestion] 这条新增的用户可见文案包在 t() 中,但 9 个语言文件里一个都没有(连 en.js 也没有)——而同一 diff 新增的姊妹文案 Kept model as {{model}} 在全部 9 个语言文件中都做了翻译。— 失败场景:该 catch 块是这个 key 唯一的使用点(已用 grep 核实:此处及其测试),仓库的 t() 找不到 key 时回退为 key 原文,因此每个非英语用户在遇到该持久化失败路径时看到的都是未翻译的英文。这个缺口对翻译工作流也不可见:在本提交上运行 scripts/check-i18n.ts 输出 "All checks passed!",因为其一致性检查以 en.js 的 key 为锚点,无法检测「代码中使用但未注册」的 key。

修复见英文部分代码块:在 en.js 注册恒等映射,并在其余 8 个语言文件中按 Kept model as {{model}} 的方式补齐翻译。

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

Comment on lines +1491 to +1496
} = useEditorSettings(
settings,
setEditorError,
historyManager.addItem,
config,
);

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 new config wiring from AppContainer into useEditorSettings (and identically into useThemeCommand at ~1426-1431) has no test: AppContainer.test.tsx mocks both hooks wholesale (vi.mock('./hooks/useThemeCommand.js'), vi.mock('./hooks/useEditorSettings.js')), and the parameter is optional (config?: Config), so nothing type-checks the wiring either. — Failure scenario: a future change that drops the config argument from either call site compiles and leaves every test green (the hook-level tests inject their own config), while /editor selection feedback and /theme-under-NO_COLOR feedback silently stop calling recordSlashCommand — resumed/branched sessions and desktop reconstruction then lose those outcome lines with no error anywhere.

// AppContainer.test.tsx — assert the hooks receive the config instance:
expect(mockUseEditorSettings).toHaveBeenCalledWith(
  mockLoadedSettings,
  mockSetEditorError,
  mockAddItem,
  config,
);
// (and the same for mockUseThemeCommand's trailing arg)
中文说明

[Suggestion] AppContainer 向 useEditorSettings 传入 config 的新接线(以及 ~1426-1431 处 useThemeCommand 的相同接线)没有任何测试:AppContainer.test.tsx 对这两个 hook 做了整体 mock(vi.mock('./hooks/useThemeCommand.js')vi.mock('./hooks/useEditorSettings.js')),且该参数是可选的(config?: Config),因此类型检查也无法兜住这个接线。— 失败场景:未来某个改动把任一调用点的 config 参数删掉后,编译通过、所有测试仍为绿色(hook 层测试自带 config 注入),而 /editor 选择反馈与 NO_COLOR 下的 /theme 反馈会静默停止调用 recordSlashCommand——resume/branch 会话与桌面端重建将丢失这些结果行,且任何地方都不会报错。

修复见英文部分代码块:在 AppContainer.test.tsx 中断言两个 hook mock 的末尾参数是 config 实例。

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

Comment on lines +783 to +785
phase: 'invocation',
rawCommand: '/model',
hiddenInvocation: 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.

[Suggestion] Re-marking this fixture's /model invocation hiddenInvocation: true removed the file's only pin that a VISIBLE (non-hidden) invocation whose results produce no output emits no user row — the suppression here is now attributable solely to the hidden flag. (Pre-PR, this fixture carried a non-hidden /model invocation with an empty-only result and expected no user row.) — Failure scenario: probe-verified against the real loadSlashCommandInvocationMessages (the bun suite cannot run on the review host): a regression emitting user rows for non-hidden invocations unconditionally (dropping the pairing / outputTexts.length === 0 requirement) produces phantom user rows in desktop-reconstructed history — these messages feed model context, not just display — while every existing test in this file stays green (the mutated code reproduced both existing expectations byte-for-byte). The shape is still produced by real transcripts: any non-hidden dialog command dismissed without feedback (e.g. /memory, /arena), and every pre-PR transcript of bare /model or /theme.

// Keep a non-hidden invocation whose only result has empty output, e.g.:
{
  uuid: 'memory-invocation',
  parentUuid: '<previous record>',
  timestamp: '2026-03-25T07:40:00.000Z',
  type: 'system',
  subtype: 'slash_command',
  systemPayload: { phase: 'invocation', rawCommand: '/memory' },
},
{
  uuid: 'memory-open-result',
  parentUuid: 'memory-invocation',
  timestamp: '2026-03-25T07:40:01.000Z',
  type: 'system',
  subtype: 'slash_command',
  systemPayload: {
    phase: 'result',
    rawCommand: '/memory',
    outputHistoryItems: [],
  },
},
// and assert no messages are emitted for it
中文说明

[Suggestion] 把该 fixture 的 /model invocation 重新标记为 hiddenInvocation: true 后,本文件失去了唯一固定「可见(非隐藏)invocation 在 result 无输出时不产生用户行」的用例——此处的用户行抑制现在只能归因于 hidden 标志。(改动前,该 fixture 是一个非隐藏的 /model invocation 加空 result,并断言没有用户行。)— 失败场景:已在真实的 loadSlashCommandInvocationMessages 上用探针核实(review 主机无 bun,无法运行该套件):若回归为「对非隐藏 invocation 无条件发出用户行」(丢弃配对 / outputTexts.length === 0 条件),桌面端重建历史会出现幽灵用户行——这些消息会进入模型上下文而不仅仅是展示——而本文件现有测试全部仍然通过(变异后的代码逐字节复现了现有两处断言)。该形状在真实记录中依然存在:任何未产生反馈就关闭的非隐藏对话框命令(如 /memory/arena),以及所有 PR 之前裸 /model/theme 的记录。

修复见英文部分代码块:保留一个非隐藏、且唯一 result 输出为空的 invocation fixture(如 /memory),并断言不产生任何消息。

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

@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 changes (PR #8365)

Round context. Critical-only mode is active (after 5 change-producing rounds). The only new feedback this round is the maintainer verification report from @wenshao (issue-level): an A/B comparison of base 6195895d vs head 92d1dc34, driving the real TUI in a PTY against a recording mock provider. Its verdict: "Every behaviour claimed in the PR description reproduced on the real stack. No regression found", with two explicitly non-blocking observations and one mutation-probe coverage note. There are no inline comments, no failed checks, and no merge conflict this round. Each point is triaged below; nothing qualifies as a Critical defect, so no code changes were made.

1. Verification report (sections 1–6) — informational, no action

The report reproduces the suppression behaviour, the explicit /model outcomes, resume reconstruction, the double-keystroke latch behaviour, and both test suites (including the macOS and desktop bun legs that CI does not gate for this PR). It confirms the shipped behaviour and needs no code response.

2. Observation (a): auxiliary pickers leave zero trace when cancelled — escalated for a maintainer decision

Current state: docs/design/slash-command-feedback.md documents this as intentional — "The other pickers leave no trace when dismissed; the model picker states the outcome explicitly because the active model is session-critical and otherwise invisible in history." The observation challenges that design judgment: the PR's own rationale arguably extends to --fast/--vision/--compaction settings. Changing documented, intentional behaviour is a product/scope tradeoff, not a call to make unilaterally in round 6 on a non-blocking observation.

Options:

  1. Keep the documented behaviour (status quo).
  2. Mirror the primary picker: on auxiliary dismissal add an info item such as "Kept fast model as X" and record it — a small change contained to closeWithoutSelection in ModelDialog.tsx, plus updating the design-doc bullet and the dialog tests.
  3. Reveal the hidden invocation row on auxiliary dismissal instead of adding feedback.

Recommendation: land this PR as documented, and implement option 2 as a small follow-up PR if symmetric dismissal feedback is wanted, so this PR does not grow after six review rounds.

Question for the maintainer: should auxiliary-picker dismissal feedback ship in this PR, in a follow-up, or not at all?

3. Observation (b): extra slash_command records per dialog-completed command skew /insight — escalated for a maintainer decision

Verified mechanics:

  • The TUI processor already writes two records per recorded command (phase:'invocation' + phase:'result'), and packages/cli/src/services/insight/generators/DataProcessor.ts counts every slash_command record toward totalMessages, the heatmap, and active hours — the pre-existing double counting the report mentions.
  • This PR's dialog-side result records add a third record for dialog-completed commands (8 → 10 records in the report's 4-command session), i.e. the 1.5× worsening for /model and /editor. This is the same point as the existing review comment at ModelDialog.tsx:696, now with counts.
  • Other phase:'result'-only writers predate this PR and have no invocation record at all: ACP slash-command output (Session.ts), the auto away-recap (useAwaySummary.ts), and goal-status records (restoreGoal.ts). Any dedupe semantics therefore cuts across writers with different meanings.

Options:

  1. Count only phase:'invocation' records in DataProcessor — removes the TUI double/triple counting, but stops counting ACP-issued slash commands (result-only records) and the auto recap / goal records (the latter two are arguably not user interactions).
  2. Mark dialog-side result records with a new payload flag and skip them in DataProcessor — reverts only this PR's increment, leaves the pre-existing 2× counting, and adds a schema field consumed by a single reader.
  3. Accept the drift for now and track a holistic fix of /insight counting semantics in a follow-up issue.

Recommendation: option 3 for this PR — the counting was already an approximation before this PR, the maintainer classified this as non-blocking, and the right dedupe semantics depends on the question below. If a fix is still wanted inside this PR, option 1 with updated DataProcessor.test.ts expectations is the cleanest, but only after explicitly accepting that ACP slash commands drop out of /insight counts.

Question for the maintainer: which counting semantics should /insight use (one count per user interaction?), and should ACP-issued slash commands keep being counted?

4. Mutation probe: hiddenInvocation !== true in isPatchableQwenUserRecord has no test coverage — declined this round, follow-up candidate

Evidence: packages/desktop is excluded from the npm workspace ("!packages/desktop" in the root package.json), its suite runs under bun, which is not installed on this runner, and the report itself notes the suite runs in no gating CI. Adding a test that cannot be executed here would violate this workflow's requirement to actually run verification, and per AGENTS.md a missing test for changed behaviour is a Suggestion rather than a Critical. Suggested follow-up: add a case in qwen-agent-slash-history.test.ts where the newest otherwise-patchable record is a phase:'invocation' record with hiddenInvocation: true, asserting the text-element patch lands on the previous visible record instead.

5. Non-load-bearing ancestor-walk guards — no action

The mutation probes show the current suite cannot observe those guards' effects; that proves test-insensitivity, not redundancy. Removing working desktop reconstruction behaviour in Critical-only mode is riskier than keeping it; if the maintainer wants those guards simplified, that should be an explicit follow-up task.

Outcome

No code changes and no commit this round; the branch remains at 92d1dc3465. Since nothing was changed, no build/test commands were run this round — the deterministic gate re-verifies the unchanged head. The two open questions above ride along until a maintainer answers them; the answers will arrive as ordinary feedback in a later round.

中文说明

Autofix 审查轮次 —— 本轮无代码改动(PR #8365

轮次背景。 当前处于仅处理 Critical 的模式(此前已完成 5 个产生改动的轮次)。本轮唯一的新反馈是 @wenshao 的维护者验证报告(issue 级评论):对 base 6195895d 与 head 92d1dc34 的 A/B 对比,在 PTY 中驱动真实 TUI,provider 为可记录请求的 mock。其结论:"PR 描述中声称的所有行为都在真实环境中复现。未发现回归",并附有两个明确非阻塞的观察点和一条变异测试覆盖说明。本轮没有 inline 评论、没有失败的检查、也没有合并冲突。以下逐条分类;没有条目构成 Critical 缺陷,因此未做任何代码改动。

1. 验证报告(第 1–6 节)—— 信息性,无需处理

报告复现了 invocation 隐藏行为、/model 的明确结果、resume 重建、重复按键下的锁行为,以及两套测试套件(包括本 PR 在 CI 中不被 gating 的 macOS 腿和 desktop bun 腿)。报告确认了已交付的行为,不需要任何代码响应。

2. 观察点 (a):辅助选择器取消后不留任何痕迹 —— 升级为维护者决策

现状:docs/design/slash-command-feedback.md 明确记载这是有意设计 —— "其他选择器被关闭时不留痕迹;模型选择器之所以显式说明结果,是因为当前模型对会话至关重要,否则在历史中完全不可见"。该观察点挑战的是这个设计判断:本 PR 自身的理由同样适用于 --fast/--vision/--compaction 设置。修改已被文档化的有意行为属于产品/范围权衡,不应在第 6 轮基于一个非阻塞观察点单方面决定。

选项:

  1. 保持文档化的现状行为。
  2. 与主选择器对齐:辅助选择器被关闭时补充一条 info 消息(如 "Kept fast model as X")并写入记录 —— 改动范围小,仅限 ModelDialog.tsxcloseWithoutSelection,外加更新设计文档条目和 dialog 测试。
  3. 辅助选择器被关闭时不补反馈,而是重新显示被隐藏的 invocation 行。

建议:本 PR 按文档现状合入;如果想要对称的取消反馈,以小型 follow-up PR 实现选项 2,避免本 PR 在 6 轮审查后继续膨胀。

给维护者的问题: 辅助选择器的取消反馈应该在本 PR 中交付、以 follow-up 交付,还是不做?

3. 观察点 (b):dialog 完成的命令多出一条 slash_command 记录,导致 /insight 偏差 —— 升级为维护者决策

已核实的机制:

  • TUI 处理器本来就为每个被记录的命令写 两条 记录(phase:'invocation' + phase:'result'),而 packages/cli/src/services/insight/generators/DataProcessor.ts 把每一条 slash_command 记录都计入 totalMessages、热力图和活跃时段 —— 这就是报告所说的既有重复计数。
  • 本 PR 的 dialog 侧 result 记录为 dialog 完成的命令增加了第三条记录(报告中 4 命令会话为 8 → 10 条),即 /model/editor 的 1.5 倍恶化。这与 ModelDialog.tsx:696 已有评论是同一点,此次补上了具体数字。
  • 其余只写 phase:'result' 的来源早于本 PR,且完全没有 invocation 记录:ACP 斜杠命令输出(Session.ts)、自动离开 recap(useAwaySummary.ts)、goal 状态记录(restoreGoal.ts)。因此任何去重语义都会横跨多个含义不同的写入方。

选项:

  1. DataProcessor 中只统计 phase:'invocation' 记录 —— 消除 TUI 的双重/三重计数,但 ACP 发出的斜杠命令(只有 result 记录)以及自动 recap / goal 记录将不再被计入(后两者本来也算不上用户交互)。
  2. 为 dialog 侧 result 记录增加一个新的 payload 标记,并在 DataProcessor 中跳过 —— 只回退本 PR 引入的增量,既有的 2 倍计数仍在,且为一个读者新增了一个 schema 字段。
  3. 本轮接受偏差,开 follow-up issue 整体修正 /insight 的计数语义。

建议:本 PR 采用选项 3 —— 计数在本 PR 之前就已是近似值,维护者已将此判定为非阻塞,且正确的去重语义取决于下面的问题。如果仍希望在本 PR 内修复,选项 1(同步更新 DataProcessor.test.ts 预期)最干净,但前提是明确接受 ACP 斜杠命令从 /insight 计数中消失。

给维护者的问题: /insight 应该采用哪种计数语义(每次用户交互计一次?),ACP 发出的斜杠命令是否应继续被计入?

4. 变异测试:isPatchableQwenUserRecord 中的 hiddenInvocation !== true 无测试覆盖 —— 本轮不做,可作为 follow-up

证据:packages/desktop 被排除在 npm workspace 之外(根 package.json 中的 "!packages/desktop"),其套件依赖 bun 运行,而本 runner 未安装 bun,且报告本身指出该套件不被任何 gating CI 执行。在此环境下添加无法运行的测试违反本工作流"必须实际运行验证"的要求;按照 AGENTS.md,改动行为缺少测试属于 Suggestion 而非 Critical。建议 follow-up:在 qwen-agent-slash-history.test.ts 中新增用例,使最新的可修补记录为一条 hiddenInvocation: truephase:'invocation' 记录,断言 text-element 修补落在上一条可见记录上。

5. 非 load-bearing 的 ancestor-walk 保护 —— 无需处理

变异测试表明当前套件无法观测到这些保护的效果;这只证明测试不敏感,不证明代码冗余。在仅处理 Critical 的模式下删除仍在工作的 desktop 重建逻辑比保留它风险更高;如果维护者希望简化这些保护,应作为明确的 follow-up 任务。

结果

本轮无代码改动、无提交;分支保持在 92d1dc3465。由于没有任何改动,本轮未运行构建/测试命令 —— 确定性验证门槛会对未变更的 head 重新验证。上述两个开放问题将随轮次保留,直到维护者回答;回答会作为普通的新反馈进入后续轮次。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review-response summary — PR #8365 (round)

Feedback triage

# Finding Classification Disposition
rc:3765843998 [Suggestion] isPickerOnlyModelInvocation lacks a direct unit test in modelCommand.test.ts (only covered indirectly via slashCommandProcessor tests) Optional (automated reviewer suggestion, in this round's actionable section) Implemented

Deferred non-Critical feedback (critical-only mode audit record — review by @qwen-code-ci-bot and inline rc:3765898786, rc:3765898792, rc:3765898797, rc:3765898804) was intentionally left untouched per the round rules: no code changes, no thread resolutions, no replies.

No failed checks and no conflict (--conflict false), so no merge was performed.

Changes

Added a top-level describe('isPickerOnlyModelInvocation', ...) block in packages/cli/src/ui/commands/modelCommand.test.ts with two it.each tables (20 cases) that pin the regex boundaries independently of the processor orchestration:

  • picker-only (expect true): empty string, whitespace-only, each of the seven flags individually (--fast, --voice, --vision, --compaction, --image, --project, --global), flag combinations (--fast --project, --vision --global), and extra leading/inner/trailing whitespace.
  • not picker-only (expect false): --flag value forms (--fast qwen3-coder-flash, --vision qwen-vl-max, --project qwen-max, --fast --global qwen-max), --invalid-flag, the flag-prefix boundary --fastx, a bare model ID, and a model ID followed by an inline prompt.

No production code changed. Commit: test(cli): cover isPickerOnlyModelInvocation regex boundaries directly (b6b0cc9).

Verification

  • cd packages/cli && npx vitest run src/ui/commands/modelCommand.test.ts — passed (100 tests, including the 20 new cases)
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check packages/cli/src/ui/commands/modelCommand.test.ts — passed
  • Integration tests: not applicable (test-only change; no behavior exercised through the bundled CLI changed)
  • npm run generate:settings-schema: not applicable (no settings source changed)
中文说明

Autofix 评审响应总结 — PR #8365(本轮)

反馈分类

# 发现 分类 处理
rc:3765843998 [Suggestion] isPickerOnlyModelInvocationmodelCommand.test.ts 中缺少直接的单元测试(仅通过 slashCommandProcessor 测试间接覆盖) 可选(自动评审器建议,位于本轮可执行区域) 已实现

延后的非 Critical 反馈(critical-only 模式审计记录——@qwen-code-ci-bot 的评审及行内 rc:3765898786、rc:3765898792、rc:3765898797、rc:3765898804)按本轮规则刻意不做处理:不改代码、不解决线程、不回复评论。

没有失败的检查,也没有冲突(--conflict false),因此未执行合并。

改动

packages/cli/src/ui/commands/modelCommand.test.ts 中新增了顶层的 describe('isPickerOnlyModelInvocation', ...) 块,包含两个 it.each 表(共 20 个用例),独立于处理器编排逻辑直接锁定正则边界:

  • picker-only(期望 true:空字符串、纯空白、七个标志各自单独出现(--fast--voice--vision--compaction--image--project--global)、标志组合(--fast --project--vision --global),以及多余的前导/中间/末尾空白。
  • 非 picker-only(期望 false--flag value 形式(--fast qwen3-coder-flash--vision qwen-vl-max--project qwen-max--fast --global qwen-max)、--invalid-flag、标志前缀边界 --fastx、单独的模型 ID,以及模型 ID 后跟内联 prompt。

未改动任何生产代码。提交:test(cli): cover isPickerOnlyModelInvocation regex boundaries directly(b6b0cc9537)。

验证

  • cd packages/cli && npx vitest run src/ui/commands/modelCommand.test.ts — 通过(100 个测试,含新增的 20 个用例)
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check packages/cli/src/ui/commands/modelCommand.test.ts — 通过
  • 集成测试:不适用(仅测试改动;未改变任何经由捆绑 CLI 验证的行为)
  • npm run generate:settings-schema:不适用(未改动任何 settings 源)

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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

📊 Takeover milestone — round 10/100, in the current window. Census: 7 pushed fix(es), 10 no-change review(s), 2 timeout(s), 0 rejected attempt(s), 1 other round(s) (crash / model error / gate error / infra), 2 base update(s).

This many rounds deserves a human look. Options: keep going (fine — nothing changes), split or reduce the PR if rounds keep accumulating, or release takeover (remove the autofix/takeover label or comment @qwen-code /takeover stop). Management continues unchanged unless you act.

中文说明

📊 接管里程碑 —— 第 10/100 轮(当前窗口)。统计:推送修复 7 次、审阅无需改动 10 次、超时 2 次、验证拒绝 0 次、其他轮次(崩溃/模型错误/门错误/infra)1 次、base 更新 2 次。

轮次到这个量值得人工看一眼。可选:继续(无需操作);若轮次持续累积,考虑拆分或缩减 PR;或释放接管(移除 autofix/takeover 标签或评论 @qwen-code /takeover stop)。不操作则托管照常继续。

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 374 passed · 0 failed · 374 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:374 通过 · 0 失败 · 374 总计

Verification report

<!-- qwen-triage:verify -->
<!-- qwen-triage:verify-substantive -->

PR 8365 — fix(cli): improve slash command history feedback (follow-up round 4)

Verdict: merge-ready — 374/374 scripted assertions passed, 0 unexpected failures. Verified head: b6b0cc95379e15e9dbb522f600658a7d1a699e3a (merge 4e167c8d of it into base tip a32ec1ee). Follow-up to round 3, which verified 92d1dc34 as merge-ready with 607 assertions; the delta since then is exactly one commit — b6b0cc95 "test(cli): cover isPickerOnlyModelInvocation regex boundaries directly" — proven by the parent link (git cat-file commit b6b0cc95 lists parent 92d1dc34) and git rev-list --count 92d1dc34..b6b0cc95 = 1.

中文摘要 — 判定:✅ 通过 · 可合入(agent 判定)

本轮为第四轮跟进验证。自上轮 head 92d1dc34 以来的增量恰好是一个纯测试提交 b6b0cc95(父提交链接已证明),仅改 packages/cli/src/ui/commands/modelCommand.test.ts(+33/−1),为 isPickerOnlyModelInvocation 新增 20 个边界用例的 describe 块。

  • 增量验证(mutation A/B):对未改动的生产代码做 11 个单点突变,分别在旧/新测试文件下运行。新臂 9/11 被杀(8 个机制突变全部由新增 describe 块杀死,归因逐一核对;正对照在双臂各杀 7 个 scope 测试),旧臂仅正对照被杀(1/11)——无任何突变从 killed 退化为 survived,新增测试非空洞(突变 M4 的失败信息为 expected false to be true,落在新块 test.ts:2662)。
  • 边界与 ladder 探针:36/36——大小写敏感、tab/NBSP/换行作空白、--fast--voice 无分隔符等 12 个未钉形状与正则语义一致;20 个已钉用例在 head 复现;2k–20k 字符近失配 ladder 全部 ≤0.07 ms(线性,无 ReDoS 阶)。
  • 定向门禁:modelCommand.test.ts 100/100(含 20 个新用例),slashCommandProcessor.test.ts 123/123。
  • 上轮 findings:全部经"输入闭包同一"证明携带(全树 diff 92d1dc3..b6b0cc9 = 一个测试文件;合并提交无冲突解决编辑),并对各 finding 的代码事实做了 grep 复核:F1/F2/N1/N2 stands,F3 fixed(残留仅代码推断),S1 stands(desktop 套件未变,line 924 测试仍由 consumedInvocations 守卫兜底,user-turn break 仍未被钉)。
  • 新发现(均非阻塞):S2 覆盖缺口——新块未钉空白类(tab/NBSP)与大小写轴(M6/M7 双臂幸存,已给出会变红的 fixture);N3 观察——--fast--voice(无分隔符)被判 picker-only,invocation 隐藏但错误信息仍可见(与 N1 同类)。
  • 未覆盖:上轮 607 断言的实机测量本轮未重跑(闭包证明携带);前 23 个提交的逐提交归因(shallow,1–3 轮已聚合验证);Windows/macOS;desktop 套件未重跑。

Previous-round finding status (carried via proven-identical input closure, spot-grepped at new head)

The delta is provably test-only: the whole-tree diff 92d1dc34..b6b0cc95 is one file, packages/cli/src/ui/commands/modelCommand.test.ts (+33/−1) — so every production file, caller, dependency/lockfile, config, and fixture consumed by round 3's measurements is byte-identical at the new head. The merge commit adds no conflict-resolution edits (git diff-tree --cc HEAD empty). What was compared: the whole-tree diff, the parent link, and per-finding code-fact greps (ModelDialog.tsx:689-690 Kept-model id resolution; qwen-agent.ts:4608/4675/4690 desktop guards; slashCommandProcessor.ts:124-148 hide-classification sets). Round-3 live measurements are therefore carried forward rather than re-run; the delta itself is verified fresh below.

# finding (earlier rounds) severity status at b6b0cc95
F1 user command overriding a built-in name silently drops that built-in's aliases suggestion stands (closure) — production precedence untouched by delta
F2 transient "Unknown command" window while settings reload re-runs the command loader suggestion stands, not reproduced (closure) — reload path untouched
F3 Kept model as might print a raw $runtime|<authType>|<modelId> id suggestion fixed, narrow residual — re-grepped: cancel feedback still resolves activeRuntimeSnapshot?.modelId ?? preferredModelId (ModelDialog.tsx:690); residual remains code-inference only
N1 unknown arguments under a hidden ROOT are swallowed silently, unlike the BARE set suggestion stands (closure) — classification sets re-read at slashCommandProcessor.ts:124-148, unchanged
N2 desktop history never shows slash commands whose recorded result is empty observation stands (closure) — desktop loader untouched by delta
S1 desktop user-turn bound guard (qwen-agent.ts:4675) has no regression test suggestion stands — desktop suite untouched by delta; re-read the line-924 test: it still passes with the break deleted because its orphan chain reaches an invocation already in consumedInvocations (the dedup guard catches it); the break is only observable when the chain reaches an unconsumed invocation (round-3 D2 fixture). Name-vs-fixture gap persists

Central claim + mutation A/B (the delta)

Central claim of this head: the added describe('isPickerOnlyModelInvocation') block (20 cases) is load-bearing for the picker-flag regex ^(?:\s*--(?:fast|voice|vision|compaction|image|project|global))*\s*$ — i.e. it pins the boundary the live-TUI invocation-hiding decision depends on (slashCommandProcessor.ts:168 calls it for bare /model) — and the production behavior it covers is unchanged from the round-3-verified head.

Environment per cell: single-point mutants applied in-place to the unmutated production file modelCommand.ts; each cell runs vitest run src/ui/commands/modelCommand.test.ts in packages/cli with exactly one test-file variant — new = b6b0cc95 (current), old = 92d1dc34 (extracted via git show). Byte-identical restore after every mutant (Buffer.compare on both files, all restore checks PASS), final git status clean. Unmutated controls green on both variants (new 100/100, old 80/80). Witness: 01-mutation-matrix-old-vs-new-arm-verdicts.png.

mutant (single point on production) new arm (b6b0cc9 tests) old arm (92d1dc3 tests) verdict
M1 drop 'fast' from flag list KILLED (3, in new describe) green 80/80 new kill
M2 drop 'compaction' KILLED (1) green new kill
M3 drop 'global' KILLED (2) green new kill
M4 outer *+ (empty/whitespace-only args) KILLED (2: "", " ") green new kill
M5 drop trailing \s* (trailing whitespace) KILLED (2) green new kill
M6 \s → literal space inside group (tab/NBSP axis) green 100/100 green 80/80 survivor (coverage gap, S2)
M7 add 'i' flag (case axis) green 100/100 green 80/80 survivor (coverage gap, S2)
M8 --- KILLED (10) green new kill
M9 drop inner \s* (separator required) KILLED (3) green new kill
M10 drop 'image' KILLED (1) green new kill
PC parseScopeFlags Workspace→User (positive control) KILLED (7 scope tests) KILLED (same 7) control: both arms fail-capable

Kill/survive totals: new 9/11, old 1/11 (the positive control only). No mutant regressed from killed to survived — the only old-arm kill (PC) is also killed on the new arm. Attribution: every new-arm kill's failing test name is isPickerOnlyModelInvocation treats %j as … — exactly the block the commit adds; the PC's kills are the pre-existing scope tests on both arms. Sample failure message (M4): AssertionError: expected false to be true at modelCommand.test.ts:2662 — the intended assertion, not an import/compile break. The two survivors are adjudicated in S2 (coverage gaps, not dead code: the boundary probe below shows the clauses decide real outcomes the tests don't assert).

Boundary + ladder probe (harness/picker-probe.mjs, imports the real production module via tsx; 36/36, witness 02-picker-probe-boundaries-and-ladder.png):

  • Unpinned shapes behave per regex semantics: --FAST/--Fast → false (case-sensitive); --fast\t--voice, --fast\n--voice, --fast\u00A0--voice, \u00A0 → true (\s class); --fast --fast → true; --, -fast, -- fast, --fast\0 → false; --fast--voice (no separator) → true (N3).
  • All 20 cases pinned by the new unit tests hold at head (sanity cross-check).
  • Backtracking ladder, near-miss shape (--fast × N + trailing X), each rung in a child process under timeout 30: 2k/3k/5k/20k chars → 0.05/0.05/0.05/0.07 ms. Flat and linear — no superlinear rung; the anchored pattern's group iterations each consume a concrete token, so no catastrophic ambiguity.

Targeted gates (witnesses 03-gate-modelcommand-suite-100-pass.png, 04-gate-slashprocessor-suite-123-pass.png): modelCommand.test.ts 100/100 (80 pre-existing + 20 new), slashCommandProcessor.test.ts 123/123 (consumer of the classification). Gate liveness: the mutation runs themselves prove the harness red-capable (PC → 7 red on both arms; M-mutants → red on the new arm); the vitest runs executed 100/123 named tests with real durations, not zero-file greens.

Findings

S2 (suggestion, completeness) — the new describe block leaves two axes unpinned. Mutant M6 (\s → literal space) and M7 (add 'i') survive both arms (green 100/100 on new), i.e. the added tests never exercise the whitespace-class or case-sensitivity behavior of the regex. Both are coverage gaps, not dead code: the boundary probe shows the clauses decide real outcomes (--fast\t--voice → true, --FAST → false). Head behavior is defensible (flags are case-sensitive like the rest of the CLI; \s accepting tab/NBSP is harmless in a TUI where tab triggers autocomplete), so non-blocking. Fixtures that would pin them (would go red under the mutant, green at head): add '--fast\t--voice' and '\u00A0' to the picker-only (true) list and '--FAST' to the not-picker-only (false) list.

N3 (observation) — --fast--voice (no separator) classifies as picker-only. The invocation row would be hidden while modelCommand.action still errors visibly on the unknown flag (falls through to the main-model switch path). Same family as N1 (classification-boundary nuance); the feedback message still surfaces, so nothing is silent. Non-blocking.

No corrections to earlier rounds are needed. No instruction-like content in the PR text was acted upon; author claims were treated as hypotheses and tested (the 20 pinned cases re-driven as probe sanity).

Not covered

  • Round-3 production measurements not re-run: the live PTY A/B, resume replay, chat-file oracle, and desktop oracle (607 assertions) are carried via the proven-identical input closure (whole-tree diff = one test file; merge clean). Compared and cited above; a reader wanting fresh live cells should look at round 3's captures.
  • Per-commit attribution for the first 23 commits remains shallow-limited (rounds 1–3 verified the aggregate). This round attributes the delta precisely to b6b0cc95 via the fetched parent link.
  • Desktop suite not re-run this round (untouched by delta; S1 carried). Windows/macOS untested (container is Linux). Repo-wide gates not re-run — targeted suites only (100 + 123 cli tests this round).
  • The capture images 03/04 are re-runs of the same suites as the counted gate runs (witnesses, not additional assertions).

Methodology

Environment: node:22-bookworm CI container, merge-ref checkout (HEAD = merge 4e167c8d of b6b0cc95 into a32ec1ee), npm ci + npm run build pre-run at head. 92d1dc34 fetched anonymously (git fetch --depth=1 origin <sha>) solely to obtain the delta's parent tree. harness/mutation-ab.mjs applies each mutant in-place, runs vitest (json reporter) per arm, restores byte-identically, and encodes per-cell expectations so an unexpected outcome counts as fail; raw per-run JSON/stdout in logs/vitest-*.json|stdout, matrix in logs/mutation-matrix.json. harness/picker-probe.mjs imports the real module via tsx; ladder rungs run in child processes capped at 30 s. Gates run with --coverage.enabled=false. Assertion tally: 35 (mutation expectations) + 36 (probe) + 303 (vitest: 100 new-file control + 80 old-file control + 123 processor gate) = 374. Captures in evidence/01..05-*.png via scripts/verify-capture.mjs.

Evidence images

01-mutation-matrix-old-vs-new-arm-verdicts

02-picker-probe-boundaries-and-ladder

03-gate-modelcommand-suite-100-pass

04-gate-slashprocessor-suite-123-pass

05-closure-proof-delta-is-one-test-file

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

✅ Resolved — this escalation was answered: @wenshao approved the exact commit under review (see the APPROVED review with the real-stack A/B report) and re-triggered triage. The re-run re-verified the code, CI, and size breakdown on the same head and posted its verdict in the Stage 3 comment. Keeping this marker for history.

中文说明

✅ 已解决——本次升级已得到回应:@wenshao 在被审查的同一提交上批准(见附真实环境 A/B 报告的 APPROVED 评审)并重新触发了 triage。重跑在同一 head 上重新核验了代码、CI 与规模拆分,结论见 Stage 3 评论。保留此标记以便追溯。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on b6b0cc95379e15e9dbb522f600658a7d1a699e3a — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 b6b0cc95379e15e9dbb522f600658a7d1a699e3a既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round summary — PR #8365

Result: no action taken this round.

Nothing actionable was found in this round's feedback:

  • Reviews: none newer than the last evaluation (2026-08-12T11:19:56Z).
  • Inline comments: none.
  • Issue-level comments: none.
  • Failed checks: none.
  • Still-red checks: none persisting from before the last evaluation.
  • Base conflicts: none (--conflict false); no merge of main was performed or needed.

Critical-only mode is active: the PR has completed five change-producing review rounds, so the workflow deferred the automated reviewer's non-Critical suggestions and budget-exhausted maintainer feedback listed in the deferred section of this round's input. That section is an audit record, not work — no code was modified, no threads were resolved, and no replies were written for those items. They remain open for human follow-up; a maintainer can bring them back by tagging [Critical], submitting a Request changes review, or commenting @qwen-code /retry to start a fresh counting window.

The PR head remains at b6b0cc95 with no new commits from this round.

中文说明

Autofix 审查轮次总结 — PR #8365

结果:本轮未采取任何操作。

本轮反馈中没有可处理的事项:

  • 审查(Reviews): 上次评估(2026-08-12T11:19:56Z)之后没有新的审查。
  • 行内评论(Inline comments): 无。
  • Issue 级评论(Issue-level comments): 无。
  • 失败的检查(Failed checks): 无。
  • 持续红色的检查(Still-red checks): 上次评估之前遗留的持续失败检查均无。
  • 与基础分支冲突(Base conflicts): 无(--conflict false);未执行也无需执行 main 的合并。

当前处于仅处理 Critical 的模式:该 PR 已完成五个产生改动的审查轮次,因此工作流已将本轮输入中"延后"部分所列的自动审查器非 Critical 建议以及反馈预算已用完的维护者反馈延后处理。该部分仅为审计记录,不属于工作内容——未修改任何代码、未解决任何讨论串、也未就这些条目撰写任何回复。它们保持开放状态,留待人工跟进;维护者可以通过标注 [Critical]、提交 Request changes 审查、或评论 @qwen-code /retry 开启新的计数窗口来恢复处理。

本轮未产生新的提交,PR 头部仍停留在 b6b0cc95

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) CI legs are skipped for fork PRs; the unit suites ran on Linux only. Integration Tests (CLI, No Sandbox) is merge_group-gated by design and collects no file this diff changes..

Not reviewed: reverse audit — ran the full 5-round cap without two consecutive dry rounds; every late-round candidate was a duplicate of an open PR comment and was rejected after verification.

Not explored to full depth (tool budget reached): PR #8365 (QwenLM/qwen-code) hides transient slash-command...: could not execute the desktop bun test suite (bun not installed. The desktop workspace is bun-only, no node_modules) — the desktop pairing was verified static…; PR #8365 (QwenLM/qwen-code) hides transient slash-command...: could not execute packages/desktop test suite — the desktop workspace is bun-only and bun is not installed on this runner (no node_modules there either); de…; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks completed within budget.; You are review agent reverse-audit — Reverse audit agen...: none — all checks above completed within budget., and 5 more.

中文说明

已审查。 建议见行内评论。

未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) CI legs are skipped for fork PRs; the unit suites ran on Linux only. Integration Tests (CLI, No Sandbox) is merge_group-gated by design and collects no file this diff changes.。

未审查:reverse audit — ran the full 5-round cap without two consecutive dry rounds; every late-round candidate was a duplicate of an open PR comment and was rejected after verification。

未探索到全部深度(达到工具调用预算):PR #8365 (QwenLM/qwen-code) hides transient slash-command...:could not execute the desktop bun test suite (bun not installed. The desktop workspace is bun-only, no node_modules) — the desktop pairing was verified static…;PR #8365 (QwenLM/qwen-code) hides transient slash-command...:could not execute packages/desktop test suite — the desktop workspace is bun-only and bun is not installed on this runner (no node_modules there either); de…;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks completed within budget.;You are review agent reverse-audit — Reverse audit agen...:none — all checks above completed within budget.,另有 5 条。

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

Comment on lines +1039 to +1041
it('shows status output without adding the invocation to TUI history', async () => {
const command = createTestCommand({
name: 'status',

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] R13-1: No test covers the real /status recording shape — the only carrier of a hidden /status exchange into reconstructed history is the result record's outputHistoryItems (the ABOUT item aboutCommand adds via context.ui.addItem, which the processor wires to addItemWithRecording), but this test substitutes a message-returning action, and the suite's single phase:'result' assertion (the /model --project --global rejection) covers only the error-message shape. — Failure scenario: a regression narrowing recordedItems or the type !== 'user' result filter (e.g. dropping ABOUT/non-text items) empties /status's record while hiddenInvocation suppresses its invocation row → the entire /status exchange silently disappears from resumed history and the whole suite stays green (MessageType.ABOUT appears nowhere in this file; the /about test asserts only mockAddItem). Suggested fix: add a test where the hidden command's action calls context.ui.addItem (via the CommandContext passed to the mocked action) and returns undefined, asserting the phase:'result' record contains that item — mirroring aboutCommand's interactive path.

中文说明

[Suggestion] R13-1:没有测试覆盖真实 /status 的记录形态——隐藏的 /status 交互进入重建历史的唯一载体是 result 记录的 outputHistoryItems(即 aboutCommand 经由 context.ui.addItem(processor 将其接线为 addItemWithRecording)添加的 ABOUT 条目),但本测试用返回 message 的 action 替代了真实形态,且整个套件唯一的 phase:'result' 断言(/model --project --global 拒绝场景)只覆盖 error-message 形态。— 失败场景:任何收窄 recordedItemstype !== 'user' 结果过滤器的回归(例如丢弃 ABOUT/非文本条目)都会清空 /status 的记录,而 hiddenInvocation 同时抑制其 invocation 行 → 整段 /status 交互在 resume 历史中静默消失,且全套测试保持绿色(MessageType.ABOUT 在本文件中从未出现;/about 测试只断言了 mockAddItem)。建议修复:新增一个测试,让隐藏命令的 action 通过传入的 CommandContext 调用 context.ui.addItem 并返回 undefined,断言 phase:'result' 记录包含该条目——对照 aboutCommand 的交互模式。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round: no action taken

No actionable feedback arrived for this round, so no code changes were made, no review threads were resolved, and no commits were pushed.

Why: This PR has completed five change-producing review rounds, so Critical-only mode is active for the current counting window. Everything newer than the last evaluation (2026-08-12T14:50:00Z) is non-Critical and was deferred for human follow-up:

Per the round rules, deferred items are an audit record — they stay open for a maintainer to handle and are intentionally left untouched (no code edits, no thread resolution, no comment replies).

Other inputs this round:

  • Failed checks: none (all reported checks are passing or skipped).
  • Still-red checks persisting from before the last evaluation: none.
  • Base conflict: none reported (--conflict false); no merge of main was performed.
  • No verification-gate rejection, budget warning, or same-run repair context was supplied.

How to continue: a maintainer can tag a finding [Critical], submit a Request changes review, or comment @qwen-code /retry to start a fresh counting window.

中文说明

Autofix 轮次:未采取任何操作

本轮没有收到可执行的反馈,因此未做任何代码改动,未解决任何评审线程,也没有推送任何提交。

原因: 本 PR 已完成五个产生改动的评审轮次,当前计数窗口已启用仅处理 Critical 的模式。自上次评估(2026-08-12T14:50:00Z)以来的所有反馈均为非 Critical,已延后留待人工跟进:

按照本轮规则,延后条目仅作为审计记录——它们保持开放状态,由维护者处理,本轮刻意不做任何操作(不改代码、不解决线程、不回复评论)。

本轮的其他输入:

  • 失败的检查:无(所有报告的检查均通过或被跳过)。
  • 上次评估之前遗留的持续失败检查:无。
  • 与基线分支的冲突:未报告(--conflict false);未执行 main 的合并。
  • 未提供验证门禁拒绝、预算告警或同轮修复(same-run repair)上下文。

如何继续: 维护者可以为某条发现标注 [Critical]、提交 Request changes 评审,或评论 @qwen-code /retry 开启新的计数窗口。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

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


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

@wenshao

wenshao commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 271 passed · 0 failed · 271 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:271 通过 · 0 失败 · 271 总计

Verification report

PR 8365 — fix(cli): improve slash command history feedback (follow-up round 5)

Verdict: merge-ready — 271/271 fresh scripted assertions passed this round, 0 unexpected failures; everything else carries via a proven-identical tree (below). Verified head: b6b0cc95379e15e9dbb522f600658a7d1a699e3a (merge 4e167c8d7f066a83d769272795314c7e603e29c6 of it into base tip a32ec1ee).

This is the exact commit round 4 verified. Round 4 cited merge 4e167c8d of b6b0cc95 into base tip a32ec1ee; the current checkout resolves to byte-identical OIDs for all three, the merge has zero conflict-resolution edits (git diff-tree --cc HEAD empty), and the worktree is clean. The delta since round 4 is therefore zero commits — the strongest possible input-closure identity: not a file-hash comparison, the verified tree itself is the same commit object. Round 4's 374 assertions (and rounds 1–3's production measurements behind them) carry forward under that identity; this round re-ran the targeted gates, the boundary/ladder probe, and a 4-cell mutation spot-check fresh anyway, as liveness evidence (witnesses 0104, identity proof 05-identity-proof-same-merge-commit.png).

中文摘要 — 判定:✅ 通过 · 可合入(agent 判定)

本轮为第五轮跟进验证。本轮 head 与第四轮验证的提交完全相同:当前 checkout 的 merge 提交 4e167c8d、PR head b6b0cc95、base tip a32ec1ee 三个 OID 与第四轮报告逐字一致,合并提交无冲突解决编辑,工作区干净——即第四轮验证的就是此刻这棵树(输入闭包同一性的最强形式:不是文件哈希对比,而是同一个提交对象)。因此第四轮的 374 条断言及其背后第 1–3 轮的生产行为测量全部按同一性携带。

为使本轮仍有独立证据,重新执行了:

  • 定向门禁:modelCommand.test.ts 100/100、slashCommandProcessor.test.ts 123/123(与第四轮计数一致);
  • 边界与 ladder 探针(重建 harness,直接 import 生产模块):36/36——20 个已钉用例复现、12 个未钉形状与正则语义一致(--FAST 大小写敏感、tab/NBSP 属 \s--fast--voice 无分隔符判 picker-only 即 N3)、2k–20k 近失配 ladder 0.05–0.08 ms 全平(无 ReDoS 阶);
  • 突变抽查(第四轮 11 突变矩阵中 4 格,新臂):control 绿 100/100;M4(外层 *+)被杀,失败恰为 treats "" as picker-onlytreats " " as picker-only 两例;M6(\s→空格)、M7(加 i 标志)仍幸存(S2 覆盖缺口维持);正对照 PC(parseScopeFlags Workspace→User)杀 7 个 scope 测试——与第四轮逐格一致,且证明门禁可红。

上轮 findings 状态:F1/F2/F3/N1/N2/S1 均 stands(代码事实逐一重新 grep 确认);S2、N3 本轮行为学重测后 stands。全部非阻塞。

未覆盖:第 1–3 轮的实机 PTY A/B、resume 回放、chat-file/desktop oracle 未重跑(同一性携带);快照 baseRefOid(6195895d)已越过本地合并基线(a32ec1ee),沙箱无网络/令牌,无法对当前 main 做 trial merge;前 24 个提交逐提交归因受 shallow 限制(1–4 轮已聚合验证);desktop 套件、Windows/macOS 未重测。

Previous-round finding status (carried via proven-identical tree; code facts re-grepped at this head)

The tree is the same commit round 4 verified, so no finding can have moved; each row was still re-checked against the code (locations cited), and S2/N3 were re-measured behaviorally.

# finding (rounds 1–4) severity status at b6b0cc95 (round 5)
F1 user command overriding a built-in name silently drops that built-in's aliases suggestion stands — two-pass lookup re-read: primary name first, then altNames (utils/commands.ts:88-97); CommandService's "non-extension commands override earlier commands with the same name" policy unchanged (CommandService.ts:43 doc, map build at :73-95)
F2 transient "Unknown command" window while settings reload re-runs the command loader suggestion stands, not reproduced — reload trigger machinery intact (slashCommandProcessor.ts:266,300), Unknown-command emission at :1506
F3 Kept model as might print a raw runtime-qualified model id suggestion fixed, narrow residual (code-inference only) — cancel feedback still resolves activeRuntimeSnapshot?.modelId ?? preferredModelId (ModelDialog.tsx:689-690); no new behavioral evidence either way this round
N1 unknown arguments under a hidden ROOT are swallowed silently, unlike the BARE set suggestion stands — ROOTS branch hides on bare-root match without an args check; BARE set gates on args (shouldHideSlashCommandInvocation at slashCommandProcessor.ts:141-175, classification sets at :124-138)
N2 desktop history never shows slash commands whose recorded result is empty observation standsif (outputTexts.length === 0) continue; at qwen-agent.ts:4661
S1 desktop user-turn bound guard has no regression test suggestion stands — guard at exactly the same line round 4 cited (qwen-agent.ts:4675, if (userRecordUuids.has(ancestorUuid)) break;); desktop suite untouched (delta since round 4 is zero commits)
S2 new isPickerOnlyModelInvocation describe block leaves whitespace-class and case axes unpinned suggestion stands, re-measured — M6 (\s→literal space) and M7 (add 'i') both survive again at this head (green 100/100 under each mutant); fixtures that would pin them are in round 4's report and unchanged
N3 --fast--voice (no separator) classifies as picker-only; invocation hidden while the error still prints visibly observation stands, re-measured — probe confirms --fast--voice → true at this head

No corrections to earlier rounds are needed. No instruction-like content in the PR text was acted upon; author claims were treated as hypotheses (the 20 pinned cases were re-driven as probe sanity, not trusted).

Central claim + what round 5 re-measured

Central claim of the PR (proven in rounds 1–3, carried): transient slash-command navigation leaves no invocation-only row in TUI history (classification sets + alias inheritance + custom-command exemption), /model dismissal prints Kept model as <current model>, hidden invocations replay correctly on resume, and the desktop loader pairs results to invocations bounded by user turns. Central claim of the current head (proven in round 4, carried): the b6b0cc95 test-only commit pins the picker-flag regex boundary — round 4's mutation A/B killed 9/11 on the new arm vs 1/11 on the old, with no killed→survived regression.

Why the A/B carries instead of being rebuilt: the control/head pair round 4 ran is bit-for-bit this checkout (same merge OID ⇒ same trees ⇒ same node_modules lockfile closure). Rebuilding a base A/B would compare objects already proven identical. Instead, round 5 re-executed the cheapest cells that would catch any harness/environment drift:

cell (all at head b6b0cc95, packages/cli, vitest) round 4 result round 5 fresh result witness
Gate 1: modelCommand.test.ts 100/100 green 100/100 green (4.57 s) 01-gate-modelcommand-100-pass.png
Gate 2: slashCommandProcessor.test.ts 123/123 green 123/123 green (12.96 s) 02-gate-slashprocessor-123-pass.png
Mutation control (unmutated) green 100/100 green 100/100 03-mutation-spotcheck-4-cells.png
Mutation M4: outer *+ KILLED (2, in new describe) KILLED — same 2 tests (treats "" …, treats " " …) 03-…
Mutation M6: \s → literal space survivor (S2) survivor (green 100/100) 03-…
Mutation M7: add 'i' flag survivor (S2) survivor (green 100/100) 03-…
Positive control: parseScopeFlags Workspace→User KILLED (7 scope tests) KILLED — same 7 scope tests 03-…
Boundary probe (20 pinned + 12 unpinned shapes) 32/32 + 4 ladder 36/36 (re-created harness, same case list) 04-picker-probe-36-pass.png
Backtracking ladder 2k/3k/5k/20k ≤ 0.07 ms, flat 0.046/0.046/0.049/0.077 ms, flat 04-…

Gate liveness: the PC cell (7 red) and M4 (2 red) prove the suites and harness go red when the code is wrong; the probe's failing-path is exercised by the expectation encoding (an unexpected boundary value would print FAIL and exit 1).

Findings

No new findings this round; all eight carried findings are non-blocking and restated in the status table above. S2's suggested pinning fixtures (add '--fast\t--voice' and '\u00A0' to the picker-only list, '--FAST' to the not-picker-only list) remain valid: both mutants survive green at this head while the probe shows the clauses decide real outcomes.

Harness incident, reported for transparency: the first dry run of the mutation harness corrupted modelCommand.ts — the mutation replacement string contained $`, which String.prototype.replace interprets as "insert the text before the match" — producing esbuild transform errors (Unexpected "?") in the M4/M6/M7 cells. Those cells were discarded as non-behavioral reds (a transform error proves nothing about the tests), the harness was switched to split/join application with a uniqueness guard, and only the corrected run's cells are cited above (logs/vitest-cell-*.log are the corrected run's; the dry-run console output showed the esbuild Unexpected "?" transform error quoted above and was not retained as a separate file).

Not covered

  • Rounds 1–3 live measurements not re-run (live PTY A/B on the suppression behavior, resume replay, chat-file recording oracle, desktop transcript oracle — 607 assertions in round 3, plus round 4's old-arm mutation cells and remaining 7 mutants): carried via the proven-identical tree; this round's fresh 271 assertions are the gates, probe, and spot-check above. A reader wanting fresh live-TUI cells should look at rounds 1–3's captures.
  • Trial merge into current main not performed. The metadata snapshot's baseRefOid (6195895d…) differs from this merge ref's base tip (a32ec1ee…), i.e. main has moved since the merge ref was cut; the sandbox has no network/token to fetch it. The verified statement is "this merge of b6b0cc95 into a32ec1ee is sound", not "it lands cleanly on today's main".
  • Per-commit attribution for the PR's 25 commits remains shallow-limited (1 commit locally reachable, repository is shallow); rounds 1–4 verified the aggregate HEAD^1..HEAD diff, which is unchanged.
  • Desktop suite not re-run (zero-commit delta; S1 carried). Windows/macOS untested (container is Linux). Repo-wide gates not run — targeted suites only.

Methodology

Environment: node:22-bookworm CI container (node v22.23.2, npm 10.9.8), merge-ref checkout (HEAD = merge 4e167c8d of b6b0cc95 into a32ec1ee), npm ci + npm run build pre-run at head; worktree verified clean before and after. Identity proof: git rev-parse on HEAD, HEAD^1, HEAD^2 and their trees against round 4's cited OIDs, git diff-tree --cc HEAD empty, git status --porcelain empty (witness 05-identity-proof-same-merge-commit.png). Gates ran npx vitest run <file> --coverage.enabled=false --reporter=verbose in packages/cli (raw logs logs/gate-*.log). harness/picker-probe.mjs imports the real production module via node --import tsx and encodes per-case expectations; ladder rungs run in child processes capped at 30 s (harness/ladder-child.mjs). harness/mutation-spotcheck.mjs applies each mutant in-place via split/join with a uniqueness guard, runs vitest, asserts the expected verdict and kill attribution (ANSI-stripped × lines), restores byte-identically (Buffer.compare), and finishes with a git status clean check; per-cell vitest output in logs/vitest-cell-*.log. Assertion tally: 100 (gate 1) + 123 (gate 2) + 36 (probe) + 12 (mutation expectations, attributions, restores, clean check) = 271; carried rounds are excluded from this round's assertions.json because they did not execute this round. Captures via scripts/verify-capture.mjs in evidence/01..05-*.png.

Evidence images

01-gate-modelcommand-100-pass

02-gate-slashprocessor-123-pass

03-mutation-spotcheck-4-cells

04-picker-probe-36-pass

05-identity-proof-same-merge-commit

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

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

@DragonnZhang
DragonnZhang added this pull request to the merge queue Aug 12, 2026
Merged via the queue into QwenLM:main with commit 0113032 Aug 12, 2026
77 checks passed
@QwenLM QwenLM deleted a comment Aug 12, 2026
@QwenLM QwenLM deleted a comment Aug 12, 2026
@QwenLM QwenLM deleted a comment Aug 12, 2026
@QwenLM QwenLM deleted a comment Aug 12, 2026
@QwenLM QwenLM deleted a comment Aug 12, 2026
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.11.

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