Skip to content

fix(web-shell): encode vision model picker selection & polish dispatch#6236

Merged
wenshao merged 16 commits into
QwenLM:mainfrom
AmariahAK:main
Jul 3, 2026
Merged

fix(web-shell): encode vision model picker selection & polish dispatch#6236
wenshao merged 16 commits into
QwenLM:mainfrom
AmariahAK:main

Conversation

@AmariahAK

Copy link
Copy Markdown
Contributor

What this PR does

Fixes a critical bug where selecting a vision model from the web-shell picker silently fails to pin the model. The picker passes model IDs in ACP format (modelId(authType)), but core's resolveVisionModelSelection() expects authType:modelId — so the stored value never resolves and the vision bridge falls back to auto-select. The fix strips the ACP suffix and re-encodes before persisting, mirroring what the CLI's encodeVisionModelSelector already does.

Also addresses three polish items from Wenshao's review on #6209: derives currentVisionModel so the picker highlights the active vision model (instead of falling back to the main model), extracts a MODE_TITLE_KEY record and exhaustive handlers record to replace a duplicated 4-way ternary and if/else chain, and adds missing settings.label.visionModel / settings.description.visionModel i18n keys to both EN and ZH locales.

Why it's needed

Without the encoding fix, users who select a vision model from the picker or Settings button see a success toast but their choice is silently discarded — the vision bridge can't resolve the stored coder-model(qwen-oauth) string and falls back to auto-select. The Settings page still shows the value, which masks the failure. This is a data-loss bug: the user's explicit choice has no effect.

The polish items prevent future regressions: the exhaustive handlers record means adding a new ModelDialogMode member would fail at compile time if no handler is wired, and the i18n keys ensure Chinese users see localized labels instead of English fallbacks.

Reviewer Test Plan

How to verify

  1. Start the daemon (npm run dev:daemon) and open the web UI
  2. Run /model --vision — confirm the picker opens and the currently active vision model (if any) is highlighted with the "current" indicator
  3. Select a model from the picker — confirm the setting persists and the vision bridge actually uses it (check .qwen/settings.json shows authType:modelId format, not modelId(authType))
  4. Open Settings → Vision Model — confirm the button shows the persisted value and opens the picker in vision mode
  5. Switch UI language to Chinese — confirm "视觉模型" appears as the settings label and description
  6. Run cd packages/web-shell && npx vitest run — confirm all 699 tests pass
  7. Run cd packages/web-shell && npx tsc --noEmit -p tsconfig.lib.json — confirm zero type errors

Evidence (Before & After)

N/A (non-visual change; verified via unit tests — 699 tests pass, 0 failures; tsc clean)

Tested on

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

Environment (optional)

npm run dev:daemon (local), cd packages/web-shell && npx vitest run (unit tests)

Risk & Scope

  • Main risk or tradeoff: None — this is a bug fix + polish. The encoding change only affects the vision model picker path; the /model --vision <bare-id> inline command is unchanged (bare IDs already resolve correctly).
  • Not validated / out of scope: Windows and Linux manual testing (CI covers these). The vision model validation logic (image-capability check, primary-model rejection) remains in the CLI and is not duplicated here.
  • Breaking changes / migration notes: None. Existing visionModel settings stored as bare IDs continue to resolve correctly.

Linked Issues

Follow-up to #6209 (addresses Wenshao's review comments).

中文说明

本 PR 做了什么

修复了一个关键 bug:从 web-shell 选择器中选择视觉模型时,选择会被静默丢弃。选择器传递的模型 ID 是 ACP 格式(modelId(authType)),但 core 的 resolveVisionModelSelection() 期望的是 authType:modelId 格式——因此存储的值永远无法解析,视觉桥接会回退到自动选择。修复方案是去除 ACP 后缀并在持久化前重新编码,与 CLI 的 encodeVisionModelSelector 行为一致。

同时处理了 Wenshao 在 #6209 审查中提出的三个优化项:派生 currentVisionModel 使选择器能高亮当前活动的视觉模型(而非回退到主模型);提取 MODE_TITLE_KEY 记录和穷举 handlers 记录,替换重复的四元条件表达式和 if/else 链;为 EN 和 ZH 两个语言环境添加缺失的 settings.label.visionModel / settings.description.visionModel i18n 键。

为什么需要

如果不修复编码问题,用户从选择器或设置按钮中选择视觉模型后会看到成功提示,但他们的选择实际上被静默丢弃——视觉桥接无法解析存储的 coder-model(qwen-oauth) 字符串,回退到自动选择。设置页面仍然显示该值,掩盖了故障。这是一个数据丢失 bug:用户的明确选择不生效。

优化项可防止未来回归:穷举 handlers 记录意味着添加新的 ModelDialogMode 成员时,如果未连接处理器,编译时就会报错;i18n 键确保中文用户看到本地化标签而非英文回退。

审查者测试计划

如何验证

  1. 启动守护进程(npm run dev:daemon)并打开 web UI
  2. 运行 /model --vision——确认选择器打开,且当前活动的视觉模型(如有)以"current"指示器高亮
  3. 从选择器中选择一个模型——确认设置已持久化,且视觉桥接实际使用了它(检查 .qwen/settings.json 显示 authType:modelId 格式,而非 modelId(authType)
  4. 打开设置 → 视觉模型——确认按钮显示持久化的值,并以视觉模式打开选择器
  5. 将 UI 语言切换为中文——确认"视觉模型"显示为设置标签和描述
  6. 运行 cd packages/web-shell && npx vitest run——确认全部 699 个测试通过
  7. 运行 cd packages/web-shell && npx tsc --noEmit -p tsconfig.lib.json——确认零类型错误

证据(修改前与修改后)

N/A(非可视化变更;通过单元测试验证——699 个测试全部通过,0 失败;tsc 无错误)

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

npm run dev:daemon(本地),cd packages/web-shell && npx vitest run(单元测试)

风险与范围

  • 主要风险或权衡:无——这是一个 bug 修复 + 优化。编码变更仅影响视觉模型选择器路径;/model --vision <bare-id> 内联命令未更改(裸 ID 已能正确解析)。
  • 未验证 / 超出范围:Windows 和 Linux 手动测试(CI 覆盖)。视觉模型验证逻辑(图像能力检查、主模型拒绝)仍在 CLI 中,此处未重复。
  • 破坏性变更 / 迁移说明:无。以裸 ID 存储的现有 visionModel 设置仍可正确解析。

关联 Issue

#6209 的后续(处理 Wenshao 的审查意见)。

[{"url":"https://github.com/anthropics/claude-code/issues/62056","title":"System reminder persists task-specific instruction across sessions, contributed to model self-prompting · Issue #62056 · anthropics/claude-code"},{"url":"https://github.com/Yeachan-Heo/gajae-code/pull/444","title":"fix(models): prefer vision-capable variant over provider order for ambiguous ids"},{"url":"https://www.stepcodex.com/en/issue/bug-mode-switch-signals-mislabeled-and","title":"claude-code - 💡(How to fix) Fix [Bug] Mode switch signals mislabeled and missing in system reminders to Claude [3 comments, 3 participants]"},{"url":"https://github.com/PostHog/code/pull/2885","title":"feat(sessions): inline model swap on plan-approval card"},{"url":"https://github.com/pingdotgg/t3code/pull/3021","title":"Model picker UI Improvements, Virtualize Model List"}]

atlarix-agent and others added 4 commits July 3, 2026 09:28
Address Wenshao's review comments on QwenLM#6209:

[Critical] Encode vision model selection before persisting
- handleVisionModelSelect now strips ACP (authType) suffix and stores
  as authType:modelId format expected by core's resolveVisionModelSelection()
- Without this, picker selections silently fail to resolve when the same
  model ID appears on multiple providers

[Suggestion] Add currentVisionModel derivation
- Mirror currentVoiceModel pattern so the picker highlights the active
  vision model instead of falling back to the main model

[Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch
- Replace duplicated 4-way ternary in App.tsx dialog title with a single
  Record<ModelDialogMode, string> lookup
- Replace if/else if/else onSelect chain with a handlers record that
  would fail at compile time if a new mode is added without a handler

[Suggestion] Add settings.label/description.visionModel i18n keys
- Add to both EN and ZH locales so Chinese users see proper labels
  in the Settings dialog

Files changed:
- App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record
- i18n.tsx: visionModel label + description (EN + ZH)
Address Wenshao's review comments on QwenLM#6209:

[Critical] Encode vision model selection before persisting
- handleVisionModelSelect now strips ACP (authType) suffix and stores
  as authType:modelId format expected by core's resolveVisionModelSelection()
- Without this, picker selections silently fail to resolve when the same
  model ID appears on multiple providers

[Suggestion] Add currentVisionModel derivation
- Mirror currentVoiceModel pattern so the picker highlights the active
  vision model instead of falling back to the main model

[Suggestion] Extract MODE_TITLE_KEY record for exhaustive dispatch
- Replace duplicated 4-way ternary in App.tsx dialog title with a single
  Record<ModelDialogMode, string> lookup
- Replace if/else if/else onSelect chain with a handlers record that
  would fail at compile time if a new mode is added without a handler

[Suggestion] Add settings.label/description.visionModel i18n keys
- Add to both EN and ZH locales so Chinese users see proper labels
  in the Settings dialog

Files changed:
- App.tsx: encoding fix, currentVisionModel, MODE_TITLE_KEY, handlers record
- i18n.tsx: visionModel label + description (EN + ZH)
@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot & @wenshao kindly review

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, @AmariahAK!

Template looks good ✓

On direction: this fixes a genuine data-loss bug — users pick a vision model from the web-shell picker, see a success toast, but their choice is silently discarded because the stored format doesn't match what core expects. That's a real user problem and squarely within qwen-code's web-shell + model selection scope. The polish items (exhaustive handlers, i18n keys, active model highlighting) are natural follow-ups from #6209's review. Aligned.

On approach: the scope feels right for a fix + polish PR. The encoding fix is a targeted 3-line change. The MODE_TITLE_KEY record and exhaustive handlers record replace fragile ternary/if-else chains with compile-time-safe dispatch — good defensive refactoring. The currentVisionModel derivation mirrors the existing currentVoiceModel pattern exactly. The i18n keys are minimal additions. No scope creep.

One minor note: the regex ^(.+)\(([^()]+)\)$ handles the ACP modelId(authType) format correctly, but it's an inline pattern rather than reusing the CLI's encodeVisionModelSelector. This is reasonable — web-shell and CLI are separate packages with no shared model-encoding utility, and extracting one would be over-engineering for a 3-line conversion. Just worth flagging that if the ACP format ever changes, this regex needs updating independently.

Moving on to code review. 🔍

中文说明

感谢贡献,@AmariahAK

模板完整 ✓

方向:这修复了一个真实的数据丢失 bug——用户从 web-shell 选择器中选择视觉模型后看到成功提示,但选择被静默丢弃,因为存储格式与 core 期望的格式不匹配。这是真实的用户问题,完全在 qwen-code 的 web-shell + 模型选择范围内。优化项(穷举 handlers、i18n 键、活动模型高亮)是 #6209 审查中的自然后续。方向对齐。

方案:作为修复+优化的 PR,范围合理。编码修复是一个精准的 3 行改动。MODE_TITLE_KEY 记录和穷举 handlers 记录将脆弱的三元/if-else 链替换为编译时安全的分发——很好的防御性重构。currentVisionModel 的派生完全复制了现有的 currentVoiceModel 模式。i18n 键是最小化的添加。没有范围蔓延。

一个小注:正则 ^(.+)\(([^()]+)\)$ 正确处理了 ACP modelId(authType) 格式,但没有复用 CLI 的 encodeVisionModelSelector。这合理——web-shell 和 CLI 是独立的包,没有共享的模型编码工具类,为此提取一个共享工具属于过度工程。只是提醒如果 ACP 格式未来变化,这个正则需要独立更新。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal (before reading the diff): I would have done exactly what this PR does — add a conversion step in handleVisionModelSelect to re-encode ACP format to authType:modelId, derive currentVisionModel mirroring currentVoiceModel, replace the if/else chain with an exhaustive Record, replace the nested ternary with a Record lookup, and add the missing i18n keys.

Comparison: The PR's approach matches my independent proposal point for point. The regex /^(.+)\(([^()]+)\)$/ correctly handles the ACP modelId(authType)authType:modelId conversion. The MODE_TITLE_KEY and handlers records are clean compile-time-safe dispatch patterns.

Reuse check: The CLI has encodeVisionModelSelector in packages/cli/src/ui/components/ModelDialog.tsx, but it operates on a different input format (authType::modelId[\0baseUrl] / $runtime|authType|modelId / bare id). The web-shell picker receives ACP format modelId(authType) which the CLI's encoder doesn't handle. No existing shared utility covers this — the inline regex is the right call.

No critical blockers found. No security issues. Follows project conventions.

One minor nit (non-blocking): in the handlers record dispatch, modelDialogMode ?? 'main' is redundant since we're inside {modelDialogMode && ...} — TypeScript narrows modelDialogMode to ModelDialogMode (non-null). The ?? is harmless but unnecessary.

Real-Scenario Testing

This PR modifies a browser-based React component (web-shell model picker dialog). It cannot be meaningfully tested via tmux/CLI — there's no terminal interaction to drive. The feature requires npm run dev:daemon + a browser to open the picker UI and select a vision model.

CI status at time of review: precheck-pr ✅ pass, authorize ✅ pass, Test (ubuntu-latest) ⏳ pending, Test (macos/windows) ⏭️ skipping.

Verdict: Code review clean. Real-scenario testing deferred to the reviewer's local environment (browser) or CI once it completes.

中文说明

代码审查

独立方案(阅读 diff 之前):我会做和这个 PR 完全一样的事情——在 handleVisionModelSelect 中添加转换步骤,将 ACP 格式重新编码为 authType:modelId,镜像 currentVoiceModel 派生 currentVisionModel,用穷举 Record 替换 if/else 链,用 Record 查找替换嵌套三元,添加缺失的 i18n 键。

对比:PR 的方案与我的独立方案逐点吻合。正则 /^(.+)\(([^()]+)\)$/ 正确处理了 ACP modelId(authType)authType:modelId 转换。MODE_TITLE_KEYhandlers 记录是干净的编译时安全分发模式。

复用检查:CLI 有 encodeVisionModelSelector,但它处理的是不同的输入格式(authType::modelId[\0baseUrl] / $runtime|authType|modelId / 裸 id)。web-shell 选择器接收的 ACP 格式 modelId(authType) 不在 CLI 编码器的处理范围内。没有现有的共享工具覆盖此场景——内联正则是正确选择。

未发现关键阻塞问题。 无安全问题。遵循项目约定。

一个小 nit(不阻塞):handlers 记录分发中,modelDialogMode ?? 'main' 是多余的,因为我们在 {modelDialogMode && ...} 内——TypeScript 已将 modelDialogMode 收窄为 ModelDialogMode(非 null)。?? 无害但多余。

真实场景测试

此 PR 修改的是浏览器端 React 组件(web-shell 模型选择对话框)。无法通过 tmux/CLI 进行有意义的测试——没有终端交互可驱动。该功能需要 npm run dev:daemon + 浏览器来打开选择器 UI 并选择视觉模型。

CI 状态:precheck-pr ✅ 通过,authorize ✅ 通过,Test (ubuntu-latest) ⏳ 等待中,Test (macos/windows) ⏭️ 跳过。

结论:代码审查通过。真实场景测试留待审查者在本地环境(浏览器)中验证,或等 CI 完成后确认。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Reflection

This is a clean, focused PR that solves a real problem: users select a vision model from the web-shell picker and their choice gets silently discarded because of a format mismatch between the picker's ACP encoding and core's expected format. The fix is 3 lines of conversion logic that correctly bridges the two formats.

The polish items are all natural follow-ups from #6209's review — exhaustive handlers for compile-time safety, a single-source-of-truth title map, active model highlighting, and missing i18n keys. Nothing here is speculative or over-engineered.

The approach matches my independent proposal exactly. The code is straightforward — no abstractions, no unnecessary indirection, no drive-by refactors. If I had to maintain this in six months, I'd thank the author for making the dispatch exhaustive and the encoding conversion obvious.

One non-blocking nit: modelDialogMode ?? 'main' is redundant inside the {modelDialogMode && ...} guard — TypeScript already narrows to non-null. Harmless.

Can't verify in browser from this environment (web-shell is a React UI, not a CLI feature), but the code review is clean and the logic is correct. Approving.

中文说明

总结

这是一个干净、聚焦的 PR,解决了一个真实问题:用户从 web-shell 选择器中选择视觉模型后,选择因选择器的 ACP 编码与 core 期望格式不匹配而被静默丢弃。修复是 3 行转换逻辑,正确桥接了两种格式。

优化项都是 #6209 审查中的自然后续——穷举 handlers 确保编译时安全、单一来源的标题映射、活动模型高亮、缺失的 i18n 键。没有投机性代码或过度工程。

方案与我的独立方案完全吻合。代码直截了当——没有抽象、没有不必要的间接、没有顺手重构。如果六个月后我要维护这段代码,我会感谢作者让分发穷举且编码转换一目了然。

一个不阻塞的 nit:modelDialogMode ?? 'main'{modelDialogMode && ...} 保护内是多余的——TypeScript 已收窄为非 null。无害。

无法在此环境中通过浏览器验证(web-shell 是 React UI,不是 CLI 功能),但代码审查通过,逻辑正确。批准。

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

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/App.tsx Outdated

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No review findings from independent multi-agent analysis. The encoding fix correctly converts ACP format to authType:modelId, the MODE_TITLE_KEY / handlers record extraction is type-safe and exhaustive, and 694/699 functional tests pass (5 build-artifact test failures are environment-only — require npm run build first). Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

🔬 Maintainer verification — built & tested locally (as a merge reference)

I built PR #6236 at its head (05c6c71) in a real worktree (Node v22, real vite/vitest/tsc, real Chromium via Playwright) and drove the actual code paths. The core bug fix is correct and worth landing — but the PR's Reviewer-Test-Plan step 2 ("current vision model is highlighted") does not hold, and I want to flag that before merge.

TL;DR

Claim Result
Encoding fix modelId(authType)authType:modelId resolves in core Correct (runtime round-trip, incl. colon-in-id)
web-shell suite (699) + tsc -p tsconfig.lib.json 703 pass (699 + 4 I added), tsc exit 0
New EN/ZH i18n keys resolve (no raw-key fallback) Confirmed
Step 2: picker highlights the active vision model Fails — the "current" ✓ never lights up
CI-bot "voice/fast are equally broken" ⚠️ Not accurate — different code paths (details below)

Evidence (real ModelDialog rendered headless, dark theme)

overview

  • Left (BUG): currentModelId = the value the PR persists (qwen-oauth:qwen-vl-max). No ✓ marker anywhere; the roving highlight even falls back to row 1 (coder-model) — the wrong model.
  • Middle (CONTROL): currentModelId = ACP form (qwen-vl-max(qwen-oauth)). ✓ correctly marks row 2. → the picker can highlight; it's purely the format being fed to it.
  • Right (i18n): 视觉模型 + ZH description resolve via the real formatSettingLabel / getTranslator('zh-CN').

🔴 The one real problem: the highlight is fed the wrong format

The fix stores authType:modelId (correct for core). But the same stored string is then handed back to the picker as currentModelId, and the picker matches with strict === against ACP-format ids:

  • ModelDialog.tsx:111availableModels.findIndex((m) => m.id === currentModel)
  • m.id is ACP modelId(authType) (built at packages/cli/src/utils/acpModelUtils.ts:20)
  • currentVisionModel is authType:modelId (App.tsx, after the encoding fix)

'qwen-vl-max(qwen-oauth)' === 'qwen-oauth:qwen-vl-max' is never true → currentIdx = -1 → the ✓ badge (ModelDialog.tsx:189, aria-selected) never appears, and activeIndex falls back to 0.

This is exactly the confusion the polish item set out to remove. The minimal fix (matches the inline suggestion already on the diff) is to decode back to ACP before passing to the picker:

const colonIdx = value.indexOf(':');
const currentVisionModel = colonIdx > 0
  ? `${value.slice(colonIdx + 1)}(${value.slice(0, colonIdx)})`
  : value;

I confirmed this inverse is correct even for colon-bearing ids (openai:gpt-4o:onlinegpt-4o:online(openai)).

On the automated-review comments (verified, so you don't have to)

  • Encoding correctness — ✅ real. parseModelIdSelector (modelId.ts:90-113) splits on the first colon and accepts the prefix when it's a known AuthType (qwen-oauth is one). Round-trip test passes; the pre-fix raw ACP string demonstrably loses the authType.
  • "voice/fast are equally broken"⚠️ overgeneralized. I checked both paths:
    • handleFastModelSelect does not write a raw setting — it sends /model --fast <id> (sendPrompt), which goes through the command handler, a different path.
    • voiceModel is an ASR id consumed by use-voice-input.tstranscribe(...), not resolveModelId.
    • So the mismatch is genuinely vision-specific, and the PR's narrow scope is defensible. (Open question, not blocking: whether the ASR endpoint tolerates an ACP-wrapped id — worth a separate look.)
  • "mirrors the CLI encodeVisionModelSelector" — mostly, but the regex doesn't handle the CLI's $runtime|authType|modelId snapshot ids or \0baseUrl. Fine for standard models; a runtime-snapshot pick would encode to a malformed selector. Edge case, non-blocking.
  • Zero test coverage for the encoding — ✅ real; I've written the tests locally (encoding + highlight A/B + i18n) that should live in the repo.

Recommendation

The encoding fix (the actual data-loss bug) is correct — 👍 to land it. Please add the ~3-line currentVisionModel decode so step 2 actually works (it's trivial and the PR explicitly claims it), plus a test for the encoding. Dispatch cleanup (MODE_TITLE_KEY / handlers) and the i18n keys are fine as-is.

🀄 中文版(完整对应)

🔬 维护者验证 —— 本地真实构建并测试(作为合并参考)

我在真实 worktree 中检出 PR #6236 的 head(05c6c71),用真实的 vite/vitest/tsc 构建,并通过 Playwright 驱动真实 Chromium 渲染真实组件,跑通了实际代码路径。核心 bug 修复是正确的,值得合并——但 PR 的「审查者测试计划」第 2 步("当前视觉模型被高亮")并不成立,合并前想先指出这一点。

结论速览

声明 结果
编码修复 modelId(authType)authType:modelId 能被 core 解析 正确(运行时往返测试,含 id 内含冒号的情形)
web-shell 测试套件(699)+ tsc -p tsconfig.lib.json 703 通过(699 + 我新增 4 个),tsc 退出码 0
新增 EN/ZH i18n 键能解析(不回退到裸键) 确认
第 2 步:选择器高亮当前视觉模型 失败——"current" ✓ 从不出现
CI 机器人称 "voice/fast 同样损坏" ⚠️ 不准确——是不同代码路径(见下)

证据(真实 ModelDialog 无头渲染,暗色主题)

overview

  • 左(BUG): currentModelId = PR 实际持久化的值(qwen-oauth:qwen-vl-max)。任何位置都没有 ✓ 标记;漫游高亮甚至回退到第 1 行(coder-model)——错误的模型。
  • 中(对照): currentModelId = ACP 形式(qwen-vl-max(qwen-oauth))。✓ 正确标记第 2 行。→ 说明选择器高亮,问题纯粹出在喂给它的格式。
  • 右(i18n): 视觉模型 + 中文描述通过真实的 formatSettingLabel / getTranslator('zh-CN') 解析得到。

🔴 唯一真正的问题:高亮被喂了错误的格式

修复把值存为 authType:modelId(对 core 是正确的)。但同一个存储字符串又被作为 currentModelId 传回选择器,而选择器用严格 === 去匹配 ACP 格式的 id:

  • ModelDialog.tsx:111availableModels.findIndex((m) => m.id === currentModel)
  • m.id 是 ACP 格式 modelId(authType)(构造于 packages/cli/src/utils/acpModelUtils.ts:20
  • currentVisionModelauthType:modelId(App.tsx,编码修复之后)

'qwen-vl-max(qwen-oauth)' === 'qwen-oauth:qwen-vl-max' 永远为假 → currentIdx = -1 → ✓ 标记(ModelDialog.tsx:189aria-selected)从不出现,activeIndex 回退到 0

这恰恰是该优化项本想消除的困惑。 最小修复(与 diff 上已有的行内建议一致)是在传给选择器前解码回 ACP:

const colonIdx = value.indexOf(':');
const currentVisionModel = colonIdx > 0
  ? `${value.slice(colonIdx + 1)}(${value.slice(0, colonIdx)})`
  : value;

我确认即便 id 内含冒号,这个逆变换也正确(openai:gpt-4o:onlinegpt-4o:online(openai))。

关于自动化审查的评论(我已代为核验)

  • 编码正确性 —— ✅ 属实。parseModelIdSelectormodelId.ts:90-113)按第一个冒号切分,且当前缀是已知 AuthType 时接受它(qwen-oauth 就是其一)。往返测试通过;修复前的裸 ACP 串确实会丢掉 authType。
  • "voice/fast 同样损坏" —— ⚠️ 过度概括。 我核对了两条路径:
    • handleFastModelSelect 写裸设置——它发送 /model --fast <id>sendPrompt),走命令处理器,是另一条路径。
    • voiceModel 是被 use-voice-input.tstranscribe(...) 消费的 ASR id,经过 resolveModelId
    • 所以这个格式不匹配确实是视觉专属的,PR 的窄范围是站得住脚的。(一个不阻塞的开放问题:ASR 端点是否容忍 ACP 包裹的 id,值得单独看看。)
  • "镜像 CLI 的 encodeVisionModelSelector" —— 大体如此,但该正则未处理 CLI 的 $runtime|authType|modelId 快照 id 和 \0baseUrl。对标准模型没问题;选中运行时快照会编码出畸形 selector。属边缘情形,不阻塞。
  • 零测试覆盖 —— ✅ 属实;我已在本地写好相应测试(编码 + 高亮 A/B + i18n),建议纳入仓库。

建议

编码修复(真正的数据丢失 bug)是正确的——👍 可以合并。 请补上约 3 行的 currentVisionModel 解码,让第 2 步真正生效(很简单,且 PR 明确声称了它),并为编码补一个测试。分发清理(MODE_TITLE_KEY / handlers)与 i18n 键保持现状即可。

Verified locally against head 05c6c71: real worktree build, vitest/tsc, and real-component screenshots via Playwright/Chromium. Not a CI run.

@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot & @wenshao all changes requested have been correctly implemented, kindly take a look

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/App.tsx Outdated
- Extract encodeVisionModelForSetting / decodeVisionModelForPicker into
  shared utils/modelEncoding.ts so they can be tested in isolation
- Add 17 unit tests covering ACP encoding, colon-bearing IDs, empty
  parens passthrough, and round-trip identity
- Memoize modelHandlers record with useMemo to avoid re-allocation on
  every model picker click
- Replace dead fallback (?? 'main') — the outer modelDialogMode guard
  already ensures non-null, so use an explicit if-guard instead
- Add passthrough tests for already-encoded colon format
- Add malformed input tests (bare authType, unclosed paren, double-parens)
- Add leadin-colon malformed input test for decode
- Add empty string passthrough test
- 23 encoding tests passing (up from 17), full suite: 735 passing
@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot & @wenshao kindly review

Comment thread packages/web-shell/client/App.tsx Outdated
// Model IDs from the picker arrive in ACP format: `modelId(authType)`.
// Core's resolveFastModelSelector() expects `authType:modelId`.
const encoded = encodeFastModelForSetting(modelId);
sendPrompt(`/model --fast ${encoded}`).catch((error: unknown) => {

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] encodeFastModelForSetting is dead code here. getModelSelectId() in ModelDialog.tsx strips the (authType) suffix when isFastMode is true (return model.baseModelId ?? model.id.replace(/\([^()]+\)$/, '')), so this handler receives a bare model ID like qwen-max. The regex in encodeFastModelForSetting never matches, making it a pass-through.

The comment "Model IDs from the picker arrive in ACP format" is incorrect for fast mode. The code works because core's resolveModelId has a fallback for bare IDs, but the dead encode call and misleading comment suggest the fast-model path wasn't traced end-to-end.

Consider either removing the dead encode call (and fixing the comment), or changing getModelSelectId to NOT strip authType in fast mode — mirroring the vision path.

— qwen3.7-max via Qwen Code /review

currentModelId={
modelDialogMode === 'voice' ? currentVoiceModel : undefined
modelDialogMode === 'voice'
? currentVoiceModel

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 currentModelId ternary has no 'fast' branch. When modelDialogMode === 'fast', it falls through to undefined, and ModelDialog falls back to connection.currentModel (the main chat model) — highlighting the wrong model in the fast picker.

This PR added currentVisionModel (great!) but missed currentFastModel. Consider adding it following the same derivation pattern.

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/App.tsx Outdated
(modelId: string) => {
setWorkspaceSetting('workspace', 'voiceModel', modelId).catch(
// Model IDs from the picker arrive in ACP format: `modelId(authType)`.
// Voice model resolution expects bare model IDs (not authType:modelId).

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.

[Nice to have] The comment says "Model IDs from the picker arrive in ACP format" but this is incorrect for voice mode — extractVoiceModels() sets id: model.baseModelId (a bare ID with no (authType) suffix). extractBareModelId is a no-op on the already-bare input. The behavior is correct, but the comment documents the wrong contract.

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

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

Two suggestions below — no blocking issues found.

— qwen3.7-max via Qwen Code /review

/**
* Encodes a fast model ID from ACP format (modelId(authType)) to storage format
* (authType:modelId). Core's resolveFastModelSelector() expects this format.
* If the input is not in ACP format, returns it unchanged.

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] encodeVisionModelForSetting and encodeFastModelForSetting have byte-identical implementations (same regex, same template string). Two named exports with the same body is a maintenance trap — a future fix in one could be missed in the other.

Since encodeFastModelForSetting is effectively dead code anyway (the fast-mode picker strips the ACP suffix before calling the handler), consider removing it entirely and using encodeVisionModelForSetting at both call sites — or extracting a single encodeAcpToStorage(modelId) function.

Suggested change
* If the input is not in ACP format, returns it unchanged.
/**
* Encodes a model ID from ACP format (modelId(authType)) to storage format
* (authType:modelId). Used by vision and fast model handlers.
* If the input is not in ACP format, returns it unchanged.
*/
export function encodeAcpToStorage(modelId: string): string {
const match = modelId.match(/^(.+)\(([^()]+)\)$/);
return match ? `${match[2]}:${match[1]}` : modelId;
}

— qwen3.7-max via Qwen Code /review

Comment thread packages/web-shell/client/App.tsx Outdated
const modelHandlers: Record<ModelDialogMode, (id: string) => void> = useMemo(
() => ({
main: handleModelSelect,
fast: handleFastModelSelect,

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.

[Nice to have] The useMemo wrapping modelHandlers provides no benefit here — it's only consumed inside an inline onSelect closure (which already creates a new function reference each render), and ModelDialog is not React.memo-wrapped. The four useCallback dependencies are stable references, so the useMemo dependency-array comparison overhead exceeds the cost of a plain object literal.

A plain const modelHandlers = { main: handleModelSelect, ... } without useMemo would be equivalent and less noisy.

— qwen3.7-max via Qwen Code /review

* (e.g., 'openai:gpt-4o:online' → 'gpt-4o:online(openai)').
* If the input has no colon, returns it unchanged.
*/
export function decodeVisionModelForPicker(storedValue: string): string {

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] decodeVisionModelForPicker doesn't handle the \0baseUrl suffix that the CLI stores for vision models with custom endpoints. In modelCommand.ts:590-596, the CLI constructs authType:modelId\0baseUrl when the matched model has a baseUrl. This decoder splits on the first : and produces a corrupted ACP string like modelId\0https://endpoint(authType), which won't match any picker model ID — the "current" highlight silently fails for custom-endpoint vision models.

Suggested change
export function decodeVisionModelForPicker(storedValue: string): string {
export function decodeVisionModelForPicker(storedValue: string): string {
const nullIdx = storedValue.indexOf('\0');
const selector = nullIdx >= 0 ? storedValue.slice(0, nullIdx) : storedValue;
const colonIdx = selector.indexOf(':');
if (colonIdx > 0) {
return `${selector.slice(colonIdx + 1)}(${selector.slice(0, colonIdx)})`;
}
return selector;
}

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification — real build + tests + screenshot (HEAD f710b40d)

I verified this PR against the actual HEAD with real component/browser tests (not just re-reading the diff). Verdict: correct and mergeable. Both deliverables — the vision-model encoding data-loss fix and the highlight polish — work end-to-end. The one still-open bot [Critical] "current indicator never lights up" is stale (its premise no longer holds at HEAD); I reproduce both the broken and fixed states below.

Method

  • Fresh worktree checked out at the PR head f710b40d.
  • Component tests via the real ModelDialog (jsdom + createRoot), a real-Chromium Playwright render of the picker, and a core round-trip through the real resolveModelId / parseVisionModelSetting in the main clone (native install, to avoid the ajv deep-import split-brain).

1. Encoding fix (the data-loss bug) — ✅ verified end-to-end

handleVisionModelSelect (App.tsx:3350) encodes the picker's ACP id modelId(authType) → core's authType:modelId before persisting. I fed the PR's real encodeVisionModelForSetting output through core's real resolver:

Stored value resolveModelId result Matches a configured model?
qwen-oauth:qwen-vl-max (after fix) {authType:'qwen-oauth', modelId:'qwen-vl-max'} yes
qwen-vl-max(qwen-oauth) (old ACP) {modelId:'qwen-vl-max(qwen-oauth)'} (no authType) ❌ no → silent auto-select fallback

The old ACP string has no authType: prefix, so parseModelIdSelector treats the whole thing (suffix included) as a bare id that can never equal a configured id — exactly the "success toast, choice silently discarded" bug. 5/5 core round-trip tests pass, plus the PR's own 23/23 modelEncoding.test.ts.

2. Highlight polish — ✅ works at HEAD (the [Critical] is stale)

The [Critical] says "currentVisionModel reads the stored setting in authType:modelId format … these two formats never match … the indicator will never light up." That premise is no longer true at HEAD: App.tsx:1667-1673 decodes the stored value back to ACP via decodeVisionModelForPicker before handing it to the picker, and App.tsx:3521-3522 wires it to currentModelId. The decode was added in 4ad0e11a ("address review comments"); GitHub re-anchored the older inline comment onto the latest commit.

I proved it three ways (real component render + real browser), including the counterfactual that the decode is load-bearing:

Case currentModelId passed Result
A — with fix decode('qwen-oauth:qwen-vl-max')qwen-vl-max(qwen-oauth) exactly 1 highlight, on the qwen-vl-max row ✅
B — no decode raw qwen-oauth:qwen-vl-max 0 highlights, roving falls to row 0 (⇐ the bug the [Critical] describes)
C — no derivation undefined falls back to the main model row (the pre-PR regression)

Real-Chromium assertion (computed ::after content = the ):

withFix    : rows=3, checkMarkRows=["2.[qwen-oauth]qwen-vl-max"], ariaSelected=["2.[qwen-oauth]qwen-vl-max"]
withoutFix : rows=3, checkMarkRows=[],                            ariaSelected=[]

vision picker highlight A/B

Left = HEAD (decode wired): the blue + rail sit on qwen-vl-max, and its detail panel shows the true vision modality (text·image·pdf). Right = the same picker if the raw authType:modelId were handed to it undecoded: no anywhere (row-1's faint tint is just default keyboard focus on the main model).

3. i18n — ✅ present in both locales

settings.label.visionModel / settings.description.visionModel and model.setVision exist in EN (i18n.tsx:966,1219-1220) and ZH (:2158,2462-2463) — no bare-key fallback.

4. Tests & types

  • web-shell vitest: 760/760 green. (In a fresh worktree, build-artifact.test.ts's 5 checks read dist/index.js; after npm run build they pass.)
  • tsc -p tsconfig.lib.json: 0 new errors. The 12 errors seen in a symlinked worktree (loadingTranscript, parentAgentId/parentName/depth) are identical at the merge-base without this PR — including in TasksStatusMessage.tsx, a file this PR never touches. They're stale symlinked-SDK declarations, not a PR regression; a clean CI checkout resolves them.

Minor / non-blocking (optional follow-ups)

  1. \0baseUrl decode gap (bot [Suggestion], confirmed real but narrow): decodeVisionModelForPicker splits only on : and doesn't strip the authType:modelId\0baseUrl form the CLI persists for custom-endpoint vision pins → the silently won't light up for those (highlight-only, cross-path CLI→web; the web-shell's own encode never produces \0baseUrl, so its round-trip is fine).
  2. encodeFastModelForSetting is effectively dead (App.tsx:3330): getModelSelectId already strips (authType) for fast mode, so this handler receives a bare id and the regex never fires — harmless pass-through.
  3. currentModelId ternary has no 'fast' branch (App.tsx:3518): the fast picker highlights the main model, not the pinned fast model. Pre-existing (voice/vision only); worth a follow-up.
  4. encodeVisionModelForSetting / encodeFastModelForSetting are byte-identical — a maintenance trap; fine to collapse later.

None of these block merge. LGTM — the data-loss fix and the highlight both verified working.

中文版(完整对应)

✅ 本地验证 —— 真实构建 + 测试 + 截图(HEAD f710b40d

我针对真实 HEAD 做了验证(真跑组件/浏览器测试,不只是重读 diff)。结论:正确,可合并。 两个交付物 —— 视觉模型编码数据丢失修复高亮 polish —— 都端到端可用。唯一仍未关闭的机器人 [Critical]「当前指示器永不点亮」是过期评论(其前提在 HEAD 已不成立);下面我把「坏」和「好」两种状态都复现出来。

方法

  • 在 PR head f710b40d 新建 worktree。
  • 用真实 ModelDialog 做组件测试(jsdom + createRoot)、用真实 Chromium Playwright 渲染选择器、并在主克隆(原生安装,避开 ajv 深导入 split-brain)跑一遍 core 的真实 resolveModelId / parseVisionModelSetting 往返。

1. 编码修复(数据丢失 bug)—— ✅ 端到端验证

handleVisionModelSelectApp.tsx:3350)在持久化前把选择器的 ACP id modelId(authType) 编码成 core 的 authType:modelId。我把 PR 真实的 encodeVisionModelForSetting 输出喂给 core 的真实解析器:

存储值 resolveModelId 结果 是否匹配已配置模型
qwen-oauth:qwen-vl-max(修复后) {authType:'qwen-oauth', modelId:'qwen-vl-max'} ✅ 匹配
qwen-vl-max(qwen-oauth)(旧 ACP) {modelId:'qwen-vl-max(qwen-oauth)'}(无 authType) ❌ 不匹配 → 静默回退自动选择

旧 ACP 串没有 authType: 前缀,parseModelIdSelector 会把整串(含后缀)当作裸 id,永远等不上任何已配置 id —— 正是「弹成功提示但选择被静默丢弃」的 bug。core 往返 5/5 通过,PR 自带 modelEncoding.test.ts 23/23 通过。

2. 高亮 polish —— ✅ HEAD 上有效([Critical] 已过期)

[Critical] 称*「currentVisionModelauthType:modelId 格式读取……两种格式永不相等……指示器永不点亮」*。该前提在 HEAD 已不成立App.tsx:1667-1673 先用 decodeVisionModelForPicker 把存储值解码回 ACP,再由 App.tsx:3521-3522 接到 currentModelId。解码是在 4ad0e11a("address review comments")加入的;GitHub 把旧的行内评论重新锚定到了最新 commit。

我用三种方式(真实组件渲染 + 真实浏览器)证明,并给出「解码不可或缺」的反事实

用例 传入的 currentModelId 结果
A —— 有修复 decode('qwen-oauth:qwen-vl-max')qwen-vl-max(qwen-oauth) 恰好 1 处高亮,落在 qwen-vl-max 行 ✅
B —— 无解码 原始 qwen-oauth:qwen-vl-max 0 处高亮,roving 回退到第 0 行(⇐ [Critical] 描述的 bug)
C —— 无派生 undefined 回退高亮模型行(PR 之前的回归)

真实 Chromium 断言(计算得到的 ::after 内容 = ):

withFix    : rows=3, checkMarkRows=["2.[qwen-oauth]qwen-vl-max"], ariaSelected=["2.[qwen-oauth]qwen-vl-max"]
withoutFix : rows=3, checkMarkRows=[],                            ariaSelected=[]

(截图见上方英文部分)左 = HEAD(已接解码):蓝色 + 左侧竖条落在 qwen-vl-max,其详情面板显示真正的视觉模态(text·image·pdf)。右 = 若把原始 authType:modelId 未解码直接交给选择器:任何行都没有 (第 1 行淡淡的底色只是键盘焦点默认落在主模型上)。

3. i18n —— ✅ 双语齐全

settings.label.visionModel / settings.description.visionModelmodel.setVisionENi18n.tsx:966,1219-1220)与 ZH:2158,2462-2463)均存在,无裸键回退。

4. 测试与类型

  • web-shell vitest:760/760 全绿。(新 worktree 里 build-artifact.test.ts 的 5 项要读 dist/index.jsnpm run build 后即通过。)
  • tsc -p tsconfig.lib.json:0 个新增错误。 符号链接 worktree 里看到的 12 个错误(loadingTranscriptparentAgentId/parentName/depth)在不含本 PR 的 merge-base 上完全一致 —— 包括本 PR 从未改动的 TasksStatusMessage.tsx。这是陈旧的符号链接 SDK 声明导致的 split-brain,不是 PR 回归;干净的 CI 检出即可消除。

次要 / 不阻塞(可选后续)

  1. \0baseUrl 解码缺口(机器人 [Suggestion],确属真实但很窄):decodeVisionModelForPicker 只按 : 切分,不处理 CLI 为自定义端点视觉 pin 持久化的 authType:modelId\0baseUrl,导致这类情况 静默不亮(仅影响高亮,且是 CLI→web 跨路径;web-shell 自身编码从不产生 \0baseUrl,故其往返没问题)。
  2. encodeFastModelForSetting 实为死代码App.tsx:3330):getModelSelectId 在 fast 模式已剥掉 (authType),该 handler 收到裸 id,正则永不命中 —— 无害透传。
  3. currentModelId 三元表达式没有 'fast' 分支App.tsx:3518):fast 选择器高亮的是模型而非固定的 fast 模型。属既有行为(只有 voice/vision),可后续补。
  4. encodeVisionModelForSetting / encodeFastModelForSetting 字节完全相同 —— 维护陷阱,日后可合并。

以上均不阻塞合并。LGTM —— 数据丢失修复与高亮均已验证有效。

Verified locally by the maintainer: real worktree @ f710b40d, real ModelDialog component tests, real-Chromium Playwright render, and a core resolveModelId/parseVisionModelSetting round-trip.

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

No new review findings beyond the existing discussion by @qwen-code-ci-bot and @wenshao. The core data-loss bug (ACP→authType:modelId encoding for the vision picker) is correctly fixed, the new modelEncoding helpers have solid test coverage (23 tests, round-trip + edge cases), i18n keys are added for both EN and ZH, and the MODE_TITLE_KEY/modelHandlers record extraction improves compile-time safety. Deterministic analysis (tsc, eslint, vitest) passes for all changed files. Downgraded from Approve to Comment: CI still running.

— qwen3.7-max via Qwen Code /review

wenshao
wenshao previously approved these changes Jul 3, 2026
AmariahAK and others added 2 commits July 3, 2026 15:11
…ghlight

- decodeVisionModelForPicker: strip \0baseUrl suffix before decoding to ACP
- Remove dead encodeFastModelForSetting (fast picker strips ACP suffix before handler)
- Add currentFastModel derivation + 'fast' branch to currentModelId ternary
- Fix misleading voice handler comment (bare IDs, not ACP)
- Replace unnecessary useMemo on modelHandlers with plain object

Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK

Copy link
Copy Markdown
Contributor Author

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

No new issues found beyond the existing discussion. The encoding fix correctly converts ACP format to authType:modelId, the MODE_TITLE_KEY/handlers record extraction is type-safe and exhaustive, and the new modelEncoding.ts utility has solid test coverage (round-trip, edge cases, \0baseUrl stripping). 761 tests pass, eslint clean.

— qwen3.7-max via Qwen Code /review

@wenshao wenshao added this pull request to the merge queue Jul 3, 2026
Merged via the queue into QwenLM:main with commit 8123c6b Jul 3, 2026
28 checks passed
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.

4 participants