mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 09:30:41 -07:00
feat(cli): add deep work mode with integrated execution flow
This commit is contained in:
@@ -1251,6 +1251,31 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
|
||||
});
|
||||
|
||||
it('should exclude all interactive tools in non-interactive mode with deep work approval mode', async () => {
|
||||
process.argv = [
|
||||
'node',
|
||||
'script.js',
|
||||
'--approval-mode',
|
||||
'deep_work',
|
||||
'-p',
|
||||
'test',
|
||||
];
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
deepWork: true,
|
||||
},
|
||||
});
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
|
||||
const excludedTools = config.getExcludeTools();
|
||||
expect(excludedTools).toContain(SHELL_TOOL_NAME);
|
||||
expect(excludedTools).toContain(EDIT_TOOL_NAME);
|
||||
expect(excludedTools).toContain(WRITE_FILE_TOOL_NAME);
|
||||
expect(excludedTools).toContain(ASK_USER_TOOL_NAME);
|
||||
});
|
||||
|
||||
it('should exclude only ask_user in non-interactive mode with legacy yolo flag', async () => {
|
||||
process.argv = ['node', 'script.js', '--yolo', '-p', 'test'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
@@ -1341,7 +1366,7 @@ describe('Approval mode tool exclusion logic', () => {
|
||||
await expect(
|
||||
loadCliConfig(settings, 'test-session', invalidArgv as CliArgs),
|
||||
).rejects.toThrow(
|
||||
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, default',
|
||||
'Invalid approval mode: invalid_mode. Valid values are: yolo, auto_edit, plan, deep_work, default',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -2534,6 +2559,18 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should set Deep Work approval mode when --approval-mode=deep_work is used and experimental.deepWork is enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'deep_work'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
deepWork: true,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.DEEP_WORK);
|
||||
});
|
||||
|
||||
it('should ignore "yolo" in settings.tools.approvalMode and fall back to DEFAULT', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
@@ -2571,6 +2608,20 @@ describe('loadCliConfig approval mode', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when --approval-mode=deep_work is used but experimental.deepWork is disabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--approval-mode', 'deep_work'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
experimental: {
|
||||
deepWork: false,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(loadCliConfig(settings, 'test-session', argv)).rejects.toThrow(
|
||||
'Approval mode "deep_work" is only available when experimental.deepWork is enabled.',
|
||||
);
|
||||
});
|
||||
|
||||
// --- Untrusted Folder Scenarios ---
|
||||
describe('when folder is NOT trusted', () => {
|
||||
beforeEach(() => {
|
||||
@@ -2671,6 +2722,19 @@ describe('loadCliConfig approval mode', () => {
|
||||
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.PLAN);
|
||||
});
|
||||
|
||||
it('should respect deep work mode from settings when experimental.deepWork is enabled', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: { defaultApprovalMode: 'deep_work' },
|
||||
experimental: { deepWork: true },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getApprovalMode()).toBe(
|
||||
ServerConfig.ApprovalMode.DEEP_WORK,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if plan mode is in settings but experimental.plan is disabled', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
@@ -2684,6 +2748,20 @@ describe('loadCliConfig approval mode', () => {
|
||||
'Approval mode "plan" is only available when experimental.plan is enabled.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if deep work mode is in settings but experimental.deepWork is disabled', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const settings = createTestMergedSettings({
|
||||
general: { defaultApprovalMode: 'deep_work' },
|
||||
experimental: { deepWork: false },
|
||||
});
|
||||
const argv = await parseArguments(settings);
|
||||
await expect(
|
||||
loadCliConfig(settings, 'test-session', argv),
|
||||
).rejects.toThrow(
|
||||
'Approval mode "deep_work" is only available when experimental.deepWork is enabled.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -155,9 +155,9 @@ export async function parseArguments(
|
||||
.option('approval-mode', {
|
||||
type: 'string',
|
||||
nargs: 1,
|
||||
choices: ['default', 'auto_edit', 'yolo', 'plan'],
|
||||
choices: ['default', 'auto_edit', 'yolo', 'plan', 'deep_work'],
|
||||
description:
|
||||
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools), plan (read-only mode)',
|
||||
'Set the approval mode: default (prompt for approval), auto_edit (auto-approve edit tools), yolo (auto-approve all tools), plan (read-only mode), deep_work (iterative execution mode)',
|
||||
})
|
||||
.option('policy', {
|
||||
type: 'array',
|
||||
@@ -560,12 +560,20 @@ export async function loadCliConfig(
|
||||
}
|
||||
approvalMode = ApprovalMode.PLAN;
|
||||
break;
|
||||
case 'deep_work':
|
||||
if (!(settings.experimental?.deepWork ?? false)) {
|
||||
throw new Error(
|
||||
'Approval mode "deep_work" is only available when experimental.deepWork is enabled.',
|
||||
);
|
||||
}
|
||||
approvalMode = ApprovalMode.DEEP_WORK;
|
||||
break;
|
||||
case 'default':
|
||||
approvalMode = ApprovalMode.DEFAULT;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, default`,
|
||||
`Invalid approval mode: ${rawApprovalMode}. Valid values are: yolo, auto_edit, plan, deep_work, default`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@@ -655,6 +663,10 @@ export async function loadCliConfig(
|
||||
// TODO(#16625): Replace this default exclusion logic with specific rules for plan mode.
|
||||
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
|
||||
break;
|
||||
case ApprovalMode.DEEP_WORK:
|
||||
// Deep Work still requires interactive confirmation for mutating tools.
|
||||
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
|
||||
break;
|
||||
case ApprovalMode.DEFAULT:
|
||||
// In default non-interactive mode, all tools that require approval are excluded.
|
||||
extraExcludes.push(...defaultExcludes.filter(toolExclusionFilter));
|
||||
@@ -810,6 +822,7 @@ export async function loadCliConfig(
|
||||
enableExtensionReloading: settings.experimental?.extensionReloading,
|
||||
enableAgents: settings.experimental?.enableAgents,
|
||||
plan: settings.experimental?.plan,
|
||||
deepWork: settings.experimental?.deepWork,
|
||||
enableEventDrivenScheduler: true,
|
||||
skillsSupport: settings.skills?.enabled ?? true,
|
||||
disabledSkills: settings.skills?.disabled,
|
||||
|
||||
@@ -496,7 +496,7 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.TOGGLE_COPY_MODE]: 'Toggle copy mode when in alternate buffer mode.',
|
||||
[Command.TOGGLE_YOLO]: 'Toggle YOLO (auto-approval) mode for tool calls.',
|
||||
[Command.CYCLE_APPROVAL_MODE]:
|
||||
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only).',
|
||||
'Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), plan (read-only), and deep_work (iterative; when enabled).',
|
||||
[Command.SHOW_MORE_LINES]:
|
||||
'Expand and collapse blocks of content when not in alternate buffer mode.',
|
||||
[Command.EXPAND_PASTE]:
|
||||
|
||||
@@ -390,6 +390,19 @@ describe('SettingsSchema', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should have deepWork setting in schema', () => {
|
||||
const setting = getSettingsSchema().experimental.properties.deepWork;
|
||||
expect(setting).toBeDefined();
|
||||
expect(setting.type).toBe('boolean');
|
||||
expect(setting.category).toBe('Experimental');
|
||||
expect(setting.default).toBe(false);
|
||||
expect(setting.requiresRestart).toBe(true);
|
||||
expect(setting.showInDialog).toBe(true);
|
||||
expect(setting.description).toBe(
|
||||
'Enable Deep Work mode (iterative execution mode and tools).',
|
||||
);
|
||||
});
|
||||
|
||||
it('should have hooksConfig.notifications setting in schema', () => {
|
||||
const setting = getSettingsSchema().hooksConfig?.properties.notifications;
|
||||
expect(setting).toBeDefined();
|
||||
|
||||
@@ -200,13 +200,15 @@ const SETTINGS_SCHEMA = {
|
||||
description: oneLine`
|
||||
The default approval mode for tool execution.
|
||||
'default' prompts for approval, 'auto_edit' auto-approves edit tools,
|
||||
and 'plan' is read-only mode. 'yolo' is not supported yet.
|
||||
'plan' is read-only mode, and 'deep_work' is iterative execution mode.
|
||||
'yolo' is not supported yet.
|
||||
`,
|
||||
showInDialog: true,
|
||||
options: [
|
||||
{ value: 'default', label: 'Default' },
|
||||
{ value: 'auto_edit', label: 'Auto Edit' },
|
||||
{ value: 'plan', label: 'Plan' },
|
||||
{ value: 'deep_work', label: 'Deep Work' },
|
||||
],
|
||||
},
|
||||
devtools: {
|
||||
@@ -1605,6 +1607,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable planning features (Plan Mode and tools).',
|
||||
showInDialog: true,
|
||||
},
|
||||
deepWork: {
|
||||
type: 'boolean',
|
||||
label: 'Deep Work',
|
||||
category: 'Experimental',
|
||||
requiresRestart: true,
|
||||
default: false,
|
||||
description:
|
||||
'Enable Deep Work mode (iterative execution mode and tools).',
|
||||
showInDialog: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -111,6 +111,16 @@ vi.mock('../ui/commands/planCommand.js', async () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock('../ui/commands/deepworkCommand.js', async () => {
|
||||
const { CommandKind } = await import('../ui/commands/types.js');
|
||||
return {
|
||||
deepworkCommand: {
|
||||
name: 'deepwork',
|
||||
description: 'Deep Work command',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../ui/commands/mcpCommand.js', () => ({
|
||||
mcpCommand: {
|
||||
@@ -130,6 +140,7 @@ describe('BuiltinCommandLoader', () => {
|
||||
mockConfig = {
|
||||
getFolderTrust: vi.fn().mockReturnValue(true),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isDeepWorkEnabled: vi.fn().mockReturnValue(false),
|
||||
getEnableExtensionReloading: () => false,
|
||||
getEnableHooks: () => false,
|
||||
getEnableHooksUI: () => false,
|
||||
@@ -247,6 +258,22 @@ describe('BuiltinCommandLoader', () => {
|
||||
expect(planCmd).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should include deepwork command when deep work mode is enabled', async () => {
|
||||
(mockConfig.isDeepWorkEnabled as Mock).mockReturnValue(true);
|
||||
const loader = new BuiltinCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
const deepworkCmd = commands.find((c) => c.name === 'deepwork');
|
||||
expect(deepworkCmd).toBeDefined();
|
||||
});
|
||||
|
||||
it('should exclude deepwork command when deep work mode is disabled', async () => {
|
||||
(mockConfig.isDeepWorkEnabled as Mock).mockReturnValue(false);
|
||||
const loader = new BuiltinCommandLoader(mockConfig);
|
||||
const commands = await loader.loadCommands(new AbortController().signal);
|
||||
const deepworkCmd = commands.find((c) => c.name === 'deepwork');
|
||||
expect(deepworkCmd).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should exclude agents command when agents are disabled', async () => {
|
||||
mockConfig.isAgentsEnabled = vi.fn().mockReturnValue(false);
|
||||
const loader = new BuiltinCommandLoader(mockConfig);
|
||||
@@ -288,6 +315,7 @@ describe('BuiltinCommandLoader profile', () => {
|
||||
mockConfig = {
|
||||
getFolderTrust: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isDeepWorkEnabled: vi.fn().mockReturnValue(false),
|
||||
getCheckpointingEnabled: () => false,
|
||||
getEnableExtensionReloading: () => false,
|
||||
getEnableHooks: () => false,
|
||||
|
||||
@@ -41,6 +41,7 @@ import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||
import { modelCommand } from '../ui/commands/modelCommand.js';
|
||||
import { oncallCommand } from '../ui/commands/oncallCommand.js';
|
||||
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
|
||||
import { deepworkCommand } from '../ui/commands/deepworkCommand.js';
|
||||
import { planCommand } from '../ui/commands/planCommand.js';
|
||||
import { policiesCommand } from '../ui/commands/policiesCommand.js';
|
||||
import { privacyCommand } from '../ui/commands/privacyCommand.js';
|
||||
@@ -146,6 +147,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
modelCommand,
|
||||
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
|
||||
...(this.config?.isPlanEnabled() ? [planCommand] : []),
|
||||
...(this.config?.isDeepWorkEnabled?.() ? [deepworkCommand] : []),
|
||||
policiesCommand,
|
||||
privacyCommand,
|
||||
...(isDevelopment ? [profileCommand] : []),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { deepworkCommand } from './deepworkCommand.js';
|
||||
import { type CommandContext } from './types.js';
|
||||
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
|
||||
import { ApprovalMode, coreEvents } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
coreEvents: {
|
||||
emitFeedback: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('deepworkCommand', () => {
|
||||
let mockContext: CommandContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
isDeepWorkEnabled: vi.fn(),
|
||||
setApprovalMode: vi.fn(),
|
||||
getApprovalMode: vi.fn(),
|
||||
getApprovedPlanPath: vi.fn(),
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
addItem: vi.fn(),
|
||||
},
|
||||
} as unknown as CommandContext);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have the correct name and description', () => {
|
||||
expect(deepworkCommand.name).toBe('deepwork');
|
||||
expect(deepworkCommand.description).toBe(
|
||||
'Switch to Deep Work mode for iterative execution',
|
||||
);
|
||||
});
|
||||
|
||||
it('should switch to deep work mode when enabled', async () => {
|
||||
vi.mocked(mockContext.services.config!.isDeepWorkEnabled).mockReturnValue(
|
||||
true,
|
||||
);
|
||||
vi.mocked(mockContext.services.config!.getApprovalMode).mockReturnValue(
|
||||
ApprovalMode.DEFAULT,
|
||||
);
|
||||
vi.mocked(mockContext.services.config!.getApprovedPlanPath).mockReturnValue(
|
||||
undefined,
|
||||
);
|
||||
|
||||
if (!deepworkCommand.action) throw new Error('Action missing');
|
||||
await deepworkCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.config!.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEEP_WORK,
|
||||
);
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Switched to Deep Work mode.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit error when deep work mode is disabled', async () => {
|
||||
vi.mocked(mockContext.services.config!.isDeepWorkEnabled).mockReturnValue(
|
||||
false,
|
||||
);
|
||||
|
||||
if (!deepworkCommand.action) throw new Error('Action missing');
|
||||
await deepworkCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.services.config!.setApprovalMode).not.toHaveBeenCalled();
|
||||
expect(coreEvents.emitFeedback).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Deep Work mode is disabled. Enable experimental.deepWork in settings first.',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { CommandKind, type SlashCommand } from './types.js';
|
||||
import { ApprovalMode, coreEvents, debugLogger } from '@google/gemini-cli-core';
|
||||
|
||||
export const deepworkCommand: SlashCommand = {
|
||||
name: 'deepwork',
|
||||
description: 'Switch to Deep Work mode for iterative execution',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const config = context.services.config;
|
||||
if (!config) {
|
||||
debugLogger.debug(
|
||||
'Deep Work command: config is not available in context',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.isDeepWorkEnabled()) {
|
||||
coreEvents.emitFeedback(
|
||||
'error',
|
||||
'Deep Work mode is disabled. Enable experimental.deepWork in settings first.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const previousApprovalMode = config.getApprovalMode();
|
||||
config.setApprovalMode(ApprovalMode.DEEP_WORK);
|
||||
|
||||
if (previousApprovalMode !== ApprovalMode.DEEP_WORK) {
|
||||
coreEvents.emitFeedback('info', 'Switched to Deep Work mode.');
|
||||
}
|
||||
|
||||
const approvedPlanPath = config.getApprovedPlanPath();
|
||||
if (approvedPlanPath) {
|
||||
coreEvents.emitFeedback(
|
||||
'info',
|
||||
`Deep Work will follow approved plan: ${approvedPlanPath}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -106,6 +106,12 @@ describe('policiesCommand', () => {
|
||||
expect(content).toContain(
|
||||
'### Yolo Mode Policies (combined with normal mode policies)',
|
||||
);
|
||||
expect(content).toContain(
|
||||
'### Plan Mode Policies (combined with normal mode policies)',
|
||||
);
|
||||
expect(content).toContain(
|
||||
'### Deep Work Mode Policies (combined with normal mode policies)',
|
||||
);
|
||||
expect(content).toContain(
|
||||
'**DENY** tool: `dangerousTool` [Priority: 10]',
|
||||
);
|
||||
|
||||
@@ -12,6 +12,8 @@ interface CategorizedRules {
|
||||
normal: PolicyRule[];
|
||||
autoEdit: PolicyRule[];
|
||||
yolo: PolicyRule[];
|
||||
plan: PolicyRule[];
|
||||
deepWork: PolicyRule[];
|
||||
}
|
||||
|
||||
const categorizeRulesByMode = (
|
||||
@@ -21,6 +23,8 @@ const categorizeRulesByMode = (
|
||||
normal: [],
|
||||
autoEdit: [],
|
||||
yolo: [],
|
||||
plan: [],
|
||||
deepWork: [],
|
||||
};
|
||||
const ALL_MODES = Object.values(ApprovalMode);
|
||||
rules.forEach((rule) => {
|
||||
@@ -29,6 +33,8 @@ const categorizeRulesByMode = (
|
||||
if (modeSet.has(ApprovalMode.DEFAULT)) result.normal.push(rule);
|
||||
if (modeSet.has(ApprovalMode.AUTO_EDIT)) result.autoEdit.push(rule);
|
||||
if (modeSet.has(ApprovalMode.YOLO)) result.yolo.push(rule);
|
||||
if (modeSet.has(ApprovalMode.PLAN)) result.plan.push(rule);
|
||||
if (modeSet.has(ApprovalMode.DEEP_WORK)) result.deepWork.push(rule);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
@@ -82,6 +88,12 @@ const listPoliciesCommand: SlashCommand = {
|
||||
const uniqueYolo = categorized.yolo.filter(
|
||||
(rule) => !normalRulesSet.has(rule),
|
||||
);
|
||||
const uniquePlan = categorized.plan.filter(
|
||||
(rule) => !normalRulesSet.has(rule),
|
||||
);
|
||||
const uniqueDeepWork = categorized.deepWork.filter(
|
||||
(rule) => !normalRulesSet.has(rule),
|
||||
);
|
||||
|
||||
let content = '**Active Policies**\n\n';
|
||||
content += formatSection('Normal Mode Policies', categorized.normal);
|
||||
@@ -93,6 +105,14 @@ const listPoliciesCommand: SlashCommand = {
|
||||
'Yolo Mode Policies (combined with normal mode policies)',
|
||||
uniqueYolo,
|
||||
);
|
||||
content += formatSection(
|
||||
'Plan Mode Policies (combined with normal mode policies)',
|
||||
uniquePlan,
|
||||
);
|
||||
content += formatSection(
|
||||
'Deep Work Mode Policies (combined with normal mode policies)',
|
||||
uniqueDeepWork,
|
||||
);
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
|
||||
@@ -40,6 +40,27 @@ describe('ApprovalModeIndicator', () => {
|
||||
expect(output).toContain('shift+tab to accept edits');
|
||||
});
|
||||
|
||||
it('renders correctly for PLAN mode with deep work enabled', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.PLAN}
|
||||
isDeepWorkEnabled={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('plan');
|
||||
expect(output).toContain('shift+tab to deep work');
|
||||
});
|
||||
|
||||
it('renders correctly for DEEP_WORK mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.DEEP_WORK} />,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('deep work');
|
||||
expect(output).toContain('shift+tab to accept edits');
|
||||
});
|
||||
|
||||
it('renders correctly for YOLO mode', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator approvalMode={ApprovalMode.YOLO} />,
|
||||
@@ -67,4 +88,15 @@ describe('ApprovalModeIndicator', () => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('shift+tab to plan');
|
||||
});
|
||||
|
||||
it('renders correctly for DEFAULT mode with deep work enabled', () => {
|
||||
const { lastFrame } = render(
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={ApprovalMode.DEFAULT}
|
||||
isDeepWorkEnabled={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('shift+tab to deep work');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,11 +12,13 @@ import { ApprovalMode } from '@google/gemini-cli-core';
|
||||
interface ApprovalModeIndicatorProps {
|
||||
approvalMode: ApprovalMode;
|
||||
isPlanEnabled?: boolean;
|
||||
isDeepWorkEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||
approvalMode,
|
||||
isPlanEnabled,
|
||||
isDeepWorkEnabled,
|
||||
}) => {
|
||||
let textColor = '';
|
||||
let textContent = '';
|
||||
@@ -31,6 +33,13 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||
case ApprovalMode.PLAN:
|
||||
textColor = theme.status.success;
|
||||
textContent = 'plan';
|
||||
subText = isDeepWorkEnabled
|
||||
? 'shift+tab to deep work'
|
||||
: 'shift+tab to accept edits';
|
||||
break;
|
||||
case ApprovalMode.DEEP_WORK:
|
||||
textColor = theme.status.success;
|
||||
textContent = 'deep work';
|
||||
subText = 'shift+tab to accept edits';
|
||||
break;
|
||||
case ApprovalMode.YOLO:
|
||||
@@ -44,7 +53,9 @@ export const ApprovalModeIndicator: React.FC<ApprovalModeIndicatorProps> = ({
|
||||
textContent = '';
|
||||
subText = isPlanEnabled
|
||||
? 'shift+tab to plan'
|
||||
: 'shift+tab to accept edits';
|
||||
: isDeepWorkEnabled
|
||||
? 'shift+tab to deep work'
|
||||
: 'shift+tab to accept edits';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -586,6 +586,7 @@ describe('Composer', () => {
|
||||
[ApprovalMode.DEFAULT],
|
||||
[ApprovalMode.AUTO_EDIT],
|
||||
[ApprovalMode.PLAN],
|
||||
[ApprovalMode.DEEP_WORK],
|
||||
[ApprovalMode.YOLO],
|
||||
])(
|
||||
'shows ApprovalModeIndicator when approval mode is %s and shell mode is inactive',
|
||||
@@ -634,6 +635,7 @@ describe('Composer', () => {
|
||||
it.each([
|
||||
[ApprovalMode.YOLO, 'YOLO'],
|
||||
[ApprovalMode.PLAN, 'plan'],
|
||||
[ApprovalMode.DEEP_WORK, 'deep work'],
|
||||
[ApprovalMode.AUTO_EDIT, 'auto edit'],
|
||||
])(
|
||||
'shows minimal mode badge "%s" when clean UI details are hidden',
|
||||
|
||||
@@ -117,9 +117,11 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
? { text: 'YOLO', color: theme.status.error }
|
||||
: showApprovalModeIndicator === ApprovalMode.PLAN
|
||||
? { text: 'plan', color: theme.status.success }
|
||||
: showApprovalModeIndicator === ApprovalMode.AUTO_EDIT
|
||||
? { text: 'auto edit', color: theme.status.warning }
|
||||
: null;
|
||||
: showApprovalModeIndicator === ApprovalMode.DEEP_WORK
|
||||
? { text: 'deep work', color: theme.status.success }
|
||||
: showApprovalModeIndicator === ApprovalMode.AUTO_EDIT
|
||||
? { text: 'auto edit', color: theme.status.warning }
|
||||
: null;
|
||||
const hideMinimalModeHintWhileBusy =
|
||||
!showUiDetails && (showLoadingIndicator || hasPendingActionRequired);
|
||||
const minimalModeBleedThrough = hideMinimalModeHintWhileBusy
|
||||
@@ -335,6 +337,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
<ApprovalModeIndicator
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
isPlanEnabled={config.isPlanEnabled()}
|
||||
isDeepWorkEnabled={
|
||||
config.isDeepWorkEnabled?.() ?? false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{uiState.shellModeActive && (
|
||||
|
||||
@@ -137,10 +137,16 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
const renderDialog = (options?: { useAlternateBuffer?: boolean }) =>
|
||||
const renderDialog = (options?: {
|
||||
useAlternateBuffer?: boolean;
|
||||
deepWorkEnabled?: boolean;
|
||||
recommendedApprovalMode?: ApprovalMode;
|
||||
}) =>
|
||||
renderWithProviders(
|
||||
<ExitPlanModeDialog
|
||||
planPath={mockPlanFullPath}
|
||||
deepWorkEnabled={options?.deepWorkEnabled}
|
||||
recommendedApprovalMode={options?.recommendedApprovalMode}
|
||||
onApprove={onApprove}
|
||||
onFeedback={onFeedback}
|
||||
onCancel={onCancel}
|
||||
@@ -208,6 +214,29 @@ Implement a comprehensive authentication system with multiple providers.
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onApprove with DEEP_WORK when deep work is recommended and selected first', async () => {
|
||||
const { stdin, lastFrame } = renderDialog({
|
||||
useAlternateBuffer,
|
||||
deepWorkEnabled: true,
|
||||
recommendedApprovalMode: ApprovalMode.DEEP_WORK,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
vi.runAllTimers();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Add user authentication');
|
||||
expect(lastFrame()).toContain('Deep Work');
|
||||
});
|
||||
|
||||
writeKey(stdin, '\r');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEEP_WORK);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onApprove with DEFAULT when second option is selected', async () => {
|
||||
const { stdin, lastFrame } = renderDialog({ useAlternateBuffer });
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ import { AskUserDialog } from './AskUserDialog.js';
|
||||
|
||||
export interface ExitPlanModeDialogProps {
|
||||
planPath: string;
|
||||
recommendedApprovalMode?: ApprovalMode;
|
||||
recommendationReason?: string;
|
||||
deepWorkEnabled?: boolean;
|
||||
onApprove: (approvalMode: ApprovalMode) => void;
|
||||
onFeedback: (feedback: string) => void;
|
||||
onCancel: () => void;
|
||||
@@ -41,10 +44,53 @@ interface PlanContentState {
|
||||
}
|
||||
|
||||
enum ApprovalOption {
|
||||
DeepWork = 'Yes, start Deep Work execution',
|
||||
Auto = 'Yes, automatically accept edits',
|
||||
Manual = 'Yes, manually accept edits',
|
||||
}
|
||||
|
||||
const DEEP_WORK_SIGNALS = [
|
||||
'iterate',
|
||||
'iteration',
|
||||
'loop',
|
||||
'phases',
|
||||
'phase',
|
||||
'refactor',
|
||||
'migrate',
|
||||
'end-to-end',
|
||||
'e2e',
|
||||
'comprehensive',
|
||||
'cross-cutting',
|
||||
'multi-step',
|
||||
'verification',
|
||||
'test suite',
|
||||
];
|
||||
|
||||
function recommendApprovalModeFromPlan(
|
||||
planContent: string,
|
||||
deepWorkEnabled: boolean,
|
||||
): ApprovalMode {
|
||||
if (!deepWorkEnabled) {
|
||||
return ApprovalMode.AUTO_EDIT;
|
||||
}
|
||||
|
||||
const normalized = planContent.toLowerCase();
|
||||
const stepCount = (normalized.match(/^\s*\d+\.\s+/gm) ?? []).length;
|
||||
const signalCount = DEEP_WORK_SIGNALS.filter((signal) =>
|
||||
normalized.includes(signal),
|
||||
).length;
|
||||
|
||||
if (
|
||||
stepCount >= 6 ||
|
||||
(stepCount >= 4 && signalCount >= 2) ||
|
||||
signalCount >= 4
|
||||
) {
|
||||
return ApprovalMode.DEEP_WORK;
|
||||
}
|
||||
|
||||
return ApprovalMode.AUTO_EDIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* A tiny component for loading and error states with consistent styling.
|
||||
*/
|
||||
@@ -127,6 +173,9 @@ function usePlanContent(planPath: string, config: Config): PlanContentState {
|
||||
|
||||
export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
|
||||
planPath,
|
||||
recommendedApprovalMode,
|
||||
recommendationReason,
|
||||
deepWorkEnabled = false,
|
||||
onApprove,
|
||||
onFeedback,
|
||||
onCancel,
|
||||
@@ -183,6 +232,72 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const computedRecommendation = recommendApprovalModeFromPlan(
|
||||
planContent,
|
||||
deepWorkEnabled,
|
||||
);
|
||||
const effectiveRecommendation =
|
||||
recommendedApprovalMode === ApprovalMode.DEEP_WORK && deepWorkEnabled
|
||||
? ApprovalMode.DEEP_WORK
|
||||
: recommendedApprovalMode === ApprovalMode.AUTO_EDIT
|
||||
? ApprovalMode.AUTO_EDIT
|
||||
: computedRecommendation;
|
||||
|
||||
const approvalOptions = deepWorkEnabled
|
||||
? effectiveRecommendation === ApprovalMode.DEEP_WORK
|
||||
? [
|
||||
{
|
||||
label: `${ApprovalOption.DeepWork} (Recommended)`,
|
||||
description:
|
||||
'Approves plan and uses iterative Deep Work execution with readiness checks.',
|
||||
},
|
||||
{
|
||||
label: ApprovalOption.Auto,
|
||||
description:
|
||||
'Approves plan and runs regular implementation with automatic edits.',
|
||||
},
|
||||
{
|
||||
label: ApprovalOption.Manual,
|
||||
description:
|
||||
'Approves plan but requires confirmation before each tool call.',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: `${ApprovalOption.Auto} (Recommended)`,
|
||||
description:
|
||||
'Approves plan and runs regular implementation with automatic edits.',
|
||||
},
|
||||
{
|
||||
label: ApprovalOption.DeepWork,
|
||||
description:
|
||||
'Approves plan and uses iterative Deep Work execution with readiness checks.',
|
||||
},
|
||||
{
|
||||
label: ApprovalOption.Manual,
|
||||
description:
|
||||
'Approves plan but requires confirmation before each tool call.',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: ApprovalOption.Auto,
|
||||
description: 'Approves plan and allows tools to run automatically.',
|
||||
},
|
||||
{
|
||||
label: ApprovalOption.Manual,
|
||||
description: 'Approves plan but requires confirmation for each tool.',
|
||||
},
|
||||
];
|
||||
|
||||
const recommendationText =
|
||||
recommendationReason && recommendationReason.trim().length > 0
|
||||
? recommendationReason.trim()
|
||||
: effectiveRecommendation === ApprovalMode.DEEP_WORK
|
||||
? 'Recommendation: Deep Work execution for iterative implementation.'
|
||||
: 'Recommendation: Regular execution.';
|
||||
const promptWithRecommendation = `${recommendationText}\n\n${planContent}`;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<AskUserDialog
|
||||
@@ -190,28 +305,19 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
|
||||
{
|
||||
type: QuestionType.CHOICE,
|
||||
header: 'Approval',
|
||||
question: planContent,
|
||||
options: [
|
||||
{
|
||||
label: ApprovalOption.Auto,
|
||||
description:
|
||||
'Approves plan and allows tools to run automatically',
|
||||
},
|
||||
{
|
||||
label: ApprovalOption.Manual,
|
||||
description:
|
||||
'Approves plan but requires confirmation for each tool',
|
||||
},
|
||||
],
|
||||
question: promptWithRecommendation,
|
||||
options: approvalOptions,
|
||||
placeholder: 'Type your feedback...',
|
||||
multiSelect: false,
|
||||
},
|
||||
]}
|
||||
onSubmit={(answers) => {
|
||||
const answer = answers['0'];
|
||||
if (answer === ApprovalOption.Auto) {
|
||||
if (answer?.startsWith(ApprovalOption.DeepWork)) {
|
||||
onApprove(ApprovalMode.DEEP_WORK);
|
||||
} else if (answer?.startsWith(ApprovalOption.Auto)) {
|
||||
onApprove(ApprovalMode.AUTO_EDIT);
|
||||
} else if (answer === ApprovalOption.Manual) {
|
||||
} else if (answer?.startsWith(ApprovalOption.Manual)) {
|
||||
onApprove(ApprovalMode.DEFAULT);
|
||||
} else if (answer) {
|
||||
onFeedback(answer);
|
||||
|
||||
@@ -1378,6 +1378,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
!shellModeActive && approvalMode === ApprovalMode.YOLO;
|
||||
const showPlanStyling =
|
||||
!shellModeActive && approvalMode === ApprovalMode.PLAN;
|
||||
const showDeepWorkStyling =
|
||||
!shellModeActive && approvalMode === ApprovalMode.DEEP_WORK;
|
||||
|
||||
let statusColor: string | undefined;
|
||||
let statusText = '';
|
||||
@@ -1390,6 +1392,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
} else if (showPlanStyling) {
|
||||
statusColor = theme.status.success;
|
||||
statusText = 'Plan mode';
|
||||
} else if (showDeepWorkStyling) {
|
||||
statusColor = theme.status.success;
|
||||
statusText = 'Deep Work mode';
|
||||
} else if (showAutoAcceptStyling) {
|
||||
statusColor = theme.status.warning;
|
||||
statusText = 'Accepting edits';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
@@ -13,21 +15,21 @@ Implementation Steps
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
... last 3 lines hidden ...
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
● 3. Type your feedback...
|
||||
|
||||
Enter to submit · Esc to cancel"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
@@ -39,14 +41,12 @@ Implementation Steps
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
... last 3 lines hidden ...
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
● 3. Add tests
|
||||
|
||||
Enter to submit · Esc to cancel"
|
||||
@@ -55,7 +55,9 @@ Enter to submit · Esc to cancel"
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = `" Error reading plan: File not found"`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > handles long plan content appropriately 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Implement a comprehensive authentication system with multiple providers.
|
||||
|
||||
@@ -67,21 +69,21 @@ Implementation Steps
|
||||
4. Add OAuth2 provider support in src/auth/providers/OAuth2Provider.ts
|
||||
5. Add SAML provider support in src/auth/providers/SAMLProvider.ts
|
||||
6. Add LDAP provider support in src/auth/providers/LDAPProvider.ts
|
||||
7. Create token refresh mechanism in src/auth/TokenManager.ts
|
||||
8. Add multi-factor authentication in src/auth/MFAService.ts
|
||||
... last 22 lines hidden ...
|
||||
... last 24 lines hidden ...
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
3. Type your feedback...
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: false > renders correctly with plan content 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
@@ -93,21 +95,21 @@ Implementation Steps
|
||||
4. Add tests in src/auth/__tests__/
|
||||
|
||||
Files to Modify
|
||||
|
||||
- src/index.ts - Add auth middleware
|
||||
- src/config.ts - Add auth configuration options
|
||||
... last 3 lines hidden ...
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
3. Type your feedback...
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
@@ -124,16 +126,18 @@ Files to Modify
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
● 3. Type your feedback...
|
||||
|
||||
Enter to submit · Esc to cancel"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
@@ -150,9 +154,9 @@ Files to Modify
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
● 3. Add tests
|
||||
|
||||
Enter to submit · Esc to cancel"
|
||||
@@ -161,7 +165,9 @@ Enter to submit · Esc to cancel"
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = `" Error reading plan: File not found"`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > handles long plan content appropriately 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Implement a comprehensive authentication system with multiple providers.
|
||||
|
||||
@@ -199,16 +205,18 @@ Testing Strategy
|
||||
- Load testing for session management
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
3. Type your feedback...
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
||||
`;
|
||||
|
||||
exports[`ExitPlanModeDialog > useAlternateBuffer: true > renders correctly with plan content 1`] = `
|
||||
"Overview
|
||||
"Recommendation: Regular execution.
|
||||
|
||||
Overview
|
||||
|
||||
Add user authentication to the CLI application.
|
||||
|
||||
@@ -225,9 +233,9 @@ Files to Modify
|
||||
- src/config.ts - Add auth configuration options
|
||||
|
||||
● 1. Yes, automatically accept edits
|
||||
Approves plan and allows tools to run automatically
|
||||
Approves plan and allows tools to run automatically.
|
||||
2. Yes, manually accept edits
|
||||
Approves plan but requires confirmation for each tool
|
||||
Approves plan but requires confirmation for each tool.
|
||||
3. Type your feedback...
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel"
|
||||
|
||||
@@ -55,12 +55,14 @@ exports[`ToolConfirmationQueue > renders ExitPlanMode tool confirmation with Suc
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Ready to start implementation? │
|
||||
│ │
|
||||
│ Recommendation: Regular execution. │
|
||||
│ │
|
||||
│ Plan content goes here │
|
||||
│ │
|
||||
│ ● 1. Yes, automatically accept edits │
|
||||
│ Approves plan and allows tools to run automatically │
|
||||
│ Approves plan and allows tools to run automatically. │
|
||||
│ 2. Yes, manually accept edits │
|
||||
│ Approves plan but requires confirmation for each tool │
|
||||
│ Approves plan but requires confirmation for each tool. │
|
||||
│ 3. Type your feedback... │
|
||||
│ │
|
||||
│ Enter to select · ↑/↓ to navigate · Esc to cancel │
|
||||
|
||||
@@ -281,6 +281,9 @@ export const ToolConfirmationMessage: React.FC<
|
||||
bodyContent = (
|
||||
<ExitPlanModeDialog
|
||||
planPath={confirmationDetails.planPath}
|
||||
recommendedApprovalMode={confirmationDetails.recommendedApprovalMode}
|
||||
recommendationReason={confirmationDetails.recommendationReason}
|
||||
deepWorkEnabled={confirmationDetails.deepWorkEnabled}
|
||||
onApprove={(approvalMode) => {
|
||||
handleConfirm(ToolConfirmationOutcome.ProceedOnce, {
|
||||
approved: true,
|
||||
|
||||
@@ -90,7 +90,7 @@ export const INFORMATIVE_TIPS = [
|
||||
'Toggle the todo list display with Ctrl+T…',
|
||||
'See full, untruncated responses with Ctrl+O…',
|
||||
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…',
|
||||
'Cycle through approval modes (Default, Auto-Edit, Plan) with Shift+Tab…',
|
||||
'Cycle through approval modes (Default, Auto-Edit, Plan, Deep Work) with Shift+Tab…',
|
||||
'Toggle Markdown rendering (raw markdown mode) with Alt+M…',
|
||||
'Toggle shell mode by typing ! in an empty prompt…',
|
||||
'Insert a newline with a backslash (\\) followed by Enter…',
|
||||
|
||||
@@ -41,6 +41,7 @@ interface MockConfigInstanceShape {
|
||||
setApprovalMode: Mock<(value: ApprovalMode) => void>;
|
||||
isYoloModeDisabled: Mock<() => boolean>;
|
||||
isPlanEnabled: Mock<() => boolean>;
|
||||
isDeepWorkEnabled?: Mock<() => boolean>;
|
||||
isTrustedFolder: Mock<() => boolean>;
|
||||
getCoreTools: Mock<() => string[]>;
|
||||
getToolDiscoveryCommand: Mock<() => string | undefined>;
|
||||
@@ -87,6 +88,7 @@ describe('useApprovalModeIndicator', () => {
|
||||
>,
|
||||
isYoloModeDisabled: vi.fn().mockReturnValue(false),
|
||||
isPlanEnabled: vi.fn().mockReturnValue(false),
|
||||
isDeepWorkEnabled: vi.fn().mockReturnValue(false),
|
||||
isTrustedFolder: vi.fn().mockReturnValue(true) as Mock<() => boolean>,
|
||||
getCoreTools: vi.fn().mockReturnValue([]) as Mock<() => string[]>,
|
||||
getToolDiscoveryCommand: vi.fn().mockReturnValue(undefined) as Mock<
|
||||
@@ -271,6 +273,32 @@ describe('useApprovalModeIndicator', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should cycle through DEFAULT -> DEEP_WORK -> AUTO_EDIT when deep work is enabled and plan is disabled', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
mockConfigInstance.isPlanEnabled.mockReturnValue(false);
|
||||
mockConfigInstance.isDeepWorkEnabled?.mockReturnValue(true);
|
||||
renderHook(() =>
|
||||
useApprovalModeIndicator({
|
||||
config: mockConfigInstance as unknown as ActualConfigType,
|
||||
addItem: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.DEEP_WORK,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({ name: 'tab', shift: true } as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.AUTO_EDIT,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not toggle if only one key or other keys combinations are pressed', () => {
|
||||
mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);
|
||||
renderHook(() =>
|
||||
|
||||
@@ -70,13 +70,24 @@ export function useApprovalModeIndicator({
|
||||
: ApprovalMode.YOLO;
|
||||
} else if (keyMatchers[Command.CYCLE_APPROVAL_MODE](key)) {
|
||||
const currentMode = config.getApprovalMode();
|
||||
const deepWorkEnabled =
|
||||
typeof config.isDeepWorkEnabled === 'function'
|
||||
? config.isDeepWorkEnabled()
|
||||
: false;
|
||||
switch (currentMode) {
|
||||
case ApprovalMode.DEFAULT:
|
||||
nextApprovalMode = config.isPlanEnabled()
|
||||
? ApprovalMode.PLAN
|
||||
: ApprovalMode.AUTO_EDIT;
|
||||
: deepWorkEnabled
|
||||
? ApprovalMode.DEEP_WORK
|
||||
: ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case ApprovalMode.PLAN:
|
||||
nextApprovalMode = deepWorkEnabled
|
||||
? ApprovalMode.DEEP_WORK
|
||||
: ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case ApprovalMode.DEEP_WORK:
|
||||
nextApprovalMode = ApprovalMode.AUTO_EDIT;
|
||||
break;
|
||||
case ApprovalMode.AUTO_EDIT:
|
||||
|
||||
Reference in New Issue
Block a user