fix(core): keep YOLO mode when the model calls enter_plan_mode - #6630
Conversation
A model-initiated enter_plan_mode call from YOLO silently switched the session into the read-only Plan mode, surprising users who explicitly chose YOLO for low-friction execution and then blocking the reads/writes they expected to proceed. Genuine user-driven plan-mode entries (Shift+Tab, /plan) call setApprovalMode directly and never route through this tool, so guarding the tool only affects the model deciding to plan on its own. From YOLO the tool now keeps the current mode and returns a message telling the model to continue planning without switching. Fixes QwenLM#5970
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI failing: triage, Test (ubuntu-latest, Node 22.x).
— qwen3.7-max via Qwen Code /review
| // call config.setApprovalMode directly and never route through this tool, so | ||
| // gating here only affects the model deciding to plan on its own. Keep the | ||
| // current mode and tell the model to continue planning without switching. | ||
| if (this.config.getApprovalMode() === ApprovalMode.YOLO) { |
There was a problem hiding this comment.
[Critical] This guard blocks every enter_plan_mode invocation while the current mode is YOLO, including the valid path where the user explicitly asks in chat or ACP to enter plan mode. The tool description still says to use this tool after the user asks for plan mode, but this branch returns before setApprovalMode(PLAN), and the returned message even tells the model to switch only if the user explicitly asks despite there being no different tool path to do that. In YOLO sessions, an explicit user request to lower privileges through enter_plan_mode now becomes impossible unless the UI has a separate /plan or shortcut path, which breaks the core approval-mode boundary for headless/ACP and natural-language mode switching.
Suggested fix: only suppress truly unsolicited model-initiated entries, or add an explicit invocation flag/context that distinguishes a user-requested plan-mode entry from an autonomous one; user-requested entries from YOLO should still call setApprovalMode(ApprovalMode.PLAN, { enteredByModel: true }) or an equivalent user-requested path.
— GPT-5 via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed with 10 parallel agents. Build passes, all 15 tests pass. Core logic correctly implements the maintainer-confirmed direction from issue #5970 — model-initiated enter_plan_mode from YOLO is now a no-op that preserves the user's chosen mode.
Three low-confidence items for human review: (1) no debug log on the new YOLO guard path — other early-return paths emit debugLogger calls; (2) no test for YOLO + non-interactive mode ordering; (3) test doesn't assert result.returnDisplay.
Note: the existing open comment at enterPlanMode.ts:86 raises a related concern about the guard blocking user-explicit chat requests — worth addressing in a follow-up.
— qwen3.7-max via Qwen Code /review
Addresses review feedback on QwenLM#6630. The previous guard suppressed every enter_plan_mode invocation while the session was in YOLO mode. That fixes the unsolicited switch reported in QwenLM#5970, but it also blocks the legitimate path: the tool description tells the model to call this tool only after the user explicitly asks, and /plan is interactive-only (supportedModes: ['interactive']) with no Shift+Tab equivalent. In a headless or ACP YOLO session the tool is the only door into plan mode, so a blanket guard made an explicit user request unreachable. Add an optional userRequested flag to the tool schema and only no-op when the entry is NOT user-requested. A user-requested entry still goes through setApprovalMode(PLAN, { enteredByModel: true }) so the Plan Approval Gate on exit continues to run for AUTO/YOLO sessions (QwenLM#5574).
|
Thanks for the review, the objection was correct. The tool description makes this tool the user-requested path, and since Pushed the suggested design in 5b16e74:
One honest caveat: the flag is model-attested, so a model that ignores the tool description could also set it incorrectly. For users who want a hard guarantee, |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
The userRequested flag correctly distinguishes model-initiated from user-requested plan-mode entries, preserving headless/ACP access while blocking unsolicited switches from YOLO. Guard ordering, test coverage (18/18 pass), and typecheck are all clean.
— qwen3.7-max via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
| return { | ||
| llmContent: | ||
| 'Plan mode was not entered: the session is in YOLO mode, which the user explicitly chose for low-friction execution. Continue investigating and presenting your plan in the current mode without switching. If the user explicitly asked for plan mode in this turn, retry this tool call with userRequested: true.', | ||
| returnDisplay: 'Stayed in YOLO mode (plan mode not entered).', |
There was a problem hiding this comment.
[Suggestion] The YOLO guard's no-op return has no debugLogger call. Other early-return paths (subagent block via logger.warn, catch block via debugLogger.error) log — this one should too. When a user reports "I asked for plan mode and nothing happened," the oncall engineer grepping ENTER_PLAN_MODE finds zero evidence the guard fired.
| returnDisplay: 'Stayed in YOLO mode (plan mode not entered).', | |
| debugLogger.info( | |
| 'Blocked model-initiated plan entry from YOLO (userRequested=%s)', | |
| this.params.userRequested, | |
| ); | |
| return { | |
| llmContent: |
— qwen3.7-max via Qwen Code /review
| // The tool description instructs the model to call this only after the | ||
| // user asks, and `/plan` is interactive-only — so this tool is the only | ||
| // door into plan mode for headless/ACP sessions. A blanket YOLO guard | ||
| // would make an explicit user request unreachable there. |
There was a problem hiding this comment.
[Suggestion] This test only asserts setApprovalMode was not called and mode stayed YOLO, but does not verify the return value. The sibling "unsolicited" test asserts on llmContent (contains 'YOLO', not 'Plan mode is now active', contains 'userRequested: true'). Both paths share the same return object today, but the weaker test provides no regression safety on the user-facing message.
| // would make an explicit user request unreachable there. | |
| expect(mockConfig.setApprovalMode).not.toHaveBeenCalled(); | |
| expect(approvalMode).toBe(ApprovalMode.YOLO); | |
| expect(result.llmContent).toContain('YOLO'); | |
| expect(result.returnDisplay).toContain('Stayed in YOLO'); |
— qwen3.7-max via Qwen Code /review
| expect(approvalMode).toBe(ApprovalMode.PLAN); | ||
| expect(result.llmContent).not.toContain('non-interactive'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
[Suggestion] All userRequested: true tests start from ApprovalMode.YOLO. No test passes the flag from DEFAULT, AUTO, or AUTO_EDIT. The parameter is inert in non-YOLO modes (the guard never fires), but a defensive test would catch future regressions if the flag accidentally gained significance outside YOLO. Consider adding a test like tool.build({ userRequested: true }) from ApprovalMode.DEFAULT asserting normal plan-mode entry.
— qwen3.7-max via Qwen Code /review
- Log via debugLogger.info when the guard suppresses a model-initiated entry, so a "I asked for plan mode and nothing happened" report is diagnosable by grepping ENTER_PLAN_MODE (the other early-return paths already log). - Strengthen the userRequested:false test to assert on the returned llmContent/returnDisplay, matching the unsolicited-entry sibling test. - Add a defensive test pinning that userRequested is inert outside YOLO: DEFAULT with the flag set enters plan mode normally.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
packages/core/src/tools/enterPlanMode.ts:110 |
debugLogger.info uses printf-style %s formatting, but formatArgs joins args with spaces (no substitution). Log output will contain literal %s. |
Use a template literal: debugLogger.info(`Blocked model-initiated plan entry from YOLO (userRequested=${this.params.userRequested})`) |
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
|
Thanks for the PR @Nas01010101! Template looks good ✓ — all required sections present, bilingual, test plan included. Problem: Observed bug with clear evidence. Issue #5970 has a screenshot showing the model silently switching from YOLO to Plan mode and then failing to write files. This is a confirmed regression of #5428 — priority/P2, type/bug, labeled by maintainers. Not theoretical. Direction: Aligned. The maintainer-confirmed direction in #5970's thread is exactly what this PR implements: model-initiated Size: 45 production logic lines (43+2 in Approach: Scope is tight — one file changed in production, one in tests, doing exactly one thing. The One thing worth noting: the Moving on to code review and testing. 🔍 中文说明感谢贡献 @Nas01010101! 模板完整 ✓ — 所有章节齐全,双语,测试计划完备。 问题: 已观测到的 bug,有明确证据。Issue #5970 附有截图,显示模型从 YOLO 静默切换到 Plan 模式后写入文件失败。这是 #5428 的回归——已被维护者标记为 priority/P2、type/bug。不是理论性问题。 方向: 对齐。#5970 讨论中维护者确认的方向正是此 PR 所实现的:YOLO 下模型主动调用 规模: 45 行生产代码( 方案: 范围紧凑——生产改一个文件、测试改一个文件,只做一件事。 进入代码审查和测试阶段 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading diff): I'd add a guard in PR's approach vs mine: The PR is materially better. A blanket YOLO guard would break headless/ACP sessions where Critical blockers: None. AGENTS.md violations: None. Minimal change, focused scope, no over-abstraction. Reuse check: No existing mechanism for distinguishing user-requested vs model-initiated tool calls in this codebase. The TestingUnit tests
Real-scenario tmux testBoth the installed build (v0.19.8) and dev build ( Before (installed build)After (this PR via dev build)Both runs exit at the same point (API auth) — the guard is never reached because the model call fails before tool selection. Unit tests are the correct verification surface for this change. 中文说明代码审查独立方案(读 diff 之前): 在 PR 方案 vs 我的方案: PR 更优。简单 YOLO 守卫会破坏 headless/ACP 会话——那里 阻断性问题: 无。 AGENTS.md 违规: 无。改动最小化,范围聚焦,无过度抽象。 测试
tmux 真实场景测试在此 CI 环境因 API 认证问题均未能完成(非代码问题)。PR 自身描述也注明此为非用户可见的控制流变更,单元测试是正确验证面。 — Qwen Code · qwen3.7-max |
|
This is a clean bug fix. Let me say that plainly because it's worth saying: the Issue #5970 is real — P2 bug with a screenshot, confirmed regression of #5428. The fix is 45 production lines doing exactly one thing: preventing YOLO sessions from silently flipping to read-only Plan mode when the model decides to plan on its own. Tests go from 15 to 19, all green, covering the guard, the escape hatch ( The tmux testing couldn't run (CI auth), but the PR is upfront about this being a control-flow change best verified by unit tests — and the unit tests are thorough. One note: @qqqys has a Approving on the strength of the code review and test results. ✅ 中文说明这是一个干净的 bug 修复。直说: Issue #5970 是真实的——P2 bug,附截图,#5428 的确认回归。修复用 45 行生产代码做一件事:阻止 YOLO 会话在模型自主决定规划时静默切换到只读 Plan 模式。测试从 15 增至 19,全部通过,覆盖守卫、逃生通道( tmux 测试因 CI 认证问题未能运行,但 PR 已明确说明这是最适合通过单元测试验证的控制流变更——而单元测试是彻底的。 基于代码审查和测试结果,批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
✅ Maintainer local verification reportI built and ran a real local verification of this PR — not only the PR's own unit tests, but an independent test that drives the real
1) PR unit tests + independent real-
|
| Start mode | userRequested |
Result | Note |
|---|---|---|---|
| YOLO | unset | stays YOLO (no switch) | ⭐ the fix — #5970 |
| YOLO | false |
stays YOLO | explicit opt-out honored |
| YOLO | true |
enters PLAN, enteredByModel=true |
escape hatch for headless/ACP; Plan Approval Gate still runs on exit (#5574) |
| DEFAULT | any | enters PLAN | unchanged |
| AUTO | unset | enters PLAN | unchanged (fix is YOLO-scoped) |
| AUTO_EDIT | unset | enters PLAN | unchanged |
For the YOLO no-op path I also asserted getPrePlanMode() stays DEFAULT and getPlanGateState() stays undefined — i.e. plan mode was genuinely never entered, not just cosmetically suppressed.
2) Regression proof — the guard is load-bearing
I temporarily deleted the guard block and re-ran the PR suite. The session flips YOLO → PLAN again and the two guard tests fail with the exact #5970 symptom (setApprovalMode called once). Restoring the guard makes all 19 pass again.
3) Full packages/core suite + static checks — 0 failed
vitest run(full core): 15051 passed | 8 skipped, 0 failed (491 files)tsc --noEmit(core): clean ·eslint --max-warnings 0: clean ·prettier --check: clean
Notes for merge
- ✅ Correct and well-scoped: a model-initiated
enter_plan_modefrom YOLO is now a no-op that keeps the mode and tells the model to keep planning; user-driven entries (Shift+Tab,/plan) are untouched. - ✅ The
userRequested: trueescape hatch keeps the only plan-mode door reachable in headless/ACP sessions while still routing through the Plan Approval Gate on exit (exit_plan_mode auto-executes without confirmation when entering plan mode via Shift+Tab and prePlanMode appears to be yolo #5574) — good. - ℹ️ By design the fix is YOLO-only; AUTO / AUTO_EDIT still allow model-initiated plan entry. The PR description already calls this out as intentional and a one-line extension if maintainers later want AUTO covered. No change requested.
Reproduce locally
git fetch origin pull/6630/head:pr-6630 && git checkout pr-6630
npm ci
# PR's own tests
npx vitest run packages/core/src/tools/enterPlanMode.test.ts
# full core suite
cd packages/core && npx vitest run
# static checks
npx tsc --noEmit
npx eslint --max-warnings 0 src/tools/enterPlanMode.ts src/tools/enterPlanMode.test.ts
npx prettier --check src/tools/enterPlanMode.ts src/tools/enterPlanMode.test.tsThe independent real-Config test used above is a local-only helper (not part of this PR). Its core is:
function makeTrustedConfig(startMode: ApprovalMode): Config {
const config = new Config({ targetDir: '.', cwd: '.', model: 'test-model', interactive: true });
vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true);
if (startMode !== ApprovalMode.DEFAULT) config.setApprovalMode(startMode);
return config;
}
// YOLO + {} -> tool.build({}).execute(): getApprovalMode() stays YOLO,
// getPrePlanMode() === DEFAULT, getPlanGateState() === undefined
// YOLO + { userRequested: true } -> enters PLAN, planGateState.enteredByModel === true🇨🇳 中文版本(点击展开)
✅ 维护者本地验证报告
我在本地对该 PR 做了真实验证——不仅跑了 PR 自带的单元测试,还额外写了一个**用真实 Config 驱动真实 EnterPlanModeTool(不使用行为 mock)**的独立测试,并复现了回归、跑了整个 packages/core 测试套件。结论供合并参考。
| 项目 | 值 |
|---|---|
| 验证 commit | a94d4213b(PR head) |
| 基线 | main @ f06e93226 |
| 环境 | macOS 15.7.7 · Node v22.23.1 |
| 受测改动 | enterPlanMode.ts、enterPlanMode.test.ts — +127 / -6 |
| 结论 | ✅ 行为与描述一致,无回归。 |
1) PR 单元测试 + 独立真实 Config 验证 — 全部通过。 PR 自带的 enterPlanMode.test.ts(19 个用例)通过。我另加了一个仅本地使用的测试(7 个用例),它不复用作者的 mock Config,而是构造真实 Config(仅 stub isTrustedFolder 以便在临时目录可进入 YOLO,沿用 exitPlanMode.test.ts 中已有的"真实 Config + 真实工具"先例),端到端地走真实的 setApprovalMode / prePlanMode / planGateState 逻辑,并打印每个场景实际观测到的模式切换(见上方第 1 张截图)。
行为矩阵:YOLO + 未设标志 → 保持 YOLO 不切换(即本次修复,#5970);YOLO + userRequested:false → 保持 YOLO;YOLO + userRequested:true → 进入 PLAN 且 enteredByModel=true(headless/ACP 下的逃生通道,退出时仍走 Plan Approval Gate,#5574);DEFAULT / AUTO / AUTO_EDIT → 照常进入 PLAN(不变)。对 YOLO no-op 路径,我还断言了 getPrePlanMode() 仍为 DEFAULT、getPlanGateState() 仍为 undefined,即确实从未进入 plan 模式,而非仅表面抑制。
2) 回归证明 — 该守卫是关键。 临时删除守卫代码块后重跑,会话再次 YOLO → PLAN,两个守卫用例以 #5970 的确切症状失败(setApprovalMode 被调用一次);恢复守卫后 19 个用例全部通过(见第 2 张截图)。
3) 完整 packages/core 套件 + 静态检查 — 0 失败。 vitest run(core 全量):15051 通过 | 8 跳过,0 失败(491 个文件);tsc --noEmit、eslint --max-warnings 0、prettier --check 均干净(见第 3 张截图)。
合并意见: 修复正确且范围收敛——YOLO 下模型主动 enter_plan_mode 现在为 no-op,保持当前模式并提示模型继续规划;用户主动进入(Shift+Tab、/plan)不受影响。userRequested:true 逃生通道在保证 headless/ACP 可进入 plan 的同时仍触发退出时的 Plan Approval Gate(#5574),设计良好。按设计本修复仅限 YOLO,AUTO/AUTO_EDIT 仍允许模型主动进入 plan——PR 描述已说明这是有意为之、如需覆盖 AUTO 只需一行扩展,无需改动。
截图为本地运行结果,托管在我个人 fork 的独立分支
wenshao/qwen-code@assets/pr-6630-verify,不影响上游仓库。
Verified locally by the maintainer. Screenshots are hosted on an isolated branch of a personal fork and can be removed later.
…M#6630) * fix(core): keep YOLO mode when the model calls enter_plan_mode A model-initiated enter_plan_mode call from YOLO silently switched the session into the read-only Plan mode, surprising users who explicitly chose YOLO for low-friction execution and then blocking the reads/writes they expected to proceed. Genuine user-driven plan-mode entries (Shift+Tab, /plan) call setApprovalMode directly and never route through this tool, so guarding the tool only affects the model deciding to plan on its own. From YOLO the tool now keeps the current mode and returns a message telling the model to continue planning without switching. Fixes QwenLM#5970 * fix(core): gate the YOLO plan-mode guard on an explicit user request Addresses review feedback on QwenLM#6630. The previous guard suppressed every enter_plan_mode invocation while the session was in YOLO mode. That fixes the unsolicited switch reported in QwenLM#5970, but it also blocks the legitimate path: the tool description tells the model to call this tool only after the user explicitly asks, and /plan is interactive-only (supportedModes: ['interactive']) with no Shift+Tab equivalent. In a headless or ACP YOLO session the tool is the only door into plan mode, so a blanket guard made an explicit user request unreachable. Add an optional userRequested flag to the tool schema and only no-op when the entry is NOT user-requested. A user-requested entry still goes through setApprovalMode(PLAN, { enteredByModel: true }) so the Plan Approval Gate on exit continues to run for AUTO/YOLO sessions (QwenLM#5574). * fix(core): address review suggestions on the YOLO plan-mode guard - Log via debugLogger.info when the guard suppresses a model-initiated entry, so a "I asked for plan mode and nothing happened" report is diagnosable by grepping ENTER_PLAN_MODE (the other early-return paths already log). - Strengthen the userRequested:false test to assert on the returned llmContent/returnDisplay, matching the unsolicited-entry sibling test. - Add a defensive test pinning that userRequested is inert outside YOLO: DEFAULT with the flag set enters plan mode normally. --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
* feat(web-shell): add mobile welcome composer slots * refactor(web-shell): deduplicate MessageList JSX and remove dead CSS reference - Extract ~80 lines of duplicated MessageList rendering into shared variables with conditional props and wrapper - Remove dead chatPaneWithWelcomeMiddle className reference (CSS class never defined) - Document mobileWelcomeFooterMiddle dependency on renderWelcomeFooter in JSDoc * fix(web-shell): stabilize MessageList tree position and conditional customFooter wrapper - Use stable outer wrapper div for IIFE to prevent MessageList unmount/remount when showMobileWelcomeFooterMiddle toggles - Only wrap CustomFooter in styles.customFooter div when hasMobileComposerBottom is true, avoiding DOM depth change for non-mobile consumers * fix(release): raise package size budget to 85 MiB (QwenLM#6688) * fix(interactive): configure Docker sandbox networking for protocol tag retry test (QwenLM#6684) (QwenLM#6689) The protocol-tags-interactive.test.ts started the fake OpenAI server on 127.0.0.1 without Docker-aware host options, making it unreachable from inside the Docker sandbox container. The CLI running in the container tried to connect to 127.0.0.1 which resolved to the container's own loopback, not the host where the test server listens. Bind the fake server to 0.0.0.0 and advertise host.docker.internal as the base URL host when QWEN_SANDBOX is docker or podman, matching the established pattern in tool-control.test.ts. Also set NO_PROXY to include host.docker.internal so the CLI does not route sandbox model requests through an HTTP proxy. Co-authored-by: qwen-autofix[bot] <qwen-autofix[bot]@users.noreply.github.com> * fix(core): keep YOLO mode when the model calls enter_plan_mode (QwenLM#6630) * fix(core): keep YOLO mode when the model calls enter_plan_mode A model-initiated enter_plan_mode call from YOLO silently switched the session into the read-only Plan mode, surprising users who explicitly chose YOLO for low-friction execution and then blocking the reads/writes they expected to proceed. Genuine user-driven plan-mode entries (Shift+Tab, /plan) call setApprovalMode directly and never route through this tool, so guarding the tool only affects the model deciding to plan on its own. From YOLO the tool now keeps the current mode and returns a message telling the model to continue planning without switching. Fixes QwenLM#5970 * fix(core): gate the YOLO plan-mode guard on an explicit user request Addresses review feedback on QwenLM#6630. The previous guard suppressed every enter_plan_mode invocation while the session was in YOLO mode. That fixes the unsolicited switch reported in QwenLM#5970, but it also blocks the legitimate path: the tool description tells the model to call this tool only after the user explicitly asks, and /plan is interactive-only (supportedModes: ['interactive']) with no Shift+Tab equivalent. In a headless or ACP YOLO session the tool is the only door into plan mode, so a blanket guard made an explicit user request unreachable. Add an optional userRequested flag to the tool schema and only no-op when the entry is NOT user-requested. A user-requested entry still goes through setApprovalMode(PLAN, { enteredByModel: true }) so the Plan Approval Gate on exit continues to run for AUTO/YOLO sessions (QwenLM#5574). * fix(core): address review suggestions on the YOLO plan-mode guard - Log via debugLogger.info when the guard suppresses a model-initiated entry, so a "I asked for plan mode and nothing happened" report is diagnosable by grepping ENTER_PLAN_MODE (the other early-return paths already log). - Strengthen the userRequested:false test to assert on the returned llmContent/returnDisplay, matching the unsolicited-entry sibling test. - Add a defensive test pinning that userRequested is inert outside YOLO: DEFAULT with the flag set enters plan mode normally. --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * feat(cli): forward ask_user_question answers from SDK can_use_tool (QwenLM#6655) * feat(cli): forward ask_user_question answers from SDK can_use_tool SDK-hosted agents could receive ask_user_question calls through the can_use_tool callback and approve them, but the user's answers never reached the tool: the CLI called onConfirm(ProceedOnce) with no payload, so the tool read an empty answers map and the model never got the decisions. Route updatedInput.answers from the SDK's allow response into the tool confirmation payload so the collected answers reach the tool. Reuses the existing updatedInput channel — no new SDK API or types. Document the pattern in the TypeScript and Python SDK READMEs. * fix(cli): forward ask_user_question answers on teammate approval path Address review feedback on QwenLM#6655: - handleTeammateApproval now mirrors the leader path and promotes the user's answers from updatedInput into the confirmation payload, so ask_user_question calls approved through a teammate no longer drop the user's choices (wenshao). - Extract a shared buildAllowConfirmationPayload helper used by both the leader and teammate paths, and only promote `answers` for ask_user_question so a same-named field on any other tool's input can't leak into the payload. - Add tests for the teammate path and the defensive guards (array updatedInput, array/null/empty answers, foreign answers field). * test(web-shell): stub Range client-rect methods to fix flaky CI CodeMirror's async measure pass (scheduled via requestAnimationFrame) calls getClientRects()/getBoundingClientRect() on a text Range. jsdom implements these on Element but not on Range, so the call throws "textRange(...).getClientRects is not a function" from a rAF callback after the test completed. Vitest surfaces it as an unhandled error and fails the whole run with exit code 1 even though every assertion passed (seen intermittently in useComposerCore.dom.test.tsx). Polyfill both methods on Range.prototype in the shared test setup, mirroring the existing ResizeObserver/scrollIntoView stubs. * refactor(cli): use ToolNames constant and broaden permission tests Address review suggestions on QwenLM#6655: - buildAllowConfirmationPayload now gates answers-promotion on the ToolNames.ASK_USER_QUESTION constant instead of a bare string literal, so a future rename of the tool name is a compile-time break rather than a silent regression. - Add an it.each case for a non-object primitive updatedInput (string) to cover the `typeof updatedInput !== 'object'` guard branch. - Assert the leader path overrides toolCall.request.args with the host's sanitized updatedInput before confirming. - Add a teammate-path test for an allow response with no updatedInput, asserting respond is called with (ProceedOnce, undefined). --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> * fix(cli): localize approval mode UI labels (QwenLM#6592) * fix(cli): localize approval mode UI labels * fix(cli): address approval mode i18n review * fix(cli): stabilize approval mode i18n key * test(cli): cover approval mode i18n follow-up * test(cli): cover localized auto indicator * test(cli): address approval i18n suggestions --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * feat(dingtalk): mention response senders (QwenLM#6679) * docs: design DingTalk at-sender replies * docs: plan DingTalk at-sender replies * feat(channels): preserve session for response delivery * feat(dingtalk): optionally mention response sender * docs(dingtalk): explain response mentions * fix(dingtalk): retain queued mention targets * fix(dingtalk): bound mention target lifecycle * fix(dingtalk): clear synthetic command mention target * fix(dingtalk): clear buffered targets on session death * debug(dingtalk): log mention delivery result * fix(dingtalk): render response mentions * fix(dingtalk): send visible response mentions * feat(dingtalk): use text replies for mentions * fix(dingtalk): preserve mentioned text replies * feat(web-shell): add artifact right panel (QwenLM#6591) * feat(web-shell): add artifact right panel * fix(web-shell): address artifact panel review feedback * fix(web-shell): handle artifact panel review edge cases * fix(web-shell): tighten scheduled task parsing * fix(web-shell): address artifact panel review followups * fix(web-shell): guard large file diff stats * fix(web-shell): address review panel suggestions * test(webui): stabilize heartbeat prompt cleanup test * fix(web-shell): address artifact review refresh issues * test(web-shell): stabilize ChatPane artifact hook mock * fix(web-shell): clear stale session artifacts while loading * fix(web-shell): preserve artifact tabs during refresh * fix(web-shell): address artifact review followups * fix(web-shell): respect workspace cwd for artifact outputs * fix(web-shell): scope artifact panel actions to pane * fix(web-shell): resolve split pane merge conflict * fix(web-shell): clear stale artifact panel state * fix(web-shell): preserve leading turn outputs * fix(web-shell): tighten turn output selectors * fix(web-shell): harden artifact preview sanitizer * fix(web-shell): address artifact panel review regressions * fix(web-shell): reconcile split pane artifact snapshots * fix(web-shell): clear pane artifacts on session switch * fix(web-shell): clear stale right panel snapshots * fix(web-shell): repair scheduled task hint string --------- Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> * feat(cli): workspace-qualified ACP transport (daemon multi-workspace phase 4) (QwenLM#6621) * docs(design): add daemon multi-workspace phase 4 (workspace-qualified ACP) design * feat(cli): add workspace-qualified ACP transport (issue QwenLM#6378 phase 4) Per-runtime ACP dispatcher at /workspaces/:workspace/acp (HTTP + WS) dispatched by URL path from the single upgrade listener; per-runtime device-flow + reverse client-MCP; owner-index via bridge lifecycle; untrusted/unknown rejected; legacy /acp unchanged; advertise workspace_qualified_acp for multi-workspace. * fix(cli): keep per-runtime device-flow registry out of serve fast-path bundle Phase 4 secondary-runtime device-flow statically imported createDeviceFlowRegistry into run-qwen-serve, pulling glob/@iarna/toml into the serve fast-path bundle and failing the closure check. Import it dynamically at the creation site; the check now passes and behavior is unchanged. * refactor(cli): drop per-runtime device-flow for secondary workspaces Follow-up to the fast-path fix: instead of dynamically importing createDeviceFlowRegistry for secondary runtimes, drop the per-runtime device-flow wiring entirely. Secondary ACP device-flow falls back to the dispatcher default, keeping the serve fast-path bundle closure clean without the dynamic-import indirection. WorkspaceRuntime.deviceFlowRegistry stays optional for a future per-runtime hook. * fix(cli): share daemon-global device-flow across ACP mounts; harden WS path parsing Secondary ACP mounts share the daemon-global device-flow registry (single instance per daemon) instead of a per-runtime one; the event sink fans out to every trusted runtime bridge so secondary ACP clients receive their own flow events, fixing the reviewer QwenLM#6621 Critical and the CI test failure. Drops WorkspaceRuntime.deviceFlowRegistry. WS upgrade path is parsed from the raw request-target instead of new URL().pathname, rejecting %2e%2e / backslash / dot-segment traversal. * refactor(cli): gate CDP claim on primary mount; return plural ACP POST promise Add a primary flag to RuntimeAcpMount so a secondary workspace's ACP connection cannot claim the CDP tunnel -- the claim is gated on activeMount.primary, matching the primary-only chrome-devtools MCP wiring. The plural /workspaces/:workspace/acp POST handler returns the dispatch promise instead of voiding it. * refactor(cli): centralize ACP-HTTP enablement in resolveAcpHttpEnabled Add resolveAcpHttpEnabled() as the single interpretation of the QWEN_SERVE_ACP_HTTP opt-out, replacing four independent env checks across mount, voice-WS advertisement, and CDP-MCP gating. Advertise workspace_qualified_acp only when the ACP HTTP surface is enabled AND multi-workspace sessions are active, so it is not announced when ACP HTTP is disabled. * feat(cli): ACP dispose 503 gate + aggregate connection snapshot across mounts After dispose() the shared ACP HTTP handlers (legacy /acp + workspace-qualified) return 503 server_disposed instead of racing torn-down registries during the shutdown drain. Add AcpHttpHandle.getSnapshot() aggregating connection and wsStream counts across the primary mount and every trusted secondary runtime, and switch the metrics sampler to it so daemon metrics report all workspaces' ACP connections rather than only the primary's. * test(cli): cover ACP dispose 503, aggregate snapshot, and raw dot-segment WS reject * docs(design): record Phase 4 ACP systematic rework (8-axis hardening) Correct the Summary (the device-flow registry stays daemon-global and shared, not per-runtime) and add a section documenting the final architecture: runtime mount factory, routing/trust isolation, raw request-target WS parsing, daemon-global device-flow with event-sink fan-out, primary-only CDP, disposed 503 gate, aggregate getSnapshot, and resolveAcpHttpEnabled-gated capability advertisement. * fix(cli): align /daemon/status ACP counts with the aggregate mount snapshot Code review found a drift: the metrics sampler switched to the aggregate AcpHttpHandle.getSnapshot() (all mounts) while /daemon/status still read the primary-only registry snapshot, so the two observability surfaces diverged under multi-workspace. Extend AcpHttpSnapshot to aggregate all transport counters (connection/session/sse/ws streams + pending client requests) and feed the /daemon/status transport summary from it; per-connection diagnostics and the connection cap stay primary-scoped. Also refresh the device-flow-registry doc comment to the daemon-global shared model. * test(cli): regression-test device-flow on a trusted secondary workspace Locks in the reviewer Critical fix: a trusted secondary workspace's ACP now shares the daemon-global device-flow registry, so device_flow/start reaches provider resolution (an unsupported-provider error here) instead of erroring 'Device flow not configured'. Wires a shared DeviceFlowRegistry into the test harness and drives initialize + device_flow/start over the secondary WebSocket. * docs(design): mark the superseded per-runtime device-flow section Address PR QwenLM#6621 review: the pre-rework 'Per-runtime device-flow registry' section contradicted Systematic rework axis 4 (daemon-global shared registry + fan-out). Flag it as superseded design-history so readers don't build the wrong mental model. * refactor(cli): mount ACP only for trusted secondary workspaces Address PR QwenLM#6621 review suggestions: (1) skip creating a dispatcher/registry/remember-lane for untrusted non-primary workspaces (they are 403-rejected before any mount lookup), so they no longer appear as always-zero entries in the aggregate getSnapshot(); (2) test that a secondary workspace cannot claim the process-wide CDP tunnel (primary-only guard); (3) test that a WS upgrade to an unknown selector is rejected 400. * test(cli): cover device-flow event fan-out across bridges Address PR QwenLM#6621 review: the resolveEventBridges fan-out (the reviewer Critical fix's core delivery path) had zero test coverage. Add unit tests that a device-flow event reaches every resolved bridge, that one bridge throwing does not block the others (best-effort), and that it falls back to the single bridge when no resolver is provided. * fix(cli): report ACP connection pressure across all mounts Address PR QwenLM#6621 review: the connection_capacity_high warning read the primary mount's snapshot only, so a saturated secondary workspace was invisible. Compute the busiest mount from the aggregate snapshot (per-mount cap is uniform, opts.maxConnections) so any mount nearing capacity triggers the warning. * test(cli): allow acp-http-enabled.ts in the serve process.env guard Fix CI failure on PR QwenLM#6621: the serve process.env guard flagged the new acp-http-enabled.ts as a direct process.env reader. It is the QWEN_SERVE_ACP_HTTP interpreter extracted from index.ts and serve-features.ts (both already allow-listed); QWEN_SERVE_ACP_HTTP is a daemon-level process-global toggle, so the file inherits their allow-list entry. * docs: harden workspace-qualified ACP design Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * docs: plan workspace-qualified ACP hardening Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): align workspace-qualified ACP routing Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): harden qualified ACP request errors Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): cover unmarked URIError fallback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): make ACP disposal terminal Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): aggregate ACP connection diagnostics Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * chore: remove review process artifact Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): address workspace ACP review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(cli): finish ACP review follow-ups Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qqqys <qys177@gmail.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-autofix[bot] <qwen-autofix[bot]@users.noreply.github.com> Co-authored-by: nas <156536069+Nas01010101@users.noreply.github.com> Co-authored-by: Tianyuan <2720711917@qq.com> Co-authored-by: han <2992336417@qq.com> Co-authored-by: ytahdn <1294726970@qq.com> Co-authored-by: ytahdn <ytahdn@gmail.com> Co-authored-by: jinye <djy1989418@126.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>



What this PR does
When the session is in YOLO mode, a model-initiated
enter_plan_modetool call no longer switches the session into the read-only Plan mode. The tool now keeps the current mode and returns a message telling the model to continue investigating and presenting its plan without switching. Genuine user-driven plan-mode entries (Shift+Tab,/plan) are unaffected, and behavior in DEFAULT, AUTO_EDIT, and AUTO modes is unchanged.Why it's needed
A user who starts with
qwen -yhas explicitly opted into low-friction execution. Today, if the model decides on its own to callenter_plan_mode, the session silently flips into the read-only Plan mode — which then blocks the file reads and writes the user expected to proceed, and can leave them stuck. This is a regression of #5428. The maintainer-confirmed direction (issue thread) is that a model-initiated plan-mode entry from YOLO should be a no-op that tells the model to keep planning in the current mode, rather than prompting or switching.enter_plan_mode.execute()is only ever reached for model-initiated entries — user actions callsetApprovalModedirectly and never route through the tool — so guarding here targets exactly the unwanted case.Reviewer Test Plan
How to verify
npx vitest run packages/core/src/tools/enterPlanMode.test.ts— theshould not switch from YOLO to PLAN (model-initiated entry is a no-op)case asserts that from YOLO,setApprovalModeis not called, the mode stays YOLO, and the result does not claim plan mode was entered. Reverting the guard makes this case fail (setApprovalModecalled once).qwen -y, give a task that tempts the model to plan; confirm the session stays in YOLO and the model keeps working/planning inline instead of switching to Plan mode. Confirm Shift+Tab //planstill enter Plan mode manually.Evidence (Before & After)
N/A (non-user-visible control-flow change; verified via unit tests below).
Before (unpatched):
AssertionError: expected "spy" to not be called at all, but actually been called 1 times.After:
Test Files 1 passed (1) / Tests 15 passed (15).Full core suite:
14984 passed | 8 skipped (14992), 0 failed.Tested on
Environment (optional)
Unit tests only (Node v22+). eslint
--max-warnings 0, prettier--check, andtsc --noEmit(core) all clean.Risk & Scope
enter_plan_modefrom YOLO changes behavior; user-driven entry and all other modes are unchanged.Linked Issues
Fixes #5970
中文说明
这个 PR 做了什么
当会话处于 YOLO 模式时,模型主动调用
enter_plan_mode工具将不再把会话切换到只读的 Plan 模式。该工具现在会保持当前模式,并返回一条消息,告诉模型在不切换模式的情况下继续调查并给出计划。用户主动进入 Plan 模式的方式(Shift+Tab、/plan)不受影响;DEFAULT、AUTO_EDIT、AUTO 模式下的行为也保持不变。为什么需要
使用
qwen -y启动的用户显式选择了低摩擦执行。目前,如果模型自行决定调用enter_plan_mode,会话会静默切换到只读的 Plan 模式,从而阻止用户期望进行的文件读写,甚至让用户卡住。这是 #5428 的回归。issue 讨论中维护者确认的方向是:在 YOLO 下由模型发起的进入 Plan 模式应当是一个 no-op,告知模型在当前模式下继续规划,而不是弹窗或切换。enter_plan_mode.execute()只会在模型发起时被触达——用户操作直接调用setApprovalMode,从不经过该工具——因此在此加守卫正好只针对不希望发生的情况。审阅者测试计划
如何验证
npx vitest run packages/core/src/tools/enterPlanMode.test.ts——should not switch from YOLO to PLAN用例断言:从 YOLO 出发不调用setApprovalMode,模式仍为 YOLO,且结果不会声称已进入 Plan 模式。移除守卫会使该用例失败(setApprovalMode被调用一次)。qwen -y启动,给一个会诱导模型规划的任务;确认会话保持 YOLO,模型继续内联工作/规划而不切换到 Plan 模式。确认 Shift+Tab //plan仍可手动进入 Plan 模式。证据(前后对比)
N/A(非用户可见的控制流变更;通过单元测试验证)。修复前:
expected "spy" to not be called at all, but actually been called 1 times;修复后:15/15 通过;核心测试套件 14984 通过、0 失败。测试平台
macOS ✅;Windows⚠️ ;Linux ⚠️ 。
风险与范围
关联 Issue
Fixes #5970