mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-19 01:30:42 -07:00
feat(cli): refine tool output formatting for compact mode (#24677)
This commit is contained in:
@@ -357,9 +357,8 @@ describe('DenseToolMessage', () => {
|
||||
await waitUntilReady();
|
||||
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');
|
||||
// Matches should no longer be rendered in dense mode to keep it compact
|
||||
expect(output).not.toContain('file1.ts:10: match 1');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -400,9 +399,8 @@ describe('DenseToolMessage', () => {
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Attempting to read files from **/*.ts');
|
||||
expect(output).toContain('→ Read 3 file(s) (1 ignored)');
|
||||
expect(output).toContain('file1.ts');
|
||||
expect(output).toContain('file2.ts');
|
||||
expect(output).toContain('file3.ts');
|
||||
// File lists should no longer be rendered in dense mode
|
||||
expect(output).not.toContain('file1.ts');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -477,6 +475,28 @@ describe('DenseToolMessage', () => {
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('truncates long description but preserves tool name (< 25 chars)', async () => {
|
||||
const longDescription =
|
||||
'This is a very long description that should definitely be truncated because it exceeds the available terminal width and we want to see how it behaves.';
|
||||
const toolName = 'tool-name-is-24-chars-!!'; // Exactly 24 chars
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<DenseToolMessage
|
||||
{...defaultProps}
|
||||
name={toolName}
|
||||
description={longDescription}
|
||||
terminalWidth={50} // Narrow width to force truncation
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
// Tool name should be fully present (it plus one space is exactly 25, fitting the maxWidth)
|
||||
expect(output).toContain(toolName);
|
||||
// Description should be present but truncated
|
||||
expect(output).toContain('This is a');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Toggleable Diff View (Alternate Buffer)', () => {
|
||||
const diffResult: FileDiff = {
|
||||
fileDiff: '@@ -1,1 +1,1 @@\n-old line\n+new line',
|
||||
|
||||
@@ -72,27 +72,6 @@ const hasPayload = (res: unknown): res is PayloadResult => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
const RenderItemsList: React.FC<{
|
||||
items?: string[];
|
||||
maxVisible?: number;
|
||||
}> = ({ 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>
|
||||
);
|
||||
};
|
||||
|
||||
function getFileOpData(
|
||||
diff: FileDiff,
|
||||
status: CoreToolCallStatus,
|
||||
@@ -188,8 +167,6 @@ function getFileOpData(
|
||||
}
|
||||
|
||||
function getReadManyFilesData(result: ReadManyFilesResult): ViewParts {
|
||||
const items = result.files ?? [];
|
||||
const maxVisible = 10;
|
||||
const includePatterns = result.include?.join(', ') ?? '';
|
||||
const description = (
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
@@ -198,18 +175,12 @@ function getReadManyFilesData(result: ReadManyFilesResult): ViewParts {
|
||||
);
|
||||
|
||||
const skippedCount = result.skipped?.length ?? 0;
|
||||
const summaryStr = `Read ${items.length} file(s)${
|
||||
const summaryStr = `Read ${result.files.length} file(s)${
|
||||
skippedCount > 0 ? ` (${skippedCount} ignored)` : ''
|
||||
}`;
|
||||
const summary = <Text color={theme.text.accent}>→ {summaryStr}</Text>;
|
||||
const hasItems = items.length > 0;
|
||||
const payload = hasItems ? (
|
||||
<Box flexDirection="column" marginLeft={2}>
|
||||
{hasItems && <RenderItemsList items={items} maxVisible={maxVisible} />}
|
||||
</Box>
|
||||
) : undefined;
|
||||
|
||||
return { description, summary, payload };
|
||||
return { description, summary, payload: undefined };
|
||||
}
|
||||
|
||||
function getListDirectoryData(
|
||||
@@ -258,20 +229,11 @@ function getGenericSuccessData(
|
||||
</Text>
|
||||
);
|
||||
} else if (isGrepResult(resultDisplay)) {
|
||||
summary = <Text color={theme.text.accent}>→ {resultDisplay.summary}</Text>;
|
||||
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>
|
||||
);
|
||||
}
|
||||
summary = (
|
||||
<Text color={theme.text.accent} wrap="truncate-end">
|
||||
→ {resultDisplay.summary}
|
||||
</Text>
|
||||
);
|
||||
} else if (isTodoList(resultDisplay)) {
|
||||
summary = (
|
||||
<Text color={theme.text.accent} wrap="wrap">
|
||||
@@ -488,15 +450,18 @@ export const DenseToolMessage: React.FC<DenseToolMessageProps> = (props) => {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box marginLeft={2} flexDirection="row" flexWrap="wrap">
|
||||
<ToolStatusIndicator status={status} name={name} />
|
||||
<Box maxWidth={25} flexShrink={1} flexGrow={0}>
|
||||
<Text color={theme.text.primary} bold wrap="truncate-end">
|
||||
{name}{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={1} flexShrink={1} flexGrow={0}>
|
||||
{description}
|
||||
<Box flexDirection="row" flexShrink={1}>
|
||||
<ToolStatusIndicator status={status} name={name} />
|
||||
<Box maxWidth={25} flexShrink={0} flexGrow={0}>
|
||||
<Text color={theme.text.primary} bold wrap="truncate-end">
|
||||
{name}{' '}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={1} flexShrink={1} flexGrow={0}>
|
||||
{description}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{summary && (
|
||||
<Box
|
||||
key="tool-summary"
|
||||
|
||||
@@ -35,8 +35,6 @@ import {
|
||||
WRITE_FILE_DISPLAY_NAME,
|
||||
READ_MANY_FILES_DISPLAY_NAME,
|
||||
isFileDiff,
|
||||
isGrepResult,
|
||||
isListResult,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
||||
@@ -81,15 +79,6 @@ export const hasDensePayload = (tool: IndividualToolCallDisplay): boolean => {
|
||||
// TODO(24053): Usage of type guards makes this class too aware of internals
|
||||
if (isFileDiff(res)) return true;
|
||||
if (tool.confirmationDetails?.type === 'edit') return true;
|
||||
if (isGrepResult(res) && res.matches.length > 0) return true;
|
||||
|
||||
// ReadManyFilesResult check (has 'include' and 'files')
|
||||
if (isListResult(res) && 'include' in res) {
|
||||
const includeProp = (res as { include?: unknown }).include;
|
||||
if (Array.isArray(includeProp) && res.files.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Generic summary/payload pattern
|
||||
if (
|
||||
|
||||
@@ -51,10 +51,6 @@ exports[`DenseToolMessage > renders correctly for Errored Edit tool 1`] = `
|
||||
|
||||
exports[`DenseToolMessage > renders correctly for ReadManyFiles results 1`] = `
|
||||
" ✓ test-tool Attempting to read files from **/*.ts → Read 3 file(s) (1 ignored)
|
||||
|
||||
file1.ts
|
||||
file2.ts
|
||||
file3.ts
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -110,9 +106,6 @@ exports[`DenseToolMessage > renders correctly for file diff results with stats 1
|
||||
|
||||
exports[`DenseToolMessage > renders correctly for grep results 1`] = `
|
||||
" ✓ test-tool Test description → Found 2 matches
|
||||
|
||||
file1.ts:10: match 1
|
||||
file2.ts:20: match 2
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -136,6 +129,12 @@ exports[`DenseToolMessage > renders generic output message for unknown object re
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`DenseToolMessage > truncates long description but preserves tool name (< 25 chars) 1`] = `
|
||||
" ✓ tool-name-is-24-chars-!! This is a very long description that should definitely be truncated …
|
||||
→ Success result
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`DenseToolMessage > truncates long string results 1`] = `
|
||||
" ✓ test-tool Test description
|
||||
→ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA…
|
||||
|
||||
Reference in New Issue
Block a user