mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-29 07:21:27 -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. - docs: sync settings and configuration with latest schema - wrap waitUntilReady in `act` to resolve spinner warnings
This commit is contained in:
@@ -105,7 +105,7 @@ they appear in the UI.
|
||||
| UI Label | Setting | Description | Default |
|
||||
| ------------------------------------ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| Memory Discovery Max Dirs | `context.discoveryMaxDirs` | Maximum number of directories to search for memory. | `200` |
|
||||
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory refresh loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
|
||||
| Load Memory From Include Directories | `context.loadMemoryFromIncludeDirectories` | Controls how /memory reload loads GEMINI.md files. When true, include directories are scanned; when false, only the current directory is used. | `false` |
|
||||
| Respect .gitignore | `context.fileFiltering.respectGitIgnore` | Respect .gitignore files when searching. | `true` |
|
||||
| Respect .geminiignore | `context.fileFiltering.respectGeminiIgnore` | Respect .geminiignore files when searching. | `true` |
|
||||
| Enable Recursive File Search | `context.fileFiltering.enableRecursiveFileSearch` | Enable recursive file search functionality when completing @ references in the prompt. | `true` |
|
||||
|
||||
@@ -752,7 +752,7 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Default:** `[]`
|
||||
|
||||
- **`context.loadMemoryFromIncludeDirectories`** (boolean):
|
||||
- **Description:** Controls how /memory refresh loads GEMINI.md files. When
|
||||
- **Description:** Controls how /memory reload loads GEMINI.md files. When
|
||||
true, include directories are scanned; when false, only the current
|
||||
directory is used.
|
||||
- **Default:** `false`
|
||||
@@ -1097,8 +1097,8 @@ their corresponding top-level category object in your `settings.json` file.
|
||||
- **Requires restart:** Yes
|
||||
|
||||
- **`experimental.gemmaModelRouter.enabled`** (boolean):
|
||||
- **Description:** Enable the Gemma Model Router. Requires a local endpoint
|
||||
serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
- **Description:** Enable the Gemma Model Router (experimental). Requires a
|
||||
local endpoint serving Gemma via the Gemini API using LiteRT-LM shim.
|
||||
- **Default:** `false`
|
||||
- **Requires restart:** Yes
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../../test-utils/render.js';
|
||||
import { act } from 'react';
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import type {
|
||||
@@ -94,7 +95,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -119,7 +122,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
);
|
||||
|
||||
// Should now render confirming tools
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('test-tool');
|
||||
unmount();
|
||||
@@ -164,7 +169,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
// pending-tool should now be visible
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('successful-tool');
|
||||
expect(output).toContain('pending-tool');
|
||||
@@ -203,7 +210,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('successful-tool');
|
||||
expect(output).not.toContain('error-tool');
|
||||
@@ -237,7 +246,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('client-error-tool');
|
||||
unmount();
|
||||
@@ -282,7 +293,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
// write_file (Pending) should now be visible
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('read_file');
|
||||
expect(output).toContain('run_shell_command');
|
||||
@@ -328,7 +341,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -362,7 +377,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -385,7 +402,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -424,7 +443,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -455,7 +476,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -510,7 +533,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -540,7 +565,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -570,7 +597,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -613,7 +642,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -664,7 +695,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||
);
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
if (shouldHide) {
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
@@ -695,7 +728,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -723,7 +758,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||
);
|
||||
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -754,7 +791,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -777,7 +816,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).not.toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -808,7 +849,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -841,7 +884,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).not.toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -936,7 +981,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('visible-tool');
|
||||
expect(output).not.toContain('hidden-error-0');
|
||||
@@ -962,7 +1009,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
expect(lastFrame({ allowEmpty: true })).not.toBe('');
|
||||
unmount();
|
||||
});
|
||||
@@ -1000,7 +1049,9 @@ describe('<ToolGroupMessage />', () => {
|
||||
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
await act(async () => {
|
||||
await waitUntilReady();
|
||||
});
|
||||
|
||||
if (visible) {
|
||||
expect(lastFrame()).toContain(name);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
import * as glob from 'glob';
|
||||
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
|
||||
import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js';
|
||||
import type { ReadManyFilesResult } from './tools.js';
|
||||
|
||||
vi.mock('glob', { spy: true });
|
||||
|
||||
@@ -277,7 +278,7 @@ describe('ReadManyFilesTool', () => {
|
||||
`--- ${expectedPath} ---\n\nContent of file1\n\n`,
|
||||
`\n--- End of content ---`,
|
||||
]);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **1 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -301,7 +302,7 @@ describe('ReadManyFilesTool', () => {
|
||||
c.includes(`--- ${expectedPath2} ---\n\nContent2\n\n`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **2 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -327,7 +328,7 @@ describe('ReadManyFilesTool', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
expect(content.find((c) => c.includes('sub/data.json'))).toBeUndefined();
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **2 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -347,7 +348,7 @@ describe('ReadManyFilesTool', () => {
|
||||
expect(
|
||||
content.find((c) => c.includes('src/main.test.ts')),
|
||||
).toBeUndefined();
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **1 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -359,7 +360,7 @@ describe('ReadManyFilesTool', () => {
|
||||
expect(result.llmContent).toEqual([
|
||||
'No files matching the criteria were found or all were skipped.',
|
||||
]);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'No files were read and concatenated based on the criteria.',
|
||||
);
|
||||
});
|
||||
@@ -379,7 +380,7 @@ describe('ReadManyFilesTool', () => {
|
||||
expect(
|
||||
content.find((c) => c.includes('node_modules/some-lib/index.js')),
|
||||
).toBeUndefined();
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **1 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -406,7 +407,7 @@ describe('ReadManyFilesTool', () => {
|
||||
c.includes(`--- ${expectedPath2} ---\n\napp code\n\n`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **2 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -430,7 +431,7 @@ describe('ReadManyFilesTool', () => {
|
||||
},
|
||||
'\n--- End of content ---',
|
||||
]);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **1 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -471,8 +472,10 @@ describe('ReadManyFilesTool', () => {
|
||||
c.includes(`--- ${expectedPath} ---\n\ntext notes\n\n`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(result.returnDisplay).toContain('**Skipped 1 item(s):**');
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'**Skipped 1 item(s):**',
|
||||
);
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'- `document.pdf` (Reason: asset file (image/pdf/audio) was not explicitly requested by name or extension)',
|
||||
);
|
||||
});
|
||||
@@ -516,9 +519,15 @@ describe('ReadManyFilesTool', () => {
|
||||
const params = { include: ['foo.bar', 'bar.ts', 'foo.quux'] };
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.returnDisplay).not.toContain('foo.bar');
|
||||
expect(result.returnDisplay).not.toContain('foo.quux');
|
||||
expect(result.returnDisplay).toContain('bar.ts');
|
||||
expect((result.returnDisplay as ReadManyFilesResult).files).not.toContain(
|
||||
'foo.bar',
|
||||
);
|
||||
expect((result.returnDisplay as ReadManyFilesResult).files).not.toContain(
|
||||
'foo.quux',
|
||||
);
|
||||
expect((result.returnDisplay as ReadManyFilesResult).files).toContain(
|
||||
'bar.ts',
|
||||
);
|
||||
});
|
||||
|
||||
it('should read files from multiple workspace directories', async () => {
|
||||
@@ -594,7 +603,7 @@ describe('ReadManyFilesTool', () => {
|
||||
c.includes(`--- ${expectedPath2} ---\n\nContent2\n\n`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **2 file(s)**',
|
||||
);
|
||||
|
||||
@@ -646,7 +655,7 @@ Content of receive-detail
|
||||
`,
|
||||
`\n--- End of content ---`,
|
||||
]);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **1 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -665,7 +674,7 @@ Content of file[1]
|
||||
`,
|
||||
`\n--- End of content ---`,
|
||||
]);
|
||||
expect(result.returnDisplay).toContain(
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read and concatenated content from **1 file(s)**',
|
||||
);
|
||||
});
|
||||
@@ -764,7 +773,9 @@ Content of file[1]
|
||||
|
||||
// Should successfully process valid files despite one failure
|
||||
expect(content.length).toBeGreaterThanOrEqual(3);
|
||||
expect(result.returnDisplay).toContain('Successfully read');
|
||||
expect((result.returnDisplay as ReadManyFilesResult).summary).toContain(
|
||||
'Successfully read',
|
||||
);
|
||||
|
||||
// Verify valid files were processed
|
||||
const expectedPath1 = path.join(tempRootDir, 'valid1.txt');
|
||||
|
||||
Reference in New Issue
Block a user