import { useT } from "../i18n"; import { navigate } from "../router"; import { useStore } from "../store"; import type { Part, Todo } from "../types"; import { Spinner } from "./Activity"; import { Diff } from "./Diff"; import { IMAGE_EXT, projectFileUrl } from "./Markdown"; type ToolCall = Extract; type ToolResult = Extract; function short(s: unknown, n = 90): string { const str = typeof s === "string" ? s : JSON.stringify(s); return str.length > n ? `${str.slice(0, n)}…` : str; } /** One-line summary of a tool call. */ export function toolTitle(name: string, input: any): string { switch (name) { case "read": case "write": case "edit": return input?.path ?? ""; case "list": return input?.path ?? "."; case "bash": return input?.description || input?.command || ""; case "glob": case "grep": return `${input?.pattern ?? ""}${input?.path ? ` in ${input.path}` : ""}`; case "web_fetch": case "browser_snapshot": return input?.url ?? ""; case "task": return `${input?.subagent_type ?? "general"}: ${input?.description ?? ""}`; case "image_compare": return `${input?.original ?? ""} ↔ ${input?.candidate ?? ""}`; case "skill": return input?.name ?? ""; case "ask_user": return input?.questions?.[0]?.question ?? ""; default: return short(input); } } export function TodoList({ todos }: { todos: Todo[] }) { const icon = { completed: "☑", in_progress: "▶", pending: "☐" } as const; return ( ); } export function ToolCard({ call, result, running, baseDir }: { call: ToolCall; result?: ToolResult; running: boolean; baseDir?: string }) { const t = useT(); const progress = useStore((s) => s.progress[call.id]); const status = result ? (result.is_error ? "err" : "ok") : running ? "run" : "err"; const statusText = result ? (result.is_error ? "✗" : "✓") : running ? "…" : "–"; const meta = result?.meta; // The live task list has its own panel; updates are a single line here. if (call.name === "todo_write" && !result?.is_error) { const todos: Todo[] = call.input?.todos ?? []; const done = todos.filter((x) => x.status === "completed").length; const current = todos.find((x) => x.status === "in_progress"); return (
☑ {t("tool.todosUpdated", { done, total: todos.length })} {current && · ▶ {current.content}}
); } // Image files written by the agent are shown as images (SVG too, rendered via ). const writtenImage = (call.name === "write" || call.name === "edit") && typeof call.input?.path === "string" && IMAGE_EXT.test(call.input.path) && result && !result.is_error ? projectFileUrl(call.input.path, baseDir) : undefined; if (writtenImage) { return (
{call.name} {call.input.path} ✓
{call.input.path}
); } // Short diffs are shown right away, like in a terminal agent. const diffLines = typeof meta?.diff === "string" ? meta.diff.split("\n").length : 0; const openByDefault = (status === "err" && !!result) || (!!result && !result.is_error && ((diffLines > 0 && diffLines <= 40) || call.name === "generate_image")); return (
{call.name} {toolTitle(call.name, call.input)} {meta?.similarity !== undefined && {meta.similarity} %} {call.name === "task" && meta?.model && {meta.model}} {call.name === "task" && call.input?.background && {t("tool.background")}} {status === "run" ? : {statusText}}
{call.name === "bash" &&
$ {call.input?.command}
} {(call.name === "edit" || call.name === "write") && meta?.diff && } {call.name === "task" && meta?.session_id && ( )} {!["bash", "edit", "write", "task"].includes(call.name) && ( <>
{t("tool.input")}
{JSON.stringify(call.input, null, 2)}
)} {!result && progress && ( <>
{t("tool.output")}
{progress}
)} {result && !((call.name === "edit" || call.name === "write") && meta?.diff && !result.is_error) && ( <>
{t("tool.output")}
{result.content}
)} {Array.isArray(meta?.image_ids) ? (
{meta.image_ids.map((id: string) => ( ))}
) : ( result?.images?.map((img, i) => ) )}
); }