import { useEffect, useState } from 'react'; import * as api from '../api'; import type { ProviderUsage, 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() { // Each provider loads independently (undefined = loading, null = failed), so // the page frame paints immediately and cards fill in as answers arrive — // one hung provider no longer blanks the whole page. const [prov, setProv] = useState>({}); const [t, setT] = useState(null); useEffect(() => { let alive = true; for (const p of PROVS) { api.getUsage(p.id) .then((r) => { if (alive) setProv((m) => ({ ...m, [p.id]: r.providers[p.id] || {} })); }) .catch(() => { if (alive) setProv((m) => ({ ...m, [p.id]: null })); }); } api.getTraces().then((r) => { if (alive) setT(r); }).catch(() => {}); return () => { alive = false; }; }, []); return (
{PROVS.map((p) => { const d = prov[p.id]; const q = d?.quota; return (
{p.label}
{d === undefined ? ( <>
Today
This week
) : d === null ? (
unavailable — is ccusage installed?
) : ( <>
Today{d.tokensToday == null ? '—' : `${fmtTok(d.tokensToday)} tok`}
This week{d.tokensWeek == null ? '—' : `${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 agent transcript stored on this Space (Claude, Codex, opencode, Hermes, OpenClaw) — hover the tools count for the breakdown, tokens-in for the cache share. The total also counts traces of deleted agents.
{!t ? ( [0, 1, 2].map((i) => ( )) ) : ( <> {t.sessions.map((s) => )} )}
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.
); }