g1t/apps/web/app/components/token-usage.tsx
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Mission control shows model usage, yours and the workspace's: tokens, cost, active days, cache share, each day, and the mix | 1 | import { useState } from "react"; |
| 2 | import { Link } from "react-router"; | |
| 3 | ||
| 4 | import { cn } from "../lib/cn"; | |
| 5 | import { usd } from "../lib/mission-control"; | |
| 6 | import { | |
| 7 | HEAT_LEVELS, | |
| 8 | type MixPart, | |
| 9 | type TokenUsageView, | |
| 10 | bestDay, | |
| 11 | cacheShare, | |
| 12 | compactTokens, | |
| 13 | heatLevel, | |
| 14 | shortDay, | |
| 15 | tokenMix, | |
| 16 | } from "../lib/token-usage"; | |
| 17 | ||
| 18 | /** | |
| 19 | * The token mix's colours, in its stacking order. Checked against the dark | |
| 20 | * surface (lightness band, chroma, contrast, and neighbours apart for every | |
| 21 | * kind of colour vision); change them only by re-running that check. | |
| 22 | */ | |
| 23 | const MIX_COLOR: Record<MixPart["key"], string> = { | |
| 24 | input: "#9085e9", | |
| 25 | cacheRead: "#199e70", | |
| 26 | cacheWrite: "#3987e5", | |
| 27 | output: "#d55181", | |
| 28 | }; | |
| 29 | ||
| 30 | /** The daily grid's single hue, stronger with more tokens. */ | |
| 31 | const HEAT_SHADE = ["var(--color-line)", ...Array.from({ length: HEAT_LEVELS }, (_, i) => `color-mix(in oklab, var(--color-merged) ${[28, 48, 72, 100][i]}%, var(--color-surface))`)]; | |
| 32 | ||
| 33 | type Scope = "workspace" | "mine"; | |
| 34 | ||
| 35 | /** | |
| 36 | * Model tokens over the last weeks, for the workspace or for you: what was | |
| 37 | * used and what it cost, how many days, how much came from the cache, each | |
| 38 | * day's intensity, and the mix of input, cache and output. | |
| 39 | */ | |
| 40 | export function TokenUsagePanel({ | |
| 41 | workspace, | |
| 42 | mine, | |
| 43 | usageHref, | |
| 44 | }: { | |
| 45 | workspace: TokenUsageView | null; | |
| 46 | mine: TokenUsageView | null; | |
| 47 | usageHref: string | null; | |
| 48 | }) { | |
| 49 | const [scope, setScope] = useState<Scope>("workspace"); | |
| 50 | const usage = scope === "mine" ? mine : workspace; | |
| 51 | return ( | |
| 52 | <section aria-labelledby="token-usage" className="rounded-xl border border-line bg-surface p-5"> | |
| 53 | <div className="flex items-center justify-between gap-3"> | |
| 54 | <h2 id="token-usage" className="text-base font-semibold tracking-tight"> | |
| 55 | Usage | |
| 56 | </h2> | |
| 57 | <div role="tablist" aria-label="Whose usage" className="flex rounded-md border border-line p-0.5 text-xs"> | |
| 58 | {(["workspace", "mine"] as const).map((option) => ( | |
| 59 | <button | |
| 60 | key={option} | |
| 61 | type="button" | |
| 62 | role="tab" | |
| 63 | aria-selected={scope === option} | |
| 64 | disabled={option === "mine" && !mine} | |
| 65 | onClick={() => setScope(option)} | |
| 66 | className={cn( | |
| 67 | "rounded px-2 py-0.5 transition-colors disabled:opacity-40", | |
| 68 | scope === option ? "bg-raised text-fg" : "text-muted hover:text-fg", | |
| 69 | )} | |
| 70 | > | |
| 71 | {option === "workspace" ? "Workspace" : "You"} | |
| 72 | </button> | |
| 73 | ))} | |
| 74 | </div> | |
| 75 | </div> | |
| 76 | {usage ? <Figures usage={usage} usageHref={usageHref} /> : <p className="mt-4 text-sm text-muted">Usage could not be loaded.</p>} | |
| 77 | </section> | |
| 78 | ); | |
| 79 | } | |
| 80 | ||
| 81 | function Figures({ usage, usageHref }: { usage: TokenUsageView; usageHref: string | null }) { | |
| 82 | const share = cacheShare(usage); | |
| 83 | const weeks = Math.round(usage.days / 7); | |
| 84 | if (usage.totalTokens === 0) { | |
| 85 | return ( | |
| 86 | <p className="mt-4 text-sm text-muted"> | |
| 87 | No model tokens in the last {weeks} weeks{usage.person ? " on work you asked for" : ""}. Agents' runs show here as they work. | |
| 88 | </p> | |
| 89 | ); | |
| 90 | } | |
| 91 | return ( | |
| 92 | <> | |
| 93 | <dl className="mt-4 grid grid-cols-2 gap-2"> | |
| 94 | <Tile label="Tokens" value={compactTokens(usage.totalTokens)} /> | |
| 95 | <Tile label="Cost" value={usd(usage.costMicros / 1e6)} title="What these runs were charged" /> | |
| 96 | <Tile label="Active days" value={String(usage.activeDays)} hint={`of ${usage.days}`} /> | |
| 97 | <Tile label="From cache" value={share == null ? "—" : `${Math.round(share * 100)}%`} title="Prompt tokens read from the model's cache" /> | |
| 98 | </dl> | |
| 99 | <DailyGrid byDay={usage.byDay} /> | |
| 100 | <Mix usage={usage} /> | |
| 101 | {usageHref && ( | |
| 102 | <Link to={usageHref} className="mt-4 block border-t border-line pt-3 text-xs text-muted hover:text-fg"> | |
| 103 | Every run, by repository and model | |
| 104 | </Link> | |
| 105 | )} | |
| 106 | </> | |
| 107 | ); | |
| 108 | } | |
| 109 | ||
| 110 | function Tile({ label, value, hint, title }: { label: string; value: string; hint?: string; title?: string }) { | |
| 111 | return ( | |
| 112 | <div className="rounded-lg border border-line bg-bg/40 px-3 py-2.5" title={title}> | |
| 113 | <dt className="text-[0.6875rem] text-muted">{label}</dt> | |
| 114 | <dd className="mt-0.5 text-lg font-semibold tracking-tight tabular-nums"> | |
| 115 | {value} | |
| 116 | {hint && <span className="ml-1 text-xs font-normal text-faint">{hint}</span>} | |
| 117 | </dd> | |
| 118 | </div> | |
| 119 | ); | |
| 120 | } | |
| 121 | ||
| 122 | /** | |
| 123 | * Each day as a cell, two rows, oldest first; shaded by quartile of the | |
| 124 | * busiest day. Hover or focus a day to read it. | |
| 125 | */ | |
| 126 | function DailyGrid({ byDay }: { byDay: TokenUsageView["byDay"] }) { | |
| 127 | const best = bestDay(byDay); | |
| 128 | const busiest = best?.tokens ?? 0; | |
| 129 | const [shown, setShown] = useState<{ day: string; tokens: number } | null>(null); | |
| 130 | const columns = Math.ceil(byDay.length / 2); | |
| 131 | const readout = shown ?? best; | |
| 132 | return ( | |
| 133 | <div className="mt-5"> | |
| 134 | <div className="flex items-baseline justify-between gap-2"> | |
| 135 | <h3 className="text-xs font-medium text-fg-soft">Each day</h3> | |
| 136 | <p className="text-[0.6875rem] text-muted tabular-nums" aria-live="polite"> | |
| 137 | {readout ? `${shown ? "" : "Busiest: "}${shortDay(readout.day)} · ${compactTokens(readout.tokens)}` : ""} | |
| 138 | </p> | |
| 139 | </div> | |
| 140 | <div | |
| 141 | className="mt-2 grid gap-[3px]" | |
| 142 | style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }} | |
| 143 | onMouseLeave={() => setShown(null)} | |
| 144 | > | |
| 145 | {byDay.map((entry) => ( | |
| 146 | <span | |
| 147 | key={entry.day} | |
| 148 | role="img" | |
| 149 | tabIndex={0} | |
| 150 | aria-label={`${shortDay(entry.day)}: ${compactTokens(entry.tokens)} tokens`} | |
| 151 | onMouseEnter={() => setShown(entry)} | |
| 152 | onFocus={() => setShown(entry)} | |
| 153 | onBlur={() => setShown(null)} | |
| 154 | className="aspect-square rounded-[3px] outline-offset-1 hover:ring-1 hover:ring-fg-soft focus-visible:outline-2 focus-visible:outline-accent" | |
| 155 | style={{ background: HEAT_SHADE[heatLevel(entry.tokens, busiest)] }} | |
| 156 | /> | |
| 157 | ))} | |
| 158 | </div> | |
| 159 | <div className="mt-1.5 flex items-center justify-between text-[0.6875rem] text-faint tabular-nums"> | |
| 160 | <span>{byDay[0] ? shortDay(byDay[0].day) : ""}</span> | |
| 161 | <span className="flex items-center gap-1" aria-hidden> | |
| 162 | Less | |
| 163 | {HEAT_SHADE.map((shade) => ( | |
| 164 | <span key={shade} className="size-2 rounded-[2px]" style={{ background: shade }} /> | |
| 165 | ))} | |
| 166 | More | |
| 167 | </span> | |
| 168 | <span>{byDay.length ? shortDay(byDay[byDay.length - 1].day) : ""}</span> | |
| 169 | </div> | |
| 170 | </div> | |
| 171 | ); | |
| 172 | } | |
| 173 | ||
| 174 | /** Input, cache reads, cache writes and output as one bar, each part labelled. */ | |
| 175 | function Mix({ usage }: { usage: TokenUsageView }) { | |
| 176 | const parts = tokenMix(usage).filter((part) => part.tokens > 0); | |
| 177 | return ( | |
| 178 | <div className="mt-5"> | |
| 179 | <h3 className="text-xs font-medium text-fg-soft">Token mix</h3> | |
| 180 | <div className="mt-2 flex h-2.5 gap-[2px] overflow-hidden rounded" role="img" aria-label={parts.map((p) => `${p.label} ${Math.round(p.share * 100)}%`).join(", ")}> | |
| 181 | {parts.map((part) => ( | |
| 182 | <span | |
| 183 | key={part.key} | |
| 184 | title={`${part.label}: ${compactTokens(part.tokens)} (${Math.round(part.share * 100)}%)`} | |
| 185 | className="h-full first:rounded-l last:rounded-r" | |
| 186 | style={{ width: `${part.share * 100}%`, minWidth: 3, background: MIX_COLOR[part.key] }} | |
| 187 | /> | |
| 188 | ))} | |
| 189 | </div> | |
| 190 | <ul className="mt-2.5 grid grid-cols-2 gap-x-3 gap-y-1.5 text-[0.6875rem]"> | |
| 191 | {tokenMix(usage).map((part) => ( | |
| 192 | <li key={part.key} className="flex min-w-0 items-center gap-1.5"> | |
| 193 | <span className="size-2 shrink-0 rounded-[2px]" style={{ background: MIX_COLOR[part.key] }} /> | |
| 194 | <span className="truncate text-muted">{part.label}</span> | |
| 195 | <span className="ml-auto text-fg-soft tabular-nums"> | |
| 196 | {compactTokens(part.tokens)} <span className="text-faint">{Math.round(part.share * 100)}%</span> | |
| 197 | </span> | |
| 198 | </li> | |
| 199 | ))} | |
| 200 | </ul> | |
| 201 | </div> | |
| 202 | ); | |
| 203 | } |