From 142ccf21401b6dc4c505d838b5c89f8bd4e3a57b Mon Sep 17 00:00:00 2001 From: Jarrod Whelan Date: Mon, 9 Feb 2026 20:46:37 -0800 Subject: [PATCH] feat(ui): Re-apply Dense Tool Output features --- docs/cli/settings.md | 7 +- docs/get-started/configuration.md | 6 + packages/cli/src/config/settingsSchema.ts | 15 + packages/cli/src/ui/AppContainer.tsx | 4 +- .../ui/components/HistoryItemDisplay.test.tsx | 12 + .../src/ui/components/HistoryItemDisplay.tsx | 22 + .../src/ui/components/MainContent.test.tsx | 59 +- .../cli/src/ui/components/MainContent.tsx | 26 +- ...ternateBufferQuittingDisplay.test.tsx.snap | 80 ++- .../__snapshots__/MainContent.test.tsx.snap | 12 +- .../SettingsDialog.test.tsx.snap | 592 +++++++++--------- .../messages/DenseToolMessage.test.tsx | 125 ++++ .../components/messages/DenseToolMessage.tsx | 78 +++ .../components/messages/ToolGroupMessage.tsx | 86 ++- .../ToolStickyHeaderRegression.test.tsx | 8 +- .../ToolGroupMessage.test.tsx.snap | 384 ++++++------ .../ToolStickyHeaderRegression.test.tsx.snap | 12 +- .../cli/src/ui/hooks/shellCommandProcessor.ts | 6 +- packages/cli/src/ui/hooks/toolMapping.test.ts | 24 +- packages/cli/src/ui/hooks/toolMapping.ts | 5 + .../src/ui/hooks/useHistoryManager.test.ts | 70 ++- .../cli/src/ui/hooks/useHistoryManager.ts | 51 +- packages/cli/src/ui/types.ts | 72 ++- packages/core/src/tools/ripGrep.ts | 8 +- packages/core/src/tools/web-search.test.ts | 12 +- packages/core/src/tools/web-search.ts | 2 +- schemas/settings.schema.json | 8 + 27 files changed, 1165 insertions(+), 621 deletions(-) create mode 100644 packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx create mode 100644 packages/cli/src/ui/components/messages/DenseToolMessage.tsx diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 9a60f89a53..7faf68de40 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -32,9 +32,10 @@ they appear in the UI. ### Output -| UI Label | Setting | Description | Default | -| ------------- | --------------- | ------------------------------------------------------ | -------- | -| Output Format | `output.format` | The format of the CLI output. Can be `text` or `json`. | `"text"` | +| UI Label | Setting | Description | Default | +| ---------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------- | +| Output Format | `output.format` | The format of the CLI output. Can be `text` or `json`. | `"text"` | +| Verbose Output History | `output.verbosity` | Show verbose output history. When enabled, output history will include autonomous tool calls, additional logs, etc. | `"verbose"` | ### UI diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 28578ae364..da28808d40 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -163,6 +163,12 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `"text"` - **Values:** `"text"`, `"json"` +- **`output.verbosity`** (enum): + - **Description:** Show verbose output history. When enabled, output history + will include autonomous tool calls, additional logs, etc. + - **Default:** `"verbose"` + - **Values:** `"info"`, `"verbose"` + #### `ui` - **`ui.theme`** (string): diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 2e53997a5d..95b8f9917c 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -328,6 +328,21 @@ const SETTINGS_SCHEMA = { { value: 'json', label: 'JSON' }, ], }, + // Defined as enum type to support the addition of more verbosity levels + verbosity: { + type: 'enum', + label: 'Verbose Output History', + category: 'General', + requiresRestart: false, + default: 'verbose', + description: + 'Show verbose output history. When enabled, output history will include autonomous tool calls, additional logs, etc.', + showInDialog: true, + options: [ + { value: 'info', label: 'false' }, + { value: 'verbose', label: 'true' }, + ], + }, }, }, diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index fbfa93ac3a..4422930896 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -862,7 +862,7 @@ Logging in with Google... Restarting Gemini CLI to continue. const performMemoryRefresh = useCallback(async () => { historyManager.addItem( { - type: MessageType.INFO, + type: MessageType.VERBOSE, text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...', }, Date.now(), @@ -873,7 +873,7 @@ Logging in with Google... Restarting Gemini CLI to continue. historyManager.addItem( { - type: MessageType.INFO, + type: MessageType.VERBOSE, text: `Memory refreshed successfully. ${ memoryContent.length > 0 ? `Loaded ${memoryContent.length} characters from ${fileCount} file(s).` diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx index 1aecb9a0ba..06bdf155c5 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.test.tsx @@ -171,6 +171,18 @@ describe('', () => { expect(lastFrame()).toContain('Agent powering down. Goodbye!'); }); + it('renders InfoMessage for "debug" type with gear icon', () => { + const item: HistoryItem = { + ...baseItem, + type: MessageType.DEBUG, + text: 'Debug info', + }; + const { lastFrame } = renderWithProviders( + , + ); + expect(lastFrame()).toContain('⚙ Debug info'); + }); + it('should escape ANSI codes in text content', () => { const historyItem: HistoryItem = { id: 1, diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index ed399dd38f..15e68909a3 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -18,6 +18,7 @@ import { GeminiMessageContent } from './messages/GeminiMessageContent.js'; import { CompressionMessage } from './messages/CompressionMessage.js'; import { WarningMessage } from './messages/WarningMessage.js'; import { Box } from 'ink'; +import { theme } from '../semantic-colors.js'; import { AboutBox } from './AboutBox.js'; import { StatsDisplay } from './StatsDisplay.js'; import { ModelStatsDisplay } from './ModelStatsDisplay.js'; @@ -96,6 +97,27 @@ export const HistoryItemDisplay: React.FC = ({ color={itemForDisplay.color} /> )} + {itemForDisplay.type === 'verbose' && ( + + )} + {itemForDisplay.type === 'debug' && ( + + )} + {itemForDisplay.type === 'trace' && ( + + )} {itemForDisplay.type === 'warning' && ( )} diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx index 0445b11b4b..395d5acc5b 100644 --- a/packages/cli/src/ui/components/MainContent.test.tsx +++ b/packages/cli/src/ui/components/MainContent.test.tsx @@ -7,13 +7,14 @@ import { renderWithProviders } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { MainContent } from './MainContent.js'; -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { Box, Text } from 'ink'; import type React from 'react'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; import { ToolCallStatus } from '../types.js'; import { SHELL_COMMAND_NAME } from '../constants.js'; import type { UIState } from '../contexts/UIStateContext.js'; +import type { HistoryItem } from '../types.js'; // Mock dependencies vi.mock('../contexts/AppContext.js', async () => { @@ -26,6 +27,23 @@ vi.mock('../contexts/AppContext.js', async () => { }; }); +const mockSettings = { + merged: { + output: { + verbosity: 'info', + }, + }, +}; + +vi.mock('../contexts/SettingsContext.js', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + useSettings: () => mockSettings, + }; +}); + vi.mock('../hooks/useAlternateBuffer.js', () => ({ useAlternateBuffer: vi.fn(), })); @@ -78,6 +96,11 @@ describe('MainContent', () => { beforeEach(() => { vi.mocked(useAlternateBuffer).mockReturnValue(false); + mockSettings.merged.output.verbosity = 'info'; + }); + + afterEach(() => { + vi.clearAllMocks(); }); it('renders in normal buffer mode', async () => { @@ -221,4 +244,38 @@ describe('MainContent', () => { }, ); }); + + it('filters out verbose items when verbosity is info', async () => { + const history: HistoryItem[] = [ + { id: 1, type: 'user', text: 'Visible User Message' }, + { id: 2, type: 'verbose', text: 'Hidden Verbose Log' }, + ]; + mockSettings.merged.output.verbosity = 'info'; + + const { lastFrame } = renderWithProviders(, { + uiState: { ...defaultMockUiState, history } as Partial, + }); + await waitFor(() => expect(lastFrame()).toContain('AppHeader')); + const output = lastFrame(); + + expect(output).toContain('Visible User Message'); + expect(output).not.toContain('Hidden Verbose Log'); + }); + + it('shows verbose items when verbosity is verbose', async () => { + const history: HistoryItem[] = [ + { id: 1, type: 'user', text: 'Visible User Message' }, + { id: 2, type: 'verbose', text: 'Visible Verbose Log' }, + ]; + mockSettings.merged.output.verbosity = 'verbose'; + + const { lastFrame } = renderWithProviders(, { + uiState: { ...defaultMockUiState, history } as Partial, + }); + await waitFor(() => expect(lastFrame()).toContain('AppHeader')); + const output = lastFrame(); + + expect(output).toContain('Visible User Message'); + expect(output).toContain('Visible Verbose Log'); + }); }); diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx index 32c70e8cad..bbf0144b34 100644 --- a/packages/cli/src/ui/components/MainContent.tsx +++ b/packages/cli/src/ui/components/MainContent.tsx @@ -20,6 +20,8 @@ import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js'; import { useConfirmingTool } from '../hooks/useConfirmingTool.js'; import { ToolConfirmationQueue } from './ToolConfirmationQueue.js'; import { useConfig } from '../contexts/ConfigContext.js'; +import { VERBOSITY_MAPPING, Verbosity } from '../types.js'; +import { useSettings } from '../contexts/SettingsContext.js'; const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay); const MemoizedAppHeader = memo(AppHeader); @@ -32,6 +34,7 @@ export const MainContent = () => { const { version } = useAppContext(); const uiState = useUIState(); const config = useConfig(); + const settings = useSettings(); const isAlternateBuffer = useAlternateBuffer(); const confirmingTool = useConfirmingTool(); @@ -53,9 +56,24 @@ export const MainContent = () => { availableTerminalHeight, } = uiState; + const currentVerbosity = + VERBOSITY_MAPPING[settings.merged.output?.verbosity ?? 'info'] ?? + Verbosity.INFO; + + const filteredHistory = useMemo( + () => + uiState.history.filter((item) => { + const itemType = item.type; + const itemVerbosity = + item.verbosity ?? VERBOSITY_MAPPING[itemType] ?? Verbosity.INFO; + return itemVerbosity <= currentVerbosity; + }), + [uiState.history, currentVerbosity], + ); + const historyItems = useMemo( () => - uiState.history.map((h) => ( + filteredHistory.map((h) => ( { /> )), [ - uiState.history, + filteredHistory, mainAreaWidth, staticAreaMaxItemHeight, uiState.slashCommands, @@ -116,10 +134,10 @@ export const MainContent = () => { const virtualizedData = useMemo( () => [ { type: 'header' as const }, - ...uiState.history.map((item) => ({ type: 'history' as const, item })), + ...filteredHistory.map((item) => ({ type: 'history' as const, item })), { type: 'pending' as const }, ], - [uiState.history], + [filteredHistory], ); const renderItem = useCallback( diff --git a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap index 72a031d7f3..7cfa551edf 100644 --- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap @@ -1,28 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = ` -" - ███ █████████ -░░░███ ███░░░░░███ - ░░░███ ███ ░░░ - ░░░███░███ - ███░ ░███ █████ - ███░ ░░███ ░░███ - ███░ ░░█████████ -░░░ ░░░░░░░░░ - -Tips for getting started: -1. Ask questions, edit files, or run commands. -2. Be specific for the best results. -3. Create GEMINI.md files to customize your interactions with Gemini. -4. /help for more information. - -Action Required (was prompted): - -? confirming_tool Confirming tool description -" -`; - exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = ` " ███ █████████ @@ -39,14 +16,21 @@ Tips for getting started: 2. Be specific for the best results. 3. Create GEMINI.md files to customize your interactions with Gemini. 4. /help for more information. -╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool1 Description for tool 1 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯ -╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool2 Description for tool 2 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯" +╭─────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool1 Description for tool 1 │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────╯ + +╭─────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool2 Description for tool 2 │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────╯ + +╭─────────────────────────────────────────────────────────────────────────────╮ +│ o tool3 Description for tool 3 │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────╯ +" `; exports[`AlternateBufferQuittingDisplay > renders with empty history and no pending items > empty 1`] = ` @@ -83,14 +67,16 @@ Tips for getting started: 2. Be specific for the best results. 3. Create GEMINI.md files to customize your interactions with Gemini. 4. /help for more information. -╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool1 Description for tool 1 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯ -╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool2 Description for tool 2 │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯" +╭─────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool1 Description for tool 1 │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────╯ + +╭─────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool2 Description for tool 2 │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────╯ +" `; exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_pending_no_history 1`] = ` @@ -108,7 +94,12 @@ Tips for getting started: 1. Ask questions, edit files, or run commands. 2. Be specific for the best results. 3. Create GEMINI.md files to customize your interactions with Gemini. -4. /help for more information." +4. /help for more information. +╭─────────────────────────────────────────────────────────────────────────────╮ +│ o tool3 Description for tool 3 │ +│ │ +╰─────────────────────────────────────────────────────────────────────────────╯ +" `; exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = ` @@ -127,9 +118,8 @@ Tips for getting started: 2. Be specific for the best results. 3. Create GEMINI.md files to customize your interactions with Gemini. 4. /help for more information. -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ - > Hello Gemini -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ -✦ Hello User! -" + +> Hello Gemini + +✦ Hello User!" `; diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap index c134cde022..cae69d7064 100644 --- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap @@ -28,7 +28,8 @@ AppHeader │ Line 20 │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ - ShowMoreLines" + ShowMoreLines +" `; exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocused shell' 1`] = ` @@ -53,7 +54,8 @@ AppHeader │ Line 19 █ │ │ Line 20 █ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ - ShowMoreLines" + ShowMoreLines +" `; exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = ` @@ -77,7 +79,8 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Con │ Line 19 │ │ Line 20 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ - ShowMoreLines" + ShowMoreLines +" `; exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = ` @@ -101,7 +104,8 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc │ Line 19 │ │ Line 20 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ - ShowMoreLines" + ShowMoreLines +" `; exports[`MainContent > does not constrain height in alternate buffer mode 1`] = ` diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index 786867ccc0..ef63c3e244 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -10,236 +10,9 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ ▲ │ -│ ● Vim Mode false │ -│ Enable Vim keybindings │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ │ │ -│ Enable Auto Update true │ -│ Enable automatic updates. │ -│ │ -│ Enable Prompt Completion false │ -│ Enable AI-powered prompt completion suggestions while typing. │ -│ │ -│ Debug Keystroke Logging false │ -│ Enable debug logging of keystrokes to the console. │ -│ │ -│ Enable Session Cleanup false │ -│ Enable automatic session cleanup │ -│ │ -│ Output Format Text │ -│ The format of the CLI output. Can be \`text\` or \`json\`. │ -│ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ -│ │ -│ ▼ │ -│ │ -│ Apply To │ -│ ● User Settings │ -│ Workspace Settings │ -│ System Settings │ -│ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Settings │ -│ │ -│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ Search to filter │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ ▲ │ -│ ● Vim Mode true* │ -│ Enable Vim keybindings │ -│ │ -│ Enable Auto Update true │ -│ Enable automatic updates. │ -│ │ -│ Enable Prompt Completion false │ -│ Enable AI-powered prompt completion suggestions while typing. │ -│ │ -│ Debug Keystroke Logging false │ -│ Enable debug logging of keystrokes to the console. │ -│ │ -│ Enable Session Cleanup false │ -│ Enable automatic session cleanup │ -│ │ -│ Output Format Text │ -│ The format of the CLI output. Can be \`text\` or \`json\`. │ -│ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ -│ │ -│ ▼ │ -│ │ -│ Apply To │ -│ ● User Settings │ -│ Workspace Settings │ -│ System Settings │ -│ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings disabled' correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Settings │ -│ │ -│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ Search to filter │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ ▲ │ -│ ● Vim Mode false* │ -│ Enable Vim keybindings │ -│ │ -│ Enable Auto Update true* │ -│ Enable automatic updates. │ -│ │ -│ Enable Prompt Completion false* │ -│ Enable AI-powered prompt completion suggestions while typing. │ -│ │ -│ Debug Keystroke Logging false* │ -│ Enable debug logging of keystrokes to the console. │ -│ │ -│ Enable Session Cleanup false │ -│ Enable automatic session cleanup │ -│ │ -│ Output Format Text │ -│ The format of the CLI output. Can be \`text\` or \`json\`. │ -│ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ -│ │ -│ ▼ │ -│ │ -│ Apply To │ -│ ● User Settings │ -│ Workspace Settings │ -│ System Settings │ -│ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[`SettingsDialog > Snapshot Tests > should render 'default state' correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Settings │ -│ │ -│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ Search to filter │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ ▲ │ -│ ● Vim Mode false │ -│ Enable Vim keybindings │ -│ │ -│ Enable Auto Update true │ -│ Enable automatic updates. │ -│ │ -│ Enable Prompt Completion false │ -│ Enable AI-powered prompt completion suggestions while typing. │ -│ │ -│ Debug Keystroke Logging false │ -│ Enable debug logging of keystrokes to the console. │ -│ │ -│ Enable Session Cleanup false │ -│ Enable automatic session cleanup │ -│ │ -│ Output Format Text │ -│ The format of the CLI output. Can be \`text\` or \`json\`. │ -│ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ -│ │ -│ ▼ │ -│ │ -│ Apply To │ -│ ● User Settings │ -│ Workspace Settings │ -│ System Settings │ -│ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[`SettingsDialog > Snapshot Tests > should render 'file filtering settings configured' correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Settings │ -│ │ -│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ Search to filter │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ ▲ │ -│ ● Vim Mode false │ -│ Enable Vim keybindings │ -│ │ -│ Enable Auto Update true │ -│ Enable automatic updates. │ -│ │ -│ Enable Prompt Completion false │ -│ Enable AI-powered prompt completion suggestions while typing. │ -│ │ -│ Debug Keystroke Logging false │ -│ Enable debug logging of keystrokes to the console. │ -│ │ -│ Enable Session Cleanup false │ -│ Enable automatic session cleanup │ -│ │ -│ Output Format Text │ -│ The format of the CLI output. Can be \`text\` or \`json\`. │ -│ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ -│ │ -│ ▼ │ -│ │ -│ Apply To │ -│ ● User Settings │ -│ Workspace Settings │ -│ System Settings │ -│ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Settings │ -│ │ -│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ Search to filter │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ ▲ │ │ Vim Mode false │ │ Enable Vim keybindings │ │ │ @@ -258,57 +31,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec │ Output Format Text │ │ The format of the CLI output. Can be \`text\` or \`json\`. │ │ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ -│ │ -│ ▼ │ -│ │ -│ > Apply To │ -│ ● 1. User Settings │ -│ 2. Workspace Settings │ -│ 3. System Settings │ -│ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; - -exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and number settings' correctly 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ > Settings │ -│ │ -│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ -│ │ Search to filter │ │ -│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ -│ │ -│ ▲ │ -│ ● Vim Mode false* │ -│ Enable Vim keybindings │ -│ │ -│ Enable Auto Update false* │ -│ Enable automatic updates. │ -│ │ -│ Enable Prompt Completion false │ -│ Enable AI-powered prompt completion suggestions while typing. │ -│ │ -│ Debug Keystroke Logging false │ -│ Enable debug logging of keystrokes to the console. │ -│ │ -│ Enable Session Cleanup false │ -│ Enable automatic session cleanup │ -│ │ -│ Output Format Text │ -│ The format of the CLI output. Can be \`text\` or \`json\`. │ -│ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ │ │ │ ▼ │ │ │ @@ -317,12 +41,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb │ Workspace Settings │ │ System Settings │ │ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; -exports[`SettingsDialog > Snapshot Tests > should render 'tools and security settings' correctly 1`] = ` +exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings enabled' correctly 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ │ > Settings │ @@ -332,7 +56,10 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ ▲ │ -│ ● Vim Mode false │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode true* │ │ Enable Vim keybindings │ │ │ │ Enable Auto Update true │ @@ -350,11 +77,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ Output Format Text │ │ The format of the CLI output. Can be \`text\` or \`json\`. │ │ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ │ │ │ ▼ │ │ │ @@ -363,7 +87,283 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ Workspace Settings │ │ System Settings │ │ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings disabled' correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Settings │ +│ │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Search to filter │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ ▲ │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode false* │ +│ Enable Vim keybindings │ +│ │ +│ Enable Auto Update true* │ +│ Enable automatic updates. │ +│ │ +│ Enable Prompt Completion false* │ +│ Enable AI-powered prompt completion suggestions while typing. │ +│ │ +│ Debug Keystroke Logging false* │ +│ Enable debug logging of keystrokes to the console. │ +│ │ +│ Enable Session Cleanup false │ +│ Enable automatic session cleanup │ +│ │ +│ Output Format Text │ +│ The format of the CLI output. Can be \`text\` or \`json\`. │ +│ │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ +│ │ +│ ▼ │ +│ │ +│ Apply To │ +│ ● User Settings │ +│ Workspace Settings │ +│ System Settings │ +│ │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`SettingsDialog > Snapshot Tests > should render 'default state' correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Settings │ +│ │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Search to filter │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ ▲ │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode false │ +│ Enable Vim keybindings │ +│ │ +│ Enable Auto Update true │ +│ Enable automatic updates. │ +│ │ +│ Enable Prompt Completion false │ +│ Enable AI-powered prompt completion suggestions while typing. │ +│ │ +│ Debug Keystroke Logging false │ +│ Enable debug logging of keystrokes to the console. │ +│ │ +│ Enable Session Cleanup false │ +│ Enable automatic session cleanup │ +│ │ +│ Output Format Text │ +│ The format of the CLI output. Can be \`text\` or \`json\`. │ +│ │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ +│ │ +│ ▼ │ +│ │ +│ Apply To │ +│ ● User Settings │ +│ Workspace Settings │ +│ System Settings │ +│ │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`SettingsDialog > Snapshot Tests > should render 'file filtering settings configured' correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Settings │ +│ │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Search to filter │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ ▲ │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode false │ +│ Enable Vim keybindings │ +│ │ +│ Enable Auto Update true │ +│ Enable automatic updates. │ +│ │ +│ Enable Prompt Completion false │ +│ Enable AI-powered prompt completion suggestions while typing. │ +│ │ +│ Debug Keystroke Logging false │ +│ Enable debug logging of keystrokes to the console. │ +│ │ +│ Enable Session Cleanup false │ +│ Enable automatic session cleanup │ +│ │ +│ Output Format Text │ +│ The format of the CLI output. Can be \`text\` or \`json\`. │ +│ │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ +│ │ +│ ▼ │ +│ │ +│ Apply To │ +│ ● User Settings │ +│ Workspace Settings │ +│ System Settings │ +│ │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selector' correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Settings │ +│ │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Search to filter │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ ▲ │ +│ Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode false │ +│ Enable Vim keybindings │ +│ │ +│ Enable Auto Update true │ +│ Enable automatic updates. │ +│ │ +│ Enable Prompt Completion false │ +│ Enable AI-powered prompt completion suggestions while typing. │ +│ │ +│ Debug Keystroke Logging false │ +│ Enable debug logging of keystrokes to the console. │ +│ │ +│ Enable Session Cleanup false │ +│ Enable automatic session cleanup │ +│ │ +│ Output Format Text │ +│ The format of the CLI output. Can be \`text\` or \`json\`. │ +│ │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ +│ │ +│ ▼ │ +│ │ +│ > Apply To │ +│ ● 1. User Settings │ +│ 2. Workspace Settings │ +│ 3. System Settings │ +│ │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and number settings' correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Settings │ +│ │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Search to filter │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ ▲ │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode false* │ +│ Enable Vim keybindings │ +│ │ +│ Enable Auto Update false* │ +│ Enable automatic updates. │ +│ │ +│ Enable Prompt Completion false │ +│ Enable AI-powered prompt completion suggestions while typing. │ +│ │ +│ Debug Keystroke Logging false │ +│ Enable debug logging of keystrokes to the console. │ +│ │ +│ Enable Session Cleanup false │ +│ Enable automatic session cleanup │ +│ │ +│ Output Format Text │ +│ The format of the CLI output. Can be \`text\` or \`json\`. │ +│ │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ +│ │ +│ ▼ │ +│ │ +│ Apply To │ +│ ● User Settings │ +│ Workspace Settings │ +│ System Settings │ +│ │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`SettingsDialog > Snapshot Tests > should render 'tools and security settings' correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Settings │ +│ │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ Search to filter │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ │ +│ ▲ │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode false │ +│ Enable Vim keybindings │ +│ │ +│ Enable Auto Update true │ +│ Enable automatic updates. │ +│ │ +│ Enable Prompt Completion false │ +│ Enable AI-powered prompt completion suggestions while typing. │ +│ │ +│ Debug Keystroke Logging false │ +│ Enable debug logging of keystrokes to the console. │ +│ │ +│ Enable Session Cleanup false │ +│ Enable automatic session cleanup │ +│ │ +│ Output Format Text │ +│ The format of the CLI output. Can be \`text\` or \`json\`. │ +│ │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ +│ │ +│ ▼ │ +│ │ +│ Apply To │ +│ ● User Settings │ +│ Workspace Settings │ +│ System Settings │ +│ │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; @@ -378,7 +378,10 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ ▲ │ -│ ● Vim Mode true* │ +│ ● Preview Features (e.g., models) false │ +│ Enable preview features (e.g., preview models). │ +│ │ +│ Vim Mode true* │ │ Enable Vim keybindings │ │ │ │ Enable Auto Update false* │ @@ -396,11 +399,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ Output Format Text │ │ The format of the CLI output. Can be \`text\` or \`json\`. │ │ │ -│ Auto Theme Switching true │ -│ Automatically switch between default light and dark themes based on terminal backgro… │ -│ │ -│ Terminal Background Polling Interval 60 │ -│ Interval in seconds to poll the terminal background color. │ +│ Verbose Output History true │ +│ Show verbose output history. When enabled, output history will include autonomous to… │ │ │ │ ▼ │ │ │ @@ -409,7 +409,7 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ Workspace Settings │ │ System Settings │ │ │ -│ (Use Enter to select, Ctrl+L to reset, Tab to change focus, Esc to close) │ +│ (Use Enter to select, Tab to change focus, Esc to close) │ │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx new file mode 100644 index 0000000000..8ea4a184b5 --- /dev/null +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx @@ -0,0 +1,125 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { renderWithProviders } from '../../../test-utils/render.js'; +import { DenseToolMessage } from './DenseToolMessage.js'; +import type { ToolResultDisplay } from '../../types.js'; +import { ToolCallStatus } from '../../types.js'; + +describe('DenseToolMessage', () => { + const defaultProps = { + callId: 'call-1', + name: 'test-tool', + description: 'Test description', + status: ToolCallStatus.Success, + resultDisplay: 'Success result', + confirmationDetails: undefined, + isFirst: true, + }; + + it('renders correctly for a successful string result', () => { + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('test-tool'); + expect(output).toContain('Test description'); + expect(output).toContain('→ Success result'); + }); + + it('truncates long string results', () => { + const longResult = 'A'.repeat(200); + const { lastFrame } = renderWithProviders( + , + ); + // Remove all whitespace to check the continuous string content truncation + const output = lastFrame()?.replace(/\s/g, ''); + expect(output).toContain('A'.repeat(117) + '...'); + }); + + it('flattens newlines in string results', () => { + const multilineResult = 'Line 1\nLine 2'; + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('→ Line 1 Line 2'); + }); + + it('renders correctly for file diff results', () => { + const diffResult = { + fileDiff: 'diff content', + fileName: 'test.ts', + filePath: '/path/to/test.ts', + originalContent: 'old content', + newContent: 'new content', + }; + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('→ Diff applied to test.ts'); + }); + + it('renders correctly for todo updates', () => { + const todoResult = { + todos: [], + }; + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('→ Todos updated'); + }); + + it('renders generic output message for unknown object results', () => { + const genericResult = { + some: 'data', + } as unknown as ToolResultDisplay; + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('→ Output received'); + }); + + it('renders correctly for error status with string message', () => { + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('→ Error occurred'); + }); + + it('renders generic failure message for error status without string message', () => { + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).toContain('→ Failed'); + }); + + it('does not render result arrow if resultDisplay is missing', () => { + const { lastFrame } = renderWithProviders( + , + ); + const output = lastFrame(); + expect(output).not.toContain('→'); + }); +}); diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx new file mode 100644 index 0000000000..1c54f2f0fd --- /dev/null +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import { ToolCallStatus } from '../../types.js'; +import type { IndividualToolCallDisplay } from '../../types.js'; +import { ToolStatusIndicator } from './ToolShared.js'; +import { theme } from '../../semantic-colors.js'; + +interface DenseToolMessageProps extends IndividualToolCallDisplay { + isFirst: boolean; +} + +interface FileDiffResult { + fileDiff: string; + fileName: string; +} + +export const DenseToolMessage: React.FC = ({ + name, + description, + status, + resultDisplay, +}) => { + let denseResult: string | undefined; + + if (status === ToolCallStatus.Success && resultDisplay) { + if (typeof resultDisplay === 'string') { + const flattened = resultDisplay.replace(/\n/g, ' ').trim(); + denseResult = + flattened.length > 120 ? flattened.slice(0, 117) + '...' : flattened; + } else if (typeof resultDisplay === 'object') { + if ('fileDiff' in resultDisplay) { + denseResult = `Diff applied to ${(resultDisplay as FileDiffResult).fileName}`; + } else if ('todos' in resultDisplay) { + denseResult = 'Todos updated'; + } else { + // Fallback for AnsiOutput or other objects + denseResult = 'Output received'; + } + } + } else if (status === ToolCallStatus.Error) { + if (typeof resultDisplay === 'string') { + const flattened = resultDisplay.replace(/\n/g, ' ').trim(); + denseResult = + flattened.length > 120 ? flattened.slice(0, 117) + '...' : flattened; + } else { + denseResult = 'Failed'; + } + } + + return ( + + + + + {name} + + + + + {description} + + + {denseResult && ( + + + → {denseResult} + + + )} + + ); +}; diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 118b198edf..c45383fc39 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -18,6 +18,8 @@ import { isShellTool, isThisShellFocused } from './ToolShared.js'; import { ASK_USER_DISPLAY_NAME } from '@google/gemini-cli-core'; import { ShowMoreLines } from '../ShowMoreLines.js'; import { useUIState } from '../../contexts/UIStateContext.js'; +import { useSettings } from '../../contexts/SettingsContext.js'; +import { DenseToolMessage } from './DenseToolMessage.js'; interface ToolGroupMessageProps { groupId: number; @@ -63,6 +65,8 @@ export const ToolGroupMessage: React.FC = ({ const config = useConfig(); const { constrainHeight } = useUIState(); + const { merged: settings } = useSettings(); + const isVerboseMode = settings.output?.verbosity === 'verbose'; const isEventDriven = config.isEventDrivenSchedulerEnabled(); @@ -161,12 +165,25 @@ export const ToolGroupMessage: React.FC = ({ */ width={terminalWidth} paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN} + marginBottom={1} > {visibleToolCalls.map((tool, index) => { const isConfirming = toolAwaitingApproval?.callId === tool.callId; const isFirst = index === 0; const isShellToolCall = isShellTool(tool.name); + // Use dense view if not verbose, not a shell tool (for interactivity), and not confirming (needs prompt) + const useDenseView = + !isVerboseMode && + !isShellToolCall && + tool.status !== ToolCallStatus.Confirming; + + if (useDenseView) { + return ( + + ); + } + const commonProps = { ...tool, availableTerminalHeight: availableTerminalHeightPerToolMessage, @@ -242,22 +259,59 @@ export const ToolGroupMessage: React.FC = ({ })} { /* - We have to keep the bottom border separate so it doesn't get - drawn over by the sticky header directly inside it. - */ - (visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && ( - - ) + We have to keep the bottom border separate so it doesn't get + drawn over by the sticky header directly inside it. + Only render if the last tool was displayed in full/verbose mode, + or if explicitly overridden. + */ + (() => { + if (visibleToolCalls.length === 0) { + if (borderBottomOverride !== undefined) { + return ( + + ); + } + return null; + } + + const lastTool = visibleToolCalls[visibleToolCalls.length - 1]; + const isShell = isShellTool(lastTool.name); + const isConfirming = lastTool.status === ToolCallStatus.Confirming; + + // Logic: If dense view (not verbose, not shell, not confirming), hide border by default + const isDense = !isVerboseMode && !isShell && !isConfirming; + let showBottomBorder = !isDense; + + if (borderBottomOverride !== undefined) { + showBottomBorder = borderBottomOverride; + } + + if (!showBottomBorder) return null; + + return ( + + ); + })() } {(borderBottomOverride ?? true) && visibleToolCalls.length > 0 && ( diff --git a/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx b/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx index eaba97a8eb..7755d882f2 100644 --- a/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolStickyHeaderRegression.test.tsx @@ -110,7 +110,7 @@ describe('ToolMessage Sticky Header Regression', () => { // Scroll down so that tool-1's header should be stuck await act(async () => { - listRef?.scrollBy(5); + listRef?.scrollBy(6); }); // tool-1 header should still be visible because it is sticky @@ -120,10 +120,10 @@ describe('ToolMessage Sticky Header Regression', () => { expect(lastFrame()).toContain('Description for tool-1'); // Content lines 1-4 should be scrolled off expect(lastFrame()).not.toContain('c1-01'); - expect(lastFrame()).not.toContain('c1-04'); - // Line 6 and 7 should be visible (terminalHeight=5 means only 2 lines of content show below 3-line header) - expect(lastFrame()).toContain('c1-06'); + expect(lastFrame()).not.toContain('c1-05'); + // Line 7 and 8 should be visible (terminalHeight=5 means only 2 lines of content show below 3-line header) expect(lastFrame()).toContain('c1-07'); + expect(lastFrame()).toContain('c1-08'); expect(lastFrame()).toMatchSnapshot(); // Scroll further so tool-1 is completely gone and tool-2's header should be stuck diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index 369fa59174..a40802e3dc 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -1,260 +1,238 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[` > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ x Ask User │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ Ask User │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`; - -exports[` > Ask User Filtering > filters out ask_user when status is Executing 1`] = `""`; - -exports[` > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`; - -exports[` > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ other-tool A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" -`; - exports[` > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ test-tool A tool for testing │ -│ │ -│ Test result │ -│ │ -│ ✓ another-tool A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ test-tool A tool for testing │ +│ │ +│ Test result │ +│ │ +│ ✓ another-tool A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Border Color Logic > uses yellow border for shell commands even when successful 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ run_shell_command A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ run_shell_command A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Border Color Logic > uses yellow border when tools are pending 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ o test-tool A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ o test-tool A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ? confirm-tool A tool for testing ← │ -│ │ -│ Test result │ -│ Do you want to proceed? │ -│ Do you want to proceed? │ -│ │ -│ ● 1. Allow once │ -│ 2. Allow for this session │ -│ 3. No, suggest changes (esc) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ? confirm-tool A tool for testing ← │ +│ │ +│ Test result │ +│ Do you want to proceed? │ +│ │ +│ Do you want to proceed? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. No, suggest changes (esc) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Confirmation Handling > renders confirmation with permanent approval enabled 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ? confirm-tool A tool for testing ← │ -│ │ -│ Test result │ -│ Do you want to proceed? │ -│ Do you want to proceed? │ -│ │ -│ ● 1. Allow once │ -│ 2. Allow for this session │ -│ 3. Allow for all future sessions │ -│ 4. No, suggest changes (esc) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ? confirm-tool A tool for testing ← │ +│ │ +│ Test result │ +│ Do you want to proceed? │ +│ │ +│ Do you want to proceed? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. Allow for all future sessions │ +│ 4. No, suggest changes (esc) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Confirmation Handling > shows confirmation dialog for first confirming tool only 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ? first-confirm A tool for testing ← │ -│ │ -│ Test result │ -│ Confirm first tool │ -│ Do you want to proceed? │ -│ │ -│ ● 1. Allow once │ -│ 2. Allow for this session │ -│ 3. No, suggest changes (esc) │ -│ │ -│ │ -│ ? second-confirm A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" -`; - -exports[` > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`; - -exports[` > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`; - -exports[` > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ success-tool A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ? first-confirm A tool for testing ← │ +│ │ +│ Test result │ +│ Confirm first tool │ +│ │ +│ Do you want to proceed? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. No, suggest changes (esc) │ +│ │ +│ │ +│ ? second-confirm A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders empty tool calls array 1`] = `""`; exports[` > Golden Snapshots > renders header when scrolled 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool-1 Description 1. This is a long description that will need to b… │ -│──────────────────────────────────────────────────────────────────────────│ -│ line5 │ █ -│ │ █ -│ ✓ tool-2 Description 2 │ █ -│ │ █ -│ line1 │ █ -│ line2 │ █ -╰──────────────────────────────────────────────────────────────────────────╯ █" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool-1 Description 1. This is a long description that will need to be tr… │ +│──────────────────────────────────────────────────────────────────────────────│ +│ │ ▄ +│ ✓ tool-2 Description 2 │ █ +│ │ █ +│ line1 │ █ +│ line2 │ █ +╰──────────────────────────────────────────────────────────────────────────────╯ █ + █" `; exports[` > Golden Snapshots > renders mixed tool calls including shell command 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ read_file Read a file │ -│ │ -│ Test result │ -│ │ -│ ⊷ run_shell_command Run command │ -│ │ -│ Test result │ -│ │ -│ o write_file Write to file │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ read_file Read a file │ +│ │ +│ Test result │ +│ │ +│ ⊷ run_shell_command Run command │ +│ │ +│ Test result │ +│ │ +│ o write_file Write to file │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders multiple tool calls with different statuses 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ successful-tool This tool succeeded │ -│ │ -│ Test result │ -│ │ -│ o pending-tool This tool is pending │ -│ │ -│ Test result │ -│ │ -│ x error-tool This tool failed │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ successful-tool This tool succeeded │ +│ │ +│ Test result │ +│ │ +│ o pending-tool This tool is pending │ +│ │ +│ Test result │ +│ │ +│ x error-tool This tool failed │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders shell command with yellow border 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ run_shell_command Execute shell command │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ run_shell_command Execute shell command │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders single successful tool call 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ test-tool A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ test-tool A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders tool call awaiting confirmation 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ? confirmation-tool This tool needs confirmation ← │ -│ │ -│ Test result │ -│ Are you sure you want to proceed? │ -│ Do you want to proceed? │ -│ │ -│ ● 1. Allow once │ -│ 2. Allow for this session │ -│ 3. No, suggest changes (esc) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ? confirmation-tool This tool needs confirmation ← │ +│ │ +│ Test result │ +│ Are you sure you want to proceed? │ +│ │ +│ Do you want to proceed? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. No, suggest changes (esc) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders tool call with outputFile 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool-with-file Tool that saved output to file │ -│ │ -│ Test result │ -│ Output too long and was saved to: /path/to/output.txt │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool-with-file Tool that saved output to file │ +│ │ +│ Test result │ +│ Output too long and was saved to: /path/to/output.txt │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders two tool groups where only the last line of the previous group is visible 1`] = ` -"╰──────────────────────────────────────────────────────────────────────────╯ -╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool-2 Description 2 │ -│ │ ▄ -│ line1 │ █ -╰──────────────────────────────────────────────────────────────────────────╯ █" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool-2 Description 2 │ +│ │ +│ line1 │ ▄ +╰──────────────────────────────────────────────────────────────────────────────╯ █ + █" `; exports[` > Golden Snapshots > renders when not focused 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ test-tool A tool for testing │ -│ │ -│ Test result │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ test-tool A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders with limited terminal height 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ tool-with-result Tool with output │ -│ │ -│ This is a long result that might need height constraints │ -│ │ -│ ✓ another-tool Another tool │ -│ │ -│ More output here │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool-with-result Tool with output │ +│ │ +│ This is a long result that might need height constraints │ +│ │ +│ ✓ another-tool Another tool │ +│ │ +│ More output here │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; exports[` > Golden Snapshots > renders with narrow terminal width 1`] = ` -"╭──────────────────────────────────╮ -│ ✓ very-long-tool-name-that-mig… │ -│ │ -│ Test result │ -╰──────────────────────────────────╯" +"╭──────────────────────────────────────╮ +│ ✓ very-long-tool-name-that-might-w… │ +│ │ +│ Test result │ +╰──────────────────────────────────────╯ +" `; exports[` > Height Calculation > calculates available height correctly with multiple tools with results 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────╮ -│ ✓ test-tool A tool for testing │ -│ │ -│ Result 1 │ -│ │ -│ ✓ test-tool A tool for testing │ -│ │ -│ Result 2 │ -│ │ -│ ✓ test-tool A tool for testing │ -│ │ -╰──────────────────────────────────────────────────────────────────────────╯" +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ✓ test-tool A tool for testing │ +│ │ +│ Result 1 │ +│ │ +│ ✓ test-tool A tool for testing │ +│ │ +│ Result 2 │ +│ │ +│ ✓ test-tool A tool for testing │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────╯ +" `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap index 58cb3697f3..1a3a8d6117 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap @@ -2,7 +2,7 @@ exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 1`] = ` "╭────────────────────────────────────────────────────────────────────────╮ █ -│ ✓ Shell Command Description for Shell Command │ █ +│ ✓ Shell Command Description for Shell Command │ ▀ │ │ │ shell-01 │ │ shell-02 │" @@ -10,7 +10,7 @@ exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage i exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 2`] = ` "╭────────────────────────────────────────────────────────────────────────╮ -│ ✓ Shell Command Description for Shell Command │ ▄ +│ ✓ Shell Command Description for Shell Command │ │────────────────────────────────────────────────────────────────────────│ █ │ shell-06 │ ▀ │ shell-07 │" @@ -28,14 +28,14 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa "╭────────────────────────────────────────────────────────────────────────╮ │ ✓ tool-1 Description for tool-1 │ █ │────────────────────────────────────────────────────────────────────────│ -│ c1-06 │ -│ c1-07 │" +│ c1-07 │ +│ c1-08 │" `; exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessages in a ToolGroupMessage in a ScrollableList have sticky headers 3`] = ` "│ │ │ ✓ tool-2 Description for tool-2 │ │────────────────────────────────────────────────────────────────────────│ -│ c2-10 │ -╰────────────────────────────────────────────────────────────────────────╯ █" +╰────────────────────────────────────────────────────────────────────────╯ + █" `; diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index 860bece5d8..441f7e6665 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -8,7 +8,7 @@ import type { HistoryItemWithoutId, IndividualToolCallDisplay, } from '../types.js'; -import { ToolCallStatus } from '../types.js'; +import { ToolCallStatus, Verbosity } from '../types.js'; import { useCallback, useReducer, useRef, useEffect } from 'react'; import type { AnsiOutput, Config, GeminiClient } from '@google/gemini-cli-core'; import { isBinary, ShellExecutionService } from '@google/gemini-cli-core'; @@ -480,10 +480,14 @@ export const useShellCommandProcessor = ( resultDisplay: finalOutput, }; + // Add the complete, contextual result to the local UI history. + // We skip this for cancelled commands because useGeminiStream handles the + // immediate addition of the cancelled item to history to prevent flickering/duplicates. if (finalStatus !== ToolCallStatus.Canceled) { addItemToHistory( { type: 'tool_group', + verbosity: Verbosity.INFO, tools: [finalToolDisplay], } as HistoryItemWithoutId, userMessageTimestamp, diff --git a/packages/cli/src/ui/hooks/toolMapping.test.ts b/packages/cli/src/ui/hooks/toolMapping.test.ts index 16900f3ad7..6fcded6a3c 100644 --- a/packages/cli/src/ui/hooks/toolMapping.test.ts +++ b/packages/cli/src/ui/hooks/toolMapping.test.ts @@ -19,7 +19,7 @@ import { type WaitingToolCall, type CancelledToolCall, } from '@google/gemini-cli-core'; -import { ToolCallStatus } from '../types.js'; +import { ToolCallStatus, Verbosity } from '../types.js'; describe('toolMapping', () => { beforeEach(() => { @@ -274,5 +274,27 @@ describe('toolMapping', () => { expect(result.tools[0].resultDisplay).toBeUndefined(); expect(result.tools[0].status).toBe(ToolCallStatus.Pending); }); + + it('sets verbosity to INFO for client-initiated tools', () => { + const toolCall: ScheduledToolCall = { + status: 'scheduled', + request: { ...mockRequest, isClientInitiated: true }, + tool: mockTool, + invocation: mockInvocation, + }; + const result = mapToDisplay(toolCall); + expect(result.verbosity).toBe(Verbosity.INFO); + }); + + it('sets verbosity to undefined (defaulting to VERBOSE) for autonomous tools', () => { + const toolCall: ScheduledToolCall = { + status: 'scheduled', + request: { ...mockRequest, isClientInitiated: false }, + tool: mockTool, + invocation: mockInvocation, + }; + const result = mapToDisplay(toolCall); + expect(result.verbosity).toBeUndefined(); + }); }); }); diff --git a/packages/cli/src/ui/hooks/toolMapping.ts b/packages/cli/src/ui/hooks/toolMapping.ts index e83fb583bf..6de60c4f25 100644 --- a/packages/cli/src/ui/hooks/toolMapping.ts +++ b/packages/cli/src/ui/hooks/toolMapping.ts @@ -14,6 +14,7 @@ import { } from '@google/gemini-cli-core'; import { ToolCallStatus, + Verbosity, type HistoryItemToolGroup, type IndividualToolCallDisplay, } from '../types.js'; @@ -54,6 +55,9 @@ export function mapToDisplay( ): HistoryItemToolGroup { const toolCalls = Array.isArray(toolOrTools) ? toolOrTools : [toolOrTools]; const { borderTop, borderBottom } = options; + const isClientInitiated = toolCalls.some( + (tc) => tc.request.isClientInitiated, + ); const toolDisplays = toolCalls.map((call): IndividualToolCallDisplay => { let description: string; @@ -129,6 +133,7 @@ export function mapToDisplay( return { type: 'tool_group', + verbosity: isClientInitiated ? Verbosity.INFO : undefined, tools: toolDisplays, borderTop, borderBottom, diff --git a/packages/cli/src/ui/hooks/useHistoryManager.test.ts b/packages/cli/src/ui/hooks/useHistoryManager.test.ts index 696f9d60c0..d91bf67a7c 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.test.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.test.ts @@ -8,7 +8,12 @@ import { describe, it, expect } from 'vitest'; import { act } from 'react'; import { renderHook } from '../../test-utils/render.js'; import { useHistory } from './useHistoryManager.js'; -import type { HistoryItem } from '../types.js'; +import type { + HistoryItem, + HistoryItemWithoutId, + IndividualToolCallDisplay, + HistoryItemToolGroup, +} from '../types.js'; describe('useHistoryManager', () => { it('should initialize with an empty history', () => { @@ -255,4 +260,67 @@ describe('useHistoryManager', () => { expect(result.current.history[0].type).toBe('info'); }); }); + + it('should store all items regardless of verbosity level (filtering is done at render time)', () => { + // @ts-expect-error - verbosity prop was removed, but we want to ensure it's ignored if passed by mistake + const { result } = renderHook(() => useHistory({ verbosity: 'info' })); + const timestamp = Date.now(); + const verboseItem: HistoryItemWithoutId = { + type: 'verbose', + text: 'Hidden detail', + verbosity: 3, // Verbosity.VERBOSE + }; + + act(() => { + result.current.addItem(verboseItem, timestamp); + }); + + expect(result.current.history).toHaveLength(1); + expect(result.current.history[0]).toEqual( + expect.objectContaining({ + text: 'Hidden detail', + }), + ); + }); + + it('should merge consecutive tool_group items', () => { + const { result } = renderHook(() => useHistory()); + const timestamp = Date.now(); + const toolGroup1: HistoryItemWithoutId = { + type: 'tool_group', + tools: [{ callId: '1', name: 'tool-a' } as IndividualToolCallDisplay], + }; + const toolGroup2: HistoryItemWithoutId = { + type: 'tool_group', + tools: [{ callId: '2', name: 'tool-b' } as IndividualToolCallDisplay], + }; + const userMessage: HistoryItemWithoutId = { + type: 'user', + text: 'hello', + }; + const toolGroup3: HistoryItemWithoutId = { + type: 'tool_group', + tools: [{ callId: '3', name: 'tool-c' } as IndividualToolCallDisplay], + }; + + act(() => { + result.current.addItem(toolGroup1, timestamp); + result.current.addItem(toolGroup2, timestamp + 1); + result.current.addItem(userMessage, timestamp + 2); + result.current.addItem(toolGroup3, timestamp + 3); + }); + + expect(result.current.history).toHaveLength(3); + expect(result.current.history[0].type).toBe('tool_group'); + const firstItem = result.current.history[0] as HistoryItemToolGroup; + expect(firstItem.tools).toHaveLength(2); + expect(firstItem.tools[0].name).toBe('tool-a'); + expect(firstItem.tools[1].name).toBe('tool-b'); + + expect(result.current.history[1].type).toBe('user'); + + const thirdItem = result.current.history[2] as HistoryItemToolGroup; + expect(thirdItem.type).toBe('tool_group'); + expect(thirdItem.tools).toHaveLength(1); + }); }); diff --git a/packages/cli/src/ui/hooks/useHistoryManager.ts b/packages/cli/src/ui/hooks/useHistoryManager.ts index 93f7f01f28..23346cf389 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.ts @@ -66,16 +66,30 @@ export function useHistory({ const newItem: HistoryItem = { ...itemData, id } as HistoryItem; setHistory((prevHistory) => { - if (prevHistory.length > 0) { - const lastItem = prevHistory[prevHistory.length - 1]; - // Prevent adding duplicate consecutive user messages - if ( - lastItem.type === 'user' && - newItem.type === 'user' && - lastItem.text === newItem.text - ) { - return prevHistory; // Don't add the duplicate - } + const lastItem = + prevHistory.length > 0 ? prevHistory[prevHistory.length - 1] : null; + + // If the last item and the new item are both tool groups, merge them. + if ( + lastItem && + lastItem.type === 'tool_group' && + newItem.type === 'tool_group' + ) { + const updatedLastItem: HistoryItem = { + ...lastItem, + tools: [...lastItem.tools, ...newItem.tools], + }; + return [...prevHistory.slice(0, -1), updatedLastItem]; + } + + // Prevent adding duplicate consecutive user messages + if ( + lastItem && + lastItem.type === 'user' && + newItem.type === 'user' && + lastItem.text === newItem.text + ) { + return prevHistory; // Don't add the duplicate } return [...prevHistory, newItem]; }); @@ -83,36 +97,38 @@ export function useHistory({ // Record UI-specific messages, but don't do it if we're actually loading // an existing session. if (!isResuming && chatRecordingService) { + // Safe access to text property + const content = itemData.text || ''; + switch (itemData.type) { case 'compression': + case 'verbose': case 'info': chatRecordingService?.recordMessage({ model: undefined, type: 'info', - content: itemData.text ?? '', + content: content || '', }); break; case 'warning': chatRecordingService?.recordMessage({ model: undefined, type: 'warning', - content: itemData.text ?? '', + content: content || '', }); break; case 'error': chatRecordingService?.recordMessage({ model: undefined, type: 'error', - content: itemData.text ?? '', + content: content || '', }); break; case 'user': - case 'gemini': - case 'gemini_content': - // Core conversation recording handled by GeminiChat. + // User messages are recorded by the input handler break; default: - // Ignore the rest. + // Other types might not need recording or are recorded elsewhere break; } } @@ -128,7 +144,6 @@ export function useHistory({ * rendering all history items in for performance reasons. Only use * if ABSOLUTELY NECESSARY */ - // const updateItem = useCallback( ( id: number, diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 08452c98f5..5cdaa1ed35 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -19,7 +19,7 @@ import type { import type { PartListUnion } from '@google/genai'; import { type ReactNode } from 'react'; -export type { ThoughtSummary, SkillDefinition }; +export type { ThoughtSummary, SkillDefinition, ToolResultDisplay }; export enum AuthState { // Attempting to authenticate or re-authenticate @@ -101,6 +101,7 @@ export const emptyIcon = ' '; export interface HistoryItemBase { text?: string; // Text content for user/gemini/info/error messages + verbosity?: Verbosity; } export type HistoryItemUser = HistoryItemBase & { @@ -135,6 +136,23 @@ export type HistoryItemWarning = HistoryItemBase & { text: string; }; +export type HistoryItemVerbose = HistoryItemBase & { + type: 'verbose'; + text: string; + icon?: string; + color?: string; +}; + +export type HistoryItemDebug = HistoryItemBase & { + type: 'debug'; + text: string; +}; + +export type HistoryItemTrace = HistoryItemBase & { + type: 'trace'; + text: string; +}; + export type HistoryItemAbout = HistoryItemBase & { type: 'about'; cliVersion: string; @@ -318,6 +336,9 @@ export type HistoryItemWithoutId = | HistoryItemInfo | HistoryItemError | HistoryItemWarning + | HistoryItemVerbose + | HistoryItemDebug + | HistoryItemTrace | HistoryItemAbout | HistoryItemHelp | HistoryItemToolGroup @@ -335,11 +356,16 @@ export type HistoryItemWithoutId = | HistoryItemChatList | HistoryItemHooksList; +export type HistoryItemType = HistoryItemWithoutId['type']; + export type HistoryItem = HistoryItemWithoutId & { id: number }; // Message types used by internal command feedback (subset of HistoryItem types) export enum MessageType { INFO = 'info', + VERBOSE = 'verbose', + DEBUG = 'debug', + TRACE = 'trace', ERROR = 'error', WARNING = 'warning', USER = 'user', @@ -360,10 +386,52 @@ export enum MessageType { HOOKS_LIST = 'hooks_list', } +export enum Verbosity { + ERROR = 0, + WARN = 1, + INFO = 2, + VERBOSE = 3, + DEBUG = 4, + TRACE = 5, +} + +export const VERBOSITY_MAPPING: Record = { + error: Verbosity.ERROR, + warning: Verbosity.WARN, + info: Verbosity.INFO, + user: Verbosity.INFO, + gemini: Verbosity.INFO, + gemini_content: Verbosity.INFO, + tool_group: Verbosity.INFO, + user_shell: Verbosity.INFO, + about: Verbosity.INFO, + stats: Verbosity.INFO, + model_stats: Verbosity.INFO, + tool_stats: Verbosity.INFO, + model: Verbosity.INFO, + quit: Verbosity.INFO, + extensions_list: Verbosity.INFO, + tools_list: Verbosity.INFO, + skills_list: Verbosity.INFO, + agents_list: Verbosity.INFO, + mcp_status: Verbosity.INFO, + chat_list: Verbosity.INFO, + hooks_list: Verbosity.INFO, + help: Verbosity.INFO, + verbose: Verbosity.VERBOSE, + compression: Verbosity.VERBOSE, + debug: Verbosity.DEBUG, + trace: Verbosity.TRACE, +}; + // Simplified message structure for internal feedback export type Message = | { - type: MessageType.INFO | MessageType.ERROR | MessageType.USER; + type: + | MessageType.INFO + | MessageType.VERBOSE + | MessageType.ERROR + | MessageType.USER; content: string; // Renamed from text for clarity in this context timestamp: Date; } diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index 68fa8cfb20..57a156e2f1 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -475,10 +475,10 @@ class GrepToolInvocation extends BaseToolInvocation< if (resolvedPath === this.config.getTargetDir() || pathParam === '.') { description += ` within ./`; } else { - const relativePath = makeRelative( - resolvedPath, - this.config.getTargetDir(), - ); + let relativePath = makeRelative(resolvedPath, this.config.getTargetDir()); + if (!relativePath.startsWith('.') && !path.isAbsolute(relativePath)) { + relativePath = `.${path.sep}${relativePath}`; + } description += ` within ${shortenPath(relativePath)}`; } return description; diff --git a/packages/core/src/tools/web-search.test.ts b/packages/core/src/tools/web-search.test.ts index 3812a54879..6e3d17c5bf 100644 --- a/packages/core/src/tools/web-search.test.ts +++ b/packages/core/src/tools/web-search.test.ts @@ -94,9 +94,7 @@ describe('WebSearchTool', () => { expect(result.llmContent).toBe( 'Web search results for "successful query":\n\nHere are your results.', ); - expect(result.returnDisplay).toBe( - 'Search results for "successful query" returned.', - ); + expect(result.returnDisplay).toBe('Search results returned.'); expect(result.sources).toBeUndefined(); }); @@ -177,9 +175,7 @@ Sources: [2] Google (https://google.com)`; expect(result.llmContent).toBe(expectedLlmContent); - expect(result.returnDisplay).toBe( - 'Search results for "grounding query" returned.', - ); + expect(result.returnDisplay).toBe('Search results returned.'); expect(result.sources).toHaveLength(2); }); @@ -249,9 +245,7 @@ Sources: [3] Gemini CLI: your open-source AI agent (https://blog.google/technology/developers/introducing-gemini-cli-open-source-ai-agent/)`; expect(result.llmContent).toBe(expectedLlmContent); - expect(result.returnDisplay).toBe( - 'Search results for "multibyte query" returned.', - ); + expect(result.returnDisplay).toBe('Search results returned.'); expect(result.sources).toHaveLength(3); }); }); diff --git a/packages/core/src/tools/web-search.ts b/packages/core/src/tools/web-search.ts index 4a1a6d0ae8..d2c8bfc897 100644 --- a/packages/core/src/tools/web-search.ts +++ b/packages/core/src/tools/web-search.ts @@ -162,7 +162,7 @@ class WebSearchToolInvocation extends BaseToolInvocation< return { llmContent: `Web search results for "${this.params.query}":\n\n${modifiedResponseText}`, - returnDisplay: `Search results for "${this.params.query}" returned.`, + returnDisplay: 'Search results returned.', sources, }; } catch (error: unknown) { diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 80bc484a3b..a73533d737 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -153,6 +153,14 @@ "default": "text", "type": "string", "enum": ["text", "json"] + }, + "verbosity": { + "title": "Verbose Output History", + "description": "Show verbose output history. When enabled, output history will include autonomous tool calls, additional logs, etc.", + "markdownDescription": "Show verbose output history. When enabled, output history will include autonomous tool calls, additional logs, etc.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `verbose`", + "default": "verbose", + "type": "string", + "enum": ["info", "verbose"] } }, "additionalProperties": false