import { useState } from 'react'; import type { ReactNode } from 'react'; import { Check, ChevronRight, Loader2, X } from 'lucide-react'; import type { ToolCall, ToolResult } from '@/api/types'; import { sessionEvents } from '@/hooks/sessionEvents'; // v1.8.2: cap on the inline arg-summary length. Expanded view shows full // args + full result, so this is purely a single-line render budget. const ARG_SUMMARY_MAX = 60; export interface ToolRun { call: ToolCall; // null while the call is in flight or the matching tool result hasn't // arrived yet on the WS stream. result: ToolResult | null; } function truncate(s: string, n: number): string { return s.length > n ? s.slice(0, n - 1) + '…' : s; } // Per-tool argument summary mapping from the v1.8.2 spec. Goal is a single // scannable line that surfaces the *what* (path / pattern) without // overwhelming the chat with full JSON. export function formatToolArgs(name: string, args: Record): string { if (name === 'view_file') { const path = String(args.path ?? ''); const start = args.start_line; const end = args.end_line; if (typeof start === 'number' && typeof end === 'number') { return truncate(`${path}:${start}-${end}`, ARG_SUMMARY_MAX); } if (typeof start === 'number') { return truncate(`${path}:${start}`, ARG_SUMMARY_MAX); } return truncate(path, ARG_SUMMARY_MAX); } if (name === 'list_dir') { return truncate(String(args.path ?? '.'), ARG_SUMMARY_MAX); } if (name === 'grep') { const pattern = String(args.pattern ?? ''); const path = args.path ? ` ${String(args.path)}` : ''; return truncate(`"${pattern}"${path}`, ARG_SUMMARY_MAX); } if (name === 'find_files') { return truncate(String(args.pattern ?? ''), ARG_SUMMARY_MAX); } if (name === 'git_status') { return ''; } // Unknown tool — surface first arg value or the literal {} so the user can // see something happened. Forward-compatible with future tools. const keys = Object.keys(args); if (keys.length === 0) return '{}'; const first = keys[0]!; return truncate(`${first}: ${String(args[first])}`, ARG_SUMMARY_MAX); } export function runStatus(run: ToolRun): 'pending' | 'success' | 'error' { if (run.result === null) return 'pending'; if (run.result.error) return 'error'; return 'success'; } // Path-shaped paths in tool output text get a click handler so users can // jump to the file. Same heuristic as MessageBubble.linkifyPaths. const PATH_REGEX = /([a-zA-Z0-9._/-]+\.[a-zA-Z0-9]+)/g; function linkifyOutput(text: string): ReactNode[] { const out: ReactNode[] = []; let lastIdx = 0; let idx = 0; for (const match of text.matchAll(PATH_REGEX)) { const matchedText = match[0]; const start = match.index ?? 0; if (!matchedText.includes('/')) continue; if (start > lastIdx) out.push(text.slice(lastIdx, start)); out.push( ); lastIdx = start + matchedText.length; idx += 1; } if (lastIdx < text.length) out.push(text.slice(lastIdx)); return out.length > 0 ? out : [text]; } interface Props { run: ToolRun; // When rendered inside a ToolCallGroup the line is already nested under a // shared header, so the leading arrow is dropped to avoid double indent. insideGroup?: boolean; } export function ToolCallLine({ run, insideGroup }: Props) { const [open, setOpen] = useState(false); const status = runStatus(run); const args = run.call.args ?? {}; const summary = formatToolArgs(run.call.name, args); return (
{open && (
            {JSON.stringify(args, null, 2)}
          
{run.result && (
              {run.result.error ? (
                {run.result.error}
              ) : (
                linkifyOutput(
                  typeof run.result.output === 'string'
                    ? run.result.output
                    : JSON.stringify(run.result.output, null, 2)
                )
              )}
              {run.result.truncated && (
                
— output truncated —
)}
)}
)}
); }