'use client' import { type ToolCallMessagePartProps } from '@assistant-ui/react' import { useStore } from '@nanostores/react' import { ChevronRight } from 'lucide-react' import { useEffect, useState } from 'react' import { useElapsedSeconds } from '@/components/assistant-ui/activity-timer' import { ActivityTimerText } from '@/components/assistant-ui/activity-timer-text' import { cn } from '@/lib/utils' import { $toolInlineDiffs } from '@/store/tool-diffs' const TOOL_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] const TOOL_SPINNER_INTERVAL_MS = 80 function titleForTool(name: string): string { return ( name .split('_') .filter(Boolean) .map(part => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`) .join(' ') || name ) } function toolLabel(name: string, isPending: boolean): string { const labels: Record = { edit_file: { done: 'Edited file', pending: 'Editing file' }, execute_code: { done: 'Ran code', pending: 'Running code' }, image_generate: { done: 'Generated image', pending: 'Generating image' }, list_files: { done: 'Listed files', pending: 'Listing files' }, read_file: { done: 'Read file', pending: 'Reading file' }, search_files: { done: 'Searched files', pending: 'Searching files' }, session_search_recall: { done: 'Searched session history', pending: 'Searching session history' }, terminal: { done: 'Ran command', pending: 'Running command' }, todo: { done: 'Updated todos', pending: 'Updating todos' }, web_extract: { done: 'Read webpage', pending: 'Reading webpage' }, web_search: { done: 'Searched the web', pending: 'Searching the web' }, write_file: { done: 'Edited file', pending: 'Editing file' } } if (labels[name]) { return isPending ? labels[name].pending : labels[name].done } return `${isPending ? 'Using' : 'Used'} ${titleForTool(name)}` } function compactPreview(value: unknown, max = 72): string { const text = typeof value === 'string' ? value : value && typeof value === 'object' && 'context' in value ? String((value as { context?: unknown }).context ?? '') : '' const oneLine = text.replace(/\s+/g, ' ').trim() return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine } function shouldShowInlinePreview(toolName: string): boolean { return !['image_generate', 'terminal', 'execute_code'].includes(toolName) } function contextValue(value: unknown): string { if (typeof value === 'string') { return value } if (value && typeof value === 'object' && 'context' in value) { return String((value as { context?: unknown }).context ?? '') } return '' } function prettyJson(value: unknown): string { return typeof value === 'string' ? value : JSON.stringify(value, null, 2) } function recordValue(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) : {} } function stripAnsi(value: string): string { return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g'), '') } function stripInlineDiffChrome(value: string): string { return value ? stripAnsi(value).replace(/^\s*┊\s*review diff\s*\n/i, '').trim() : '' } function inlineDiffFromResult(result: unknown): string { const value = recordValue(result).inline_diff return typeof value === 'string' ? stripInlineDiffChrome(value) : '' } function detailLabel(toolName: string): string { if (toolName === 'image_generate') { return 'Prompt' } if (toolName === 'web_search') { return 'Query' } if (toolName === 'web_extract') { return 'URL' } if (toolName === 'terminal') { return 'Command' } if (toolName === 'execute_code') { return 'Code' } return 'Input' } function detailText(args: unknown, result: unknown): string { const argContext = contextValue(args) const resultContext = contextValue(result) if (resultContext && resultContext !== argContext) { return resultContext } if (argContext) { return argContext } if (result !== undefined) { return prettyJson(result) } return prettyJson(args) } export const ToolFallback = ({ toolCallId, toolName, args, result }: ToolCallMessagePartProps) => { const [open, setOpen] = useState(false) const isPending = result === undefined const [tick, setTick] = useState(0) const elapsed = useElapsedSeconds(isPending) const preview = compactPreview(args) || compactPreview(result) const label = toolLabel(toolName, isPending) const detail = detailText(args, result) const liveDiffs = useStore($toolInlineDiffs) const sideDiff = toolCallId ? liveDiffs[toolCallId] || '' : '' const inlineDiff = stripInlineDiffChrome(sideDiff) || inlineDiffFromResult(result) const spinnerFrame = TOOL_SPINNER_FRAMES[tick % TOOL_SPINNER_FRAMES.length] useEffect(() => { if (!isPending) { return } const id = window.setInterval(() => setTick(value => value + 1), TOOL_SPINNER_INTERVAL_MS) return () => window.clearInterval(id) }, [isPending]) return (
{open && (
{detailLabel(toolName)}: {detail}
)} {inlineDiff && }
) } function InlineDiff({ text }: { text: string }) { return (
      {text.split('\n').map((line, index) => {
        const added = line.startsWith('+') && !line.startsWith('+++')
        const removed = line.startsWith('-') && !line.startsWith('---')
        const hunk = line.startsWith('@@')
        const fileHeader = line.startsWith('---') || line.startsWith('+++') || / → /.test(line.slice(0, 60))

        return (
          
            {line || ' '}
          
        )
      })}
    
) }