feat(ui): Re-apply Dense Tool Output features

This commit is contained in:
Jarrod Whelan
2026-02-09 20:46:37 -08:00
parent eb94284256
commit 142ccf2140
27 changed files with 1165 additions and 621 deletions
+2 -2
View File
@@ -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).`
@@ -171,6 +171,18 @@ describe('<HistoryItemDisplay />', () => {
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(
<HistoryItemDisplay {...baseItem} item={item} />,
);
expect(lastFrame()).toContain('⚙ Debug info');
});
it('should escape ANSI codes in text content', () => {
const historyItem: HistoryItem = {
id: 1,
@@ -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<HistoryItemDisplayProps> = ({
color={itemForDisplay.color}
/>
)}
{itemForDisplay.type === 'verbose' && (
<InfoMessage
text={itemForDisplay.text}
icon={itemForDisplay.icon || ' '}
color={theme.text.secondary}
/>
)}
{itemForDisplay.type === 'debug' && (
<InfoMessage
text={itemForDisplay.text}
icon={'⚙ '}
color={theme.text.accent}
/>
)}
{itemForDisplay.type === 'trace' && (
<InfoMessage
text={itemForDisplay.text}
icon={'🔍 '}
color={theme.text.accent}
/>
)}
{itemForDisplay.type === 'warning' && (
<WarningMessage text={itemForDisplay.text} />
)}
@@ -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<typeof import('../contexts/SettingsContext.js')>();
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(<MainContent />, {
uiState: { ...defaultMockUiState, history } as Partial<UIState>,
});
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(<MainContent />, {
uiState: { ...defaultMockUiState, history } as Partial<UIState>,
});
await waitFor(() => expect(lastFrame()).toContain('AppHeader'));
const output = lastFrame();
expect(output).toContain('Visible User Message');
expect(output).toContain('Visible Verbose Log');
});
});
+22 -4
View File
@@ -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) => (
<MemoizedHistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={staticAreaMaxItemHeight}
@@ -67,7 +85,7 @@ export const MainContent = () => {
/>
)),
[
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(
@@ -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!"
`;
@@ -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`] = `
@@ -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)
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
@@ -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(
<DenseToolMessage {...defaultProps} />,
);
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(
<DenseToolMessage {...defaultProps} resultDisplay={longResult} />,
);
// 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(
<DenseToolMessage {...defaultProps} resultDisplay={multilineResult} />,
);
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(
<DenseToolMessage {...defaultProps} resultDisplay={diffResult} />,
);
const output = lastFrame();
expect(output).toContain('→ Diff applied to test.ts');
});
it('renders correctly for todo updates', () => {
const todoResult = {
todos: [],
};
const { lastFrame } = renderWithProviders(
<DenseToolMessage {...defaultProps} resultDisplay={todoResult} />,
);
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(
<DenseToolMessage {...defaultProps} resultDisplay={genericResult} />,
);
const output = lastFrame();
expect(output).toContain('→ Output received');
});
it('renders correctly for error status with string message', () => {
const { lastFrame } = renderWithProviders(
<DenseToolMessage
{...defaultProps}
status={ToolCallStatus.Error}
resultDisplay="Error occurred"
/>,
);
const output = lastFrame();
expect(output).toContain('→ Error occurred');
});
it('renders generic failure message for error status without string message', () => {
const { lastFrame } = renderWithProviders(
<DenseToolMessage
{...defaultProps}
status={ToolCallStatus.Error}
resultDisplay={undefined}
/>,
);
const output = lastFrame();
expect(output).toContain('→ Failed');
});
it('does not render result arrow if resultDisplay is missing', () => {
const { lastFrame } = renderWithProviders(
<DenseToolMessage
{...defaultProps}
status={ToolCallStatus.Pending}
resultDisplay={undefined}
/>,
);
const output = lastFrame();
expect(output).not.toContain('→');
});
});
@@ -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<DenseToolMessageProps> = ({
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 (
<Box marginLeft={3} flexDirection="row" flexWrap="wrap">
<ToolStatusIndicator status={status} name={name} />
<Box maxWidth={25} flexShrink={1} flexGrow={0}>
<Text color={theme.text.primary} bold wrap="truncate-end">
{name}
</Text>
</Box>
<Box marginLeft={1} flexShrink={1} flexGrow={0}>
<Text color={theme.text.secondary} wrap="truncate-end">
{description}
</Text>
</Box>
{denseResult && (
<Box marginLeft={1} flexGrow={1}>
<Text color={theme.text.accent} wrap="wrap">
{denseResult}
</Text>
</Box>
)}
</Box>
);
};
@@ -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<ToolGroupMessageProps> = ({
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<ToolGroupMessageProps> = ({
*/
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 (
<DenseToolMessage key={tool.callId} {...tool} isFirst={isFirst} />
);
}
const commonProps = {
...tool,
availableTerminalHeight: availableTerminalHeightPerToolMessage,
@@ -242,22 +259,59 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
})}
{
/*
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) && (
<Box
height={0}
width={contentWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={borderBottomOverride ?? true}
borderColor={borderColor}
borderDimColor={borderDimColor}
borderStyle="round"
/>
)
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 (
<Box
height={0}
width={contentWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={borderBottomOverride}
borderColor={borderColor}
borderDimColor={borderDimColor}
borderStyle="round"
/>
);
}
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 (
<Box
height={0}
width={contentWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={true}
borderColor={borderColor}
borderDimColor={borderDimColor}
borderStyle="round"
/>
);
})()
}
{(borderBottomOverride ?? true) && visibleToolCalls.length > 0 && (
<Box paddingX={1}>
@@ -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
@@ -1,260 +1,238 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ x Ask User │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ Ask User │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Executing 1`] = `""`;
exports[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
exports[`<ToolGroupMessage /> > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ other-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > renders nothing when only tool is in-progress AskUser with borderBottom=false 1`] = `""`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > Golden Snapshots > renders when not focused 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯"
"╭──────────────────────────────────────────────────────────────────────────────
│ ✓ test-tool A tool for testing
│ Test result
╰──────────────────────────────────────────────────────────────────────────────
"
`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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
╰──────────────────────────────────────────────────────────────────────────────
"
`;
@@ -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 │
╰────────────────────────────────────────────────────────────────────────╯ █"
╰────────────────────────────────────────────────────────────────────────╯
█"
`;
@@ -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,
+23 -1
View File
@@ -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();
});
});
});
+5
View File
@@ -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,
@@ -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);
});
});
+33 -18
View File
@@ -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 <Static /> for performance reasons. Only use
* if ABSOLUTELY NECESSARY
*/
//
const updateItem = useCallback(
(
id: number,
+70 -2
View File
@@ -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<HistoryItemType, Verbosity> = {
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;
}