feat(plan): enable Plan Mode by default (#21713)

This commit is contained in:
Jerop Kipruto
2026-03-09 11:58:46 -04:00
committed by GitHub
parent a253938ac5
commit 35ee2a841a
15 changed files with 32 additions and 48 deletions
+3 -22
View File
@@ -1,4 +1,4 @@
# Plan Mode (experimental) # Plan Mode
Plan Mode is a read-only environment for architecting robust solutions before Plan Mode is a read-only environment for architecting robust solutions before
implementation. With Plan Mode, you can: implementation. With Plan Mode, you can:
@@ -8,27 +8,8 @@ implementation. With Plan Mode, you can:
- **Design:** Understand problems, evaluate trade-offs, and choose a solution. - **Design:** Understand problems, evaluate trade-offs, and choose a solution.
- **Plan:** Align on an execution strategy before any code is modified. - **Plan:** Align on an execution strategy before any code is modified.
> **Note:** This is a preview feature currently under active development. Your Plan Mode is enabled by default. You can manage this setting using the
> feedback is invaluable as we refine this feature. If you have ideas, `/settings` command.
> suggestions, or encounter issues:
>
> - [Open an issue] on GitHub.
> - Use the **/bug** command within Gemini CLI to file an issue.
## How to enable Plan Mode
Enable Plan Mode in **Settings** or by editing your configuration file.
- **Settings:** Use the `/settings` command and set **Plan** to `true`.
- **Configuration:** Add the following to your `settings.json`:
```json
{
"experimental": {
"plan": true
}
}
```
## How to enter Plan Mode ## How to enter Plan Mode
+1 -1
View File
@@ -144,7 +144,7 @@ they appear in the UI.
| Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` | | Enable Tool Output Masking | `experimental.toolOutputMasking.enabled` | Enables tool output masking to save tokens. | `true` |
| Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | | Use OSC 52 Paste | `experimental.useOSC52Paste` | Use OSC 52 for pasting. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` | | Use OSC 52 Copy | `experimental.useOSC52Copy` | Use OSC 52 for copying. This may be more robust than the default system when using remote terminal sessions (if your terminal is configured to allow it). | `false` |
| Plan | `experimental.plan` | Enable planning features (Plan Mode and tools). | `false` | | Plan | `experimental.plan` | Enable Plan Mode. | `true` |
| Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` | | Model Steering | `experimental.modelSteering` | Enable model steering (user hints) to guide the model during tool execution. | `false` |
| Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` | | Direct Web Fetch | `experimental.directWebFetch` | Enable web fetch behavior that bypasses LLM summarization. | `false` |
+2 -2
View File
@@ -279,8 +279,8 @@ Slash commands provide meta-level control over the CLI itself.
- **Description:** Switch to Plan Mode (read-only) and view the current plan if - **Description:** Switch to Plan Mode (read-only) and view the current plan if
one has been generated. one has been generated.
- **Note:** This feature requires the `experimental.plan` setting to be - **Note:** This feature is enabled by default. It can be disabled via the
enabled in your configuration. `experimental.plan` setting in your configuration.
- **Sub-commands:** - **Sub-commands:**
- **`copy`**: - **`copy`**:
- **Description:** Copy the currently approved plan to your clipboard. - **Description:** Copy the currently approved plan to your clipboard.
+2 -2
View File
@@ -1021,8 +1021,8 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `false` - **Default:** `false`
- **`experimental.plan`** (boolean): - **`experimental.plan`** (boolean):
- **Description:** Enable planning features (Plan Mode and tools). - **Description:** Enable Plan Mode.
- **Default:** `false` - **Default:** `true`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`experimental.taskTracker`** (boolean): - **`experimental.taskTracker`** (boolean):
+2 -2
View File
@@ -172,7 +172,7 @@ describe('GeminiAgent', () => {
unsubscribe: vi.fn(), unsubscribe: vi.fn(),
}), }),
getApprovalMode: vi.fn().mockReturnValue('default'), getApprovalMode: vi.fn().mockReturnValue('default'),
isPlanEnabled: vi.fn().mockReturnValue(false), isPlanEnabled: vi.fn().mockReturnValue(true),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false), getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false), getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getCheckpointingEnabled: vi.fn().mockReturnValue(false), getCheckpointingEnabled: vi.fn().mockReturnValue(false),
@@ -650,7 +650,7 @@ describe('Session', () => {
getMessageBus: vi.fn().mockReturnValue(mockMessageBus), getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
setApprovalMode: vi.fn(), setApprovalMode: vi.fn(),
setModel: vi.fn(), setModel: vi.fn(),
isPlanEnabled: vi.fn().mockReturnValue(false), isPlanEnabled: vi.fn().mockReturnValue(true),
getCheckpointingEnabled: vi.fn().mockReturnValue(false), getCheckpointingEnabled: vi.fn().mockReturnValue(false),
getGitService: vi.fn().mockResolvedValue({} as GitService), getGitService: vi.fn().mockResolvedValue({} as GitService),
waitForMcpInit: vi.fn(), waitForMcpInit: vi.fn(),
+6 -1
View File
@@ -92,7 +92,7 @@ describe('GeminiAgent Session Resume', () => {
getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'), getProjectTempDir: vi.fn().mockReturnValue('/tmp/project'),
}, },
getApprovalMode: vi.fn().mockReturnValue('default'), getApprovalMode: vi.fn().mockReturnValue('default'),
isPlanEnabled: vi.fn().mockReturnValue(false), isPlanEnabled: vi.fn().mockReturnValue(true),
getModel: vi.fn().mockReturnValue('gemini-pro'), getModel: vi.fn().mockReturnValue('gemini-pro'),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false), getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
getGemini31LaunchedSync: vi.fn().mockReturnValue(false), getGemini31LaunchedSync: vi.fn().mockReturnValue(false),
@@ -204,6 +204,11 @@ describe('GeminiAgent Session Resume', () => {
name: 'YOLO', name: 'YOLO',
description: 'Auto-approves all tools', description: 'Auto-approves all tools',
}, },
{
id: ApprovalMode.PLAN,
name: 'Plan',
description: 'Read-only mode',
},
], ],
currentModeId: ApprovalMode.DEFAULT, currentModeId: ApprovalMode.DEFAULT,
}, },
+2 -2
View File
@@ -2622,13 +2622,13 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT); expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT);
}); });
it('should throw error when --approval-mode=plan is used but experimental.plan setting is missing', async () => { it('should allow plan approval mode by default when --approval-mode=plan is used', async () => {
process.argv = ['node', 'script.js', '--approval-mode', 'plan']; process.argv = ['node', 'script.js', '--approval-mode', 'plan'];
const argv = await parseArguments(createTestMergedSettings()); const argv = await parseArguments(createTestMergedSettings());
const settings = createTestMergedSettings({}); const settings = createTestMergedSettings({});
const config = await loadCliConfig(settings, 'test-session', argv); const config = await loadCliConfig(settings, 'test-session', argv);
expect(config.getApprovalMode()).toBe(ApprovalMode.DEFAULT); expect(config.getApprovalMode()).toBe(ApprovalMode.PLAN);
}); });
it('should pass planSettings.directory from settings to config', async () => { it('should pass planSettings.directory from settings to config', async () => {
@@ -424,12 +424,10 @@ describe('SettingsSchema', () => {
expect(setting).toBeDefined(); expect(setting).toBeDefined();
expect(setting.type).toBe('boolean'); expect(setting.type).toBe('boolean');
expect(setting.category).toBe('Experimental'); expect(setting.category).toBe('Experimental');
expect(setting.default).toBe(false); expect(setting.default).toBe(true);
expect(setting.requiresRestart).toBe(true); expect(setting.requiresRestart).toBe(true);
expect(setting.showInDialog).toBe(true); expect(setting.showInDialog).toBe(true);
expect(setting.description).toBe( expect(setting.description).toBe('Enable Plan Mode.');
'Enable planning features (Plan Mode and tools).',
);
}); });
it('should have hooksConfig.notifications setting in schema', () => { it('should have hooksConfig.notifications setting in schema', () => {
+2 -2
View File
@@ -1823,8 +1823,8 @@ const SETTINGS_SCHEMA = {
label: 'Plan', label: 'Plan',
category: 'Experimental', category: 'Experimental',
requiresRestart: true, requiresRestart: true,
default: false, default: true,
description: 'Enable planning features (Plan Mode and tools).', description: 'Enable Plan Mode.',
showInDialog: true, showInDialog: true,
}, },
taskTracker: { taskTracker: {
@@ -151,7 +151,7 @@ describe('BuiltinCommandLoader', () => {
vi.clearAllMocks(); vi.clearAllMocks();
mockConfig = { mockConfig = {
getFolderTrust: vi.fn().mockReturnValue(true), getFolderTrust: vi.fn().mockReturnValue(true),
isPlanEnabled: vi.fn().mockReturnValue(false), isPlanEnabled: vi.fn().mockReturnValue(true),
getEnableExtensionReloading: () => false, getEnableExtensionReloading: () => false,
getEnableHooks: () => false, getEnableHooks: () => false,
getEnableHooksUI: () => false, getEnableHooksUI: () => false,
@@ -351,7 +351,7 @@ describe('BuiltinCommandLoader profile', () => {
vi.resetModules(); vi.resetModules();
mockConfig = { mockConfig = {
getFolderTrust: vi.fn().mockReturnValue(false), getFolderTrust: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(false), isPlanEnabled: vi.fn().mockReturnValue(true),
getCheckpointingEnabled: () => false, getCheckpointingEnabled: () => false,
getEnableExtensionReloading: () => false, getEnableExtensionReloading: () => false,
getEnableHooks: () => false, getEnableHooks: () => false,
@@ -231,7 +231,7 @@ const createMockConfig = (overrides = {}): Config =>
getDebugMode: vi.fn(() => false), getDebugMode: vi.fn(() => false),
getAccessibility: vi.fn(() => ({})), getAccessibility: vi.fn(() => ({})),
getMcpServers: vi.fn(() => ({})), getMcpServers: vi.fn(() => ({})),
isPlanEnabled: vi.fn(() => false), isPlanEnabled: vi.fn(() => true),
getToolRegistry: () => ({ getToolRegistry: () => ({
getTool: vi.fn(), getTool: vi.fn(),
}), }),
@@ -86,7 +86,7 @@ describe('useApprovalModeIndicator', () => {
(value: ApprovalMode) => void (value: ApprovalMode) => void
>, >,
isYoloModeDisabled: vi.fn().mockReturnValue(false), isYoloModeDisabled: vi.fn().mockReturnValue(false),
isPlanEnabled: vi.fn().mockReturnValue(false), isPlanEnabled: vi.fn().mockReturnValue(true),
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>, isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>, getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock< getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
+2 -2
View File
@@ -2788,9 +2788,9 @@ describe('Config Quota & Preview Model Access', () => {
}); });
describe('isPlanEnabled', () => { describe('isPlanEnabled', () => {
it('should return false by default', () => { it('should return true by default', () => {
const config = new Config(baseParams); const config = new Config(baseParams);
expect(config.isPlanEnabled()).toBe(false); expect(config.isPlanEnabled()).toBe(true);
}); });
it('should return true when plan is enabled', () => { it('should return true when plan is enabled', () => {
+1 -1
View File
@@ -885,7 +885,7 @@ export class Config implements McpContext {
this.enableAgents = params.enableAgents ?? false; this.enableAgents = params.enableAgents ?? false;
this.agents = params.agents ?? {}; this.agents = params.agents ?? {};
this.disableLLMCorrection = params.disableLLMCorrection ?? true; this.disableLLMCorrection = params.disableLLMCorrection ?? true;
this.planEnabled = params.plan ?? false; this.planEnabled = params.plan ?? true;
this.trackerEnabled = params.tracker ?? false; this.trackerEnabled = params.tracker ?? false;
this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true; this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true;
this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true; this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true;
+3 -3
View File
@@ -1712,9 +1712,9 @@
}, },
"plan": { "plan": {
"title": "Plan", "title": "Plan",
"description": "Enable planning features (Plan Mode and tools).", "description": "Enable Plan Mode.",
"markdownDescription": "Enable planning features (Plan Mode and tools).\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", "markdownDescription": "Enable Plan Mode.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`",
"default": false, "default": true,
"type": "boolean" "type": "boolean"
}, },
"taskTracker": { "taskTracker": {