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

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 #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 #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

@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:111 → availableModels.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:online → gpt-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.ts → transcribe(...), 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:111 → availableModels.findIndex((m) => m.id === currentModel)
  • m.id 是 ACP 格式 modelId(authType)(构造于 packages/cli/src/utils/acpModelUtils.ts:20)
  • currentVisionModel 是 authType:modelId(App.tsx,编码修复之后)

'qwen-vl-max(qwen-oauth)' === 'qwen-oauth:qwen-vl-max' 永远为假 → currentIdx = -1 → ✓ 标记(ModelDialog.tsx:189、aria-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:online → gpt-4o:online(openai))。

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

  • 编码正确性 —— ✅ 属实。parseModelIdSelector(modelId.ts:90-113)按第一个冒号切分,且当前缀是已知 AuthType 时接受它(qwen-oauth 就是其一)。往返测试通过;修复前的裸 ACP 串确实会丢掉 authType。
  • "voice/fast 同样损坏" —— ⚠️ 过度概括。 我核对了两条路径:
    • handleFastModelSelect 不写裸设置——它发送 /model --fast <id>(sendPrompt),走命令处理器,是另一条路径。
    • voiceModel 是被 use-voice-input.ts → transcribe(...) 消费的 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

- 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

@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)—— ✅ 端到端验证

handleVisionModelSelect(App.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] 称*「currentVisionModel 以 authType: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.visionModel 与 model.setVision 在 EN(i18n.tsx:966,1219-1220)与 ZH(:2158,2462-2463)均存在,无裸键回退。

4. 测试与类型

  • web-shell vitest:760/760 全绿。(新 worktree 里 build-artifact.test.ts 的 5 项要读 dist/index.js;npm run build 后即通过。)
  • tsc -p tsconfig.lib.json:0 个新增错误。 符号链接 worktree 里看到的 12 个错误(loadingTranscript、parentAgentId/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.

wenshao
wenshao previously approved these changes Jul 3, 2026
AmariahAK and others added 2 commits July 3, 2026 15:11
- 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

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

3 participants