Skip to content

fix(cli): show ⌥T instead of alt+T on macOS for thinking expansion#3

Closed
Alex-ai-future wants to merge 549 commits into
mainfrom
fix/alt-t
Closed

fix(cli): show ⌥T instead of alt+T on macOS for thinking expansion#3
Alex-ai-future wants to merge 549 commits into
mainfrom
fix/alt-t

Conversation

@Alex-ai-future

Copy link
Copy Markdown
Owner

What this PR does

Changes the thinking block expand/collapse keyboard shortcut hint from "alt+t" to "⌥t" on macOS, so the on-screen hint matches the actual key users need to press.

Why it's needed

The keybinding is defined as { key: 't', meta: true }, which maps to the Option (⌥) key on Mac. However, the UI always displayed "alt+t to expand/collapse" — macOS keyboards have no dedicated Alt key, so Mac users never knew which key to press. This PR detects process.platform === 'darwin' and shows the native ⌥ symbol instead.

Reviewer Test Plan

How to verify

  1. Start the CLI on macOS: npm run dev
  2. Ask a question that triggers a thinking block (e.g. "explain this code")
  3. While the thinking block is collapsed, verify the hint reads "⌥t to expand"
  4. Press Option+T and verify the thinking block expands
  5. Press Option+T again and verify it collapses, showing "⌥t to collapse"

Evidence (Before & After)

Before: (alt+t to expand) — confusing on Mac, no Alt key exists
After: (⌥t to expand) — matches the actual Option key on Mac keyboards

Tested on

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

Risk & Scope

  • Main risk or tradeoff: None — purely a display string change, no logic affected
  • Not validated / out of scope: Windows and Linux (they continue to show "alt+t" as before)
  • Breaking changes / migration notes: N/A

Linked Issues

N/A

中文说明

这个 PR 做了什么

在 macOS 上将思考块的展开/折叠快捷键提示从 "alt+t" 改为 "⌥t",使界面提示与实际需要按的键一致。

为什么需要

快捷键定义为 { key: 't', meta: true },在 Mac 上对应 Option (⌥) 键。但界面始终显示 "alt+t",而 Mac 键盘没有独立的 Alt 键,导致 Mac 用户不知道按哪个键。此 PR 检测 process.platform === 'darwin' 并显示原生的 ⌥ 符号。

审查者验证方案

如何验证

  1. 在 macOS 上启动 CLI:npm run dev
  2. 触发一个思考块(例如问"解释这段代码")
  3. 思考块折叠时,确认提示显示 "⌥t to expand"
  4. 按 Option+T,确认思考块展开
  5. 再按 Option+T,确认折叠并显示 "⌥t to collapse"

证据(Before & After)

Before: (alt+t to expand) — Mac 上令人困惑,不存在 Alt 键
After: (⌥t to expand) — 与 Mac 键盘上的 Option 键匹配

风险与范围

  • 主要风险或权衡:无 — 纯显示字符串变更,不影响逻辑
  • 未验证 / 超出范围:Windows 和 Linux(继续显示 "alt+t")
  • 破坏性变更 / 迁移说明:无

tt-a1i and others added 30 commits June 19, 2026 04:07
…enLM#5285)

detectImageMime treated any data starting with the four RIFF bytes as
image/webp, but RIFF is a generic container also used by WAV and AVI. A
non-WebP RIFF file (e.g. a WAV renamed to .webp) therefore passed the
magic-byte check in validateImagePath, which exists to confirm a file's real
type. Verify the 'WEBP' marker at bytes 8-11 as well, matching the stricter
check already in core's imageTokenizer and the earlier full-signature PNG fix.
* feat(channel): add QQ Bot channel adapter

Add @qwen-code/channel-qqbot package implementing QQ Bot WebSocket
Gateway connection via the official QQ Bot API.

Supports:
- WebSocket Gateway (HELLO/IDENTIFY/HEARTBEAT/DISPATCH/RECONNECT)
- C2C single chat (C2C_MESSAGE_CREATE)
- Group @mention (GROUP_AT_MESSAGE_CREATE) — code path exists, unverified
- Streaming output via msg_id + msg_seq multi-block sending
- Auto-reconnect with exponential backoff
- Sandbox environment toggle

TODO (technical debt acknowledged):
- Group chat not verified end-to-end
- Single-file architecture (should split into gateway/send/auth modules
  like weixin channel)
- No tests (weixin has send.test.ts + media.test.ts)
- No typing indicator (onPromptStart/onPromptEnd not yet implemented)
- No channel instructions injection in connect()
- No structured error types

Closes QwenLM#5201

* feat(qqbot): add QR login, group chat support with typed events

- Add QR code login via @tencent-connect/qqbot-connector with credential persistence
- Add Intent constants for C2C (1<<12) and GROUP_AT_MESSAGE (1<<25)
- Use QQGroupMessageEvent type in handleGroup instead of cast
- Remove resolved TODO comments for group chat verification
- Add msg_seq to send error log for debugging

* fix(qqbot): address PR review — lint errors, token refresh, security

- Use bracket notation for Record<string, unknown> to fix TS4111 lint errors
- Add chmodSync(credsFile, 0o600) for credential file permissions
- Implement token refresh at 80% TTL with expires_in tracking
- Fix RECONNECT opcode: use code 4000 + serverRequestedReconnect flag
- Fix connect() Promise: reject on close before READY via connectReject
- Log empty-token case in sendMessage, drain response body on error
- Clear chatTypeMap/replyMsgId/msgSeqMap in disconnect()
- Capture msgId at send-time to avoid race on replyMsgId
- Switch channel-registry.ts to Promise.allSettled (isolated channel failures)
- Add chatId validation (isValidChatId) to prevent SSRF

* fix(qqbot): add qqbot to build order, fix ESLint default-case

- Add packages/channels/qqbot to scripts/build.js buildOrder
  (CLI imports @qwen-code/channel-qqbot but it wasn't being built)
- Add default case to handleGatewayMessage switch

* feat(qqbot): prepend sender name in group messages for shared context

When sessionScope is set to 'thread', all group members share one
session. Prepending [senderName] helps the agent distinguish who
said what in the shared context.

* feat(qqbot): cross-server context continuation via SessionRouter persistence

- Persist SessionRouter mappings to disk via sessionsPath, surviving daemon restarts
- Persist QQ routing state (chatTypeMap, replyMsgId, msgSeqMap) to {name}-state.json
- Backup/restore global sessions.json on disconnect/connect to survive start.ts cleanup
- fixRestoredSessions() workaround for ACP LoadSessionResponse missing sessionId
- READY handler delays resolve() until restoreSessions() completes, preventing race

* feat(qqbot): add Session Resume + reconnect retry resilience

- Support WS session resume (RESUME opcode 6) on reconnect,
  falling back to full IDENTIFY when session is invalid
- Add reconnectWithRetry() loop: retries gateway fetch up to 5x
  with exponential backoff, then schedules 60s fallback retry
  (fixes silent death after GW HTTP 500)
- connect() now retries up to 3 times on initial failure
- Bump maxReconnectAttempts from 10 to 20
- Refresh token before each reconnect attempt

* fix(qqbot): address review feedback from wenshao

- fixRestoredSessions: use entry.target directly instead of tt.get(undefined)
  (fixes first restored session routing to wrong conversation when 2+ sessions)
- scheduleTokenRefresh: retry in 60s on token refresh failure, not just log
- sendMessage: move saveQQState() after chunk loop, avoid redundant disk I/O
- handleGroup: drop message when group_openid is missing instead of falling
  back to author.id (which would cause 404 on group message send)

* fix(qqbot): address 3rd review from doudouOUC (12 issues)

- QWEN_HOME: use getGlobalQwenDir() instead of homedir()
- name sanitization: prevent path traversal in file paths
- fetch timeouts: AbortSignal.timeout(15s) on all 3 fetch calls
- TOCTOU: writeFileSync with {mode: 0o600} instead of chmodSync after
- msg_seq gaps: only increment seq on send success, break on failure
- message dedup: seenMessages Map with 5min TTL cleanup timer
- disconnect: set disposed flag + flushQQState sync + clear timers
- heartbeat ACK: track lastHeartbeatAck, force close on 2x interval timeout
- reconnect exhaustion: FATAL log when max attempts reached post-connect
- debounced saveQQState: 500ms debounce, flush on disconnect
- handleGroup: skip [senderName] prefix for slash commands, log for audit
- disposed guard: connectGateway checks disposed before creating WS

* fix(qqbot): robustness round — RESUMED, token expiry, SSRF, disposed, typing stubs

- Handle RESUMED event on RESUME success (start heartbeat, restore sessions)
- Check token expiry before sendMessage, refresh if expired
- Tighten isValidChatId regex (remove . and /) to close path traversal
- Reset disposed flag in connect() for reusability
- Add onPromptStart/onPromptEnd stubs (QQ Bot has no typing API)
- Add robustness comments for splitText surrogate pairs, restoreQQState
  corruption, and senderId identity fragmentation across contexts

* refactor(qqbot): split into modules — api, accounts, login

Extract HTTP calls, credential I/O, and QR login into separate files
matching the weixin channel's architecture:

- api.ts: fetchAccessToken, fetchGatewayUrl, getApiBase, sendQQMessage
- accounts.ts: getCredsFilePath, loadCredentials, saveCredentials
- login.ts: qrCodeLogin (qrConnect wrapper)

QQChannel.ts drops inline fetch/credential/qrConnect logic and imports
from the new modules. Net -41 lines in the adapter.

* feat(qqbot): markdown message support (msg_type: 2)

Detect markdown syntax in AI responses and send as msg_type=2
with markdown.content field instead of plain-text msg_type=0.

Detection covers headers, code blocks, bold, italic, strikethrough,
inline code, links, and lists via a single regex.

* fix(qqbot): defensive patches from complete review

- reconnectWithRetry: guard against disposed channel to prevent infinite loop
- handleGroup: broaden @mention regex to match both legacy <@!id> and V2 <@openid>
- handleGroup: set isReplyToBot=true (every group msg is an @mention)
- fixRestoredSessions: document fragile private-field access
- saveCredentials: correct TOCTOU claim in comment
- hasMarkdownSyntax: document false-positive trade-off

* fix(qqbot): guard against empty content in C2C and group handlers

- handleC2C: return early when event.content is null/empty (image/sticker msgs)
- handleGroup: return early when cleanText is empty after @mention stripping

* fix(qqbot): close remaining review gaps — disposed guard, connectReject, token retry, RESUMED restore

* fix(qqbot): address wenshao review — RESUME restore removal, disposed guards, timer tracking, logging, heartbeat floor, requiredConfigFields, channel-registry error labels

* fix(qqbot): markdown fallback to plain text on rejection

* docs(qqbot): clarify markdown permission — Open Platform has no gate, FAQ is a different platform

* feat(qqbot): add Ark (msg_type=3) and Media (msg_type=7) message support

- types.ts: ArkKV, ArkPayload, FileType, MediaUploadRequest/Response, MediaPayload
- api.ts: uploadQQMedia() — file upload for rich media
- QQChannel.ts: sendArk(chatId, templateId, kv) + sendMedia(chatId, fileType, url, text?)
  - C2C/group upload paths separated (file_info not interchangeable)
  - file_type=4 (文件) blocked for groups per QQ API
  - Embed (msg_type=4) skipped — QQ频道专用, not available for Bot Open Platform

* feat(qqbot): auto-route !ark / !media commands from LLM text via sendMessage

LLM outputs text — the channel now parses structured commands inline:
  !ark(24, #TITLE#=标题, #META_DESC#=描述)
  !media(image, https://example.com/photo.jpg, caption text)

parseArkCommand / parseMediaCommand extract at sendMessage entry;
normal text/markdown flow unchanged.

* feat(qqbot): inject channel instructions for ark/media commands

Sets config.instructions on connect() so the LLM learns about:
  !ark(template_id, key=val, ...) — 3 default templates (23/24/37)
  !media(type, url, [caption])   — image/video/voice/file

Fixes known debt: 'No channel instructions'.

* feat(qqbot): gate ark/media behind config flags (enableArk/enableMedia)

Both features default to false — opt-in via settings.json:
  channels.my-qq.enableArk = true
  channels.my-qq.enableMedia = true

Instructions injected conditionally; command routing gated per-flag.

* refactor(qqbot): extract resolveRoute() to eliminate duplication across sendMessage/sendArk/sendMedia

disposed check, token refresh, chatId validation, sandbox path selection
now in one place. All three methods call resolveRoute() instead of
repeating the same 15-line preamble.

* chore(qqbot): remove Ark and Media message support

Remove !ark() / !media() text parsing, sendArk/sendMedia methods,
uploadQQMedia, and all related types. The text-parsing approach
was too fragile against LLM output formatting. Only text/markdown
messaging remains.

* fix(qqbot): robustness patches for review findings

- Add { mode: 0o600 } to all writeFileSync calls (state/session files)
- Guard against stale WebSocket close event nuking new connection
- Add isReconnecting guard to prevent parallel reconnectWithRetry chains
- Reset isReconnecting flag in READY, RESUMED, and exhaustion paths

* docs(channel): add QQ Bot user documentation

Add user-facing documentation for the QQ Bot channel adapter:

- New docs/users/features/channels/qqbot.md covering setup, configuration,
  QR code login, group chat, Markdown support, token management, connection
  resilience, and troubleshooting
- Update docs/users/features/channels/_meta.ts to include QQ Bot in nav
- Update docs/users/features/channels/overview.md to reference QQ Bot
  across the intro, quick start, type options, slash commands, and the
  media platform differences table

* docs(qqbot): fix prerequisites — QR login needs no developer account

QR code login via qrConnect() does not require a developer account or
manual app registration. First qwen channel start is all you need.

* docs(qqbot): emphasize QR login, keep developer portal as secondary path

Both paths work (config → persisted file → QR scan), confirmed against
fetchToken() code. Reposition QR code login as the primary setup flow,
remove redundant tips/troubleshooting entries.

* docs(qqbot): remove Images and Files section — not supported in channel code

handleC2C/handleGroup both skip messages with no text content.
No media download or upload logic exists in this channel adapter.

* test(qqbot): add unit tests for send utilities

Add vitest test suite for QQ Bot channel following the weixin channel
testing patterns. Extract isValidChatId, hasMarkdownSyntax, and splitText
as exported module-level functions to enable direct testing.

- 27 tests covering: chatId SSRF validation, Markdown syntax detection, and
  text chunking for QQ's 2000-char message limit
- Add vitest.config.ts and test script to qqbot package
- Register qqbot in root vitest workspace projects

Refs: QwenLM#5202

* test(qqbot): add sendMessage flow tests with mocked API

Follow the weixin sendImage test pattern: mock sendQQMessage and
channel-base dependencies to test sendMessage end-to-end.

- C2C/group routing verification
- Markdown msg_type=2 vs plain text msg_type=0
- Markdown rejection fallback to plain text
- Disposed guard and error-stop behavior
- msg_id + msg_seq tracking for multi-chunk streaming

9 new tests, 36 total (all passing)

* test(qqbot): fix review issues — add missing edge cases

Self-review fixes:
- Fix misleading test name: 'returns early when chatId not in chatTypeMap'
  → 'defaults to C2C path for unknown chatId' (code doesn't return early)
- Add SSRF validation test: sendMessage rejects '../traversal' chatId
- Add network error test: thrown sendQQMessage caught by try/catch
- Add token expiration test: expired token + failed refresh → early return
- Hoist mockFetchAccessToken and set default resolved value in beforeEach
  to prevent silent undefined-access failures in accidental token-refresh paths

39 tests, all passing

* test(qqbot): add api and accounts unit tests

Add api.test.ts (13 tests) and accounts.test.ts (8 tests) following
weixin channel vitest patterns: vi.hoisted() mocks, vi.mock() module
replacement, and dynamic import() after mock setup.

api.test.ts covers getApiBase, sendQQMessage, fetchAccessToken, and
fetchGatewayUrl — including HTTP errors, missing fields, and request
body format.

accounts.test.ts covers getCredsFilePath, loadCredentials (missing file,
corrupt JSON, missing fields, valid data), and saveCredentials (dir
creation + 0o600 permissions).

All 60 tests pass (39 existing + 21 new). tsc --build and eslint clean.

* chore(qqbot): suppress CodeQL ReDoS false positives

Add codeql[js/polynomial-redos] suppression comments for two
regexes flagged by CodeQL:

- hasMarkdownSyntax(): input is LLM-generated reply text,
  never attacker-controlled in Qwen Code Channel context.
- handleGroup(): <@...> prefix is injected by QQ servers;
  openid is assigned by QQ, not attacker-chosen.

Both paths have no practical exploit vector — an adversary
would need to either control an LLM's output or register a
malicious openid with QQ, neither of which is achievable.

* fix(qqbot): allow QR-code-only login and guard qrConnect return

- requiredConfigFields: [] — fetchToken() already resolves credentials
  from config → persisted file → QR fallback chain. Blocking at config
  validation prevented QR-code-only users from starting the channel.
- qrCodeLogin(): add bounds check for empty qrConnect() return value.
  If the external library returns an empty array, throw descriptive
  error instead of crashing with TypeError on creds.appId.

* chore(qqbot): add comments for requiredConfigFields and qrConnect guard

- index.ts: explain why requiredConfigFields is empty — fetchToken()
  already resolves credentials via config → file → QR fallback chain.
  Requiring appID/appSecret at config level would block QR-only users
  from reaching the fallback through the built-in channel path.

- login.ts: clarify qrConnect() guard is a defensive robustness patch,
  not a response to an observed failure. Verified by removing appID
  from config and running qwen channel start — QR login triggers
  correctly and returns valid credentials.

* fix(qqbot): replace quadratic regexes with linear patterns, remove failed suppress comments

* fix(qqbot): split hasMarkdownSyntax into individual tests to pass CodeQL

* fix(qqbot): replace markdown link regex with indexOf to eliminate CodeQL ReDoS
- Show session title in terminal window instead of fixed 'Qwen - <folder>'
- Clear terminal title on exit so it reverts to shell default (empty OSC
  sequence without 80-char padding)
- Add try/catch around exit handler to suppress EPIPE when stdout is
  already closed
- Extend multiplexer detection to include Zellij (ZELLIJ) and dvtm (DVTM)
- Chain title callbacks so AppContainer preserves Session's existing ACP
  notification callback instead of overwriting it
- Revert terminal title to static fallback when showStatusInTitle is
  toggled off at runtime
- Reuse sanitizeForOsc for BiDi/RTL directional override protection
- Update tests for new behavior

Co-authored-by: 俊良 <[email protected]>
…#5328)

modalityDefaults only marks the multimodal variants (qwen3.6-plus,
kimi-k2.5) as image/video capable; qwen3.6-flash and kimi-k2.6 fall
through to the text-only default. The Token Plan and Idealab presets
still declared image/video for them, overstating what the models
accept. Drop the stray modalities so they match the defaults, as a
follow-up to the DeepSeek cleanup in QwenLM#5268.
…4808)

Add a bundled skill at .qwen/skills/desktop-pet/ that generates chibi
pixel-art desktop pet companions for any character the user names
(F1 drivers, anime characters, celebrities, animals, etc.).

- SKILL.md: agent instructions for character research, color palette
  design, spritesheet generation, and pet activation
- scripts/gen_spritesheet.py: parameterized generator producing a
  1536x1872 RGBA spritesheet (8x9 grid, 192x208 cells) compatible
  with the pet animation system (9 states, 10 headgear, 5 hair styles)

Output goes to ~/.qwen/pets/ for auto-discovery by the custom pet system.

Closes QwenLM#4807
…enLM#5287)

formatDuration formats durations under a minute with toFixed(1). Values from
59.95s up round to "60.0", so the tool printed "60.0s" (or "60s" with
hideTrailingZeros) — not a valid sub-minute reading — instead of "1m". Detect
when the rounded value reaches 60 and render it as the minute it rounds to,
matching formatDuration(60000) === "1m".
* feat(stats): expose token usage for cost visibility

Persist content-free API token counters and surface daily/monthly summaries plus CSV/JSON export through /stats.

Constraint: Issue QwenLM#4479 requested CLI token visibility with monthly/model breakdowns and export while coordinating with QwenLM#4252/QwenLM#4182.\nRejected: Add a separate top-level token command | /stats keeps related statistics in one surface.\nConfidence: high\nScope-risk: moderate\nDirective: Keep TTFT/TPS generation timing and memory diagnostics outside this token-usage surface unless their issues explicitly broaden scope.\nTested: npx vitest run src/services/tokenUsageService.test.ts src/telemetry/loggers.test.ts; npx vitest run src/ui/commands/statsCommand.test.ts src/ui/hooks/useAutoAcceptIndicator.test.ts src/ui/components/AutoAcceptIndicator.test.tsx; npm run check-i18n --workspace=packages/cli; npm run lint --workspace=packages/cli; npm run lint --workspace=packages/core; npm run typecheck; npm run build; git diff --check\nNot-tested: full integration suite

* fix: Address token usage review feedback

Tighten persisted token usage so internal prompt traffic and disabled usage statistics do not write history, while surfacing non-ENOENT write failures outside debug logs. Complete the reviewer-requested i18n coverage and regression tests around auto mode notices and best-effort writes.

Constraint: Follow-up to wenshao review comments on PR QwenLM#4564.

Rejected: Keeping token usage recording outside the internal-prompt gate | It would inflate daily and monthly stats with background prompts.

Confidence: high

Scope-risk: narrow

Directive: Keep /stats token usage scoped to user-visible API responses unless future requirements explicitly include background traffic.

Tested: npx vitest run src/services/tokenUsageService.test.ts src/telemetry/loggers.test.ts; npx vitest run src/ui/hooks/useAutoAcceptIndicator.test.ts src/ui/commands/statsCommand.test.ts; npm run typecheck; npm run lint --workspace=packages/core; npm run lint --workspace=packages/cli; npm run check-i18n --workspace=packages/cli; npm run build; git diff --check

Not-tested: Full repository test suite

* fix(stats): satisfy token usage review contract

Constraint: wenshao review required consistent token stats, exports, i18n, and best-effort logging behavior.

Rejected: Change cached-token labeling | keeping cached tokens included in input preserves the accepted /stats display contract.

Confidence: high

Scope-risk: narrow

Directive: Keep cached tokens included in input whenever cached-only metadata is used in total fallback.

Tested: cd packages/core && npx vitest run src/services/tokenUsageService.test.ts src/telemetry/loggers.test.ts

Tested: cd packages/cli && npx vitest run src/ui/commands/statsCommand.test.ts src/i18n/mustTranslateKeys.test.ts

Tested: npm run check-i18n --workspace=packages/cli; npm run typecheck; git diff --check

Not-tested: full integration suite

* Refine token usage recording after review

Constraint: Address wenshao's latest PR QwenLM#4564 review suggestions without expanding the /stats command surface. Rejected: Keeping synchronous token-usage writes | sync I/O remains on the API response hot path. Confidence: high Scope-risk: narrow Directive: Keep token usage persistence best-effort and gated by explicit usage-statistics enablement. Tested: cd packages/core; npx vitest run src/services/tokenUsageService.test.ts src/telemetry/loggers.test.ts; cd packages/cli; npx vitest run src/ui/commands/statsCommand.test.ts; npm run typecheck; npm run build; npm run lint --workspace=packages/core; npm run lint --workspace=packages/cli; git diff --check Not-tested: Full repository test suite

* fix(stats): avoid silent zero usage on read failures

Propagate token usage read failures through the existing /stats error path while keeping missing usage files empty, and remove the unreachable telemetry wrapper catch.

Constraint: PR QwenLM#4564 review requested user-visible read failures, full i18n for export errors, and removal of dead telemetry catch code.
Rejected: Adding warning fields to TokenUsageSummary | It would expand the JSON/export schema when the existing command error path already fits read failures.
Confidence: high
Scope-risk: narrow
Directive: Keep jsonl.read default swallowing behavior for existing session/history callers unless a user-visible caller opts into rethrowing non-ENOENT errors.
Tested: npx vitest run src/utils/jsonl-utils.test.ts src/services/tokenUsageService.test.ts src/telemetry/loggers.test.ts
Tested: npx vitest run src/ui/commands/statsCommand.test.ts
Tested: npm run check-i18n --workspace=packages/cli
Tested: npx prettier --check changed files
Tested: npm run typecheck
Tested: npm run lint --workspace=packages/core
Tested: npm run lint --workspace=packages/cli
Tested: git diff --check
Tested: npm run build
Not-tested: Full integration test suite

* fix: close token usage review gaps

Keep the review follow-ups local to token usage accounting and stats export without adding new abstractions.

Constraint: Address PR QwenLM#4564 reviewer requests on token usage export/query reuse, write-failure stderr noise, and invalid-record diagnostics.
Confidence: high
Scope-risk: narrow
Directive: Keep token usage writes best-effort and avoid noisy stderr loops for repeated local failures.
Tested: git diff --check; prior targeted core/cli tests, typecheck, and lint passed for this working tree.
Not-tested: Full repository test suite.

* fix(stats): address token usage review feedback

* Update packages/core/src/services/tokenUsageService.ts

Co-authored-by: Shaojin Wen <[email protected]>

* test(core): keep token usage stderr assertion current

Keep the repeated write-failure regression test aligned with the runtime wording that the PR now emits.

Constraint: PR QwenLM#4564 CI failed after the implementation wording changed to "since last log".
Rejected: Reverting the implementation wording | it is the latest PR behavior and the failure is test-only.
Confidence: high
Scope-risk: narrow
Tested: cd packages/core && npx vitest run src/services/tokenUsageService.test.ts
Not-tested: full repository test suite

* fix(stats): clarify export review edge cases

Address the remaining PR review polish without changing token accounting, export formats, or path containment behavior.

Constraint: Review 4452925552 requested narrow documentation, ENOENT wording, and NOTICES cleanup only.

Rejected: Broader merge-conflict rework | GitHub currently reports the PR as mergeable, and the requested fixes are review polish.

Confidence: high

Scope-risk: narrow

Directive: Keep token usage records content-free and preserve export path validation semantics except for the final ENOENT message.

Tested: cd packages/core && npx vitest run src/services/tokenUsageService.test.ts; cd packages/cli && npx vitest run src/ui/commands/statsCommand.test.ts; npm run check-i18n --workspace=packages/cli; npm run typecheck; git diff --check on changed code and i18n files

Not-tested: Full test suite not run.

* fix: address token usage review feedback

* fix(stats): harden token usage CSV export

* fix(stats): remove unrelated auto mode noise

---------

Co-authored-by: Shaojin Wen <[email protected]>
…in (QwenLM#5185)

* fix(plan-gate): isolate gate agent AbortSignal from parent signal chain

The Plan Approval Gate's review agent inherited the parent round's
AbortController signal. When transient parent-side issues occurred
(stream NO_FINISH_REASON retries, round cleanup), the abort cascaded
into the gate agent, causing it to terminate with mode=CANCELLED.

The runAgentWithRetry loop then saw signal.aborted=true and immediately
skipped all remaining retry attempts, leaving the user stuck in plan
mode with a repeated 'Gate review agent unavailable' message.

Changes:
- runGateAgent: create an independent AbortController with its own
  timeout (5 min) instead of inheriting the parent signal. This
  isolates the gate agent from transient parent-side aborts.
- runAgentWithRetry: add a 1s delay between retries to allow
  transient issues to settle.
- Improved log messages to distinguish attempt numbers in retry loop.

Fixes: exit_plan_mode getting stuck when NO_FINISH_REASON stream errors
cascade through the abort signal chain.

* fix(plan-gate): address review comments

- Fix docstring: parent signal is pre-checked only, not monitored
  during execution. Matches the actual isolation behavior.
- Remove signal.aborted check from retry loop. The gate agent is now
  fully signal-isolated with its own timeout, so parent-side aborts
  (typically transient) should not short-circuit retries. The caller
  discards the result if the parent is truly gone.
- Replace plain setTimeout delay with abortableSleep() that returns
  early if the parent signal aborts during the 1s wait. Prevents
  wasted shutdown time on genuine cancellation.
- Fix timeout comment: gate agent typically completes in 1-2 minutes;
  the 5-minute ceiling matches runConfig.max_time_minutes.

* fix(plan-gate): address follow-up review comments

- Remove parentSignal.aborted pre-check from runGateAgent. The hard-fail
  conflicted with the isolation goal: a transient parent abort would throw,
  abortableSleep would resolve immediately, and retries would spin in a
  rapid loop. The gate agent's own 5-minute timeout is the sole cancellation
  mechanism; the parent signal is intentionally unused.

- Fix race window in abortableSleep: attach the abort listener first, then
  re-check signal.aborted to handle the case where abort fires between the
  initial check and listener registration. Eliminates the edge case where
  the sleep would wait the full duration despite an already-aborted signal.

* fix(plan-gate): use existing delay() for abort-aware retry backoff

Replace custom abortableSleep with the existing delay() utility from
utils/retry.ts, which was already battle-tested for exactly this pattern:

- delay() REJECTS (not resolves) when the signal is aborted, so the
  retry loop can catch the rejection and cleanly exit — no rapid-fire
  retry spin after cancellation.
- delay() already handles the TOCTOU race window (attach listener first,
  re-check signal.aborted after) — addresses the second review comment.
- Also exports delay() from retry.ts since planApprovalGate needs it.

This eliminates the risk of abortableSleep resolving early during abort
and immediately proceeding to another gate agent attempt, which would
cause rapid successive retries and log spam.

* fix(plan-gate): address final review comments

- Add entry-time signal.aborted check to avoid launching gate agent on obvious cancellations
- Only delay between retries when attempt < MAX_AGENT_RETRIES (not after final failure)
- Clarify timeout comment: 5-min ceiling is fixed, coincides with runConfig but independent

* fix(plan-gate): address Copilot review round 5

- Clarify entry-time comment: it's the only synchronous pre-run check;
  delay() still provides abort-awareness between retries.
- Guard gateAbortController.abort() in finally: only abort if not
  already aborted, avoiding unnecessary abort events on success.
Replace the oversized opt-in refactor with the minimal approach from
review feedback: keep main's default-on language append, add
skipOutputLanguagePreference for internal structured queries, and remove
the conflicting English/Chinese-only hints from session title/recap
prompts so the appended preference can take effect.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: JerryLee <[email protected]>
* fix(cli): avoid stale git branch watcher setup

* Align with origin/main: drop incidental merge-formatting drift on unrelated files

---------

Co-authored-by: Shaojin Wen <[email protected]>
…#5336)

detectExtensionFromMagic matched the bare "RIFF" prefix and always
returned .wav, so WebP and AVI files (which share the RIFF container)
were mislabeled as .wav. Disambiguate by the four-character form tag at
bytes 8-11, mirroring the existing weixin WebP signature check.
…wenLM#4993)

* feat(input): move physical cursor to visual cursor for IME input (QwenLM#4652)

* feat(input): add useCursor hook for IME physical cursor tracking

* refactor(input): optimize cursor positioning effect

* feat(input): move setCursorPosition to render phase for immediate cursor positioning

* fix(input): calculate absolute cursor position by walking yoga tree

* fix(input): use addLayoutListener instead of useCursor for zero-jitter cursor positioning

* perf(input): stable addLayoutListener subscription and skip redundant cursor updates

* fix(input): revert lastPos dedup that broke cursorDirty one-shot flag

* feat(input): use patch-package to expose Ink internals for IME cursor positioning

* fix(input): address review feedback — prefixWidth, remove useBoxMetrics, pin ink version

Co-authored-by: Shaojin Wen <[email protected]>

---------

Co-authored-by: Shaojin Wen <[email protected]>

* fix(cli): restore approval-mode reference dropped in QwenLM#4600

QwenLM#4600 unified the approval-mode prompt styling into approvalModePromptStyle and removed the showAutoAcceptStyling / showYoloStyling constants, but left a dangling showYoloStyling reference in the prefixWidth calculation. This broke the tsc build (TS2304: Cannot find name showYoloStyling). npm run bundle (esbuild) skips type-checking, so the break only surfaced under npm run build / tsc --build.

Replace it with approvalMode === ApprovalMode.YOLO, matching how the rest of the file branches on approval mode after QwenLM#4600. Both ternary arms stay 2 because every approval-mode prefix is a single char (getApprovalModePromptStyle returns prefix > or *), so the rendered prefix width and cursor positioning are unchanged.

---------

Co-authored-by: yao <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: LaZzyMan <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
* fix(cli): close @path completion dropdown on Enter accept

When accepting an @path suggestion with Enter, the dropdown did not
close for folder paths because handleAutocomplete intentionally omits
the trailing space (to allow continued Tab-completion deeper into the
directory). Without the space, the @ completion pattern re-matches and
re-shows the dropdown.

Fix: call completion.resetCompletionState() after accepting on Enter.
Tab is intentionally excluded so users can still continue navigating
deeper into directories.

Also adds two regression tests:
- Enter after @path accept resets completion state
- Tab after @path accept does NOT reset completion state

Signed-off-by: Alex <[email protected]>

* fix(cli): suppress @path completion re-open after Enter accept

The previous fix called resetCompletionState() on Enter, but the
dropdown re-opened because useCommandCompletion's useEffect sets
showSuggestions(true) when suggestions.length > 0, which happens
asynchronously after useAtCompletion re-globs for the still-active
@token (folder paths have no trailing space).

Add a dismissed flag to the completion state:
- resetCompletionState() now sets dismissed = true
- The showSuggestions useEffect skips re-opening when dismissed
- dismissed is cleared when the completion query changes (user types)

This ensures the dropdown stays closed after Enter accept until the
user modifies the input again.

Tests: 153 passed in InputPrompt.test.tsx, 26 passed in
useCommandCompletion.test.ts, typecheck clean.

Signed-off-by: Alex <[email protected]>

* fix(cli): address review feedback on dismissed completion guard

- Move setDismissed out of resetCompletionState: resetCompletionState
  is called from 6+ sites (submit, history, Escape, Ctrl+C), not all
  are user dismissals. New dismissCompletion() method sets dismissed
  flag + resets state, called only on Enter accept.

- Keep dismissed/internal state private to useCompletion. Expose
  dismissCompletion() and clearDismissed() instead of raw
  dismissed/setDismissed in UseCommandCompletionReturn.

- Remove setDismissed from useEffect dependency arrays (stable
  useState setter, never called in body).

- Add test for dismissCompletion being called on Enter accept.

Reviewed-by: wenshao
Signed-off-by: Alex <[email protected]>

* restore removed comments in useCompletion.ts

The original navigateUp/navigateDown callbacks had explanatory comments
describing the wrap-around and scroll adjustment logic. Restore them.

Signed-off-by: Alex <[email protected]>

* fix(cli): fix dismissed guard bypass after accept+dismiss

DragonZhang identified that clearDismissed fires in the same render
cycle as dismissCompletion, because accepting a suggestion also changes
the query. The dismissed flag gets reset to false immediately, and on
the next render the guard is bypassed — dropdown re-opens.

Fix: move the query-change effect into useCompletion and use a
skipNextClearRef to prevent clearing dismissed after an accept:

- useCompletion: add skipNextClearRef, set to true in dismissCompletion
- useCompletion: accept query option, run query-change effect internally
- useCompletion: skip clearDismissed if skipNextClearRef is true
- useCommandCompletion: reorder useMemo before useCompletion to compute
  query first; pass query to useCompletion; remove the moved effect
- useCommandCompletion: remove clearDismissed from destructuring

This ensures dismissed stays true after accept+dismiss, and only gets
cleared when the user actually types more.

Reviewed-by: DragonZhang
Signed-off-by: Alex <[email protected]>

* fix(cli): allow null query in UseCompletionOptions to fix CI build

The query from useCommandCompletion's useMemo is string | null, but
UseCompletionOptions.query was typed as string | undefined.

Reviewed-by: CI
Signed-off-by: Alex <[email protected]>

* fix(cli): fix prevQueryRef type to accept null from options.query

TS2322: prevQueryRef was typed as string | undefined but options.query
is string | null | undefined. The assignment on line 89 failed tsc
--build (CI). tsc --noEmit didn't catch this because it doesn't emit.

Signed-off-by: Alex <[email protected]>

* fix(cli): address Jun 14 review comments on @path completion fix

- Add setDismissed(false) to resetCompletionState so all reset paths
  (ESC, submit, mode change) recover dismissed state
- Reorder dismissCompletion to set dismissed after resetCompletionState
  to prevent the reset from clearing it immediately
- Remove clearDismissed from public API (no external consumer uses it);
  inline setDismissed(false) in the query-change effect
- Add useCompletion.test.ts with 13 tests covering the dismissed /
  skipNextClearRef state machine

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): gate dismissCompletion on isDirectory to preserve slash-command suggestions

Only call dismissCompletion() when accepting a directory suggestion,
preventing slash-command sub-suggestions from being suppressed after
Enter accept. Add assertion to Tab test verifying dismissCompletion
is not called.

Addresses review feedback from @wenshao.

Signed-off-by: AlexHuang <[email protected]>
Signed-off-by: Alex <[email protected]>

* fix(cli): scope dismissCompletion to AT-mode directory suggestions

Add completionMode === CompletionMode.AT guard so slash-command
directory suggestions are not affected by dismissCompletion.
Add negative assertion for non-directory Enter test to prevent
regression of the isDirectory check.

Addresses review feedback from @wenshao.

Signed-off-by: AlexHuang <[email protected]>
Signed-off-by: Alex <[email protected]>

* fix(cli): expose completionMode in UseCommandCompletionReturn

The previous commit referenced completion.completionMode in InputPrompt
but the property was not exposed from useCommandCompletion, causing a
TypeScript build error (TS2339) in CI.

Add completionMode to the UseCommandCompletionReturn interface and
return object. Update useExportCompletion.test.ts mock accordingly.

Signed-off-by: AlexHuang <[email protected]>
Signed-off-by: Alex <[email protected]>

---------

Signed-off-by: Alex <[email protected]>
Signed-off-by: AlexHuang <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
wenshao and others added 28 commits June 23, 2026 16:32
…M#5089) (QwenLM#5745)

* revert(core): revert Protocol enum & model-identity decoupling (QwenLM#5089)

Reverts the structural changes from QwenLM#5089 back to the pre-QwenLM#5089 shape:
AuthType stays a fixed enum (not `string`), the Protocol enum is removed,
modelProviders is `Record<authType, ModelConfig[]>` again (not
`{ protocol, models }`), and createContentGenerator dispatches on authType.
The v4->v5 settings migration is removed and SETTINGS_VERSION reverts to 4.

Features merged on top of QwenLM#5089 are kept and re-adapted to the old
enum+array structure (not reverted):
- QwenLM#5632 fastOnly/voiceOnly model flags (test fixtures reshaped to arrays)
- QwenLM#5638 workspace provider defaults (readProviderModels already tolerates
  both shapes; test fixtures reshaped to arrays)
- QwenLM#5729 active-runtime-model listing (pre-QwenLM#5089 getAllConfiguredModels
  already enumerates Object.values(AuthType), so the runtime model is
  included natively)
- QwenLM#5728 ACP set_config_option deterministic provider fixture (reshaped to
  array; the flake fix is preserved)

KNOWN DOWNGRADE CAVEAT: settings already migrated to $version:5 (shipped in
v0.19.0) retain the v5 `{ protocol, models }` modelProviders shape, which
the reverted ModelRegistry consumes as an array. Such settings will throw
on load until re-configured. A v5->v4 downgrade guard/migration is a
separate follow-up if backward compatibility for migrated users is needed.

* feat(cli): add v5->v4 settings downgrade migration for QwenLM#5089 revert

After reverting QwenLM#5089, settings already migrated to $version:5 (shipped in
v0.19.0) carry a modelProviders `{ protocol, models }` shape that the
reverted v4 readers consume as arrays, throwing "models is not iterable"
on load. This adds the inverse migration so those configs auto-converge to
v4 on load (the user-facing "automatically migrate $version:5 to 4").

- V5ToV4Migration: unwraps each modelProviders `{ protocol, models }` back
  to its `models` array, drops the now-implicit protocol (warning only when
  the explicit protocol differs from the key-derived one), and resets
  $version to 4.
- DOWNGRADE_MIGRATIONS keeps the downgrade out of the ascending forward
  ALL_MIGRATIONS chain (preserving its invariants); runMigrations and
  needsMigration consider both via a combined convergence set.
- needsMigration now gates on `=== SETTINGS_VERSION` instead of `>=`, so a
  newer-but-handled version (v5) is reported as needing migration while a
  genuinely unknown newer version (v6+) is still left untouched.

Covered by unit tests for the migration, the framework wiring, and an
end-to-end loadSettings downgrade-on-load test.

* fix(test): align integration settings-version constant with reverted v4

The integration suites hard-coded CURRENT_SETTINGS_VERSION = 5 (introduced
by QwenLM#5676), which mismatched the reverted SETTINGS_VERSION = 4 and failed the
migration assertions ($version now writes 4, not 5). Revert the constant to
4 in both settings-migration and qwen-config-dir integration tests.

Verified: QWEN_SANDBOX=false vitest run --root ./integration-tests
cli/settings-migration.test.ts cli/qwen-config-dir.test.ts → 21 passed.

* fix: harden v5-era settings handling on the QwenLM#5089 revert path

Addresses /qreview feedback on the revert:

- vscode findOpenaiModels: restore read-side tolerance for the V5
  { protocol, models } shape. The extension reads/writes settings.json
  without running the CLI v5->v4 migration, so a not-yet-downgraded
  $version:5 file would otherwise return [] and silently drop existing
  OpenAI models on the next write. (Critical)
- modelRegistry.registerAuthTypeModels: guard against a non-array provider
  value (skip + warn) instead of throwing an opaque "models is not
  iterable" — covers hand-edited or unmigrated files the downgrade misses.
- needsMigration JSDoc: update the stale ">= SETTINGS_VERSION" wording to
  match the "=== SETTINGS_VERSION, else fall through" logic the downgrade
  path depends on.
- settings.test.ts: also assert the v5->v4 downgrade is persisted to disk
  (.tmp write-back), not just the in-memory merged result.

Adds tests for the registry guard and the vscode V5 read tolerance.

* fix(core): break contentGenerator import cycle + cover reverted error paths

Addresses /review suggestions on the revert:

- contentGenerator: import PROVIDER_SOURCED_FIELDS from constants.js (where
  it is actually defined) instead of modelsConfig.js, breaking the runtime
  import cycle contentGenerator -> modelsConfig -> contentGenerator.
  constants.js only references contentGenerator at the type level, which is
  erased at runtime, so no cycle remains.
- contentGenerator.test: add coverage for the two authType error paths the
  revert restored (missing authType -> "must have an authType"; unknown
  authType -> "Unsupported authType"), which QwenLM#5089's protocol-based tests
  had replaced. Neither was covered before.

The acpAgent z.nativeEnum(AuthType).parse(methodId) suggestion is left as-is:
that line is byte-identical to pre-QwenLM#5089, so it is pre-existing behavior the
revert faithfully restores rather than a regression of this PR.
…wenLM#5732)

* ci: check out PR head SHA to avoid missing merge-ref checkout failures

* ci: skip coverage comment on cancelled runs to free concurrency slot

post_coverage_comment used if: always(), so on a cancelled run it still ran
on the hosted pool. During rapid pushes the cancelled run's coverage job sat
queued for a hosted runner, keeping the run non-terminal and holding the
branch concurrency slot — so the next push's required Test (ubuntu-latest)
check waited behind it (observed ~9 min, with thrash). Switch to !cancelled():
coverage still posts on test failure, but a cancelled run releases the slot
immediately. Its coverage would be stale anyway.

* ci(triage): route /triage comment runs to the ECS pool

`/triage` comments trigger the authorize+triage jobs on hosted runners,
which queue behind CI during peak hours (~2 min vs 4 s off-peak observed).
Route the comment-triggered triage job to the ECS self-hosted pool, which
is not bound by the GitHub-hosted concurrency limit.

Safe scope: only `issue_comment` events; the job is read-only and checks
out the base ref (never PR-head code). Gated on the same
MAINTAINER_ECS_RUNNER_DISABLED kill-switch as `authorize`, so a disabled
ECS pool falls back to hosted. PR/issues/dispatch triggers stay hosted.

* ci: fetch PR head by ref name to fix 'not our ref' checkout failure

* ci: align Test checkout ref with Lint (add branch_ref fallback)

On workflow_dispatch the Test job runs (its if: !cancelled() overrides the
implicit success() gate when classify_pr is skipped), so its checkout must
honor the branch_ref input like Lint does. Without it, dispatch runs validated
github.ref in Test while Lint validated the user-supplied branch_ref.

* ci: retry merge-ref checkout instead of pinning PR head; drop /triage ECS routing

Per review feedback: checking out refs/pull/N/head dropped merge-result
validation, and merge queue isn't enabled on main to backstop it. Keep merge-ref
semantics (github.ref) for the required Lint/Test checkouts and retry once to
absorb the transient merge-ref lag the PR-head change was working around.

Also revert /triage routing to ECS: that job runs an agent (sandbox:false,
shell/write tools, repo tokens) over untrusted PR/issue text, so it stays on
ephemeral hosted runners, not the persistent self-hosted pool.

* ci: back off before the merge-ref checkout retry

The merge ref (refs/pull/N/merge) is built asynchronously, so a back-to-back
retry can hit the still-unbuilt ref again. Sleep 10s before retrying so the
retry absorbs the multi-second build lag, not just instantaneous blips.
* chore(release): v0.19.1

* docs(changelog): sync for v0.19.1

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…#5737)

* fix(agent): cap fork turns and bubble fork permission prompts

A detached fork subagent runs fire-and-forget in the background with no
inline UI. Two robustness gaps followed from that:

- It was launched with an empty RunConfig (`{} as RunConfig`), so its
  reasoning loop had no max_turns cap and could burn tokens unbounded.
  Add FORK_DEFAULT_MAX_TURNS=200 and pass it to AgentHeadless.create.

- FORK_AGENT used approvalMode 'default', so in the background launch
  path shouldBubble was false and permission-gated tool calls were
  silently auto-denied (the fork couldn't even run its own "commit
  before reporting"). Switch to 'bubble' so prompts surface to the
  parent's Background-tasks UI; non-interactive forks are already gated
  off, so there is no headless regression.

Closes QwenLM#5734.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(agent): cap resumed fork turns

---------

Co-authored-by: Qwen-Coder <[email protected]>
…wenLM#5126)

* feat(vision-bridge): transcribe images to text for text-only models

Add an opt-in "vision bridge": when a text-only primary model receives an
image, a configured (or auto-selected) multimodal model transcribes it to
text that is then sent to the primary model. Disabled by default.

- New UI-agnostic service (visionBridgeService) + image-part utils, with an
  injection-aware UNTRUSTED fence (trusted guidance first, delimiters defanged)
  and bounded, clamped settings (maxImages / timeoutMs).
- Gate in prepareQueryForGemini: runs only when enabled, the resolved query
  carries image parts, and the primary model is text-only.
- fileUtils.processSingleFileContent: keep image inlineData instead of
  stripping it to an "unsupported" note when the bridge is enabled, so the
  image survives @-resolution for the bridge to convert (audio/video still
  skipped).
- Auto-select an image-capable model from registered providers, preferring the
  same provider as the primary model; user can pin visionBridge.model.
- New settings block (default off, hidden from dialog).

* docs(vision-bridge): document the vision bridge feature

- Add docs/users/features/vision-bridge.md (what/when/how, settings, privacy,
  failure behavior) and register it in the features nav.
- Add a visionBridge settings section to the settings reference.
- Add a visionBridge row to the README top-level settings table.

* refactor(vision-bridge): stop the turn on any conversion failure

Drop the adaptive failure policy (proceed-with-note when the user also asked a
text question) in favor of a single rule: any bridge failure stops the turn and
surfaces the reason. Simpler failure semantics and a smaller surface; the
primary model never answers as if it had seen the image. Removes
MEANINGFUL_TEXT_THRESHOLD and buildFailureNote; updates tests and the docs
failure-behavior section to match.

* fix(vision-bridge): guard Config calls in the gate so partial Configs no-op

The bridge gate called config.getVisionBridgeConfig()/getEffectiveInputModalities()
unconditionally; test/mock Configs that don't implement them threw inside
prepareQueryForGemini, failing useGeminiStream submission tests. Optional-chain
both calls so a Config lacking them simply skips the bridge (the real Config
always provides them).

* fix(vision-bridge): address review — cross-provider auto-select, logging, docs

- Auto-select now uses getAllConfiguredModels() so a vision model registered
  under a different provider/authType than the primary can be picked
  (runSideQuery resolves its credentials).
- Add VISION_BRIDGE debug logging at model resolution, side-query invocation,
  success, and failure for diagnosability.
- Fix stale doc comments (model is auto-selectable when unset; failed results
  carry no parts after the stop-on-failure change).
- Note the clamp ranges in the maxImages/timeoutMs setting descriptions.
- Document the interactive-only limitation (agent/headless image reads are not
  bridged).

* feat(vision-bridge): name the bridge endpoint in the egress notice

Adopt review follow-up: cross-provider auto-select can route the image to a
different endpoint than the primary model, so the conversion notice now names
the endpoint host (e.g. "via qwen3.7-plus (dashscope.aliyuncs.com)") alongside
the model id, making the data-egress disclosure precise. Adds modelEndpoint to
the result and a unit test.

* fix(vision-bridge): scope the image-preserve bypass to the interactive @-path

Previously, with the bridge enabled, the fileUtils image-preserve bypass fired
for ALL processSingleFileContent callers — so agent tool reads (read_file /
read_many_files) and headless/ACP runs carried image inlineData to a text-only
model (omitted by the converter) instead of the clear "model doesn't support
image input" message. The bridge only runs in the interactive path, so this was
a silent regression for non-interactive image reads.

Thread an explicit preserveUnsupportedImageForBridge flag from the interactive
@-resolution path (atCommandProcessor → readManyFiles → processSingleFileContent).
Agent tools and headless don't set it, so they keep the prior "Skipped" behavior;
only the interactive @-path preserves images for the bridge. Adds tests for both
paths and updates the docs limitation note.

* test(vision-bridge): cover the useGeminiStream bridge gate

Add hook-level tests for the gate that were previously missing: bridge runs and
replaces image parts with text (enabled + text-only); turn stops with no model
stream when conversion fails; bridge is skipped when the primary model already
accepts images; bridge is skipped when disabled. Mocks runVisionBridge and
drives @-resolution via the existing handleAtCommand spy.

* refactor(vision-bridge): drop unused intentText param; log in the gate

- Remove the never-passed intentText parameter from runVisionBridge; intent is
  derived from the request's non-image text.
- Add GEMINI_STREAM debug logging in the gate (match/run + result status).

* fix(vision-bridge): preserve provider routing for bridge models

Carry the selected auth type and base URL through side queries so duplicate model ids resolve to the exact provider.

Add a tmux E2E test plan for the text-primary vision bridge flow.

* fix(vision-bridge): keep e2e docs out of gitignore whitelist

* fix(core): harden vision bridge review fixes

* fix(vision-bridge): address review feedback

* chore(vision-bridge): drop unrelated diff churn

* fix(vision-bridge): address review hardening

* fix(vision-bridge): address remaining review cleanup

* fix(vision-bridge): skip unknown primary modalities

* fix(vision-bridge): address review hardening gaps

* feat(vision-bridge): auto-enable for text-only models

* fix(vision-bridge): tighten text-model image handling

* fix(vision-bridge): transcribe mid-turn image references

* fix(vision-bridge): harden mid-turn bridge handling

* fix(vision-bridge): share bridge gate logic

* chore(vision-bridge): trim unnecessary review changes

* refactor(vision-bridge): slim implementation, scope auto-select to same provider

Roughly halve the vision-bridge implementation and fix the auto-select issue
that local tmux e2e surfaced.

- Auto-select now borrows an image-capable model ONLY on the same provider as
  the text-only primary (same endpoint, else same auth type); when none exists
  the bridge stays off. The previous "pick first image-capable" routed to a
  misclassified OAuth runtime model on a different endpoint that hung the turn.
- Revert the per-query provider-hint plumbing (modelAuthType/modelBaseUrl/
  onDispatch) from baseLlmClient/sideQuery/modelRegistry/modelsConfig/
  content-generator-config; a bare model id already resolves across auth types.
- Replace the prompt-injection fence machinery with a one-line untrusted
  wrapper, the depth-tracking <think> parser with a small regex strip, and drop
  dead result fields, the per-call maxImages knob, and a discarded validator
  message.
- Collapse the processSingleFileContent overload into a single options arg.

Verified locally: core + cli unit suites green, typecheck/eslint/prettier clean,
and a real tmux TUI run (text-only qwen3.7-max) auto-selects qwen3.7-plus on the
same dashscope endpoint and transcribes successfully.

* refactor(vision-bridge): cap bridge intent; drop phantom test fields

- Cap the intent sent to the borrowed vision model so @-referenced file
  contents (also carried in nonImageParts) aren't dumped into the bridge
  request. The primary model still receives them in full.
- Remove imageCount/omittedInvalidCount/omittedCappedCount from test mocks;
  those fields no longer exist on VisionBridgeResult.

Addresses the one real finding from the latest /review pass (full @-file
context reaching the bridge model); the other "critical" findings did not hold
up (greedy <think> strip is deliberate for nested blocks; debug-log error is
standard and opt-in; `error` is a local result field, not an egress).

* fix(vision-bridge): handle interleaved <think> blocks; sanitize transcript for terminal

- stripThinkTags now removes innermost balanced <think> pairs until stable, so
  interleaved think/answer output (reasoning vision models) no longer loses the
  answer text between blocks; nested and unterminated cases still handled.
- Strip ANSI/C0 control escapes from the untrusted transcript before it is shown
  in the terminal notice, so a crafted image can't inject terminal sequences.

* fix(file-utils): keep processSingleFileContent positional signature back-compatible

processSingleFileContent is part of the public core API (re-exported via
index.ts). Restore the legacy (offset, limit, pages) positional overload
alongside the new options object so external/positional callers don't break,
with a regression test.

* test(vision-bridge): cover timeout-only path + transcript escape strip; extend to C1

- Add a test for the timeout-only failure path (the bridge's AbortSignal.timeout
  fires while the user signal is NOT aborted) — asserts a failed result whose
  reason is the safe "timed out" message.
- Add a test that terminal control/escape chars are stripped from the untrusted
  transcript shown in the notice (terminal-injection boundary), and extend the
  strip to C1 controls (U+0080–U+009F) that some terminals interpret.

---------

Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
… mode (QwenLM#5754)

Add a Layer 0 deterministic pre-filter in evaluateAutoMode() that blocks
destructive git and IaC commands before the LLM classifier runs. This
closes the gap where the only defense against `git reset --hard` or
`terraform destroy` was the non-deterministic LLM classifier, which can
fail due to API unavailability, timeout, or poor judgment.

Blocked patterns:
- git reset --hard, git checkout -- ., git clean -f*, git stash drop
  (unless user prompt explicitly mentions discarding work)
- git commit --amend (unless the target commit was made this session)
- terraform/pulumi/cdk destroy (unless user mentions the target stack)

Shell indirection bypass prevention: strips one layer of shell quoting
(bash -c "...", sh -c '...') so wrapped destructive commands are still
detected.

The guard only applies in AUTO mode — YOLO mode is unaffected as it is
an explicit opt-out of all safety checks.
* feat(cli): add extension operation polling

* fix(cli): harden extension operation polling

---------

Co-authored-by: ytahdn <[email protected]>
…M#5757)

On VS Code >=1.106 the version-gated `when` clauses hid the primary Activity Bar container in favor of a secondary-sidebar container, so the Qwen Code view flashed and disappeared from the left bar. Drop the secondary-sidebar container and context key and always register the chat view in the Activity Bar, which VS Code lets users move to the secondary sidebar natively.
…LM#5764)

collectContextData read apiTotalTokens from uiTelemetryService, a
process-global singleton shared by every session in a serve daemon, so
each session's /context-usage reported whichever session most recently
completed a turn. Read the per-session GeminiChat's count instead
(config already in scope), falling back to the global singleton only
when no chat is initialized yet. Fixes both /context and the serve
GET /session/:id/context-usage route at the source.

Closes QwenLM#5763

Co-authored-by: Qwen-Coder <[email protected]>
The TUI paints no background of its own and relies on the terminal's own
background. Two elements broke that and rendered as off-colour blocks that
could not be made consistent across terminals and themes:

- The input box (QwenLM#5568) flood-filled theme.background.primary. Even when
  the theme's light/dark bucket matched the terminal (the QwenLM#5746 gate), the
  exact colour usually differed from the terminal's real background, so the
  prompt rendered as a distinct block — worst over SSH/remote where
  brightness detection is unreliable and defaults to dark.

- The user-message half-line band (QwenLM#4595) painted a subtleBandColor band
  behind each user message, gated on the same theme/terminal match. Because
  history is rendered through Ink <Static> (committed rows are never
  repainted) and the gate only fires when the active theme matches the
  terminal, the band showed on some messages but not others across a theme
  switch — it cannot be made consistent.

Stop painting both so the input area and user messages blend into the
terminal background everywhere. User messages fall back to marginTop=1 for
separation. The software cursor now derives its contrast from the
terminal's detected brightness (getEffectiveTerminalBackground) so it stays
visible with no fill painted.

Also remove the band's now-unused helpers — subtleBandColor and
supportsTrueColor (added by QwenLM#4595) and the dead UserMessageProps.width prop.

Fixes QwenLM#5771.

Generated with AI

Co-authored-by: 秦奇 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
…ences (QwenLM#5774)

Bare `@<partial>` with no `<server>:` prefix now matches resource URIs and friendly names across all connected MCP servers, surfaced alongside the file results and injected as the canonical `@server:uri` reference — so a user can pull up a resource by a memorable fragment without first recalling which server exposes it.

In the @-mention dropdown, resource rows keep the full `server:uri` reference intact on a single line, aligned, with the description column truncating instead — so resources that share a long URI prefix stay distinguishable.
* ci: collapse pr checks into ubuntu gate

* ci: keep platform PR tests in matrix

* ci: update no-ak gate wiring test

* ci: bypass stale self-hosted git cache

* ci: refresh self-hosted checkout cache

* ci: refresh pr refs after cached checkout

* ci: fetch fresh pr refs via github url

* ci: retry stale pr merge checkout

* ci: fetch stale pr merge ref directly

* test: isolate qwen serve streaming home

* test: keep qwen serve fake server off proxy

* ci: check out PR head ref to avoid merge-ref lag

The Ubuntu gate kept failing on the self-hosted runner: the PR checkout
used github.ref (refs/pull/N/merge), which GitHub rebuilds asynchronously
and can serve stale for minutes after a push, so the verify guard saw a
tree without the PR head. The retry/refresh machinery added to work
around this could not help — its direct GitHub fetch fallback times out
on the self-hosted squid proxy.

Check out refs/pull/N/head instead (immutable, published the instant the
branch is pushed) for pull_request events in both the test and
test_platforms jobs, and drop the 6-step retry/verify/refresh block. A
single sanity guard stays to fail loud if the head is missing. Non-PR
events keep github.ref; the merge queue validates the merged result.
…age (QwenLM#5735)

* docs: fix config/command/auth drift and surface model-providers page

Audit docs/ against the current code and correct the highest-impact drift:

- settings.md: move the mis-filed experimental.emitToolUseSummaries row into a
  new experimental section (cron/agentTeam/artifact/emitToolUseSummaries) and
  add general.language/outputLanguage/dynamicCommandTranslation and
  output.showTimestamps.
- commands.md: document /cd, /history, /voice, /import-config and the
  /model --voice and /model <model-id> forms.
- auth.md + model-providers.md: convert all modelProviders examples to the v5
  { protocol, models } object shape, correct the /auth menu (Alibaba
  ModelStudio / Third-party Providers / Custom Provider), fix the default
  OpenAI model (qwen3.5-plus), document the vertex-ai auth type, mark envKey
  optional, and use kebab-case --openai-api-key/--openai-base-url flags.
- overview.md + quickstart.md: rewrite the stale first-run auth flow; fix typo.
- configuration/_meta.ts: surface the orphaned model-providers page in the nav.
- qc-helper SKILL.md: add the 8 missing feature pages to the doc index.

* docs: resolve review feedback — fix provider-name and ModelStudio casing

Align docs with the code's provider labels and UI strings:
- Z.ai -> Z.AI (presets/zai.ts: label 'Z.AI API Key')
- iDeaLab -> Idealab (presets/idealab.ts: label 'Idealab API Key')
- 'Model Studio' -> 'ModelStudio' (UI flowTitle 'Alibaba ModelStudio'; no 'Model Studio' in code)

Applied across auth.md, overview.md, quickstart.md. Used --no-verify to avoid
lint-staged reformatting pre-existing, unrelated (non-CI-enforced) table padding
in auth.md; the five changed lines are individually prettier-clean.

* docs: resolve review feedback — /history subcommands, language type, jsonc fence

- commands.md: add missing '/history expand-on-resume' subcommand (historyCommand.ts registers collapse-on-resume, expand-on-resume, expand-now)
- settings.md: general.language Type string -> enum (settingsSchema.ts declares type: 'enum')
- model-providers.md: relabel the Example fence json -> jsonc (it contains // comments and two JSON docs)

--no-verify: avoids lint-staged re-padding pre-existing, unrelated (non-CI-enforced)
table columns; the three changed lines are content-only.
* docs: Align docs with current CLI behavior

Update stale documentation and user-facing MCP OAuth guidance to match the current dialog-based flows, SDK permission semantics, current links, and Qwen OAuth status.

Also replace Ink internal imports with public Ink APIs for the shared text input so the workspace builds against Ink 7.

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: fix BaseTextInput Ink import (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5589)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): surface MCP OAuth credential read failures

Fix SSE OAuth credential pre-check failures by reporting token storage read errors before connecting.

Update SDK coreTools docs and extension release link text from the follow-up review.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): harden MCP OAuth error handling

Handle stderr warning failures as best-effort and keep SSE 401 OAuth guidance when credential re-read fails.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): keep SSE OAuth pre-read best effort

Avoid blocking SSE MCP connections when the diagnostic credential pre-read fails, and cover BaseTextInput absolute-position edge cases.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): handle SSE OAuth validation errors

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): surface MCP OAuth recovery guidance

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): cover MCP OAuth retry paths

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): address OAuth guidance review

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
* feat(cli): add workspace permissions rules API

Co-authored-by: Qwen-Coder <[email protected]>

* codex: fix CI failure on PR QwenLM#5743

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli,sdk): address PR review comments on workspace permissions

- normalizePermissionRules: skip malformed rules instead of rejecting
  the entire request, fixing read-modify-write bricking (review #1 & #4)
- Add tests for addWorkspacePermissionRule/removeWorkspacePermissionRule
  covering the actual POST path (review #2)
- Add JSDoc documenting non-atomic read-modify-write and TOCTOU risk
  on add/remove helpers (review #3)

* fix(cli): wrap persist-fallback response in try/catch and add error path tests

- Add try/catch around buildPermissionSettings in persist-fallback
  POST path, matching GET handler error handling
- Add tests for ACP non-SessionNotFoundError, persistSetting failure,
  and unknown client id rejection

* fix(cli): update acpAgent test for silent malformed rule dropping

The normalizePermissionRules change to skip (instead of reject)
malformed rules requires updating the acpAgent test to expect
successful resolution with the malformed rule filtered out.

* fix(cli): reject newly malformed permission rules

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): tighten workspace permission writes

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): harden workspace permission rules

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): handle ACP invalid params errors

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): pin workspace permission writes

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): report workspace permission write backend

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
…ols (QwenLM#5788)

The TUI uses a consistent vocabulary of width-1 Unicode text glyphs
(✦ ● ○ ◐ ✓ ↓ ↑ + ink-spinner). Two status indicators broke this with
emoji, which render at width=2 and drift column alignment across the
CI/LANG fallback branches.

- THINKING_ICON: replace the three-way fallback (💡 / ⟡ / empty) with
  a single ∴ (U+2234 THEREFORE) constant. The math "therefore" sign
  matches the thinking block's "deriving a conclusion" semantics and
  sits on a stable wide base. Drops the isUtf8/CI branch entirely.
- SummaryMessage success: replace ✅ with ✓ to match ToolMessage's
  existing success glyph — same "operation succeeded" meaning, no
  redundant emoji variant.

Both replacements are width=1 and render identically across
terminals/CI, removing the column drift the old fallback caused.

Refs QwenLM#5787
…zard (QwenLM#5654)

* fix(cli): restore saved custom model IDs when re-entering the auth wizard

When re-entering the provider auth wizard for a ModelStudio provider that
already had saved model IDs, the Model IDs step reset to the provider's
built-in defaults instead of the user's saved models. Submitting then
overwrote the previously saved custom model IDs with the defaults, losing the
user's configuration.

Share the existing model-lookup logic from core between the desktop/ACP path
and the CLI wizard, and have the wizard pre-fill the Model IDs step with the
saved models when present, falling back to the built-in defaults otherwise.

Closes QwenLM#5636

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): preserve user custom model IDs during provider update

executeUpdate rebuilt the install plan with only built-in model IDs, so the prepend-and-remove-owned merge silently deleted user-added custom models. Carry custom IDs into the install plan so an update only refreshes built-in models and leaves user-configured ones untouched.

---------

Co-authored-by: Qwen-Coder <[email protected]>
* fix(daemon): Reject stale prompt client admission

Return invalid prompt client ids as synchronous admission failures so daemon clients do not wait forever after a stale client id is rejected.

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5784)

Log invalid prompt client admission rejections and document the fake bridge's synchronous admission contract.

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (QwenLM#5784)

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
…M#5755)

* feat(serve): voice dictation over the daemon for the Web Shell

Bring voice input to the `qwen serve` Web Shell. The browser captures the
microphone, streams raw 16 kHz mono PCM to a new `/voice/stream` WebSocket, and
the daemon transcribes server-side by reusing the existing CLI voice pipeline
(realtime streaming + on-stop batch) — provider credentials never reach the
browser, and it works whether the daemon is local or remote.

Daemon:
- `/voice/stream` WebSocket handler (serve/voice) reusing openVoiceStream /
  openQwenAsrRealtimeStream / transcribeVoiceAudio; resolves the workspace
  voiceModel from a ModelsConfig built off settings.
- Routed through the existing ACP upgrade listener so it shares the
  loopback / host-allowlist / CSRF / bearer checks; concurrency-capped.
- Advertises the `voice_transcribe` capability (unconditional, like
  auth_device_flow; the WS errors when no voice model is configured).
- Relax `Permissions-Policy` to `microphone=(self)` so the same-origin shell can
  request the mic — an empty `microphone=()` allowlist blocked the prompt
  entirely.
- Allowlist `voiceModel` on `/workspace/settings` so the picker can read it.

Web Shell:
- Mic button in the composer: click to record, click to stop -> the transcript
  is inserted into the input for review before sending.
- `/model --voice` picker (sourced from `/workspace/providers`, since voice
  models are hidden from the session model list) and `/model --voice <id>`,
  persisted via the prompt channel like `/model --fast`.

The voiceModel resolver now accepts a structural model lookup so the daemon can
resolve it without constructing a full CLI Config.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(serve): harden web voice streaming

* fix(serve): clean stale voice capture resources

* fix(serve): harden voice websocket errors

* fix(serve): hide voice capability when token auth is configured

* fix(serve): address voice review blockers

* fix(web-shell): keep voice cancel available when disabled

* fix(web-shell): inline the voice mic in the composer toolbar

The mic was an absolute-positioned overlay anchored to the composer
wrapper, so after the QwenLM#5775 chat-UI restructure it floated outside the
input box (clipped at the bottom-right). Render it inside ChatEditor's
`toolbarRight`, just before the send button, wiring the transcript insert
to the composer core — it sits inline with the `/`, `@`, and send
controls like the rest of the toolbar.

Restyle to a Codex-style recording experience: an idle mic that matches
the other toolbar icon buttons, and a recording pill with a live waveform
(driven by the RMS meter), an elapsed `M:SS` timer, and a stop control.
Also forward `/voice/stream` through the vite dev proxy (`ws: true`,
scoped to the exact path so it doesn't shadow the client's own
`client/voice/*` source modules) so the WebSocket reaches the daemon in
dev instead of silently failing against vite.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(serve): authenticate the voice WebSocket via a bearer subprotocol

Voice was suppressed on any token-configured daemon because browsers
cannot set an `Authorization` header on a WebSocket, so the bearer check
on the shared ACP upgrade listener always failed for the browser mic.

Let the browser carry the token in `Sec-WebSocket-Protocol` as
`qwen-bearer.<base64url(token)>` (the only header a browser can set on a
WS). The upgrade listener now accepts the token from either the
`Authorization` header (non-browser clients, unchanged) or the
subprotocol, hash-compared in constant time. `handleProtocols` selects a
non-secret subprotocol (or none) so the token is never echoed back in the
handshake response. This aligns the voice WS with how the ACP WS will
authenticate browsers.

With the WS now authenticated, drop the `!tokenConfigured` /
`requireAuth` suppression of the `voice_transcribe` capability — it is
advertised whenever the `/voice/stream` endpoint exists. Tests updated to
assert voice is advertised under a token / `--require-auth`.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(serve): make the voice WS subprotocol handshake complete + test it

Review follow-up to the bearer-subprotocol auth. Fixes a real handshake
bug and closes the test gap on the security-critical path.

- Handshake bug: when the browser offered only the `qwen-bearer.*`
  subprotocol, the daemon's `handleProtocols` selected none, and a strict
  WS client (the `ws` library) aborts with "Server sent no subprotocol".
  The web-shell now offers a non-secret marker (`qwen-ws`) alongside the
  bearer one, so the daemon always selects the marker — never echoing the
  secret — and the handshake completes for strict and lenient clients
  alike.
- Tests: add daemon-side coverage for the subprotocol auth path (accept
  valid token, reject wrong/malformed, no-token loopback) and assert the
  secret is never echoed back; add client coverage that the bearer token
  is offered as `[qwen-ws, qwen-bearer.<b64url>]` and round-trips.
- Drop the unreachable try/catch around `Buffer.from(_, 'base64url')`
  (it never throws) and correct the comment.
- a11y: mark the transcribing/interim regions as `role="status"` /
  `aria-live`, and add a `:focus-visible` ring to the idle mic.
- Refresh the stale `useVoiceCapture` doc comment (token deployments are
  now supported in the browser) and fix the mock typing in
  VoiceButton.test.tsx.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): ignore stale voice socket callbacks

* test(serve): sync voice capability integration expectation

* fix(serve): harden voice websocket review gaps

* fix(serve): sanitize voice websocket failures

* fix(serve): address voice review suggestions

* fix(serve): harden voice websocket lifecycle

---------

Co-authored-by: Qwen-Coder <[email protected]>
On macOS the Option key (⌥) maps to the meta keybinding, but the UI
hint displayed "alt+t" which is confusing since Mac keyboards have no
dedicated Alt key. Show the native ⌥ symbol on darwin so the on-screen
hint matches the actual key users need to press.
Alex-ai-future added a commit that referenced this pull request Jul 1, 2026
- Reject inline prompt + scope flag combination with clear error (#1)
- Add mutual exclusivity check for --project and --global (#5)
- Verify setValue scope parameter in tests + add --global test (#2)
- Extract scopeSuffix to shared variable, remove duplication (#3)
- Remove dead i18n keys 'Select Model (this project)' / '(global)' (#4)
- Fix scopeSuffix placement on model line not API key line (QwenLM#8)
- Add fr.js / ja.js translations for scope keys (QwenLM#10)
- Remove unused export ModelDialogPersistScope (#6)
- Wrap non-interactive help text in t() with new flags (QwenLM#7)
- Fix argumentHint grouping to show mode vs scope flags (QwenLM#11)

Signed-off-by: Alex <[email protected]>
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.