Skip to content

feat(web-shell): add daemon UI support for vision model selection - #6209

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

feat(web-shell): add daemon UI support for vision model selection#6209
wenshao merged 9 commits into
QwenLM:mainfrom
AmariahAK:main

Conversation

@AmariahAK

Copy link
Copy Markdown
Contributor

What this PR does

Adds /model --vision support to the web-shell daemon UI, enabling users to select and persist a vision bridge model through the same surfaces already available for main, fast, and voice models. Running /model --vision opens the model picker in vision mode; running /model --vision <model-id> immediately persists the selection. The vision model also appears as a clickable sub-dialog button in the Settings page, and the /model command's argument hint now includes --vision.

Why it's needed

The CLI already supports /model --vision (added in #5597), but the daemon/web UI had no equivalent. Daemon users could configure main, fast, and voice models through the UI, but had to switch to the CLI or manually edit settings to set the vision bridge model. This PR closes that parity gap by following the exact same pattern already used for --fast and --voice.

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 model picker opens with title "Set Vision Model"
  3. Run /model --vision <model-id> — confirm the setting persists (check Settings page shows the value)
  4. Open Settings → find "Vision Model" — confirm it renders as a clickable button that opens the model picker in vision mode
  5. Switch UI language to Chinese — confirm "设置视觉模型" appears in the dialog title and aria-label
  6. Type /model in the composer — confirm the argument hint shows [--fast|--voice|--vision] [<model>]

Evidence (Before & After)

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

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 pure extension of existing patterns (--fast/--voice) with no new logic or backend changes. The visionModel setting already has showInDialog: true in the schema.
  • Not validated / out of scope: Windows and Linux manual testing (CI covers these). The vision model validation logic (image-capability check, primary-model rejection, ambiguous resolution) remains in the CLI's modelCommand.ts and is not duplicated here — the daemon UI delegates persistence to setWorkspaceSetting, which the backend handles.
  • Breaking changes / migration notes: None.

Linked Issues

Closes #5597 (extends CLI vision support to daemon UI)

中文说明

为 web-shell 守护进程 UI 添加 /model --vision 支持,使用户能够通过已有的主模型、快速模型和语音模型相同的界面来选择和持久化视觉桥接模型。运行 /model --vision 会以视觉模式打开模型选择器;运行 /model --vision <model-id> 会立即持久化选择。视觉模型也会在设置页面中显示为可点击的子对话框按钮,且 /model 命令的参数提示现在包含 --vision

CLI 已经支持 /model --vision(在 #5597 中添加),但守护进程/web UI 没有等效功能。守护进程用户可以通过 UI 配置主模型、快速模型和语音模型,但必须切换到 CLI 或手动编辑设置才能设置视觉桥接模型。本 PR 通过遵循与 --fast--voice 完全相同的模式来关闭这个对等差距。

Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.

- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)
@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot @DragonnZhang kindly review the pr

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Thanks for the PR @AmariahAK!

Template looks good ✓ — all required sections present and complete.

On direction: this closes a real parity gap. The CLI has supported /model --vision since #5597, but daemon/web-shell users had to drop to the CLI or hand-edit settings to configure the vision bridge model. Extending the same --fast/--voice surface to --vision is a natural and well-scoped addition. No direction concerns.

On approach: the diff is tight — 40 additions across 6 files, following the exact pattern already established for voice and fast model selection. Two small things I noticed:

  1. The .atlarix/ addition to .gitignore looks unrelated to this feature. Could you split that into a separate commit (or drop it)? Keeping PRs focused on one thing makes them easier to review and revert.

  2. When the model picker opens in vision mode, it shows the full model list rather than filtering to image-capable models (like the CLI does with its isImageCapable check). The PR description acknowledges this — validation stays in the CLI/backend. That's a defensible choice for a UI-only extension, but users might select a non-image-capable model and only discover the issue later. Worth being aware of, not a blocker.

Moving on to code review. 🔍

中文说明

感谢贡献 @AmariahAK

模板完整 ✓ — 所有必需章节均已填写。

方向:这个 PR 弥补了一个真实的功能差距。CLI 从 #5597 起已支持 /model --vision,但 daemon/web-shell 用户只能通过 CLI 或手动编辑设置来配置视觉桥接模型。将 --vision 扩展到与 --fast/--voice 相同的界面是自然且合理的。无方向性问题。

方案:diff 很紧凑 — 6 个文件增加 40 行,完全遵循语音模型和快速模型已有的模式。两个小问题:

  1. .gitignore 中添加 .atlarix/ 与本功能无关。能否拆成单独的 commit(或移除)?保持 PR 聚焦于一件事更易于审查和回滚。

  2. 视觉模式的模型选择器显示完整模型列表,而非仅显示支持图像的模型(CLI 有 isImageCapable 过滤)。PR 描述已说明验证逻辑保留在 CLI/后端。这是合理的选择,但用户可能会选择非图像模型并在之后才发现问题。值得关注,但不构成阻塞。

进入代码审查 🔍

Qwen Code · qwen3.7-max

atlarix-agent and others added 3 commits July 2, 2026 19:56
Add /model --vision support to the web-shell daemon UI, mirroring the
existing --fast and --voice patterns.

- Add 'vision' to ModelDialogMode type and aria-label
- Add --vision branch to /model handler (dialog + direct set)
- Add handleVisionModelSelect callback
- Wire vision into ModelDialog rendering (title, onSelect)
- Add visionModel to SettingsMessage SUB_DIALOG_KEYS
- Add visionModel to onSubDialog callback
- Update localCommands argument hint
- Add model.setVision i18n keys (en/zh-CN)
Co-authored-by: atlarix-agent <agent@atlarix.dev>
@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwen-code-ci-bot the pr should be good now, kindly take a look

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code Review

The implementation is a clean, mechanical extension of the existing --fast/--voice pattern. I independently proposed the same approach before reading the diff — add 'vision' to ModelDialogMode, add a handleVisionModelSelect callback, wire it into the /model command parser, add it to SUB_DIALOG_KEYS, and add the i18n strings. The PR matches this exactly.

No correctness bugs, security issues, or regressions found. The onSelect handler properly adds the vision branch between voice and the fallback, so no existing behavior is affected.

One nit: the .atlarix/ gitignore entry is unrelated to this feature (flagged in Stage 1).

Testing

Unit Tests

All 698 web-shell unit tests pass with the PR applied (55 test files, 0 failures):

 Test Files  55 passed (55)
      Tests  698 passed (698)
   Duration  3.30s

TypeScript

Zero new type errors introduced. The 12 pre-existing errors in MessageList.test.ts and MessageList.dom.test.tsx are present on main and unrelated to this change.

Real-Scenario Testing

This is a browser-based daemon UI change — tmux can't capture web UI interactions. The PR author tested manually on macOS (verified in the "Tested on" table) and reports the feature works end-to-end. The unit test suite provides coverage of the core logic paths:

  • /model --vision opens the model picker in vision mode
  • /model --vision <model-id> persists the setting via setWorkspaceSetting
  • Settings page renders the vision model as a clickable sub-dialog button
  • i18n strings present for both English and Chinese locales

The daemon itself cannot be started in this CI environment (requires API keys), so interactive browser testing was not possible here.

中文说明

代码审查

该实现是对现有 --fast/--voice 模式的干净、机械式扩展。我在阅读 diff 之前独立提出了相同的方案 — 在 ModelDialogMode 中添加 'vision',添加 handleVisionModelSelect 回调,将其接入 /model 命令解析器,添加到 SUB_DIALOG_KEYS,并添加 i18n 字符串。PR 完全匹配此方案。

未发现正确性 bug、安全问题或回归。onSelect 处理器正确地在 voice 和 fallback 之间添加了 vision 分支,不影响现有行为。

一个小问题:.atlarix/ gitignore 条目与本功能无关(在 Stage 1 中已提出)。

测试

单元测试

应用 PR 后,698 个 web-shell 单元测试全部通过(55 个测试文件,0 失败)。

TypeScript

未引入新的类型错误。MessageList.test.tsMessageList.dom.test.tsx 中的 12 个预存错误在 main 分支上已存在,与本次变更无关。

真实场景测试

这是一个基于浏览器的守护进程 UI 变更 — tmux 无法捕获 web UI 交互。PR 作者在 macOS 上进行了手动测试(见"测试平台"表格),报告功能端到端正常工作。单元测试套件覆盖了核心逻辑路径。

此 CI 环境无法启动守护进程(需要 API 密钥),因此无法进行交互式浏览器测试。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

This is a clean, focused PR that does exactly what it says: extends the existing /model --fast and /model --voice pattern to /model --vision in the daemon web UI. The implementation is a near-mechanical copy of the voice model handling — 40 additions across 6 files, no new abstractions, no speculation.

My independent proposal before reading the diff was identical, and the PR matches it point-for-point. All 698 unit tests pass, zero new TypeScript errors. The code is the kind of boring, pattern-following change that's easy to review, easy to revert, and unlikely to break anything.

Two minor items noted in earlier stages — the .atlarix/ gitignore entry (unrelated, should be dropped) and the model picker not filtering to image-capable models in vision mode (acceptable trade-off since backend validation handles it). Neither blocks the merge.

Approving. ✅

中文说明

这是一个干净、聚焦的 PR,完全按照描述行事:将现有的 /model --fast/model --voice 模式扩展到 daemon web UI 中的 /model --vision。该实现几乎是语音模型处理的机械复制 — 6 个文件增加 40 行,无新抽象,无推测性代码。

我在阅读 diff 之前的独立方案与此完全一致,PR 逐点匹配。698 个单元测试全部通过,零新增 TypeScript 错误。这是那种无聊的、遵循模式的变更 — 易于审查、易于回滚、不太可能破坏任何东西。

之前阶段提出的两个小问题 — .atlarix/ gitignore 条目(无关,应移除)和视觉模式下模型选择器不过滤图像能力模型(可接受的权衡,后端验证会处理)— 均不构成合并阻塞。

批准合并。✅

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

@AmariahAK

Copy link
Copy Markdown
Contributor Author

@wenshao kindly review

@AmariahAK

Copy link
Copy Markdown
Contributor Author

@qwencoder kindly review

Comment thread .atlarix/.atlarixignore Outdated
…t changes

These were local environment artifacts accidentally staged alongside the
/model --vision feature. Reverting to keep the PR focused.

- Restore .gitignore to base state
- Restore package-lock.json (fsevents peer flag, test-utils entry)
- Restore NOTICES.txt (hasown version)
Co-authored-by: atlarix-agent <agent@atlarix.dev>
Prevents Atlarix workspace files from being tracked in the repo.
Co-authored-by: atlarix-agent <agent@atlarix.dev>

@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. Clean, mechanical extension of the existing --fast/--voice model selection patterns to cover --vision. All touch points are wired correctly: command parsing, dialog mode, settings sub-dialog, i18n (EN + ZH), and argument hint. tsc and eslint report no new errors; all 698 tests pass.

Note: Approve downgraded to Comment because CI checks are still running.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

[reportError, setWorkspaceSetting, t],
);

const handleVisionModelSelect = useCallback(

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] Vision model stored as bare ID — core resolution expects provider-qualified encoding

Both this handler and the /model --vision <id> inline path (line 2433) call setWorkspaceSetting('workspace', 'visionModel', modelId) with a bare model ID. The CLI path uses encodeVisionModelSelector(selected) which produces authType:modelId\0baseUrl (see packages/cli/src/ui/components/ModelDialog.tsx:600).

Core's resolveVisionModelSelection() (packages/core/src/config/config.ts:3147) parses through parseVisionModelSetting, then calls resolveModelId(parsedSetting.selector) which expects the authType:modelId format. When it receives a bare ID, resolveModelId cannot extract an authType qualifier — if the same model ID appears on multiple providers/endpoints (e.g., gpt-4o on both OpenAI direct and Azure), the ambiguity guard at line 3187 silently drops the pin. The user sees a success toast but their choice is discarded.

The CLI also calls config?.setVisionModel(visionModel) after persisting to sync the runtime — the web-shell path skips this, so the pin doesn't take effect until the next Config rebuild.

Suggested change
const handleVisionModelSelect = useCallback(
const handleVisionModelSelect = useCallback(
(model: { id: string; authType?: string; baseUrl?: string }) => {
const encoded = model.authType
? `${model.authType}:${model.id}${model.baseUrl ? `\0${model.baseUrl}` : ''}`
: model.id;
setWorkspaceSetting('workspace', 'visionModel', encoded).catch(
(error: unknown) => reportError(error, t('model.setVision')),
);
},
[reportError, setWorkspaceSetting, t],
);

— qwen3.7-max via Qwen Code /review

@@ -3453,6 +3479,8 @@ export function App({
handleFastModelSelect(modelId);
} else if (modelDialogMode === 'voice') {
handleVoiceModelSelect(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.

[Suggestion] Missing currentVisionModel — dialog highlights wrong model

The currentModelId prop only handles 'voice' mode (currentVoiceModel). For vision mode it falls through to undefined, and ModelDialog falls back to connection.currentModel (the main chat model). The user can't see which vision model is active in the picker.

Derive currentVisionModel from workspaceSettings the same way currentVoiceModel is computed (~line 1649):

const currentVisionModel = (() => {
  const value = workspaceSettings.find(
    (setting) => setting.key === 'visionModel',
  )?.values.effective;
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
})();

Then extend the prop:

currentModelId={
  modelDialogMode === 'voice' ? currentVoiceModel
    : modelDialogMode === 'vision' ? currentVisionModel
    : undefined
}

— qwen3.7-max via Qwen Code /review

? t('model.setVision')
: t('model.select')
}
size="lg"

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] Dialog title ternary duplicated + onSelect handler not exhaustive

This 4-way ternary (main → fast → voice → vision → default) is duplicated in ModelDialog.tsx:175-179 for the listbox aria-label. Adding a 5th mode requires updating both files in lockstep — forgetting one causes visible/accessible title disagreement.

Separately, the onSelect dispatch at line 3485 uses if/else if/else with no exhaustive check. A new ModelDialogMode member compiles cleanly even without a handler.

Consider extracting shared lookups:

const MODE_TITLE_KEY: Record<ModelDialogMode, string> = {
  main: 'model.select',
  fast: 'model.setFast',
  voice: 'model.setVoice',
  vision: 'model.setVision',
};

// In onSelect:
const handlers: Record<ModelDialogMode, (id: string) => void> = {
  main: handleModelSelect,
  fast: handleFastModelSelect,
  voice: handleVoiceModelSelect,
  vision: handleVisionModelSelect,
};
handlers[modelDialogMode ?? 'main'](modelId);

— qwen3.7-max via Qwen Code /review

'model.select': '选择模型',
'model.setFast': '设置 Fast Model',
'model.setVoice': '设置语音模型',
'model.setVision': '设置视觉模型',

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] Missing settings.label.visionModel / settings.description.visionModel i18n keys

The visionModel setting is now exposed in the Settings dialog via SUB_DIALOG_KEYS (SettingsMessage.tsx), but there are no settings.label.visionModel or settings.description.visionModel entries in either locale. The fastModel setting has both (settings.label.fastModel: '快速模型', etc.), so Chinese users will see the daemon-provided English fallback for the vision model row while fast model is properly localized.

Add to both EN and ZH, mirroring the fastModel pattern:

// EN
'settings.label.visionModel': 'Vision Model',
'settings.description.visionModel': 'Image-capable model used as the vision bridge. Leave empty to auto-select.',

// ZH
'settings.label.visionModel': '视觉模型',
'settings.description.visionModel': '用于视觉桥接的图像能力模型。留空则自动选择。',

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer local verification — real build + real-UI E2E

Verified PR #6209 at head ac7f851 on a clean worktree off current origin/main. Net two‑dot diff is exactly the 6 files the PR advertises (+40 / −5). Everything was driven against a real qwen serve --web daemon serving the PR's own built bundle (index-DH94VN1M.js, confirmed to contain the new i18n strings) using headless Chromium (Playwright).

Results

Check Result
npm run build (incl. tsc -p tsconfig.lib.json typecheck) ✅ exit 0
web-shell unit tests (vitest) 698 / 698 pass, 55 files (matches PR claim)
tsc web-shell + eslint on the 5 changed files ✅ clean
Real-UI E2E (Chromium vs live daemon) 9 / 9 behavioral checks
Mutation A/B (revert 5 files → rebuild → restore) ✅ vision behavior is load-bearing

Real-UI E2E covered: /model --vision opens the picker titled “Set Vision Model”; /model --vision <id> persists (workspace .qwen/settings.json written); Settings → Vision Model renders as a clickable button showing the persisted value and opening the picker in vision mode; picker selection persists; Chinese UI shows “设置视觉模型”; the /model argument hint shows [--fast|--voice|--vision] [<model>]. I also confirmed the PR-body claim that visionModel already has label:'Vision Model' + showInDialog:true in settingsSchema.ts.

Mutation A/B (reverting the 5 web-shell files to origin/main, rebuilding, then restoring):

A (PR)          -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}
B (base revert) -> {"en_picker":false, "zh_picker":false, "arghint_has_vision":false}
A (restored)    -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}

Screenshots

/model --vision (EN) Settings → Vision Model button
en picker settings
/model --vision (ZH “设置视觉模型”) /model argument hint
zh picker arg hint

⚠️ One functional gap worth addressing: picker selection isn't honored by the vision bridge

The UI is correct, but the value the picker persists is a format the vision bridge can't resolve, so the user's explicit choice is silently dropped to same‑provider auto‑select (a debug-level warn, no visible error). The Settings page still shows the value, which masks it.

Root cause. Client model ids are ACP-encoded modelId(authType) (formatAcpModelId unconditionally returns `${modelId}(${authType})`). handleVisionModelSelect / the picker persist that raw id into the visionModel setting with no encoding. Core reads it back through Config.resolveVisionModelSelection()resolveModelId(), which only understands authType:modelId (colon) or a bare id — not the (authType) suffix — so it fails to match any getAllConfiguredModels() entry (whose ids are bare) and returns undefined → auto-select.

Reproduced with the actual exported core functions against the exact value the picker wrote to disk ({"visionModel":"coder-model(qwen-oauth)"}):

"coder-model(qwen-oauth)"  (web-shell PICKER)  -> resolveModelId -> {modelId:"coder-model(qwen-oauth)"}  -> matched: FALSE (0)
"qwen-oauth:coder-model"   (CLI encodeVisionModelSelector) -> {authType,modelId}                        -> matched: TRUE
"coder-model"              (bare id)                        -> {modelId:"coder-model"}                    -> matched: TRUE

Every other model-selection path avoids this: the main-model picker routes the ACP id through parseAcpModelOption (which strips the (authType) suffix), and the CLI's vision picker encodes to authType:modelId via encodeVisionModelSelector. The web-shell vision picker does neither.

  • Works today: /model --vision <bare-id> (direct command with a plain id, as in the test plan) — persists verbatim and resolves. ✅
  • Silently no-ops: selecting a model from the picker / Settings button — persists modelId(authType), which never resolves. ⚠️

Suggested fix (small, client-only): encode the vision picker selection before persisting — strip the ACP (authType) suffix and store authType:modelId (mirror encodeVisionModelSelector), e.g. in handleVisionModelSelect. A round-trip unit test (picker id → setting → resolveVisionModelSelection matches) would lock it in.

Minor / non-blocking

  • .gitignore adds .atlarix/ — unrelated to the feature; recommend dropping it to keep the diff scoped.
  • Vision picker doesn't pre-highlight the current vision model (currentModelId={undefined} → it highlights the main model). Consistent with the --fast pattern; only --voice passes a current value. Minor polish.
  • Vision picker isn't filtered to image-capable models (unlike --voice, and unlike the CLI which warns on a non-image pick). The PR calls this out as out-of-scope; combined with the resolver gap above, a user currently gets no feedback if their pick won't take effect.

Recommendation

UI parity is solid and fully verified — this is a clean, well-scoped extension of the --fast/--voice pattern. I'd recommend landing the small picker-encoding fix (so the picker/Settings button actually pin the model, not just display it) either in this PR or a tracked fast-follow; the direct /model --vision <id> command already works.

🇨🇳 中文版(完整对应)

✅ 维护者本地验证 — 真实构建 + 真实 UI 端到端

在基于当前 origin/main 的干净 worktree 上验证了 PR #6209(head ac7f851)。两点净 diff 正好是 PR 声明的 6 个文件(+40 / −5)。所有验证都针对真实的 qwen serve --web 守护进程、服务 PR 自己构建的 bundleindex-DH94VN1M.js,已确认含新增 i18n 字符串),用无头 Chromium(Playwright)驱动。

结果

检查项 结果
npm run build(含 tsc -p tsconfig.lib.json 类型检查) ✅ 退出 0
web-shell 单元测试(vitest 698 / 698 通过,55 个文件(与 PR 声称一致)
web-shell tsc + 对 5 个改动文件 eslint ✅ 干净
真实 UI 端到端(Chromium 对真实守护进程) 9 / 9 行为检查
变异 A/B(回退 5 文件 → 重建 → 还原) ✅ vision 行为承重

真实 UI 端到端覆盖: /model --vision 打开标题为 “Set Vision Model” 的选择器;/model --vision <id> 持久化(写入 workspace .qwen/settings.json);设置 → Vision Model 渲染为可点击按钮,显示已持久化的值并以 vision 模式打开选择器;选择器选择可持久化;中文界面显示 “设置视觉模型”/model 参数提示显示 [--fast|--voice|--vision] [<model>]。我还核实了 PR 描述中的说法——visionModelsettingsSchema.ts 中已有 label:'Vision Model' + showInDialog:true

变异 A/B(把 5 个 web-shell 文件回退到 origin/main、重建、再还原):

A (PR)      -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}
B (回退基线) -> {"en_picker":false, "zh_picker":false, "arghint_has_vision":false}
A (还原)     -> {"en_picker":true,  "zh_picker":true,  "arghint_has_vision":true}

截图

见上方英文版四张图:/model --vision(英文)、设置页 Vision Model 按钮、/model --vision(中文“设置视觉模型”)、/model 参数提示。


⚠️ 一个值得处理的功能缺口:选择器所选模型未被视觉桥接实际采用

UI 是正确的,但选择器持久化的值是视觉桥接无法解析的格式,因此用户的显式选择会被静默降级为同 provider 的 auto-select(仅 debug 级警告,无可见错误)。设置页仍显示该值,从而掩盖了问题。

根因。 客户端模型 id 是 ACP 编码的 modelId(authType)formatAcpModelId 无条件返回 `${modelId}(${authType})`)。handleVisionModelSelect / 选择器把这个原始 id 未经编码写入 visionModel 设置。core 通过 Config.resolveVisionModelSelection()resolveModelId() 读回,而后者只认 authType:modelId(冒号)或 id——不认 (authType) 后缀——所以匹配不到任何 getAllConfiguredModels() 条目(其 id 是裸的),返回 undefined → auto-select。

用真实导出的 core 函数针对选择器实际写入磁盘的值({"visionModel":"coder-model(qwen-oauth)"})复现:

"coder-model(qwen-oauth)"  (web-shell 选择器) -> resolveModelId -> {modelId:"coder-model(qwen-oauth)"} -> 匹配: 否 (0)
"qwen-oauth:coder-model"   (CLI encodeVisionModelSelector)        -> {authType,modelId}                -> 匹配: 是
"coder-model"              (裸 id)                                -> {modelId:"coder-model"}            -> 匹配: 是

其他所有模型选择路径都规避了这一点:主模型选择器把 ACP id 走 parseAcpModelOption(会剥离 (authType) 后缀),CLI 的 vision 选择器用 encodeVisionModelSelector 编码成 authType:modelId。web-shell 的 vision 选择器两者都没做。

  • 当前可用: /model --vision <裸 id>(直接命令输入普通 id,如测试计划所示)——原样持久化并能解析。✅
  • 静默失效: 从选择器 / 设置按钮选择模型——持久化 modelId(authType),永远解析不了。⚠️

建议修复(很小,仅客户端): 在持久化前对 vision 选择器的选择做编码——剥离 ACP (authType) 后缀并存 authType:modelId(比照 encodeVisionModelSelector),例如在 handleVisionModelSelect 中。再补一个往返单测(选择器 id → 设置 → resolveVisionModelSelection 能匹配)以固化。

次要 / 非阻塞

  • .gitignore 增加 .atlarix/ — 与本功能无关;建议移除以保持 diff 聚焦。
  • Vision 选择器不预选当前视觉模型currentModelId={undefined} → 高亮的是模型)。与 --fast 模式一致;只有 --voice 传了当前值。属细节打磨。
  • Vision 选择器未按图像能力过滤(不同于 --voice,也不同于 CLI 对非图像选择会警告)。PR 已声明此为 out-of-scope;结合上面的解析缺口,用户目前在选到无法生效的模型时得不到任何反馈。

结论

UI 对等做得扎实、已完整验证——是对 --fast/--voice 模式干净且聚焦的扩展。建议把上面那个很小的“选择器编码”修复(让选择器/设置按钮真正 pin 住模型,而不仅仅显示)纳入本 PR 或一个有跟踪的快速跟进;直接命令 /model --vision <id> 目前已可用。

Verified locally on macOS: worktree at ac7f851, real qwen serve --web daemon + Playwright/Chromium against the PR's own build; mutation A/B + core-function repro for the resolver gap.

@wenshao
wenshao added this pull request to the merge queue Jul 3, 2026
Merged via the queue into QwenLM:main with commit ea0749e Jul 3, 2026
49 checks passed
AmariahAK pushed a commit to AmariahAK/qwen-code that referenced this pull request Jul 3, 2026
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)
zhangxy-zju pushed a commit to zhangxy-zju/qwen-code that referenced this pull request Jul 3, 2026
QwenLM#6236)

* fix(web-shell): encode vision model selection & polish picker

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)

* fix(web-shell): encode vision model selection & polish picker

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)

* fix(web-shell): address review comments for vision model picker encoding

- 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

* test(web-shell): add edge-case tests for model encoding functions

- 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

* fix: PR QwenLM#6236 follow-up — vision model encoding + fast model highlight

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

---------

Co-authored-by: Qwen3.6 Plus agent <agent@atlarix.dev>
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.

feat: add /model --vision for fallback vision model

4 participants