110 lines
4 KiB
TypeScript
110 lines
4 KiB
TypeScript
|
|
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&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>
|
||
|
|
);
|
||
|
|
}
|