This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/web/src/components/TaskDrawer.tsx

110 lines
4 KiB
TypeScript
Raw Normal View History

import type { TaskDetail } from "../api";
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 (
<section className="drawer" aria-label="task detail" role="complementary">
<div className="drawer-head">
<h2>Task history</h2>
<button className="drawer-close" aria-label="Close detail" onClick={onClose}>
✕
</button>
</div>
{loading && <div className="drawer-loading">Loading…</div>}
{error && <div className="drawer-error">{error}</div>}
{detail && detail.ok && (
<>
<div className="drawer-summary">
<div className="drawer-task">{detail.task}</div>
<div className="drawer-meta">
<span className={`badge ${detail.status}`}>{detail.status}</span>
<span className="mono">{detail.short_id}</span>
<span className="spend">spend {fmtCost(detail.total_usd)}</span>
</div>
{detail.partial && (
<div className="partial-banner">
Partial history — recorded since the v3 ledger; earlier steps are
reconstructed.
</div>
)}
</div>
<ol className="timeline">
{detail.timeline.map((step, i) => {
const open = step.exited_at == null;
return (
<li className={`tl-step ${open ? "open" : ""}`} key={i}>
<span className="tl-dot" aria-hidden />
<div className="tl-body">
<div className="tl-node">{step.to_phase}</div>
<div className="tl-times">
<span>{step.entered_at ?? "—"}</span>
<span className="tl-dur">
{open ? "in progress" : fmtDuration(step.duration_s)}
</span>
</div>
<div className="tl-foot">
{step.status && <span className={`badge ${step.status}`}>{step.status}</span>}
<span className="tl-cost">{fmtCost(step.cost_usd)}</span>
{step.note && <span className="tl-note">{step.note}</span>}
</div>
</div>
</li>
);
})}
{detail.timeline.length === 0 && (
<li className="empty">No transitions recorded.</li>
)}
</ol>
{detail.qa_history.length > 0 && (
<details className="drawer-section" open>
<summary>Q&amp;A ({detail.qa_history.length})</summary>
<pre className="json">{JSON.stringify(detail.qa_history, null, 2)}</pre>
</details>
)}
{detail.review_verdicts.length > 0 && (
<details className="drawer-section">
<summary>Review verdicts ({detail.review_verdicts.length})</summary>
<pre className="json">{JSON.stringify(detail.review_verdicts, null, 2)}</pre>
</details>
)}
{detail.plan && (
<details className="drawer-section">
<summary>Plan</summary>
<pre className="json">{JSON.stringify(detail.plan, null, 2)}</pre>
</details>
)}
</>
)}
{detail && !detail.ok && (
<div className="drawer-error">{detail.error ?? "Could not load task."}</div>
)}
</section>
);
}