From 135699dd5ae1dbd3d4c869ec203446b77e32fb82 Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 06:31:04 +0800 Subject: [PATCH 01/13] feat(cli): implement /plan command for plan mode - Add /plan command to switch to plan mode or execute the current plan - Update BuiltinCommandLoader to include planCommand - Allow planCommand in non-interactive environments - Add corresponding unit tests - Update i18n locales to include new translations - Document /plan command in features/commands.md --- docs/users/features/commands.md | 1 + package-lock.json | 22 ++- packages/cli/src/i18n/locales/de.js | 11 ++ packages/cli/src/i18n/locales/en.js | 13 ++ packages/cli/src/i18n/locales/ja.js | 13 ++ packages/cli/src/i18n/locales/pt.js | 13 ++ packages/cli/src/i18n/locales/ru.js | 11 ++ packages/cli/src/i18n/locales/zh.js | 14 ++ packages/cli/src/nonInteractiveCliCommands.ts | 1 + .../cli/src/services/BuiltinCommandLoader.ts | 2 + .../cli/src/ui/commands/planCommand.test.ts | 118 ++++++++++++++ packages/cli/src/ui/commands/planCommand.ts | 99 ++++++++++++ scripts/unused-keys-only-in-locales.json | 145 ++++++++++++++++-- 13 files changed, 448 insertions(+), 15 deletions(-) create mode 100644 packages/cli/src/ui/commands/planCommand.test.ts create mode 100644 packages/cli/src/ui/commands/planCommand.ts diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index d99e3a6473..e97fd5e51a 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -61,6 +61,7 @@ Commands for managing AI tools and models. | `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` | | `/tools` | Display currently available tool list | `/tools`, `/tools desc` | | `/skills` | List and run available skills | `/skills`, `/skills ` | +| `/plan` | Switch to plan mode or execute current plan | `/plan`, `/plan execute` | | `/approval-mode` | Change approval mode for tool usage | `/approval-mode --project` | | →`plan` | Analysis only, no execution | Secure review | | →`default` | Require approval for edits | Daily use | diff --git a/package-lock.json b/package-lock.json index 42e4a92977..b4d3617627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1542,10 +1542,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@google/gemini-cli-test-utils": { - "resolved": "packages/test-utils", - "link": true - }, "node_modules/@grammyjs/types": { "version": "3.25.0", "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.25.0.tgz", @@ -19046,6 +19042,16 @@ "@teddyzhu/clipboard-win32-x64-msvc": "0.0.5" } }, + "packages/cli/node_modules/@google/gemini-cli-test-utils": { + "name": "@qwen-code/qwen-code-test-utils", + "version": "0.14.1", + "resolved": "file:packages/test-utils", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=20" + } + }, "packages/cli/node_modules/@google/genai": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", @@ -23059,7 +23065,6 @@ "packages/test-utils": { "name": "@qwen-code/qwen-code-test-utils", "version": "0.14.1", - "dev": true, "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" @@ -23875,9 +23880,14 @@ "vite-plugin-dts": "^4.5.4" }, "peerDependencies": { - "@qwen-code/qwen-code-core": ">=0.13.0", + "@qwen-code/qwen-code-core": ">=0.13.1", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@qwen-code/qwen-code-core": { + "optional": true + } } }, "packages/webui/node_modules/@esbuild/aix-ppc64": { diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 92ee2a01e9..30788ca56e 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1968,4 +1968,15 @@ export default { 'Raw-Modus nicht verfügbar. Bitte in einem interaktiven Terminal ausführen.', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(↑ ↓ Pfeiltasten zum Navigieren, Enter zum Auswählen, Strg+C zum Beenden)\n', + + 'Switch to plan mode or execute the current plan': + 'Switch to plan mode or execute the current plan', + 'Exited plan mode. The agent will now execute the plan.': + 'Exited plan mode. The agent will now execute the plan.', + 'Enabled plan mode. The agent will analyze and plan without executing tools.': + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + 'Already in plan mode. Use "/plan execute" to execute the plan.': + 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index fdc572fcbe..39b30bf717 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -2008,4 +2008,17 @@ export default { 'Raw mode not available. Please run in an interactive terminal.', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n', + + 'Switch to plan mode or execute the current plan': + 'Switch to plan mode or execute the current plan', + 'Exited plan mode. The agent will now execute the plan.': + 'Exited plan mode. The agent will now execute the plan.', + 'Enabled plan mode. The agent will analyze and plan without executing tools.': + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + 'Already in plan mode. Use "/plan execute" to execute the plan.': + 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'The name of the extension to update.': + 'The name of the extension to update.', + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', }; diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index 838214f6e1..e7568e378e 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -1460,4 +1460,17 @@ export default { 'Rawモードが利用できません。インタラクティブターミナルで実行してください。', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(↑ ↓ 矢印キーで移動、Enter で選択、Ctrl+C で終了)\n', + + 'Switch to plan mode or execute the current plan': + 'Switch to plan mode or execute the current plan', + 'Exited plan mode. The agent will now execute the plan.': + 'Exited plan mode. The agent will now execute the plan.', + 'Enabled plan mode. The agent will analyze and plan without executing tools.': + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + 'Already in plan mode. Use "/plan execute" to execute the plan.': + 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'The name of the extension to update.': + 'The name of the extension to update.', + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', }; diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 8fd6a79d17..eaf074f5ef 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1958,4 +1958,17 @@ export default { 'Modo raw não disponível. Execute em um terminal interativo.', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(Use ↑ ↓ para navegar, Enter para selecionar, Ctrl+C para sair)\n', + + 'Switch to plan mode or execute the current plan': + 'Switch to plan mode or execute the current plan', + 'Exited plan mode. The agent will now execute the plan.': + 'Exited plan mode. The agent will now execute the plan.', + 'Enabled plan mode. The agent will analyze and plan without executing tools.': + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + 'Already in plan mode. Use "/plan execute" to execute the plan.': + 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'The name of the extension to update.': + 'The name of the extension to update.', + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 6b910ff48b..fc0b49889a 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1965,4 +1965,15 @@ export default { 'Raw-режим недоступен. Пожалуйста, запустите в интерактивном терминале.', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(↑ ↓ стрелки для навигации, Enter для выбора, Ctrl+C для выхода)\n', + + 'Switch to plan mode or execute the current plan': + 'Switch to plan mode or execute the current plan', + 'Exited plan mode. The agent will now execute the plan.': + 'Exited plan mode. The agent will now execute the plan.', + 'Enabled plan mode. The agent will analyze and plan without executing tools.': + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + 'Already in plan mode. Use "/plan execute" to execute the plan.': + 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': + 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 357a4ccd32..e300b2f7c7 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1813,4 +1813,18 @@ export default { '原始模式不可用。请在交互式终端中运行。', '(Use ↑ ↓ arrows to navigate, Enter to select, Ctrl+C to exit)\n': '(使用 ↑ ↓ 箭头导航,Enter 选择,Ctrl+C 退出)\n', + + 'Switch to plan mode or execute the current plan': + '切换到计划模式或执行当前计划', + 'Exited plan mode. The agent will now execute the plan.': + '退出计划模式。智能体现在将执行计划。', + 'Enabled plan mode. The agent will analyze and plan without executing tools.': + '启用计划模式。智能体将只分析和规划,而不执行工具。', + 'Already in plan mode. Use "/plan execute" to execute the plan.': + '已处于计划模式。使用 "/plan execute" 执行计划。', + 'Value:': '值:', + 'No server selected': '未选择服务器', + prompts: '提示词', + required: '必填', + Enum: '枚举', }; diff --git a/packages/cli/src/nonInteractiveCliCommands.ts b/packages/cli/src/nonInteractiveCliCommands.ts index e6344f5d0b..0f57796b9a 100644 --- a/packages/cli/src/nonInteractiveCliCommands.ts +++ b/packages/cli/src/nonInteractiveCliCommands.ts @@ -44,6 +44,7 @@ export const ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE = [ 'compress', 'btw', 'bug', + 'plan', ] as const; /** diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 5db6c965a0..20e9b13c62 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -32,6 +32,7 @@ import { languageCommand } from '../ui/commands/languageCommand.js'; import { mcpCommand } from '../ui/commands/mcpCommand.js'; import { memoryCommand } from '../ui/commands/memoryCommand.js'; import { modelCommand } from '../ui/commands/modelCommand.js'; +import { planCommand } from '../ui/commands/planCommand.js'; import { permissionsCommand } from '../ui/commands/permissionsCommand.js'; import { trustCommand } from '../ui/commands/trustCommand.js'; import { quitCommand } from '../ui/commands/quitCommand.js'; @@ -103,6 +104,7 @@ export class BuiltinCommandLoader implements ICommandLoader { mcpCommand, memoryCommand, modelCommand, + planCommand, permissionsCommand, ...(this.config?.getFolderTrust() ? [trustCommand] : []), quitCommand, diff --git a/packages/cli/src/ui/commands/planCommand.test.ts b/packages/cli/src/ui/commands/planCommand.test.ts new file mode 100644 index 0000000000..4e45528efe --- /dev/null +++ b/packages/cli/src/ui/commands/planCommand.test.ts @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'; +import { planCommand } from './planCommand.js'; +import { type CommandContext } from './types.js'; +import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; +import { ApprovalMode } from '@qwen-code/qwen-code-core'; + +describe('planCommand', () => { + let mockContext: CommandContext; + + beforeEach(() => { + mockContext = createMockCommandContext({ + services: { + config: { + getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), + setApprovalMode: vi.fn(), + } as unknown as import('@qwen-code/qwen-code-core').Config, + }, + }); + }); + + it('should switch to plan mode if not in plan mode', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + const result = await planCommand.action(mockContext, ''); + + expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.PLAN, + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + }); + }); + + it('should return submit prompt if arguments are provided when switching to plan mode', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + const result = await planCommand.action(mockContext, 'refactor the code'); + + expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.PLAN, + ); + expect(result).toEqual({ + type: 'submit_prompt', + content: [{ text: 'refactor the code' }], + }); + }); + + it('should return already in plan mode if mode is already plan', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + (mockContext.services.config?.getApprovalMode as Mock).mockReturnValue( + ApprovalMode.PLAN, + ); + + const result = await planCommand.action(mockContext, ''); + + expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: 'Already in plan mode. Use "/plan execute" to execute the plan.', + }); + }); + + it('should return submit prompt if arguments are provided and already in plan mode', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + (mockContext.services.config?.getApprovalMode as Mock).mockReturnValue( + ApprovalMode.PLAN, + ); + + const result = await planCommand.action(mockContext, 'keep planning'); + + expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled(); + expect(result).toEqual({ + type: 'submit_prompt', + content: [{ text: 'keep planning' }], + }); + }); + + it('should exit plan mode when execute argument is passed', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + (mockContext.services.config?.getApprovalMode as Mock).mockReturnValue( + ApprovalMode.PLAN, + ); + + const result = await planCommand.action(mockContext, 'execute'); + + expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.DEFAULT, + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: 'Exited plan mode. The agent will now execute the plan.', + }); + }); +}); diff --git a/packages/cli/src/ui/commands/planCommand.ts b/packages/cli/src/ui/commands/planCommand.ts new file mode 100644 index 0000000000..1445d9097e --- /dev/null +++ b/packages/cli/src/ui/commands/planCommand.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2026 Qwen Team + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + type CommandContext, + CommandKind, + type SlashCommand, + type MessageActionReturn, + type SubmitPromptActionReturn, +} from './types.js'; +import { t } from '../../i18n/index.js'; +import { ApprovalMode } from '@qwen-code/qwen-code-core'; + +export const planCommand: SlashCommand = { + name: 'plan', + get description() { + return t('Switch to plan mode or execute the current plan'); + }, + kind: CommandKind.BUILT_IN, + action: async ( + context: CommandContext, + args: string, + ): Promise => { + const { config } = context.services; + if (!config) { + return { + type: 'message', + messageType: 'error', + content: t('Configuration is not available.'), + }; + } + + const trimmedArgs = args.trim(); + + if (trimmedArgs === 'execute') { + try { + config.setApprovalMode(ApprovalMode.DEFAULT); + } catch (e) { + return { + type: 'message', + messageType: 'error', + content: (e as Error).message, + }; + } + return { + type: 'message', + messageType: 'info', + content: t('Exited plan mode. The agent will now execute the plan.'), + }; + } + + const currentMode = config.getApprovalMode(); + if (currentMode !== ApprovalMode.PLAN) { + try { + config.setApprovalMode(ApprovalMode.PLAN); + } catch (e) { + return { + type: 'message', + messageType: 'error', + content: (e as Error).message, + }; + } + + if (trimmedArgs) { + return { + type: 'submit_prompt', + content: [{ text: trimmedArgs }], + }; + } + + return { + type: 'message', + messageType: 'info', + content: t( + 'Enabled plan mode. The agent will analyze and plan without executing tools.', + ), + }; + } + + // Already in plan mode + if (trimmedArgs) { + return { + type: 'submit_prompt', + content: [{ text: trimmedArgs }], + }; + } + + return { + type: 'message', + messageType: 'info', + content: t( + 'Already in plan mode. Use "/plan execute" to execute the plan.', + ), + }; + }, +}; diff --git a/scripts/unused-keys-only-in-locales.json b/scripts/unused-keys-only-in-locales.json index 45097f8da6..cd9e96c00d 100644 --- a/scripts/unused-keys-only-in-locales.json +++ b/scripts/unused-keys-only-in-locales.json @@ -1,62 +1,189 @@ { - "generatedAt": "2026-01-07T14:56:23.662Z", + "generatedAt": "2026-04-05T22:26:23.764Z", "keys": [ - " - en-US: English", - " - zh-CN: Simplified Chinese", + "(Press Enter to submit, Escape to cancel)", + "(Press Esc to close)", + "(Press Escape to go back)", + "(disabled)", + "(esc to cancel, {{time}})", + "(set)", "A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?", + "API Key:", + "API key is stored in settings.env. You can migrate it to a .env file for better security.", + "API-KEY", + "About Qwen Code", + "Accept suggestion / Autocomplete", + "Add file context", + "Any other key", "Apply to current session only (temporary)", "Approval mode changed to: {{mode}}", "Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})", + "Auth Method", + "Authenticate with an OAuth-enabled MCP server", + "Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).", "Auto-edit mode - Automatically approve file edits", "Available approval modes:", + "CLI Version", + "Cannot ask user questions in non-interactive mode. Please run in interactive mode to use this tool.", "Change auth (executes the /auth command)", "Chat history is already compressed.", + "Checking...", + "Coding Plan API key not found. Please re-authenticate with Coding Plan.", + "Coding Plan configuration updated successfully. New models are now available.", + "Configured Hooks ({{count}} total)", "Continue with {{model}}", "Conversation checkpoint '{{tag}}' has been deleted.", "Conversation checkpoint saved with tag: {{tag}}.", "Conversation shared to {{filePath}}", + "Current (effective) configuration", "Current approval mode: {{mode}}", "Default mode - Require approval for file edits or shell commands", "Delete a conversation checkpoint. Usage: /chat delete ", + "Destructive", + "Disable Auto Update", + "Disable Cache Control", + "Disable Fuzzy Search", + "Disable Loading Phrases", + "Disable Server", + "Disable an active hook", + "Disable an extension", "Enable Prompt Completion", + "Enable Tool Output Truncation", + "Enable a disabled hook", + "Enable an extension", "Error sharing conversation: {{error}}", "Error: No checkpoint found with tag '{{tag}}'.", + "Example: /language output Português", + "Extension \"{{name}}\" disabled for scope \"{{scope}}\"", + "Extension \"{{name}}\" disabled successfully.", + "Extension \"{{name}}\" enabled for scope \"{{scope}}\"", + "Extension \"{{name}}\" enabled successfully.", + "Extension \"{{name}}\" uninstalled successfully.", + "Failed to authenticate with MCP server '{{name}}': {{error}}", "Failed to change approval mode: {{error}}", "Failed to login. Message: {{message}}", + "Failed to process user answers:", "Failed to save approval mode: {{error}}", "Failed to switch model to '{{modelId}}'.\n\n{{error}}", + "Failed to uninstall extension \"{{name}}\": {{error}}", + "Failed to update extension \"{{name}}\": {{error}}", + "Failed to validate credentials", + "Get started", + "Git Commit", + "Global memory is currently empty.", + "IDE Mode", + "If the browser does not open, copy and paste this URL into your browser:", + "Installs an extension from a git repository URL, local path, or claude marketplace (marketplace-url:plugin-name).", + "Invalid credentials: {{errorMessage}}", "Invalid file format. Only .md and .json are supported.", - "Invalid language. Available: en-US, zh-CN", + "LLM output language not set", + "LLM output language rule file generated at {{path}}", + "List active extensions", + "List available Qwen Code tools. Usage: /tools [desc]", + "List configured MCP servers and tools", + "List configured MCP servers and tools, or authenticate with OAuth-enabled servers", "List of saved conversations:", "List saved conversation checkpoints", + "Login with QwenChat account to use daily free quota.", + "MCP Management", + "MCP server '{{name}}' not found.", + "MCP servers with OAuth authentication:", + "Make sure to copy the COMPLETE URL - it may wrap across multiple lines.", "Manage conversation history.", + "Memory Discovery Max Dirs", "Missing tag. Usage: /chat delete ", "Missing tag. Usage: /chat resume ", "Missing tag. Usage: /chat save ", + "More instructions about configuring `modelProviders` manually.", + "NPM Version", + "New model configurations are available for Alibaba Cloud Coding Plan. Update now?", + "No MCP servers configured with OAuth authentication.", "No chat client available to save conversation.", "No chat client available to share conversation.", "No conversation found to save.", "No conversation found to share.", + "No extensions found.", "No saved checkpoint found with tag: {{tag}}.", "No saved conversation checkpoints found.", + "Node.js Version", + "Not Sure Yet", "Note: Newest last, oldest first", + "Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.", + "Note: Your existing API key will not be cleared when using Qwen OAuth.", + "OS Arch", + "OS Platform", + "OS Release", + "Open MCP management dialog, or authenticate with OAuth-enabled servers", + "Open World", + "Open command menu", "OpenAI API key is required to use OpenAI authentication.", + "OpenAI Configuration Required", + "Or scan the QR code below:", + "Paste your api key of ModelStudio Coding Plan and you're all set!", "Persist for this project/workspace", "Persist for this user on this machine", "Plan mode - Analyze only, do not modify files or execute commands", + "Please answer the following question(s):", + "Please enter your OpenAI configuration. You can get an API key from", + "Press ? again to close", + "Press Enter or Esc to go back", + "Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel", + "Press Enter to start authentication, Esc to go back", + "Press Y/Enter to confirm, N/Esc to cancel", "Pro quota limit reached for {{model}}.", + "Project memory is currently empty.", + "Project settings (local)", + "Qwen 3.6 Plus — efficient hybrid model with leading coding performance", "Qwen OAuth authentication cancelled.", "Qwen OAuth authentication timed out. Please try again.", + "Rate limit error: {{reason}}", + "Read Only", + "Restarting MCP servers...", + "Restarts MCP servers.", "Resume a conversation from a checkpoint. Usage: /chat resume ", + "Reverse search history", "Save the current conversation as a checkpoint. Usage: /chat save ", + "Saved in .qwen/settings.local.json", "Scope subcommands do not accept additional arguments.", - "Set UI language to English (en-US)", - "Set UI language to Simplified Chinese (zh-CN)", + "Select API-KEY configuration mode:", + "Select the scope for this action:", "Settings service is not available; unable to persist the approval mode.", "Share the current conversation to a markdown or json file. Usage: /chat share ", + "The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)", + "This extension will exclude the following core tools: {{tools}}", + "Toggle shell mode", + "Toggle this help display", + "Tools for {{name}}", + "Uninstall an extension", + "Uninstalling extension \"{{name}}\"...", + "Unsupported scope \"{{scope}}\", should be one of \"user\" or \"workspace\"", + "Up to date", + "Update available", + "Update extensions. Usage: update |--all", "Usage: /approval-mode [--session|--user|--project]", - "Usage: /language ui [zh-CN|en-US]", - "YOLO mode - Automatically approve all tools" + "Usage: /extensions uninstall ", + "Usage: /extensions update |--all", + "Usage: /extensions {{command}} [--scope=]", + "Use /mcp auth to authenticate.", + "Use /trust to manage folder trust settings for this workspace.", + "Use coding plan credentials or your own api-keys/providers.", + "User - Applies to all projects", + "User Scope", + "User declined to answer the questions.", + "User has provided the following answers:", + "View Extension", + "Vision Model Preview", + "Workspace - Applies to current project only", + "Workspace Scope", + "Y/Enter to confirm, N/Esc to cancel", + "YOLO mode - Automatically approve all tools", + "Yes, allow always ...", + "Yes, allow always for this session", + "Yes, always allow all tools from server \"{{server}}\"", + "Yes, always allow tool \"{{tool}}\" from server \"{{server}}\"", + "change the auth method", + "↑/↓: Navigate | Space/Enter: Toggle | Esc: Cancel", + "↑/↓: Navigate | ←/→: Switch tabs | Space/Enter: Toggle | Esc: Cancel" ], - "count": 56 + "count": 183 } From 007fff67ba042636f0c5af76f31c9a262b8d5a5f Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 06:56:41 +0800 Subject: [PATCH 02/13] fix(cli): fix /plan command bugs and clean up hallucinated i18n keys - Fix /plan execute running without checking if currently in plan mode - Remove hallucinated i18n keys (e.g. "Coding Plan API keys") added by prior AI-generated code - Remove /plan from non-interactive command allowlist (plan mode is interactive) - Revert unrelated package-lock.json and unused-keys-only-in-locales.json changes - Add test case for /plan execute when not in plan mode Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 22 +-- packages/cli/src/i18n/locales/de.js | 4 +- packages/cli/src/i18n/locales/en.js | 6 +- packages/cli/src/i18n/locales/ja.js | 6 +- packages/cli/src/i18n/locales/pt.js | 6 +- packages/cli/src/i18n/locales/ru.js | 4 +- packages/cli/src/i18n/locales/zh.js | 7 +- packages/cli/src/nonInteractiveCliCommands.ts | 1 - .../cli/src/ui/commands/planCommand.test.ts | 16 ++ packages/cli/src/ui/commands/planCommand.ts | 9 +- scripts/unused-keys-only-in-locales.json | 145 ++---------------- 11 files changed, 51 insertions(+), 175 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4d3617627..42e4a92977 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1542,6 +1542,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/gemini-cli-test-utils": { + "resolved": "packages/test-utils", + "link": true + }, "node_modules/@grammyjs/types": { "version": "3.25.0", "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.25.0.tgz", @@ -19042,16 +19046,6 @@ "@teddyzhu/clipboard-win32-x64-msvc": "0.0.5" } }, - "packages/cli/node_modules/@google/gemini-cli-test-utils": { - "name": "@qwen-code/qwen-code-test-utils", - "version": "0.14.1", - "resolved": "file:packages/test-utils", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=20" - } - }, "packages/cli/node_modules/@google/genai": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", @@ -23065,6 +23059,7 @@ "packages/test-utils": { "name": "@qwen-code/qwen-code-test-utils", "version": "0.14.1", + "dev": true, "license": "Apache-2.0", "devDependencies": { "typescript": "^5.3.3" @@ -23880,14 +23875,9 @@ "vite-plugin-dts": "^4.5.4" }, "peerDependencies": { - "@qwen-code/qwen-code-core": ">=0.13.1", + "@qwen-code/qwen-code-core": ">=0.13.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@qwen-code/qwen-code-core": { - "optional": true - } } }, "packages/webui/node_modules/@esbuild/aix-ppc64": { diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 30788ca56e..b429e57ee3 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1977,6 +1977,6 @@ export default { 'Enabled plan mode. The agent will analyze and plan without executing tools.', 'Already in plan mode. Use "/plan execute" to execute the plan.': 'Already in plan mode. Use "/plan execute" to execute the plan.', - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', + 'Not in plan mode. Use "/plan" to enter plan mode first.': + 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 39b30bf717..fea60fac79 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -2017,8 +2017,6 @@ export default { 'Enabled plan mode. The agent will analyze and plan without executing tools.', 'Already in plan mode. Use "/plan execute" to execute the plan.': 'Already in plan mode. Use "/plan execute" to execute the plan.', - 'The name of the extension to update.': - 'The name of the extension to update.', - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', + 'Not in plan mode. Use "/plan" to enter plan mode first.': + 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index e7568e378e..0f52f6def4 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -1469,8 +1469,6 @@ export default { 'Enabled plan mode. The agent will analyze and plan without executing tools.', 'Already in plan mode. Use "/plan execute" to execute the plan.': 'Already in plan mode. Use "/plan execute" to execute the plan.', - 'The name of the extension to update.': - 'The name of the extension to update.', - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', + 'Not in plan mode. Use "/plan" to enter plan mode first.': + 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index eaf074f5ef..616cad2da3 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1967,8 +1967,6 @@ export default { 'Enabled plan mode. The agent will analyze and plan without executing tools.', 'Already in plan mode. Use "/plan execute" to execute the plan.': 'Already in plan mode. Use "/plan execute" to execute the plan.', - 'The name of the extension to update.': - 'The name of the extension to update.', - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', + 'Not in plan mode. Use "/plan" to enter plan mode first.': + 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index fc0b49889a..4c0c93dab3 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1974,6 +1974,6 @@ export default { 'Enabled plan mode. The agent will analyze and plan without executing tools.', 'Already in plan mode. Use "/plan execute" to execute the plan.': 'Already in plan mode. Use "/plan execute" to execute the plan.', - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.': - 'Invalid API key. Coding Plan API keys start with "sk-sp-". Please check.', + 'Not in plan mode. Use "/plan" to enter plan mode first.': + 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index e300b2f7c7..afd8da2820 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1822,9 +1822,6 @@ export default { '启用计划模式。智能体将只分析和规划,而不执行工具。', 'Already in plan mode. Use "/plan execute" to execute the plan.': '已处于计划模式。使用 "/plan execute" 执行计划。', - 'Value:': '值:', - 'No server selected': '未选择服务器', - prompts: '提示词', - required: '必填', - Enum: '枚举', + 'Not in plan mode. Use "/plan" to enter plan mode first.': + '未处于计划模式。请先使用 "/plan" 进入计划模式。', }; diff --git a/packages/cli/src/nonInteractiveCliCommands.ts b/packages/cli/src/nonInteractiveCliCommands.ts index 0f57796b9a..e6344f5d0b 100644 --- a/packages/cli/src/nonInteractiveCliCommands.ts +++ b/packages/cli/src/nonInteractiveCliCommands.ts @@ -44,7 +44,6 @@ export const ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE = [ 'compress', 'btw', 'bug', - 'plan', ] as const; /** diff --git a/packages/cli/src/ui/commands/planCommand.test.ts b/packages/cli/src/ui/commands/planCommand.test.ts index 4e45528efe..cd49ece5c6 100644 --- a/packages/cli/src/ui/commands/planCommand.test.ts +++ b/packages/cli/src/ui/commands/planCommand.test.ts @@ -115,4 +115,20 @@ describe('planCommand', () => { content: 'Exited plan mode. The agent will now execute the plan.', }); }); + + it('should return error when execute is used but not in plan mode', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + // Default mock returns ApprovalMode.DEFAULT (not PLAN) + const result = await planCommand.action(mockContext, 'execute'); + + expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled(); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Not in plan mode. Use "/plan" to enter plan mode first.', + }); + }); }); diff --git a/packages/cli/src/ui/commands/planCommand.ts b/packages/cli/src/ui/commands/planCommand.ts index 1445d9097e..dff3df5525 100644 --- a/packages/cli/src/ui/commands/planCommand.ts +++ b/packages/cli/src/ui/commands/planCommand.ts @@ -34,8 +34,16 @@ export const planCommand: SlashCommand = { } const trimmedArgs = args.trim(); + const currentMode = config.getApprovalMode(); if (trimmedArgs === 'execute') { + if (currentMode !== ApprovalMode.PLAN) { + return { + type: 'message', + messageType: 'error', + content: t('Not in plan mode. Use "/plan" to enter plan mode first.'), + }; + } try { config.setApprovalMode(ApprovalMode.DEFAULT); } catch (e) { @@ -52,7 +60,6 @@ export const planCommand: SlashCommand = { }; } - const currentMode = config.getApprovalMode(); if (currentMode !== ApprovalMode.PLAN) { try { config.setApprovalMode(ApprovalMode.PLAN); diff --git a/scripts/unused-keys-only-in-locales.json b/scripts/unused-keys-only-in-locales.json index cd9e96c00d..45097f8da6 100644 --- a/scripts/unused-keys-only-in-locales.json +++ b/scripts/unused-keys-only-in-locales.json @@ -1,189 +1,62 @@ { - "generatedAt": "2026-04-05T22:26:23.764Z", + "generatedAt": "2026-01-07T14:56:23.662Z", "keys": [ - "(Press Enter to submit, Escape to cancel)", - "(Press Esc to close)", - "(Press Escape to go back)", - "(disabled)", - "(esc to cancel, {{time}})", - "(set)", + " - en-US: English", + " - zh-CN: Simplified Chinese", "A checkpoint with the tag {{tag}} already exists. Do you want to overwrite it?", - "API Key:", - "API key is stored in settings.env. You can migrate it to a .env file for better security.", - "API-KEY", - "About Qwen Code", - "Accept suggestion / Autocomplete", - "Add file context", - "Any other key", "Apply to current session only (temporary)", "Approval mode changed to: {{mode}}", "Approval mode changed to: {{mode}} (saved to {{scope}} settings{{location}})", - "Auth Method", - "Authenticate with an OAuth-enabled MCP server", - "Authenticated successfully with {{region}}. API key and model configs saved to settings.json (backed up).", "Auto-edit mode - Automatically approve file edits", "Available approval modes:", - "CLI Version", - "Cannot ask user questions in non-interactive mode. Please run in interactive mode to use this tool.", "Change auth (executes the /auth command)", "Chat history is already compressed.", - "Checking...", - "Coding Plan API key not found. Please re-authenticate with Coding Plan.", - "Coding Plan configuration updated successfully. New models are now available.", - "Configured Hooks ({{count}} total)", "Continue with {{model}}", "Conversation checkpoint '{{tag}}' has been deleted.", "Conversation checkpoint saved with tag: {{tag}}.", "Conversation shared to {{filePath}}", - "Current (effective) configuration", "Current approval mode: {{mode}}", "Default mode - Require approval for file edits or shell commands", "Delete a conversation checkpoint. Usage: /chat delete ", - "Destructive", - "Disable Auto Update", - "Disable Cache Control", - "Disable Fuzzy Search", - "Disable Loading Phrases", - "Disable Server", - "Disable an active hook", - "Disable an extension", "Enable Prompt Completion", - "Enable Tool Output Truncation", - "Enable a disabled hook", - "Enable an extension", "Error sharing conversation: {{error}}", "Error: No checkpoint found with tag '{{tag}}'.", - "Example: /language output Português", - "Extension \"{{name}}\" disabled for scope \"{{scope}}\"", - "Extension \"{{name}}\" disabled successfully.", - "Extension \"{{name}}\" enabled for scope \"{{scope}}\"", - "Extension \"{{name}}\" enabled successfully.", - "Extension \"{{name}}\" uninstalled successfully.", - "Failed to authenticate with MCP server '{{name}}': {{error}}", "Failed to change approval mode: {{error}}", "Failed to login. Message: {{message}}", - "Failed to process user answers:", "Failed to save approval mode: {{error}}", "Failed to switch model to '{{modelId}}'.\n\n{{error}}", - "Failed to uninstall extension \"{{name}}\": {{error}}", - "Failed to update extension \"{{name}}\": {{error}}", - "Failed to validate credentials", - "Get started", - "Git Commit", - "Global memory is currently empty.", - "IDE Mode", - "If the browser does not open, copy and paste this URL into your browser:", - "Installs an extension from a git repository URL, local path, or claude marketplace (marketplace-url:plugin-name).", - "Invalid credentials: {{errorMessage}}", "Invalid file format. Only .md and .json are supported.", - "LLM output language not set", - "LLM output language rule file generated at {{path}}", - "List active extensions", - "List available Qwen Code tools. Usage: /tools [desc]", - "List configured MCP servers and tools", - "List configured MCP servers and tools, or authenticate with OAuth-enabled servers", + "Invalid language. Available: en-US, zh-CN", "List of saved conversations:", "List saved conversation checkpoints", - "Login with QwenChat account to use daily free quota.", - "MCP Management", - "MCP server '{{name}}' not found.", - "MCP servers with OAuth authentication:", - "Make sure to copy the COMPLETE URL - it may wrap across multiple lines.", "Manage conversation history.", - "Memory Discovery Max Dirs", "Missing tag. Usage: /chat delete ", "Missing tag. Usage: /chat resume ", "Missing tag. Usage: /chat save ", - "More instructions about configuring `modelProviders` manually.", - "NPM Version", - "New model configurations are available for Alibaba Cloud Coding Plan. Update now?", - "No MCP servers configured with OAuth authentication.", "No chat client available to save conversation.", "No chat client available to share conversation.", "No conversation found to save.", "No conversation found to share.", - "No extensions found.", "No saved checkpoint found with tag: {{tag}}.", "No saved conversation checkpoints found.", - "Node.js Version", - "Not Sure Yet", "Note: Newest last, oldest first", - "Note: Your existing API key in settings.json will not be cleared when using Qwen OAuth. You can switch back to OpenAI authentication later if needed.", - "Note: Your existing API key will not be cleared when using Qwen OAuth.", - "OS Arch", - "OS Platform", - "OS Release", - "Open MCP management dialog, or authenticate with OAuth-enabled servers", - "Open World", - "Open command menu", "OpenAI API key is required to use OpenAI authentication.", - "OpenAI Configuration Required", - "Or scan the QR code below:", - "Paste your api key of ModelStudio Coding Plan and you're all set!", "Persist for this project/workspace", "Persist for this user on this machine", "Plan mode - Analyze only, do not modify files or execute commands", - "Please answer the following question(s):", - "Please enter your OpenAI configuration. You can get an API key from", - "Press ? again to close", - "Press Enter or Esc to go back", - "Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel", - "Press Enter to start authentication, Esc to go back", - "Press Y/Enter to confirm, N/Esc to cancel", "Pro quota limit reached for {{model}}.", - "Project memory is currently empty.", - "Project settings (local)", - "Qwen 3.6 Plus — efficient hybrid model with leading coding performance", "Qwen OAuth authentication cancelled.", "Qwen OAuth authentication timed out. Please try again.", - "Rate limit error: {{reason}}", - "Read Only", - "Restarting MCP servers...", - "Restarts MCP servers.", "Resume a conversation from a checkpoint. Usage: /chat resume ", - "Reverse search history", "Save the current conversation as a checkpoint. Usage: /chat save ", - "Saved in .qwen/settings.local.json", "Scope subcommands do not accept additional arguments.", - "Select API-KEY configuration mode:", - "Select the scope for this action:", + "Set UI language to English (en-US)", + "Set UI language to Simplified Chinese (zh-CN)", "Settings service is not available; unable to persist the approval mode.", "Share the current conversation to a markdown or json file. Usage: /chat share ", - "The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)", - "This extension will exclude the following core tools: {{tools}}", - "Toggle shell mode", - "Toggle this help display", - "Tools for {{name}}", - "Uninstall an extension", - "Uninstalling extension \"{{name}}\"...", - "Unsupported scope \"{{scope}}\", should be one of \"user\" or \"workspace\"", - "Up to date", - "Update available", - "Update extensions. Usage: update |--all", "Usage: /approval-mode [--session|--user|--project]", - "Usage: /extensions uninstall ", - "Usage: /extensions update |--all", - "Usage: /extensions {{command}} [--scope=]", - "Use /mcp auth to authenticate.", - "Use /trust to manage folder trust settings for this workspace.", - "Use coding plan credentials or your own api-keys/providers.", - "User - Applies to all projects", - "User Scope", - "User declined to answer the questions.", - "User has provided the following answers:", - "View Extension", - "Vision Model Preview", - "Workspace - Applies to current project only", - "Workspace Scope", - "Y/Enter to confirm, N/Esc to cancel", - "YOLO mode - Automatically approve all tools", - "Yes, allow always ...", - "Yes, allow always for this session", - "Yes, always allow all tools from server \"{{server}}\"", - "Yes, always allow tool \"{{tool}}\" from server \"{{server}}\"", - "change the auth method", - "↑/↓: Navigate | Space/Enter: Toggle | Esc: Cancel", - "↑/↓: Navigate | ←/→: Switch tabs | Space/Enter: Toggle | Esc: Cancel" + "Usage: /language ui [zh-CN|en-US]", + "YOLO mode - Automatically approve all tools" ], - "count": 183 + "count": 56 } From 63c4bd0b0598342a5774c4b912d6429c5241f9a8 Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 07:06:31 +0800 Subject: [PATCH 03/13] fix(docs): align /plan description wording with i18n keys Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/users/features/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index e97fd5e51a..5f5b6c9b2a 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -61,7 +61,7 @@ Commands for managing AI tools and models. | `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` | | `/tools` | Display currently available tool list | `/tools`, `/tools desc` | | `/skills` | List and run available skills | `/skills`, `/skills ` | -| `/plan` | Switch to plan mode or execute current plan | `/plan`, `/plan execute` | +| `/plan` | Switch to plan mode or execute the current plan | `/plan`, `/plan execute` | | `/approval-mode` | Change approval mode for tool usage | `/approval-mode --project` | | →`plan` | Analysis only, no execution | Secure review | | →`default` | Require approval for edits | Daily use | From 09ccc1b896704331e2a2049f282599bba1807737 Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 07:11:25 +0800 Subject: [PATCH 04/13] fix(docs): add /plan usage example to commands table Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/users/features/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index 5f5b6c9b2a..8ade7eada0 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -61,7 +61,7 @@ Commands for managing AI tools and models. | `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` | | `/tools` | Display currently available tool list | `/tools`, `/tools desc` | | `/skills` | List and run available skills | `/skills`, `/skills ` | -| `/plan` | Switch to plan mode or execute the current plan | `/plan`, `/plan execute` | +| `/plan` | Switch to plan mode or execute the current plan | `/plan`, `/plan `, `/plan execute` | | `/approval-mode` | Change approval mode for tool usage | `/approval-mode --project` | | →`plan` | Analysis only, no execution | Secure review | | →`default` | Require approval for edits | Daily use | From c597847bc3cdd30f9a363cf8e70f85d402ca7980 Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 17:39:13 +0800 Subject: [PATCH 05/13] feat(core): add prePlanMode tracking to restore previous approval mode When entering plan mode, Config now saves the previous approval mode (e.g. AUTO_EDIT, YOLO) so it can be restored when exiting. Previously, /plan execute and ExitPlanModeTool both hardcoded a return to DEFAULT, losing the user's prior mode. Changes: - Config: add prePlanMode field, getPrePlanMode(), auto-track in setApprovalMode() - planCommand: /plan execute restores prePlanMode instead of DEFAULT - ExitPlanModeTool: ProceedOnce restores prePlanMode instead of DEFAULT - Tests: 4 new Config tests, 1 new planCommand test, 1 new ExitPlanModeTool test Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cli/src/ui/commands/planCommand.test.ts | 25 +++++++++++++ packages/cli/src/ui/commands/planCommand.ts | 2 +- packages/core/src/config/config.test.ts | 37 +++++++++++++++++++ packages/core/src/config/config.ts | 18 +++++++++ packages/core/src/tools/exitPlanMode.test.ts | 23 ++++++++++++ packages/core/src/tools/exitPlanMode.ts | 2 +- 6 files changed, 105 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/commands/planCommand.test.ts b/packages/cli/src/ui/commands/planCommand.test.ts index cd49ece5c6..d4a671c3dc 100644 --- a/packages/cli/src/ui/commands/planCommand.test.ts +++ b/packages/cli/src/ui/commands/planCommand.test.ts @@ -18,6 +18,7 @@ describe('planCommand', () => { services: { config: { getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), + getPrePlanMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), setApprovalMode: vi.fn(), } as unknown as import('@qwen-code/qwen-code-core').Config, }, @@ -116,6 +117,30 @@ describe('planCommand', () => { }); }); + it('should restore pre-plan mode when executing from plan mode', async () => { + if (!planCommand.action) { + throw new Error('The plan command must have an action.'); + } + + (mockContext.services.config?.getApprovalMode as Mock).mockReturnValue( + ApprovalMode.PLAN, + ); + (mockContext.services.config?.getPrePlanMode as Mock).mockReturnValue( + ApprovalMode.AUTO_EDIT, + ); + + const result = await planCommand.action(mockContext, 'execute'); + + expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.AUTO_EDIT, + ); + expect(result).toEqual({ + type: 'message', + messageType: 'info', + content: 'Exited plan mode. The agent will now execute the plan.', + }); + }); + it('should return error when execute is used but not in plan mode', async () => { if (!planCommand.action) { throw new Error('The plan command must have an action.'); diff --git a/packages/cli/src/ui/commands/planCommand.ts b/packages/cli/src/ui/commands/planCommand.ts index dff3df5525..415dfe0afc 100644 --- a/packages/cli/src/ui/commands/planCommand.ts +++ b/packages/cli/src/ui/commands/planCommand.ts @@ -45,7 +45,7 @@ export const planCommand: SlashCommand = { }; } try { - config.setApprovalMode(ApprovalMode.DEFAULT); + config.setApprovalMode(config.getPrePlanMode()); } catch (e) { return { type: 'message', diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 17d88b1749..1fa7bdbf1f 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1203,6 +1203,43 @@ describe('setApprovalMode with folder trust', () => { expect(() => config.setApprovalMode(ApprovalMode.PLAN)).not.toThrow(); }); + describe('prePlanMode tracking', () => { + it('should save pre-plan mode when entering plan mode', () => { + const config = new Config(baseParams); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); + + config.setApprovalMode(ApprovalMode.AUTO_EDIT); + config.setApprovalMode(ApprovalMode.PLAN); + expect(config.getPrePlanMode()).toBe(ApprovalMode.AUTO_EDIT); + }); + + it('should clear pre-plan mode when leaving plan mode', () => { + const config = new Config(baseParams); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); + + config.setApprovalMode(ApprovalMode.AUTO_EDIT); + config.setApprovalMode(ApprovalMode.PLAN); + config.setApprovalMode(ApprovalMode.DEFAULT); + expect(config.getPrePlanMode()).toBe(ApprovalMode.DEFAULT); + }); + + it('should default to DEFAULT when no pre-plan mode was recorded', () => { + const config = new Config(baseParams); + expect(config.getPrePlanMode()).toBe(ApprovalMode.DEFAULT); + }); + + it('should not update pre-plan mode when already in plan mode', () => { + const config = new Config(baseParams); + vi.spyOn(config, 'isTrustedFolder').mockReturnValue(true); + + config.setApprovalMode(ApprovalMode.YOLO); + config.setApprovalMode(ApprovalMode.PLAN); + // Setting PLAN again should not overwrite prePlanMode + config.setApprovalMode(ApprovalMode.PLAN); + expect(config.getPrePlanMode()).toBe(ApprovalMode.YOLO); + }); + }); + describe('registerCoreTools', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index c54b557052..27b33cd063 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -529,6 +529,7 @@ export class Config { private sdkMode: boolean; private geminiMdFileCount: number; private approvalMode: ApprovalMode; + private prePlanMode: ApprovalMode | undefined; private readonly accessibility: AccessibilitySettings; private readonly telemetrySettings: TelemetrySettings; private readonly gitCoAuthor: GitCoAuthorSettings; @@ -1634,6 +1635,14 @@ export class Config { return this.approvalMode; } + /** + * Returns the approval mode that was active before entering plan mode. + * Falls back to DEFAULT if no pre-plan mode was recorded. + */ + getPrePlanMode(): ApprovalMode { + return this.prePlanMode ?? ApprovalMode.DEFAULT; + } + setApprovalMode(mode: ApprovalMode): void { if ( !this.isTrustedFolder() && @@ -1644,6 +1653,15 @@ export class Config { 'Cannot enable privileged approval modes in an untrusted folder.', ); } + // Track the mode before entering plan mode so it can be restored later + if (mode === ApprovalMode.PLAN && this.approvalMode !== ApprovalMode.PLAN) { + this.prePlanMode = this.approvalMode; + } else if ( + mode !== ApprovalMode.PLAN && + this.approvalMode === ApprovalMode.PLAN + ) { + this.prePlanMode = undefined; + } this.approvalMode = mode; } diff --git a/packages/core/src/tools/exitPlanMode.test.ts b/packages/core/src/tools/exitPlanMode.test.ts index 81a1dbad63..721069429e 100644 --- a/packages/core/src/tools/exitPlanMode.test.ts +++ b/packages/core/src/tools/exitPlanMode.test.ts @@ -18,6 +18,7 @@ describe('ExitPlanModeTool', () => { approvalMode = ApprovalMode.PLAN; mockConfig = { getApprovalMode: vi.fn(() => approvalMode), + getPrePlanMode: vi.fn(() => ApprovalMode.DEFAULT), setApprovalMode: vi.fn((mode: ApprovalMode) => { approvalMode = mode; }), @@ -173,6 +174,28 @@ describe('ExitPlanModeTool', () => { expect(approvalMode).toBe(ApprovalMode.AUTO_EDIT); }); + it('should restore pre-plan mode on ProceedOnce', async () => { + // Simulate entering plan mode from AUTO_EDIT + (mockConfig.getPrePlanMode as ReturnType).mockReturnValue( + ApprovalMode.AUTO_EDIT, + ); + + const params: ExitPlanModeParams = { plan: 'Restore test' }; + const signal = new AbortController().signal; + + const invocation = tool.build(params); + const confirmation = await invocation.getConfirmationDetails(signal); + + if (confirmation) { + await confirmation.onConfirm(ToolConfirmationOutcome.ProceedOnce); + } + + expect(mockConfig.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.AUTO_EDIT, + ); + expect(approvalMode).toBe(ApprovalMode.AUTO_EDIT); + }); + it('should remain in plan mode when confirmation is rejected', async () => { const params: ExitPlanModeParams = { plan: 'Remain in planning', diff --git a/packages/core/src/tools/exitPlanMode.ts b/packages/core/src/tools/exitPlanMode.ts index ca71a97753..123760cf93 100644 --- a/packages/core/src/tools/exitPlanMode.ts +++ b/packages/core/src/tools/exitPlanMode.ts @@ -99,7 +99,7 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation< break; case ToolConfirmationOutcome.ProceedOnce: this.wasApproved = true; - this.setApprovalModeSafely(ApprovalMode.DEFAULT); + this.setApprovalModeSafely(this.config.getPrePlanMode()); break; case ToolConfirmationOutcome.Cancel: this.wasApproved = false; From b8e81c02b428b8fc8bf5ec11b56989b6faaeca5e Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 17:50:01 +0800 Subject: [PATCH 06/13] feat(core): add plan file persistence to disk Save approved plans to ~/.qwen/plans/{sessionId}.md so they can be referenced later in the session or reviewed outside the CLI. Changes: - Storage: add getPlansDir() and getPlanFilePath() - Config: add savePlan(), loadPlan(), getPlanFilePath() methods - ExitPlanModeTool: persist plan to disk when user approves - Tests: 4 new Config tests, 2 new ExitPlanModeTool assertions Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/config/config.test.ts | 51 ++++++++++++++++++++ packages/core/src/config/config.ts | 30 ++++++++++++ packages/core/src/config/storage.ts | 9 ++++ packages/core/src/tools/exitPlanMode.test.ts | 7 +++ packages/core/src/tools/exitPlanMode.ts | 9 ++++ 5 files changed, 106 insertions(+) diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 1fa7bdbf1f..67f854be9c 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -8,6 +8,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { Mock } from 'vitest'; import type { ConfigParameters, SandboxConfig } from './config.js'; import { Config, ApprovalMode } from './config.js'; +import * as fs from 'node:fs'; import * as path from 'node:path'; import { setGeminiMdFilename as mockSetGeminiMdFilename } from '../tools/memoryTool.js'; import { @@ -57,6 +58,9 @@ vi.mock('node:fs', async (importOriginal) => { isDirectory: vi.fn().mockReturnValue(true), }), realpathSync: vi.fn((path) => path), + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), + readFileSync: vi.fn(), }; return { ...mocked, @@ -1240,6 +1244,53 @@ describe('setApprovalMode with folder trust', () => { }); }); + describe('plan file persistence', () => { + it('should save plan to disk', () => { + const config = new Config(baseParams); + + config.savePlan('# My Plan\n1. Step one\n2. Step two'); + + expect(fs.mkdirSync).toHaveBeenCalledWith( + expect.stringContaining('plans'), + { recursive: true }, + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('.md'), + '# My Plan\n1. Step one\n2. Step two', + 'utf-8', + ); + }); + + it('should load plan from disk', () => { + const config = new Config(baseParams); + (fs.readFileSync as Mock).mockReturnValue('# Saved Plan'); + + const plan = config.loadPlan(); + expect(plan).toBe('# Saved Plan'); + }); + + it('should return undefined when no plan file exists', () => { + const config = new Config(baseParams); + (fs.readFileSync as Mock).mockImplementation(() => { + throw new Error('ENOENT'); + }); + + const plan = config.loadPlan(); + expect(plan).toBeUndefined(); + }); + + it('should use session ID in plan file path', () => { + const config = new Config({ + ...baseParams, + sessionId: 'test-session-123', + }); + + const filePath = config.getPlanFilePath(); + expect(filePath).toContain('test-session-123'); + expect(filePath).toMatch(/\.md$/); + }); + }); + describe('registerCoreTools', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 27b33cd063..62076887ac 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -6,6 +6,7 @@ // Node built-ins import type { EventEmitter } from 'node:events'; +import * as fs from 'node:fs'; import * as path from 'node:path'; import process from 'node:process'; @@ -1665,6 +1666,35 @@ export class Config { this.approvalMode = mode; } + /** + * Returns the file path for this session's plan file. + */ + getPlanFilePath(): string { + return Storage.getPlanFilePath(this.sessionId); + } + + /** + * Saves a plan to disk for the current session. + */ + savePlan(plan: string): void { + const filePath = this.getPlanFilePath(); + const dir = path.dirname(filePath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, plan, 'utf-8'); + } + + /** + * Loads the plan for the current session, or returns undefined if none exists. + */ + loadPlan(): string | undefined { + const filePath = this.getPlanFilePath(); + try { + return fs.readFileSync(filePath, 'utf-8'); + } catch { + return undefined; + } + } + getInputFormat(): 'text' | 'stream-json' { return this.inputFormat; } diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index e29cefa621..d14998dec5 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -18,6 +18,7 @@ const TMP_DIR_NAME = 'tmp'; const BIN_DIR_NAME = 'bin'; const PROJECT_DIR_NAME = 'projects'; const IDE_DIR_NAME = 'ide'; +const PLANS_DIR_NAME = 'plans'; const DEBUG_DIR_NAME = 'debug'; const ARENA_DIR_NAME = 'arena'; @@ -165,6 +166,14 @@ export class Storage { return path.join(Storage.getRuntimeBaseDir(), IDE_DIR_NAME); } + static getPlansDir(): string { + return path.join(Storage.getGlobalQwenDir(), PLANS_DIR_NAME); + } + + static getPlanFilePath(sessionId: string): string { + return path.join(Storage.getPlansDir(), `${sessionId}.md`); + } + static getGlobalBinDir(): string { return path.join(Storage.getGlobalQwenDir(), BIN_DIR_NAME); } diff --git a/packages/core/src/tools/exitPlanMode.test.ts b/packages/core/src/tools/exitPlanMode.test.ts index 721069429e..022a78fd69 100644 --- a/packages/core/src/tools/exitPlanMode.test.ts +++ b/packages/core/src/tools/exitPlanMode.test.ts @@ -22,6 +22,7 @@ describe('ExitPlanModeTool', () => { setApprovalMode: vi.fn((mode: ApprovalMode) => { approvalMode = mode; }), + savePlan: vi.fn(), } as unknown as Config; tool = new ExitPlanModeTool(mockConfig); @@ -148,6 +149,9 @@ describe('ExitPlanModeTool', () => { ApprovalMode.DEFAULT, ); expect(approvalMode).toBe(ApprovalMode.DEFAULT); + + // Plan should be saved to disk + expect(mockConfig.savePlan).toHaveBeenCalledWith(params.plan); }); it('should request confirmation with plan details', async () => { @@ -222,6 +226,9 @@ describe('ExitPlanModeTool', () => { ApprovalMode.PLAN, ); expect(approvalMode).toBe(ApprovalMode.PLAN); + + // Plan should NOT be saved when rejected + expect(mockConfig.savePlan).not.toHaveBeenCalled(); }); it('should have correct description', () => { diff --git a/packages/core/src/tools/exitPlanMode.ts b/packages/core/src/tools/exitPlanMode.ts index 123760cf93..b6dd1cdfd0 100644 --- a/packages/core/src/tools/exitPlanMode.ts +++ b/packages/core/src/tools/exitPlanMode.ts @@ -147,6 +147,15 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation< }; } + // Persist the approved plan to disk + try { + this.config.savePlan(plan); + } catch (error) { + debugLogger.warn( + `[ExitPlanModeTool] Failed to save plan to disk: ${error instanceof Error ? error.message : String(error)}`, + ); + } + const llmMessage = `User has approved your plan. You can now start coding. Start with updating your todo list if applicable.`; const displayMessage = 'User approved the plan.'; From 7a2f5887b981c92272754dc1a78b78454a4f953d Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 17:52:27 +0800 Subject: [PATCH 07/13] fix(core): use optional field syntax for prePlanMode declaration Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 62076887ac..57f7627c53 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -530,7 +530,7 @@ export class Config { private sdkMode: boolean; private geminiMdFileCount: number; private approvalMode: ApprovalMode; - private prePlanMode: ApprovalMode | undefined; + private prePlanMode?: ApprovalMode; private readonly accessibility: AccessibilitySettings; private readonly telemetrySettings: TelemetrySettings; private readonly gitCoAuthor: GitCoAuthorSettings; From e038ce99584e44a16b34f70a2226ab7a3fce8b6c Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 18:05:36 +0800 Subject: [PATCH 08/13] fix(core): only suppress ENOENT in loadPlan, rethrow other errors Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/core/src/config/config.test.ts | 15 ++++++++++++++- packages/core/src/config/config.ts | 12 ++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 67f854be9c..f828ff4ee3 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1271,14 +1271,27 @@ describe('setApprovalMode with folder trust', () => { it('should return undefined when no plan file exists', () => { const config = new Config(baseParams); + const enoentError = new Error('ENOENT') as NodeJS.ErrnoException; + enoentError.code = 'ENOENT'; (fs.readFileSync as Mock).mockImplementation(() => { - throw new Error('ENOENT'); + throw enoentError; }); const plan = config.loadPlan(); expect(plan).toBeUndefined(); }); + it('should rethrow non-ENOENT errors from loadPlan', () => { + const config = new Config(baseParams); + const permError = new Error('EACCES') as NodeJS.ErrnoException; + permError.code = 'EACCES'; + (fs.readFileSync as Mock).mockImplementation(() => { + throw permError; + }); + + expect(() => config.loadPlan()).toThrow('EACCES'); + }); + it('should use session ID in plan file path', () => { const config = new Config({ ...baseParams, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 57f7627c53..31d1c4c040 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1690,8 +1690,16 @@ export class Config { const filePath = this.getPlanFilePath(); try { return fs.readFileSync(filePath, 'utf-8'); - } catch { - return undefined; + } catch (error: unknown) { + if ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as NodeJS.ErrnoException).code === 'ENOENT' + ) { + return undefined; + } + throw error; } } From ee8fcc9230ebd139b07b86e427199e076224cb33 Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 18:28:27 +0800 Subject: [PATCH 09/13] docs: add /plan command usage to approval mode documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/users/features/approval-mode.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/users/features/approval-mode.md b/docs/users/features/approval-mode.md index c46067093d..cb217eb3d4 100644 --- a/docs/users/features/approval-mode.md +++ b/docs/users/features/approval-mode.md @@ -40,6 +40,18 @@ You can switch into Plan Mode during a session using **Shift+Tab** (or **Tab** o If you are in Normal Mode, **Shift+Tab** (or **Tab** on Windows) first switches into `auto-edits` Mode, indicated by `⏵⏵ accept edits on` at the bottom of the terminal. A subsequent **Shift+Tab** (or **Tab** on Windows) will switch into Plan Mode, indicated by `⏸ plan mode`. +**Use the `/plan` command** + +The `/plan` command provides a quick shortcut for entering and exiting Plan Mode: + +```bash +/plan # Enter plan mode +/plan refactor the auth module # Enter plan mode and start planning +/plan execute # Exit plan mode and start executing +``` + +When you exit plan mode with `/plan execute`, your previous approval mode is automatically restored (e.g., if you were in Auto-Edit before entering Plan Mode, you'll return to Auto-Edit). + **Start a new session in Plan Mode** To start a new session in Plan Mode, use the `/approval-mode` then select `plan` From 2e089cce712b89d73e5e234b6218af6249e7866e Mon Sep 17 00:00:00 2001 From: wenshao Date: Mon, 6 Apr 2026 18:31:52 +0800 Subject: [PATCH 10/13] fix(docs): fix mode count and update plan example in approval-mode docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix "three distinct permission modes" → "four" (Plan was always listed) - Update refactor example to use /plan command instead of /approval-mode - Fix grammar in example description Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/users/features/approval-mode.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/users/features/approval-mode.md b/docs/users/features/approval-mode.md index cb217eb3d4..f9d214d806 100644 --- a/docs/users/features/approval-mode.md +++ b/docs/users/features/approval-mode.md @@ -1,6 +1,6 @@ # Approval Mode -Qwen Code offers three distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level. +Qwen Code offers four distinct permission modes that allow you to flexibly control how AI interacts with your code and system based on task complexity and risk level. ## Permission Modes Comparison @@ -47,10 +47,10 @@ The `/plan` command provides a quick shortcut for entering and exiting Plan Mode ```bash /plan # Enter plan mode /plan refactor the auth module # Enter plan mode and start planning -/plan execute # Exit plan mode and start executing +/plan execute # Exit plan mode, restore previous mode ``` -When you exit plan mode with `/plan execute`, your previous approval mode is automatically restored (e.g., if you were in Auto-Edit before entering Plan Mode, you'll return to Auto-Edit). +When you exit Plan Mode with `/plan execute`, your previous approval mode is automatically restored (e.g., if you were in Auto-Edit before entering Plan Mode, you'll return to Auto-Edit). **Start a new session in Plan Mode** @@ -71,14 +71,10 @@ qwen --prompt "What is machine learning?" ### Example: Planning a complex refactor ```bash -/approval-mode plan +/plan I need to refactor our authentication system to use OAuth2. Create a detailed migration plan. ``` -``` -I need to refactor our authentication system to use OAuth2. Create a detailed migration plan. -``` - -Qwen Code analyzes the current implementation and create a comprehensive plan. Refine with follow-ups: +Qwen Code enters Plan Mode and analyzes the current implementation to create a comprehensive plan. Refine with follow-ups: ``` What about backward compatibility? @@ -247,7 +243,7 @@ qwen --prompt "Run the test suite, fix all failing tests, then commit changes" ### Keyboard Shortcut Switching -During a Qwen Code session, use **Shift+Tab**​ (or **Tab** on Windows) to quickly cycle through the three modes: +During a Qwen Code session, use **Shift+Tab**​ (or **Tab** on Windows) to quickly cycle through the four modes: ``` Default Mode → Auto-Edit Mode → YOLO Mode → Plan Mode → Default Mode From b50f23f91c44854200d914f45b8928564f113b71 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 8 Apr 2026 06:53:44 +0800 Subject: [PATCH 11/13] fix: correct copyright year from 2026 to 2025 in planCommand.ts --- packages/cli/src/ui/commands/planCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/commands/planCommand.ts b/packages/cli/src/ui/commands/planCommand.ts index 415dfe0afc..7f45ce57d8 100644 --- a/packages/cli/src/ui/commands/planCommand.ts +++ b/packages/cli/src/ui/commands/planCommand.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen Team + * Copyright 2025 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ From 1d4c17b1ce5588e975d5a282e1a799929b27e491 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 8 Apr 2026 10:21:00 +0800 Subject: [PATCH 12/13] fix: rename /plan execute to /plan exit Rename the subcommand to accurately reflect its behavior (exits plan mode and restores previous approval mode, does not trigger execution). Update source, tests, i18n keys (6 locales), and docs. --- docs/users/features/approval-mode.md | 4 ++-- docs/users/features/commands.md | 2 +- packages/cli/src/i18n/locales/de.js | 12 ++++++------ packages/cli/src/i18n/locales/en.js | 12 ++++++------ packages/cli/src/i18n/locales/ja.js | 12 ++++++------ packages/cli/src/i18n/locales/pt.js | 12 ++++++------ packages/cli/src/i18n/locales/ru.js | 12 ++++++------ packages/cli/src/i18n/locales/zh.js | 11 +++++------ packages/cli/src/ui/commands/planCommand.test.ts | 14 +++++++------- packages/cli/src/ui/commands/planCommand.ts | 10 ++++------ 10 files changed, 49 insertions(+), 52 deletions(-) diff --git a/docs/users/features/approval-mode.md b/docs/users/features/approval-mode.md index f9d214d806..60fb0d97cb 100644 --- a/docs/users/features/approval-mode.md +++ b/docs/users/features/approval-mode.md @@ -47,10 +47,10 @@ The `/plan` command provides a quick shortcut for entering and exiting Plan Mode ```bash /plan # Enter plan mode /plan refactor the auth module # Enter plan mode and start planning -/plan execute # Exit plan mode, restore previous mode +/plan exit # Exit plan mode, restore previous mode ``` -When you exit Plan Mode with `/plan execute`, your previous approval mode is automatically restored (e.g., if you were in Auto-Edit before entering Plan Mode, you'll return to Auto-Edit). +When you exit Plan Mode with `/plan exit`, your previous approval mode is automatically restored (e.g., if you were in Auto-Edit before entering Plan Mode, you'll return to Auto-Edit). **Start a new session in Plan Mode** diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md index 8ade7eada0..03587e15ae 100644 --- a/docs/users/features/commands.md +++ b/docs/users/features/commands.md @@ -61,7 +61,7 @@ Commands for managing AI tools and models. | `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` | | `/tools` | Display currently available tool list | `/tools`, `/tools desc` | | `/skills` | List and run available skills | `/skills`, `/skills ` | -| `/plan` | Switch to plan mode or execute the current plan | `/plan`, `/plan `, `/plan execute` | +| `/plan` | Switch to plan mode or exit plan mode | `/plan`, `/plan `, `/plan exit` | | `/approval-mode` | Change approval mode for tool usage | `/approval-mode --project` | | →`plan` | Analysis only, no execution | Secure review | | →`default` | Require approval for edits | Daily use | diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index e2cfc62005..6f028f9574 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1974,14 +1974,14 @@ export default { 'Press Ctrl+O to show full tool output': 'Strg+O für vollständige Tool-Ausgabe drücken', - 'Switch to plan mode or execute the current plan': - 'Switch to plan mode or execute the current plan', - 'Exited plan mode. The agent will now execute the plan.': - 'Exited plan mode. The agent will now execute the plan.', + 'Switch to plan mode or exit plan mode': + 'Switch to plan mode or exit plan mode', + 'Exited plan mode. Previous approval mode restored.': + 'Exited plan mode. Previous approval mode restored.', 'Enabled plan mode. The agent will analyze and plan without executing tools.': 'Enabled plan mode. The agent will analyze and plan without executing tools.', - 'Already in plan mode. Use "/plan execute" to execute the plan.': - 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Already in plan mode. Use "/plan exit" to exit plan mode.': + 'Already in plan mode. Use "/plan exit" to exit plan mode.', 'Not in plan mode. Use "/plan" to enter plan mode first.': 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 201ce4a425..065bb8b8f4 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -2014,14 +2014,14 @@ export default { 'Press Ctrl+O to show full tool output': 'Press Ctrl+O to show full tool output', - 'Switch to plan mode or execute the current plan': - 'Switch to plan mode or execute the current plan', - 'Exited plan mode. The agent will now execute the plan.': - 'Exited plan mode. The agent will now execute the plan.', + 'Switch to plan mode or exit plan mode': + 'Switch to plan mode or exit plan mode', + 'Exited plan mode. Previous approval mode restored.': + 'Exited plan mode. Previous approval mode restored.', 'Enabled plan mode. The agent will analyze and plan without executing tools.': 'Enabled plan mode. The agent will analyze and plan without executing tools.', - 'Already in plan mode. Use "/plan execute" to execute the plan.': - 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Already in plan mode. Use "/plan exit" to exit plan mode.': + 'Already in plan mode. Use "/plan exit" to exit plan mode.', 'Not in plan mode. Use "/plan" to enter plan mode first.': 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index 971a01cfd4..df79083c00 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -1465,14 +1465,14 @@ export default { '詳細モードで完全なツール出力と思考を表示します(Ctrl+O で切り替え)。', 'Press Ctrl+O to show full tool output': 'Ctrl+O で完全なツール出力を表示', - 'Switch to plan mode or execute the current plan': - 'Switch to plan mode or execute the current plan', - 'Exited plan mode. The agent will now execute the plan.': - 'Exited plan mode. The agent will now execute the plan.', + 'Switch to plan mode or exit plan mode': + 'Switch to plan mode or exit plan mode', + 'Exited plan mode. Previous approval mode restored.': + 'Exited plan mode. Previous approval mode restored.', 'Enabled plan mode. The agent will analyze and plan without executing tools.': 'Enabled plan mode. The agent will analyze and plan without executing tools.', - 'Already in plan mode. Use "/plan execute" to execute the plan.': - 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Already in plan mode. Use "/plan exit" to exit plan mode.': + 'Already in plan mode. Use "/plan exit" to exit plan mode.', 'Not in plan mode. Use "/plan" to enter plan mode first.': 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 810e068d02..6c67fedb76 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1964,14 +1964,14 @@ export default { 'Press Ctrl+O to show full tool output': 'Pressione Ctrl+O para exibir a saída completa da ferramenta', - 'Switch to plan mode or execute the current plan': - 'Switch to plan mode or execute the current plan', - 'Exited plan mode. The agent will now execute the plan.': - 'Exited plan mode. The agent will now execute the plan.', + 'Switch to plan mode or exit plan mode': + 'Switch to plan mode or exit plan mode', + 'Exited plan mode. Previous approval mode restored.': + 'Exited plan mode. Previous approval mode restored.', 'Enabled plan mode. The agent will analyze and plan without executing tools.': 'Enabled plan mode. The agent will analyze and plan without executing tools.', - 'Already in plan mode. Use "/plan execute" to execute the plan.': - 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Already in plan mode. Use "/plan exit" to exit plan mode.': + 'Already in plan mode. Use "/plan exit" to exit plan mode.', 'Not in plan mode. Use "/plan" to enter plan mode first.': 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index e5f7eb9291..3ccb15b674 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1971,14 +1971,14 @@ export default { 'Press Ctrl+O to show full tool output': 'Нажмите Ctrl+O для показа полного вывода инструментов', - 'Switch to plan mode or execute the current plan': - 'Switch to plan mode or execute the current plan', - 'Exited plan mode. The agent will now execute the plan.': - 'Exited plan mode. The agent will now execute the plan.', + 'Switch to plan mode or exit plan mode': + 'Switch to plan mode or exit plan mode', + 'Exited plan mode. Previous approval mode restored.': + 'Exited plan mode. Previous approval mode restored.', 'Enabled plan mode. The agent will analyze and plan without executing tools.': 'Enabled plan mode. The agent will analyze and plan without executing tools.', - 'Already in plan mode. Use "/plan execute" to execute the plan.': - 'Already in plan mode. Use "/plan execute" to execute the plan.', + 'Already in plan mode. Use "/plan exit" to exit plan mode.': + 'Already in plan mode. Use "/plan exit" to exit plan mode.', 'Not in plan mode. Use "/plan" to enter plan mode first.': 'Not in plan mode. Use "/plan" to enter plan mode first.', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 5a56e0b7f3..3ac427a088 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1818,14 +1818,13 @@ export default { '详细模式下显示完整工具输出和思考过程(Ctrl+O 切换)。', 'Press Ctrl+O to show full tool output': '按 Ctrl+O 查看详细工具调用结果', - 'Switch to plan mode or execute the current plan': - '切换到计划模式或执行当前计划', - 'Exited plan mode. The agent will now execute the plan.': - '退出计划模式。智能体现在将执行计划。', + 'Switch to plan mode or exit plan mode': '切换到计划模式或退出计划模式', + 'Exited plan mode. Previous approval mode restored.': + '已退出计划模式,已恢复之前的审批模式。', 'Enabled plan mode. The agent will analyze and plan without executing tools.': '启用计划模式。智能体将只分析和规划,而不执行工具。', - 'Already in plan mode. Use "/plan execute" to execute the plan.': - '已处于计划模式。使用 "/plan execute" 执行计划。', + 'Already in plan mode. Use "/plan exit" to exit plan mode.': + '已处于计划模式。使用 "/plan exit" 退出计划模式。', 'Not in plan mode. Use "/plan" to enter plan mode first.': '未处于计划模式。请先使用 "/plan" 进入计划模式。', }; diff --git a/packages/cli/src/ui/commands/planCommand.test.ts b/packages/cli/src/ui/commands/planCommand.test.ts index d4a671c3dc..f23b6afb45 100644 --- a/packages/cli/src/ui/commands/planCommand.test.ts +++ b/packages/cli/src/ui/commands/planCommand.test.ts @@ -74,7 +74,7 @@ describe('planCommand', () => { expect(result).toEqual({ type: 'message', messageType: 'info', - content: 'Already in plan mode. Use "/plan execute" to execute the plan.', + content: 'Already in plan mode. Use "/plan exit" to exit plan mode.', }); }); @@ -96,7 +96,7 @@ describe('planCommand', () => { }); }); - it('should exit plan mode when execute argument is passed', async () => { + it('should exit plan mode when exit argument is passed', async () => { if (!planCommand.action) { throw new Error('The plan command must have an action.'); } @@ -105,7 +105,7 @@ describe('planCommand', () => { ApprovalMode.PLAN, ); - const result = await planCommand.action(mockContext, 'execute'); + const result = await planCommand.action(mockContext, 'exit'); expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith( ApprovalMode.DEFAULT, @@ -113,7 +113,7 @@ describe('planCommand', () => { expect(result).toEqual({ type: 'message', messageType: 'info', - content: 'Exited plan mode. The agent will now execute the plan.', + content: 'Exited plan mode. Previous approval mode restored.', }); }); @@ -129,7 +129,7 @@ describe('planCommand', () => { ApprovalMode.AUTO_EDIT, ); - const result = await planCommand.action(mockContext, 'execute'); + const result = await planCommand.action(mockContext, 'exit'); expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith( ApprovalMode.AUTO_EDIT, @@ -137,7 +137,7 @@ describe('planCommand', () => { expect(result).toEqual({ type: 'message', messageType: 'info', - content: 'Exited plan mode. The agent will now execute the plan.', + content: 'Exited plan mode. Previous approval mode restored.', }); }); @@ -147,7 +147,7 @@ describe('planCommand', () => { } // Default mock returns ApprovalMode.DEFAULT (not PLAN) - const result = await planCommand.action(mockContext, 'execute'); + const result = await planCommand.action(mockContext, 'exit'); expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled(); expect(result).toEqual({ diff --git a/packages/cli/src/ui/commands/planCommand.ts b/packages/cli/src/ui/commands/planCommand.ts index 7f45ce57d8..494ee463fe 100644 --- a/packages/cli/src/ui/commands/planCommand.ts +++ b/packages/cli/src/ui/commands/planCommand.ts @@ -17,7 +17,7 @@ import { ApprovalMode } from '@qwen-code/qwen-code-core'; export const planCommand: SlashCommand = { name: 'plan', get description() { - return t('Switch to plan mode or execute the current plan'); + return t('Switch to plan mode or exit plan mode'); }, kind: CommandKind.BUILT_IN, action: async ( @@ -36,7 +36,7 @@ export const planCommand: SlashCommand = { const trimmedArgs = args.trim(); const currentMode = config.getApprovalMode(); - if (trimmedArgs === 'execute') { + if (trimmedArgs === 'exit') { if (currentMode !== ApprovalMode.PLAN) { return { type: 'message', @@ -56,7 +56,7 @@ export const planCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: t('Exited plan mode. The agent will now execute the plan.'), + content: t('Exited plan mode. Previous approval mode restored.'), }; } @@ -98,9 +98,7 @@ export const planCommand: SlashCommand = { return { type: 'message', messageType: 'info', - content: t( - 'Already in plan mode. Use "/plan execute" to execute the plan.', - ), + content: t('Already in plan mode. Use "/plan exit" to exit plan mode.'), }; }, }; From 121af70cc03c4959f3ac2b51b032ed507a6490a5 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 8 Apr 2026 14:51:26 +0800 Subject: [PATCH 13/13] fix: ProceedOnce should set DEFAULT mode, not restore pre-plan mode "Yes, and manually approve edits" was restoring getPrePlanMode() which could be YOLO, contradicting the label. Now hardcodes DEFAULT to match the "manually approve" semantics. --- packages/core/src/tools/exitPlanMode.test.ts | 9 +++++---- packages/core/src/tools/exitPlanMode.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/core/src/tools/exitPlanMode.test.ts b/packages/core/src/tools/exitPlanMode.test.ts index 022a78fd69..f1d80430bf 100644 --- a/packages/core/src/tools/exitPlanMode.test.ts +++ b/packages/core/src/tools/exitPlanMode.test.ts @@ -178,8 +178,9 @@ describe('ExitPlanModeTool', () => { expect(approvalMode).toBe(ApprovalMode.AUTO_EDIT); }); - it('should restore pre-plan mode on ProceedOnce', async () => { - // Simulate entering plan mode from AUTO_EDIT + it('should set DEFAULT mode on ProceedOnce regardless of pre-plan mode', async () => { + // Even if pre-plan mode was AUTO_EDIT, ProceedOnce ("manually approve + // edits") should always set DEFAULT to match the option label semantics. (mockConfig.getPrePlanMode as ReturnType).mockReturnValue( ApprovalMode.AUTO_EDIT, ); @@ -195,9 +196,9 @@ describe('ExitPlanModeTool', () => { } expect(mockConfig.setApprovalMode).toHaveBeenCalledWith( - ApprovalMode.AUTO_EDIT, + ApprovalMode.DEFAULT, ); - expect(approvalMode).toBe(ApprovalMode.AUTO_EDIT); + expect(approvalMode).toBe(ApprovalMode.DEFAULT); }); it('should remain in plan mode when confirmation is rejected', async () => { diff --git a/packages/core/src/tools/exitPlanMode.ts b/packages/core/src/tools/exitPlanMode.ts index b6dd1cdfd0..03485e4cf0 100644 --- a/packages/core/src/tools/exitPlanMode.ts +++ b/packages/core/src/tools/exitPlanMode.ts @@ -99,7 +99,7 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation< break; case ToolConfirmationOutcome.ProceedOnce: this.wasApproved = true; - this.setApprovalModeSafely(this.config.getPrePlanMode()); + this.setApprovalModeSafely(ApprovalMode.DEFAULT); break; case ToolConfirmationOutcome.Cancel: this.wasApproved = false;