"use client"; import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ChevronDown, FileText, ShieldCheck, Square, Wrench, Bot, CheckCheck } from "lucide-react"; import { api } from "@/lib/client/api"; import { Dialog, DialogContent, Badge, Spinner } from "@/components/ui/primitives"; import { Button } from "@/components/ui/button"; import { Markdown } from "@/components/markdown"; import { toast } from "@/components/ui/toast"; import { cn, fmtDate } from "@/lib/utils"; import { ms, usd } from "@/components/ai/models/format"; import { ProposalCard, type ProposalView } from "./ProposalCard"; export const STATUS_TONE: Record = { pending: "neutral", running: "ai", awaiting_approval: "warning", approved: "success", completed: "success", rejected: "neutral", failed: "danger" }; export const isLive = (s: string | undefined) => s === "pending" || s === "running"; interface Step { tool: string; input: unknown; output?: unknown; at: string; ms?: number; error?: string; proposalIdx?: number } interface Detail { id: string; agent: string; agentName: string; agentIcon: string; task: string; model: string | null; status: string; scopes: string[]; toolsInvoked: Step[]; documents: { id: string; title: string; icon: string | null; section: string | null; deletedAt: string | null }[]; proposedChanges: ProposalView[]; approvals: { change: number; approved: boolean; at: string }[]; outcome: string | null; result: string | null; error: string | null; costUsd: number | null; tokensIn: number | null; tokensOut: number | null; createdAt: string; startedAt: string | null; finishedAt: string | null; contextPageId: string | null } function StepRow({ s, i }: { s: Step; i: number }) { const [open, setOpen] = useState(false); const isModel = s.tool === "model"; const out: any = s.output ?? {}; const summary = isModel ? `turn ${(s.input as any)?.turn ?? i} · ${out.toolCalls?.length ? `called ${out.toolCalls.join(", ")}` : "final answer"} · ${out.tokens ? `${out.tokens.inputTokens}→${out.tokens.outputTokens} tok` : ""} · ${usd(out.costUsd)}` : s.error ? `error: ${s.error}` : out.count != null ? `${out.count} result${out.count === 1 ? "" : "s"}` : out.title ? out.title : out.recorded ? `proposal #${(s.proposalIdx ?? 0) + 1} recorded` : out.created ? `created ${out.title ?? out.pageId}` : "ok"; return (
  • {open &&
    Input
    {JSON.stringify(s.input, null, 1)}
    Output
    {isModel && out.text ? out.text + (out.toolCalls?.length ? `\n\n→ tool calls: ${out.toolCalls.join(", ")}` : "") : JSON.stringify(s.output, null, 1)}
    }
  • ); } export function ExecutionDrawer({ id, onClose }: { id: string | null; onClose: () => void }) { const qc = useQueryClient(); const [busy, setBusy] = useState(false); const { data: d } = useQuery({ queryKey: ["agent-exec", id], queryFn: () => api(`/api/agents/${id}`), enabled: Boolean(id), refetchInterval: (q) => (isLive(q.state.data?.status) ? 2000 : false) }); const refresh = () => { qc.invalidateQueries({ queryKey: ["agent-exec", id] }); qc.invalidateQueries({ queryKey: ["agents"] }); }; async function decide(action: "approve" | "reject", changes: number[]) { if (!id) return; setBusy(true); try { const r = await api(`/api/agents/${id}`, { method: "POST", json: { action, changes } }); toast(action === "approve" ? `${r.applied.length} change${r.applied.length === 1 ? "" : "s"} applied${r.failed.length ? `, ${r.failed.length} failed` : ""}` : `${changes.length} rejected`, { tone: r.failed?.length ? "danger" : "success" }); if (action === "approve" && r.applied.length) qc.invalidateQueries({ queryKey: ["tree"] }); } catch (e: any) { toast("Decision failed", { description: e.message, tone: "danger" }); } finally { setBusy(false); refresh(); } } async function cancel() { if (!id) return; setBusy(true); try { await api(`/api/agents/${id}`, { method: "POST", json: { action: "cancel" } }); toast("Execution cancelled"); } catch (e: any) { toast("Cancel failed", { description: e.message, tone: "danger" }); } finally { setBusy(false); refresh(); } } const pending = d?.proposedChanges?.filter((p) => p.status === "proposed") ?? []; const dur = d?.startedAt ? ((d.finishedAt ? new Date(d.finishedAt) : new Date()).getTime() - new Date(d.startedAt).getTime()) : null; return ( { if (!o) onClose(); }}> {!d ?
    : (
    {d.agentIcon}
    {d.agentName}
    {d.task}
    {isLive(d.status) && }{d.status.replace("_", " ")} {isLive(d.status) && }
    {[["Model", d.model ?? "auto (agent route)"], ["Cost", usd(d.costUsd)], ["Tokens", `${d.tokensIn ?? 0} → ${d.tokensOut ?? 0}`], ["Duration", dur != null ? ms(dur) : "—"], ["Tool calls", String(d.toolsInvoked.filter((s) => s.tool !== "model").length)], ["Started", d.startedAt ? fmtDate(d.startedAt, { timeStyle: "short", dateStyle: "short" }) : "queued"]].map(([l, v]) =>
    {l}
    {v}
    )}
    {d.error &&
    {d.error}
    } {d.outcome &&
    Outcome: {d.outcome}
    } {d.proposedChanges.length > 0 &&

    Proposals

    {d.proposedChanges.length} · {pending.length} pending{pending.length > 1 && }{pending.length > 1 && }
    {d.proposedChanges.map((p) => decide("approve", [p.idx])} onReject={() => decide("reject", [p.idx])} />)}
    } {d.result &&

    Answer

    }

    Timeline

    {d.toolsInvoked.length ?
      {d.toolsInvoked.map((s, i) => )}
    :
    {isLive(d.status) ? "Waiting for the worker…" : "No steps recorded."}
    }

    Documents accessed

    {d.documents.length ? :
    None.
    }
    Audit record
    Execution
    {d.id}
    Scopes granted
    {d.scopes.length ? d.scopes.map((s) => {s}) : "none"}
    Created
    {fmtDate(d.createdAt, { dateStyle: "medium", timeStyle: "short" })}
    Finished
    {d.finishedAt ? fmtDate(d.finishedAt, { dateStyle: "medium", timeStyle: "short" }) : "—"}
    Decisions
    {d.approvals.length ?
      {d.approvals.map((a, i) =>
    • #{a.change + 1} {a.approved ? "approved" : "rejected"} · {fmtDate(a.at, { dateStyle: "short", timeStyle: "short" })}
    • )}
    : "none yet"}
    Guarantees
    No shell, no network; workspace content passed as data; only approved proposals are written (plus at most one draft page under “AI drafts” when write:pages was granted); bulk edits and deletions are never automatic.
    )}
    ); }