import { useMemo, useState } from 'react' import { CodeViewer } from './CodeViewer' import { DiffViewer } from './DiffViewer' import { TerminalChrome } from './TerminalChrome' import { CopyButton } from '../shared/CopyButton' import { useTranslation } from '../../i18n' import type { TranslationKey } from '../../i18n' import { InlineImageGallery } from './InlineImageGallery' import type { AgentTaskNotification } from '../../types/chat' type Props = { toolName: string input: unknown result?: { content: unknown; isError: boolean } | null agentTaskNotification?: AgentTaskNotification compact?: boolean } const TOOL_ICONS: Record = { Bash: 'terminal', Read: 'description', Write: 'edit_document', Edit: 'edit_note', Glob: 'search', Grep: 'find_in_page', Agent: 'smart_toy', WebSearch: 'travel_explore', WebFetch: 'cloud_download', NotebookEdit: 'note', Skill: 'auto_awesome', } export function ToolCallBlock({ toolName, input, result, compact = false }: Props) { const [expanded, setExpanded] = useState(false) const t = useTranslation() const obj = input && typeof input === 'object' ? (input as Record) : {} const icon = TOOL_ICONS[toolName] || 'build' const filePath = typeof obj.file_path === 'string' ? obj.file_path : '' const summary = getToolSummary(toolName, obj, t) const outputSummary = getToolResultSummary( toolName, result?.content, result?.isError ?? false, t, ) const preview = useMemo(() => renderPreview(toolName, obj, result, t), [obj, result, toolName, t]) const details = useMemo(() => renderDetails(toolName, obj, t), [obj, toolName, t]) const hasResultDetails = Boolean(result && extractTextContent(result.content)) const expandable = toolName === 'Edit' || toolName === 'Write' || hasResultDetails return (
{expandable && expanded && (
{preview} {details}
)}
) } function renderPreview( toolName: string, obj: Record, result?: { content: unknown; isError: boolean } | null, t?: (key: TranslationKey, params?: Record) => string, ) { const filePath = typeof obj.file_path === 'string' ? obj.file_path : 'file' if (toolName === 'Edit' && typeof obj.old_string === 'string' && typeof obj.new_string === 'string') { return } if (toolName === 'Write' && typeof obj.content === 'string') { return } if (toolName === 'Bash' && typeof obj.command === 'string') { return (
$ {obj.command}
) } if (toolName === 'Read') { return null } if (result) { const text = extractTextContent(result.content) if (text) { return ( <>
{result.isError ? t?.('tool.errorOutput') ?? 'Error Output' : t?.('tool.toolOutput') ?? 'Tool Output'}
) } } return null } function renderDetails(toolName: string, obj: Record, t?: (key: TranslationKey, params?: Record) => string) { if (toolName === 'Edit' || toolName === 'Write') { return null } const text = JSON.stringify(obj, null, 2) return (
{t?.('tool.toolInput') ?? 'Tool Input'}
) } function getToolResultSummary( toolName: string, content: unknown, isError: boolean, t?: (key: TranslationKey, params?: Record) => string, ): string { const text = extractTextContent(content) if (!text) return '' if (isError) { const firstLine = text .split('\n') .map((line) => stripAnsi(line).replace(/\s+/g, ' ').trim()) .find(Boolean) if (!firstLine) { return t?.('tool.error') ?? 'Error' } return firstLine.length <= 72 ? firstLine : `${firstLine.slice(0, 72)}…` } if (toolName === 'Bash') return '' const lineCount = text.split('\n').length if (lineCount > 1) { return t?.('tool.linesOutput', { count: lineCount }) ?? `${lineCount} lines output` } const compact = text.replace(/\s+/g, ' ').trim() if (!compact) return '' if (compact.length <= 36) return compact return `${compact.slice(0, 36)}…` } function stripAnsi(value: string): string { return value.replace(/\x1B\[[0-9;]*m/g, '') } function getToolSummary(toolName: string, obj: Record, t?: (key: TranslationKey, params?: Record) => string): string { switch (toolName) { case 'Bash': return typeof obj.command === 'string' ? obj.command : '' case 'Read': return t?.('tool.readFileContents') ?? 'Read file contents' case 'Write': return typeof obj.content === 'string' ? (t?.('tool.linesCreated', { count: obj.content.split('\n').length }) ?? `${obj.content.split('\n').length} lines created`) : (t?.('tool.createFile') ?? 'Create file') case 'Edit': return typeof obj.old_string === 'string' && typeof obj.new_string === 'string' ? changedLineSummary(obj.old_string, obj.new_string, t) : (t?.('tool.updateFileContents') ?? 'Update file contents') case 'Glob': return typeof obj.pattern === 'string' ? obj.pattern : '' case 'Grep': return typeof obj.pattern === 'string' ? obj.pattern : '' case 'Agent': return typeof obj.description === 'string' ? obj.description : '' default: return '' } } function extractTextContent(content: unknown): string | null { if (typeof content === 'string') return content if (Array.isArray(content)) { return content .map((chunk: any) => (typeof chunk === 'string' ? chunk : chunk?.text || '')) .filter(Boolean) .join('\n') } if (content && typeof content === 'object') { return JSON.stringify(content, null, 2) } return null } function changedLineSummary(oldString: string, newString: string, t?: (key: TranslationKey, params?: Record) => string): string { const oldLines = oldString.split('\n') const newLines = newString.split('\n') let changed = 0 const max = Math.max(oldLines.length, newLines.length) for (let index = 0; index < max; index += 1) { if ((oldLines[index] ?? '') !== (newLines[index] ?? '')) { changed += 1 } } return t?.('tool.linesChanged', { count: changed }) ?? `${changed} lines changed` }