mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-03 09:50:40 -07:00
fix(cli,core): resolve build issues with structured tool result display
- Added `isStructuredToolResult` type guard to safely check for `summary` properties on tool results in `ToolResultDisplay`. - Resolved `data.slice is not a function` errors in UI tests caused by structured tool results falling back to `AnsiOutput` parsing. - Updated `DiffRenderer` assertions to include `disableColor: false`. - Updated `ReadManyFilesTool` tests to assert against the new `ReadManyFilesResult` object structure instead of flat strings. - Removed unsafe type assertions (`any`) in `ToolGroupMessage.compact.test.tsx` and `ToolGroupMessage.tsx` where possible.
This commit is contained in:
@@ -118,7 +118,9 @@ describe('DenseToolMessage', () => {
|
||||
name="Edit"
|
||||
status={CoreToolCallStatus.AwaitingApproval}
|
||||
resultDisplay={undefined}
|
||||
confirmationDetails={confirmationDetails as SerializableConfirmationDetails}
|
||||
confirmationDetails={
|
||||
confirmationDetails as SerializableConfirmationDetails
|
||||
}
|
||||
/>,
|
||||
{ useAlternateBuffer: false },
|
||||
);
|
||||
@@ -194,7 +196,9 @@ describe('DenseToolMessage', () => {
|
||||
name="Edit"
|
||||
status={CoreToolCallStatus.Cancelled}
|
||||
resultDisplay={undefined}
|
||||
confirmationDetails={confirmationDetails as unknown as SerializableConfirmationDetails}
|
||||
confirmationDetails={
|
||||
confirmationDetails as unknown as SerializableConfirmationDetails
|
||||
}
|
||||
/>,
|
||||
{ useAlternateBuffer: false },
|
||||
);
|
||||
@@ -391,7 +395,7 @@ describe('DenseToolMessage', () => {
|
||||
<DenseToolMessage
|
||||
{...defaultProps}
|
||||
status={CoreToolCallStatus.Error}
|
||||
resultDisplay={"Error occurred" as ToolResultDisplay}
|
||||
resultDisplay={'Error occurred' as ToolResultDisplay}
|
||||
/>,
|
||||
);
|
||||
await waitUntilReady();
|
||||
@@ -497,4 +501,3 @@ describe('DenseToolMessage', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ index 0000000..e69de29
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
disableColor: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -84,6 +85,7 @@ index 0000000..e69de29
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
disableColor: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -112,6 +114,7 @@ index 0000000..e69de29
|
||||
maxWidth: 80,
|
||||
theme: undefined,
|
||||
settings: expect.anything(),
|
||||
disableColor: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
@@ -7,73 +12,92 @@ import {
|
||||
READ_FILE_DISPLAY_NAME,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { expect, it, describe } from 'vitest';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import type { LoadedSettings } from '../../../config/settings.js';
|
||||
|
||||
describe('ToolGroupMessage Compact Rendering', () => {
|
||||
const defaultProps = {
|
||||
item: {
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
item: {
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
type: 'help' as const, // Adding type property to satisfy HistoryItem type
|
||||
},
|
||||
terminalWidth: 80,
|
||||
};
|
||||
|
||||
const compactSettings = {
|
||||
const compactSettings: LoadedSettings = {
|
||||
merged: {
|
||||
ui: {
|
||||
compactToolOutput: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
sources: [],
|
||||
} as unknown as LoadedSettings; // Test mock of settings
|
||||
|
||||
it('renders consecutive compact tools without empty lines between them', async () => {
|
||||
const toolCalls = [
|
||||
const toolCalls: IndividualToolCallDisplay[] = [
|
||||
{
|
||||
callId: 'call1',
|
||||
name: LS_DISPLAY_NAME,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'file1.txt\nfile2.txt',
|
||||
description: 'Listing files',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
{
|
||||
callId: 'call2',
|
||||
name: LS_DISPLAY_NAME,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'file3.txt',
|
||||
description: 'Listing files',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls as any} />,
|
||||
{ settings: compactSettings as any }
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls} />,
|
||||
{ settings: compactSettings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
|
||||
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not add an extra empty line between a compact tool and a standard tool', async () => {
|
||||
const toolCalls = [
|
||||
const toolCalls: IndividualToolCallDisplay[] = [
|
||||
{
|
||||
callId: 'call1',
|
||||
name: LS_DISPLAY_NAME,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'file1.txt',
|
||||
description: 'Listing files',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
{
|
||||
callId: 'call2',
|
||||
name: 'non-compact-tool',
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'some large output',
|
||||
description: 'Doing something',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls as any} />,
|
||||
{ settings: compactSettings as any }
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls} />,
|
||||
{ settings: compactSettings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
@@ -82,24 +106,32 @@ describe('ToolGroupMessage Compact Rendering', () => {
|
||||
});
|
||||
|
||||
it('does not add an extra empty line if a compact tool has a dense payload', async () => {
|
||||
const toolCalls = [
|
||||
const toolCalls: IndividualToolCallDisplay[] = [
|
||||
{
|
||||
callId: 'call1',
|
||||
name: LS_DISPLAY_NAME,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'file1.txt',
|
||||
description: 'Listing files',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
{
|
||||
callId: 'call2',
|
||||
name: READ_FILE_DISPLAY_NAME,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: { summary: 'read file', payload: 'file content' }, // Dense payload
|
||||
description: 'Reading file',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls as any} />,
|
||||
{ settings: compactSettings as any }
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls} />,
|
||||
{ settings: compactSettings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
@@ -108,24 +140,32 @@ describe('ToolGroupMessage Compact Rendering', () => {
|
||||
});
|
||||
|
||||
it('does not add an extra empty line between a standard tool and a compact tool', async () => {
|
||||
const toolCalls = [
|
||||
const toolCalls: IndividualToolCallDisplay[] = [
|
||||
{
|
||||
callId: 'call1',
|
||||
name: 'non-compact-tool',
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'some large output',
|
||||
description: 'Doing something',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
{
|
||||
callId: 'call2',
|
||||
name: LS_DISPLAY_NAME,
|
||||
status: CoreToolCallStatus.Success,
|
||||
resultDisplay: 'file1.txt',
|
||||
description: 'Listing files',
|
||||
confirmationDetails: undefined,
|
||||
isClientInitiated: true,
|
||||
parentCallId: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
const { lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls as any} />,
|
||||
{ settings: compactSettings as any }
|
||||
<ToolGroupMessage {...defaultProps} toolCalls={toolCalls} />,
|
||||
{ settings: compactSettings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
mapCoreStatusToDisplayStatus,
|
||||
isFileDiff,
|
||||
isGrepResult,
|
||||
isListResult,
|
||||
} from '../../types.js';
|
||||
import { ToolMessage } from './ToolMessage.js';
|
||||
import { ShellToolMessage } from './ShellToolMessage.js';
|
||||
@@ -78,14 +79,7 @@ export const hasDensePayload = (tool: IndividualToolCallDisplay): boolean => {
|
||||
if (isGrepResult(res) && (res.matches?.length ?? 0) > 0) return true;
|
||||
|
||||
// ReadManyFilesResult check (has 'include' and 'files')
|
||||
if (
|
||||
typeof res === 'object' &&
|
||||
res !== null &&
|
||||
'include' in res &&
|
||||
'files' in res &&
|
||||
Array.isArray((res as any).files) &&
|
||||
(res as any).files.length > 0
|
||||
) {
|
||||
if (isListResult(res) && 'include' in res && (res.files?.length ?? 0) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -212,14 +206,14 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
height += 1; // Base height for compact tool
|
||||
// Spacing logic (matching marginTop)
|
||||
if (isFirst) {
|
||||
height += borderTopOverride ?? true ? 1 : 0;
|
||||
height += (borderTopOverride ?? true) ? 1 : 0;
|
||||
} else if (!prevIsCompact) {
|
||||
height += 1;
|
||||
}
|
||||
} else {
|
||||
height += 3; // Static overhead for standard tool
|
||||
if (isFirst) {
|
||||
height += borderTopOverride ?? true ? 1 : 0;
|
||||
height += (borderTopOverride ?? true) ? 1 : 0;
|
||||
} else {
|
||||
height += 1; // marginTop is always 1 for non-compact tools (not first)
|
||||
}
|
||||
@@ -270,7 +264,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
*/
|
||||
width={terminalWidth}
|
||||
paddingRight={TOOL_MESSAGE_HORIZONTAL_MARGIN}
|
||||
marginBottom={borderBottomOverride ?? true ? 1 : 0}
|
||||
marginBottom={(borderBottomOverride ?? true) ? 1 : 0}
|
||||
>
|
||||
{visibleToolCalls.map((tool, index) => {
|
||||
const isFirst = index === 0;
|
||||
@@ -290,7 +284,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
|
||||
let marginTop = 0;
|
||||
if (isFirst) {
|
||||
marginTop = borderTopOverride ?? true ? 1 : 0;
|
||||
marginTop = (borderTopOverride ?? true) ? 1 : 0;
|
||||
} else if (!(isCompact && prevIsCompact)) {
|
||||
marginTop = 1;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type AnsiOutput,
|
||||
type AnsiLine,
|
||||
isSubagentProgress,
|
||||
isStructuredToolResult,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||
@@ -118,7 +119,28 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
{contentData}
|
||||
</Text>
|
||||
);
|
||||
} else if (typeof contentData === 'object' && 'fileDiff' in contentData) {
|
||||
} else if (isStructuredToolResult(contentData)) {
|
||||
if (renderOutputAsMarkdown) {
|
||||
content = (
|
||||
<MarkdownDisplay
|
||||
text={contentData.summary}
|
||||
terminalWidth={childWidth}
|
||||
renderMarkdown={renderMarkdown}
|
||||
isPending={false}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<Text wrap="wrap" color={theme.text.primary}>
|
||||
{contentData.summary}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
typeof contentData === 'object' &&
|
||||
contentData !== null &&
|
||||
'fileDiff' in contentData
|
||||
) {
|
||||
content = (
|
||||
<DiffRenderer
|
||||
diffContent={
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line between a compact tool and a standard tool 1`] = `
|
||||
"
|
||||
✓ ReadFolder → file1.txt
|
||||
✓ ReadFolder Listing files → file1.txt
|
||||
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ non-compact-tool │
|
||||
│ ✓ non-compact-tool Doing something │
|
||||
│ │
|
||||
│ some large output │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
@@ -15,25 +15,25 @@ exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line b
|
||||
exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line between a standard tool and a compact tool 1`] = `
|
||||
"
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ non-compact-tool │
|
||||
│ ✓ non-compact-tool Doing something │
|
||||
│ │
|
||||
│ some large output │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
✓ ReadFolder → file1.txt
|
||||
✓ ReadFolder Listing files → file1.txt
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line if a compact tool has a dense payload 1`] = `
|
||||
"
|
||||
✓ ReadFolder → file1.txt
|
||||
✓ ReadFile → read file
|
||||
✓ ReadFolder Listing files → file1.txt
|
||||
✓ ReadFile Reading file → read file
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolGroupMessage Compact Rendering > renders consecutive compact tools without empty lines between them 1`] = `
|
||||
"
|
||||
✓ ReadFolder → file1.txt file2.txt
|
||||
✓ ReadFolder → file3.txt
|
||||
✓ ReadFolder Listing files → file1.txt file2.txt
|
||||
✓ ReadFolder Listing files → file3.txt
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -36,6 +36,7 @@ exports[`<ToolGroupMessage /> > Border Color Logic > uses gray border when all t
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
|
||||
│ │
|
||||
│ ✓ another-tool A tool for testing │
|
||||
│ │
|
||||
@@ -57,10 +58,10 @@ exports[`<ToolGroupMessage /> > Border Color Logic > uses yellow border for shel
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description 1. This is a long description that will need to b… │
|
||||
"│ ✓ tool-1 Description 1. This is a long description that will need to b… │
|
||||
│──────────────────────────────────────────────────────────────────────────│
|
||||
│ │ ▄
|
||||
|
||||
│ │
|
||||
│ ✓ tool-2 Description 2 │ █
|
||||
│ │ █
|
||||
│ line1 │ █
|
||||
@@ -76,6 +77,7 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls incl
|
||||
│ ✓ read_file Read a file │
|
||||
│ │
|
||||
│ Test result │
|
||||
|
||||
│ │
|
||||
│ ⊶ run_shell_command Run command │
|
||||
│ │
|
||||
@@ -94,6 +96,7 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders multiple tool calls w
|
||||
│ ✓ successful-tool This tool succeeded │
|
||||
│ │
|
||||
│ Test result │
|
||||
|
||||
│ │
|
||||
│ o pending-tool This tool is pending │
|
||||
│ │
|
||||
@@ -143,6 +146,7 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal
|
||||
│ ✓ tool-with-result Tool with output │
|
||||
│ │
|
||||
│ This is a long result that might need height constraints │
|
||||
|
||||
│ │
|
||||
│ ✓ another-tool Another tool │
|
||||
│ │
|
||||
@@ -167,10 +171,12 @@ exports[`<ToolGroupMessage /> > Height Calculation > calculates available height
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Result 1 │
|
||||
|
||||
│ │
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
│ Result 2 │
|
||||
|
||||
│ │
|
||||
│ ✓ test-tool A tool for testing │
|
||||
│ │
|
||||
|
||||
@@ -40,7 +40,7 @@ exports[`ToolMessage Sticky Header Regression > verifies that multiple ToolMessa
|
||||
"│ │
|
||||
│ ✓ tool-2 Description for tool-2 │
|
||||
│────────────────────────────────────────────────────────────────────────│
|
||||
│ c2-09 │ ▄
|
||||
│ c2-10 │ ▀
|
||||
│ c2-08 │ ▄
|
||||
│ c2-09 │ ▀
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -75,9 +75,7 @@ import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import { useLogger } from './useLogger.js';
|
||||
import { SHELL_COMMAND_NAME } from '../constants.js';
|
||||
import { mapToDisplay as mapTrackedToolCallsToDisplay } from './toolMapping.js';
|
||||
import {
|
||||
isCompactTool,
|
||||
} from '../components/messages/ToolGroupMessage.js';
|
||||
import { isCompactTool } from '../components/messages/ToolGroupMessage.js';
|
||||
import {
|
||||
useToolScheduler,
|
||||
type TrackedToolCall,
|
||||
@@ -305,13 +303,11 @@ export const useGeminiStream = (
|
||||
// If the first tool in this push is non-compact but follows a compact tool,
|
||||
// we must start a new border group.
|
||||
const currentIsCompact = isCompactTool(
|
||||
mapTrackedToolCallsToDisplay(firstToolToPush as TrackedToolCall)
|
||||
.tools[0],
|
||||
mapTrackedToolCallsToDisplay(firstToolToPush).tools[0],
|
||||
isCompactModeEnabled,
|
||||
);
|
||||
const prevWasCompact = isCompactTool(
|
||||
mapTrackedToolCallsToDisplay(prevTool as TrackedToolCall)
|
||||
.tools[0],
|
||||
mapTrackedToolCallsToDisplay(prevTool).tools[0],
|
||||
isCompactModeEnabled,
|
||||
);
|
||||
if (!currentIsCompact && prevWasCompact) {
|
||||
@@ -356,9 +352,7 @@ export const useGeminiStream = (
|
||||
}
|
||||
|
||||
// Handle tool response submission immediately when tools complete
|
||||
await handleCompletedTools(
|
||||
completedToolCallsFromScheduler as TrackedToolCall[],
|
||||
);
|
||||
await handleCompletedTools(completedToolCallsFromScheduler);
|
||||
}
|
||||
},
|
||||
config,
|
||||
|
||||
Reference in New Issue
Block a user