import type { GenericToolStatus } from "./generic-tool-model"; import { safeActivityDetail, summarizeShellCommand } from "./activity-text"; import { presentWebSearchAction } from "./web-search-model"; import { displayWebHost, formatCompactWebUrl, parseSafeActivityHttpUrl } from "./web-url"; export interface TraceDescription { kind: "search" | "tool" | "done" | "trace"; label: string; detail: string; icon?: "clock"; url?: string; host?: string; } export function describeTraceLine( line: string, status: GenericToolStatus, result?: unknown, ): TraceDescription { const trimmed = line.trim(); const functionMatch = /^([a-zA-Z0-9_.-]+)\((.*)\)$/.exec(trimmed); const name = (functionMatch?.[1] ?? "").toLowerCase().split(".").pop() || ""; const args = functionMatch?.[2] ?? ""; const parsedUrl = traceUrlFromArgs(args, trimmed); const webDetail = parsedUrl ? formatCompactWebUrl(parsedUrl) : ""; const plainWebReadTrace = !!parsedUrl && /\b(fetch(?:ing|ed)?|read(?:ing)?|opened?|opening)\b/i.test(trimmed); if (/search/i.test(name)) { const query = traceFieldFromArgs(args, ["query", "q", "text"]) || args || trimmed; return { kind: "search", label: presentWebSearchAction(query, status), detail: "", }; } if (/fetch|read|open/i.test(name) || plainWebReadTrace) { const rawTarget = traceFieldFromArgs(args, ["path", "file_path", "url"]) || args || trimmed; const pageTitle = parsedUrl ? webPageTitle(result) : ""; return { kind: "tool", label: pageTitle || statusCopy(status, "Reading", "Read", "Could not read"), detail: webDetail || (/^https?:\/\//i.test(rawTarget.trim()) ? "Private address" : safeActivityDetail(rawTarget)), url: parsedUrl?.href, host: parsedUrl ? displayWebHost(parsedUrl.hostname) : undefined, }; } if (isShellTraceName(name)) return describeShellTrace(args, trimmed, status); if (name === "write_file") { return describeFileMutationTrace(args, status, "Writing file", "Wrote file", "Could not write file"); } if (name === "edit_file" || name === "apply_patch") { return describeFileMutationTrace(args, status, "Editing file", "Edited file", "Could not edit file"); } if (name) { const action = humanizeTraceToolName(name); return { kind: "tool", label: statusCopy( status, `Running ${action}`, `Completed ${action}`, `Could not complete ${action}`, ), detail: "", }; } if (/done|complete|success/i.test(trimmed)) { return { kind: "done", label: "Completed step", detail: safeActivityDetail(trimmed) }; } return { kind: status === "done" ? "done" : "trace", label: statusCopy(status, "Working", "Completed step", "Step failed"), detail: safeActivityDetail(trimmed), }; } function webPageTitle(result: unknown): string { if (result && typeof result === "object" && !Array.isArray(result)) { const title = (result as Record).title; if (typeof title === "string") return safeActivityDetail(title); } if (typeof result !== "string") return ""; const heading = result.match(/^#\s+(.+)$/m)?.[1]?.trim(); return heading ? safeActivityDetail(heading) : ""; } function describeShellTrace( args: string, fallback: string, status: GenericToolStatus, ): TraceDescription { const command = shellCommandFromArgs(args) || fallback; if (/^(?:\/(?:usr\/)?bin\/)?date(?:\s|$)/i.test(command.trim())) { return { kind: "tool", label: statusCopy( status, "Checking current time", "Checked current time", "Could not check current time", ), detail: "", icon: "clock", }; } return { kind: "tool", label: statusCopy(status, "Running command", "Ran command", "Command failed"), detail: summarizeShellCommand(command), }; } function describeFileMutationTrace( args: string, status: GenericToolStatus, running: string, done: string, failed: string, ): TraceDescription { const path = traceFieldFromArgs(args, ["path", "file_path"]); return { kind: "tool", label: statusCopy(status, running, done, failed), detail: path ? safeActivityDetail(path) : "", }; } function statusCopy( status: GenericToolStatus, running: string, done: string, failed: string, ): string { return status === "running" ? running : status === "error" ? failed : done; } function traceFieldFromArgs(args: string, keys: string[]): string { const compactArgs = args.trim(); if (!compactArgs) return ""; try { const parsed = JSON.parse(compactArgs) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return ""; const record = parsed as Record; for (const key of keys) { const value = record[key]; if (typeof value === "string" && value.trim()) return value.trim(); } } catch { return ""; } return ""; } function isShellTraceName(name: string): boolean { return [ "exec", "exec_command", "execute_command", "run_command", "run_shell", "shell", "terminal", "bash", "sh", ].includes(name.toLowerCase().split(".").pop() || name.toLowerCase()); } function shellCommandFromArgs(args: string): string { const compactArgs = args.trim(); if (!compactArgs) return ""; try { const parsed = JSON.parse(compactArgs) as unknown; if (typeof parsed === "string") return parsed; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return ""; const record = parsed as Record; for (const key of ["command", "cmd", "script", "input"]) { const value = record[key]; if (typeof value === "string" && value.trim()) return value; } } catch { return compactArgs.replace(/^["']|["']$/g, ""); } return ""; } function humanizeTraceToolName(name: string): string { const words = name .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/[._-]+/g, " ") .replace(/\s+/g, " ") .trim() .toLowerCase(); return words ? `${words[0].toUpperCase()}${words.slice(1)}` : "tool action"; } function traceUrlFromArgs(args: string, fallback: string): URL | null { const candidates: string[] = []; const compactArgs = args.trim(); if (compactArgs) { try { collectUrlCandidates(JSON.parse(compactArgs), candidates); } catch { candidates.push(compactArgs.replace(/^["']|["']$/g, "")); } } candidates.push(fallback); for (const candidate of candidates) { const url = parseSafeActivityHttpUrl(candidate); if (url) return url; const embedded = candidate.match(/https?:\/\/[^\s"'<>),]+/i)?.[0]; if (embedded) { const embeddedUrl = parseSafeActivityHttpUrl(embedded); if (embeddedUrl) return embeddedUrl; } } return null; } function collectUrlCandidates(value: unknown, candidates: string[]) { if (typeof value === "string") { candidates.push(value); return; } if (!value || typeof value !== "object") return; if (Array.isArray(value)) { for (const item of value.slice(0, 6)) collectUrlCandidates(item, candidates); return; } const record = value as Record; for (const key of ["url", "uri", "href", "link"]) { if (typeof record[key] === "string") candidates.push(record[key]); } }