import { memo, useMemo, useState } from 'react' import { LoaderCircle } from 'lucide-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 isPending?: boolean partialInput?: string } 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', } const WRITER_PREVIEW_MAX_LINES = 120 const WRITER_PREVIEW_MAX_CHARS = 30000 export const ToolCallBlock = memo(function ToolCallBlock({ toolName, input, result, compact = false, isPending = false, partialInput }: 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 pendingSummary = isPending && !result ? getPendingSummary(toolName, t) : '' const preview = useMemo(() => renderPreview(toolName, obj, result, t), [obj, result, toolName, t]) const details = useMemo(() => renderDetails(toolName, obj, t, isPending ? partialInput : undefined), [isPending, obj, partialInput, toolName, t]) const hasResultDetails = Boolean(result && extractTextContent(result.content)) const hasEditPreview = toolName === 'Edit' && typeof obj.old_string === 'string' && typeof obj.new_string === 'string' const hasWritePreview = toolName === 'Write' && typeof obj.content === 'string' const expandable = hasEditPreview || hasWritePreview || hasResultDetails || Boolean(isPending && partialInput) 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, partialInput?: string, ) { if (partialInput) { if (toolName === 'Write') { const writerContent = extractPartialJsonStringField(partialInput, 'content') if (writerContent) { return renderWriterPreview(writerContent, t) } } return renderPartialInput(partialInput, t) } if (toolName === 'Edit' || toolName === 'Write') { return null } const text = JSON.stringify(obj, null, 2) return (
{t?.('tool.toolInput') ?? 'Tool Input'}
) } function extractPartialJsonStringField(source: string, field: string): string | null { const key = `"${field}"` const keyIndex = source.indexOf(key) if (keyIndex < 0) return null const colonIndex = source.indexOf(':', keyIndex + key.length) if (colonIndex < 0) return null let index = colonIndex + 1 while (index < source.length && /\s/.test(source[index] ?? '')) index += 1 if (source[index] !== '"') return null index += 1 let value = '' while (index < source.length) { const char = source[index] if (char === '"') return value if (char !== '\\') { value += char index += 1 continue } const escaped = source[index + 1] if (escaped === undefined) break switch (escaped) { case 'n': value += '\n' index += 2 break case 'r': value += '\r' index += 2 break case 't': value += '\t' index += 2 break case 'b': value += '\b' index += 2 break case 'f': value += '\f' index += 2 break case '"': case '\\': case '/': value += escaped index += 2 break case 'u': { const hex = source.slice(index + 2, index + 6) if (/^[0-9a-fA-F]{4}$/.test(hex)) { value += String.fromCharCode(Number.parseInt(hex, 16)) index += 6 } else { index = source.length } break } default: value += escaped index += 2 break } } return value } function renderWriterPreview( content: string, t?: (key: TranslationKey, params?: Record) => string, ) { const lines = content.split('\n') const totalLines = lines.length const visibleLines = lines.length > WRITER_PREVIEW_MAX_LINES ? lines.slice(-WRITER_PREVIEW_MAX_LINES) : lines let visibleContent = visibleLines.join('\n') const charTruncated = visibleContent.length > WRITER_PREVIEW_MAX_CHARS if (charTruncated) { visibleContent = visibleContent.slice(-WRITER_PREVIEW_MAX_CHARS) } const lineWindowed = totalLines > visibleLines.length const isWindowed = lineWindowed || charTruncated return (
{t?.('tool.writerPreview') ?? 'Writer'} {isWindowed ? ( {t?.('tool.writerPreviewLatest', { visible: visibleLines.length, total: totalLines }) ?? `Showing latest ${visibleLines.length} of ${totalLines} lines`} ) : null}
        {visibleContent}
      
) } function renderPartialInput( partialInput: string, t?: (key: TranslationKey, params?: Record) => string, ) { return (
{t?.('tool.partialInput') ?? 'Partial input'}
) } function getPendingSummary( toolName: string, t?: (key: TranslationKey, params?: Record) => string, ): string { if (toolName === 'Write') return t?.('tool.generatingContent') ?? 'Generating content' if (toolName === 'Edit' || toolName === 'MultiEdit') return t?.('tool.preparingEdit') ?? 'Preparing edit' return t?.('tool.preparingTool') ?? 'Preparing tool' } 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` }