diff --git a/packages/cli/GEMINI.md b/packages/cli/GEMINI.md index 8ab50f6b57..5518696d60 100644 --- a/packages/cli/GEMINI.md +++ b/packages/cli/GEMINI.md @@ -15,4 +15,11 @@ - **Utilities**: Use `renderWithProviders` and `waitFor` from `packages/cli/src/test-utils/`. - **Snapshots**: Use `toMatchSnapshot()` to verify Ink output. +- **SVG Snapshots**: Use `await expect(renderResult).toMatchSvgSnapshot()` for + UI components whenever colors or detailed visual layout matter. SVG snapshots + capture styling accurately. Make sure to await the `waitUntilReady()` of the + render result before asserting. After updating SVG snapshots, always examine + the resulting `.svg` files (e.g. by reading their content or visually + inspecting them) to ensure the render and colors actually look as expected and + don't just contain an error message. - **Mocks**: Use mocks as sparingly as possible. diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 1b64c07d7b..0420252149 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -547,6 +547,11 @@ const baseMockUiState = { }, hintMode: false, hintBuffer: '', + bannerData: { + defaultText: '', + warningText: '', + }, + bannerVisible: false, }; export const mockAppState: AppState = { diff --git a/packages/cli/src/ui/utils/__snapshots__/InlineMarkdownRenderer.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/InlineMarkdownRenderer.test.tsx.snap deleted file mode 100644 index c8a5a7ff15..0000000000 --- a/packages/cli/src/ui/utils/__snapshots__/InlineMarkdownRenderer.test.tsx.snap +++ /dev/null @@ -1,52 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`InlineMarkdownRenderer > RenderInline > handles nested/complex markdown gracefully (best effort) 1`] = ` -"Bold *Italic -*" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders bold text correctly 1`] = ` -"Hello -World" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders inline code correctly 1`] = ` -"Hello -World" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders italic text correctly 1`] = ` -"Hello -World" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders links correctly 1`] = `"Google (https://google.com)"`; - -exports[`InlineMarkdownRenderer > RenderInline > renders mixed markdown correctly 1`] = ` -"Bold - and -Italic - and -Code - and -Link (https://example.com)" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders plain text correctly 1`] = `"Hello World"`; - -exports[`InlineMarkdownRenderer > RenderInline > renders raw URLs correctly 1`] = ` -"Visit -https://google.com" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders strikethrough text correctly 1`] = ` -"Hello -World" -`; - -exports[`InlineMarkdownRenderer > RenderInline > renders underline correctly 1`] = ` -"Hello -World" -`; - -exports[`InlineMarkdownRenderer > RenderInline > respects defaultColor prop 1`] = `"Hello"`; diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg new file mode 100644 index 0000000000..b9290efcac --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + ⊷ google_web_search + + + + + Searching... + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg new file mode 100644 index 0000000000..0ba0125a62 --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + ⊷ run_shell_command + + + + + Running command... + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg new file mode 100644 index 0000000000..b9290efcac --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + ⊷ google_web_search + + + + + Searching... + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap new file mode 100644 index 0000000000..fbdc559480 --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap @@ -0,0 +1,55 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for a pending search dialog (google_web_search) 1`] = ` +" + ███ █████████ +░░░███ ███░░░░░███ + ░░░███ ███ ░░░ + ░░░███░███ + ███░ ░███ █████ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ + +╭──────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ⊷ google_web_search │ +│ │ +│ Searching... │ +╰──────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for a shell tool 1`] = ` +" + ███ █████████ +░░░███ ███░░░░░███ + ░░░███ ███ ░░░ + ░░░███░███ + ███░ ░███ █████ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ + +╭──────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ⊷ run_shell_command │ +│ │ +│ Running command... │ +╰──────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`MainContent tool group border SVG snapshots > should render SVG snapshot for an empty slice following a search tool 1`] = ` +" + ███ █████████ +░░░███ ███░░░░░███ + ░░░███ ███ ░░░ + ░░░███░███ + ███░ ░███ █████ + ███░ ░░███ ░░███ + ███░ ░░█████████ +░░░ ░░░░░░░░░ + +╭──────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ⊷ google_web_search │ +│ │ +│ Searching... │ +╰──────────────────────────────────────────────────────────────────────────────────────────────╯" +`; diff --git a/packages/cli/src/ui/utils/borderStyles.test.tsx b/packages/cli/src/ui/utils/borderStyles.test.tsx new file mode 100644 index 0000000000..91b2497f7f --- /dev/null +++ b/packages/cli/src/ui/utils/borderStyles.test.tsx @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it } from 'vitest'; +import { getToolGroupBorderAppearance } from './borderStyles.js'; +import { CoreToolCallStatus } from '@google/gemini-cli-core'; +import { theme } from '../semantic-colors.js'; +import type { IndividualToolCallDisplay } from '../types.js'; +import { renderWithProviders } from '../../test-utils/render.js'; +import { MainContent } from '../components/MainContent.js'; + +describe('getToolGroupBorderAppearance', () => { + it('should use warning color for pending non-shell tools', () => { + const item = { + type: 'tool_group' as const, + tools: [ + { + name: 'google_web_search', + status: CoreToolCallStatus.Executing, + resultDisplay: '', + callId: 'call-1', + }, + ] as IndividualToolCallDisplay[], + }; + const appearance = getToolGroupBorderAppearance(item, undefined, false, []); + expect(appearance.borderColor).toBe(theme.status.warning); + expect(appearance.borderDimColor).toBe(true); + }); + + it('should use correct color for empty slice by looking at pending items', () => { + const pendingItem = { + type: 'tool_group' as const, + tools: [ + { + name: 'google_web_search', + status: CoreToolCallStatus.Executing, + resultDisplay: '', + callId: 'call-1', + }, + ] as IndividualToolCallDisplay[], + }; + const sliceItem = { + type: 'tool_group' as const, + tools: [] as IndividualToolCallDisplay[], + }; + const allPendingItems = [pendingItem, sliceItem]; + + const appearance = getToolGroupBorderAppearance( + sliceItem, + undefined, + false, + allPendingItems, + ); + + // It should match the pendingItem appearance + expect(appearance.borderColor).toBe(theme.status.warning); + expect(appearance.borderDimColor).toBe(true); + }); + + it('should use symbol color for shell tools', () => { + const item = { + type: 'tool_group' as const, + tools: [ + { + name: 'run_shell_command', + status: CoreToolCallStatus.Executing, + resultDisplay: '', + callId: 'call-1', + }, + ] as IndividualToolCallDisplay[], + }; + const appearance = getToolGroupBorderAppearance(item, undefined, false, []); + expect(appearance.borderColor).toBe(theme.ui.symbol); + expect(appearance.borderDimColor).toBe(true); + }); +}); + +describe('MainContent tool group border SVG snapshots', () => { + it('should render SVG snapshot for a pending search dialog (google_web_search)', async () => { + const renderResult = renderWithProviders(, { + uiState: { + history: [], + pendingHistoryItems: [ + { + type: 'tool_group', + tools: [ + { + name: 'google_web_search', + status: CoreToolCallStatus.Executing, + resultDisplay: 'Searching...', + callId: 'call-1', + } as unknown as IndividualToolCallDisplay, + ], + }, + ], + }, + }); + + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); + }); + + it('should render SVG snapshot for an empty slice following a search tool', async () => { + const renderResult = renderWithProviders(, { + uiState: { + history: [], + pendingHistoryItems: [ + { + type: 'tool_group', + tools: [ + { + name: 'google_web_search', + status: CoreToolCallStatus.Executing, + resultDisplay: 'Searching...', + callId: 'call-1', + } as unknown as IndividualToolCallDisplay, + ], + }, + { + type: 'tool_group', + tools: [], + }, + ], + }, + }); + + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); + }); + + it('should render SVG snapshot for a shell tool', async () => { + const renderResult = renderWithProviders(, { + uiState: { + history: [], + pendingHistoryItems: [ + { + type: 'tool_group', + tools: [ + { + name: 'run_shell_command', + status: CoreToolCallStatus.Executing, + resultDisplay: 'Running command...', + callId: 'call-1', + } as unknown as IndividualToolCallDisplay, + ], + }, + ], + }, + }); + + await renderResult.waitUntilReady(); + await expect(renderResult).toMatchSvgSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/utils/borderStyles.ts b/packages/cli/src/ui/utils/borderStyles.ts index b3a0cb52bb..276d4a2502 100644 --- a/packages/cli/src/ui/utils/borderStyles.ts +++ b/packages/cli/src/ui/utils/borderStyles.ts @@ -47,7 +47,10 @@ export function getToolGroupBorderAppearance( : allPendingItems .filter( (i): i is HistoryItemToolGroup => - i !== null && i !== undefined && i.type === 'tool_group', + i !== null && + i !== undefined && + i.type === 'tool_group' && + i.tools.length > 0, ) .slice(-1) .flatMap((i) => i.tools);