From 03de28960fb997dddc55b7e18af09e3f5457725e Mon Sep 17 00:00:00 2001 From: Jarrod Whelan Date: Wed, 11 Feb 2026 02:31:00 -0800 Subject: [PATCH] feat(cli): implement modular dense tool output with summary/payload pattern - Support structured summaries and payloads in ToolGroupMessage/DenseToolMessage. - Add specialized box-layout rendering for file and read-many-files tools. - Refine tool state management in useGeminiStream during cancellations. - Update UI tests and snapshots to reflect new compact rendering styles. --- .../AlternateBufferQuittingDisplay.test.tsx | 11 + ...ternateBufferQuittingDisplay.test.tsx.snap | 20 +- .../SettingsDialog.test.tsx.snap | 54 ++--- .../messages/DenseToolMessage.test.tsx | 1 + .../components/messages/DenseToolMessage.tsx | 107 ++++++---- .../components/messages/DiffRenderer.test.tsx | 45 ++-- .../messages/ToolGroupMessage.test.tsx | 136 ++++++++++++ .../components/messages/ToolGroupMessage.tsx | 40 +++- .../ToolGroupMessage.test.tsx.snap | 201 ++++++++++++++---- .../cli/src/ui/hooks/useGeminiStream.test.tsx | 21 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 2 +- 11 files changed, 479 insertions(+), 159 deletions(-) diff --git a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx index 68b662df7b..0b01f288db 100644 --- a/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx +++ b/packages/cli/src/ui/components/AlternateBufferQuittingDisplay.test.tsx @@ -8,6 +8,7 @@ import { renderWithProviders, persistentStateMock, } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AlternateBufferQuittingDisplay } from './AlternateBufferQuittingDisplay.js'; import { ToolCallStatus } from '../types.js'; @@ -90,6 +91,10 @@ const mockPendingHistoryItems: HistoryItemWithoutId[] = [ ]; describe('AlternateBufferQuittingDisplay', () => { + const mockSettings = createMockSettings({ + ui: { enableCompactToolOutput: false }, + }); + beforeEach(() => { vi.clearAllMocks(); }); @@ -116,6 +121,7 @@ describe('AlternateBufferQuittingDisplay', () => { history: mockHistory, pendingHistoryItems: mockPendingHistoryItems, }, + settings: mockSettings, }, ); expect(lastFrame()).toMatchSnapshot('with_history_and_pending'); @@ -131,6 +137,7 @@ describe('AlternateBufferQuittingDisplay', () => { history: [], pendingHistoryItems: [], }, + settings: mockSettings, }, ); expect(lastFrame()).toMatchSnapshot('empty'); @@ -146,6 +153,7 @@ describe('AlternateBufferQuittingDisplay', () => { history: mockHistory, pendingHistoryItems: [], }, + settings: mockSettings, }, ); expect(lastFrame()).toMatchSnapshot('with_history_no_pending'); @@ -161,6 +169,7 @@ describe('AlternateBufferQuittingDisplay', () => { history: [], pendingHistoryItems: mockPendingHistoryItems, }, + settings: mockSettings, }, ); expect(lastFrame()).toMatchSnapshot('with_pending_no_history'); @@ -196,6 +205,7 @@ describe('AlternateBufferQuittingDisplay', () => { history: [], pendingHistoryItems, }, + settings: mockSettings, }, ); const output = lastFrame(); @@ -219,6 +229,7 @@ describe('AlternateBufferQuittingDisplay', () => { history, pendingHistoryItems: [], }, + settings: mockSettings, }, ); expect(lastFrame()).toMatchSnapshot('with_user_gemini_messages'); 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 d108cb8911..e539cec78e 100644 --- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap @@ -39,9 +39,15 @@ 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 +╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool1 Description for tool 1 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ - ✓ tool2 Description for tool 2 +╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool2 Description for tool 2 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; @@ -79,9 +85,15 @@ 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 +╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool1 Description for tool 1 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ - ✓ tool2 Description for tool 2 +╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool2 Description for tool 2 │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; 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 2d05a2bf83..786867ccc0 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -28,12 +28,12 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v │ 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… │ -│ │ │ 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 │ @@ -74,12 +74,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings │ 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… │ -│ │ │ 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 │ @@ -120,12 +120,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d │ 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… │ -│ │ │ 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 │ @@ -166,12 +166,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct │ 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… │ -│ │ │ 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 │ @@ -212,12 +212,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting │ 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… │ -│ │ │ 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 │ @@ -258,12 +258,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec │ 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… │ -│ │ │ 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 │ @@ -304,12 +304,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb │ 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… │ -│ │ │ 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 │ @@ -350,12 +350,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ 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… │ -│ │ │ 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 │ @@ -396,12 +396,12 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ 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… │ -│ │ │ 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 │ diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx index f255a1a690..0861c60e8e 100644 --- a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx @@ -222,6 +222,7 @@ describe('DenseToolMessage', () => { ); const output = lastFrame(); expect(output).toContain('→ Found 2 matches'); + // Matches are rendered in a secondary list for high-signal summaries expect(output).toContain('file1.ts:10: match 1'); expect(output).toContain('file2.ts:20: match 2'); }); diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx index 67af65af63..f9fb3f842c 100644 --- a/packages/cli/src/ui/components/messages/DenseToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.tsx @@ -39,13 +39,16 @@ const isFileDiff = (res: unknown): res is FileDiff => const isGrepResult = (res: unknown): res is GrepResult => typeof res === 'object' && res !== null && - 'matches' in res && - 'summary' in res; + 'summary' in res && + ('matches' in res || 'payload' in res); const isListResult = ( res: unknown, ): res is ListDirectoryResult | ReadManyFilesResult => - typeof res === 'object' && res !== null && 'files' in res && 'summary' in res; + typeof res === 'object' && + res !== null && + 'summary' in res && + ('files' in res || 'include' in res); const hasPayload = ( res: unknown, @@ -63,22 +66,25 @@ const isTodoList = (res: unknown): res is { todos: unknown[] } => */ const RenderItemsList: React.FC<{ - items: string[]; + items?: string[]; maxVisible?: number; -}> = ({ items, maxVisible = 20 }) => ( - - {items.slice(0, maxVisible).map((item, i) => ( - - {item} - - ))} - {items.length > maxVisible && ( - - ... and {items.length - maxVisible} more - - )} - -); +}> = ({ items, maxVisible = 20 }) => { + if (!items || items.length === 0) return null; + return ( + + {items.slice(0, maxVisible).map((item, i) => ( + + {item} + + ))} + {items.length > maxVisible && ( + + ... and {items.length - maxVisible} more + + )} + + ); +}; /** * --- SCENARIO LOGIC (Pure Functions) --- @@ -144,14 +150,15 @@ function getListResultData( originalDescription?: string, ): ViewParts { let description = originalDescription; - const items: string[] = result.files; - const maxVisible = 20; + const items: string[] = result.files ?? []; + const maxVisible = 10; // Enhance with ReadManyFiles specific data if present const rmf = result as ReadManyFilesResult; if (toolName === 'ReadManyFiles' && rmf.include) { const includePatterns = rmf.include.join(', '); description = `Attempting to read files from ${includePatterns}`; + result.summary = `Read ${items.length} file(s)`; } const summary = → {result.summary}; @@ -165,21 +172,23 @@ function getListResultData( ? `Excluded patterns: ${rmf.excludes.slice(0, 3).join(', ')}${rmf.excludes.length > 3 ? '...' : ''}` : undefined; - const payload = ( - - - {skippedText && ( - - {skippedText} - - )} - {excludedText && ( - - {excludedText} - - )} - - ); + const hasItems = items.length > 0; + const payload = + hasItems || skippedText || excludedText ? ( + + {hasItems && } + {skippedText && ( + + {skippedText} + + )} + {excludedText && ( + + {excludedText} + + )} + + ) : undefined; return { description, summary, payload }; } @@ -200,16 +209,19 @@ function getGenericSuccessData( ); } else if (isGrepResult(resultDisplay)) { summary = → {resultDisplay.summary}; - payload = ( - - `${m.filePath}:${m.lineNumber}: ${m.line.trim()}`, - )} - maxVisible={10} - /> - - ); + const matches = resultDisplay.matches ?? []; + if (matches.length > 0) { + payload = ( + + `${m.filePath}:${m.lineNumber}: ${m.line.trim()}`, + )} + maxVisible={10} + /> + + ); + } } else if (isTodoList(resultDisplay)) { summary = ( @@ -280,6 +292,11 @@ export const DenseToolMessage: React.FC = (props) => { if (isListResult(resultDisplay)) { return getListResultData(resultDisplay, name, originalDescription); } + + if (isGrepResult(resultDisplay)) { + return getGenericSuccessData(resultDisplay, originalDescription); + } + if (status === ToolCallStatus.Success && resultDisplay) { return getGenericSuccessData(resultDisplay, originalDescription); } diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx index 9063606146..c6f0a9f7df 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx @@ -45,14 +45,13 @@ index 0000000..e69de29 { useAlternateBuffer }, ); await waitFor(() => - expect(mockColorizeCode).toHaveBeenCalledWith({ - code: 'print("hello world")', - language: 'python', - availableHeight: undefined, - maxWidth: 80, - theme: undefined, - settings: expect.anything(), - }), + expect(mockColorizeCode).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'print("hello world")', + language: 'python', + maxWidth: 80, + }), + ), ); }); @@ -77,14 +76,13 @@ index 0000000..e69de29 { useAlternateBuffer }, ); await waitFor(() => - expect(mockColorizeCode).toHaveBeenCalledWith({ - code: 'some content', - language: null, - availableHeight: undefined, - maxWidth: 80, - theme: undefined, - settings: expect.anything(), - }), + expect(mockColorizeCode).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'some content', + language: null, + maxWidth: 80, + }), + ), ); }); @@ -105,14 +103,13 @@ index 0000000..e69de29 { useAlternateBuffer }, ); await waitFor(() => - expect(mockColorizeCode).toHaveBeenCalledWith({ - code: 'some text content', - language: null, - availableHeight: undefined, - maxWidth: 80, - theme: undefined, - settings: expect.anything(), - }), + expect(mockColorizeCode).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'some text content', + language: null, + maxWidth: 80, + }), + ), ); }); diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx index 5368684ea2..23a04131c6 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx @@ -754,4 +754,140 @@ describe('', () => { unmount(); }); }); + + describe('Compact Tool Output (Dense Mode)', () => { + const compactSettings = createMockSettings({ + ui: { enableCompactToolOutput: true }, + }); + + it('renders single tool call compactly', () => { + const toolCalls = [ + createToolCall({ + name: 'read_file', + description: 'packages/cli/src/app.tsx', + resultDisplay: 'Read 150 lines', + }), + ]; + const { lastFrame, unmount } = renderWithProviders( + , + { + config: baseMockConfig, + settings: compactSettings, + uiState: { + pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], + }, + }, + ); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + + it('renders multiple tool calls compactly without boxes', () => { + const toolCalls = [ + createToolCall({ + callId: 't1', + name: 'read_file', + description: 'file1.ts', + resultDisplay: 'Success', + }), + createToolCall({ + callId: 't2', + name: 'grep_search', + description: 'search term', + resultDisplay: { + summary: 'Found 3 matches', + matches: [ + { filePath: 'f1.ts', lineNumber: 10, line: 'match 1' }, + { filePath: 'f2.ts', lineNumber: 20, line: 'match 2' }, + { filePath: 'f3.ts', lineNumber: 30, line: 'match 3' }, + ], + }, + }), + ]; + const { lastFrame, unmount } = renderWithProviders( + , + { + config: baseMockConfig, + settings: compactSettings, + uiState: { + pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], + }, + }, + ); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + + it('renders mixed boxed (shell) and dense tools correctly', () => { + const toolCalls = [ + createToolCall({ + callId: 'shell-1', + name: 'run_shell_command', + description: 'npm test', + status: ToolCallStatus.Success, + resultDisplay: 'All tests passed', + }), + createToolCall({ + callId: 'file-1', + name: 'write_file', + description: 'packages/core/index.ts', + status: ToolCallStatus.Success, + resultDisplay: 'File updated', + }), + ]; + const { lastFrame, unmount } = renderWithProviders( + , + { + config: baseMockConfig, + settings: compactSettings, + uiState: { + pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], + }, + }, + ); + // Boxed shell tool should have its own bottom border before the dense tool + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + + it('renders confirming tools as boxed even in compact mode', () => { + const toolCalls = [ + createToolCall({ + callId: 'confirm-1', + name: 'write_file', + description: 'critical_file.ts', + status: ToolCallStatus.Confirming, + confirmationDetails: { + type: 'edit', + title: 'Apply edit', + fileName: 'critical_file.ts', + filePath: '/path/to/critical_file.ts', + fileDiff: 'diff...', + originalContent: 'old', + newContent: 'new', + onConfirm: vi.fn(), + }, + }), + ]; + // When confirming, it should be boxed for visibility/interactivity + const mockConfigNoEventDriven = makeFakeConfig({ + ...baseMockConfig, + enableEventDrivenScheduler: false, + }); + + const { lastFrame, unmount } = renderWithProviders( + , + { + config: mockConfigNoEventDriven, + settings: compactSettings, + uiState: { + pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }], + }, + }, + ); + expect(lastFrame()).toContain('Apply this change?'); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }); + }); }); diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 5fa6a6f34f..ef580aaf64 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -190,6 +190,13 @@ export const ToolGroupMessage: React.FC = ({ !isShellToolCall && tool.status !== ToolCallStatus.Confirming; + const nextTool = visibleToolCalls[index + 1]; + const nextIsDense = + nextTool && + compactMode && + !isShellTool(nextTool.name) && + nextTool.status !== ToolCallStatus.Confirming; + if (useDenseView) { return ( = ({ )} )} + {/* If the NEXT tool is dense, we must close THIS tool's box now */} + {nextIsDense && ( + + )} ); })} @@ -336,12 +357,19 @@ export const ToolGroupMessage: React.FC = ({ /> ); })()} - {compactMode - ? null - : (borderBottomOverride ?? true) && - visibleToolCalls.length > 0 && ( - - )} + {(() => { + const lastTool = visibleToolCalls[visibleToolCalls.length - 1]; + const isShell = lastTool && isShellTool(lastTool.name); + const isConfirming = + lastTool && lastTool.status === ToolCallStatus.Confirming; + const isDense = compactMode && lastTool && !isShell && !isConfirming; + + return isDense + ? null + : (borderBottomOverride ?? true) && visibleToolCalls.length > 0 && ( + + ); + })()} ); }; 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 1908088f75..1d98645ace 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,12 +1,20 @@ // 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 A tool for testing → Test result +"╭──────────────────────────────────────────────────────────────────────────╮ +│ x Ask User │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; exports[` > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = ` -" ✓ Ask User A tool for testing → Test result +"╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ Ask User │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; @@ -17,13 +25,24 @@ exports[` > Ask User Filtering > filters out ask_user when s 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 +"╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ 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 │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; @@ -36,7 +55,60 @@ exports[` > Border Color Logic > uses yellow border for shel " `; -exports[` > Border Color Logic > uses yellow border when tools are pending 1`] = `""`; +exports[` > Border Color Logic > uses yellow border when tools are pending 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────╮ +│ o test-tool A tool for testing │ +│ │ +│ Test result │ +╰──────────────────────────────────────────────────────────────────────────╯ +" +`; + +exports[` > Compact Tool Output (Dense Mode) > renders confirming tools as boxed even in compact mode 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────╮ +│ ? write_file critical_file.ts ← │ +│ │ +│ Test result │ +│ ╭──────────────────────────────────────────────────────────────────────╮ │ +│ │ │ │ +│ │ No changes detected. │ │ +│ │ │ │ +│ ╰──────────────────────────────────────────────────────────────────────╯ │ +│ Apply this change? │ +│ │ +│ ● 1. Allow once │ +│ 2. Allow for this session │ +│ 3. Modify with external editor │ +│ 4. No, suggest changes (esc) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────╯ +" +`; + +exports[` > Compact Tool Output (Dense Mode) > renders mixed boxed (shell) and dense tools correctly 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ run_shell_command npm test │ +│ │ +│ All tests passed │ +╰──────────────────────────────────────────────────────────────────────────╯ + ✓ write_file packages/core/index.ts → File updated +" +`; + +exports[` > Compact Tool Output (Dense Mode) > renders multiple tool calls compactly without boxes 1`] = ` +" ✓ read_file file1.ts → Success + ✓ grep_search search term → Found 3 matches + f1.ts:10: match 1 + f2.ts:20: match 2 + f3.ts:30: match 3 + +" +`; + +exports[` > Compact Tool Output (Dense Mode) > renders single tool call compactly 1`] = ` +" ✓ read_file packages/cli/src/app.tsx → Read 150 lines +" +`; exports[` > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = ` "╭──────────────────────────────────────────────────────────────────────────╮ @@ -96,38 +168,60 @@ exports[` > Event-Driven Scheduler > hides confirming tools 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 +"╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ success-tool 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 be truncate… - → line1 line2 line3 line4 line5 - ✓ tool-2 Description 2 → line1 line2 - - - - - -" +"╭──────────────────────────────────────────────────────────────────────────╮ +│ ✓ tool-1 Description 1. This is a long description that will need to b… │ +│──────────────────────────────────────────────────────────────────────────│ +│ │ ▄ +│ ✓ tool-2 Description 2 │ █ +│ │ █ +│ line1 │ █ +│ line2 │ █ +╰──────────────────────────────────────────────────────────────────────────╯ █ + █" `; exports[` > Golden Snapshots > renders mixed tool calls including shell command 1`] = ` -" ✓ read_file Read a 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 - 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 │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; @@ -141,7 +235,11 @@ exports[` > Golden Snapshots > renders shell command with ye `; 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 │ +╰──────────────────────────────────────────────────────────────────────────╯ " `; @@ -162,42 +260,67 @@ exports[` > Golden Snapshots > renders tool call awaiting co `; exports[` > Golden Snapshots > renders tool call with outputFile 1`] = ` -" ✓ tool-with-file Tool that saved output to file → Test result - (Output 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-1 Description 1 → line1 line2 line3 line4 line5 - - ✓ 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… - This is a very long description… - → Test result +"╭──────────────────────────────────╮ +│ ✓ very-long-tool-name-that-mig… │ +│ │ +│ 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/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index caea09beff..b188d908c9 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -9,6 +9,7 @@ import type { Mock, MockInstance } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { act } from 'react'; import { renderHookWithProviders } from '../../test-utils/render.js'; +import { createMockSettings } from '../../test-utils/settings.js'; import { waitFor } from '../../test-utils/async.js'; import { useGeminiStream } from './useGeminiStream.js'; import { useKeypress } from './useKeypress.js'; @@ -42,7 +43,7 @@ import type { Part, PartListUnion } from '@google/genai'; import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import type { SlashCommandProcessorResult } from '../types.js'; import { MessageType, StreamingState, ToolCallStatus } from '../types.js'; -import type { LoadedSettings } from '../../config/settings.js'; +// import type { LoadedSettings } from '../../config/settings.js'; // --- MOCKS --- const mockSendMessageStream = vi @@ -292,19 +293,13 @@ describe('useGeminiStream', () => { vi.spyOn(coreEvents, 'emitFeedback'); }); - const mockLoadedSettings: LoadedSettings = { - merged: { - preferredEditor: 'vscode', - ui: { - enableCompactToolOutput: true, - }, + const mockLoadedSettings = createMockSettings({ + ui: { + enableCompactToolOutput: false, + showCitations: true, + showModelInfoInChat: true, }, - user: { path: '/user/settings.json', settings: {} }, - workspace: { path: '/workspace/.gemini/settings.json', settings: {} }, - errors: [], - forScope: vi.fn(), - setValue: vi.fn(), - } as unknown as LoadedSettings; + }); const renderTestHook = ( initialToolCalls: TrackedToolCall[] = [], diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index c919c7923c..33528cb3d9 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -556,7 +556,7 @@ export const useGeminiStream = ( if (pendingHistoryItemRef.current) { if (pendingHistoryItemRef.current.type === 'tool_group') { // Mark all in-progress tools as Canceled when the turn is cancelled. - + const toolGroup = pendingHistoryItemRef.current; const updatedTools = toolGroup.tools.map((tool) => { if (