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
This commit is contained in:
wenshao 2026-04-06 06:31:04 +08:00
parent 6785a8d908
commit 135699dd5a
13 changed files with 448 additions and 15 deletions

View file

@ -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 <name>` |
| `/plan` | Switch to plan mode or execute current plan | `/plan`, `/plan execute` |
| `/approval-mode` | Change approval mode for tool usage | `/approval-mode <mode (auto-edit)> --project` |
| →`plan` | Analysis only, no execution | Secure review |
| →`default` | Require approval for edits | Daily use |

22
package-lock.json generated
View file

@ -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": {

View file

@ -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.',
};

View file

@ -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.',
};

View file

@ -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.',
};

View file

@ -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.',
};

View file

@ -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.',
};

View file

@ -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: '枚举',
};

View file

@ -44,6 +44,7 @@ export const ALLOWED_BUILTIN_COMMANDS_NON_INTERACTIVE = [
'compress',
'btw',
'bug',
'plan',
] as const;
/**

View file

@ -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,

View file

@ -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.',
});
});
});

View file

@ -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<MessageActionReturn | SubmitPromptActionReturn> => {
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.',
),
};
},
};

View file

@ -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 <tag>",
"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 <tag>",
"Missing tag. Usage: /chat resume <tag>",
"Missing tag. Usage: /chat save <tag>",
"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 <tag>",
"Reverse search history",
"Save the current conversation as a checkpoint. Usage: /chat save <tag>",
"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 <file>",
"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 <extension-names>|--all",
"Usage: /approval-mode <mode> [--session|--user|--project]",
"Usage: /language ui [zh-CN|en-US]",
"YOLO mode - Automatically approve all tools"
"Usage: /extensions uninstall <extension-name>",
"Usage: /extensions update <extension-names>|--all",
"Usage: /extensions {{command}} <extension> [--scope=<user|workspace>]",
"Use /mcp auth <server-name> 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
}