feat(cli): add deep work mode with integrated execution flow

This commit is contained in:
Dmitry Lyalin
2026-02-12 23:21:11 -05:00
parent d82f66973f
commit c57b55fa03
46 changed files with 2348 additions and 109 deletions
+79 -1
View File
@@ -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.',
);
});
});
});
+16 -3
View File
@@ -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,
+1 -1
View File
@@ -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();
+13 -1
View File
@@ -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',
+8 -3
View File
@@ -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,
+1 -1
View File
@@ -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: