import type { TaskDetail } from "../api"; import { Markdown } from "./Markdown"; 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; return (

Task history

{loading &&
Loading…
} {error &&
{error}
} {detail && detail.ok && ( <>
{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.
)}
    {detail.timeline.map((step, i) => { const open = step.exited_at == null; return (
  1. {step.to_phase}
    {step.entered_at ?? "—"} {open ? "in progress" : fmtDuration(step.duration_s)}
    {step.status && {step.status}} {fmtCost(step.cost_usd)} {step.note && {step.note}}
  2. ); })} {detail.timeline.length === 0 && (
  3. No transitions recorded.
  4. )}
{detail.qa_history.length > 0 && (
Q&A ({detail.qa_history.length})
)} {detail.review_verdicts.length > 0 && (
Review verdicts ({detail.review_verdicts.length})
)} {detail.plan && (
Plan
)} )} {detail && !detail.ok && (
{detail.error ?? "Could not load task."}
)}
); } 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); const cls = verdict.replace(/[^a-z_]/gi, "_").toLowerCase(); 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)}
))}
)}
); }