From defc28e42d97d4d275e6b42e40db1a771bd88c63 Mon Sep 17 00:00:00 2001 From: "A.K.M. Adib" Date: Wed, 4 Mar 2026 16:37:12 -0500 Subject: [PATCH] complete --- packages/cli/src/test-utils/render.tsx | 1 + packages/cli/src/ui/AppContainer.tsx | 29 ++- .../ui/components/ExitPlanModeDialog.test.tsx | 24 ++- .../src/ui/components/ExitPlanModeDialog.tsx | 31 ++- .../ExitPlanModeDialog.test.tsx.snap | 176 +++++------------- .../messages/ToolConfirmationMessage.tsx | 25 ++- .../cli/src/ui/contexts/UIActionsContext.tsx | 1 + packages/core/src/config/config.ts | 14 ++ .../core/src/tools/exit-plan-mode.test.ts | 46 +++++ packages/core/src/tools/exit-plan-mode.ts | 28 +++ packages/core/src/tools/tools.ts | 2 + packages/core/src/utils/events.ts | 21 +++ 12 files changed, 249 insertions(+), 149 deletions(-) diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 86c46e79e5..468e7b6bfc 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -591,6 +591,7 @@ const mockUIActions: UIActions = { setConstrainHeight: vi.fn(), onEscapePromptChange: vi.fn(), refreshStatic: vi.fn(), + handleClearPlanContext: vi.fn(), handleFinalSubmit: vi.fn(), handleClearScreen: vi.fn(), handleProQuotaChoice: vi.fn(), diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index d656169c51..5f7b1893cb 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -50,7 +50,7 @@ import { type GeminiUserTier, type UserFeedbackPayload, type AgentDefinition, - type ApprovalMode, + ApprovalMode, IdeClient, ideContextStore, getErrorMessage, @@ -398,6 +398,20 @@ export const AppContainer = (props: AppContainerProps) => { ); const [isConfigInitialized, setConfigInitialized] = useState(false); + const [planModeUIHistoryStartIndex, setPlanModeUIHistoryStartIndex] = + useState(null); + + useEffect(() => { + const handleApprovalModeChanged = ({ mode }: { mode: ApprovalMode }) => { + if (mode === ApprovalMode.PLAN) { + setPlanModeUIHistoryStartIndex(historyManager.history.length); + } + }; + coreEvents.on(CoreEvent.ApprovalModeChanged, handleApprovalModeChanged); + return () => { + coreEvents.off(CoreEvent.ApprovalModeChanged, handleApprovalModeChanged); + }; + }, [historyManager.history.length]); const logger = useLogger(config.storage); const { inputHistory, addInput, initializeFromLogger } = @@ -1346,6 +1360,17 @@ Logging in with Google... Restarting Gemini CLI to continue. triggerExpandHint, ]); + const handleClearPlanContext = useCallback(() => { + if (planModeUIHistoryStartIndex !== null) { + const newHistory = historyManager.history.slice( + 0, + planModeUIHistoryStartIndex, + ); + historyManager.loadHistory(newHistory); + refreshStatic(); + } + }, [planModeUIHistoryStartIndex, historyManager, refreshStatic]); + const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit); /** @@ -2456,6 +2481,7 @@ Logging in with Google... Restarting Gemini CLI to continue. setConstrainHeight, onEscapePromptChange: handleEscapePromptChange, refreshStatic, + handleClearPlanContext, handleFinalSubmit, handleClearScreen, handleProQuotaChoice, @@ -2548,6 +2574,7 @@ Logging in with Google... Restarting Gemini CLI to continue. setConstrainHeight, handleEscapePromptChange, refreshStatic, + handleClearPlanContext, handleFinalSubmit, handleClearScreen, handleProQuotaChoice, diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx index 2bf1f723a6..262f4e635d 100644 --- a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx @@ -207,7 +207,7 @@ Implement a comprehensive authentication system with multiple providers. writeKey(stdin, '\r'); await waitFor(() => { - expect(onApprove).toHaveBeenCalledWith(ApprovalMode.AUTO_EDIT); + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.AUTO_EDIT, false); }); }); @@ -222,11 +222,12 @@ Implement a comprehensive authentication system with multiple providers. expect(lastFrame()).toContain('Add user authentication'); }); + writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\x1b[B'); // Down arrow writeKey(stdin, '\r'); await waitFor(() => { - expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT); + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT, false); }); }); @@ -349,11 +350,11 @@ Implement a comprehensive authentication system with multiple providers. expect(lastFrame()).toContain('Add user authentication'); }); - // Press '2' to select second option directly - writeKey(stdin, '2'); + // Press '3' to select third option directly + writeKey(stdin, '3'); await waitFor(() => { - expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT); + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT, false); }); }); @@ -371,6 +372,8 @@ 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 @@ -493,7 +496,9 @@ Implement a comprehensive authentication system with multiple providers. expect(lastFrame()).toContain('Add user authentication'); }); - // Navigate to feedback option + // 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 @@ -523,6 +528,8 @@ 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') { @@ -531,12 +538,13 @@ 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 second option (manually accept edits) + // Press Enter to select the manually accept edits option writeKey(stdin, '\r'); await waitFor(() => { - expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT); + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT, false); }); expect(onFeedback).not.toHaveBeenCalled(); }); diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.tsx index 39e1b8a155..d1dbdba627 100644 --- a/packages/cli/src/ui/components/ExitPlanModeDialog.tsx +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.tsx @@ -27,7 +27,7 @@ import { formatCommand } from '../utils/keybindingUtils.js'; export interface ExitPlanModeDialogProps { planPath: string; - onApprove: (approvalMode: ApprovalMode) => void; + onApprove: (approvalMode: ApprovalMode, clearConversation?: boolean) => void; onFeedback: (feedback: string) => void; onCancel: () => void; getPreferredEditor: () => EditorType | undefined; @@ -50,7 +50,9 @@ 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', } /** @@ -239,11 +241,21 @@ export const ExitPlanModeDialog: React.FC = ({ description: 'Approves plan and allows tools to run automatically', }, + { + label: ApprovalOption.AutoClear, + description: + 'Approves plan, runs automatically, and clears prior conversation context', + }, { label: ApprovalOption.Manual, description: 'Approves plan but requires confirmation for each tool', }, + { + label: ApprovalOption.ManualClear, + description: + 'Approves plan, requires confirmation, and clears prior conversation context', + }, ], placeholder: 'Type your feedback...', multiSelect: false, @@ -251,10 +263,19 @@ export const ExitPlanModeDialog: React.FC = ({ ]} onSubmit={(answers) => { const answer = answers['0']; - if (answer === ApprovalOption.Auto) { - onApprove(ApprovalMode.AUTO_EDIT); - } else if (answer === ApprovalOption.Manual) { - onApprove(ApprovalMode.DEFAULT); + 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); } 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 9e210e3438..63fe6a1693 100644 --- a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap @@ -17,43 +17,19 @@ 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, manually accept edits - Approves plan but requires confirmation for each tool - 3. Type your feedback... +● 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 +▼ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; -exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 2`] = ` -"Overview - -Add user authentication to the CLI application. - -Implementation Steps - - 1. Create src/auth/AuthService.ts with login/logout methods - 2. Add session storage in src/storage/SessionStore.ts - 3. Update src/commands/index.ts to check auth status - 4. Add tests in src/auth/__tests__/ - -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, manually accept edits - Approves plan but requires confirmation for each tool -● 3. Type your feedback... - -Enter to submit · Ctrl+X to edit plan · Esc to cancel -" -`; - exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = ` "Overview @@ -71,43 +47,19 @@ 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, manually accept edits + 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 - 3. Type your feedback... +▼ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; -exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 2`] = ` -"Overview - -Add user authentication to the CLI application. - -Implementation Steps - - 1. Create src/auth/AuthService.ts with login/logout methods - 2. Add session storage in src/storage/SessionStore.ts - 3. Update src/commands/index.ts to check auth status - 4. Add tests in src/auth/__tests__/ - -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, manually accept edits - Approves plan but requires confirmation for each tool -● 3. Add tests - -Enter to submit · Ctrl+X to edit plan · Esc to cancel -" -`; - exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = ` " Error reading plan: File not found " @@ -130,11 +82,14 @@ 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, manually accept edits + 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 - 3. Type your feedback... +▼ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -157,11 +112,14 @@ 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, manually accept edits + 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 - 3. Type your feedback... +▼ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -186,41 +144,19 @@ Files to Modify 1. Yes, automatically accept edits Approves plan and allows tools to run automatically -● 2. Yes, manually accept edits - Approves plan but requires confirmation for each tool - 3. Type your feedback... +● 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... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; -exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = ` -"Overview - -Add user authentication to the CLI application. - -Implementation Steps - - 1. Create src/auth/AuthService.ts with login/logout methods - 2. Add session storage in src/storage/SessionStore.ts - 3. Update src/commands/index.ts to check auth status - 4. Add tests in src/auth/__tests__/ - -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, manually accept edits - Approves plan but requires confirmation for each tool -● 3. Type your feedback... - -Enter to submit · Ctrl+X to edit plan · Esc to cancel -" -`; - exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = ` "Overview @@ -240,41 +176,19 @@ Files to Modify ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, manually accept edits + 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 - 3. Type your feedback... + 4. Yes, manually accept edits & clear conversation + Approves plan, requires confirmation, and clears prior conversation + context + 5. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; -exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 2`] = ` -"Overview - -Add user authentication to the CLI application. - -Implementation Steps - - 1. Create src/auth/AuthService.ts with login/logout methods - 2. Add session storage in src/storage/SessionStore.ts - 3. Update src/commands/index.ts to check auth status - 4. Add tests in src/auth/__tests__/ - -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, manually accept edits - Approves plan but requires confirmation for each tool -● 3. Add tests - -Enter to submit · Ctrl+X to edit plan · Esc to cancel -" -`; - exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = ` " Error reading plan: File not found " @@ -320,9 +234,14 @@ Testing Strategy ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, manually accept edits + 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 - 3. Type your feedback... + 4. Yes, manually accept edits & clear conversation + Approves plan, requires confirmation, and clears prior conversation + context + 5. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " @@ -347,9 +266,14 @@ Files to Modify ● 1. Yes, automatically accept edits Approves plan and allows tools to run automatically - 2. Yes, manually accept edits + 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 - 3. Type your feedback... + 4. Yes, manually accept edits & clear conversation + Approves plan, requires confirmation, and clears prior conversation + context + 5. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 022a68e953..4f4a604ca2 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -29,6 +29,7 @@ import { import { useKeypress } from '../../hooks/useKeypress.js'; import { theme } from '../../semantic-colors.js'; import { useSettings } from '../../contexts/SettingsContext.js'; +import { useUIActions } from '../../contexts/UIActionsContext.js'; import { keyMatchers, Command } from '../../keyMatchers.js'; import { formatCommand } from '../../utils/keybindingUtils.js'; import { @@ -68,6 +69,7 @@ export const ToolConfirmationMessage: React.FC< terminalWidth, }) => { const { confirm, isDiffingEnabled } = useToolActions(); + const { handleClearPlanContext } = useUIActions(); const [mcpDetailsExpansionState, setMcpDetailsExpansionState] = useState<{ callId: string; expanded: boolean; @@ -428,10 +430,14 @@ export const ToolConfirmationMessage: React.FC< { + onApprove={(approvalMode, clearConversation) => { + if (clearConversation) { + handleClearPlanContext(); + } handleConfirm(ToolConfirmationOutcome.ProceedOnce, { approved: true, approvalMode, + clearConversation, }); }} onFeedback={(feedback) => { @@ -623,17 +629,18 @@ export const ToolConfirmationMessage: React.FC< return { question, bodyContent, options, securityWarnings }; }, [ - confirmationDetails, getOptions, - availableBodyContentHeight, - terminalWidth, - handleConfirm, deceptiveUrlWarningText, - isMcpToolDetailsExpanded, - hasMcpToolDetails, - mcpToolDetailsText, - expandDetailsHintKey, + confirmationDetails, + terminalWidth, + availableBodyContentHeight, + handleConfirm, getPreferredEditor, + handleClearPlanContext, + hasMcpToolDetails, + isMcpToolDetailsExpanded, + expandDetailsHintKey, + mcpToolDetailsText, ]); const bodyOverflowDirection: 'top' | 'bottom' = diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 988837df4d..67b52ff072 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -57,6 +57,7 @@ export interface UIActions { setConstrainHeight: (value: boolean) => void; onEscapePromptChange: (show: boolean) => void; refreshStatic: () => void; + handleClearPlanContext: () => void; handleFinalSubmit: (value: string) => Promise; handleClearScreen: () => void; handleProQuotaChoice: ( diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index ce07271139..03776c888a 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1960,6 +1960,12 @@ export class Config implements McpContext { this.geminiMdFilePaths = paths; } + private planModeHistoryStartIndex = 0; + + getPlanModeHistoryStartIndex(): number { + return this.planModeHistoryStartIndex; + } + getApprovalMode(): ApprovalMode { return this.policyEngine.getApprovalMode(); } @@ -2017,6 +2023,14 @@ export class Config implements McpContext { this.policyEngine.setApprovalMode(mode); + if (mode === ApprovalMode.PLAN && currentMode !== ApprovalMode.PLAN) { + this.planModeHistoryStartIndex = this.geminiClient?.isInitialized() + ? this.geminiClient.getHistory().length + : 0; + } + + coreEvents.emitApprovalModeChanged(mode); + const isPlanModeTransition = currentMode !== mode && (currentMode === ApprovalMode.PLAN || mode === ApprovalMode.PLAN); diff --git a/packages/core/src/tools/exit-plan-mode.test.ts b/packages/core/src/tools/exit-plan-mode.test.ts index 22de81fc7f..f9e8aea7b4 100644 --- a/packages/core/src/tools/exit-plan-mode.test.ts +++ b/packages/core/src/tools/exit-plan-mode.test.ts @@ -9,6 +9,7 @@ import { ExitPlanModeTool, ExitPlanModeInvocation } from './exit-plan-mode.js'; import { createMockMessageBus } from '../test-utils/mock-message-bus.js'; import path from 'node:path'; import type { Config } from '../config/config.js'; +import type { GeminiClient } from '../core/client.js'; import type { MessageBus } from '../confirmation-bus/message-bus.js'; import { ToolConfirmationOutcome } from './tools.js'; import { ApprovalMode } from '../policy/types.js'; @@ -44,6 +45,8 @@ describe('ExitPlanModeTool', () => { getTargetDir: vi.fn().mockReturnValue(tempRootDir), setApprovalMode: vi.fn(), setApprovedPlanPath: vi.fn(), + getGeminiClient: vi.fn(), + getPlanModeHistoryStartIndex: vi.fn().mockReturnValue(0), storage: { getPlansDir: vi.fn().mockReturnValue(mockPlansDir), } as unknown as Config['storage'], @@ -240,6 +243,49 @@ Read and follow the plan strictly during implementation.`, expect(mockConfig.setApprovedPlanPath).toHaveBeenCalledWith(expectedPath); }); + it('should truncate history surgically when clearConversation is true', async () => { + const planRelativePath = createPlanFile('test.md', '# Content'); + const invocation = tool.build({ plan_path: planRelativePath }); + + const mockSetHistory = vi.fn(); + const mockHistory = [ + { role: 'user', parts: [{ text: 'Pre-plan prompt' }] }, + { role: 'model', parts: [{ text: 'Pre-plan response' }] }, + { role: 'user', parts: [{ text: 'Enter plan mode' }] }, // Planning start turn (index 2) + { role: 'model', parts: [{ text: 'Draft 1' }] }, + { role: 'user', parts: [{ text: 'No, change it' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'exit_plan_mode', args: {} } }], + }, + ]; + vi.mocked(mockConfig.getGeminiClient!).mockReturnValue({ + getHistory: vi.fn().mockReturnValue(mockHistory), + setHistory: mockSetHistory, + } as unknown as GeminiClient); + vi.mocked(mockConfig.getPlanModeHistoryStartIndex!).mockReturnValue(2); + + const confirmDetails = await invocation.shouldConfirmExecute( + new AbortController().signal, + ); + if (confirmDetails === false) return; + + await confirmDetails.onConfirm(ToolConfirmationOutcome.ProceedOnce, { + approved: true, + approvalMode: ApprovalMode.AUTO_EDIT, + clearConversation: true, + }); + + await invocation.execute(new AbortController().signal); + + expect(mockSetHistory).toHaveBeenCalledWith([ + mockHistory[0], // pre-plan user + mockHistory[1], // pre-plan model + mockHistory[2], // first planning user message + mockHistory[5], // last model message (exit_plan_mode) + ]); + }); + it('should return feedback message when plan is rejected with feedback', async () => { const planRelativePath = createPlanFile('test.md', '# Content'); const invocation = tool.build({ plan_path: planRelativePath }); diff --git a/packages/core/src/tools/exit-plan-mode.ts b/packages/core/src/tools/exit-plan-mode.ts index 442b00e5cb..ee17d35d62 100644 --- a/packages/core/src/tools/exit-plan-mode.ts +++ b/packages/core/src/tools/exit-plan-mode.ts @@ -214,6 +214,34 @@ export class ExitPlanModeInvocation extends BaseToolInvocation< this.config.setApprovalMode(newMode); this.config.setApprovedPlanPath(resolvedPlanPath); + if (payload.clearConversation) { + const geminiClient = this.config.getGeminiClient(); + if (geminiClient) { + const history = geminiClient.getHistory(); + const startIndex = this.config.getPlanModeHistoryStartIndex(); + + // Find the first user message at or after the start index. + // This represents the beginning of the current planning phase. + let planningUserIndex = -1; + for (let i = startIndex; i < history.length - 1; i++) { + if (history[i].role === 'user') { + planningUserIndex = i; + break; + } + } + + if (planningUserIndex !== -1) { + const lastModelMessage = history[history.length - 1]; + // Keep everything before the plan, plus the initial plan request turn. + const newHistory = [ + ...history.slice(0, planningUserIndex + 1), + lastModelMessage, + ]; + geminiClient.setHistory(newHistory); + } + } + } + logPlanExecution(this.config, new PlanExecutionEvent(newMode)); const exitMessage = getPlanModeExitMessage(newMode); diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index 0a82cc1510..1b0c3f8bbe 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -757,6 +757,8 @@ export interface ToolExitPlanModeConfirmationPayload { approvalMode?: ApprovalMode; /** If rejected, the user's feedback */ feedback?: string; + /** If the user wants to clear the conversation context upon approval */ + clearConversation?: boolean; } export type ToolConfirmationPayload = diff --git a/packages/core/src/utils/events.ts b/packages/core/src/utils/events.ts index 159dde2a6d..c29cf8261b 100644 --- a/packages/core/src/utils/events.ts +++ b/packages/core/src/utils/events.ts @@ -9,6 +9,7 @@ import type { AgentDefinition } from '../agents/types.js'; import type { McpClient } from '../tools/mcp-client.js'; import type { ExtensionEvents } from './extensionLoader.js'; import type { EditorType } from './editor.js'; +import type { ApprovalMode } from '../policy/types.js'; import type { TokenStorageInitializationEvent, KeychainAvailabilityEvent, @@ -52,6 +53,16 @@ export interface ModelChangedPayload { model: string; } +/** + * Payload for the 'approval-mode-changed' event. + */ +export interface ApprovalModeChangedPayload { + /** + * The new approval mode that was set. + */ + mode: ApprovalMode; +} + /** * Payload for the 'console-log' event. */ @@ -167,6 +178,7 @@ export interface QuotaChangedPayload { export enum CoreEvent { UserFeedback = 'user-feedback', ModelChanged = 'model-changed', + ApprovalModeChanged = 'approval-mode-changed', ConsoleLog = 'console-log', Output = 'output', MemoryChanged = 'memory-changed', @@ -200,6 +212,7 @@ export interface EditorSelectedPayload { export interface CoreEvents extends ExtensionEvents { [CoreEvent.UserFeedback]: [UserFeedbackPayload]; [CoreEvent.ModelChanged]: [ModelChangedPayload]; + [CoreEvent.ApprovalModeChanged]: [ApprovalModeChangedPayload]; [CoreEvent.ConsoleLog]: [ConsoleLogPayload]; [CoreEvent.Output]: [OutputPayload]; [CoreEvent.MemoryChanged]: [MemoryChangedPayload]; @@ -301,6 +314,14 @@ export class CoreEventEmitter extends EventEmitter { this.emit(CoreEvent.ModelChanged, payload); } + /** + * Notifies subscribers that the approval mode has changed. + */ + emitApprovalModeChanged(mode: ApprovalMode): void { + const payload: ApprovalModeChangedPayload = { mode }; + this.emit(CoreEvent.ApprovalModeChanged, payload); + } + /** * Notifies subscribers that settings have been modified. */