From ab23cdc6af5f0f4b50b58e89ca2eacf64e443c1f Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Thu, 5 Mar 2026 16:42:55 -0500 Subject: [PATCH] update to a dialog for clearing context --- packages/cli/src/config/settingsSchema.ts | 10 + packages/cli/src/ui/AppContainer.tsx | 16 +- .../cli/src/ui/components/AskUserDialog.tsx | 13 +- .../ui/components/ExitPlanModeDialog.test.tsx | 51 ++++- .../src/ui/components/ExitPlanModeDialog.tsx | 209 ++++++++++++------ .../ExitPlanModeDialog.test.tsx.snap | 68 ++---- packages/core/src/config/config.ts | 21 ++ packages/core/src/confirmation-bus/types.ts | 2 + 8 files changed, 264 insertions(+), 126 deletions(-) diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index fb0520d334..04d1bda8e1 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -299,6 +299,16 @@ const SETTINGS_SCHEMA = { 'Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase.', showInDialog: true, }, + clearContextOnApproval: { + type: 'boolean', + label: 'Clear Context on Plan Approval', + category: 'General', + requiresRestart: false, + default: undefined as boolean | undefined, + description: + 'Automatically clear conversation context after a plan is approved and implementation begins.', + showInDialog: true, + }, }, }, retryFetchErrors: { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 5f7b1893cb..5243a1984d 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -399,12 +399,23 @@ export const AppContainer = (props: AppContainerProps) => { const [isConfigInitialized, setConfigInitialized] = useState(false); const [planModeUIHistoryStartIndex, setPlanModeUIHistoryStartIndex] = - useState(null); + useState(() => + // Initialize if starting in PLAN mode (e.g. session resume) + config.getApprovalMode() === ApprovalMode.PLAN ? 0 : null + ); useEffect(() => { const handleApprovalModeChanged = ({ mode }: { mode: ApprovalMode }) => { if (mode === ApprovalMode.PLAN) { - setPlanModeUIHistoryStartIndex(historyManager.history.length); + // Only set the start index if we aren't already tracking one. + // This ensures that if we are already in PLAN mode and another + // event fires, we don't accidentally move the start index forward. + setPlanModeUIHistoryStartIndex((prev) => + prev === null ? historyManager.history.length : prev, + ); + } else { + // Reset the index when leaving PLAN mode + setPlanModeUIHistoryStartIndex(null); } }; coreEvents.on(CoreEvent.ApprovalModeChanged, handleApprovalModeChanged); @@ -1367,6 +1378,7 @@ Logging in with Google... Restarting Gemini CLI to continue. planModeUIHistoryStartIndex, ); historyManager.loadHistory(newHistory); + setPlanModeUIHistoryStartIndex(null); refreshStatic(); } }, [planModeUIHistoryStartIndex, historyManager, refreshStatic]); diff --git a/packages/cli/src/ui/components/AskUserDialog.tsx b/packages/cli/src/ui/components/AskUserDialog.tsx index 9606513510..9b2747aa95 100644 --- a/packages/cli/src/ui/components/AskUserDialog.tsx +++ b/packages/cli/src/ui/components/AskUserDialog.tsx @@ -689,8 +689,9 @@ const ChoiceQuestionView: React.FC = ({ }, ); - // Only add custom option for choice type, not yesno - if (question.type !== 'yesno') { + // Add custom option for choice type if allowed + const allowCustom = question.allowCustomOption ?? true; + if (question.type === 'choice' && allowCustom) { const otherItem: OptionItem = { key: 'other', label: customOptionText || '', @@ -713,7 +714,13 @@ const ChoiceQuestionView: React.FC = ({ } return list; - }, [questionOptions, question.multiSelect, question.type, customOptionText]); + }, [ + questionOptions, + question.allowCustomOption, + question.type, + question.multiSelect, + customOptionText, + ]); const handleHighlight = useCallback( (itemValue: OptionItem) => { diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx index 262f4e635d..6ff671a454 100644 --- a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx @@ -163,6 +163,8 @@ Implement a comprehensive authentication system with multiple providers. writeTextFile: vi.fn(), }), getUseAlternateBuffer: () => options?.useAlternateBuffer ?? true, + getClearContextOnPlanApproval: () => undefined, + setClearContextOnPlanApprovalSessionOverride: vi.fn(), } as unknown as import('@google/gemini-cli-core').Config, }, ); @@ -206,6 +208,16 @@ Implement a comprehensive authentication system with multiple providers. writeKey(stdin, '\r'); + await waitFor(() => { + expect(lastFrame()).toContain('Clear conversation context'); + }); + + // Select 'No' option (index 3) + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); + await waitFor(() => { expect(onApprove).toHaveBeenCalledWith(ApprovalMode.AUTO_EDIT, false); }); @@ -222,6 +234,15 @@ Implement a comprehensive authentication system with multiple providers. expect(lastFrame()).toContain('Add user authentication'); }); + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); + + await waitFor(() => { + expect(lastFrame()).toContain('Clear conversation context'); + }); + + // Select 'No' option (index 3) + writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\r'); @@ -350,8 +371,15 @@ Implement a comprehensive authentication system with multiple providers. expect(lastFrame()).toContain('Add user authentication'); }); - // Press '3' to select third option directly - writeKey(stdin, '3'); + // Press '2' to select second option directly (Manual) + writeKey(stdin, '2'); + + await waitFor(() => { + expect(lastFrame()).toContain('Clear conversation context'); + }); + + // Press '4' to select fourth option directly (No) + writeKey(stdin, '4'); await waitFor(() => { expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT, false); @@ -372,8 +400,6 @@ Implement a comprehensive authentication system with multiple providers. // Navigate to feedback option and start typing writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\x1b[B'); // Down arrow - writeKey(stdin, '\x1b[B'); // Down arrow - writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\r'); // Select to focus input // Type some feedback @@ -443,6 +469,8 @@ Implement a comprehensive authentication system with multiple providers. writeTextFile: vi.fn(), }), getUseAlternateBuffer: () => useAlternateBuffer ?? true, + getClearContextOnPlanApproval: () => undefined, + setClearContextOnPlanApprovalSessionOverride: vi.fn(), } as unknown as import('@google/gemini-cli-core').Config, }, ); @@ -499,8 +527,6 @@ Implement a comprehensive authentication system with multiple providers. // Focus feedback option writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\x1b[B'); // Down arrow - writeKey(stdin, '\x1b[B'); // Down arrow - writeKey(stdin, '\x1b[B'); // Down arrow // Press Enter without typing anything writeKey(stdin, '\r'); @@ -528,8 +554,6 @@ Implement a comprehensive authentication system with multiple providers. // Navigate to feedback option and start typing writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\x1b[B'); // Down arrow - writeKey(stdin, '\x1b[B'); // Down arrow - writeKey(stdin, '\x1b[B'); // Down arrow // Type some feedback for (const char of 'test') { @@ -538,11 +562,20 @@ Implement a comprehensive authentication system with multiple providers. // Now use up arrow to navigate back to a different option writeKey(stdin, '\x1b[A'); // Up arrow - writeKey(stdin, '\x1b[A'); // Up arrow // Press Enter to select the manually accept edits option writeKey(stdin, '\r'); + await waitFor(() => { + expect(lastFrame()).toContain('Clear conversation context'); + }); + + // Select 'No' option (index 3) + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); + await waitFor(() => { expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT, false); }); diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.tsx index d1dbdba627..1be6163ebd 100644 --- a/packages/cli/src/ui/components/ExitPlanModeDialog.tsx +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.tsx @@ -19,6 +19,8 @@ import { } from '@google/gemini-cli-core'; import { theme } from '../semantic-colors.js'; import { useConfig } from '../contexts/ConfigContext.js'; +import { useSettingsStore } from '../contexts/SettingsContext.js'; +import { SettingScope } from '../../config/settings.js'; import { AskUserDialog } from './AskUserDialog.js'; import { openFileInEditor } from '../utils/editorUtils.js'; import { useKeypress } from '../hooks/useKeypress.js'; @@ -41,6 +43,11 @@ enum PlanStatus { Error = 'error', } +enum ApprovalStep { + PLAN_APPROVAL = 'plan_approval', + CONTEXT_CHOICE = 'context_choice', +} + interface PlanContentState { status: PlanStatus; content?: string; @@ -50,9 +57,14 @@ interface PlanContentState { enum ApprovalOption { Auto = 'Yes, automatically accept edits', - AutoClear = 'Yes, automatically accept edits & clear conversation', Manual = 'Yes, manually accept edits', - ManualClear = 'Yes, manually accept edits & clear conversation', +} + +enum ContextOption { + Once = 'Allow once', + Session = 'Allow for this session', + Always = 'Allow for all future sessions', + No = 'No', } /** @@ -154,6 +166,10 @@ export const ExitPlanModeDialog: React.FC = ({ const planState = usePlanContent(planPath, config); const { refresh } = planState; const [showLoading, setShowLoading] = useState(false); + const [step, setStep] = useState(ApprovalStep.PLAN_APPROVAL); + const [selectedApprovalMode, setSelectedApprovalMode] = + useState(null); + const { setSetting } = useSettingsStore(); const handleOpenEditor = useCallback(async () => { try { @@ -227,64 +243,133 @@ export const ExitPlanModeDialog: React.FC = ({ const editHint = formatCommand(Command.OPEN_EXTERNAL_EDITOR); - return ( - - { - const answer = answers['0']; - const clearConversation = - answer === ApprovalOption.AutoClear || - answer === ApprovalOption.ManualClear; - if ( - answer === ApprovalOption.Auto || - answer === ApprovalOption.AutoClear - ) { - onApprove(ApprovalMode.AUTO_EDIT, clearConversation); - } else if ( - answer === ApprovalOption.Manual || - answer === ApprovalOption.ManualClear - ) { - onApprove(ApprovalMode.DEFAULT, clearConversation); - } else if (answer) { - onFeedback(answer); - } - }} - onCancel={onCancel} - width={width} - availableHeight={availableHeight} - extraParts={[`${editHint} to edit plan`]} - /> - - ); + if (step === ApprovalStep.PLAN_APPROVAL) { + return ( + + { + const answer = answers['0']; + let nextMode: ApprovalMode | null = null; + if (answer === ApprovalOption.Auto) { + nextMode = ApprovalMode.AUTO_EDIT; + } else if (answer === ApprovalOption.Manual) { + nextMode = ApprovalMode.DEFAULT; + } else if (answer) { + onFeedback(answer); + return; + } + + if (nextMode) { + const clearContext = config.getClearContextOnPlanApproval(); + if (clearContext !== undefined) { + onApprove(nextMode, clearContext); + } else { + setSelectedApprovalMode(nextMode); + setStep(ApprovalStep.CONTEXT_CHOICE); + } + } + }} + onCancel={onCancel} + width={width} + availableHeight={availableHeight} + extraParts={[`${editHint} to edit plan`]} + /> + + ); + } + + if (step === ApprovalStep.CONTEXT_CHOICE) { + return ( + + { + const answer = answers['0']; + let clearConversation = false; + + if (answer === ContextOption.Once) { + clearConversation = true; + } else if (answer === ContextOption.Session) { + clearConversation = true; + config.setClearContextOnPlanApprovalSessionOverride(true); + } else if (answer === ContextOption.Always) { + clearConversation = true; + setSetting( + SettingScope.User, + 'general.plan.clearContextOnApproval', + true, + ); + } else if (answer === ContextOption.No) { + clearConversation = false; + setSetting( + SettingScope.User, + 'general.plan.clearContextOnApproval', + false, + ); + } + + if (selectedApprovalMode) { + // Wrap in setTimeout to avoid 'Maximum update depth exceeded' + // when setSetting triggers a re-render of the parent. + setTimeout(() => { + onApprove(selectedApprovalMode, clearConversation); + }, 0); + } + }} + onCancel={() => setStep(ApprovalStep.PLAN_APPROVAL)} + width={width} + availableHeight={availableHeight} + /> + + ); + } + + return null; }; diff --git a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap index 63fe6a1693..073c106ceb 100644 --- a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap @@ -17,14 +17,11 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -▲ 1. Yes, automatically accept edits Approves plan and allows tools to run automatically -● 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits - Approves plan but requires confirmation for each tool -▼ +● 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -47,14 +44,11 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -▲ ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits + 2. Yes, manually accept edits Approves plan but requires confirmation for each tool -▼ + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -82,14 +76,11 @@ Implementation Steps 8. Add multi-factor authentication in src/auth/MFAService.ts ... last 22 lines hidden (Ctrl+O to show) ... -▲ ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits + 2. Yes, manually accept edits Approves plan but requires confirmation for each tool -▼ + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -112,14 +103,11 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -▲ ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits + 2. Yes, manually accept edits Approves plan but requires confirmation for each tool -▼ + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -144,14 +132,9 @@ Files to Modify 1. Yes, automatically accept edits Approves plan and allows tools to run automatically -● 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits - Approves plan but requires confirmation for each tool - 4. Yes, manually accept edits & clear conversation - Approves plan, requires confirmation, and clears prior conversation - context - 5. Type your feedback... +● 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -176,14 +159,9 @@ Files to Modify ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits + 2. Yes, manually accept edits Approves plan but requires confirmation for each tool - 4. Yes, manually accept edits & clear conversation - Approves plan, requires confirmation, and clears prior conversation - context - 5. Type your feedback... + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -234,14 +212,9 @@ Testing Strategy ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits + 2. Yes, manually accept edits Approves plan but requires confirmation for each tool - 4. Yes, manually accept edits & clear conversation - Approves plan, requires confirmation, and clears prior conversation - context - 5. Type your feedback... + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -266,14 +239,9 @@ Files to Modify ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, automatically accept edits & clear conversation - Approves plan, runs automatically, and clears prior conversation context - 3. Yes, manually accept edits + 2. Yes, manually accept edits Approves plan but requires confirmation for each tool - 4. Yes, manually accept edits & clear conversation - Approves plan, requires confirmation, and clears prior conversation - context - 5. Type your feedback... + 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 03776c888a..47073fe2e7 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -173,6 +173,7 @@ export interface SummarizeToolOutputSettings { export interface PlanSettings { directory?: string; modelRouting?: boolean; + clearContextOnApproval?: boolean; } export interface TelemetrySettings { @@ -796,6 +797,9 @@ export class Config implements McpContext { private readonly planEnabled: boolean; private readonly trackerEnabled: boolean; private readonly planModeRoutingEnabled: boolean; + private readonly clearContextOnPlanApproval?: boolean; + private clearContextOnPlanApprovalSessionOverride: boolean | undefined = + undefined; private readonly modelSteering: boolean; private contextManager?: ContextManager; private terminalBackground: string | undefined = undefined; @@ -887,6 +891,8 @@ export class Config implements McpContext { this.planEnabled = params.plan ?? false; this.trackerEnabled = params.tracker ?? false; this.planModeRoutingEnabled = params.planSettings?.modelRouting ?? true; + this.clearContextOnPlanApproval = + params.planSettings?.clearContextOnApproval; this.enableEventDrivenScheduler = params.enableEventDrivenScheduler ?? true; this.skillsSupport = params.skillsSupport ?? true; this.disabledSkills = params.disabledSkills ?? []; @@ -2467,6 +2473,21 @@ export class Config implements McpContext { return this.planModeRoutingEnabled; } + getClearContextOnPlanApproval(): boolean | undefined { + return ( + this.clearContextOnPlanApprovalSessionOverride ?? + this.clearContextOnPlanApproval + ); + } + + isClearContextOnPlanApprovalEnabled(): boolean { + return this.getClearContextOnPlanApproval() ?? true; + } + + setClearContextOnPlanApprovalSessionOverride(value: boolean): void { + this.clearContextOnPlanApprovalSessionOverride = value; + } + async getNumericalRoutingEnabled(): Promise { await this.ensureExperimentsLoaded(); diff --git a/packages/core/src/confirmation-bus/types.ts b/packages/core/src/confirmation-bus/types.ts index aefafe0fa0..4613483edd 100644 --- a/packages/core/src/confirmation-bus/types.ts +++ b/packages/core/src/confirmation-bus/types.ts @@ -160,6 +160,8 @@ export interface Question { options?: QuestionOption[]; /** Allow multiple selections. Only applies when type='choice'. */ multiSelect?: boolean; + /** Whether to allow a custom 'Other' option for 'choice' types. Defaults to true. */ + allowCustomOption?: boolean; /** Placeholder hint text. For type='text', shown in the input field. For type='choice', shown in the "Other" custom input. */ placeholder?: string; }