diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx new file mode 100644 index 0000000000..adf9c247d4 --- /dev/null +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx @@ -0,0 +1,535 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; +import { act } from 'react'; +import { renderWithProviders } from '../../test-utils/render.js'; +import { waitFor } from '../../test-utils/async.js'; +import { ExitPlanModeDialog } from './ExitPlanModeDialog.js'; +import { useKeypress } from '../hooks/useKeypress.js'; +import { keyMatchers, Command } from '../keyMatchers.js'; +import { + ApprovalMode, + validatePlanContent, + processSingleFileContent, + type FileSystemService, +} from '@google/gemini-cli-core'; +import * as fs from 'node:fs'; + +vi.mock('@google/gemini-cli-core', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + validatePlanPath: vi.fn(async () => null), + validatePlanContent: vi.fn(async () => null), + processSingleFileContent: vi.fn(), + }; +}); + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + existsSync: vi.fn(), + realpathSync: vi.fn((p) => p), + promises: { + ...actual.promises, + readFile: vi.fn(), + }, + }; +}); + +const writeKey = (stdin: { write: (data: string) => void }, key: string) => { + act(() => { + stdin.write(key); + }); +}; + +describe('ExitPlanModeDialog', () => { + const mockTargetDir = '/mock/project'; + const mockPlansDir = '/mock/project/plans'; + const mockPlanFullPath = '/mock/project/plans/test-plan.md'; + + const samplePlanContent = `## 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`; + + const longPlanContent = `## Overview + +Implement a comprehensive authentication system with multiple providers. + +## 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 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\` +9. Implement session timeout handling in \`src/auth/SessionManager.ts\` +10. Add audit logging for auth events in \`src/auth/AuditLogger.ts\` +11. Create user profile management in \`src/auth/UserProfile.ts\` +12. Add role-based access control in \`src/auth/RBACService.ts\` +13. Implement password policy enforcement in \`src/auth/PasswordPolicy.ts\` +14. Add brute force protection in \`src/auth/BruteForceGuard.ts\` +15. Create secure cookie handling in \`src/auth/CookieManager.ts\` + +## Files to Modify + +- \`src/index.ts\` - Add auth middleware +- \`src/config.ts\` - Add auth configuration options +- \`src/routes/api.ts\` - Add auth endpoints +- \`src/middleware/cors.ts\` - Update CORS for auth headers +- \`src/utils/crypto.ts\` - Add encryption utilities + +## Testing Strategy + +- Unit tests for each auth provider +- Integration tests for full auth flows +- Security penetration testing +- Load testing for session management`; + + let onApprove: ReturnType; + let onFeedback: ReturnType; + let onCancel: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + vi.mocked(processSingleFileContent).mockResolvedValue({ + llmContent: samplePlanContent, + returnDisplay: 'Read file', + }); + vi.mocked(fs.existsSync).mockReturnValue(true); + vi.mocked(fs.realpathSync).mockImplementation((p) => p as string); + onApprove = vi.fn(); + onFeedback = vi.fn(); + onCancel = vi.fn(); + }); + + afterEach(() => { + vi.runOnlyPendingTimers(); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + const renderDialog = (options?: { useAlternateBuffer?: boolean }) => + renderWithProviders( + , + { + ...options, + config: { + getTargetDir: () => mockTargetDir, + getIdeMode: () => false, + isTrustedFolder: () => true, + storage: { + getProjectTempPlansDir: () => mockPlansDir, + }, + getFileSystemService: (): FileSystemService => ({ + readTextFile: vi.fn(), + writeTextFile: vi.fn(), + }), + } as unknown as import('@google/gemini-cli-core').Config, + }, + ); + + describe.each([{ useAlternateBuffer: true }, { useAlternateBuffer: false }])( + 'useAlternateBuffer: $useAlternateBuffer', + ({ useAlternateBuffer }) => { + it('renders correctly with plan content', async () => { + const { lastFrame } = renderDialog({ useAlternateBuffer }); + + // Advance timers to pass the debounce period + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + await waitFor(() => { + expect(processSingleFileContent).toHaveBeenCalledWith( + mockPlanFullPath, + mockPlansDir, + expect.anything(), + ); + }); + + expect(lastFrame()).toMatchSnapshot(); + }); + + it('calls onApprove with AUTO_EDIT when first option is selected', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + writeKey(stdin, '\r'); + + await waitFor(() => { + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.AUTO_EDIT); + }); + }); + + it('calls onApprove with DEFAULT when second option is selected', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); + + await waitFor(() => { + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT); + }); + }); + + it('calls onFeedback when feedback is typed and submitted', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + // Navigate to feedback option + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); // Select to focus input + + // Type feedback + for (const char of 'Add tests') { + writeKey(stdin, char); + } + + await waitFor(() => { + expect(lastFrame()).toMatchSnapshot(); + }); + + writeKey(stdin, '\r'); + + await waitFor(() => { + expect(onFeedback).toHaveBeenCalledWith('Add tests'); + }); + }); + + it('calls onCancel when Esc is pressed', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + writeKey(stdin, '\x1b'); // Escape + + await act(async () => { + vi.runAllTimers(); + }); + + expect(onCancel).toHaveBeenCalled(); + }); + + it('displays error state when file read fails', async () => { + vi.mocked(processSingleFileContent).mockResolvedValue({ + llmContent: '', + returnDisplay: '', + error: 'File not found', + }); + + const { lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Error reading plan: File not found'); + }); + + expect(lastFrame()).toMatchSnapshot(); + }); + + it('displays error state when plan file is empty', async () => { + vi.mocked(validatePlanContent).mockResolvedValue('Plan file is empty.'); + + const { lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain( + 'Error reading plan: Plan file is empty.', + ); + }); + }); + + it('handles long plan content appropriately', async () => { + vi.mocked(processSingleFileContent).mockResolvedValue({ + llmContent: longPlanContent, + returnDisplay: 'Read file', + }); + + const { lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain( + 'Implement a comprehensive authentication system', + ); + }); + + expect(lastFrame()).toMatchSnapshot(); + }); + + it('allows number key quick selection', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + // Press '2' to select second option directly + writeKey(stdin, '2'); + + await waitFor(() => { + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT); + }); + }); + + it('clears feedback text when Ctrl+C is pressed while editing', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + // Navigate to feedback option and start typing + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); // Select to focus input + + // Type some feedback + for (const char of 'test feedback') { + writeKey(stdin, char); + } + + await waitFor(() => { + expect(lastFrame()).toContain('test feedback'); + }); + + // Press Ctrl+C to clear + writeKey(stdin, '\x03'); // Ctrl+C + + await waitFor(() => { + expect(lastFrame()).not.toContain('test feedback'); + expect(lastFrame()).toContain('Type your feedback...'); + }); + + // Dialog should still be open (not cancelled) + expect(onCancel).not.toHaveBeenCalled(); + }); + + it('bubbles up Ctrl+C when feedback is empty while editing', async () => { + const onBubbledQuit = vi.fn(); + + const BubbleListener = ({ + children, + }: { + children: React.ReactNode; + }) => { + useKeypress( + (key) => { + if (keyMatchers[Command.QUIT](key)) { + onBubbledQuit(); + } + return false; + }, + { isActive: true }, + ); + return <>{children}; + }; + + const { stdin, lastFrame } = renderWithProviders( + + + , + { + useAlternateBuffer, + config: { + getTargetDir: () => mockTargetDir, + getIdeMode: () => false, + isTrustedFolder: () => true, + storage: { + getProjectTempPlansDir: () => mockPlansDir, + }, + getFileSystemService: (): FileSystemService => ({ + readTextFile: vi.fn(), + writeTextFile: vi.fn(), + }), + } as unknown as import('@google/gemini-cli-core').Config, + }, + ); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + // Navigate to feedback option + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + + // Type some feedback + for (const char of 'test') { + writeKey(stdin, char); + } + + await waitFor(() => { + expect(lastFrame()).toContain('test'); + }); + + // First Ctrl+C to clear text + writeKey(stdin, '\x03'); // Ctrl+C + + await waitFor(() => { + expect(lastFrame()).toMatchSnapshot(); + }); + expect(onBubbledQuit).not.toHaveBeenCalled(); + + // Second Ctrl+C to exit (should bubble) + writeKey(stdin, '\x03'); // Ctrl+C + + await waitFor(() => { + expect(onBubbledQuit).toHaveBeenCalled(); + }); + expect(onCancel).not.toHaveBeenCalled(); + }); + + it('does not submit empty feedback when Enter is pressed', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + // Navigate to feedback option + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + + // Press Enter without typing anything + writeKey(stdin, '\r'); + + // Wait a bit to ensure no callback was triggered + await act(async () => { + vi.advanceTimersByTime(50); + }); + + expect(onFeedback).not.toHaveBeenCalled(); + expect(onApprove).not.toHaveBeenCalled(); + }); + + it('allows arrow navigation while typing feedback to change selection', async () => { + const { stdin, lastFrame } = renderDialog({ useAlternateBuffer }); + + await act(async () => { + vi.runAllTimers(); + }); + + await waitFor(() => { + expect(lastFrame()).toContain('Add user authentication'); + }); + + // Navigate to feedback option and start typing + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\x1b[B'); // Down arrow + writeKey(stdin, '\r'); // Select to focus input + + // Type some feedback + for (const char of 'test') { + writeKey(stdin, char); + } + + // Now use up arrow to navigate back to a different option + writeKey(stdin, '\x1b[A'); // Up arrow + + // Press Enter to select the second option (manually accept edits) + writeKey(stdin, '\r'); + + await waitFor(() => { + expect(onApprove).toHaveBeenCalledWith(ApprovalMode.DEFAULT); + }); + expect(onFeedback).not.toHaveBeenCalled(); + }); + }, + ); +}); diff --git a/packages/cli/src/ui/components/ExitPlanModeDialog.tsx b/packages/cli/src/ui/components/ExitPlanModeDialog.tsx new file mode 100644 index 0000000000..9fc1adfc23 --- /dev/null +++ b/packages/cli/src/ui/components/ExitPlanModeDialog.tsx @@ -0,0 +1,226 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { useEffect, useState } from 'react'; +import { Box, Text } from 'ink'; +import { + ApprovalMode, + validatePlanPath, + validatePlanContent, + QuestionType, + type Config, + processSingleFileContent, +} from '@google/gemini-cli-core'; +import { theme } from '../semantic-colors.js'; +import { useConfig } from '../contexts/ConfigContext.js'; +import { AskUserDialog } from './AskUserDialog.js'; + +export interface ExitPlanModeDialogProps { + planPath: string; + onApprove: (approvalMode: ApprovalMode) => void; + onFeedback: (feedback: string) => void; + onCancel: () => void; + width: number; + availableHeight?: number; +} + +enum PlanStatus { + Loading = 'loading', + Loaded = 'loaded', + Error = 'error', +} + +interface PlanContentState { + status: PlanStatus; + content?: string; + error?: string; +} + +enum ApprovalOption { + Auto = 'Yes, automatically accept edits', + Manual = 'Yes, manually accept edits', +} + +/** + * A tiny component for loading and error states with consistent styling. + */ +const StatusMessage: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => {children}; + +function usePlanContent(planPath: string, config: Config): PlanContentState { + const [state, setState] = useState({ + status: PlanStatus.Loading, + }); + + useEffect(() => { + let ignore = false; + setState({ status: PlanStatus.Loading }); + + const load = async () => { + try { + const pathError = await validatePlanPath( + planPath, + config.storage.getProjectTempPlansDir(), + config.getTargetDir(), + ); + if (ignore) return; + if (pathError) { + setState({ status: PlanStatus.Error, error: pathError }); + return; + } + + const contentError = await validatePlanContent(planPath); + if (ignore) return; + if (contentError) { + setState({ status: PlanStatus.Error, error: contentError }); + return; + } + + const result = await processSingleFileContent( + planPath, + config.storage.getProjectTempPlansDir(), + config.getFileSystemService(), + ); + + if (ignore) return; + + if (result.error) { + setState({ status: PlanStatus.Error, error: result.error }); + return; + } + + if (typeof result.llmContent !== 'string') { + setState({ + status: PlanStatus.Error, + error: 'Plan file format not supported (binary or image).', + }); + return; + } + + const content = result.llmContent; + if (!content) { + setState({ status: PlanStatus.Error, error: 'Plan file is empty.' }); + return; + } + setState({ status: PlanStatus.Loaded, content }); + } catch (err: unknown) { + if (ignore) return; + const errorMessage = err instanceof Error ? err.message : String(err); + setState({ status: PlanStatus.Error, error: errorMessage }); + } + }; + + void load(); + + return () => { + ignore = true; + }; + }, [planPath, config]); + + return state; +} + +export const ExitPlanModeDialog: React.FC = ({ + planPath, + onApprove, + onFeedback, + onCancel, + width, + availableHeight, +}) => { + const config = useConfig(); + const planState = usePlanContent(planPath, config); + const [showLoading, setShowLoading] = useState(false); + + useEffect(() => { + if (planState.status !== PlanStatus.Loading) { + setShowLoading(false); + return; + } + + const timer = setTimeout(() => { + setShowLoading(true); + }, 200); + + return () => clearTimeout(timer); + }, [planState.status]); + + if (planState.status === PlanStatus.Loading) { + if (!showLoading) { + return null; + } + + return ( + + + Loading plan... + + + ); + } + + if (planState.status === PlanStatus.Error) { + return ( + + + Error reading plan: {planState.error} + + + ); + } + + const planContent = planState.content?.trim(); + if (!planContent) { + return ( + + Error: Plan content is empty. + + ); + } + + return ( + + { + const answer = answers['0']; + if (answer === ApprovalOption.Auto) { + onApprove(ApprovalMode.AUTO_EDIT); + } else if (answer === ApprovalOption.Manual) { + onApprove(ApprovalMode.DEFAULT); + } else if (answer) { + onFeedback(answer); + } + }} + onCancel={onCancel} + width={width} + availableHeight={availableHeight} + /> + + ); +}; diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx index 0ee6fec05c..e68affbf5e 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx @@ -25,6 +25,7 @@ function getConfirmationHeader( Record > = { ask_user: 'Answer Questions', + exit_plan_mode: 'Ready to start implementation?', }; if (!details?.type) { return 'Action Required'; @@ -70,7 +71,9 @@ export const ToolConfirmationQueue: React.FC = ({ : undefined; const borderColor = theme.status.warning; - const hideToolIdentity = tool.confirmationDetails?.type === 'ask_user'; + const hideToolIdentity = + tool.confirmationDetails?.type === 'ask_user' || + tool.confirmationDetails?.type === 'exit_plan_mode'; return ( diff --git a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap new file mode 100644 index 0000000000..2e4317d990 --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap @@ -0,0 +1,234 @@ +// 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 + +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 · Esc to cancel" +`; + +exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = ` +"## 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 · 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 + +Implement a comprehensive authentication system with multiple providers. + +## 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 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 ... + +● 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 select · ↑/↓ to navigate · Esc to cancel" +`; + +exports[`ExitPlanModeDialog > useAlternateBuffer: false > renders correctly with plan content 1`] = ` +"## 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 select · ↑/↓ to navigate · Esc to cancel" +`; + +exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 1`] = ` +"## 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 · Esc to cancel" +`; + +exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = ` +"## 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 · 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 + +Implement a comprehensive authentication system with multiple providers. + +## 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 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\` +9. Implement session timeout handling in \`src/auth/SessionManager.ts\` +10. Add audit logging for auth events in \`src/auth/AuditLogger.ts\` +11. Create user profile management in \`src/auth/UserProfile.ts\` +12. Add role-based access control in \`src/auth/RBACService.ts\` +13. Implement password policy enforcement in \`src/auth/PasswordPolicy.ts\` +14. Add brute force protection in \`src/auth/BruteForceGuard.ts\` +15. Create secure cookie handling in \`src/auth/CookieManager.ts\` + +## Files to Modify + +- \`src/index.ts\` - Add auth middleware +- \`src/config.ts\` - Add auth configuration options +- \`src/routes/api.ts\` - Add auth endpoints +- \`src/middleware/cors.ts\` - Update CORS for auth headers +- \`src/utils/crypto.ts\` - Add encryption utilities + +## Testing Strategy + +- Unit tests for each auth provider +- Integration tests for full auth flows +- Security penetration testing +- Load testing for session management + +● 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 select · ↑/↓ to navigate · Esc to cancel" +`; + +exports[`ExitPlanModeDialog > useAlternateBuffer: true > renders correctly with plan content 1`] = ` +"## 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 select · ↑/↓ to navigate · Esc to cancel" +`; diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index a50669bd40..a527c13314 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -34,6 +34,7 @@ import { REDIRECTION_WARNING_TIP_TEXT, } from '../../textConstants.js'; import { AskUserDialog } from '../AskUserDialog.js'; +import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js'; export interface ToolConfirmationMessageProps { callId: string; @@ -62,7 +63,9 @@ export const ToolConfirmationMessage: React.FC< const allowPermanentApproval = settings.merged.security.enablePermanentToolApproval; - const handlesOwnUI = confirmationDetails.type === 'ask_user'; + const handlesOwnUI = + confirmationDetails.type === 'ask_user' || + confirmationDetails.type === 'exit_plan_mode'; const isTrustedFolder = config.isTrustedFolder(); const handleConfirm = useCallback( @@ -277,6 +280,32 @@ export const ToolConfirmationMessage: React.FC< return { question: '', bodyContent, options: [] }; } + if (confirmationDetails.type === 'exit_plan_mode') { + bodyContent = ( + { + handleConfirm(ToolConfirmationOutcome.ProceedOnce, { + approved: true, + approvalMode, + }); + }} + onFeedback={(feedback) => { + handleConfirm(ToolConfirmationOutcome.ProceedOnce, { + approved: false, + feedback, + }); + }} + onCancel={() => { + handleConfirm(ToolConfirmationOutcome.Cancel); + }} + width={terminalWidth} + availableHeight={availableBodyContentHeight()} + /> + ); + return { question: '', bodyContent, options: [] }; + } + if (confirmationDetails.type === 'edit') { if (!confirmationDetails.isModifying) { question = `Apply this change?`; diff --git a/packages/core/src/core/__snapshots__/prompts.test.ts.snap b/packages/core/src/core/__snapshots__/prompts.test.ts.snap index 081f7f83dc..b58dc8ce40 100644 --- a/packages/core/src/core/__snapshots__/prompts.test.ts.snap +++ b/packages/core/src/core/__snapshots__/prompts.test.ts.snap @@ -198,12 +198,12 @@ The following read-only tools are available in Plan Mode: - Only begin this phase after exploration is complete - Create a detailed implementation plan with clear steps - Include file paths, function signatures, and code snippets where helpful -- After saving the plan, present the full content of the markdown file to the user for review +- Save the implementation plan to the designated plans directory ### Phase 4: Review & Approval -- Ask the user if they approve the plan, want revisions, or want to reject it -- Address feedback and iterate as needed -- **When the user approves the plan**, prompt them to switch out of Plan Mode to begin implementation by pressing Shift+Tab to cycle to a different approval mode +- Present the plan and request approval for the finalized plan using the \`exit_plan_mode\` tool +- If plan is approved, you can begin implementation +- If plan is rejected, address the feedback and iterate on the plan ## Constraints - You may ONLY use the read-only tools listed above diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 38ba82624e..7070c8189d 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -8,6 +8,7 @@ import { ACTIVATE_SKILL_TOOL_NAME, ASK_USER_TOOL_NAME, EDIT_TOOL_NAME, + EXIT_PLAN_MODE_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, MEMORY_TOOL_NAME, @@ -326,12 +327,12 @@ ${options.planModeToolsList} - Only begin this phase after exploration is complete - Create a detailed implementation plan with clear steps - Include file paths, function signatures, and code snippets where helpful -- After saving the plan, present the full content of the markdown file to the user for review +- Save the implementation plan to the designated plans directory ### Phase 4: Review & Approval -- Ask the user if they approve the plan, want revisions, or want to reject it -- Address feedback and iterate as needed -- **When the user approves the plan**, prompt them to switch out of Plan Mode to begin implementation by pressing Shift+Tab to cycle to a different approval mode +- Present the plan and request approval for the finalized plan using the \`${EXIT_PLAN_MODE_TOOL_NAME}\` tool +- If plan is approved, you can begin implementation +- If plan is rejected, address the feedback and iterate on the plan ## Constraints - You may ONLY use the read-only tools listed above