mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-23 03:24:42 -07:00
feat(cli): truncate shell output in UI history and improve active shell display (#17438)
This commit is contained in:
@@ -8,12 +8,17 @@ import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
|
||||
import { AnsiOutputText } from '../AnsiOutput.js';
|
||||
import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
|
||||
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import type { AnsiOutput } from '@google/gemini-cli-core';
|
||||
import type { AnsiOutput, AnsiLine } from '@google/gemini-cli-core';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { tryParseJSON } from '../../../utils/jsonoutput.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
import { ScrollableList } from '../shared/ScrollableList.js';
|
||||
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
|
||||
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||
|
||||
const STATIC_HEIGHT = 1;
|
||||
const RESERVED_LINE_COUNT = 6; // for tool name, status, padding, and 'ShowMoreLines' hint
|
||||
@@ -28,6 +33,8 @@ export interface ToolResultDisplayProps {
|
||||
availableTerminalHeight?: number;
|
||||
terminalWidth: number;
|
||||
renderOutputAsMarkdown?: boolean;
|
||||
maxLines?: number;
|
||||
hasFocus?: boolean;
|
||||
}
|
||||
|
||||
interface FileDiffResult {
|
||||
@@ -40,30 +47,100 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
availableTerminalHeight,
|
||||
terminalWidth,
|
||||
renderOutputAsMarkdown = true,
|
||||
maxLines,
|
||||
hasFocus = false,
|
||||
}) => {
|
||||
const { renderMarkdown } = useUIState();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
|
||||
const availableHeight = availableTerminalHeight
|
||||
let availableHeight = availableTerminalHeight
|
||||
? Math.max(
|
||||
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
|
||||
MIN_LINES_SHOWN + 1, // enforce minimum lines shown
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (maxLines && availableHeight) {
|
||||
availableHeight = Math.min(availableHeight, maxLines);
|
||||
}
|
||||
|
||||
const combinedPaddingAndBorderWidth = 4;
|
||||
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
|
||||
|
||||
const keyExtractor = React.useCallback(
|
||||
(_: AnsiLine, index: number) => index.toString(),
|
||||
[],
|
||||
);
|
||||
|
||||
const renderVirtualizedAnsiLine = React.useCallback(
|
||||
({ item }: { item: AnsiLine }) => (
|
||||
<Box height={1} overflow="hidden">
|
||||
<AnsiLineText line={item} />
|
||||
</Box>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const truncatedResultDisplay = React.useMemo(() => {
|
||||
if (typeof resultDisplay === 'string') {
|
||||
if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||
return '...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||
// Only truncate string output if not in alternate buffer mode to ensure
|
||||
// we can scroll through the full output.
|
||||
if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
|
||||
let text = resultDisplay;
|
||||
if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
|
||||
text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
|
||||
}
|
||||
if (maxLines) {
|
||||
const hasTrailingNewline = text.endsWith('\n');
|
||||
const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
|
||||
const lines = contentText.split('\n');
|
||||
if (lines.length > maxLines) {
|
||||
text =
|
||||
lines.slice(-maxLines).join('\n') +
|
||||
(hasTrailingNewline ? '\n' : '');
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return resultDisplay;
|
||||
}, [resultDisplay]);
|
||||
}, [resultDisplay, isAlternateBuffer, maxLines]);
|
||||
|
||||
if (!truncatedResultDisplay) return null;
|
||||
|
||||
// 1. Early return for background tools (Todos)
|
||||
if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'todos' in truncatedResultDisplay
|
||||
) {
|
||||
// display nothing, as the TodoTray will handle rendering todos
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. High-performance path: Virtualized ANSI in interactive mode
|
||||
if (isAlternateBuffer && Array.isArray(truncatedResultDisplay)) {
|
||||
// If availableHeight is undefined, fallback to a safe default to prevents infinite loop
|
||||
// where Container grows -> List renders more -> Container grows.
|
||||
const limit = maxLines ?? availableHeight ?? ACTIVE_SHELL_MAX_LINES;
|
||||
const listHeight = Math.min(
|
||||
(truncatedResultDisplay as AnsiOutput).length,
|
||||
limit,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
|
||||
<ScrollableList
|
||||
width={childWidth}
|
||||
data={truncatedResultDisplay as AnsiOutput}
|
||||
renderItem={renderVirtualizedAnsiLine}
|
||||
estimatedItemHeight={() => 1}
|
||||
keyExtractor={keyExtractor}
|
||||
initialScrollIndex={SCROLL_TO_ITEM_END}
|
||||
hasFocus={hasFocus}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Compute content node for non-virtualized paths
|
||||
// Check if string content is valid JSON and pretty-print it
|
||||
const prettyJSON =
|
||||
typeof truncatedResultDisplay === 'string'
|
||||
@@ -113,22 +190,38 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
|
||||
terminalWidth={childWidth}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
typeof truncatedResultDisplay === 'object' &&
|
||||
'todos' in truncatedResultDisplay
|
||||
) {
|
||||
// display nothing, as the TodoTray will handle rendering todos
|
||||
return null;
|
||||
} else {
|
||||
const shouldDisableTruncation =
|
||||
isAlternateBuffer ||
|
||||
(availableTerminalHeight === undefined && maxLines === undefined);
|
||||
|
||||
content = (
|
||||
<AnsiOutputText
|
||||
data={truncatedResultDisplay as AnsiOutput}
|
||||
availableTerminalHeight={availableHeight}
|
||||
availableTerminalHeight={
|
||||
isAlternateBuffer ? undefined : availableHeight
|
||||
}
|
||||
width={childWidth}
|
||||
maxLines={isAlternateBuffer ? undefined : maxLines}
|
||||
disableTruncation={shouldDisableTruncation}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Final render based on session mode
|
||||
if (isAlternateBuffer) {
|
||||
return (
|
||||
<Scrollable
|
||||
width={childWidth}
|
||||
maxHeight={maxLines ?? availableHeight}
|
||||
hasFocus={hasFocus} // Allow scrolling via keyboard (Shift+Up/Down)
|
||||
scrollToBottom={true}
|
||||
>
|
||||
{content}
|
||||
</Scrollable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box width={childWidth} flexDirection="column">
|
||||
<MaxSizedBox maxHeight={availableHeight} maxWidth={childWidth}>
|
||||
|
||||
Reference in New Issue
Block a user