/** * @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 { Colors } from '../../colors.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js'; import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { TodoDisplay } from '../TodoDisplay.js'; import { TOOL_STATUS } from '../../constants.js'; import type { TodoResultDisplay, TaskResultDisplay, Config, } from '@qwen-code/qwen-code-core'; import { AgentExecutionDisplay } from '../subagents/index.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 // 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'; type DisplayRendererResult = | { type: 'none' } | { type: 'todo'; data: TodoResultDisplay } | { type: 'string'; data: string } | { type: 'diff'; data: { fileDiff: string; fileName: string } } | { type: 'task'; data: TaskResultDisplay }; /** * Custom hook to determine the type of result display and return appropriate rendering info */ const useResultDisplayRenderer = ( resultDisplay: unknown, ): DisplayRendererResult => React.useMemo(() => { if (!resultDisplay) { return { type: 'none' }; } // Check for TodoResultDisplay if ( typeof resultDisplay === 'object' && resultDisplay !== null && 'type' in resultDisplay && resultDisplay.type === 'todo_list' ) { return { type: 'todo', data: resultDisplay as TodoResultDisplay, }; } // Check for SubagentExecutionResultDisplay (for non-task tools) if ( typeof resultDisplay === 'object' && resultDisplay !== null && 'type' in resultDisplay && resultDisplay.type === 'task_execution' ) { return { type: 'task', data: resultDisplay as TaskResultDisplay, }; } // Check for FileDiff if ( typeof resultDisplay === 'object' && resultDisplay !== null && 'fileDiff' in resultDisplay ) { return { type: 'diff', data: resultDisplay as { fileDiff: string; fileName: string }, }; } // Default to string return { type: 'string', data: resultDisplay as string, }; }, [resultDisplay]); /** * Component to render todo list results */ const TodoResultRenderer: React.FC<{ data: TodoResultDisplay }> = ({ data, }) => ; /** * Component to render subagent execution results */ const SubagentExecutionRenderer: React.FC<{ data: TaskResultDisplay; availableHeight?: number; childWidth: number; config: Config; }> = ({ data, availableHeight, childWidth, config }) => ( ); /** * Component to render string results (markdown or plain text) */ const StringResultRenderer: React.FC<{ data: string; renderAsMarkdown: boolean; availableHeight?: number; childWidth: number; }> = ({ data, renderAsMarkdown, availableHeight, childWidth }) => { let displayData = data; // Truncate if too long if (displayData.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) { displayData = '...' + displayData.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS); } if (renderAsMarkdown) { return ( ); } return ( {displayData} ); }; /** * Component to render diff results */ const DiffResultRenderer: React.FC<{ data: { fileDiff: string; fileName: string }; availableHeight?: number; childWidth: number; }> = ({ data, availableHeight, childWidth }) => ( ); export interface ToolMessageProps extends IndividualToolCallDisplay { availableTerminalHeight?: number; terminalWidth: number; emphasis?: TextEmphasis; renderOutputAsMarkdown?: boolean; config: Config; } export const ToolMessage: React.FC = ({ name, description, resultDisplay, status, availableTerminalHeight, terminalWidth, emphasis = 'medium', renderOutputAsMarkdown = true, config, }) => { 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. // Use the custom hook to determine the display type const displayRenderer = useResultDisplayRenderer(resultDisplay); return ( {emphasis === 'high' && } {displayRenderer.type !== 'none' && ( {displayRenderer.type === 'todo' && ( )} {displayRenderer.type === 'task' && ( )} {displayRenderer.type === 'string' && ( )} {displayRenderer.type === 'diff' && ( )} )} ); }; type ToolStatusIndicatorProps = { status: ToolCallStatus; }; const ToolStatusIndicator: React.FC = ({ status, }) => ( {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 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 = () => ( {' '} ← );