import type { ToolCallMessagePartStatus } from "@assistant-ui/react"; import type { AskUserQuestionArgs, BashArgs, EditArgs, ExitPlanModeArgs, GlobArgs, GrepArgs, MultiEditArgs, NotebookEditArgs, ReadArgs, SlashCommandArgs, TaskArgs, TodoArgs, WebFetchArgs, WebSearchArgs, WebSearchResult, WebSearchResultItem, WriteArgs, } from "@vtit-agent-coding/shared"; import { ToolName } from "@vtit-agent-coding/shared"; import { Brain, ChevronRight, ClipboardList, FileSearch, FileText, Globe, HelpCircle, Notebook, Pencil, Plug, Search, SlashSquare, Terminal, Wrench, } from "lucide-react"; import type { ComponentType, FC } from "react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import type { SubtaskChild, TaskToolResult } from "../../RelayRuntimeProvider"; import { CodeBlock, FileDiff, langFromPath, Markdown, Mono, parseMcpToolName, resultText, ToolShell } from "./primitives"; // ─── View contract ─────────────────────────────────────────────────────────── // Each View is a stateless FC that takes the same shape the makeAssistantToolUI // render prop receives: { args, result, status }. This lets SubtaskChildren // render subagent tool calls with the same UI the main agent uses. export interface ToolViewProps { args?: A; result?: R; status?: ToolCallMessagePartStatus; } // ─── Bash ──────────────────────────────────────────────────────────────────── export const BashToolView: FC> = ({ args, result, status }) => { const cmd = args?.command ?? ""; const out = resultText(result); return ( } label="bash" status={status} summary={$ {cmd}}> {args?.description &&
{args.description}
} {`$ ${cmd}${out ? `\n${out}` : ""}`}
); }; // ─── Read ──────────────────────────────────────────────────────────────────── export const ReadToolView: FC> = ({ args, result, status }) => { const range = args?.offset ? `:${args.offset}${args.limit ? `-${args.offset + args.limit}` : ""}` : ""; const out = resultText(result); return ( } label="read" status={status} summary={ <> {args?.filePath} {range} } > {out && {out}} ); }; // ─── Edit ──────────────────────────────────────────────────────────────────── export const EditToolView: FC> = ({ args, result, status }) => { const oldCount = (args?.oldString ?? "").split("\n").length; const newCount = (args?.newString ?? "").split("\n").length; const out = resultText(result); return ( } label="edit" status={status} summary={ <> {args?.filePath}{" "} −{oldCount} +{newCount} } > {out &&
{out}
}
); }; // ─── MultiEdit ─────────────────────────────────────────────────────────────── export const MultiEditToolView: FC> = ({ args, result, status }) => { const edits = args?.edits ?? []; const out = resultText(result); return ( } label="multi-edit" status={status} summary={ <> {args?.filePath} {edits.length} edits } >
{edits.map((e, i) => (
edit {i + 1}
))}
{out &&
{out}
}
); }; // ─── Write ─────────────────────────────────────────────────────────────────── export const WriteToolView: FC> = ({ args, status }) => { const content = args?.content ?? ""; const lineCount = content.split("\n").length; return ( } label="write" status={status} summary={ <> {args?.filePath} +{lineCount} } > {content} ); }; // ─── Grep ──────────────────────────────────────────────────────────────────── export const GrepToolView: FC> = ({ args, result, status }) => { const out = resultText(result); return ( } label="grep" status={status} summary={ <> /{args?.pattern}/ {args?.path && in {args.path}} {args?.glob && ({args.glob})} } > {out && {out}} ); }; // ─── Glob ──────────────────────────────────────────────────────────────────── export const GlobToolView: FC> = ({ args, result, status }) => { const out = resultText(result); return ( } label="glob" status={status} summary={ <> {args?.pattern} {args?.path && in {args.path}} } > {out && {out}} ); }; // ─── TodoWrite ─────────────────────────────────────────────────────────────── export const TodoWriteToolView: FC> = ({ args, status }) => { const todos = args?.todos ?? []; const done = todos.filter((t) => t.status === "completed").length; return ( } label="todos" status={status} summary={ {done}/{todos.length} done } >
    {todos.map((t, i) => (
  • {t.status === "completed" ? "✓" : t.status === "in_progress" ? "→" : "○"} {t.content}
  • ))}
); }; // ─── WebFetch ──────────────────────────────────────────────────────────────── export const WebFetchToolView: FC> = ({ args, result, status }) => { const out = resultText(result); let host = ""; try { host = new URL(args?.url ?? "").host; } catch { host = args?.url ?? ""; } return ( } label="web-fetch" status={status} summary={{host}}>
{args?.url}
{args?.prompt && ( <>
prompt
{args.prompt}
)} {out && ( <>
response
)}
); }; // ─── WebSearch ─────────────────────────────────────────────────────────────── export const WebSearchToolView: FC> = ({ args, result, status }) => { const results: WebSearchResultItem[] | null = Array.isArray(result) ? (result as WebSearchResultItem[]) : null; return ( } label="web-search" status={status} summary={{args?.query}}> {results ? (
    {results.map((r, i) => (
  • {r.title &&
    {r.title}
    } {r.url &&
    {r.url}
    } {r.snippet &&
    {r.snippet}
    }
  • ))}
) : ( result != null && {resultText(result) ?? ""} )}
); }; // ─── AskUserQuestion ───────────────────────────────────────────────────────── export const AskUserQuestionToolView: FC> = ({ args, status }) => { const questions = args?.questions ?? []; return ( } label="ask" status={status} summary={{questions[0]?.question ?? (questions.length > 0 ? "question" : "")}} >
{questions.map((q, qi) => (
{q.header &&
{q.header}
}
{q.question}
{q.options && q.options.length > 0 && (
{q.options.map((opt, oi) => ( {opt.label ?? opt.description} ))}
)}
))}
); }; // ─── ExitPlanMode ──────────────────────────────────────────────────────────── export const ExitPlanModeToolView: FC> = ({ args, status }) => ( } label="plan" status={status} summary={proposed plan} > ); // ─── SlashCommand ──────────────────────────────────────────────────────────── export const SlashCommandToolView: FC> = ({ args, result, status }) => { const out = resultText(result); return ( } label="slash" status={status} summary={/{args?.command?.replace(/^\//, "")}} > {out && {out}} ); }; // ─── NotebookEdit ──────────────────────────────────────────────────────────── export const NotebookEditToolView: FC> = ({ args, status }) => { const mode = args?.editMode ?? "replace"; return ( } label={`nb:${mode}`} status={status} summary={ <> {args?.notebookPath} {args?.cellId && #{args.cellId}} } > {args?.newSource ?? ""} ); }; // ─── Task (sub-agent) ──────────────────────────────────────────────────────── type TaskToolResultShape = Partial; // Normalize result: may be legacy string or rich TaskToolResult. function coerceTaskResult(result: unknown): TaskToolResultShape { if (result == null) return {}; if (typeof result === "string") return { text: result }; if (typeof result === "object") return result as TaskToolResultShape; return {}; } export const TaskToolView: FC> = ({ args, result, status }) => { const r = coerceTaskResult(result); const metaParts: string[] = []; if (r.meta?.tokens != null) metaParts.push(`${r.meta.tokens} tok`); if (r.meta?.duration_ms != null) metaParts.push(`${Math.round(r.meta.duration_ms / 1000)}s`); if (r.meta?.last_tool) metaParts.push(r.meta.last_tool); const agentLabel = args?.subagentType || "agent"; return ( } label={agentLabel} status={status} summary={args?.description}>
prompt:
{args?.prompt ?? ""} {r.children && r.children.length > 0 && } {r.text && ( <>
report:
)} {metaParts.length > 0 &&
{metaParts.join(" · ")}
}
); }; // ─── Per-tool view registry ────────────────────────────────────────────────── // Used by SubtaskChildren to render subagent tool calls with the same UI as // the main agent. Keyed on the canonical tool name (matches block.name from // the agent event stream). Fallback to SubtaskFallback for unknown tools. export const TOOL_VIEWS: Record>> = { [ToolName.Bash]: BashToolView, [ToolName.Read]: ReadToolView, [ToolName.Edit]: EditToolView, [ToolName.MultiEdit]: MultiEditToolView, [ToolName.Write]: WriteToolView, [ToolName.Grep]: GrepToolView, [ToolName.Glob]: GlobToolView, [ToolName.Agent]: TaskToolView, [ToolName.TodoWrite]: TodoWriteToolView, [ToolName.WebFetch]: WebFetchToolView, [ToolName.WebSearch]: WebSearchToolView, [ToolName.AskUserQuestion]: AskUserQuestionToolView, [ToolName.ExitPlanMode]: ExitPlanModeToolView, [ToolName.SlashCommand]: SlashCommandToolView, [ToolName.NotebookEdit]: NotebookEditToolView, }; // ─── Fallback for unknown / MCP tools ──────────────────────────────────────── const SubtaskFallback: FC<{ toolName: string; input?: Record; result?: unknown; status?: ToolCallMessagePartStatus }> = ({ toolName, input, result, status, }) => { const mcp = parseMcpToolName(toolName); const icon = mcp ? : ; const label = mcp ? `mcp:${mcp.ns}` : "tool"; const summary = mcp ? mcp.name : toolName; const argsText = input && Object.keys(input).length > 0 ? JSON.stringify(input, null, 2) : ""; const out = resultText(result); return ( {argsText && ( <>
args
{argsText} )} {out != null && ( <>
result
{out} )}
); }; // ─── SubtaskChildren ───────────────────────────────────────────────────────── // Renders subagent event stream (children of a Task tool_call) with the same // per-tool UIs used by the main agent. Pairs tool_use with its tool_result by // id and dispatches to TOOL_VIEWS. Text/thinking blocks render in-line. // // Note: nested subagents (Agent spawned by Agent) are flattened into the outer // Task's children list by RelayRuntimeProvider, so inner steps appear as // siblings to the nested Agent tool_use. The nested Agent renders via the same // TaskToolView with no inner children — its steps are shown alongside. function statusFor(result?: Extract): ToolCallMessagePartStatus { if (!result) return { type: "running" }; if (result.error) return { type: "incomplete", reason: "error", error: result.output ?? "tool error" }; return { type: "complete" }; } export const SubtaskChildren: FC<{ items: SubtaskChild[] }> = ({ items: children }) => { if (!children.length) return null; // Pair tool_use with its tool_result by id. Tool results are skipped during // the render walk — they're consumed as siblings of their tool_use. const resultById = new Map>(); let stepCount = 0; for (const c of children) { if (c.kind === "tool_result") resultById.set(c.tool_use_id, c); else stepCount += 1; } return ( subagent steps ({stepCount})
{children.map((c, i) => { if (c.kind === "text") return ; if (c.kind === "thinking") return (
{c.text}
); if (c.kind === "tool_use") { const paired = resultById.get(c.id); const status = statusFor(paired); const View = TOOL_VIEWS[c.name]; if (View) return ; return ; } // tool_result — rendered with its paired tool_use return null; })}
); };