/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { Box, Text } from 'ink'; import { DiffRenderer } from './DiffRenderer.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js'; import { SlicingMaxSizedBox } from '../shared/SlicingMaxSizedBox.js'; import { theme } from '../../semantic-colors.js'; import { type AnsiOutput, type AnsiLine, isSubagentProgress, } 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'; import { calculateToolContentMaxLines } from '../../utils/toolLayoutUtils.js'; import { SubagentProgressDisplay } from './SubagentProgressDisplay.js'; export interface ToolResultDisplayProps { resultDisplay: string | object | undefined; availableTerminalHeight?: number; terminalWidth: number; renderOutputAsMarkdown?: boolean; maxLines?: number; hasFocus?: boolean; overflowDirection?: 'top' | 'bottom'; } interface FileDiffResult { fileDiff: string; fileName: string; } export const ToolResultDisplay: React.FC = ({ resultDisplay, availableTerminalHeight, terminalWidth, renderOutputAsMarkdown = true, maxLines, hasFocus = false, overflowDirection = 'top', }) => { const { renderMarkdown } = useUIState(); const isAlternateBuffer = useAlternateBuffer(); const availableHeight = calculateToolContentMaxLines({ availableTerminalHeight, isAlternateBuffer, maxLinesLimit: maxLines, }); const combinedPaddingAndBorderWidth = 4; const childWidth = terminalWidth - combinedPaddingAndBorderWidth; const keyExtractor = React.useCallback( (_: AnsiLine, index: number) => index.toString(), [], ); const renderVirtualizedAnsiLine = React.useCallback( ({ item }: { item: AnsiLine }) => ( ), [], ); if (!resultDisplay) return null; // 1. Early return for background tools (Todos) if (typeof resultDisplay === 'object' && 'todos' in resultDisplay) { // display nothing, as the TodoTray will handle rendering todos return null; } const renderContent = (contentData: string | object | undefined) => { // Check if string content is valid JSON and pretty-print it const prettyJSON = typeof contentData === 'string' ? tryParseJSON(contentData) : null; const formattedJSON = prettyJSON ? JSON.stringify(prettyJSON, null, 2) : null; let content: React.ReactNode; if (formattedJSON) { // Render pretty-printed JSON content = ( {formattedJSON} ); } else if (isSubagentProgress(contentData)) { content = ; } else if (typeof contentData === 'string' && renderOutputAsMarkdown) { content = ( ); } else if (typeof contentData === 'string' && !renderOutputAsMarkdown) { content = ( {contentData} ); } else if (typeof contentData === 'object' && 'fileDiff' in contentData) { content = ( ); } else { const shouldDisableTruncation = isAlternateBuffer || (availableTerminalHeight === undefined && maxLines === undefined); content = ( ); } // Final render based on session mode if (isAlternateBuffer) { return ( {content} ); } return content; }; // ASB Mode Handling (Interactive/Fullscreen) if (isAlternateBuffer) { // 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, ); return ( 1} keyExtractor={keyExtractor} initialScrollIndex={SCROLL_TO_ITEM_END} hasFocus={hasFocus} /> ); } // Standard path for strings/diffs in ASB return ( {renderContent(resultDisplay)} ); } // Standard Mode Handling (History/Scrollback) // We use SlicingMaxSizedBox which includes MaxSizedBox for precision truncation + hidden labels return ( {(truncatedResultDisplay) => renderContent(truncatedResultDisplay)} ); };