Skip to content

Commit db7488f

Browse files
authored
Merge pull request #2921 from QwenLM/feat/plan-mode
feat(cli): implement /plan command for plan mode
2 parents 28b81b8 + 121af70 commit db7488f

17 files changed

Lines changed: 553 additions & 11 deletions

File tree

docs/users/features/approval-mode.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Approval Mode
22

3-
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.
3+
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.
44

55
## Permission Modes Comparison
66

@@ -40,6 +40,18 @@ You can switch into Plan Mode during a session using **Shift+Tab** (or **Tab** o
4040

4141
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`.
4242

43+
**Use the `/plan` command**
44+
45+
The `/plan` command provides a quick shortcut for entering and exiting Plan Mode:
46+
47+
```bash
48+
/plan # Enter plan mode
49+
/plan refactor the auth module # Enter plan mode and start planning
50+
/plan exit # Exit plan mode, restore previous mode
51+
```
52+
53+
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).
54+
4355
**Start a new session in Plan Mode**
4456

4557
To start a new session in Plan Mode, use the `/approval-mode` then select `plan`
@@ -59,14 +71,10 @@ qwen --prompt "What is machine learning?"
5971
### Example: Planning a complex refactor
6072

6173
```bash
62-
/approval-mode plan
63-
```
64-
65-
```
66-
I need to refactor our authentication system to use OAuth2. Create a detailed migration plan.
74+
/plan I need to refactor our authentication system to use OAuth2. Create a detailed migration plan.
6775
```
6876

69-
Qwen Code analyzes the current implementation and create a comprehensive plan. Refine with follow-ups:
77+
Qwen Code enters Plan Mode and analyzes the current implementation to create a comprehensive plan. Refine with follow-ups:
7078

7179
```
7280
What about backward compatibility?
@@ -235,7 +243,7 @@ qwen --prompt "Run the test suite, fix all failing tests, then commit changes"
235243

236244
### Keyboard Shortcut Switching
237245

238-
During a Qwen Code session, use **Shift+Tab**​ (or **Tab** on Windows) to quickly cycle through the three modes:
246+
During a Qwen Code session, use **Shift+Tab**​ (or **Tab** on Windows) to quickly cycle through the four modes:
239247

240248
```
241249
Default Mode → Auto-Edit Mode → YOLO Mode → Plan Mode → Default Mode

docs/users/features/commands.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ Commands for managing AI tools and models.
6161
| `/mcp` | List configured MCP servers and tools | `/mcp`, `/mcp desc` |
6262
| `/tools` | Display currently available tool list | `/tools`, `/tools desc` |
6363
| `/skills` | List and run available skills | `/skills`, `/skills <name>` |
64+
| `/plan` | Switch to plan mode or exit plan mode | `/plan`, `/plan <task>`, `/plan exit` |
6465
| `/approval-mode` | Change approval mode for tool usage | `/approval-mode <mode (auto-edit)> --project` |
6566
|`plan` | Analysis only, no execution | Secure review |
6667
|`default` | Require approval for edits | Daily use |

packages/cli/src/i18n/locales/de.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,4 +1973,15 @@ export default {
19731973
'Vollständige Tool-Ausgabe und Denkprozess im ausführlichen Modus anzeigen (mit Strg+O umschalten).',
19741974
'Press Ctrl+O to show full tool output':
19751975
'Strg+O für vollständige Tool-Ausgabe drücken',
1976+
1977+
'Switch to plan mode or exit plan mode':
1978+
'Switch to plan mode or exit plan mode',
1979+
'Exited plan mode. Previous approval mode restored.':
1980+
'Exited plan mode. Previous approval mode restored.',
1981+
'Enabled plan mode. The agent will analyze and plan without executing tools.':
1982+
'Enabled plan mode. The agent will analyze and plan without executing tools.',
1983+
'Already in plan mode. Use "/plan exit" to exit plan mode.':
1984+
'Already in plan mode. Use "/plan exit" to exit plan mode.',
1985+
'Not in plan mode. Use "/plan" to enter plan mode first.':
1986+
'Not in plan mode. Use "/plan" to enter plan mode first.',
19761987
};

packages/cli/src/i18n/locales/en.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,4 +2013,15 @@ export default {
20132013
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).',
20142014
'Press Ctrl+O to show full tool output':
20152015
'Press Ctrl+O to show full tool output',
2016+
2017+
'Switch to plan mode or exit plan mode':
2018+
'Switch to plan mode or exit plan mode',
2019+
'Exited plan mode. Previous approval mode restored.':
2020+
'Exited plan mode. Previous approval mode restored.',
2021+
'Enabled plan mode. The agent will analyze and plan without executing tools.':
2022+
'Enabled plan mode. The agent will analyze and plan without executing tools.',
2023+
'Already in plan mode. Use "/plan exit" to exit plan mode.':
2024+
'Already in plan mode. Use "/plan exit" to exit plan mode.',
2025+
'Not in plan mode. Use "/plan" to enter plan mode first.':
2026+
'Not in plan mode. Use "/plan" to enter plan mode first.',
20162027
};

packages/cli/src/i18n/locales/ja.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,4 +1464,15 @@ export default {
14641464
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
14651465
'詳細モードで完全なツール出力と思考を表示します(Ctrl+O で切り替え)。',
14661466
'Press Ctrl+O to show full tool output': 'Ctrl+O で完全なツール出力を表示',
1467+
1468+
'Switch to plan mode or exit plan mode':
1469+
'Switch to plan mode or exit plan mode',
1470+
'Exited plan mode. Previous approval mode restored.':
1471+
'Exited plan mode. Previous approval mode restored.',
1472+
'Enabled plan mode. The agent will analyze and plan without executing tools.':
1473+
'Enabled plan mode. The agent will analyze and plan without executing tools.',
1474+
'Already in plan mode. Use "/plan exit" to exit plan mode.':
1475+
'Already in plan mode. Use "/plan exit" to exit plan mode.',
1476+
'Not in plan mode. Use "/plan" to enter plan mode first.':
1477+
'Not in plan mode. Use "/plan" to enter plan mode first.',
14671478
};

packages/cli/src/i18n/locales/pt.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,4 +1963,15 @@ export default {
19631963
'Mostrar saída completa da ferramenta e raciocínio no modo detalhado (alternar com Ctrl+O).',
19641964
'Press Ctrl+O to show full tool output':
19651965
'Pressione Ctrl+O para exibir a saída completa da ferramenta',
1966+
1967+
'Switch to plan mode or exit plan mode':
1968+
'Switch to plan mode or exit plan mode',
1969+
'Exited plan mode. Previous approval mode restored.':
1970+
'Exited plan mode. Previous approval mode restored.',
1971+
'Enabled plan mode. The agent will analyze and plan without executing tools.':
1972+
'Enabled plan mode. The agent will analyze and plan without executing tools.',
1973+
'Already in plan mode. Use "/plan exit" to exit plan mode.':
1974+
'Already in plan mode. Use "/plan exit" to exit plan mode.',
1975+
'Not in plan mode. Use "/plan" to enter plan mode first.':
1976+
'Not in plan mode. Use "/plan" to enter plan mode first.',
19661977
};

packages/cli/src/i18n/locales/ru.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,4 +1970,15 @@ export default {
19701970
'Показывать полный вывод инструментов и процесс рассуждений в подробном режиме (переключить с помощью Ctrl+O).',
19711971
'Press Ctrl+O to show full tool output':
19721972
'Нажмите Ctrl+O для показа полного вывода инструментов',
1973+
1974+
'Switch to plan mode or exit plan mode':
1975+
'Switch to plan mode or exit plan mode',
1976+
'Exited plan mode. Previous approval mode restored.':
1977+
'Exited plan mode. Previous approval mode restored.',
1978+
'Enabled plan mode. The agent will analyze and plan without executing tools.':
1979+
'Enabled plan mode. The agent will analyze and plan without executing tools.',
1980+
'Already in plan mode. Use "/plan exit" to exit plan mode.':
1981+
'Already in plan mode. Use "/plan exit" to exit plan mode.',
1982+
'Not in plan mode. Use "/plan" to enter plan mode first.':
1983+
'Not in plan mode. Use "/plan" to enter plan mode first.',
19731984
};

packages/cli/src/i18n/locales/zh.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,4 +1817,14 @@ export default {
18171817
'Show full tool output and thinking in verbose mode (toggle with Ctrl+O).':
18181818
'详细模式下显示完整工具输出和思考过程(Ctrl+O 切换)。',
18191819
'Press Ctrl+O to show full tool output': '按 Ctrl+O 查看详细工具调用结果',
1820+
1821+
'Switch to plan mode or exit plan mode': '切换到计划模式或退出计划模式',
1822+
'Exited plan mode. Previous approval mode restored.':
1823+
'已退出计划模式,已恢复之前的审批模式。',
1824+
'Enabled plan mode. The agent will analyze and plan without executing tools.':
1825+
'启用计划模式。智能体将只分析和规划,而不执行工具。',
1826+
'Already in plan mode. Use "/plan exit" to exit plan mode.':
1827+
'已处于计划模式。使用 "/plan exit" 退出计划模式。',
1828+
'Not in plan mode. Use "/plan" to enter plan mode first.':
1829+
'未处于计划模式。请先使用 "/plan" 进入计划模式。',
18201830
};

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { languageCommand } from '../ui/commands/languageCommand.js';
3232
import { mcpCommand } from '../ui/commands/mcpCommand.js';
3333
import { memoryCommand } from '../ui/commands/memoryCommand.js';
3434
import { modelCommand } from '../ui/commands/modelCommand.js';
35+
import { planCommand } from '../ui/commands/planCommand.js';
3536
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
3637
import { trustCommand } from '../ui/commands/trustCommand.js';
3738
import { quitCommand } from '../ui/commands/quitCommand.js';
@@ -103,6 +104,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
103104
mcpCommand,
104105
memoryCommand,
105106
modelCommand,
107+
planCommand,
106108
permissionsCommand,
107109
...(this.config?.getFolderTrust() ? [trustCommand] : []),
108110
quitCommand,
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Qwen Team
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
8+
import { planCommand } from './planCommand.js';
9+
import { type CommandContext } from './types.js';
10+
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
11+
import { ApprovalMode } from '@qwen-code/qwen-code-core';
12+
13+
describe('planCommand', () => {
14+
let mockContext: CommandContext;
15+
16+
beforeEach(() => {
17+
mockContext = createMockCommandContext({
18+
services: {
19+
config: {
20+
getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
21+
getPrePlanMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
22+
setApprovalMode: vi.fn(),
23+
} as unknown as import('@qwen-code/qwen-code-core').Config,
24+
},
25+
});
26+
});
27+
28+
it('should switch to plan mode if not in plan mode', async () => {
29+
if (!planCommand.action) {
30+
throw new Error('The plan command must have an action.');
31+
}
32+
33+
const result = await planCommand.action(mockContext, '');
34+
35+
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
36+
ApprovalMode.PLAN,
37+
);
38+
expect(result).toEqual({
39+
type: 'message',
40+
messageType: 'info',
41+
content:
42+
'Enabled plan mode. The agent will analyze and plan without executing tools.',
43+
});
44+
});
45+
46+
it('should return submit prompt if arguments are provided when switching to plan mode', async () => {
47+
if (!planCommand.action) {
48+
throw new Error('The plan command must have an action.');
49+
}
50+
51+
const result = await planCommand.action(mockContext, 'refactor the code');
52+
53+
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
54+
ApprovalMode.PLAN,
55+
);
56+
expect(result).toEqual({
57+
type: 'submit_prompt',
58+
content: [{ text: 'refactor the code' }],
59+
});
60+
});
61+
62+
it('should return already in plan mode if mode is already plan', async () => {
63+
if (!planCommand.action) {
64+
throw new Error('The plan command must have an action.');
65+
}
66+
67+
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
68+
ApprovalMode.PLAN,
69+
);
70+
71+
const result = await planCommand.action(mockContext, '');
72+
73+
expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled();
74+
expect(result).toEqual({
75+
type: 'message',
76+
messageType: 'info',
77+
content: 'Already in plan mode. Use "/plan exit" to exit plan mode.',
78+
});
79+
});
80+
81+
it('should return submit prompt if arguments are provided and already in plan mode', async () => {
82+
if (!planCommand.action) {
83+
throw new Error('The plan command must have an action.');
84+
}
85+
86+
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
87+
ApprovalMode.PLAN,
88+
);
89+
90+
const result = await planCommand.action(mockContext, 'keep planning');
91+
92+
expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled();
93+
expect(result).toEqual({
94+
type: 'submit_prompt',
95+
content: [{ text: 'keep planning' }],
96+
});
97+
});
98+
99+
it('should exit plan mode when exit argument is passed', async () => {
100+
if (!planCommand.action) {
101+
throw new Error('The plan command must have an action.');
102+
}
103+
104+
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
105+
ApprovalMode.PLAN,
106+
);
107+
108+
const result = await planCommand.action(mockContext, 'exit');
109+
110+
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
111+
ApprovalMode.DEFAULT,
112+
);
113+
expect(result).toEqual({
114+
type: 'message',
115+
messageType: 'info',
116+
content: 'Exited plan mode. Previous approval mode restored.',
117+
});
118+
});
119+
120+
it('should restore pre-plan mode when executing from plan mode', async () => {
121+
if (!planCommand.action) {
122+
throw new Error('The plan command must have an action.');
123+
}
124+
125+
(mockContext.services.config?.getApprovalMode as Mock).mockReturnValue(
126+
ApprovalMode.PLAN,
127+
);
128+
(mockContext.services.config?.getPrePlanMode as Mock).mockReturnValue(
129+
ApprovalMode.AUTO_EDIT,
130+
);
131+
132+
const result = await planCommand.action(mockContext, 'exit');
133+
134+
expect(mockContext.services.config?.setApprovalMode).toHaveBeenCalledWith(
135+
ApprovalMode.AUTO_EDIT,
136+
);
137+
expect(result).toEqual({
138+
type: 'message',
139+
messageType: 'info',
140+
content: 'Exited plan mode. Previous approval mode restored.',
141+
});
142+
});
143+
144+
it('should return error when execute is used but not in plan mode', async () => {
145+
if (!planCommand.action) {
146+
throw new Error('The plan command must have an action.');
147+
}
148+
149+
// Default mock returns ApprovalMode.DEFAULT (not PLAN)
150+
const result = await planCommand.action(mockContext, 'exit');
151+
152+
expect(mockContext.services.config?.setApprovalMode).not.toHaveBeenCalled();
153+
expect(result).toEqual({
154+
type: 'message',
155+
messageType: 'error',
156+
content: 'Not in plan mode. Use "/plan" to enter plan mode first.',
157+
});
158+
});
159+
});

0 commit comments

Comments
 (0)