/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { Box, Text } from 'ink'; import type { IndividualToolCallDisplay } from '../../types.js'; import { ToolCallStatus } from '../../types.js'; import { DiffRenderer } from './DiffRenderer.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { AnsiOutputText } from '../AnsiOutput.js'; import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js'; import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { ShellInputPrompt } from '../ShellInputPrompt.js'; import { SHELL_COMMAND_NAME, SHELL_NAME, TOOL_STATUS, } from '../../constants.js'; import { theme } from '../../semantic-colors.js'; import type { AnsiOutput, Config } from '@google/gemini-cli-core'; const STATIC_HEIGHT = 1; const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc. const STATUS_INDICATOR_WIDTH = 3; const MIN_LINES_SHOWN = 2; // show at least this many lines // Large threshold to ensure we don't cause performance issues for very large // outputs that will get truncated further MaxSizedBox anyway. const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 1000000; export type TextEmphasis = 'high' | 'medium' | 'low'; export interface ToolMessageProps extends IndividualToolCallDisplay { availableTerminalHeight?: number; terminalWidth: number; emphasis?: TextEmphasis; renderOutputAsMarkdown?: boolean; activeShellPtyId?: number | null; shellFocused?: boolean; config?: Config; } export const ToolMessage: React.FC = ({ name, description, resultDisplay, status, availableTerminalHeight, terminalWidth, emphasis = 'medium', renderOutputAsMarkdown = true, activeShellPtyId, shellFocused, ptyId, config, }) => { const isThisShellFocused = (name === SHELL_COMMAND_NAME || name === 'Shell') && status === ToolCallStatus.Executing && ptyId === activeShellPtyId && shellFocused; const isThisShellFocusable = (name === SHELL_COMMAND_NAME || name === 'Shell') && status === ToolCallStatus.Executing && config?.getShouldUseNodePtyShell(); const availableHeight = availableTerminalHeight ? Math.max( availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT, MIN_LINES_SHOWN + 1, // enforce minimum lines shown ) : undefined; // Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly, // we're forcing it to not render as markdown when the response is too long, it will fallback // to render as plain text, which is contained within the terminal using MaxSizedBox if (availableHeight) { renderOutputAsMarkdown = false; } const childWidth = terminalWidth - 3; // account for padding. if (typeof resultDisplay === 'string') { if (resultDisplay.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) { // Truncate the result display to fit within the available width. resultDisplay = '...' + resultDisplay.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS); } } return ( {isThisShellFocusable && ( {isThisShellFocused ? '(Focused)' : '(ctrl+f to focus)'} )} {emphasis === 'high' && } {resultDisplay && ( {typeof resultDisplay === 'string' && renderOutputAsMarkdown ? ( ) : typeof resultDisplay === 'string' && !renderOutputAsMarkdown ? ( {resultDisplay} ) : typeof resultDisplay === 'object' && 'fileDiff' in resultDisplay ? ( ) : ( )} )} {isThisShellFocused && config && ( )} ); }; type ToolStatusIndicatorProps = { status: ToolCallStatus; name: string; }; const ToolStatusIndicator: React.FC = ({ status, name, }) => { const isShell = name === SHELL_COMMAND_NAME || name === SHELL_NAME; const statusColor = isShell ? theme.ui.symbol : theme.status.warning; return ( {status === ToolCallStatus.Pending && ( {TOOL_STATUS.PENDING} )} {status === ToolCallStatus.Executing && ( )} {status === ToolCallStatus.Success && ( {TOOL_STATUS.SUCCESS} )} {status === ToolCallStatus.Confirming && ( {TOOL_STATUS.CONFIRMING} )} {status === ToolCallStatus.Canceled && ( {TOOL_STATUS.CANCELED} )} {status === ToolCallStatus.Error && ( {TOOL_STATUS.ERROR} )} ); }; type ToolInfo = { name: string; description: string; status: ToolCallStatus; emphasis: TextEmphasis; }; const ToolInfo: React.FC = ({ name, description, status, emphasis, }) => { const nameColor = React.useMemo(() => { switch (emphasis) { case 'high': return theme.text.primary; case 'medium': return theme.text.primary; case 'low': return theme.text.secondary; default: { const exhaustiveCheck: never = emphasis; return exhaustiveCheck; } } }, [emphasis]); return ( {name} {' '} {description} ); }; const TrailingIndicator: React.FC = () => ( {' '} ← );