From cdd722c0d67c7a361693770be6f6a2dcdd3da58a Mon Sep 17 00:00:00 2001 From: Jarrod Whelan <150866123+jwhelangoog@users.noreply.github.com> Date: Mon, 9 Mar 2026 23:48:59 -0700 Subject: [PATCH] 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 --- docs/cli/settings.md | 2 +- docs/reference/configuration.md | 6 +- .../messages/DenseToolMessage.test.tsx | 11 +- .../components/messages/DiffRenderer.test.tsx | 3 + .../ToolGroupMessage.compact.test.tsx | 78 +++++++++---- .../messages/ToolGroupMessage.test.tsx | 105 +++++++++++++----- .../components/messages/ToolGroupMessage.tsx | 18 +-- .../components/messages/ToolResultDisplay.tsx | 24 +++- .../ToolGroupMessage.compact.test.tsx.snap | 16 +-- .../ToolGroupMessage.test.tsx.snap | 12 +- .../ToolStickyHeaderRegression.test.tsx.snap | 4 +- packages/cli/src/ui/hooks/useGeminiStream.ts | 14 +-- .../core/src/tools/read-many-files.test.ts | 45 +++++--- 13 files changed, 232 insertions(+), 106 deletions(-) diff --git a/docs/cli/settings.md b/docs/cli/settings.md index c7ae96b21e..6db1110f2b 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -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` | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index b85b873162..2742c03023 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1070,7 +1070,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` @@ -1420,8 +1420,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 diff --git a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx index 68feb0ed1e..bbc72ce2b4 100644 --- a/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/DenseToolMessage.test.tsx @@ -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', () => { , ); await waitUntilReady(); @@ -497,4 +501,3 @@ describe('DenseToolMessage', () => { }); }); }); - diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx index 9063606146..8edda13bdf 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx @@ -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, }), ); }); diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx index fcc64a0fe2..a75e442b80 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.compact.test.tsx @@ -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( - , - { settings: compactSettings as any } + , + { 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( - , - { settings: compactSettings as any } + , + { 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( - , - { settings: compactSettings as any } + , + { 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( - , - { settings: compactSettings as any } + , + { settings: compactSettings }, ); await waitUntilReady(); diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx index eff418a609..db72522388 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx @@ -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('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -119,7 +122,9 @@ describe('', () => { ); // Should now hide confirming tools (to avoid duplication with Global Queue) - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -139,7 +144,9 @@ describe('', () => { { config: baseMockConfig, settings: fullVerbositySettings }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); const output = lastFrame(); expect(output).toMatchSnapshot('canceled_tool'); unmount(); @@ -184,7 +191,9 @@ describe('', () => { }, ); // 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'); @@ -223,7 +232,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); const output = lastFrame(); expect(output).toContain('successful-tool'); expect(output).not.toContain('error-tool'); @@ -257,7 +268,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); const output = lastFrame(); expect(output).toContain('client-error-tool'); unmount(); @@ -302,7 +315,9 @@ describe('', () => { }, ); // 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'); @@ -348,7 +363,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -382,7 +399,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -405,7 +424,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -444,7 +465,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -475,7 +498,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -530,7 +555,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -560,7 +587,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -590,7 +619,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -633,7 +664,9 @@ describe('', () => { }, }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -684,7 +717,9 @@ describe('', () => { , { config: baseMockConfig, settings: fullVerbositySettings }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); if (shouldHide) { expect(lastFrame({ allowEmpty: true })).toBe(''); @@ -715,7 +750,9 @@ describe('', () => { { config: baseMockConfig, settings: fullVerbositySettings }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toMatchSnapshot(); unmount(); }); @@ -743,7 +780,9 @@ describe('', () => { { 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(); }); @@ -774,7 +813,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -797,7 +838,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).not.toBe(''); unmount(); }); @@ -828,7 +871,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -861,7 +906,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).toBe(''); unmount(); }); @@ -956,7 +1003,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); const output = lastFrame(); expect(output).toContain('visible-tool'); expect(output).not.toContain('hidden-error-0'); @@ -982,7 +1031,9 @@ describe('', () => { }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); expect(lastFrame({ allowEmpty: true })).not.toBe(''); unmount(); }); @@ -1020,7 +1071,9 @@ describe('', () => { { config: baseMockConfig, settings: fullVerbositySettings }, ); - await waitUntilReady(); + await act(async () => { + await waitUntilReady(); + }); if (visible) { expect(lastFrame()).toContain(name); diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index f9f8b5c67a..efbf2375f4 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -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; } @@ -213,14 +207,14 @@ export const ToolGroupMessage: React.FC = ({ 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) } @@ -271,7 +265,7 @@ export const ToolGroupMessage: React.FC = ({ */ 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; @@ -291,7 +285,7 @@ export const ToolGroupMessage: React.FC = ({ let marginTop = 0; if (isFirst) { - marginTop = borderTopOverride ?? true ? 1 : 0; + marginTop = (borderTopOverride ?? true) ? 1 : 0; } else if (!(isCompact && prevIsCompact)) { marginTop = 1; } diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx index 0bbe3446e0..4c8e590147 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx @@ -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 = ({ {contentData} ); - } else if (typeof contentData === 'object' && 'fileDiff' in contentData) { + } else if (isStructuredToolResult(contentData)) { + if (renderOutputAsMarkdown) { + content = ( + + ); + } else { + content = ( + + {contentData.summary} + + ); + } + } else if ( + typeof contentData === 'object' && + contentData !== null && + 'fileDiff' in contentData + ) { content = ( 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 " `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index 710c24ae75..9d332387b3 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -36,6 +36,7 @@ exports[` > Border Color Logic > uses gray border when all t │ ✓ test-tool A tool for testing │ │ │ │ Test result │ + │ │ │ ✓ another-tool A tool for testing │ │ │ @@ -66,10 +67,10 @@ exports[` > Golden Snapshots > renders canceled tool calls > exports[` > Golden Snapshots > renders empty tool calls array 1`] = `""`; exports[` > 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 │ █ @@ -85,6 +86,7 @@ exports[` > Golden Snapshots > renders mixed tool calls incl │ ✓ read_file Read a file │ │ │ │ Test result │ + │ │ │ ⊶ run_shell_command Run command │ │ │ @@ -103,6 +105,7 @@ exports[` > Golden Snapshots > renders multiple tool calls w │ ✓ successful-tool This tool succeeded │ │ │ │ Test result │ + │ │ │ o pending-tool This tool is pending │ │ │ @@ -152,6 +155,7 @@ exports[` > 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 │ │ │ @@ -176,10 +180,12 @@ exports[` > 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 │ │ │ diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap index da341abe37..eb4531299d 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap @@ -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 │ ▀ " `; diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index b314f1382b..4aa05fb57a 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -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, diff --git a/packages/core/src/tools/read-many-files.test.ts b/packages/core/src/tools/read-many-files.test.ts index 6a526d2b62..dd9d146c97 100644 --- a/packages/core/src/tools/read-many-files.test.ts +++ b/packages/core/src/tools/read-many-files.test.ts @@ -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');