import { MessageSquare, X, ChevronDown } from "lucide-react"; import type { TaskDetail } from "../api"; import { Markdown } from "./Markdown"; import { Badge, statusVariant } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Collapsible, CollapsibleTrigger, CollapsibleContent, } from "@/components/ui/collapsible"; import { cn } from "@/lib/utils"; import { formatEastern } from "@/lib/time"; interface Props { detail: TaskDetail | null; loading: boolean; error: string | null; onClose: () => void; } function fmtDuration(s: number | null): string { if (s == null) return "—"; if (s < 60) return `${s.toFixed(1)}s`; if (s < 3600) return `${(s / 60).toFixed(1)}m`; return `${(s / 3600).toFixed(1)}h`; } function fmtCost(usd: number): string { return usd > 0 ? `$${usd.toFixed(4)}` : "—"; } // Right-hand drawer: a task's journey through the pipeline (vertical timeline of // task_transitions) plus Q&A, review verdicts, plan summary, and total spend. export function TaskDrawer({ detail, loading, error, onClose }: Props) { if (!detail && !loading && !error) return null; const isWaiting = detail?.ok && detail.status === "waiting_human"; const isClarifyGate = isWaiting && detail?.current_phase === "clarify"; return (
{/* Sticky header */}

Task history

{loading && (
Loading…
)} {error && (
{error}
)} {detail && detail.ok && (
{/* Summary card */}

{detail.task}

{detail.status} {detail.short_id} spend {fmtCost(detail.total_usd)}
{detail.partial && (

Partial history — recorded since the v3 ledger; earlier steps are reconstructed.

)}
{/* Gate panel — waiting_human informational only */} {isWaiting && (

Input requested

{isClarifyGate ? "The pipeline needs clarification before planning." : "The pipeline is awaiting a human decision to proceed."}

Answer in Slack{" "} #agent-team

)} {/* Timeline */}

Timeline

    {detail.timeline.map((step, i) => { const open = step.exited_at == null; const isLast = i === detail.timeline.length - 1; return (
  1. {/* Dot + connector */}
    {!isLast && ( )}
    {/* Step body */}
    {step.to_phase} {open ? "in progress" : fmtDuration(step.duration_s)}
    {step.entered_at && ( {formatEastern(step.entered_at)} )} {step.status && ( {step.status} )} {step.cost_usd > 0 && ( {fmtCost(step.cost_usd)} )} {step.note && ( {step.note} )}
  2. ); })} {detail.timeline.length === 0 && (
  3. No transitions recorded.
  4. )}
{/* Q&A section */} {detail.qa_history.length > 0 && ( <> )} {/* Review verdicts */} {detail.review_verdicts.length > 0 && ( <> )} {/* Plan */} {detail.plan && ( <> )}
)} {detail && !detail.ok && (
{detail.error ?? "Could not load task."}
)}
); } // Collapsible section wrapper used for Q&A / Verdicts / Plan function CollapsibleSection({ title, defaultOpen, children, }: { title: string; defaultOpen: boolean; children: React.ReactNode; }) { return (

{title}

{children}
); } function asText(v: unknown): string { return typeof v === "string" ? v : v == null ? "" : JSON.stringify(v, null, 2); } // Q&A: each entry is the answer text for a turn (a string), or an older // {turn, question_id, answer} dict. Render each as a turn card with the answer // as markdown so numbered sub-answers / bold render readably. function QASection({ entries }: { entries: unknown[] }) { return (
{entries.map((entry, i) => { const obj = entry && typeof entry === "object" ? (entry as Record) : null; const turn = obj && "turn" in obj ? Number(obj.turn) : i; const answer = obj && "answer" in obj ? asText(obj.answer) : asText(entry); return ( Turn {turn + 1} {answer} ); })}
); } // Review verdicts: each {verdict, round_index, outcome, findings} → a card with a // verdict badge + round + outcome header and the findings rendered as markdown. function VerdictsSection({ entries }: { entries: unknown[] }) { return (
{entries.map((entry, i) => { const v = entry && typeof entry === "object" ? (entry as Record) : {}; const verdict = asText(v.verdict) || "verdict"; const round = "round_index" in v ? Number(v.round_index) : i + 1; const outcome = asText(v.outcome); const findings = asText(v.findings); return (
Round {round} {verdict.replace(/_/g, " ")} {outcome && ( {outcome.replace(/_/g, " ")} )}
{findings && ( {findings} )}
); })}
); } // Plan: {summary, phases:[{name, steps:[...]}]} → summary (markdown) + a phase // list with nested steps. Any extra scalar keys render as a small label/value // list; an unrecognized shape falls back to a collapsed JSON view. function PlanSection({ plan }: { plan: Record }) { const summary = asText(plan.summary); const phases = Array.isArray(plan.phases) ? plan.phases : null; const extra = Object.entries(plan).filter( ([k, val]) => k !== "summary" && k !== "phases" && (typeof val !== "object" || val === null), ); if (!summary && !phases) { return (
        {JSON.stringify(plan, null, 2)}
      
); } return (
{summary && (
{summary}
)} {phases && (
    {phases.map((p, i) => { const ph = p && typeof p === "object" ? (p as Record) : {}; const name = asText(ph.name) || `Phase ${i + 1}`; const steps = Array.isArray(ph.steps) ? ph.steps : []; return (
  1. {name}

    {steps.length > 0 && (
      {steps.map((s, j) => (
    • {asText(s)}
    • ))}
    )}
  2. ); })}
)} {extra.length > 0 && (
{extra.map(([k, val]) => (
{k}
{asText(val)}
))}
)}
); }