Fix crash.

This commit is contained in:
Christian Gunderman
2026-04-02 15:27:42 -07:00
parent 9cf410478c
commit 619fcc495e
4 changed files with 64 additions and 27 deletions
@@ -35,11 +35,12 @@ export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
? Math.min(availableHeightLimit, maxLines)
: (availableHeightLimit ?? maxLines ?? DEFAULT_HEIGHT);
const arrayData = Array.isArray(data) ? data : [];
const lastLines = disableTruncation
? data
? arrayData
: numLinesRetained === 0
? []
: data.slice(-numLinesRetained);
: arrayData.slice(-numLinesRetained);
return (
<Box flexDirection="column" width={width} flexShrink={0} overflow="hidden">
{lastLines.map((line: AnsiLine, lineIndex: number) => (
@@ -148,6 +148,9 @@ describe('ToolResultDisplay', () => {
const diffResult = {
fileDiff: 'diff content',
fileName: 'test.ts',
filePath: 'test.ts',
originalContent: null,
newContent: 'new',
};
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<ToolResultDisplay
@@ -222,6 +225,34 @@ describe('ToolResultDisplay', () => {
unmount();
});
it('renders unknown objects as stringified JSON', async () => {
const unknownObject = {
hello: 'world',
nested: {
value: 42,
},
};
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
<ToolResultDisplay
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resultDisplay={unknownObject as any}
terminalWidth={80}
availableTerminalHeight={20}
/>,
{
config: makeFakeConfig({ useAlternateBuffer: false }),
settings: createMockSettings({ ui: { useAlternateBuffer: false } }),
},
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('"hello": "world"');
expect(output).toContain('"value": 42');
expect(output).toMatchSnapshot();
unmount();
});
it('does not fall back to plain text if availableHeight is set and not in alternate buffer', async () => {
// availableHeight calculation: 20 - 1 - 5 = 14 > 3
const { lastFrame, waitUntilReady, unmount } = await renderWithProviders(
@@ -12,9 +12,9 @@ import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
import { SlicingMaxSizedBox } from '../shared/SlicingMaxSizedBox.js';
import { theme } from '../../semantic-colors.js';
import {
type AnsiOutput,
type AnsiLine,
isSubagentProgress,
type ToolResultDisplay as CoreToolResultDisplay,
} from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
import { tryParseJSON } from '../../../utils/jsonoutput.js';
@@ -27,7 +27,7 @@ import { calculateToolContentMaxLines } from '../../utils/toolLayoutUtils.js';
import { SubagentProgressDisplay } from './SubagentProgressDisplay.js';
export interface ToolResultDisplayProps {
resultDisplay: string | object | undefined;
resultDisplay: CoreToolResultDisplay | undefined;
availableTerminalHeight?: number;
terminalWidth: number;
renderOutputAsMarkdown?: boolean;
@@ -36,11 +36,6 @@ export interface ToolResultDisplayProps {
overflowDirection?: 'top' | 'bottom';
}
interface FileDiffResult {
fileDiff: string;
fileName: string;
}
export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
resultDisplay,
availableTerminalHeight,
@@ -84,7 +79,7 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
return null;
}
const renderContent = (contentData: string | object | undefined) => {
const renderContent = (contentData: CoreToolResultDisplay | undefined) => {
// Check if string content is valid JSON and pretty-print it
const prettyJSON =
typeof contentData === 'string' ? tryParseJSON(contentData) : null;
@@ -123,28 +118,27 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
{contentData}
</Text>
);
} else if (typeof contentData === 'object' && 'fileDiff' in contentData) {
} else if (
contentData &&
typeof contentData === 'object' &&
'fileDiff' in contentData
) {
content = (
<DiffRenderer
diffContent={
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(contentData as FileDiffResult).fileDiff
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
filename={(contentData as FileDiffResult).fileName}
diffContent={contentData.fileDiff}
filename={contentData.fileName}
availableTerminalHeight={availableHeight}
terminalWidth={childWidth}
/>
);
} else {
} else if (Array.isArray(contentData)) {
const shouldDisableTruncation =
isAlternateBuffer ||
(availableTerminalHeight === undefined && maxLines === undefined);
content = (
<AnsiOutputText
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
data={contentData as AnsiOutput}
data={contentData}
availableTerminalHeight={
isAlternateBuffer ? undefined : availableHeight
}
@@ -153,6 +147,12 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
disableTruncation={shouldDisableTruncation}
/>
);
} else {
content = (
<Text wrap="wrap" color={theme.text.primary}>
{JSON.stringify(contentData, null, 2)}
</Text>
);
}
// Final render based on session mode
@@ -178,18 +178,13 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
// Virtualized path for large ANSI arrays
if (Array.isArray(resultDisplay)) {
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
const listHeight = Math.min(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(resultDisplay as AnsiOutput).length,
limit,
);
const listHeight = Math.min(resultDisplay.length, limit);
return (
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
<ScrollableList
width={childWidth}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
data={resultDisplay as AnsiOutput}
data={resultDisplay}
renderItem={renderVirtualizedAnsiLine}
estimatedItemHeight={() => 1}
keyExtractor={keyExtractor}
@@ -36,6 +36,16 @@ exports[`ToolResultDisplay > renders string result as plain text when renderOutp
"
`;
exports[`ToolResultDisplay > renders unknown objects as stringified JSON 1`] = `
"{
"hello": "world",
"nested": {
"value": 42
}
}
"
`;
exports[`ToolResultDisplay > truncates very long string results 1`] = `
"... 249 hidden (Ctrl+O) ...
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa