import * as React from "react"; import { api } from "../api/client"; import type { TokenUsageRecord } from "../api/client"; type SortKey = | "timestamp" | "story_id" | "agent_name" | "model" | "total_cost_usd"; type SortDir = "asc" | "desc"; function formatCost(usd: number): string { if (usd === 0) return "$0.00"; if (usd < 0.001) return `$${usd.toFixed(6)}`; if (usd < 0.01) return `$${usd.toFixed(4)}`; return `$${usd.toFixed(3)}`; } function formatTokens(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return String(n); } function formatTimestamp(iso: string): string { const d = new Date(iso); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); const h = String(d.getHours()).padStart(2, "0"); const m = String(d.getMinutes()).padStart(2, "0"); return `${year}-${month}-${day} ${h}:${m}`; } /** Infer an agent type from the agent name. */ function agentType(agentName: string): string { const lower = agentName.toLowerCase(); if (lower.startsWith("coder")) return "coder"; if (lower.startsWith("qa")) return "qa"; if (lower.startsWith("mergemaster") || lower.startsWith("merge")) return "mergemaster"; return "other"; } interface SortHeaderProps { label: string; sortKey: SortKey; current: SortKey; dir: SortDir; onSort: (key: SortKey) => void; align?: "left" | "right"; } function SortHeader({ label, sortKey, current, dir, onSort, align = "left", }: SortHeaderProps) { const active = current === sortKey; return ( onSort(sortKey)} > {label} {active ? (dir === "asc" ? " ↑" : " ↓") : ""} ); } interface TokenUsagePageProps { projectPath: string; } export function TokenUsagePage({ projectPath }: TokenUsagePageProps) { const [records, setRecords] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [sortKey, setSortKey] = React.useState("timestamp"); const [sortDir, setSortDir] = React.useState("desc"); React.useEffect(() => { setLoading(true); setError(null); api .getAllTokenUsage() .then((resp) => setRecords(resp.records)) .catch((e) => setError(e instanceof Error ? e.message : "Failed to load token usage"), ) .finally(() => setLoading(false)); }, [projectPath]); function handleSort(key: SortKey) { if (key === sortKey) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir(key === "timestamp" ? "desc" : "asc"); } } const sorted = React.useMemo(() => { return [...records].sort((a, b) => { let cmp = 0; switch (sortKey) { case "timestamp": cmp = a.timestamp.localeCompare(b.timestamp); break; case "story_id": cmp = a.story_id.localeCompare(b.story_id); break; case "agent_name": cmp = a.agent_name.localeCompare(b.agent_name); break; case "model": cmp = (a.model ?? "").localeCompare(b.model ?? ""); break; case "total_cost_usd": cmp = a.total_cost_usd - b.total_cost_usd; break; } return sortDir === "asc" ? cmp : -cmp; }); }, [records, sortKey, sortDir]); // Compute summary totals const totalCost = records.reduce((s, r) => s + r.total_cost_usd, 0); const byAgentType = React.useMemo(() => { const map: Record = {}; for (const r of records) { const t = agentType(r.agent_name); map[t] = (map[t] ?? 0) + r.total_cost_usd; } return map; }, [records]); const byModel = React.useMemo(() => { const map: Record = {}; for (const r of records) { const m = r.model ?? "unknown"; map[m] = (map[m] ?? 0) + r.total_cost_usd; } return map; }, [records]); const cellStyle: React.CSSProperties = { padding: "7px 12px", borderBottom: "1px solid #222", fontSize: "0.85em", color: "#ccc", whiteSpace: "nowrap", }; return (

Token Usage

{/* Summary totals */}
{Object.entries(byAgentType) .sort(([a], [b]) => a.localeCompare(b)) .map(([type, cost]) => ( ))} {Object.entries(byModel) .sort(([, a], [, b]) => b - a) .map(([model, cost]) => ( ))}
{loading && (

Loading...

)} {error &&

{error}

} {!loading && !error && records.length === 0 && (

No token usage records found.

)} {!loading && !error && records.length > 0 && (
{sorted.map((r, i) => ( ))}
Input Cache+ Cache↩ Output
{formatTimestamp(r.timestamp)} {r.story_id} {r.agent_name} {r.model ?? "—"} {formatTokens(r.input_tokens)} {formatTokens(r.cache_creation_input_tokens)} {formatTokens(r.cache_read_input_tokens)} {formatTokens(r.output_tokens)} {formatCost(r.total_cost_usd)}
)}
); } function SummaryCard({ label, value, highlight = false, }: { label: string; value: string; highlight?: boolean }) { return (
{label}
{value}
); }