import { useEffect, useState } from 'react'; import * as api from '../api'; import type { Usage, QuotaWindow, Traces, TraceStats } from '../api'; import Logo from './Logo'; const PROVS = [ { id: 'claude', label: 'Claude Code', color: '#d97757' }, { id: 'codex', label: 'Codex', color: '#5eb6a6' }, { id: 'gemini', label: 'Gemini CLI', color: '#4796e3' }, ]; const fmtTok = (n = 0) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n); const resetStr = (s?: number) => { if (!s) return ''; const mins = Math.round((s * 1000 - Date.now()) / 60000); if (mins <= 0) return 'resetting'; if (mins < 60) return `resets in ${mins}m`; return `resets in ${Math.floor(mins / 60)}h ${mins % 60}m`; }; const fmtAgo = (ts: number) => { if (!ts) return '—'; const m = Math.round((Date.now() - ts) / 60000); if (m < 1) return 'now'; if (m < 60) return `${m}m ago`; if (m < 48 * 60) return `${Math.round(m / 60)}h ago`; return `${Math.round(m / 1440)}d ago`; }; const topTools = (tools: Record) => Object.entries(tools).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([k, v]) => `${k} ${v}`).join(' · '); function Bar({ label, q }: { label: string; q?: QuotaWindow }) { if (!q || q.usedPercent == null) return null; const pct = Math.max(0, Math.min(100, Math.round(q.usedPercent))); return (
{label}
{pct}% {resetStr(q.resetsAt)}
); } function TraceRow({ label, cli, path, st, strong }: { label: string; cli?: string; path?: string | null; st: TraceStats; strong?: boolean }) { const cachePct = st.tokensIn + st.cacheRead > 0 ? Math.round((st.cacheRead / (st.tokensIn + st.cacheRead)) * 100) : 0; return ( {cli && } {label} {st.turns} {st.prompts} {st.toolCalls} {st.web} {fmtTok(st.tokensIn)} {fmtTok(st.tokensOut)} {fmtAgo(st.lastTs)} ); } export default function UsagePanel() { const [u, setU] = useState(null); const [t, setT] = useState(null); const [err, setErr] = useState(false); useEffect(() => { api.getUsage().then(setU).catch(() => setErr(true)); api.getTraces().then(setT).catch(() => {}); }, []); if (err) return
usage unavailable — is ccusage installed?
; if (!u) return
reading usage…
; return (
{PROVS.map((p) => { const d = u.providers[p.id] || {}; const q = d.quota; return (
{p.label}
Today{fmtTok(d.tokensToday)} tok
This week{fmtTok(d.tokensWeek)} tok
{q ? (
{!q.fiveHour && !q.weekly &&
No quota yet — run a session to populate.
}
) : p.id === 'gemini' ? (
No quota (consumer tier deprecated — uses an API key).
) : (
No quota yet — run a session to populate.
)}
); })}

Traces

Parsed from every Claude Code transcript and Codex rollout stored on this Space — hover the tools count for the breakdown, tokens-in for the cache share.
{!t ? (
analyzing traces…
) : t.totals.files === 0 ? (
no traces yet — run a Claude or Codex session.
) : ( {t.sessions.map((s) => )} {t.other && }
agentturnspromptstoolswebtok intok outlast active
)}
Token counts and quota are read from each agent's local logs on the Space. They reflect the state as of that agent's last model call here — running a session updates them; activity outside the Space won't.
); }