feat(web-shell): add daemon UI support for vision model selection - #6209
Conversation
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)
|
@qwen-code-ci-bot @DragonnZhang kindly review the pr |
|
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 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:
Moving on to code review. 🔍 中文说明感谢贡献 @AmariahAK! 模板完整 ✓ — 所有必需章节均已填写。 方向:这个 PR 弥补了一个真实的功能差距。CLI 从 #5597 起已支持 方案:diff 很紧凑 — 6 个文件增加 40 行,完全遵循语音模型和快速模型已有的模式。两个小问题:
进入代码审查 🔍 — Qwen Code · qwen3.7-max |
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>
|
@qwen-code-ci-bot the pr should be good now, kindly take a look |
Code ReviewThe implementation is a clean, mechanical extension of the existing No correctness bugs, security issues, or regressions found. The One nit: the TestingUnit TestsAll 698 web-shell unit tests pass with the PR applied (55 test files, 0 failures): TypeScriptZero new type errors introduced. The 12 pre-existing errors in Real-Scenario TestingThis 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:
The daemon itself cannot be started in this CI environment (requires API keys), so interactive browser testing was not possible here. 中文说明代码审查该实现是对现有 未发现正确性 bug、安全问题或回归。 一个小问题: 测试单元测试应用 PR 后,698 个 web-shell 单元测试全部通过(55 个测试文件,0 失败)。 TypeScript未引入新的类型错误。 真实场景测试这是一个基于浏览器的守护进程 UI 变更 — tmux 无法捕获 web UI 交互。PR 作者在 macOS 上进行了手动测试(见"测试平台"表格),报告功能端到端正常工作。单元测试套件覆盖了核心逻辑路径。 此 CI 环境无法启动守护进程(需要 API 密钥),因此无法进行交互式浏览器测试。 — Qwen Code · qwen3.7-max |
|
This is a clean, focused PR that does exactly what it says: extends the existing 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 Approving. ✅ 中文说明这是一个干净、聚焦的 PR,完全按照描述行事:将现有的 我在阅读 diff 之前的独立方案与此完全一致,PR 逐点匹配。698 个单元测试全部通过,零新增 TypeScript 错误。这是那种无聊的、遵循模式的变更 — 易于审查、易于回滚、不太可能破坏任何东西。 之前阶段提出的两个小问题 — 批准合并。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
@wenshao kindly review |
|
@qwencoder kindly review |
…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
left a comment
There was a problem hiding this comment.
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
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
| [reportError, setWorkspaceSetting, t], | ||
| ); | ||
|
|
||
| const handleVisionModelSelect = useCallback( |
There was a problem hiding this comment.
[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.
| 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); | |||
There was a problem hiding this comment.
[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" |
There was a problem hiding this comment.
[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': '设置视觉模型', |
There was a problem hiding this comment.
[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
✅ Maintainer local verification — real build + real-UI E2EVerified PR #6209 at head Results
Real-UI E2E covered: Mutation A/B (reverting the 5 web-shell files to Screenshots
|
| 检查项 | 结果 |
|---|---|
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 描述中的说法——visionModel 在 settingsSchema.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.
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)
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>




What this PR does
Adds
/model --visionsupport 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 --visionopens 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/modelcommand'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--fastand--voice.Reviewer Test Plan
How to verify
npm run dev:daemon) and open the web UI/model --vision— confirm the model picker opens with title "Set Vision Model"/model --vision <model-id>— confirm the setting persists (check Settings page shows the value)/modelin 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
Environment (optional)
npm run dev:daemon(local),cd packages/web-shell && npx vitest run(unit tests)Risk & Scope
--fast/--voice) with no new logic or backend changes. ThevisionModelsetting already hasshowInDialog: truein the schema.modelCommand.tsand is not duplicated here — the daemon UI delegates persistence tosetWorkspaceSetting, which the backend handles.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完全相同的模式来关闭这个对等差距。