/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { Box, Text } from 'ink'; import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; import { DiffRenderer } from './DiffRenderer.js'; import { Colors } from '../../colors.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js'; 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 const MIN_LINES_HIDDEN = 3; // hide at least this many lines (or don't hide any) export type TextEmphasis = 'high' | 'medium' | 'low'; export interface ToolMessageProps extends IndividualToolCallDisplay { availableTerminalHeight: number; emphasis?: TextEmphasis; renderOutputAsMarkdown?: boolean; } export const ToolMessage: React.FC = ({ name, description, resultDisplay, status, availableTerminalHeight, emphasis = 'medium', renderOutputAsMarkdown = true, }) => { const resultIsString = typeof resultDisplay === 'string' && resultDisplay.trim().length > 0; const lines = React.useMemo( () => (resultIsString ? resultDisplay.split('\n') : []), [resultIsString, resultDisplay], ); let contentHeightEstimate = Math.max( availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT, MIN_LINES_SHOWN + 1, // enforce minimum lines shown ); // enforce minimum lines hidden (don't hide any otherwise) if (lines.length - contentHeightEstimate < MIN_LINES_HIDDEN) { contentHeightEstimate = lines.length; } // Truncate the overall string content if it's too long. // MarkdownRenderer will handle specific truncation for code blocks within this content. // Estimate available height for this specific tool message content area // This is a rough estimate; ideally, we'd have a more precise measurement. const displayableResult = React.useMemo( () => resultIsString ? lines.slice(-contentHeightEstimate).join('\n') : resultDisplay, [lines, resultIsString, contentHeightEstimate, resultDisplay], ); const hiddenLines = Math.max(0, lines.length - contentHeightEstimate); return ( {emphasis === 'high' && } {displayableResult && ( {hiddenLines > 0 && ( ... first {hiddenLines} line{hiddenLines === 1 ? '' : 's'}{' '} hidden ... )} {typeof displayableResult === 'string' && renderOutputAsMarkdown && ( )} {typeof displayableResult === 'string' && !renderOutputAsMarkdown && ( {displayableResult} )} {typeof displayableResult !== 'string' && ( )} )} ); }; type ToolStatusIndicatorProps = { status: ToolCallStatus; }; const ToolStatusIndicator: React.FC = ({ status, }) => ( {status === ToolCallStatus.Pending && ( o )} {status === ToolCallStatus.Executing && ( )} {status === ToolCallStatus.Success && ( )} {status === ToolCallStatus.Confirming && ( ? )} {status === ToolCallStatus.Canceled && ( - )} {status === ToolCallStatus.Error && ( x )} ); 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 Colors.Foreground; case 'medium': return Colors.Foreground; case 'low': return Colors.Gray; default: { const exhaustiveCheck: never = emphasis; return exhaustiveCheck; } } }, [emphasis]); return ( {name} {' '} {description} ); }; const TrailingIndicator: React.FC = () => ( );