mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-12 15:10:59 -07:00
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:
@@ -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');
|
||||
|
||||
@@ -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 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
|
||||
@@ -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 │
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -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[] = [],
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user