Skip to content

feat(web-shell): add 'voice' to ComposerToolbarAction for external visibility control - #5947

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
chiga0:feat/web-shell-voice-toolbar-action
Jun 28, 2026
Merged

feat(web-shell): add 'voice' to ComposerToolbarAction for external visibility control#5947
wenshao merged 1 commit into
QwenLM:mainfrom
chiga0:feat/web-shell-voice-toolbar-action

Conversation

@ytahdn

@ytahdn ytahdn commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds voice as a new member of the ComposerToolbarAction union type and wraps the VoiceButton rendering with the existing showToolbarAction() gate. This allows external consumers of the web-shell component to control whether the voice dictation button is visible, using the same composerToolbarActions prop already used for other toolbar actions.

Why it's needed

When embedding the web-shell as a component, external consumers currently have no way to hide the voice input button. The only existing visibility control is the daemon-side voice_transcribe capability check, which is not accessible from the embedding application. By adding voice to ComposerToolbarAction, consumers can now hide the voice button simply by omitting it from the composerToolbarActions array — consistent with how approvalMode, model, commands, files, and widthMode are already controlled.

Reviewer Test Plan

How to verify

  1. Build the web-shell package: npm run build
  2. Render <WebShell /> without composerToolbarActions prop — voice button should still appear (backward compatible, default behavior unchanged)
  3. Render <WebShell composerToolbarActions={['approvalMode', 'model', 'commands', 'files']} /> — voice button should be hidden
  4. Render <WebShell composerToolbarActions={['voice']} /> — only voice button should appear in the toolbar area
  5. Run VoiceButton unit tests: cd packages/web-shell && npx vitest run client/voice/VoiceButton.test.tsx

Evidence (Before & After)

N/A — this is a programmatic API change; no visual difference unless the consumer explicitly excludes voice.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

npm run build and npx vitest run on macOS.

Risk & Scope

  • Main risk or tradeoff: None — fully backward compatible. Omitting composerToolbarActions still shows all actions including voice.
  • Not validated / out of scope: No changes to the daemon-side capability check.
  • Breaking changes / migration notes: None.

Linked Issues

N/A

中文说明

本 PR 做了什么

ComposerToolbarAction 联合类型中新增 voice 成员,并用已有的 showToolbarAction() 门控包裹 VoiceButton 的渲染。这使得 web-shell 组件的外部消费者可以通过已有的 composerToolbarActions prop 来控制语音输入按钮的显隐。

为什么需要这个改动

当以组件形式嵌入 web-shell 时,外部消费者目前无法隐藏语音输入按钮。唯一的显隐控制是 daemon 端的 voice_transcribe capability 检查,嵌入方无法触达。将 voice 加入 ComposerToolbarAction 后,消费者只需在 composerToolbarActions 数组中省略它即可隐藏——与 approvalModemodelcommandsfileswidthMode 的控制方式一致。

风险与范围

  • 主要风险:无——完全向后兼容。不传 composerToolbarActions 时仍然显示所有操作按钮(包括语音)。
  • 未验证/不在范围内:未修改 daemon 端的 capability 检查逻辑。
  • 破坏性变更/迁移说明:无。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

On direction: this is a natural extension of the existing ComposerToolbarAction API. Embedders who consume <WebShell /> already control visibility of approvalMode, model, commands, files, and widthMode — voice was the one toolbar element that couldn't be gated from the outside. Claude Code's CHANGELOG shows steady voice-related fixes (dictation silence, push-to-talk, /voice policy messaging), so this area is clearly active. Adding programmatic control for the voice button fills a real gap for embedders.

On approach: the diff is exactly the minimal change — one union member added, one existing showToolbarAction() gate applied. No scope creep, no drive-by refactors. The backward-compat story is correct too: showToolbarAction returns true when visibleToolbarActions is unset, so omitting the prop still shows the voice button as before. Nothing to cut.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

方向:这是对现有 ComposerToolbarAction API 的自然扩展。嵌入 <WebShell /> 的消费方已经可以控制 approvalModemodelcommandsfileswidthMode 的显隐——唯独语音按钮无法从外部控制。Claude Code 的 CHANGELOG 中持续有语音相关的修复(语音静音、按键说话、/voice 策略提示),说明这一区域仍在活跃迭代。为语音按钮增加程序化控制填补了嵌入方的真实缺口。

方案:diff 恰好是最小改动——联合类型加一个成员,用已有的 showToolbarAction() 门控包裹。没有范围蔓延,没有顺手重构。向后兼容也正确:不传 visibleToolbarActionsshowToolbarAction 返回 true,语音按钮照常显示。没有可砍的部分。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: add 'voice' to the ComposerToolbarAction union and wrap <VoiceButton> with the existing showToolbarAction() gate — identical to how every other toolbar element is already controlled. The PR matches this exactly.

The diff is clean: one union member, one conditional wrapper, no filler. The showToolbarAction function correctly returns true when visibleToolbarActions is unset, so backward compat holds. No reuse issues — the gate pattern is already the shared utility here.

No blockers found.

Testing

This is a programmatic API change (no TUI-visible difference), so testing focused on build + unit tests.

VoiceButton unit tests

 RUN  v3.2.4 /home/github-runner/actions-runner/_work/qwen-code/qwen-code/packages/web-shell/client

 ✓ voice/VoiceButton.test.tsx (3 tests) 34ms

 Test Files  1 passed (1)
      Tests  3 passed (3)
   Start at  10:17:21
   Duration  570ms (transform 74ms, setup 0ms, collect 114ms, tests 34ms, environment 222ms, prepare 57ms)

Project build

> @qwen-code/web-shell@0.19.2 build

✖ 15 problems (0 errors, 15 warnings)
  0 errors and 15 warnings potentially fixable with the `--fix` option.

Build exits cleanly. The 15 lint warnings are all pre-existing curly warnings in vscode-ide-companion/src/webview/providers/WebViewProvider.ts — unrelated to this PR.

Typecheck

Standalone tsc --noEmit in web-shell shows pre-existing errors across the package (missing react/vitest type declarations when running outside the monorepo build pipeline). No new errors introduced by this PR — ChatEditor.tsx has zero type errors related to the 'voice' union addition.

中文说明

代码审查

独立方案:在 ComposerToolbarAction 联合类型中加 'voice',然后用已有的 showToolbarAction() 门控包裹 <VoiceButton>——与其他工具栏元素的控制方式完全一致。PR 的实现与此吻合。

Diff 干净:一个联合成员、一个条件包裹、无多余改动。showToolbarActionvisibleToolbarActions 未设置时正确返回 true,向后兼容成立。无复用问题——门控模式已经是这里的共享工具函数。

未发现阻塞问题。

测试

这是一个程序化 API 变更(无 TUI 可见差异),因此测试聚焦于构建和单元测试。

VoiceButton 单元测试:3/3 通过 ✅。项目构建成功(0 错误,15 个 lint 警告均为 vscode-ide-companion 中已有的 curly 警告,与本 PR 无关)。独立 tsc --noEmit 未引入新错误。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is the kind of PR that's easy to approve: it does one thing, does it the obvious way, and doesn't try to be more than it needs to be.

The problem is real — embedders of <WebShell /> already control five toolbar actions via composerToolbarActions, but the voice button was the one holdout that could only be gated by the daemon's capability check. The fix is the minimal two-part change: extend the union type, apply the existing gate. No new abstractions, no parallel mechanism, no scope creep.

Going back to my independent proposal from Stage 2: the PR matches it exactly. I can't think of a simpler path that solves the stated problem.

Build passes, tests pass, backward compat holds (omitting the prop still shows voice). Nothing to object to.

Approving. ✅

中文说明

这是一个容易批准的 PR:只做一件事,用最显而易见的方式,没有多余的企图。

问题是真实的——<WebShell /> 的嵌入方已经可以通过 composerToolbarActions 控制五个工具栏动作,唯独语音按钮是唯一的例外,只能通过 daemon 端的 capability 检查来控制。修复方案是最小的两部分改动:扩展联合类型,应用已有门控。没有新抽象,没有并行机制,没有范围蔓延。

回顾第二阶段的独立方案:PR 与之完全吻合。想不出更简单的路径来解决所陈述的问题。

构建通过,测试通过,向后兼容成立(不传 prop 时语音按钮照常显示)。没有可反对的地方。

批准 ✅

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

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No issues found. voice is gated with the same showToolbarAction() pattern as the other five toolbar actions, and the default path (no composerToolbarActions prop → visibleActionSet is null → returns true) still renders the voice button, so the change is backward compatible. The gating mechanism (visibleToolbarActions/composerToolbarActions) landed only days ago in #5775 and had no external consumers, so completing it for voice carries no migration risk. JSX nesting and cross-file usage (App.tsx passthrough) check out.

Downgraded from Approve to Comment: CI still running. — claude-opus-4-8[1m] via Qwen Code /qreview

@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 issues found. Downgraded from Approve to Comment: CI still running.

The change is minimal and correct — adds 'voice' to the ComposerToolbarAction union and wraps VoiceButton with the existing showToolbarAction() gate, following the identical pattern used by all 5 other toolbar actions. Default behavior (no composerToolbarActions prop → voice button visible) is preserved. Build and tests pass. LGTM ✅

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real browser test of the voice toolbar gate (macOS)

Verdict: LGTM, behaviour confirmed. Against a live qwen serve --web daemon serving the built web-shell SPA on macOS, the voice button now follows composerToolbarActions exactly like the other toolbar actions: shown by default (backward compatible), hidden when the prop omits voice, and shown on its own when the prop is ['voice'] — all while the model button proves the gate is selective, not all-or-nothing. The web-shell suite is green (538) and tsc is clean for the new union member.

Environment

Platform macOS (Darwin 25.5.0, arm64)
Node v22.22.2
PR head 2894e3c8c (fresh worktree, npm ci + build)
Runtime real daemon qwen serve --web (127.0.0.1:8767) serving packages/web-shell/dist
Driver Playwright 1.57 + Chromium 1200, headless

The standalone entry doesn't expose composerToolbarActions via URL, so I temporarily drove that prop from a ?toolbar= query param in main.tsx (not a PR file), rebuilt, and reverted. The gate under test (showToolbarAction('voice') in ChatEditor.tsx) is unmodified.

1. Tests & types (PR head)

Result
VoiceButton.test.tsx ✅ 3 passed
Full web-shell suite 538 passed (36 files) — union member adds no regression
tsc --noEmit -p packages/web-shell/tsconfig.json ✅ clean (exit 0)

2. Real browser — gating matrix (8/8 checks pass)

Counts are live aria-label element counts: voice = Start voice dictation, model = Select Model (a second action, to prove the gate is selective).

composerToolbarActions URL voice model Meaning
(omitted) / 1 (shown) 1 Default unchanged — backward compatible
['approvalMode','model','commands','files'] ?toolbar=approvalMode,model,commands,files 0 (hidden) 1 Omitting voice hides it; others stay → selective
['voice'] ?toolbar=voice 1 (shown) 0 Voice independently controllable
[] ?toolbar= 0 (hidden) 0 Empty hides everything incl. voice

Visually confirmed in captures: the default composer shows the 🎤 alongside Ask-Approval / model / / / @; with ['voice'] the toolbar shows only the 🎤 (and the always-present send arrow), every other action gone.

3. Code observations (verified)

  • The change is minimal and correct: 'voice' added to the ComposerToolbarAction union, and <VoiceButton> wrapped in {showToolbarAction('voice') && …}. showToolbarAction returns true when no composerToolbarActions prop is set, hence the backward-compatible default.
  • The new prop gate is ANDed with the existing capability gate: VoiceButton still returns null unless the daemon advertises voice_transcribe. I confirmed the daemon advertises it (GET /capabilitiesfeatures includes voice_transcribe), so the button genuinely renders and the prop gate is what flips it on/off above.

Notes / not covered

  • No unit test covers the toolbar gating — not for voice, and not for any pre-existing action (approvalMode/model/… ); there is no ChatEditor gating test at all. The change rides the already-shared showToolbarAction path, so this real-browser matrix is the primary behavioural check. A small ChatEditor test asserting the voice gate would be a nice (non-blocking) follow-up.
  • The daemon-side voice_transcribe capability check is unchanged (and out of scope, as the PR states). Linux/Windows not exercised; this re-confirms macOS.
🇨🇳 中文版(完整对应)

✅ 维护者验证 —— 语音工具栏门控真实浏览器测试(macOS)

结论:LGTM,行为已确认。 在 macOS 上针对真实 qwen serve --web daemon(服务已构建的 web-shell SPA),语音按钮现在与其他工具栏操作一样遵循 composerToolbarActions:默认显示(向后兼容)、当 prop 省略 voice 时隐藏、当 prop 为 ['voice'] 时单独显示——同时用 model 按钮证明门控是选择性的而非全有全无。web-shell 套件全绿(538),新联合成员的 tsc 干净。

环境

平台 macOS(Darwin 25.5.0, arm64)
Node v22.22.2
PR head 2894e3c8c(全新 worktree,npm ci + 构建)
运行时 真实 daemon qwen serve --web127.0.0.1:8767)服务 packages/web-shell/dist
驱动 Playwright 1.57 + Chromium 1200(headless)

standalone 入口不通过 URL 暴露 composerToolbarActions,所以我临时在 main.tsx(非 PR 文件)里用 ?toolbar= query 参数驱动该 prop,重建后已还原。被测门控(ChatEditor.tsx 里的 showToolbarAction('voice'))未改动。

1. 测试与类型(PR head)

结果
VoiceButton.test.tsx ✅ 3 通过
整个 web-shell 套件 538 通过(36 文件)—— 联合成员无回归
tsc --noEmit -p packages/web-shell/tsconfig.json ✅ 干净(exit 0)

2. 真实浏览器 —— 门控矩阵(8/8 通过)

计数为实时 aria-label 元素数:voice = Start voice dictation,model = Select Model(第二个操作,用于证明门控的选择性)。

composerToolbarActions URL voice model 含义
(省略) / 1(显示) 1 默认不变 —— 向后兼容
['approvalMode','model','commands','files'] ?toolbar=approvalMode,model,commands,files 0(隐藏) 1 省略 voice 即隐藏;其他仍在 → 选择性
['voice'] ?toolbar=voice 1(显示) 0 voice 可独立控制
[] ?toolbar= 0(隐藏) 0 空数组隐藏一切(含 voice)

截图中已目视确认:默认 composer 在 Ask-Approval / model / / / @ 旁显示 🎤;当 ['voice'] 时工具栏显示 🎤(以及始终存在的发送箭头),其余操作全部消失。

3. 代码观察(已核实)

  • 改动最小且正确:在 ComposerToolbarAction 联合类型中加入 'voice',并把 <VoiceButton> 包进 {showToolbarAction('voice') && …}showToolbarAction 在未设置 composerToolbarActions 时返回 true,因此默认向后兼容。
  • 新的 prop 门控与既有 capability 门控是 AND 关系VoiceButton 在 daemon 未广告 voice_transcribe 时仍返回 null。我已确认 daemon 广告了它(GET /capabilitiesfeaturesvoice_transcribe),所以按钮确实渲染,上面是 prop 门控在控制其显隐。

说明 / 未覆盖

  • 没有任何单测覆盖工具栏门控 —— 不只是 voice,连既有的 approvalMode/model/… 也没有;根本没有 ChatEditor 门控测试。该改动复用已共享的 showToolbarAction 路径,所以这个真实浏览器矩阵是主要的行为验证。补一个断言 voice 门控的小型 ChatEditor 测试会是不错的(非阻塞)后续。
  • daemon 端 voice_transcribe capability 检查未改动(且如 PR 所述不在范围内)。本次未覆盖 Linux/Windows;此为 macOS 复核。

🤖 Verified locally on macOS with a real qwen serve --web daemon + Playwright/Chromium against the built SPA. The composerToolbarActions prop was driven via a temporary main.tsx URL-param shim (reverted; the gate under test is unmodified). Posted by the maintainer as a merge reference.

core.focus();
}}
/>
{showToolbarAction('voice') && (

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] Silent behavior change for embedders with an explicit composerToolbarActions allowlist. Before this PR, VoiceButton rendered unconditionally — 'voice' wasn't in the union, so no consumer could include it in their allowlist. After this change, any embedder passing an explicit list (e.g., ['approvalMode', 'model', 'commands', 'files', 'widthMode']) silently loses the voice button on upgrade — no TypeScript error, no runtime warning.

The PR describes this as "fully backward compatible," which is true for the default path (no prop → all actions show), but not for explicit allowlist consumers. Consider adding a CHANGELOG entry or migration note flagging this behavioral shift, so integrators know to add 'voice' to their existing arrays.

— qwen3.7-max via Qwen Code /review

@wenshao
wenshao added this pull request to the merge queue Jun 28, 2026
Merged via the queue into QwenLM:main with commit 8f1118a Jun 28, 2026
58 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