diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx index f0e11bddd4..6df45442c1 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Box } from 'ink'; import { ToolConfirmationQueue } from './ToolConfirmationQueue.js'; import { ToolCallStatus, StreamingState } from '../types.js'; @@ -12,6 +12,31 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import type { Config } from '@google/gemini-cli-core'; import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js'; +import { theme } from '../semantic-colors.js'; + +vi.mock('./StickyHeader.js', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + StickyHeader: vi.fn((props) => actual.StickyHeader(props)), + }; +}); + +vi.mock('@google/gemini-cli-core', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + validatePlanPath: vi.fn().mockResolvedValue(undefined), + validatePlanContent: vi.fn().mockResolvedValue(undefined), + processSingleFileContent: vi.fn().mockResolvedValue({ + llmContent: 'Plan content goes here', + error: undefined, + }), + }; +}); + +const { StickyHeader } = await import('./StickyHeader.js'); describe('ToolConfirmationQueue', () => { const mockConfig = { @@ -19,8 +44,19 @@ describe('ToolConfirmationQueue', () => { getIdeMode: () => false, getModel: () => 'gemini-pro', getDebugMode: () => false, + getTargetDir: () => '/mock/target/dir', + getFileSystemService: () => ({ + readFile: vi.fn().mockResolvedValue('Plan content'), + }), + storage: { + getProjectTempPlansDir: () => '/mock/temp/plans', + }, } as unknown as Config; + beforeEach(() => { + vi.clearAllMocks(); + }); + it('renders the confirming tool with progress indicator', () => { const confirmingTool = { tool: { @@ -60,6 +96,9 @@ describe('ToolConfirmationQueue', () => { expect(output).toContain('list files'); // Tool description expect(output).toContain("Allow execution of: 'ls'?"); expect(output).toMatchSnapshot(); + + const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0]; + expect(stickyHeaderProps.borderColor).toBe(theme.status.warning); }); it('returns null if tool has no confirmation details', () => { @@ -229,4 +268,80 @@ describe('ToolConfirmationQueue', () => { expect(output).not.toContain('Press ctrl-o to show more lines'); expect(output).toMatchSnapshot(); }); + + it('renders AskUser tool confirmation with Success color', () => { + const confirmingTool = { + tool: { + callId: 'call-1', + name: 'ask_user', + description: 'ask user', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'ask_user' as const, + questions: [], + onConfirm: vi.fn(), + }, + }, + index: 1, + total: 1, + }; + + const { lastFrame } = renderWithProviders( + , + { + config: mockConfig, + uiState: { + terminalWidth: 80, + }, + }, + ); + + const output = lastFrame(); + expect(output).toMatchSnapshot(); + + const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0]; + expect(stickyHeaderProps.borderColor).toBe(theme.status.success); + }); + + it('renders ExitPlanMode tool confirmation with Success color', async () => { + const confirmingTool = { + tool: { + callId: 'call-1', + name: 'exit_plan_mode', + description: 'exit plan mode', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'exit_plan_mode' as const, + planPath: '/path/to/plan', + onConfirm: vi.fn(), + }, + }, + index: 1, + total: 1, + }; + + const { lastFrame } = renderWithProviders( + , + { + config: mockConfig, + uiState: { + terminalWidth: 80, + }, + }, + ); + + await waitFor(() => { + expect(lastFrame()).toContain('Plan content goes here'); + }); + + const output = lastFrame(); + expect(output).toMatchSnapshot(); + + const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0]; + expect(stickyHeaderProps.borderColor).toBe(theme.status.success); + }); }); diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx index eb47d768bd..52cba7e0d7 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx @@ -70,10 +70,11 @@ export const ToolConfirmationQueue: React.FC = ({ ? Math.max(maxHeight - 6, 4) : undefined; - const borderColor = theme.status.warning; - const hideToolIdentity = + const isRoutine = tool.confirmationDetails?.type === 'ask_user' || tool.confirmationDetails?.type === 'exit_plan_mode'; + const borderColor = isRoutine ? theme.status.success : theme.status.warning; + const hideToolIdentity = isRoutine; return ( @@ -90,7 +91,7 @@ export const ToolConfirmationQueue: React.FC = ({ marginBottom={hideToolIdentity ? 0 : 1} justifyContent="space-between" > - + {getConfirmationHeader(tool.confirmationDetails)} {total > 1 && ( diff --git a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap index d4bbfdeaeb..aad58b92a7 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap @@ -40,6 +40,33 @@ exports[`ToolConfirmationQueue > does not render expansion hint when constrainHe ╰──────────────────────────────────────────────────────────────────────────────╯" `; +exports[`ToolConfirmationQueue > renders AskUser tool confirmation with Success color 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ Answer Questions │ +│ │ +│ Review your answers: │ +│ │ +│ │ +│ Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel │ +╰──────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`ToolConfirmationQueue > renders ExitPlanMode tool confirmation with Success color 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ Ready to start implementation? │ +│ │ +│ Plan content goes here │ +│ │ +│ ● 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[`ToolConfirmationQueue > renders expansion hint when content is long and constrained 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ │ Action Required │