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.
This commit is contained in:
Jarrod Whelan
2026-02-11 02:31:00 -08:00
parent 35f56a5496
commit 03de28960f
11 changed files with 479 additions and 159 deletions

View File

@@ -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');

View File

@@ -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 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;

View File

@@ -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 │

View File

@@ -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');
});

View File

@@ -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 }) => (
<Box flexDirection="column">
{items.slice(0, maxVisible).map((item, i) => (
<Text key={i} color={theme.text.secondary}>
{item}
</Text>
))}
{items.length > maxVisible && (
<Text color={theme.text.secondary}>
... and {items.length - maxVisible} more
</Text>
)}
</Box>
);
}> = ({ items, maxVisible = 20 }) => {
if (!items || items.length === 0) return null;
return (
<Box flexDirection="column">
{items.slice(0, maxVisible).map((item, i) => (
<Text key={i} color={theme.text.secondary}>
{item}
</Text>
))}
{items.length > maxVisible && (
<Text color={theme.text.secondary}>
... and {items.length - maxVisible} more
</Text>
)}
</Box>
);
};
/**
* --- 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 = <Text color={theme.text.accent}> {result.summary}</Text>;
@@ -165,21 +172,23 @@ function getListResultData(
? `Excluded patterns: ${rmf.excludes.slice(0, 3).join(', ')}${rmf.excludes.length > 3 ? '...' : ''}`
: undefined;
const payload = (
<Box flexDirection="column" marginLeft={2}>
<RenderItemsList items={items} maxVisible={maxVisible} />
{skippedText && (
<Text color={theme.text.secondary} dimColor>
{skippedText}
</Text>
)}
{excludedText && (
<Text color={theme.text.secondary} dimColor>
{excludedText}
</Text>
)}
</Box>
);
const hasItems = items.length > 0;
const payload =
hasItems || skippedText || excludedText ? (
<Box flexDirection="column" marginLeft={2}>
{hasItems && <RenderItemsList items={items} maxVisible={maxVisible} />}
{skippedText && (
<Text color={theme.text.secondary} dimColor>
{skippedText}
</Text>
)}
{excludedText && (
<Text color={theme.text.secondary} dimColor>
{excludedText}
</Text>
)}
</Box>
) : undefined;
return { description, summary, payload };
}
@@ -200,16 +209,19 @@ function getGenericSuccessData(
);
} else if (isGrepResult(resultDisplay)) {
summary = <Text color={theme.text.accent}> {resultDisplay.summary}</Text>;
payload = (
<Box flexDirection="column" marginLeft={2}>
<RenderItemsList
items={resultDisplay.matches.map(
(m) => `${m.filePath}:${m.lineNumber}: ${m.line.trim()}`,
)}
maxVisible={10}
/>
</Box>
);
const matches = resultDisplay.matches ?? [];
if (matches.length > 0) {
payload = (
<Box flexDirection="column" marginLeft={2}>
<RenderItemsList
items={matches.map(
(m) => `${m.filePath}:${m.lineNumber}: ${m.line.trim()}`,
)}
maxVisible={10}
/>
</Box>
);
}
} else if (isTodoList(resultDisplay)) {
summary = (
<Text color={theme.text.accent} wrap="wrap">
@@ -280,6 +292,11 @@ export const DenseToolMessage: React.FC<DenseToolMessageProps> = (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);
}

View File

@@ -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,
}),
),
);
});

View File

@@ -754,4 +754,140 @@ describe('<ToolGroupMessage />', () => {
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(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
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(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
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(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
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(
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
{
config: mockConfigNoEventDriven,
settings: compactSettings,
uiState: {
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
},
},
);
expect(lastFrame()).toContain('Apply this change?');
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
});

View File

@@ -190,6 +190,13 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
!isShellToolCall &&
tool.status !== ToolCallStatus.Confirming;
const nextTool = visibleToolCalls[index + 1];
const nextIsDense =
nextTool &&
compactMode &&
!isShellTool(nextTool.name) &&
nextTool.status !== ToolCallStatus.Confirming;
if (useDenseView) {
return (
<DenseToolMessage
@@ -276,6 +283,20 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
)}
</Box>
)}
{/* If the NEXT tool is dense, we must close THIS tool's box now */}
{nextIsDense && (
<Box
height={0}
width={contentWidth}
borderLeft={true}
borderRight={true}
borderTop={false}
borderBottom={true}
borderColor={borderColor}
borderDimColor={borderDimColor}
borderStyle="round"
/>
)}
</Box>
);
})}
@@ -336,12 +357,19 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
/>
);
})()}
{compactMode
? null
: (borderBottomOverride ?? true) &&
visibleToolCalls.length > 0 && (
<ShowMoreLines constrainHeight={constrainHeight} />
)}
{(() => {
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 && (
<ShowMoreLines constrainHeight={constrainHeight} />
);
})()}
</Box>
);
};

View File

@@ -1,12 +1,20 @@
// 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 A tool for testing → Test result
"╭──────────────────────────────────────────────────────────────────────────╮
│ x Ask User │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > Ask User Filtering > filters out ask_user when s
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
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ 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 │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -36,7 +55,60 @@ exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shel
"
`;
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `""`;
exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border when tools are pending 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
│ o test-tool A tool for testing │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > Compact Tool Output (Dense Mode) > renders single tool call compactly 1`] = `
" ✓ read_file packages/cli/src/app.tsx → Read 150 lines
"
`;
exports[`<ToolGroupMessage /> > Confirmation Handling > renders confirmation with permanent approval disabled 1`] = `
"╭──────────────────────────────────────────────────────────────────────────╮
@@ -96,38 +168,60 @@ exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools
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
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ success-tool 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 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > Golden Snapshots > renders shell command with ye
`;
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 │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -162,42 +260,67 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders tool call awaiting co
`;
exports[`<ToolGroupMessage /> > 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[`<ToolGroupMessage /> > 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[`<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…
This is a very long description…
→ Test result
"╭──────────────────────────────────╮
│ ✓ very-long-tool-name-that-mig… │
│ │
│ 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 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;

View File

@@ -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[] = [],

View File

@@ -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 (