feat(daemon): add POST /session/:id/language for runtime language switching#4705
Conversation
…tching Add a dedicated HTTP endpoint for switching UI language and LLM output language without polluting the session transcript. The endpoint flows through three layers (server route → bridge → ACP extMethod handler) following the same pattern as approval-mode and model switching. When syncOutputLanguage is true, the handler updates output-language.md, persists settings, and refreshes system prompts across all active sessions so the next LLM call immediately uses the new language. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
📋 Review SummaryThis PR implements a runtime language-switching API endpoint ( 🔍 General Feedback
🎯 Specific Feedback🔴 Critical
🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
Code Review Overview (AI Generated)PR: #4705 feat(daemon): add POST /session/:id/language for runtime language switching Multi-Round Review (Rounds 0-6): Clean — 0 findingsRound 0 (Design): Correct approach — three-layer flow (HTTP route → bridge extMethod → ACP handler) follows the established pattern used by Round 1 (Architecture): Clean implementation across all 5 files. Round 2 (Robustness):
Round 3 (Security): Round 4 (Performance): Round 5 (New Feature): Implementation matches PR description exactly — three-layer flow, language switching, optional output language sync, settings persistence, all-session refresh. Clean and focused, no unrelated changes. Round 6 (Undirected): Cross-file consistency verified — LGTM! This review was generated by QoderWork AI |
… debug logging - Replace hardcoded LANGUAGE_CODES array in server.ts with dynamically derived list from SUPPORTED_LANGUAGES, ensuring new languages added to the i18n module are automatically accepted by the API. - Add debugLogger.warn calls for settings persistence failures in the ACP handler instead of silently swallowing errors. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
There was a problem hiding this comment.
Pull request overview
Adds a new POST /session/:id/language HTTP endpoint that switches the daemon's UI language and (optionally) the LLM output language at runtime, wired through the standard server → bridge → ACP extMethod three-layer pattern. When syncOutputLanguage is true, the handler also rewrites ~/.qwen/output-language.md, persists the user setting, and refreshes every active session's system prompt.
Changes:
- New ext-method
qwen/control/session/language,HttpAcpBridge.setSessionLanguage, andlanguage_changedbus event. - New Express route
POST /session/:id/languagewith allow-list validation againstSUPPORTED_LANGUAGES + 'auto'. QwenAgenthandler inacpAgent.tsperforms the global UI language change, settings persistence, and per-session system-prompt refresh.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/acp-bridge/src/status.ts | Registers the new sessionLanguage ext-method constant. |
| packages/acp-bridge/src/bridgeTypes.ts | Declares the setSessionLanguage interface on HttpAcpBridge. |
| packages/acp-bridge/src/bridge.ts | Implements the bridge method: forwards via extMethod, publishes language_changed. |
| packages/cli/src/acp-integration/acpAgent.ts | Adds the ACP-side handler that mutates global UI language, persists settings, and refreshes all sessions. |
| packages/cli/src/serve/server.ts | Adds the HTTP route, language-code validation, and bridge call. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
doudouOUC
left a comment
There was a problem hiding this comment.
Overall
The three-layer pattern (server route → bridge → ACP extMethod) is well-followed, and deriving LANGUAGE_CODES dynamically from SUPPORTED_LANGUAGES (commit 2) is a good improvement over hardcoding. However, two critical issues need addressing before merge.
Additional items not covered by inline comments:
- No test coverage — 192 lines of production code, 0 lines of tests.
bridge.test.tshas tests forsetSessionApprovalMode(lines ~5083-5306);setSessionLanguageshould have comparable coverage: basic call path, session-not-found error, event publish verification, and concurrent-request serialization (if queue is added). language_changedevent has no consumer — the event type only appears in this PR's new code. No SSE consumer, no frontend handler, no test references it. If the consumer comes in a follow-up PR, please note that in the description.- Response lacks
sessionId—setSessionApprovalModereturns{ sessionId, mode, previous, persisted }.setSessionLanguagereturns{ language, outputLanguage, refreshed }withoutsessionId. Minor inconsistency but worth aligning for API uniformity.
wenshao
left a comment
There was a problem hiding this comment.
[Critical] No tests for the new POST /session/:id/language endpoint. +192 lines of new logic across 3 layers (server route, bridge forwarding, ACP handler) with zero test coverage. Existing server.test.ts has analogous test suites for model and approval-mode endpoints covering success, validation errors, 404, client identity forwarding, and bridge error propagation. Critical branches uncovered: valid/invalid language, non-boolean syncOutputLanguage, missing session (404), client-id forwarding, and the syncOutputLanguage=true path that mutates settings files and refreshes all sessions.
[Suggestion] Missing session_language capability registry entry in capabilities.ts. Every other session-level mutation endpoint has one (session_set_model, session_approval_mode_control, session_recap, session_btw). SDK clients preflighting via GET /capabilities cannot discover language-switching support.
— qwen3.7-max via Qwen Code /review
- Add sessionOrThrow() call for session existence validation (doudouOUC) - Wrap setLanguageAsync in try-catch with structured error (doudouOUC) - Wrap updateOutputLanguageFile in try-catch to prevent partial state (wenshao) - Return resolved language code instead of echoing "auto" verbatim (wenshao) - Add refreshed field to language_changed SSE event payload (wenshao) - Add language to telemetry route regex (wenshao) - Add FakeBridge setSessionLanguage and 6 server route tests Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Review Response — af375f4Thanks for the thorough reviews. Here's how each finding was handled: Fixed (in commit af375f4)
Fixed earlier (in commit d5e5a9d)
Won't fix (with reasoning)Concurrency serialization queue (doudouOUC) — Language switching is idempotent: the last writer wins and the result is consistent regardless of ordering. This differs fundamentally from approval-mode (non-idempotent state machine transitions where order determines the final state). The existing `/language` slash command has no serialization either. Workspace broadcast for peer sessions (doudouOUC) — The ACP handler already refreshes all sessions' system prompts via `Promise.allSettled`. The SSE event is for UI state updates; in practice, only one frontend client is attached to a session. Adding `broadcastWorkspaceEvent` would send duplicate events to sessions whose prompts are already refreshed. Missing `previousLanguage` in return type (doudouOUC) — The caller already knows the current language (it's what's displayed in their UI). Unlike approval-mode where the previous state matters for audit trails, language switching is a simple preference toggle. `refreshed: true` even when individual sessions failed (doudouOUC/Copilot) — `refreshed` reflects whether the refresh phase was attempted, not whether every individual session succeeded. The core mutation (file write + settings persist) is what matters; individual session refresh failures are transient and self-correcting on next prompt. Unbounded `Promise.allSettled` concurrency (wenshao) — Typical daemon has 1-5 sessions. The `initTimeoutMs` (60s) provides ample headroom. Adding a concurrency cap for a single-digit session count would be over-engineering. `withTimeout` too tight (wenshao) — `initTimeoutMs` defaults to 60s, same as all other extMethods. `setLanguageAsync` loads a small JSON file (~1ms), `writeFileSync` writes a small markdown file (~1ms), and `Promise.allSettled` refreshes 1-5 sessions (~100ms each). Total < 1s in practice. `language_changed` SSE event has no consumer (doudouOUC) — The consumer is in the frontend codebase (agent-web), not in qwen-code. This follows the same pattern as `approval_mode_changed`, `model_switched`, `tool_toggled` — all published here, consumed by frontend SSE subscribers. SDK event type registration (Copilot) — Valid but out of scope for this PR. Can be added when the SDK is updated to consume this event. Route scope: session vs workspace (Copilot) — `setLanguageAsync` is process-level, but the route needs `sessionId` for ACP channel routing and `clientId` authentication. This matches the `model` switching pattern (also process-level state routed through session endpoints). ACP handler language validation (Copilot) — The HTTP route already validates against `LANGUAGE_CODES`. The ACP handler is only reachable through the bridge, which always goes through the route first. Defense-in-depth can be added later. |
wenshao
left a comment
There was a problem hiding this comment.
(Posted as a body-level comment because the tsc error is on a line not touched by this PR's diff.)
[Nice to have] server.test.ts:752 — tsc reports FakeBridge is missing executeShellCommand from HttpAcpBridge. This is pre-existing from the base branch (not caused by this PR's changes), but will cause npm run typecheck to fail. Add a stub to FakeBridge:
async executeShellCommand() {
throw new Error('not implemented');
},— qwen3.7-max via Qwen Code /review
When language is "auto", persist the literal "auto" to settings instead of the resolved concrete locale. This ensures auto-detection via detectSystemLanguage() is re-evaluated on daemon restart rather than being permanently pinned to whatever locale was resolved at switch time. The response still returns the resolved language via getCurrentLanguage(). Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Mirror the LANGUAGE_CODES allowlist from the HTTP route into the ACP extMethod handler, so direct extMethod callers are also validated. Follows the same pattern as the approval-mode handler. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Only set refreshed=true when at least one session refresh succeeded. Log the count of failed sessions for diagnostics. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
PR Verification ReportPR: #4705 — feat(daemon): add POST /session/:id/language for runtime language switching Test Results
Pre-existing Verification
Code ReviewChanges (6 files, +357/−1):
Key observations:
Verdict✅ Ready to merge — Clean implementation following existing daemon API patterns. All 5 PR-specific functional tests pass; the 1 remaining failure and all 38 pre-existing failures share the same Verified by wenshao |
…ponse Add ?? null guard to outputLanguage in the language_changed SSE event payload, matching the HTTP response path. Without this, an undefined value would be silently omitted by JSON.stringify instead of being explicitly null. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
doudouOUC
left a comment
There was a problem hiding this comment.
Code Review Summary
Overall well-structured PR that follows existing patterns (approval-mode, model-switch). A few issues worth addressing before merge:
Critical
- Race condition on global i18n state — concurrent language-switch requests can corrupt
currentLanguage/translationsglobals. Existing peers (setSessionApprovalMode,setSessionModel) serialize through queues. updateOutputLanguageFilefailure silently swallowed — this is the mechanism by which output language actually takes effect. A silent failure here means the API response lies about the system state.
Important
- Timeout may be insufficient —
initTimeoutMs(10s) could be too short whensyncOutputLanguage=truerefreshes all sessions (disk I/O + potential network calls per session). refreshedflag semantics —refreshed = failedCount < results.lengthistrueeven when 9/10 sessions fail; andfalsewhenallSessionsis empty (vacuously nothing to do, not a failure).- Missing workspace-level event broadcast — language is a user-level setting affecting all sessions, but only the requesting session's bus gets the event.
setSessionApprovalModeusesbroadcastWorkspaceEvent()for this.
Suggestions
- Consider adding
persisted: booleanto the response (like approval-mode does). - The
catch { /* bus closed */ }should at least have a debug log to avoid masking programming errors. - Test coverage: no unit tests for the acpAgent handler logic or bridge method; consider adding tests for partial refresh failures,
language: 'auto'as valid input, and generic 500 error path.
See inline comments for details.
…emantics - Guard session refresh with fileWriteOk: if updateOutputLanguageFile fails, skip refreshHierarchicalMemory (stale file would be re-read) and return outputLanguage: null to signal the failure. - Fix refreshed edge cases: true when zero sessions (nothing to do), true only when ALL sessions succeed (failedCount === 0). - Add debug log to bridge event publish catch block. - Add res.body assertion and 500 error path test. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Add session_language to SERVE_CAPABILITY_REGISTRY so SDK clients can detect runtime language switching support via GET /capabilities. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
PR Verification Report (Re-verification)PR: #4705 — feat(daemon): add POST /session/:id/language for runtime language switching Changes Since Last Review
Test Results
Code ReviewChanges (7 files, +457/−33):
Key improvements from update:
Verdict✅ Ready to merge — Clean implementation following existing daemon API patterns. The updates improve error handling and add proper capability registration. All pre-existing issues are on the Verified by wenshao |
Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Move settings.setValue('general.outputLanguage') inside the fileWriteOk
guard so settings and file stay in sync when the file write fails.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
Second-Round Review
Thanks for the fixes across 4 commits — the PR is substantially improved. Here's a summary of what's been addressed and what remains.
Addressed (nice work)
updateOutputLanguageFilefailure now gated withfileWriteOk— downstream ops skip on failure,outputLanguagereturnsnull. Clean pattern.refreshedlogic fixed toresults.length === 0 || failedCount === 0. Correct semantics.catch { /* bus closed */ }replaced withwriteServeDebugLine(...). Good.- ACP handler defense-in-depth validation added (
allowedLanguages). Matches approval-mode pattern. setLanguageAsyncwrapped in try-catch with proper error propagation.sessionOrThrow()added.session_languagecapability registered.- Telemetry route regex updated.
- 500 test and
syncOutputLanguage: falsebody assertion added.
Still open — awaiting response
Three issues from my first review (and echoed by other reviewers) have no author response yet:
- [Critical] Race condition — no concurrency serialization (see inline)
- [Important] Missing workspace-level event broadcast (see inline)
- [Important]
initTimeoutMsinsufficient for sync path (see inline)
Plus two from wenshao's reviews:
4. [Suggestion] Missing reloadCommands() / sendAvailableCommandsUpdate() — command descriptions stay in old language after switch
5. [Suggestion] LANGUAGE_CODES duplicated between server.ts and acpAgent.ts; extract shared constant
Acknowledged/Deferred (fine for follow-up)
- Bridge-level tests → follow-up PR
- SDK
DAEMON_KNOWN_EVENT_TYPE_VALUESregistration → documented in PR description
Minor: formatting noise
The diff includes ~150 lines of unrelated formatting changes in acpAgent.ts (ternary reformatting, line-break changes). These are fine as changes but make the diff harder to review. Consider separating formatting into a dedicated commit.
See inline comments for the 3 still-open items.
doudouOUC
left a comment
There was a problem hiding this comment.
Review Summary
PR 质量不错,经过多轮 review 后大部分 Critical/Important 问题已修复。三层架构模式一致、错误处理层次清晰、fileWriteOk guard 和 refreshed 语义都处理得当。
仍需关注
-
[Important]
LANGUAGE_CODES重复定义 —server.ts:2135和acpAgent.ts:2808各自计算[...SUPPORTED_LANGUAGES.map(l => l.code), 'auto']。新增语言只更新一处时两层校验会悄悄分歧。建议提取到languageUtils.ts或i18n/index.ts作为共享常量。(wenshao 在 3 轮 review 中重复提出,未见 author 回应) -
[Important] 缺少
reloadCommands()/sendAvailableCommandsUpdate()— 现有的/language ui斜杠命令在切换后调用context.ui.reloadCommands()让命令描述在新语言下重新解析。当前 handler 在 session refresh loop 中只调用了refreshHierarchicalMemory()+refreshSystemInstruction(),缺少命令元数据刷新。API 切换语言后,命令描述停留在旧语言。(wenshao 在 3 轮 review 中重复提出,未见 author 回应)
Suggestions
-
[Suggestion] Response 缺少
sessionId—setSessionApprovalMode返回{ sessionId, mode, previous, persisted },当前只返回{ language, outputLanguage, refreshed }。对 API schema 一致性有轻微影响,不阻塞合入。 -
[Suggestion] 500 测试断言偏松 —
server.test.ts:3426使用expect.any(String)不验证具体错误信息是否正确透传。建议改为精确断言。 -
[Suggestion] ~150 行格式化噪音 —
acpAgent.tsdiff 中大量三元表达式重排和换行调整与功能无关,增加 review 难度。建议拆到独立 commit。
已确认的 defer 项(可接受)
- Bridge-level 测试 → follow-up PR
- SDK
DAEMON_KNOWN_EVENT_TYPE_VALUES注册 → PR description 已标注
doudouOUC
left a comment
There was a problem hiding this comment.
LGTM,整体实现质量不错。三层架构模式遵循一致,错误处理链路完整,fileWriteOk guard 和 refreshed 语义修正后逻辑正确。多轮 review 中的 Critical 问题均已修复。
剩余两个 Important 项(LANGUAGE_CODES 重复定义、缺少 reloadCommands)不阻塞合入,建议在 follow-up 中处理。
…4938) * fix(daemon): language switch writes to wrong output-language.md path ## Problem `POST /session/:id/language` (PR #4705) always writes `output-language.md` to the global `~/.qwen/` path, but `Config.outputLanguageFilePath` may point to a project-level `<cwd>/.qwen/output-language.md` (when it existed at startup). Since `refreshHierarchicalMemory` reads from the Config-bound path, the language switch silently fails when a project-level file exists. Additionally, on a fresh environment where no `output-language.md` exists, the first language switch creates the file but `Config.outputLanguageFilePath` remains `undefined` (readonly), so `refreshHierarchicalMemory` never reads the newly created file. ## Fix 1. **Config.outputLanguageFilePath**: remove `readonly`, add `setOutputLanguageFilePath()` so the path can be registered after first-time file creation. 2. **languageUtils.ts**: add optional `targetPath` parameter to `writeOutputLanguageFile()` and `updateOutputLanguageFile()`. Export `getOutputLanguageFilePath()` for callers that need the global default. 3. **acpAgent.ts**: write to the session Config's actual path. On first-time creation (path was undefined), register the global path on Config. On multi-session refresh, also update each session's own file if its path differs from the one already written. 4. **languageCommand.ts** and **SettingsDialog.tsx**: same Config-bound path fix for the CLI `/language` command and settings dialog. 5. **server.ts**: expose `supportedLanguages` array in `GET /capabilities` so clients can discover valid language codes before calling the endpoint. 6. **SDK**: add `DaemonClient.setSessionLanguage()` method and `SetSessionLanguageResult` type. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: address review findings — type safety, dedup helper, error handling - Add `supportedLanguages` to `CapabilitiesEnvelope` interface (TS2353) - Extract `writeOutputLanguageAndRegisterPath()` helper in languageUtils to eliminate the duplicated get-path/write/register sequence across acpAgent, languageCommand, and SettingsDialog (fixes SettingsDialog missing the registration step) - Wrap file writes in the multi-session refresh loop with try/catch so `refreshHierarchicalMemory` and `refreshSystemInstruction` always run even when a project-level write fails Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: let write errors propagate to allSettled, remove redundant write - Remove inner try/catch in multi-session loop so file-write failures are captured by Promise.allSettled and reflected in `refreshed` - For sessions with no path: only register the global path (the file was already written by the primary write), skip the redundant write - Add test assertion that setOutputLanguageFilePath is called on first-time creation Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: restore try/catch + write for !sessionPath, fix test cast - Restore try/catch around file writes in multi-session loop so refresh always runs (write failures are logged, not propagated) - Restore writeOutputLanguageAndRegisterPath for !sessionPath sessions to handle the case where writtenPath is a project-level path and the global file was never written - Fix TS cast in test assertion (double-cast + bracket notation) Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test: add multi-session language propagation test Verify the fan-out loop handles three session scenarios correctly: - Session A (project path): writeOutputLanguageAndRegisterPath called - Session B (different project path): updateOutputLanguageFile called - Session C (no path): writeOutputLanguageAndRegisterPath + path registration - All sessions: refreshHierarchicalMemory + refreshSystemInstruction Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: improve debug logs, SDK re-export, and add helper unit tests - Include session ID and target path in multi-session write error logs - Re-export SetSessionLanguageResult from top-level SDK barrel - Add 4 unit tests for writeOutputLanguageAndRegisterPath covering config-bound path, undefined fallback, null/undefined config Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: hoist sessionPath declaration out of try block sessionPath was declared with const inside try but referenced in catch, causing a block-scope ReferenceError. Move to let before try. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test: add catch-branch and targetPath coverage - acpAgent: test that refreshHierarchicalMemory still runs when a session's file write throws (catch branch coverage) - languageUtils: test writeOutputLanguageFile with custom targetPath Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: 秦奇 <gary.gq@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Summary
POST /session/:id/languageHTTP endpoint for switching UI language and LLM output language at runtime without polluting session transcriptsetSessionLanguage()→ ACP extMethod handler, following the same pattern asapproval-modeandmodelswitchingsyncOutputLanguage: true, updateoutput-language.md, persist settings, and refresh system prompts across all active sessions so the next LLM call immediately uses the new languageChanges
packages/acp-bridge/src/status.tssessionLanguagetoSERVE_CONTROL_EXT_METHODSpackages/acp-bridge/src/bridgeTypes.tssetSessionLanguage()toHttpAcpBridgeinterfacepackages/acp-bridge/src/bridge.tssetSessionLanguage()bridge methodpackages/cli/src/acp-integration/acpAgent.tssessionLanguageextMethod handlerpackages/cli/src/serve/server.tsPOST /session/:id/languagerouteAPI
Response (200):
{ "language": "zh", "outputLanguage": "Chinese", "refreshed": true }Supported language codes:
zh,zh-TW,en,ja,ru,de,fr,pt,ca,autoTest plan
POST /session/:id/languagewith{"language":"en","syncOutputLanguage":true}→ verify 200 response withoutputLanguage: "English"invalid_languagelanguagefield → 400syncOutputLanguageomitted → 200 withoutputLanguage: null~/.qwen/output-language.mdcontent matches the switched language🤖 Generated with Qwen Code