Skip to content

fix(cli): keep model switches session-scoped#6579

Open
zjunothing wants to merge 15 commits into
QwenLM:mainfrom
zjunothing:fix/model-session-default
Open

fix(cli): keep model switches session-scoped#6579
zjunothing wants to merge 15 commits into
QwenLM:mainfrom
zjunothing:fix/model-session-default

Conversation

@zjunothing

Copy link
Copy Markdown
Collaborator

What this PR does

This changes the main model switch flow so ordinary /model <model-id> and ordinary model-picker selections only update the active session. Persisting the main model as the default now requires the explicit /model --default <model-id> path, and /model --default opens a default-setting picker. Scope flags for the main model are also explicit: /model --default --project ... writes the project default, and /model --default --global ... writes the user default.

Auxiliary model settings keep their existing behavior: /model --fast, /model --voice, and /model --vision still persist their configured values.

Why it's needed

The command description says /model switches the model for this session, but the previous implementation also wrote model.name, model.baseUrl, and sometimes security.auth.selectedType to settings. That made a temporary model switch leak into later sessions and made it easy to accidentally change the global or project default.

Reviewer Test Plan

How to verify

Run /model <model-id> in a directory with configured models and confirm the active session switches while settings.json remains unchanged. Then run /model --default <model-id> and confirm settings.json updates model.name for future sessions. Also verify /model --default --project <model-id> and /model --default --global <model-id> write to the expected scope, while /model --fast, /model --voice, and /model --vision continue to persist their auxiliary settings.

Evidence (Before & After)

Before: source-inspection repro from #4331 showed /model <model-id> wrote persistent settings even though the command is described as session-scoped.

After: local CLI verification with a temporary QWEN_HOME showed /model gpt-4o printed Model: gpt-4o while settings.json kept model.name: gpt-4o-mini; running -p "/model --default gpt-4o" printed Model: gpt-4o (default) and updated settings.json to model.name: gpt-4o.

Focused tests passed: cd packages/cli && npx vitest run src/ui/commands/modelCommand.test.ts src/ui/components/ModelDialog.test.tsx src/ui/hooks/slashCommandProcessor.test.ts.

Full checks passed: npm run build and npm run typecheck.

Tested on

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

Environment (optional)

macOS, Node.js v22.23.1, local repo dev build via npm run dev.

Risk & Scope

  • Main risk or tradeoff: users who relied on plain /model <id> to update their default must now use /model --default <id>.
  • Not validated / out of scope: Windows and Linux manual CLI verification; auxiliary model persistence was covered by existing focused tests but not manually exercised in every UI path.
  • Breaking changes / migration notes: intentional behavior change for the main model switch command to match its session-scoped description.

Linked Issues

Closes #4331

中文说明

本 PR 做了什么

这个改动让主模型切换流程中普通的 /model <model-id> 和普通模型选择器只更新当前会话。要把主模型持久化为默认值,现在必须显式使用 /model --default <model-id>/model --default 会打开设置默认模型的选择器。主模型的作用域标志也变为显式语义:/model --default --project ... 写项目默认值,/model --default --global ... 写用户默认值。

辅助模型设置保持原有行为:/model --fast/model --voice/model --vision 仍然会持久化对应配置值。

为什么需要

命令描述说 /model 是切换当前会话的模型,但旧实现还会把 model.namemodel.baseUrl,有时还有 security.auth.selectedType 写入 settings。这会让一次临时模型切换泄漏到后续会话,也很容易意外改掉全局或项目默认模型。

Reviewer Test Plan

如何验证

在配置了模型的目录中运行 /model <model-id>,确认当前会话模型切换,但 settings.json 不变。然后运行 /model --default <model-id>,确认 settings.jsonmodel.name 被更新并影响后续会话。也可以验证 /model --default --project <model-id>/model --default --global <model-id> 写入预期作用域,而 /model --fast/model --voice/model --vision 仍然持久化辅助设置。

证据(Before & After)

Before:#4331 中的源码级复现显示 /model <model-id> 会写持久设置,尽管命令文案说它是会话级切换。

After:我用临时 QWEN_HOME 做了本地 CLI 验证,/model gpt-4o 输出 Model: gpt-4o,但 settings.json 仍保持 model.name: gpt-4o-mini;运行 -p "/model --default gpt-4o" 后输出 Model: gpt-4o (default),并把 settings.json 更新为 model.name: gpt-4o

聚焦测试已通过:cd packages/cli && npx vitest run src/ui/commands/modelCommand.test.ts src/ui/components/ModelDialog.test.tsx src/ui/hooks/slashCommandProcessor.test.ts

完整检查已通过:npm run buildnpm run typecheck

测试平台

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

环境(可选)

macOS,Node.js v22.23.1,本地仓库 dev build,通过 npm run dev 验证。

风险与范围

  • 主要风险或取舍:如果用户之前依赖普通 /model <id> 更新默认模型,现在需要改用 /model --default <id>
  • 未验证 / 不在范围内:没有在 Windows 和 Linux 上做手工 CLI 验证;辅助模型持久化由现有聚焦测试覆盖,但没有手工跑遍所有 UI 路径。
  • 破坏性变更 / 迁移说明:这是对主模型切换命令的有意行为变更,用来匹配其“当前会话”语义。

关联 Issue

Closes #4331

@zjunothing

Copy link
Copy Markdown
Collaborator Author

Local verification report

I verified this PR with a real local CLI run using a temporary QWEN_HOME and a fake OpenAI-compatible provider. No valid API key or networked model request was required because /model is handled as a slash command before any model call.

Real CLI behavior check

Temporary settings started with:

{
  "model": { "name": "gpt-4o-mini" },
  "security": { "auth": { "selectedType": "openai" } }
}

Session-scoped switch:

$ QWEN_HOME="$tmp" OPENAI_API_KEY=dummy npm run dev -- /model gpt-4o --auth-type openai --output-format text
Model: gpt-4o

Result: settings.json remained unchanged with model.name: gpt-4o-mini.

Explicit default switch:

$ QWEN_HOME="$tmp" OPENAI_API_KEY=dummy npm run dev -- -p "/model --default gpt-4o" --auth-type openai --output-format text
Model: gpt-4o (default)

Result: settings.json updated to:

{
  "model": { "name": "gpt-4o", "baseUrl": "" },
  "security": { "auth": { "selectedType": "openai" } }
}

Note: for the second command I used -p "/model --default ..." because passing /model --default ... as bare shell arguments makes the top-level CLI parser treat --default as a CLI option before the slash-command layer sees it. Interactive slash-command usage is not affected.

Automated checks

$ cd packages/cli && npx vitest run src/ui/commands/modelCommand.test.ts src/ui/components/ModelDialog.test.tsx src/ui/hooks/slashCommandProcessor.test.ts
Test Files  3 passed (3)
Tests  186 passed (186)
$ npm run build
# passed
$ npm run typecheck
# passed

Screenshot / visual evidence

This change is command/settings behavior rather than a visual UI rendering change, so I did not include a screenshot. The CLI transcript above is the relevant before/after evidence.

中文验证报告

本地验证报告

我使用临时 QWEN_HOME 和假的 OpenAI-compatible provider 做了真实本地 CLI 验证。不需要有效 API key 或真实模型网络请求,因为 /model 是 slash command,会在模型调用前处理。

真实 CLI 行为验证

临时 settings 初始内容为:

{
  "model": { "name": "gpt-4o-mini" },
  "security": { "auth": { "selectedType": "openai" } }
}

会话级切换:

$ QWEN_HOME="$tmp" OPENAI_API_KEY=dummy npm run dev -- /model gpt-4o --auth-type openai --output-format text
Model: gpt-4o

结果:settings.json 保持不变,仍然是 model.name: gpt-4o-mini

显式默认值切换:

$ QWEN_HOME="$tmp" OPENAI_API_KEY=dummy npm run dev -- -p "/model --default gpt-4o" --auth-type openai --output-format text
Model: gpt-4o (default)

结果:settings.json 更新为:

{
  "model": { "name": "gpt-4o", "baseUrl": "" },
  "security": { "auth": { "selectedType": "openai" } }
}

说明:第二条命令使用了 -p "/model --default ...",因为如果把 /model --default ... 作为裸 shell 参数传入,顶层 CLI parser 会先把 --default 当作 CLI 参数拦截,slash-command 层拿不到它。交互式 slash command 使用不受影响。

自动化检查

$ cd packages/cli && npx vitest run src/ui/commands/modelCommand.test.ts src/ui/components/ModelDialog.test.tsx src/ui/hooks/slashCommandProcessor.test.ts
Test Files  3 passed (3)
Tests  186 passed (186)
$ npm run build
# passed
$ npm run typecheck
# passed

截图 / 可视化证据

这个改动是命令和 settings 行为变化,不是 UI 渲染变化,所以没有附截图。上面的 CLI transcript 就是相关的 before/after 证据。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: Observed bug with clear evidence. Issue #4331 documents that /model <id> writes model.name, model.baseUrl, and security.auth.selectedType to settings.json despite the command being described as session-scoped. Your CLI verification in the PR body confirms the before/after behavior.

Direction: Aligned. The command's own description says "switch the model for this session" — persisting the switch silently is a UX bug. Adding --default as the explicit persistence opt-in is the natural fix. No CHANGELOG reference but this is squarely within CLI UX.

Size: Not applicable — all changes are in packages/cli/src/ui/, no core infrastructure paths touched. Production logic: 160 lines, test: 151 lines.

Approach: Scope feels right. The --default flag is minimal and intuitive. Requiring --default before --project/--global is correct (those are persistence scope flags). Rejecting --default with --fast/--voice/--vision is correct (auxiliary models already persist intentionally). No unrelated changes.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测到的 bug,有明确证据。Issue #4331 记录了 /model <id> 会将 model.namemodel.baseUrlsecurity.auth.selectedType 写入 settings.json,尽管命令描述说它是"当前会话"级别的切换。PR 正文中的 CLI 验证确认了 before/after 行为。

方向:对齐。命令自己的描述说"为当前会话切换模型"——默默持久化是一个 UX bug。添加 --default 作为显式持久化 opt-in 是自然的修复方式。CHANGELOG 无直接引用,但这属于 CLI UX 范畴。

规模:不适用——所有改动都在 packages/cli/src/ui/ 中,未触及核心基础设施路径。生产逻辑 160 行,测试 151 行。

方案:范围合理。--default 标志是最小且直观的。要求 --default 才能使用 --project/--global 是正确的(那些是持久化作用域标志)。拒绝 --default--fast/--voice/--vision 组合是正确的(辅助模型本来就会持久化)。没有无关改动。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: gate persistSetting calls behind a new --default flag; keep --fast/--voice/--vision untouched; require --default before --project/--global; thread the flag through the model picker dialog so interactive selection can also persist.

The PR's approach matches this exactly — parseDefaultFlag for flag extraction, conditional persistence in switchMainModel, mutual exclusion with auxiliary model flags, scope flags requiring --default, and full threading through useModelCommandUIStateContextDialogManagerModelDialog. Clean and focused.

No critical blockers found. No AGENTS.md violations. The code follows existing conventions — regex flag parsing mirrors parseScopeFlags, state management follows the persistScope pattern, tests are thorough and co-located.

Reuse check: no missed reuse opportunities. Flag parsing, state threading, and dialog props all follow established patterns in the codebase.

Test Results

Focused unit tests (PR code, worktree)

 RUN  v3.2.4 packages/cli

 ✓ src/ui/commands/modelCommand.test.ts (70 tests) 84ms
 ✓ src/ui/components/ModelDialog.test.tsx (34 tests) 224ms
 ✓ src/ui/hooks/slashCommandProcessor.test.ts (82 tests) 6097ms

 Test Files  3 passed (3)
      Tests  186 passed (186)
   Duration  14.15s

CLI verification (PR code via npm run dev)

The worktree's npm run dev hit an @qwen-code/acp-bridge module resolution error (worktree build environment issue, not a PR code issue). The PR author's own local CLI verification demonstrates the behavior change:

Session-only switch (PR code):

$ QWEN_HOME="$tmp" OPENAI_API_KEY=dummy npm run dev -- /model gpt-4o --auth-type openai --output-format text
Model: gpt-4o

settings.json unchanged: model.name: gpt-4o-mini

Explicit default switch (PR code):

$ QWEN_HOME="$tmp" OPENAI_API_KEY=dummy npm run dev -- -p "/model --default gpt-4o" --auth-type openai --output-format text
Model: gpt-4o (default)

settings.json updated: model.name: gpt-4o

中文说明

代码审查

在阅读 diff 之前的独立方案:在新的 --default 标志后面加上 persistSetting 调用的门控;保持 --fast/--voice/--vision 不变;要求 --default 才能使用 --project/--global;将标志传递到模型选择对话框,以便交互式选择也能持久化。

PR 的方案完全匹配——parseDefaultFlag 做标志提取,switchMainModel 中条件化持久化,与辅助模型标志互斥,作用域标志要求 --default,以及完整的 useModelCommandUIStateContextDialogManagerModelDialog 传递链。干净且聚焦。

未发现关键阻塞问题。未违反 AGENTS.md。代码遵循现有约定——正则标志解析与 parseScopeFlags 一致,状态管理遵循 persistScope 模式,测试充分且与源码同目录。

复用检查:未遗漏复用机会。标志解析、状态传递和对话框 props 都遵循了代码库中的既定模式。

测试结果

聚焦单元测试(PR 代码,worktree)

 ✓ src/ui/commands/modelCommand.test.ts (70 tests) 84ms
 ✓ src/ui/components/ModelDialog.test.tsx (34 tests) 224ms
 ✓ src/ui/hooks/slashCommandProcessor.test.ts (82 tests) 6097ms
 Test Files  3 passed (3)
      Tests  186 passed (186)

CLI 验证(PR 代码通过 npm run dev

worktree 的 npm run dev 遇到了 @qwen-code/acp-bridge 模块解析错误(worktree 构建环境问题,非 PR 代码问题)。PR 作者自己的本地 CLI 验证展示了行为变化:

会话级切换(PR 代码):/model gpt-4osettings.json 不变 ✓
显式默认值切换(PR 代码):/model --default gpt-4osettings.json 更新为 model.name: gpt-4o

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a clean, well-scoped fix for a genuine UX bug. The /model command's description promises session-scoped switching, but the implementation silently persisted the default — exactly the kind of thing that frustrates users who accidentally overwrite their configured model.

The implementation matches what I'd have done independently: add --default, gate persistence behind it, keep auxiliary models unchanged, and require --default before scope flags. Every edit in the diff is necessary for the stated goal — no drive-by refactors, no scope creep.

The focused test suite (186 tests across 3 files) covers the new flag parsing, the conditional persistence, the mutual exclusion with auxiliary model flags, and the dialog threading. The author's own CLI verification shows the behavior working correctly end-to-end.

The breaking change is intentional and well-documented: users who relied on plain /model <id> to update their default must now use /model --default <id>. This is the right tradeoff — the command should do what it says.

Approving. ✅

中文说明

这是一个干净、范围明确的修复,解决了一个真实的 UX bug。/model 命令的描述承诺了会话级切换,但实现却默默持久化了默认模型——这正是那种让用户意外覆盖配置模型的问题。

实现方案与我独立构思的完全一致:添加 --default,在其后面加上持久化门控,保持辅助模型不变,要求 --default 才能使用作用域标志。diff 中的每一处编辑都是实现目标所必需的——没有顺手重构,没有范围蔓延。

聚焦测试套件(3 个文件中的 186 个测试)覆盖了新的标志解析、条件持久化、与辅助模型标志的互斥,以及对话框传递。作者自己的 CLI 验证展示了端到端行为的正确性。

破坏性变更是有意的且有充分记录的:之前依赖普通 /model <id> 更新默认值的用户现在需要使用 /model --default <id>。这是正确的取舍——命令应该做它所说的事情。

批准。✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot 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.

⚠️ Downgraded from Approve to Comment: CI failing: Test (ubuntu-latest, Node 22.x).

Two additional Critical findings could not be anchored to diff lines:

[Critical] --default silently dropped with inline prompt (modelCommand.ts:830-871): /model --default qwen-max some prompt silently ignores --default. The inline prompt path checks scopeOverride (rejecting it) but never checks persistDefault. Add a guard analogous to the scopeOverride check before the submit_prompt return.

— qwen3.7-max via Qwen Code /review

}

if (!settings) {
if (persistDefault && !settings) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Voice/fast/vision model !settings guard incorrectly relaxed.

persistDefault is always false here because --default --voice is rejected by the earlier mutual-exclusion guard. This makes persistDefault && !settings dead code. But persistSetting(settings, 'voiceModel', ...) is called unconditionally downstream — when settings is undefined, this crashes with a TypeError.

The original unconditional if (!settings) was correct here since voice/fast/vision models always persist regardless of --default.

Suggested change
if (persistDefault && !settings) {
if (!settings) {

— qwen3.7-max via Qwen Code /review

settings,
authType,
modelName,
persistDefault,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The caller's !settings guard at line 877 is unconditional, but switchMainModel only accesses settings inside if (persistDefault) blocks. A session-only /model <id> (no --default) in an environment where settings is unavailable will error with "Settings service not available" even though it no longer needs settings.

Suggested change
persistDefault,
if (persistDefault && !settings) {

— qwen3.7-max via Qwen Code /review


const result = await modelCommand.action!(mockContext, 'qwen-max');

expect(switchModel).toHaveBeenCalledWith(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Missing test coverage for several --default code paths:

  1. --default --fast / --default --voice / --default --vision rejection (mutual-exclusion guard at modelCommand.ts:468)
  2. Bare /model --default (no model, no scope) opening the dialog with { persistDefault: true }
  3. --default with auth-qualified model (e.g., openai:gpt-4) which exercises the security.auth.selectedType persistence branch

Suggested tests:

it('should reject --default combined with --fast', async () => {
  const ctx = setupContext();
  const result = await modelCommand.action!(ctx, '--default --fast');
  expect(result).toMatchObject({
    type: 'message',
    messageType: 'error',
    content: expect.stringContaining('--default only applies to the main model'),
  });
});

it('should open the model dialog with persistDefault for /model --default', async () => {
  const ctx = setupContext();
  const result = await modelCommand.action!(ctx, '--default');
  expect(result).toEqual({
    type: 'dialog',
    dialog: 'model',
    persistDefault: true,
  });
});

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Suggestions — commit a6e995d

File Issue Suggested fix
packages/cli/src/ui/commands/modelCommand.ts:465 vs ModelDialog.tsx:244-250 Confirmation message inconsistency across command and dialog paths. Command path shows (this project) for --default --project (missing "default"), and no suffix at all for session-only /model <id>. Dialog path shows (this project default) and (current session) respectively. Same user operations, different feedback. Use a shared formatPersistSuffix(scope, isDefault) helper, or align both paths to show (this project default) / (global default) / (current session) consistently.
packages/cli/src/ui/contexts/UIActionsContext.tsx:56-60 openModelDialog type missing persistScope and persistDefault params that useModelCommand and slashCommandProcessor both declare. Add persistScope?: 'workspace' | 'user' and persistDefault?: boolean to the context type.
packages/cli/src/ui/commands/modelCommand.ts:904 Non-interactive help text drops --project/--global documentation — old text listed scope flags, new text only mentions --default and --fast. Add "/model --default --project <id>" and "/model --default --global <id>" to the help string.
packages/cli/src/i18n/locales/*.js New i18n keys untranslated: ' (this project default)', ' (global default)', ' (current session)', 'Set Default Model', --default error messages, and completion description absent from most locale files (fr, ru, de, ca, pt, ja). Add translations for all non-English locale files.
packages/cli/src/ui/commands/modelCommand.ts:900 /model --default in non-interactive mode with no model id is a silent no-op — flag consumed with zero effect and no error. Return error: "--default requires a model id in non-interactive mode."
packages/cli/src/ui/commands/modelCommand.test.ts Missing test: bare /model --default (no scope flag, no model id) should open dialog with persistDefault: true. Add test asserting { type: 'dialog', dialog: 'model', persistDefault: true }.
packages/cli/src/ui/components/ModelDialog.test.tsx Missing test: persistDefault: true + persistScope: 'workspace' combination (the (this project default) suffix path). Add test rendering with both props and asserting suffix text.
packages/cli/src/ui/hooks/useModelCommand.ts:33-36 Missing test: modelDialogPersistDefault state tracking through openModelDialog({ persistDefault: true }) and closeModelDialog(). Existing test file has zero references to persistDefault. Add test asserting state defaults to false, is set to true on open, and resets on close.

— qwen3.7-max via Qwen Code /review

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

[Critical] --default silently dropped with inline prompt

/model --default qwen-max hello world parses persistDefault=true and extracts inlinePrompt="hello world", but the inline prompt path (around line 831) only checks scopeOverride to reject --project/--global — it never checks persistDefault. The function returns { type: 'submit_prompt', modelOverride: 'qwen-max', content: 'hello world' }, producing a one-shot override with no persistence and no error.

A user who runs /model --default gpt-4 explain this codebase believes they have both persisted the model as default AND sent the prompt. Neither happens — only the one-shot override runs. The user discovers the persistence didn't take effect only when they start a new session.

This is the same class of silent-ignore bug that the existing scopeOverride guard was explicitly added to prevent. The fix is to add an analogous persistDefault guard inside the if (inlinePrompt) block:

if (persistDefault) {
  return {
    type: 'message',
    messageType: 'error',
    content: t(
      "Cannot combine --default with an inline prompt. Run '/model --default {{model}}' first, then send your prompt.",
      { model: modelName },
    ),
  };
}

— qwen3.7-max via Qwen Code /review

@doudouOUC

Copy link
Copy Markdown
Collaborator

Suggestions — commit 1d6940f

File Issue Suggested fix
packages/cli/src/ui/commands/modelCommand.ts:465 Confirmation message suffix inconsistency: defaultSuffix = scopeSuffix || t(' (default)') drops the word "default" when a scope flag is present. /model --default --project qwen-max shows "Model: qwen-max (this project)" — indistinguishable from a non-default scoped switch. Meanwhile ModelDialog.tsx:243-249 correctly produces " (this project default)" / " (global default)". Replace the fallback chain with explicit branches: const defaultSuffix = persistDefault ? (scopeOverride === SettingScope.Workspace ? t(' (this project default)') : scopeOverride === SettingScope.User ? t(' (global default)') : t(' (default)')) : '';
packages/cli/src/ui/commands/modelCommand.ts:915 Bare /model --default (no model id, no scope flag) dialog path untested. Tests cover --default --project and --default --global dialog returns, but not the plain --default invocation that should return { type: 'dialog', dialog: 'model', persistDefault: true }. This is the most common interactive "set default" flow. Add test: it('should open dialog with persistDefault for bare /model --default', ...) asserting the dialog return includes persistDefault: true.

— qwen3.7-max via Qwen Code /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 — no new blockers beyond the three open Critical threads. Suggestion-level recommendations are in the Suggestion summary comment below.

@zjunothing

Copy link
Copy Markdown
Collaborator Author

Follow-up validation update

I pushed a small follow-up commit (c044a021b) to add the missing Simplified Chinese and Traditional Chinese translations for the updated /model command description. This addresses the strict-parity i18n coverage failure seen in the full local test run.

Validated locally with Node v22.23.1:

cd packages/cli
PATH="$HOME/.nvm/versions/node/v22.23.1/bin:$PATH" npx vitest run src/ui/commands/modelCommand.test.ts src/i18n/mustTranslateKeys.test.ts

Result: 2 passed, 90 tests passed.

Additional repository checks:

PATH="$HOME/.nvm/versions/node/v22.23.1/bin:$PATH" npm run build
PATH="$HOME/.nvm/versions/node/v22.23.1/bin:$PATH" npm run typecheck

Result: both passed.

Notes from broader local testing: npm test -- --runInBand was also attempted. The /model focused suite passed, but the full workspace run is not clean in this local environment due to unrelated environment/localization prerequisites, including mobile-mcp missing the local mobilecli binary/Android setup and several UI locale/env-sensitive tests. The i18n failure attributable to this PR is fixed by the follow-up commit above.

No screenshot is attached for this follow-up because the change is CLI command semantics plus locale coverage; the reproducible command transcript above is the relevant evidence.

中文验证报告

已推送一个小的跟进提交(c044a021b),为更新后的 /model 命令描述补齐简体中文和繁体中文翻译,修复本地全量测试中暴露的 strict-parity i18n 覆盖失败。

本地使用 Node v22.23.1 验证:

cd packages/cli
PATH="$HOME/.nvm/versions/node/v22.23.1/bin:$PATH" npx vitest run src/ui/commands/modelCommand.test.ts src/i18n/mustTranslateKeys.test.ts

结果:2 passed90 tests passed

额外仓库检查:

PATH="$HOME/.nvm/versions/node/v22.23.1/bin:$PATH" npm run build
PATH="$HOME/.nvm/versions/node/v22.23.1/bin:$PATH" npm run typecheck

结果:均通过。

全量测试说明:也尝试运行了 npm test -- --runInBand。其中 /model 相关 focused suite 通过;完整 workspace 测试在本机环境仍不干净,剩余失败来自无关的环境/本地化前置条件,例如 mobile-mcp 缺少本地 mobilecli 二进制/Android 环境,以及若干 UI locale/env 敏感测试。本 PR 可归因的 i18n 失败已经由上述跟进提交修复。

本次跟进没有截图,因为改动是 CLI 命令语义和 locale 覆盖;上面的可复现命令输出是更直接的验证证据。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no new blockers beyond the three open Critical threads. Suggestion-level recommendations are in the Suggestion summary comment below.

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

No review findings. Downgraded from Approve to Comment: CI failing: Test (ubuntu-latest, Node 22.x).

— qwen3.7-max via Qwen Code /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 — no new blockers beyond the three open Critical threads. Suggestion-level recommendations are in the Suggestion summary comment below.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no new blockers beyond the three open Critical threads. Suggestion-level recommendations are in the Suggestion summary comment below.

@wenshao

wenshao commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — merge reference

I built and exercised this PR locally against a real CLI bundle (not just unit tests) to confirm the behavior change. Everything checks out — recommend merge.

  • PR branch: fix/model-session-default @ a6e995d21 (already merged with main)
  • Compared against: main @ b2601c861, built the same way, to capture the genuine before behavior
  • Machine: macOS (Darwin 24.6.0), Node.js v22.23.1, fresh npm ci

Checks

Check Result
npx vitest run modelCommand.test.ts ModelDialog.test.tsx slashCommandProcessor.test.ts 186 passed (3 files)
npm run typecheck ✅ pass
npm run build ✅ pass
npm run bundle ✅ pass
Manual CLI scenarios (real bundle, temp QWEN_HOME) 8 / 8 pass

The fix, before → after

To make sure this is a real behavioral change and not just a test-only diff, I reproduced issue #4331 on main and then re-ran the identical flow on this branch, using a throwaway QWEN_HOME whose settings.json default was gpt-4o-mini.

before vs after

  • On main (before): a plain /model gpt-4o session switch silently rewrote model.namegpt-4o on disk. That temporary switch became the persistent default for every future session — exactly the leak described in /model should switch only the current session unless the user explicitly sets a default #4331.
  • On this PR (after): /model gpt-4o switches the live session only; settings.json stays gpt-4o-mini. Persisting now requires the explicit /model --default gpt-4o, which correctly writes model.namegpt-4o.

I also confirmed the no-leak end-to-end: after a session-scoped /model gpt-4o, a brand-new process still reports Current model: gpt-4o-mini.

Full scenario matrix

verification matrix

Notable points I specifically checked:

  • Guard rails work: /model --project and /model --global without --default are rejected with a clear message, and /model --default --fast is rejected (--default only applies to the main model).
  • No regression on auxiliary models: /model --fast <id> still persists fastModel as before — only the main model switch became session-scoped.
  • Scope suffixes are correct: --default(default), --default --global(global).
  • Help/hint text updated to advertise --default.

How to reproduce

git fetch origin pull/6579/head && git checkout FETCH_HEAD
npm ci && npm run bundle
export QWEN_HOME=$(mktemp -d) OPENAI_API_KEY=sk-dummy
cat > "$QWEN_HOME/settings.json" <<'JSON'
{ "security": { "auth": { "selectedType": "openai" } },
  "model": { "name": "gpt-4o-mini" },
  "modelProviders": { "openai": [
    { "id": "gpt-4o", "envKey": "OPENAI_API_KEY", "baseUrl": "https://api.openai.com/v1" },
    { "id": "gpt-4o-mini", "envKey": "OPENAI_API_KEY", "baseUrl": "https://api.openai.com/v1" } ] } }
JSON
node dist/cli.js -p "/model gpt-4o"            # session only  -> settings.json unchanged
node dist/cli.js -p "/model --default gpt-4o"  # opt-in persist -> model.name = gpt-4o

Verdict

The change does exactly what the description says, is well guarded, keeps auxiliary-model behavior intact, and is covered by the updated unit tests. LGTM ✅

中文版本(点击展开)

✅ 维护者本地验证 —— 合并参考

我在本地对着真实的 CLI 产物(不仅仅是单元测试)构建并实际运行了这个 PR,确认行为变更符合预期。全部通过 —— 建议合并。

  • PR 分支: fix/model-session-default @ a6e995d21(已与 main 合并)
  • 对比基线: main @ b2601c861,用相同方式构建,以捕获真实的“修改前”行为
  • 环境: macOS(Darwin 24.6.0),Node.js v22.23.1,全新 npm ci

检查项

检查 结果
npx vitest run modelCommand.test.ts ModelDialog.test.tsx slashCommandProcessor.test.ts 186 通过(3 个文件)
npm run typecheck ✅ 通过
npm run build ✅ 通过
npm run bundle ✅ 通过
手工 CLI 场景(真实产物,临时 QWEN_HOME 8 / 8 通过

修复效果:修改前 → 修改后

为确认这是真实的行为变更、而不只是测试层面的改动,我先在 main 上复现了 issue #4331,然后在本分支上跑了完全相同的流程;使用一个临时 QWEN_HOME,其 settings.json 默认值为 gpt-4o-mini

(截图见上方英文部分)

  • main(修改前): 普通的 /model gpt-4o 会话切换会悄悄把磁盘上的 model.name 改写为 gpt-4o。这个临时切换变成了后续所有会话的持久默认值 —— 正是 /model should switch only the current session unless the user explicitly sets a default #4331 描述的泄漏问题。
  • 本 PR(修改后): /model gpt-4o 只切换当前会话;settings.json 保持 gpt-4o-mini 不变。要持久化必须显式使用 /model --default gpt-4o,此时才会正确写入 model.namegpt-4o

我还端到端确认了“无泄漏”:在一次会话级 /model gpt-4o 之后,全新的进程仍然报告 Current model: gpt-4o-mini

完整场景矩阵

(截图见上方英文部分)

我特别检查的要点:

  • 保护逻辑生效: 不带 --default/model --project/model --global 会被清晰地拒绝;/model --default --fast 也会被拒绝(--default 只对主模型生效)。
  • 辅助模型无回归: /model --fast <id> 仍然像以前一样持久化 fastModel —— 只有主模型切换变成了会话级。
  • 作用域后缀正确: --default(default)--default --global(global)
  • 帮助/提示文案 已更新以提示 --default

复现方式

git fetch origin pull/6579/head && git checkout FETCH_HEAD
npm ci && npm run bundle
export QWEN_HOME=$(mktemp -d) OPENAI_API_KEY=sk-dummy
# settings.json: model.name = gpt-4o-mini + openai modelProviders (gpt-4o / gpt-4o-mini)
node dist/cli.js -p "/model gpt-4o"            # 仅会话   -> settings.json 不变
node dist/cli.js -p "/model --default gpt-4o"  # 显式持久化 -> model.name = gpt-4o

结论

改动完全符合描述,保护逻辑完善,辅助模型行为保持不变,并有更新后的单元测试覆盖。LGTM ✅

wenshao
wenshao previously approved these changes Jul 11, 2026

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

// undefined write is dropped from JSON and would not override on merge).
persistSetting(settings, 'model.baseUrl', '', scopeOverride);
if (persistDefault) {
if (!settings) {

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] Dead code: the if (!settings) { throw new Error(...) } guards inside the if (persistDefault) blocks (lines 137-139 and 155-157) are currently unreachable. The sole caller at line 877 unconditionally checks if (!settings) before calling switchMainModel, so the function is never reached with undefined settings.

If the caller's guard is fixed to if (persistDefault && !settings) (as suggested for the open Critical at line 877/889), these internal guards become necessary — they align with the function's declared contract (settings: LoadedSettings | undefined). Consider keeping them as defensive code with a comment, or removing them and narrowing the parameter type to LoadedSettings if the caller's unconditional guard is the intended final design.

— qwen3.7-max via Qwen Code /review

@@ -31,13 +33,16 @@ export const useModelCommand = (): UseModelCommandReturn => {
const [modelDialogPersistScope, setModelDialogPersistScope] = useState<

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 hook gained modelDialogPersistDefault state and a persistDefault option on openModelDialog(), plus reset logic in closeModelDialog(). The existing test file has no coverage for: (a) openModelDialog({ persistDefault: true }) setting modelDialogPersistDefault to true, (b) closeModelDialog() resetting it to false, or (c) it defaulting to false. If this state wiring breaks, the dialog would silently lose its persist-default behavior.

Similarly, ModelDialog.tsx has no test for the title changing to "Set Default Model" when persistDefault is true (line 880), or for the (current session) confirmation suffix for non-persist selections (lines 241-249).

— qwen3.7-max via Qwen Code /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 — no blockers. Suggestions are inline.

@@ -852,7 +904,7 @@ export const modelCommand: SlashCommand = {
type: 'message',

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] Non-interactive /model --default without a model ID silently falls through to the "Current model" info message. The --default flag is ignored with no error or guidance. In CI/CD or scripted use, a user expecting --default to take effect gets no indication it was dropped.

Consider adding a guard before the non-interactive fallback:

if (persistDefault) {
  return {
    type: 'message',
    messageType: 'error',
    content: t('--default requires a model ID in non-interactive mode. Use: /model --default <model-id>'),
  };
}

— qwen3.7-max via Qwen Code /review

type: 'message',
messageType: 'info',
content: t('Model') + ': ' + effectiveModelName + scopeSuffix,
content: t('Model') + ': ' + effectiveModelName + defaultSuffix,

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 CLI confirmation for /model <id> shows Model: qwen-max with no suffix indicating the change is session-only. The model dialog correctly shows "(current session)" for the same scenario. Users migrating from the old persistent behavior get no visual feedback that their switch is ephemeral.

Consider adding a session-only suffix:

const defaultSuffix = persistDefault
  ? scopeSuffix || t(' (default)')
  : t(' (this session)');

— qwen3.7-max via Qwen Code /review

@@ -434,6 +462,19 @@ export const modelCommand: SlashCommand = {
: scopeOverride === SettingScope.User
? t(' (global)')

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] Inconsistent confirmation labels between command and dialog paths. The command path shows (this project) / (global) while the dialog shows (this project default) / (global default) for the same operation with the same flags. Users see different messages depending on which UI path they take.

Consider aligning the command path labels with the dialog:

const defaultSuffix = persistDefault
  ? scopeOverride === SettingScope.Workspace
    ? t(' (this project default)')
    : scopeOverride === SettingScope.User
      ? t(' (global default)')
      : t(' (default)')
  : '';

— qwen3.7-max via Qwen Code /review

: '';
const defaultSuffix = persistDefault ? scopeSuffix || t(' (default)') : '';
if (
persistDefault &&

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 mutual-exclusion guard rejecting --default combined with --fast, --voice, or --vision has no test coverage. If the regex or condition logic were wrong (e.g., accidentally matching --default-model or missing --vision), no test would catch it.

Consider adding tests for each rejected combination:

it('should reject --default --fast', async () => { ... });
it('should reject --default --voice', async () => { ... });
it('should reject --default --vision', async () => { ... });

— qwen3.7-max via Qwen Code /review

@@ -835,12 +886,13 @@ export const modelCommand: SlashCommand = {
settings,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] --default is silently dropped when combined with an inline prompt. /model --default qwen-max "hello" parses persistDefault=true, but the inline-prompt branch (line ~830) only rejects scopeOverride — there is no equivalent check for persistDefault. The function returns submit_prompt before reaching switchMainModel, so the persistence intent is silently discarded.

This is the same category of silent flag drop that the scopeOverride guard (line 830) was designed to prevent. Add a persistDefault check inside the if (inlinePrompt) block:

Suggested change
settings,
if (persistDefault) {
return {
type: 'message',
messageType: 'error',
content: t(
"Cannot combine --default with an inline prompt. Run '/model --default {{model}}' first, then send your prompt.",
{ model: modelName },
),
};
}
if (scopeOverride) {

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao dismissed their stale review July 12, 2026 05:37

critical comment

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — no blockers. Suggestions are inline.

@@ -1419,6 +1419,8 @@ export default {
'切换此会话的模型(--fast 可设置建议模型,--voice 可设置语音转写模型,[model-id] 可立即切换)',

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] ~10 new t() strings introduced by this PR are untranslated in both zh.js and zh-TW.js. Affected keys include: 'Set Default Model', ' (default)', ' (this project default)', ' (global default)', ' (current session)', '--default only applies to the main model. Use --fast, --voice, or --vision without --default.', 'Use --default with --project or --global when persisting the main model.', 'Settings service not available.', and the updated help text referencing --default. Chinese-speaking users will see raw English fallback strings for these keys.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

CI Failure Analysis

Run: https://github.com/QwenLM/qwen-code/actions/runs/29181441191

Root Cause

The PR adds a --default flag to the /model command, which changes the English source description string in modelCommand.ts. The new key (with --default) was added to zh.js and zh-TW.js, but the old key (without --default) was left behind. Since en.js no longer contains the old key, the i18n checker reports them as "extra keys" — translations in locale files with no matching English source.

Failing Step

Run i18n check (npm run check-i18n)

❌ Errors:
  - Extra key in zh-TW.js (not in en.js): "Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion)."
  - Extra key in zh.js (not in en.js): "Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion)."

Fix

Remove the old translation key (the one without --default) from both locale files:

  • packages/cli/src/i18n/locales/zh.js — remove the line starting with 'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings,
  • packages/cli/src/i18n/locales/zh-TW.js — remove the same key

The new key (with --default to persist the main model for future sessions) should be kept as-is.

Summary

Simple stale-key issue: the PR correctly added the new translation but forgot to delete the old one that was replaced when --default was inserted into the description string.


🇨🇳 中文分析

CI 失败分析

运行记录: https://github.com/QwenLM/qwen-code/actions/runs/29181441191

根本原因

PR 为 /model 命令新增了 --default 标志,导致 modelCommand.ts 中的英文描述字符串发生变化。新的 key(包含 --default)已添加到 zh.jszh-TW.js,但旧的 key(不含 --default)被遗留了下来。由于 en.js 中不再包含旧 key,i18n 检查脚本将其报告为"多余条目"——即 locale 文件中存在但英文源文件中不存在的翻译。

失败的步骤

Run i18n check (npm run check-i18n)

❌ 错误:
  - zh-TW.js 中存在多余 key(不在 en.js 中): "Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately, or [model-id] [prompt] to run a one-off prompt on another model; the inline prompt is sent verbatim without @file expansion)."
  - zh.js 中存在多余 key(不在 en.js 中): 同上

修复方式

从两个 locale 文件中删除旧的翻译 key(不含 --default 的那个):

  • packages/cli/src/i18n/locales/zh.js — 删除以 'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, 开头的行及其对应的翻译值
  • packages/cli/src/i18n/locales/zh-TW.js — 同上

新的 key(包含 --default to persist the main model for future sessions)保持不变。

总结

典型的残留 key 问题:PR 正确添加了新翻译,但忘记删除旧翻译——旧翻译在 --default 插入描述字符串后已被替代。

@wenshao

wenshao commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

✅ 双跑验证报告 (BASE vs PR)

真实 TUI(tmux)对 merge-base 5b4d0a3 与 PR head 60cc536 各自构建的 CLI 做 A/B。观测点是文件系统上的 settings.json,不靠肉眼判断 TUI。

环境:隔离 HOME + 隔离项目目录,起始 model.name = qwen3-coder-plus,modelProviders 配置 qwen3-coder-plus / qwen3-coder-flash 两个模型。

结果

# 构建 操作 会话内生效 user settings.json 结论
A BASE /model qwen3-coder-flash ✅ flash plus → flash bug 复现:普通切换悄悄改写了用户全局默认值
B PR /model qwen3-coder-flash ✅ flash plus(未变) ✅ 修复生效:仅会话内
C PR /model --default qwen3-coder-flash ✅ flash plus → flash ✅ 无回归:持久化仍可用
D PR /model --default --project qwen3-coder-flash ✅ flash plus(未变);项目 settings → flash ✅ scope 路由正确

A 是这个 PR 的价值证明:在 BASE 上,用户只是想临时换个模型,结果永久改掉了自己的全局默认配置。B/C/D 证明修复精确命中,且没有把持久化能力弄坏。

一处未能覆盖(如实说明)

模型选择器(picker)路径未验证。 PR 描述称「ordinary model-picker selections only update the active session」,但在我的 modelProviders 环境下,picker 选择 qwen3-coder-flashBASE 和 PR 上都失败,报同一个错:

✕ Failed to switch model to 'qwen3-coder-flash'.
  Model 'qwen3-coder-flash' not found for authType 'openai'

两边行为一致,所以这不是 PR 引入的回归 —— 看起来 picker 的模型查找走的是 auth-type 注册表而非 modelProviders,是我的测试环境不适用于该路径。

只跑 PR 单边的话,我会把这个错误误报成回归。 这正是双跑的意义。但也意味着 picker 这半条入口没有被我的验证覆盖,建议作者或 reviewer 用一个 picker 能正常工作的配置补验一次。

复现方法

# 两侧各自构建后:
HOME=<隔离home> OPENAI_API_KEY=sk-dummy node <dist>/cli.js
# TUI 内输入 /model qwen3-coder-flash
# 然后 diff <隔离home>/.qwen/settings.json

验证方式:tmux 驱动真实 TUI,BASE/PR 双跑对照,观测 settings.json 实际写入。

@yiliang114 yiliang114 added this to the v1.0.0 milestone Jul 14, 2026
doudouOUC pushed a commit to doudouOUC/qwen-code that referenced this pull request Jul 15, 2026
… compute the verdict (QwenLM#6892)

* fix(review): prove coverage on both topologies, and check the prompt survived the trip

Three defects, all measured against the harness's own transcripts of real /review
runs against QwenLM/qwen-code PRs QwenLM#6766 (Step 3B) and QwenLM#6579 (Step 3A).

1. Step 3A reviews were told nobody had read them.

   Coverage was attributed by one question: did an agent whose launch prompt says
   `chunk N of M` make a successful tool call? No Step 3A prompt says that — there
   every dimension agent walks the whole diff — so no chunk was ever attributed to
   anyone. Run against a real 3A review whose twelve agents each opened the diff,
   walked both chunks and filed findings, check-coverage returned

     Coverage: 0/2 chunk(s) reviewed. 16 agent(s) ran; 16 did work
     ERROR: 2 chunk(s) were not reviewed — 1, 2. Nobody read those lines.

   in one breath. compose-review runs the same computation on the way to the
   verdict, so a flawless small-PR review was capped away from Approve and the
   body it would have POSTED to the pull request said nobody had read it. Step 3A
   is the topology most pull requests get. The only reason it never blew up is
   that check-coverage lived inside Step 3B and was never reached from 3A — two
   bugs cancelling.

   Coverage is now the intersection of two things the harness wrote down: the
   lines each agent was pointed at (its launch prompt) and the fact that it opened
   the diff (a successful tool call naming the diff file). Topology-blind. It also
   no longer credits a chunk to an agent on the strength of any successful call —
   a glob for test files was enough.

2. The whole-diff agents were still launched blind.

   agent-prompt built the territory agents' prompts and left the other half of the
   fan-out to prose. All three whole-diff agents of the 3B run — cross-file tracer,
   test-coverage matrix, build & test — got a prompt naming no diff file at all.
   The test-coverage matrix was told to "Read the diff chunks" and given no path to
   read them from; it read the post-change source instead, which on a deletion
   shows it nothing. These agents own the classes a chunk agent is structurally
   blind to, and the gate could not see it: it only asked that question of agents
   whose prompt said `chunk N of M`.

   `agent-prompt --whole-diff` now builds their diff-reading block too.

3. The prompt the CLI built was rewritten on the way to the agent.

   The 3B run invoked agent-prompt correctly for all five chunks and then
   paraphrased what it printed: the delivered prompt dropped the rule against
   reciting a stock sentence, dropped the half-read warning, replaced the project's
   review rules with a three-sentence summary of its own, and invented an
   instruction that was never in the original. Nothing could see it, because a
   paraphrase keeps the diff path. So agent-prompt records what it emitted, at a
   path derived from the plan that the caller is never given; check-coverage reads
   it back against the launch prompt the harness recorded. Replayed against that
   run's real transcripts, all five chunk agents are now named.

Verified end to end: a fresh /review of QwenLM#6829 (3A) calls `agent-prompt
--whole-diff`, passes it verbatim, and Step 3D reports 2/2 chunks reviewed,
12 agents, 12 did work — a gate that path could not reach before, and could not
have passed if it had.

* fix(review): build every agent's prompt, from a roster the plan derives

Two more failures, both measured against the harness's own transcripts of real
/review runs.

1. Agent 0 was never launched, and nothing could tell.

   The skill says issue fidelity runs on every PR review. Dogfooded on QwenLM#6766, it
   did not run — and every check passed, because every check asks a question of an
   agent that RAN. An agent that does not run leaves no transcript to ask. An
   omission is invisible precisely because it is an omission.

   So `check-coverage` now derives a roster from the plan — which the caller does
   not write — and names every required agent that never ran, with the exact
   `agent-prompt` call that builds it. The plan already knew everything the roster
   turns on: the topology, whether the diff deletes anything, which files were
   rewritten heavily enough to need invariant agents, whether there is a worktree
   to build in and a pull request to check an issue against.

   `agent-prompt --role <role>` builds all of them: 0, 1a, 1b, 1c, 2, 3, 4, 5,
   6a/6b/6c, 7, the test matrix, and the three invariant agents per heavy file.
   The briefs move out of SKILL.md and into code, because a brief the orchestrator
   retypes is a brief that drifts.

2. A 4 652-character prompt is not a thing an orchestrator will paste twelve times.

   With the briefs welded into the launch prompt, the first dogfood delivered
   **2 893** characters of one: it kept the head, added a preamble of its own, and
   cut nineteen hundred characters out of the middle. The delivery check caught it
   — and the run then read the check's exit-3, concluded "the agents clearly did
   their job", skipped `compose-review`, and filed an **Approve it had written
   itself**. A gate that always fails is a gate that gets talked around.

   So the brief goes where the diff already goes: on disk, read by the agent that
   needs it. The launch prompt drops to ~500-800 characters — it names the role,
   points at the brief file, and lists the diff reads — and whether the agent
   actually read its brief stops being a hope and becomes a tool call the harness
   wrote down (`unreadBriefs`).

Verified end to end. A fresh /review of QwenLM#6847 built all twelve role prompts, and
against the harness's transcripts: 12 of 12 delivered **byte-for-byte verbatim**,
12 of 12 **opened their brief**, 3-19 successful tool calls each — Agent 0 among
them. Step 3D: `1/1 chunks reviewed, 12/12 agents did work`, no errors.

* fix(review): the verdict is computed, not carried

`compose-review` has computed the event and the body since the C/S table stopped
being prose. The skill then told the orchestrator to "copy event/body verbatim
into the review JSON" — a transcription, into a document the model writes, of a
decision the CLI had already made. That is the exact anti-pattern `submit`'s own
header repudiates, and it left two ways for a run to author its own verdict:

  - **The terminal.** Step 6's verdict was composed by the model, from prose
    rules. Dogfooded, a run read the coverage check's refusal, concluded that
    "the agents clearly did their job", never called `compose-review` at all, and
    printed `Review complete — Approve` on a review whose gate had just refused.

  - **The wire.** `submit` took `{event, body}` as fields. Nothing stopped a run
    that had skipped the computation from posting the conclusion it preferred.

So `submit` composes. It takes the findings — the inline comments and the states
Step 6 established — and derives everything that follows, including how many
blockers there are: `criticalsInline` and `suggestionsInline` are counted off the
`**[Critical]**` / `**[Suggestion]**` prefixes of the comments actually attached,
not accepted as numbers beside them. (A number beside a list is a number that can
disagree with the list, and one did: the breaching run posted a body reading
"Suggestions are inline" next to an empty `comments` array and a summary claiming
`0 Suggestion inline`.) A payload carrying `event`/`body` is refused rather than
silently overruled — the caller was trying to author a verdict.

Two body checks are deleted, not weakened: a body that promises inline comments it
does not carry, and a body whose footer is preceded by a literal `\n`. Both were
checks on a string the caller built. The caller no longer builds it.

`compose-review` now prints the verdict line itself, and Step 6 prints that. There
is one place a verdict exists; skipping the command does not get you a different
one, it gets you none.

Verified: a payload with `event: APPROVE` and an unreviewed dimension is refused
at the wire; the same findings without a verdict compose to `COMMENT`. A fresh
/review of QwenLM#6788 called `compose-review`, was told `Verdict: Comment`, and showed
the user "Comment — downgraded from Approve (CI failing: route)" — the presubmit
downgrade applied by code, on a run that did not post.

* fix(review): make Step 3B carryable, and stop the delivery check crying wolf

Dogfooding the Step 3B path — the one topology none of this had been run against
— found two defects, and the second is the more important of the two.

1. Eighty-seven kilobytes of chunk prompts, in one response.

   The briefs moved onto disk for the dimension agents and not for the territory
   agents. Measured on PR QwenLM#6606 (5 511 diff lines, 17 chunks): 17 chunk launch
   prompts of ~5 149 characters each — **87 546 characters** the orchestrator was
   expected to paste unedited. At a twelfth of that load it had already cut
   nineteen hundred characters out of a single prompt.

   Chunk agents get the same split: the brief on disk, and a launch prompt that
   carries only what cannot live anywhere else — `chunk N of M`, which attributes
   the territory, and the `offset`/`limit`, which are the lines coverage proves
   were delivered. 87 546 → 14 789 characters. And `check-coverage` now asks the
   territory agents the same question it asks the others: did you open your brief?

2. The delivery check failed a correct run — all nine agents of it.

   It was a substring test: the built prompt had to appear in the launch prompt,
   contiguously. That is a stricter claim than the skill makes, and both of the
   differences it fired on were legitimate. The orchestrator had inserted **the
   one-sentence summary of the change that the skill explicitly tells it to add**,
   which breaks contiguity by construction — and it had reflowed a hard-wrapped
   sentence onto one line, which changes not one character of meaning.

   This is the failure this skill keeps re-learning, and this time it was ours: a
   gate that fires on a correct run is a gate that gets talked around, and there is
   a dogfood transcript of a model doing exactly that. The rule the check enforces
   is now the rule the skill states — **you may add; you may not remove, alter, or
   reorder** — over whitespace-collapsed lines, in order.

Verified against the harness's transcripts of a real Step 3B review of QwenLM#6766: nine
agents (five chunks, issue fidelity, cross-file tracer, test matrix, build & test),
9/9 delivered intact, 9/9 opened their brief, 6-22 successful tool calls each.
`check-coverage`: 5/5 chunks, every list empty, exit 0.

* feat(review): path-scoped rules; and move the briefs out of the skill, where they were never reaching the agents

Two changes, and the second found a hole the first would not have.

1. Rules that attach to a path, not to a dimension.

   The nine dimensions are domain-blind by design — "find security bugs" is a lens,
   not a syllabus — and that holds until a file's failure modes are not guessable
   from reading it. A GitHub Actions workflow is the clearest case: it is YAML, so it
   reads as configuration, and the reviewer who treats it as configuration misses
   every one of its attack classes. Nothing in this review knew to ask whether a
   `pull_request_target` job checks out the contributor's head — which is the
   difference between a CI file and a remote code execution with the repository's
   write token. This repo runs `qwen-autofix.yml`, which posts to pull requests.

   `agent-prompt` now appends a checklist for such a file to the brief of every
   code-reviewing agent **whose territory actually contains one**. Scoped, because a
   rule that fires on every review is a rule that gets skimmed. `/review` runs on
   other people's repositories, so the calibration matters as much as the content:
   the blockers are the six that are unambiguously wrong; the two that shade into
   taste (SHA-pinning, `permissions:`) are Suggestions, exempt the conventions almost
   everyone keeps, and are scoped to lines the diff touches. No style rules — a
   linter owns those, and the Exclusion Criteria already forbid them.

2. The briefs move out of SKILL.md — and three things turned out never to have
   reached an agent at all.

   The briefs have been built in code since the roster landed, and SKILL.md still
   carried 38 KB of the same prose. Duplication is drift, and a 178 KB skill is
   ~45 000 tokens in the orchestrator's context on every review — which is itself a
   cause of the failure this whole line of work has been chasing. The skill now keeps
   what each agent is *for* (a table) and drops what it is *sent* (the command's copy
   is the one that arrives).

   Doing that surfaced what the code briefs were missing, because the deleted prose
   had to go somewhere:

   - **The Exclusion Criteria had never reached an agent.** The skill states them at
     the end of the document and tells the orchestrator to "apply" them. The agents
     do not read the document. The single largest precision control in this review
     has been governing nobody, in every run, since it was written.
   - **Nor had the anchor rules.** Agents were asked for a snippet and never told
     what makes one resolvable: prefer added lines, a removed line cannot be anchored
     at all, a bare `}` matches everywhere. `resolve-anchors` was downstream of a
     snippet nobody had given the rules to produce.
   - **Nor the severity calibration.** `SEVERITY`'s own comment warns that a chunk
     agent owns test coverage with nothing to calibrate it and will file "zero test
     coverage" as Critical — and then did not include the calibration.

   All three are in the briefs now. And two degradations the orchestrator used to be
   told to add by hand — and can no longer add, because it does not write these
   prompts — are applied by the builder: in cross-repo lightweight mode there is no
   tree, so 1b and 1c report at `Confidence: low` rather than asserting a
   re-establishment is missing. A false Critical blocks a merge.

   Step 3C (the medium-effort inline pass) now *loads* the briefs it needs rather
   than carrying them: same text as the high-effort agents get, read when that level
   actually runs instead of sitting in every review's context.

SKILL.md: 171 178 → 153 656 bytes.

* fix(review): scope an invariant agent to its own file, and let a blocker's blast radius be part of the blocker

Two corrections, both from dogfooding the paths that had never been run.

1. An invariant agent was being handed the whole chunk plan.

   It owns one heavily-rewritten file. Its brief says so, and gives it that file's
   own slice of the diff. Its *launch prompt* listed every chunk in the review —
   on PR QwenLM#6457, all twenty-one reads of a 6 149-line diff, for an agent whose job
   is one file.

   The wasted reading is the smaller half. Coverage is computed from the ranges in
   the launch prompt, so an invariant agent was being credited with having read
   **every chunk in the review**. One of them could have masked twenty missing chunk
   agents. It now gets exactly its file's `diffRange`, and nothing else.

2. `permissions: write-all` on a job that runs untrusted code is not a Suggestion.

   The path rule said it was. Dogfooded against a planted vulnerability, the security
   agent read that and escalated anyway: "grants maximum token scope to a job that
   processes untrusted contributor code, amplifying the RCE above". It was right and
   the flat rule was too coarse. A broad token on a privileged job is not a separate
   recommendation — it is how far the blocker reaches, and it belongs in that finding,
   at Critical. On an ordinary job it stays a Suggestion.

* fix(review): the six findings this skill filed against its own pull request

The repository's own `/review` bot reviewed QwenLM#6892 — this change reviewing the code
that changes it — and filed six Suggestions. Every one of them is real, and two are
fail-open holes in the gates this pull request exists to build. They are fixed here,
each with a test.

- **`submit` accepted `state: null`.** `=== undefined` is not `== null`, so the
  structural check passed it; `compose`'s `?? {}` then collapsed it to an empty state
  and would have posted a review whose footer named no model and whose caps came from
  nowhere.

- **`wasDeliveredVerbatim` was vacuously true for an empty `built` prompt.** A
  zero-byte record is what a partial write leaves behind — and `recordPrompt` swallows
  its write errors by design, so this is reachable. `readRecordedPrompts` stores it as
  `''`, not `undefined`, so the "no prompt was built" guard did not catch it, and the
  loop's body never executed. The roster would have credited a required role to
  whichever transcript it looked at first. It now fails closed.

- **A chunk read across two pages got no credit.** The check asked for a *single*
  range containing the chunk, and reads of 1-200 and 201-400 are two — so it
  contradicted the paging instruction the same review had just given, on exactly the
  oversized chunks where paging is not optional. Ranges are coalesced first.

- **Agent 7 was handed relative paths it could not resolve.** `worktreePath` and the
  plan path are repo-relative in the report, and Agent 7's working directory *is* the
  worktree — so `--worktree .qwen/tmp/review-pr-6457` resolved to
  `<worktree>/.qwen/tmp/review-pr-6457`, which does not exist. This was already
  visible and nobody had read it: in the 29-agent dogfood run, Agent 7 spent its time
  running `find … -name "*6457*fetch*"`, hunting for a plan it had been handed a path
  to. Absolute now.

- **`removePromptRecord` was dead code with a comment claiming a caller it did not
  have.** `cleanup.ts` sweeps the prompt directory by prefix instead. Deleted.

- **`--dry-run` omitted `cappedBy`.** The point of a dry run is to see what would be
  posted; `"event": "COMMENT"` with no reason leaves the reader to guess why the
  Approve went away.

* docs(review): purge the stale event/body payload examples from Step 7

The reviewer caught a real contradiction it filed as Critical: submit.ts now
refuses a payload carrying `event`/`body` (those are computed from `state` and
the attached comments), but Step 7's main-path 'Build the review JSON' examples
still showed `"event": "REQUEST_CHANGES"` / `"body"` and routed the verdict
through a copy-it-verbatim step. An orchestrator following the unchanged
instructions would have built exactly the payload submit rejects.

The correct `{commit_id, comments, state}` shape existed lower in the section (the
no-findings branch), added when submit took over composition — but the main-path
examples and the compose-review-then-transcribe bullets above them were never
reconciled. They are now: one payload shape, no verdict in it, `state` handed to
submit, and the inline counts derived from the comments rather than supplied.

Found by the repository's own /review on QwenLM#6892.

* fix(review): the three findings from the third self-review

The repository's /review passed QwenLM#6892 (no blockers) and filed three Suggestions.
All three are real; two are contradictions this PR itself introduced.

- **verdictLine printed a dangling colon.** When a would-be Approve was taken away
  by a presubmit downgrade ALONE — no cap state, `cappedBy` empty, `downgraded`
  true — the code joined the empty array and produced 'an Approve was NOT
  available:  — downgraded by a presubmit check', a colon over nothing. It now
  collects the reasons (a cap and a downgrade are both reasons, either can be the
  only one) and prints the clause only when there is a reason to. The function had
  no test; it has six now, including this case.

- **Step 3D said 'six failures' and listed seven, while check-coverage reports
  eight.** The count drifted as failure classes were added, and the uncoverable-chunk
  class had no bullet at all. Now 'eight', with the missing bullet written.

- **`submit --review` help still advertised `event` / `body`** as payload fields,
  which the same command now refuses. Updated to `commit_id / comments / state`.

Found by the repository's own /review on QwenLM#6892 — the third pass, the one that
turned CHANGES_REQUESTED into no-blockers.

* fix(review): a heavy file in a Step-3A diff must not demand invariant agents

From a human review of QwenLM#6892 (doudouOUC). `heavy` is decided independently of
topology (lib/heavy.ts): a ~300-line source file with ~120 changed lines clears
the rewrite-ratio branch while srcDiffLines stays under 500 — a Step 3A review.
The invariant-agent loop in requiredAgents ran in both topologies, so it added
invariant-a/b/c to the roster of a 3A review that never launches them; check-coverage
then reported them as missingRoles and exit-3'd, and compose-review capped the
verdict — an otherwise-complete small PR, falsely blocked.

Gate the loop on isTerritoryFanOut. Step 3A's dimension agents each walk the whole
diff, so one already sees both ends of a rewritten file; invariant agents are a 3B
mechanism for when the diff is carved into territories and no single agent holds
the whole file. roster.test.ts now pins the 3A-heavy case.

Also, same review: merge() in coverage.ts copied its first tuple and pushes copies,
so it no longer mutates a tuple owned by rec.diffReads (harmless today, pure now).

* fix(review): a downgraded Request changes must not read as a plain Comment

Fifth self-review, one behavioural finding among five (the rest are test/doc).

verdictLine printed 'Comment — downgraded by a presubmit check' for BOTH a
Suggestion-only Comment the presubmit moved and a REQUEST_CHANGES it moved down to
Comment. The second is a review with confirmed Criticals posted inline, and
'Comment — downgraded' reads to an operator as 'nothing blocking'. It could not
tell them apart from baseEvent alone — a cap may already have softened the RC
before the downgrade ran — so ComposeReviewResult now carries downgradedFrom, and
verdictLine says 'Request changes, downgraded to Comment … (the blockers are still
posted)' for that case. Six verdictLine cases now, including this one.

Also from the same review, all confirmed:
- agent-prompt.test.ts: the describe block named a function that was renamed
  (buildRolePrompt -> buildRoleBrief), and the mode-rejection it.each covered 2 of
  the invalid combinations, not the role-mode ones; now covers all five and drops
  the stale 'two modes' wording.
- SKILL.md Step 7: the review-JSON example used a /* */ comment inside a ```json
  fence (not valid JSON); switched to ```jsonc with a // comment.

* fix(review): eight review-round fixes atop the Step-3A invariant resolution

Follows 7c499d1, which resolved the doudouOUC roster finding (a heavy file in
a Step-3A diff must not demand invariant agents — gate the loop on the
topology) and the merge() purity nit. This carries the rest of the same round:

- `roster.ts` requires Agent 0 only for a positive PR number. `!== undefined`
  let `null`/`0`/`''` through. Note the reviewer's suggested `typeof === 'number'`
  is wrong for this codebase — `fetch-pr` writes the number as a *string* — so
  the guard accepts a numeric string too, or every real PR review would lose
  Agent 0. A table test pins both directions.
- `transcripts.ts` matches the diff path as a whole JSON string value, so
  `…/diff.txt.bak` no longer counts as reading `…/diff.txt`. It also documents
  why FIFO is right for a chronological transcript.
- `agent-prompt.ts` scopes path rules to `--file` only for invariant roles — a
  whole-diff reviewsCode agent passed `--file` would otherwise lose the rules
  for every other file — and guards each chunk element in `diffReadingBlock`
  like `chunkFrom`, so a corrupted chunk errors legibly instead of emitting
  `offset=NaN`.
- `compose-review.ts` stops double-wrapping `cov.missingRoles` /
  `cov.rewrittenPrompts`, which coverage.ts already writes self-explanatory.
- `agent-briefs.ts` JSDoc said "Two do not" read the diff; only Build & Test
  does not.
- The agent-prompt size-bound test now covers `test-matrix`.

The empty-prompt guard, the paged-read coverage, the verdictLine dangling-colon
and the submit help text were all already handled by earlier commits on the
branch; those threads are answered without a code change.

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

[Critical] modelCommand.ts:503 — Voice model !settings guard incorrectly relaxed (persistDefault always false here, dead code). Existing comment 3550114703 still stands.

[Critical] modelCommand.ts:811 — --default silently dropped when combined with inline prompt. Existing comment 3565595389 still stands.

[Critical] modelCommand.ts:877 — Caller !settings guard blocks session-only switches. Existing comment 3550114717 still stands.

[Critical] modelCommand.test.ts:177 — Missing test coverage for --default mutual exclusion, bare --default dialog, and auth-qualified model paths. Existing comment 3550114723 still stands.

— qwen3.7-max via Qwen Code /review

Comment on lines 913 to +916
return {
type: 'dialog',
dialog: 'model',
...(persistDefault ? { persistDefault } : {}),

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] No test for bare /model --default (no model, no scope) opening the dialog with { persistDefault: true } — Concrete cost: if the persistDefault spread is accidentally dropped or conditioned on scopeOverride being set, the dialog opens without persistence and the user's selection is session-only instead of default. No test would catch this.

Suggested change
return {
type: 'dialog',
dialog: 'model',
...(persistDefault ? { persistDefault } : {}),
return {
type: 'dialog',
dialog: 'model',
...(persistDefault ? { persistDefault } : {}),

— qwen3.7-max via Qwen Code /review

Comment on lines +503 to 506
if (persistDefault && !settings) {
return {
type: 'message',
messageType: 'error',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Voice model !settings guard incorrectly relaxed — persistDefault is always false in the voice path (rejected by mutual-exclusion guard at line 466), making this if (persistDefault && !settings) dead code. The unconditional persistSetting(settings, 'voiceModel', ...) at line 544 then crashes with TypeError when settings is undefined.

The fast model path (line 572) and vision model path (line 665) correctly retain if (!settings). Only voice was regressed.

Failure scenario: /model --voice some-model when context.services.settings is undefinedTypeError at settings.setValue(...) instead of a clean error message.

Suggested change
if (persistDefault && !settings) {
return {
type: 'message',
messageType: 'error',
if (!settings) {
return {
type: 'message',
messageType: 'error',
content: t('Settings service not available.'),
};
}

— qwen3.7-max via Qwen Code /review

Comment on lines +118 to 119
settings: LoadedSettings | undefined,
currentAuthType: AuthType,

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] settings widened to LoadedSettings | undefined, but the sole caller (line 877) guards with if (!settings) return before calling switchMainModel. The two if (!settings) throw blocks inside (lines 137, 157) are unreachable dead code. Concrete cost: a future maintainer reading LoadedSettings | undefined may add a call site that skips the caller guard, only to find the internal throws never fire.

Suggested change
settings: LoadedSettings | undefined,
currentAuthType: AuthType,
settings: LoadedSettings,
currentAuthType: AuthType,

— qwen3.7-max via Qwen Code /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.

[Critical] --default silently dropped with inline prompt (modelCommand.ts:~830): /model --default qwen-max "explain this codebase" parses persistDefault=true but the inline prompt branch only checks scopeOverride — there is no persistDefault guard. The function returns submit_prompt with modelOverride and the persistence intent is silently discarded. — Failure scenario: user believes they persisted the default AND sent the prompt, but only the one-shot override runs.

[Critical] Unconditional !settings guard blocks session-only switches (modelCommand.ts:~876): if (!settings) fires even when persistDefault=false, but switchMainModel only accesses settings inside if (persistDefault) blocks. A session-only /model <id> in an environment where settings is unavailable errors unnecessarily. — Failure scenario: bare /model qwen-max returns 'Settings service not available' even though it no longer needs settings.

— qwen3.7-max via Qwen Code /review

: scopeOverride === SettingScope.User
? t(' (global)')
: '';
const defaultSuffix = persistDefault ? scopeSuffix || t(' (default)') : '';

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] CLI confirmation suffix inconsistent with ModelDialog for scoped defaults — Concrete cost: when running /model --default --project qwen-max, the CLI shows Model: qwen-max (this project) while the dialog shows Using model: qwen-max (this project default). The word "default" — the key qualifier confirming persistence — is absent from the CLI path.

Suggested change
const defaultSuffix = persistDefault ? scopeSuffix || t(' (default)') : '';
const defaultSuffix = persistDefault
? scopeOverride === SettingScope.Workspace
? t(' (this project default)')
: scopeOverride === SettingScope.User
? t(' (global default)')
: t(' (default)')
: '';

— qwen3.7-max via Qwen Code /review

Comment on lines +2069 to 2071
it('should reject /model --project without --default', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--project');

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] No test for --default combined with --fast, --voice, or --vision rejection — Concrete cost: the mutual-exclusion guard at modelCommand.ts:468-479 is a new code path. If the regex or flag parsing regresses, --default --voice some-model could silently accept --default with no test to catch it.

it('should reject --default combined with --fast', async () => {
  const ctx = setupContext();
  const result = await modelCommand.action!(ctx, '--default --fast');
  expect(result).toMatchObject({
    type: 'message',
    messageType: 'error',
    content: expect.stringContaining('--default only applies to the main model'),
  });
});

— qwen3.7-max via Qwen Code /review

Comment on lines +2089 to +2091
it('should include persistScope in dialog return for /model --default --project', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--default --project');

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] No test for bare /model --default opening the dialog with persistDefault: true — Concrete cost: if the persistDefault spread at modelCommand.ts:916 is accidentally dropped, the dialog opens without persist context and the user's --default intent is silently lost.

it('should open model picker with persistDefault for /model --default', async () => {
  const ctx = setupContext();
  const result = await modelCommand.action!(ctx, '--default');
  expect(result).toEqual({
    type: 'dialog',
    dialog: 'model',
    persistDefault: true,
  });
});

— qwen3.7-max via Qwen Code /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. 2 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Review — fix(cli): keep model switches session-scoped

Reviewed at head 02b23fc23. Verified by running the focused suites and by driving modelCommand.action directly with a probe harness in an isolated worktree, plus reproducing the CI failure locally.

Overview

Plain /model <id> and ordinary model-picker selections now only switch the active session; persisting the main model requires the explicit /model --default <id> path, and scope flags become --default --project / --default --global. Auxiliary models (--fast, --voice, --vision) keep persisting as before. The persistDefault flag is plumbed OpenDialogActionReturn → slashCommandProcessor → useModelCommand → UIStateContext → DialogManager → ModelDialog, mirroring the existing persistScope plumbing.

The intent is right and matches the command's own description. The plumbing is clean. Three things block merge.


Blockers

1. CI red — check-i18n fails because the new description key is missing from en.js

The new --default description string was added to zh.js and zh-TW.js but not to packages/cli/src/i18n/locales/en.js. Both zh locales are strictParity: true, so a key present there but absent from en.js is a hard error.

Reproduced locally at PR head:

❌ Errors:
  - Extra key in zh-TW.js (not in en.js): "Switch the model for this session (--default to persist the main model for future sessions, ...)"
  - Extra key in zh.js (not in en.js):    "Switch the model for this session (--default to persist the main model for future sessions, ...)"

This is exactly the failure in CI run 30030160820 / job 89284486732 (npm error Lifecycle script 'check-i18n' failed), which is why Test (ubuntu-latest, Node 22.x) is red.

Verified fix: add the identical key/value pair to en.js right after line 1485 (en.js is an identity map). I applied exactly that in a scratch worktree and re-ran the check → ✅ All checks passed!

2. --default is silently dropped when combined with an inline promptmodelCommand.ts:811-875

The inline-prompt branch guards context.executionMode === 'acp' (:816) and scopeOverride (:830) but never persistDefault. Confirmed live:

input:  /model --default qwen-max explain this code
result: {"type":"submit_prompt","content":"explain this code","modelOverride":"qwen-max"}
        setValue calls = 0
        switchModel calls = 0

No persistence, no session switch, no error — the user believes they both set the default and sent the prompt, and finds out otherwise next session. This is the same silent-ignore class the scopeOverride guard at :830 was explicitly added to prevent. It has been flagged since 2026-07-09 (bot + @doudouOUC) and is still open. Fix is an analogous guard inside if (inlinePrompt):

if (persistDefault) {
  return {
    type: 'message',
    messageType: 'error',
    content: t(
      "Cannot combine --default with an inline prompt. Run '/model --default {{model}}' first, then send your prompt.",
      { model: modelName },
    ),
  };
}

3. Branch conflicts with mainmergeStateStatus: DIRTY. Conflicts in zh.js, zh-TW.js, modelCommand.ts, modelCommand.test.ts, ModelDialog.tsx. Needs a merge/rebase before this can land.


Should fix

4. [Medium] Nine new user-facing t() strings have zero locale coverage (en / zh / zh-TW all 0):

  • Persist the main model as the default for future sessions
  • --default only applies to the main model. Use --fast, --voice, or --vision without --default.
  • Use --default with --project or --global when persisting the main model.
  • ' (default)', ' (this project default)', ' (global default)', ' (current session)'
  • Set Default Model
  • the rewritten Current model: {{model}}\nUse "/model <model-id>" to switch this session, ...

Only the command description got zh/zh-TW entries — and that's the one breaking CI. Everything else falls back to raw English for zh users, which is a regression relative to the rest of /model, which is fully localized. These are missing from en.js too, so check-i18n won't catch them; fixing #1 alone leaves this gap.

5. [Medium] The --default × aux-flag collision check scans the inline prompt bodymodelCommand.ts:466-477

/(?:^|\s)--(?:fast|voice|vision)(?:\s|$)/.test(args) is unanchored and runs against the whole remaining arg string, prompt text included. Confirmed:

input:  /model --default qwen-max what does --vision do
result: error "--default only applies to the main model. Use --fast, --voice, or --vision without --default."

The pre-existing checks are anchored (args === '--voice' || args.startsWith('--voice ')), so this is new looseness. Match only the leading flag token.

6. [Medium] User docs not updated. docs/users/features/commands.md:93-96 still documents /model as "Switch model used in current session" with only /model / /model <model-id> examples — no --default row, no note about the persistence-semantics change. Since this is an intentional breaking change to behavior people rely on, it needs a docs line.


Nits — and a correction on two carried-over "Critical" findings

7. [Low] The two !settings findings are overstated; the real issue is dead code.

CommandContext.services.settings is typed LoadedSettings (non-optional, types.ts:52), and every production construction passes a real one (slashCommandProcessor.ts:464/749/910, nonInteractiveCliCommands.ts:282/419/530). I could not produce a user-visible failure for either. But the PR leaves a genuine internal contradiction:

  • switchMainModel(settings: LoadedSettings | undefined, …) plus its two inner throw new Error(t('Settings service not available.')) branches are unreachable — the caller already returns early at :877, and the type says undefined can't occur. Pick one: drop the widening + throws, or relax the caller guard to persistDefault && !settings.
  • The voice-path change at :503 (if (persistDefault && !settings)) is dead in the other direction: --default + --voice is rejected at :466, so persistDefault is provably false there and the guard can never fire. Net effect is deleting the voice path's !settings guard while :543 still dereferences settings. This change buys nothing — revert it to plain if (!settings).

8. [Low] Suffix wording differs between the two paths for the same operation. /model --default --project qwen-max prints Model: qwen-max (this project) (defaultSuffix = scopeSuffix || ' (default)', modelCommand.ts:465), while the dialog path prints (this project default) (ModelDialog.tsx:243-250). Align them.

9. [Low] /model --default alone in non-interactive mode silently no-ops — falls through to the "Current model: …" info message (modelCommand.ts:900-911) with no hint that --default was ignored. Same silent-ignore class as #2, much lower stakes.


What's good

  • Focused suites green: 186/186modelCommand.test.ts 70, ModelDialog.test.tsx 34, slashCommandProcessor.test.ts 82.
  • Tests were updated to assert the new non-persistence contract (expect(setValue).not.toHaveBeenCalled()) rather than just deleting the old assertions — that's the right way to encode a behavior flip.
  • closeModelDialog resets persistDefault alongside persistScope, so no state leaks between dialog opens.
  • Aux modes return before handleModelSwitchSuccess, so the new (current session) suffix correctly does not leak into --fast / --voice / --vision confirmations. Verified by reading the onSelect early-return branches.
  • Spot-checked and passing: --default --project --global → mutual-exclusion error; bare --default{type:'dialog',dialog:'model',persistDefault:true}.

Verdict

Changes requested. #1 is a one-line fix I've verified turns CI green. #2 is a real silent-data-loss bug that has survived several rounds. #3 needs a merge. #4#6 are convention/UX gaps that should ride along with the same push.

中文说明

在 head 02b23fc23 上审阅。除运行聚焦测试外,还在隔离 worktree 中直接驱动 modelCommand.action 做了行为探针,并在本地复现了 CI 失败。

概述: 普通 /model <id> 和模型选择器现在只切换当前会话;持久化主模型需要显式 /model --default <id>,作用域标志变为 --default --project / --default --global。辅助模型(--fast/--voice/--vision)行为不变。persistDefault 的链路铺设干净,与既有 persistScope 一致。方向正确,但有三个阻塞项。

阻塞项

  1. CI 红:check-i18n 失败 —— 新的 --default 描述键加进了 zh.js / zh-TW.js,但没有加进 en.js。两个 zh locale 都是 strictParity: true,因此报 Extra key in zh.js (not in en.js)。本地已复现,也正是 CI run 30030160820 失败的原因。已验证的修复:en.js 第 1485 行后补上同样的 key/value(en.js 是恒等映射),我在临时 worktree 中试过,检查变为 ✅ All checks passed!
  2. --default 与内联 prompt 组合时被静默丢弃modelCommand.ts:811-875)。实测 /model --default qwen-max explain this code 返回 submit_promptsetValue 调用 0 次、switchModel 调用 0 次,既不持久化也不报错。这与 :830 处 scopeOverride 守卫要防的是同一类问题,自 2026-07-09 起被反复指出,至今未修。需在 if (inlinePrompt) 内加上对应的 persistDefault 守卫。
  3. main 冲突DIRTY):zh.jszh-TW.jsmodelCommand.tsmodelCommand.test.tsModelDialog.tsx 五个文件冲突,需要合并/变基。

建议修复

  1. [Medium] 新增的 9 条用户可见 t() 文案在 en / zh / zh-TW 中覆盖率均为 0(Set Default Model (default) (current session) 等)。只有命令描述加了 zh 翻译,而那条恰好是导致 CI 失败的。修完 pre-release: fix ci #1 也补不上这个缺口。
  2. [Medium] --default 与辅助标志的互斥检查(:466-477)正则未锚定,会扫描内联 prompt 正文。实测 /model --default qwen-max what does --vision do 会被误判报错。既有检查都是锚定在开头的(args.startsWith('--voice ')),这是新引入的松动。
  3. [Medium] 未更新用户文档 docs/users/features/commands.md:93-96,既没有 --default 条目,也没有说明持久化语义的破坏性变更。

次要问题 —— 兼对两条历史 "Critical" 的更正

  1. [Low] 两条 !settings 相关的 Critical 定级偏高。services.settings 类型为非可选的 LoadedSettings,所有生产构造点都传入了真实值,我无法复现用户可见的失败。但 PR 确实留下了矛盾的死代码:switchMainModel| undefined 放宽 + 两处内部 throw 不可达(调用方 :877 已提前 return);而 :503 的 persistDefault && !settings 也是死的(--default + --voice 在 :466 已被拒,此处 persistDefault 恒为 false),净效果是删掉了 voice 路径的 !settings 守卫,而 :543 仍在解引用 settings。二者择一处理即可。
  2. [Low] 同一操作在两条路径上的后缀文案不一致:命令路径给 (this project),对话框路径给 (this project default)
  3. [Low] 非交互模式下单独的 /model --default 静默无操作,只打印 "Current model: …"。

做得好的地方

  • 聚焦测试全绿 186/186(modelCommand 70 / ModelDialog 34 / slashCommandProcessor 82)。
  • 测试改为断言新的"不持久化"契约(expect(setValue).not.toHaveBeenCalled()),而不是简单删掉旧断言 —— 这是表达行为翻转的正确方式。
  • closeModelDialog 会一并重置 persistDefault,对话框之间无状态泄漏。
  • 辅助模式在 handleModelSwitchSuccess 之前提前返回,新的 (current session) 后缀不会污染 --fast/--voice/--vision 的确认信息(已核对 onSelect 分支)。
  • 抽查通过:--default --project --global 正确报互斥错误;裸 --default 正确返回 {type:'dialog',dialog:'model',persistDefault:true}

结论:Changes requested。 #1 是我已验证能让 CI 转绿的一行修复;#2 是真实的静默丢失行为,已跨多轮存活;#3 需要合并主干;#4#6 建议随同一次推送一起处理。

@wenshao

wenshao commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

Merge Conflict Resolution Summary — PR #6579

Root cause

The origin/main branch added an --image flag to the /model command (image generation model support), colliding with this PR's --default flag (persist main model for future sessions). Both features modify the same strings, flag parsers, and ternary chains in the model command implementation and its translations.

Textual vs semantic

Mostly textual, with one semantic merge point.

The description strings in modelCommand.ts, modelCommand.test.ts, zh.js, and zh-TW.js were purely adjacent additions — both sides appended different flag descriptions to the same base string. Resolution: concatenate both.

The argumentHint string combined [--default] from the PR with [--image] from main:

argumentHint: '[--default] [--fast|--voice|--vision|--image] [--project|--global] [<model-id>] | <model-id> <prompt>',

The flag-stripping .replace() chain in completion logic was adjacent — both flags need stripping:

.replace(/(?:^|\s)--default(?:\s|$)/, ' ')
.replace(/(?:^|\s)--image(?:\s|$)/, ' ')

The action handler in modelCommand.ts had the PR's scopeOverride && !persistDefault error check and main's full --image command block. Resolution: place the PR's guard first (it only fires when scope flags are used without --default), then main's image-model block below it.

The ModelDialog title ternary chain needed both branches:

: isImageModelMode
  ? t('Select Image Model')
  : isFastModelMode
    ? t('Select Fast Model')
    : persistDefault
      ? t('Set Default Model')
      : t('Select Model')

What is load-bearing

  • Guard ordering in modelCommand.ts action handler: the PR's scopeOverride && !persistDefault error must come before the image-model block. If a user passes --project without --default, the error must fire before the image-model path is evaluated. Reversing the order would let --project --image incorrectly hit the error guard (since persistDefault is false for image commands).

  • Ternary precedence in ModelDialog title: isImageModelMode is checked before persistDefault. This is correct because image-model mode is never combined with persist-default semantics. If a future change allows both simultaneously, this chain needs revisiting.

What I could not verify

  • No build or tests were run. The PR's CI covers correctness.
  • The PR's parseDefaultFlag function and main's persistScopeSpread helper both operate on the same rawArgs pipeline. The persistDefault guard fires before the image block, so --image --project without --default would hit the error. This is likely correct, but worth verifying.
  • en.js was NOT conflicted and was NOT modified. It has the --image description key (from main) but not the --default key or the combined key. This is a pre-existing PR issue (not introduced by the merge) — the PR only added translations to zh.js/zh-TW.js and relied on the t() fallback for English (the key string IS the English text). The test modelCommand.test.ts asserts the combined description string, which works via fallback. A follow-up may want to add the combined key to en.js for completeness.
中文说明

合并冲突解决总结 — PR #6579

根本原因origin/main 分支为 /model 命令添加了 --image 标志(图像生成模型支持),与本 PR 的 --default 标志(持久化主模型为未来会话默认值)产生冲突。两个功能都修改了相同的字符串、标志解析器和模型命令实现中的三元表达式链。

解决方式

  • 描述字符串(modelCommand.ts、测试文件、中文翻译文件):将两边的标志描述拼接在一起
  • 标志过滤的 .replace() 链:同时保留 --default--image 的剥离逻辑
  • 动作处理函数:PR 的 scopeOverride && !persistDefault 错误检查在前,main 的图像模型处理逻辑在后
  • ModelDialog 标题三元表达式:按 isImageModelModeisFastModelModepersistDefault → 默认 的顺序排列

关键约束:PR 的错误检查必须在图像模型逻辑之前执行,否则 --project 不带 --default 的情况无法正确拦截。

未验证项:未运行构建和测试,PR 的 CI 将覆盖正确性验证。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] --default silently dropped with inline prompt (modelCommand.ts:~1011): existing blocker (comment 3565595389) still stands at reviewed commit. The inline-prompt branch checks scopeOverride but not persistDefault, so /model --default qwen-max "explain this" silently discards the persistence intent.

— qwen3.7-max via Qwen Code /review

Comment on lines +784 to +792
if (scopeOverride && !persistDefault) {
return {
type: 'message',
messageType: 'error',
content: t(
'Use --default with --project or --global when persisting the main model.',
),
};
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] scopeOverride && !persistDefault guard blocks /model --project --image and /model --global --image — Failure scenario: the guard is positioned between the vision handler and the image handler, so it fires for /model --global --image (which should open the image-model picker with global persistence) but not for --fast/--voice/--vision (whose handlers return earlier). The existing test should include persistScope for /model --global --image dialog fails, confirming this is an unintended regression.

Suggested change
if (scopeOverride && !persistDefault) {
return {
type: 'message',
messageType: 'error',
content: t(
'Use --default with --project or --global when persisting the main model.',
),
};
}
const isImageModelCommand =
args === '--image' || args.startsWith('--image ');
if (scopeOverride && !persistDefault && !isImageModelCommand) {
return {
type: 'message',
messageType: 'error',
content: t(
'Use --default with --project or --global when persisting the main model.',
),
};
}

— qwen3.7-max via Qwen Code /review

@danialzivehdadr

Copy link
Copy Markdown

دوست عزیز وقتی طرح به کلی از عمومی عوض میشه خصوصی مشکل ترا شیدم اشتباه شما زیر ساخت بروز کن مستقل باش

@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. Unresolved, please confirm: [Critical] modelCommand.ts:1019 — Unconditional !settings guard (existing blocker, comment 3550114717): the guard fires even for session-only switches, but CommandContext.services.settings is typed LoadedSettings (non-optional) and every production construction provides it. No user-visible failure is producible. Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it. [Critical] --default silently dropped with inline prompt (modelCommand.ts:~971): the inline-prompt branch checks scopeOverride but not persistDefault. /model --default qwen-max "explain this" parses persistDefault=true but returns submit_prompt — persistence silently discarded. User believes they persisted the default AND sent the prompt; only the one-shot override runs. Fix: add a persistDefault guard analogous to the scopeOverride check. [Critical] en.js missing the new --default description key — CI check-i18n fails with 'Extra key in zh.js/zh-TW.js (not in en.js)', blocking the test pipeline. Fix: add the identical key/value pair to packages/cli/src/i18n/locales/en.js.

中文说明

已审查。 建议见行内评论。 未决,请确认:[Critical] modelCommand.ts:1019 — Unconditional !settings guard (existing blocker, comment 3550114717): the guard fires even for session-only switches, but CommandContext.services.settings is typed LoadedSettings (non-optional) and every production construction provides it. No user-visible failure is producible. 未审查:验证——有验证 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,发布的发现不能算作经它验证。 [Critical] --default silently dropped with inline prompt (modelCommand.ts:~971): the inline-prompt branch checks scopeOverride but not persistDefault. /model --default qwen-max "explain this" parses persistDefault=true but returns submit_prompt — persistence silently discarded. User believes they persisted the default AND sent the prompt; only the one-shot override runs. Fix: add a persistDefault guard analogous to the scopeOverride check. [Critical] en.js missing the new --default description key — CI check-i18n fails with 'Extra key in zh.js/zh-TW.js (not in en.js)', blocking the test pipeline. Fix: add the identical key/value pair to packages/cli/src/i18n/locales/en.js.

— qwen3.7-max via Qwen Code /review

Comment on lines +495 to +498
if (
persistDefault &&
/(?:^|\s)--(?:fast|voice|vision)(?:\s|$)/.test(args)
) {

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] --image missing from the --default mutual-exclusion regex, and no test covers this guard.

  1. /model --default --image <id> passes silently with --default ignored — inconsistent with --default --fast/--voice/--vision which correctly error. Consider adding image to the alternation: /(?:^|\s)--(?:fast|voice|vision|image)(?:\s|$)/.

  2. No test assertion exercises the --default + --fast/--voice/--vision rejection path. If a future refactor broke this guard, the regression would ship undetected.

Concrete cost: user runs /model --default --image dall-e-3 expecting persistence feedback; the flag is silently discarded.

中文说明

--default 互斥正则表达式缺少 --image,且此守卫没有测试覆盖。

  1. /model --default --image <id> 会通过但 --default 被静默忽略——与 --default --fast/--voice/--vision(正确报错)不一致。
  2. 没有测试断言覆盖 --default--fast--voice--vision 组合的拒绝路径。

— qwen3.7-max via Qwen Code /review

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

Not reviewed: PR #6579 changes the /model command so ordinary `/model... — pointed at diff lines it never opened: it made tool calls, but none of them read the diff.

— qwen3.7-max via Qwen Code /review

Comment on lines +1031 to 1033
persistDefault,
scopeOverride,
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] Voice/fast/vision model !settings guard incorrectly relaxed at line 532 — persistDefault is always false in the voice path because --default --voice is rejected by the mutual-exclusion guard at line 496. This makes if (persistDefault && !settings) dead code. But persistSetting(settings, 'voiceModel', ...) is called unconditionally downstream — when settings is undefined, this crashes with a TypeError.

The fast model path (line 601) and vision model path (line 694) correctly retain if (!settings). Only the voice path was regressed.

The caller's unconditional if (!settings) guard here also blocks session-only model switches (/model qwen-max without --default) even though switchMainModel only accesses settings inside if (persistDefault) blocks.

Suggested change
persistDefault,
scopeOverride,
);
persistDefault,
scopeOverride,
);

Fix the voice path at line 532:

      if (!settings) {

And relax this caller guard to:

      if (persistDefault && !settings) {

— qwen3.7-max via Qwen Code /review

const defaultSuffix = persistDefault ? scopeSuffix || t(' (default)') : '';
if (
persistDefault &&
/(?:^|\s)--(?:fast|voice|vision)(?:\s|$)/.test(args)

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] --image is missing from the --default mutual-exclusion regex — Concrete cost: /model --default --image dall-e-3 passes the guard silently. The --default flag is stripped by parseDefaultFlag but the rejection check only tests for --fast, --voice, and --vision. The image model handler at line ~795 unconditionally persists imageModel via persistSetting regardless of persistDefault, so the user receives no feedback that --default was irrelevant.

Suggested change
/(?:^|\s)--(?:fast|voice|vision)(?:\s|$)/.test(args)
/(?:^|\s)--(?:fast|voice|vision|image)(?:\s|$)/.test(args)

And update the error message to include --image.

— qwen3.7-max via Qwen Code /review

Comment on lines +39 to +40
const [modelDialogPersistDefault, setModelDialogPersistDefault] =
useState(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No test coverage for the new modelDialogPersistDefault state — Concrete cost: the hook gained modelDialogPersistDefault state and a persistDefault option on openModelDialog(), plus reset logic in closeModelDialog(). The existing test file useModelCommand.test.ts has no coverage for: (a) openModelDialog({ persistDefault: true }) setting the flag, (b) closeModelDialog() resetting it. If a future refactor drops the setter calls, no test would catch it.

— qwen3.7-max via Qwen Code /review

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/model should switch only the current session unless the user explicitly sets a default

7 participants