"use client"; import { useContext } from "react"; import { CheckIcon, XIcon } from "lucide-react"; import { JobsContext } from "@/components/jobs-context"; import type { JobKind, JobRecord } from "@/src/core"; /** * The dispatch card (contracts.ts "THE ASYNC PROTOCOL" §4): the tool-call * box for a background sub-agent. The tool part's output is the FIRE-AND- * FORGET ACK (a string) — never the result. Live status comes from the * central jobs poll via JobsContext, matched by toolCallId: * * job unknown → "dispatched" pulse (the poll hasn't seen it yet) * running → live activity card (activity line + elapsed) * done/error → quiet one-liner ("delivered" / "failed") — the artifact * itself arrives in the steer message right below, so the * box must not duplicate it. */ const KIND_LABEL: Record = { research: "Research agent", demo: "Demo agent", visual: "Figure agent", pdf: "PDF reader", // The injection is server-initiated — it has no tool part, so never a // dispatch card. The entry exists only for the exhaustive record. "graph-context": "Graph context", }; export function DispatchCard({ toolCallId, kind }: { toolCallId: string; kind: JobKind }) { const jobs = useContext(JobsContext); const job = jobs.find((j) => j.toolCallId === toolCallId) ?? null; const label = KIND_LABEL[kind]; if (!job || job.status === "running") { return ; } const failed = job.status === "error"; return (
{failed ? : } {label} {failed ? "failed" : "delivered"}
); } function DispatchActivityCard({ job, label }: { job: JobRecord | null; label: string }) { // Elapsed from createdAt — jobs appear here only via the poll (post-mount), // so no SSR/hydration mismatch. const elapsed = job && job.createdAt ? Math.max(0, Math.round((Date.now() - Date.parse(job.createdAt)) / 1000)) : null; return (
{label} {elapsed !== null && · {elapsed}s}
{job?.activity && (
{job.activity}
)} {!job && (
Dispatched…
)}
); }