Skip to content
724 linesCodeBlameRaw

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.

Usage, Billing settings and prepaid AI credit; fixes from the UX audit1/**
2 * The Usage page's pieces, shared by the workspace's page and a project's:
3 * the filters, the included-usage and credit bar with what paid, the
4 * consumption chart (stacked columns by product, money on one axis, a
5 * tooltip per column, a legend, a table view), and the breakdown by
6 * product family with sparklines, allowance rings and each meter's
7 * projects. Every amount is usage at price, as on every other page.
8 */
9import { ChevronDown, Download, Ellipsis, FileCode } from "lucide-react";
10import { type ReactNode, useId, useRef } from "react";
11import { Form, Link } from "react-router";
12
13import type { MeterLine, ProductUsage, UsageReport } from "@g1t/contracts";
14
15import {
16 type Column,
17 type Grain,
18 type GroupBy,
19 PERIODS,
20 type Period,
21 axisMoney,
22 byProject,
23 money,
24 productStyle,
25 quantity,
26 rangeLabel,
27 receipt,
28 shownLines,
29 ticks,
30 usageCsv,
31} from "../lib/usage";
32import { cn } from "../lib/cn";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.33import { Button } from "./ui/button";
34import { Card } from "./ui/card";
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.35import { Checkbox } from "./ui/checkbox";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit36import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
Usage: a meter's note is the site's tooltip, caught by the no-native-title test37import { Hint } from "./ui/hint";
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.38import { Input } from "./ui/input";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project39import { SelectField } from "./ui/select";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit40import { Skeleton } from "./ui/skeleton";
41
42const SELECT =
43 "h-8 rounded-md border border-line bg-bg px-2 text-sm text-fg outline-none transition-colors hover:border-line-strong focus:border-accent-dim";
44
45// --- Filters ------------------------------------------------------------------------
46
47export type UsageFilters = {
48 period: Period;
49 from: string;
50 until: string;
51 products: string[];
52 projects: string[];
53 group: GroupBy;
54 grain: Grain;
55 cumulative: boolean;
56};
57
58/** A multi-select as a disclosure of checkboxes, part of the filter form. */
59function MultiSelect({ name, label, options, chosen }: { name: string; label: string; options: { value: string; label: string }[]; chosen: string[] }) {
60 const summary = chosen.length === 0 ? `All ${label.toLowerCase()}` : chosen.length === 1 ? (options.find((o) => o.value === chosen[0])?.label ?? chosen[0]) : `${chosen.length} ${label.toLowerCase()}`;
61 return (
62 <details className="group relative">
63 <summary className={cn(SELECT, "flex cursor-pointer list-none items-center gap-1.5 [&::-webkit-details-marker]:hidden")}>
64 <span className="text-muted">{label}:</span>
65 <span className="max-w-[10rem] truncate">{summary}</span>
66 <ChevronDown size={13} className="text-muted transition-transform group-open:rotate-180" />
67 </summary>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.68 <Card tone="plain" radius="lg" className="absolute left-0 z-20 mt-1 w-64 bg-raised p-2 shadow-xl">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit69 {options.length === 0 ? (
70 <p className="px-2 py-1.5 text-sm text-faint">None in this range.</p>
71 ) : (
72 <ul className="max-h-64 overflow-y-auto">
73 {options.map((option) => (
74 <li key={option.value}>
75 <label className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-surface">
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.76 <Checkbox name={name} value={option.value} defaultChecked={chosen.includes(option.value)} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit77 <span className="truncate">{option.label}</span>
78 </label>
79 </li>
80 ))}
81 </ul>
82 )}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.83 <Button type="submit" size="sm" className="mt-2 w-full px-2 text-sm">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit84 Apply
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.85 </Button>
86 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit87 </details>
88 );
89}
90
91/**
92 * The filters, in one row above everything: period (with its days),
93 * products, projects, group by, and a menu with the CSV and the API.
94 * A plain GET form, so a filtered page is a link.
95 */
96export function UsageFilterBar({
97 filters,
98 products,
99 projects,
100 report,
101 apiHref,
102 fixedProject,
103}: {
104 filters: UsageFilters;
105 products: { key: string; label: string }[];
106 projects: string[];
107 report: UsageReport | null;
108 apiHref: string;
109 /** A project's own page: no project filter. */
110 fixedProject?: string;
111}) {
112 const form = useRef<HTMLFormElement>(null);
113 const submit = () => form.current?.requestSubmit();
114 const download = () => {
115 if (!report) return;
116 const blob = new Blob([usageCsv(report)], { type: "text/csv" });
117 const link = document.createElement("a");
118 link.href = URL.createObjectURL(blob);
119 link.download = `usage-${report.from}-${report.until}.csv`;
120 link.click();
121 URL.revokeObjectURL(link.href);
122 };
123 return (
124 <Form ref={form} method="get" preventScrollReset className="flex flex-wrap items-center gap-2">
Chat controls, public profiles, shadcn selects, and no Docs tab in a project125 <SelectField
126 key={`period-${filters.period}`}
127 name="period"
128 defaultValue={filters.period}
129 afterChange={submit}
130 size="sm"
131 className="w-auto"
132 aria-label="Period"
133 options={(Object.keys(PERIODS) as Period[]).map((key) => ({ value: key, label: PERIODS[key] }))}
134 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit135 {filters.period === "custom" ? (
136 <span className="flex items-center gap-1 text-sm">
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.137 <Input type="date" name="from" defaultValue={filters.from} aria-label="From" className="h-8 w-auto px-2" />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit138 <span className="text-faint">to</span>
The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work.139 <Input type="date" name="until" defaultValue={filters.until} aria-label="Until" className="h-8 w-auto px-2" />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit140 </span>
141 ) : (
142 <span className="px-1 text-sm text-muted tabular-nums">{rangeLabel(filters.from, filters.until)}</span>
143 )}
144 <MultiSelect name="product" label="Products" options={products.map((p) => ({ value: p.key, label: p.label }))} chosen={filters.products} />
145 {!fixedProject && <MultiSelect name="project" label="Projects" options={projects.map((p) => ({ value: p, label: p }))} chosen={filters.projects} />}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project146 <SelectField
147 key={`group-${filters.group}`}
148 name="group"
149 defaultValue={filters.group}
150 afterChange={submit}
151 size="sm"
152 className="w-auto"
153 aria-label="Group by"
154 options={[
155 { value: "product", label: "Group by product" },
156 ...(fixedProject ? [] : [{ value: "project", label: "Group by project" }]),
157 { value: "day", label: "Group by day" },
158 ]}
159 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit160 <input type="hidden" name="grain" value={filters.grain} />
161 {filters.cumulative && <input type="hidden" name="cumulative" value="1" />}
162 <noscript>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.163 <Button type="submit" variant="outline" size="sm" className="bg-bg px-2 text-sm font-normal text-fg">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit164 Apply
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.165 </Button>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit166 </noscript>
167 <span className="ml-auto" />
168 <DropdownMenu>
169 <DropdownMenuTrigger className={cn(SELECT, "inline-flex items-center")} aria-label="More">
170 <Ellipsis size={15} />
171 </DropdownMenuTrigger>
172 <DropdownMenuContent align="end">
173 <DropdownMenuItem onSelect={download} disabled={!report}>
174 <Download size={14} /> Export CSV
175 </DropdownMenuItem>
176 <DropdownMenuItem asChild>
177 <a href={apiHref}>
178 <FileCode size={14} /> Usage API
179 </a>
180 </DropdownMenuItem>
181 </DropdownMenuContent>
182 </DropdownMenu>
183 </Form>
184 );
185}
186
187// --- What is included and what paid ------------------------------------------------------
188
189/** A bar of how much of something is used. */
190function Bar({ used, of, tone = "accent" }: { used: number; of: number; tone?: "accent" | "warn" }) {
191 const part = of > 0 ? Math.min(1, used / of) : 0;
192 return (
193 <div className="mt-2 h-1.5 overflow-hidden rounded-full bg-line" role="presentation">
Merge branch 'worktree-agent-ad7c6d88d93adc817'194 <div className={cn("h-full rounded-full", tone === "warn" ? "bg-warn" : "bg-success")} style={{ width: `${part > 0 ? Math.max(2, part * 100) : 0}%` }} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit195 </div>
196 );
197}
198
199/**
200 * The plan's included usage, the credit balances, and the receipt: usage
201 * at price, what a discount, included usage and credit took off, and what
202 * is charged.
203 */
204export function IncludedAndCredit({ report, billingHref, owner }: { report: UsageReport; billingHref: string; owner: boolean }) {
205 const lines = receipt(report.totals, report.discountPercent);
206 const free = report.plan === "free";
207 const fullDiscount = (report.discountPercent ?? 0) >= 100;
208 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.209 <Card asChild tone="plain" className="grid gap-px overflow-hidden bg-line sm:grid-cols-3">
210 <section>
211 <div className="bg-surface p-4">
212 <p className="text-sm text-muted">{report.included ? "Included usage this month" : free ? "Trial credit" : "Plan"}</p>
213 {report.included ? (
214 <>
215 <p className="mt-1 text-lg font-semibold tabular-nums">
216 {money(report.included.used)} <span className="text-sm font-normal text-faint">/ {money(report.included.of)}</span>
217 </p>
218 <Bar used={report.included.used} of={report.included.of} />
219 </>
220 ) : free ? (
221 <>
222 <p className="mt-1 text-lg font-semibold tabular-nums">{report.trialMicros != null ? `${money(report.trialMicros)} left` : "None"}</p>
223 <p className="mt-1 text-xs text-faint">
224 The forge is free. Compute needs the plan or the trial.{" "}
225 <Link to={billingHref} className="text-fg-soft underline-offset-2 hover:underline">
226 See plans
227 </Link>
228 </p>
229 </>
230 ) : (
231 <p className="mt-1 text-sm">{fullDiscount ? `${report.discountPercent}% discount from g1t` : report.plan === "enterprise" ? "Paid by an enterprise" : "On the g1t plan"}</p>
232 )}
233 </div>
234 <div className="bg-surface p-4">
235 <p className="text-sm text-muted">Credit</p>
236 {fullDiscount ? (
237 <p className="mt-1 text-sm">AI usage is free under the {report.discountPercent}% discount: nothing to buy.</p>
238 ) : (
239 <dl className="mt-1 space-y-1 text-sm">
240 <div className="flex justify-between gap-3">
241 <dt className="text-muted">AI credit</dt>
242 <dd className="font-medium tabular-nums">{money(report.aiCreditMicros)}</dd>
243 </div>
244 {report.creditMicros > 0 && (
245 <div className="flex justify-between gap-3">
246 <dt className="text-muted">Credit from g1t</dt>
247 <dd className="tabular-nums">{money(report.creditMicros)}</dd>
248 </div>
249 )}
250 {owner && report.plan === "paid" && (
251 <dd>
252 <Link to={`${billingHref}#ai-credit`} className="text-xs text-fg-soft underline-offset-2 hover:underline">
253 Buy AI credit or turn on auto-reload
254 </Link>
255 </dd>
256 )}
257 </dl>
258 )}
259 </div>
260 <div className="bg-surface p-4">
261 <p className="text-sm text-muted">This range</p>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit262 <dl className="mt-1 space-y-1 text-sm">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.263 {lines.map((line, i) => (
264 <div key={line.label} className={cn("flex justify-between gap-3", i === lines.length - 1 && "border-t border-line pt-1 font-medium")}>
265 <dt className={i === lines.length - 1 ? "" : "text-muted"}>{line.label}</dt>
266 <dd className="tabular-nums">
267 {line.minus ? "−" : ""}
268 {money(line.micros)}
269 </dd>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit270 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.271 ))}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit272 </dl>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.273 {report.totals.pendingMicros > 0 && (
274 <p className="mt-1 text-xs text-faint">{money(report.totals.pendingMicros)} of it is metered this month and charged when it closes.</p>
275 )}
276 </div>
277 </section>
278 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit279 );
280}
281
282// --- The chart -----------------------------------------------------------------------------
283
284const CHART_HEIGHT = 200;
285
286/**
287 * Consumption: one column per day (or week, or month), stacked by product,
288 * money on the one axis. Hovering or focusing a column shows its products;
289 * the legend names them; the table view has every number.
290 */
291export function UsageChart({
292 columns,
293 products,
294 grain,
295 cumulative,
296 grainHref,
297 cumulativeHref,
298}: {
299 columns: Column[];
300 products: ProductUsage[];
301 grain: Grain;
302 cumulative: boolean;
303 grainHref: (grain: Grain) => string;
304 cumulativeHref: string;
305}) {
306 const used = products.filter((p) => p.micros !== 0 && columns.some((c) => (c.parts[p.key] ?? 0) !== 0));
307 const max = Math.max(0, ...columns.map((c) => c.total));
308 const scale = ticks(max);
309 const top = scale[scale.length - 1] || 1;
310 const id = useId();
311 const labelEvery = Math.max(1, Math.ceil(columns.length / 6));
312 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.313 <Card asChild className="p-4 sm:p-5">
314 <section aria-labelledby={`${id}-title`}>
315 <div className="flex flex-wrap items-center justify-between gap-3">
316 <h2 id={`${id}-title`} className="font-medium">
317 Consumption
318 </h2>
319 <div className="flex flex-wrap items-center gap-2 text-sm">
320 <nav className="flex rounded-md border border-line p-0.5" aria-label="Columns">
321 {(["day", "week", "month"] as Grain[]).map((g) => (
322 <Link
323 key={g}
324 to={grainHref(g)}
325 preventScrollReset
326 aria-current={g === grain ? "true" : undefined}
327 className={cn("rounded px-2 py-0.5 capitalize", g === grain ? "bg-raised text-fg" : "text-muted hover:text-fg")}
328 >
329 {g === "day" ? "Daily" : g === "week" ? "Weekly" : "Monthly"}
330 </Link>
331 ))}
332 </nav>
333 <Link to={cumulativeHref} preventScrollReset className="flex items-center gap-1.5 text-muted hover:text-fg" role="checkbox" aria-checked={cumulative}>
334 <span className={cn("grid size-3.5 place-items-center rounded-sm border", cumulative ? "border-accent bg-accent text-bg" : "border-line-strong")}>
335 {cumulative && <span className="text-[0.6rem] leading-none">✓</span>}
336 </span>
337 Cumulative
338 </Link>
339 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit340 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.341 {used.length > 0 && (
342 <ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted" aria-label="Legend">
343 {used.map((p) => (
344 <li key={p.key} className="flex items-center gap-1.5">
345 <span className="size-2.5 rounded-sm" style={{ background: productStyle(p.key).color }} />
346 {p.label}
347 </li>
348 ))}
349 </ul>
350 )}
351 <div className="mt-4 flex gap-2">
352 {/* The one money axis: clean ticks, every label different. */}
353 <div className="relative w-12 shrink-0 text-right text-[0.6875rem] text-faint tabular-nums" style={{ height: CHART_HEIGHT }} aria-hidden="true">
354 {scale.map((t) => (
355 <span key={t} className="absolute right-0 -translate-y-1/2" style={{ top: `${100 - (t / top) * 100}%` }}>
356 {axisMoney(t)}
357 </span>
358 ))}
359 </div>
360 <div className="relative min-w-0 flex-1" style={{ height: CHART_HEIGHT }}>
361 {scale.map((t) => (
362 <div key={t} className="absolute inset-x-0 border-t border-line" style={{ top: `${100 - (t / top) * 100}%` }} aria-hidden="true" />
363 ))}
364 <div className="absolute inset-0 flex items-end" role="list" aria-label="Usage per column">
365 {columns.map((column, i) => (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit366 <div
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.367 key={column.key}
368 role="listitem"
369 tabIndex={0}
370 aria-label={`${column.label}: ${money(column.total)}`}
371 className="group relative flex h-full min-w-0 flex-1 flex-col items-center justify-end outline-none"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit372 >
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.373 <div className="flex w-full max-w-6 flex-col-reverse gap-[2px] px-[1px]" style={{ height: `${(column.total / top) * 100}%` }}>
374 {used
375 .filter((p) => (column.parts[p.key] ?? 0) > 0)
376 .map((p, at, all) => (
377 <div
378 key={p.key}
379 className={cn("w-full min-h-[2px]", at === all.length - 1 && "rounded-t")}
380 style={{ flexGrow: column.parts[p.key] ?? 0, flexBasis: 0, background: productStyle(p.key).color }}
381 />
382 ))}
383 </div>
384 <div className="absolute inset-y-0 left-1/2 -z-0 w-full -translate-x-1/2 rounded group-hover:bg-fg/[0.04] group-focus-visible:bg-fg/[0.06]" aria-hidden="true" />
385 <div
386 className={cn(
387 "pointer-events-none absolute bottom-full z-10 mb-2 hidden w-52 rounded-lg border border-line-strong bg-raised p-2.5 text-xs shadow-xl group-hover:block group-focus-visible:block",
388 i < columns.length / 2 ? "left-0" : "right-0",
389 )}
390 role="tooltip"
391 >
392 <p className="flex justify-between font-medium">
393 <span>{column.label}</span>
394 <span className="tabular-nums">{money(column.total)}</span>
395 </p>
396 <ul className="mt-1.5 space-y-1">
397 {used.map((p) => (
398 <li key={p.key} className="flex items-center justify-between gap-2 text-muted">
399 <span className="flex items-center gap-1.5">
400 <span className="size-2 rounded-sm" style={{ background: productStyle(p.key).color }} />
401 {p.label}
402 </span>
403 <span className="tabular-nums text-fg-soft">{money(column.parts[p.key] ?? 0)}</span>
404 </li>
405 ))}
406 </ul>
407 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit408 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.409 ))}
410 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit411 </div>
412 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.413 <div className="ml-14 mt-1.5 flex text-[0.6875rem] text-faint" aria-hidden="true">
414 {columns.map((column, i) => (
415 <span key={column.key} className="min-w-0 flex-1 truncate text-center">
416 {i % labelEvery === 0 ? column.label : ""}
417 </span>
418 ))}
419 </div>
420 {max === 0 && <p className="mt-3 text-center text-sm text-faint">Nothing used in this range.</p>}
421 <details className="mt-3 text-sm">
422 <summary className="cursor-pointer text-xs text-muted hover:text-fg">Show as a table</summary>
423 <div className="mt-2 max-h-72 overflow-auto">
424 <table className="w-full text-left text-xs tabular-nums">
425 <thead className="sticky top-0 bg-surface text-muted">
426 <tr>
427 <th className="py-1 pr-3 font-medium">{grain === "day" ? "Day" : grain === "week" ? "Week of" : "Month"}</th>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit428 {used.map((p) => (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.429 <th key={p.key} className="py-1 pr-3 text-right font-medium">
430 {p.label}
431 </th>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit432 ))}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.433 <th className="py-1 text-right font-medium">{cumulative ? "Running total" : "Total"}</th>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit434 </tr>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.435 </thead>
436 <tbody className="divide-y divide-line">
437 {columns.map((column) => (
438 <tr key={column.key}>
439 <td className="py-1 pr-3">{column.label}</td>
440 {used.map((p) => (
441 <td key={p.key} className="py-1 pr-3 text-right">
442 {money(column.parts[p.key] ?? 0)}
443 </td>
444 ))}
445 <td className="py-1 text-right">{money(column.total)}</td>
446 </tr>
447 ))}
448 </tbody>
449 </table>
450 </div>
451 </details>
452 </section>
453 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit454 );
455}
456
457// --- The breakdown ---------------------------------------------------------------------------
458
459/** A meter's days as a small line, in the product's color. */
460export function Sparkline({ values, color }: { values: number[]; color: string }) {
461 const max = Math.max(...values, 0);
462 if (values.length < 2 || max <= 0) return <span className="block h-5 w-20" aria-hidden="true" />;
463 const points = values.map((v, i) => `${(i / (values.length - 1)) * 78 + 1},${19 - (v / max) * 17}`).join(" ");
464 return (
465 <svg viewBox="0 0 80 20" className="h-5 w-20" aria-hidden="true">
466 <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
467 </svg>
468 );
469}
470
471/** How much of an allowance is used, as a ring. */
472export function AllowanceRing({ used, of }: { used: number; of: number }) {
473 const part = of > 0 ? Math.min(1, used / of) : 0;
474 const r = 7;
475 const c = 2 * Math.PI * r;
476 return (
477 <svg viewBox="0 0 18 18" className="size-4 shrink-0 -rotate-90" aria-hidden="true">
478 <circle cx="9" cy="9" r={r} fill="none" stroke="var(--g1t-line-strong)" strokeWidth="2.5" />
Merge branch 'worktree-agent-ad7c6d88d93adc817'479 <circle cx="9" cy="9" r={r} fill="none" stroke={part >= 1 ? "var(--g1t-warn)" : "var(--g1t-success)"} strokeWidth="2.5" strokeDasharray={`${part * c} ${c}`} strokeLinecap="round" />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit480 </svg>
481 );
482}
483
484function MeterUsed({ meter }: { meter: MeterLine }) {
485 if (meter.allowance) {
486 const unit = meter.allowance.unit;
487 return (
488 <span className="flex items-center gap-1.5 sm:justify-end">
489 <AllowanceRing used={meter.allowance.used} of={meter.allowance.of} />
490 <span>
491 {quantity(meter.allowance.used, unit)} <span className="text-faint">/ {quantity(meter.allowance.of, unit).replace(/ (operations|entries)$/, "")} free</span>
492 </span>
493 </span>
494 );
495 }
496 return <span>{meter.quantity ? quantity(meter.quantity, meter.unit) : <span className="text-faint">—</span>}</span>;
497}
498
499const ROW = "grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 px-4 py-2.5 sm:grid-cols-[minmax(0,1fr)_5.5rem_14rem_6rem]";
500
501/** One meter: its days, how much was used and its charge; open, its projects. */
502function MeterRow({ meter, color, projectHref }: { meter: MeterLine; color: string; projectHref?: (project: string) => string }) {
503 const parts = meter.byProject.filter((p) => p.micros !== 0 || p.quantity !== 0);
504 const body = (
505 <>
506 <span className="flex min-w-0 items-center gap-2">
507 <span className="size-2 shrink-0 rounded-sm" style={{ background: color }} />
Merge branch 'model-routing'508 <span className="min-w-0">
509 <span className="block truncate">{meter.label}</span>
Usage: a meter's note is the site's tooltip, caught by the no-native-title test510 {meter.note && (
511 <Hint label={meter.note}>
512 <span className="block truncate text-[0.6875rem] text-faint">{meter.note}</span>
513 </Hint>
514 )}
Merge branch 'model-routing'515 </span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit516 {parts.length > 0 && <ChevronDown size={13} className="shrink-0 text-faint transition-transform group-open:rotate-180" />}
517 </span>
518 <span className="hidden sm:block">
519 <Sparkline values={meter.daily} color={color} />
520 </span>
521 <span className="order-3 col-span-2 text-sm text-muted tabular-nums sm:order-none sm:col-span-1 sm:text-right">
522 <MeterUsed meter={meter} />
523 </span>
524 <span className="text-right tabular-nums">
525 {money(meter.micros)}
526 {(meter.pendingMicros ?? 0) > 0 && <span className="block text-[0.6875rem] text-faint">{money(meter.pendingMicros ?? 0)} pending</span>}
527 </span>
528 </>
529 );
530 if (parts.length === 0) return <li className={cn(ROW, "text-sm")}>{body}</li>;
531 return (
532 <li>
533 <details className="group">
534 <summary className={cn(ROW, "cursor-pointer list-none text-sm hover:bg-raised/50 [&::-webkit-details-marker]:hidden")}>{body}</summary>
535 <ul className="border-t border-line bg-bg/40 py-1">
536 {parts.map((part) => (
537 <li key={part.project || "none"} className="flex justify-between gap-3 py-1.5 pl-10 pr-4 text-sm text-muted">
538 <span className="truncate">
539 {part.project ? (
540 projectHref ? (
541 <Link to={projectHref(part.project)} className="hover:text-fg hover:underline">
542 {part.project}
543 </Link>
544 ) : (
545 part.project
546 )
547 ) : (
548 "Not tied to a project"
549 )}
550 </span>
551 <span className="flex gap-4 tabular-nums">
552 {part.quantity > 0 && meter.unit !== "entries" && <span className="hidden sm:inline">{quantity(part.quantity, meter.unit)}</span>}
553 <span className="text-fg-soft">{money(part.micros)}</span>
554 </span>
555 </li>
556 ))}
557 </ul>
558 </details>
559 </li>
560 );
561}
562
563/** The breakdown, by product family (or by project, or by day). */
564export function Breakdown({
565 report,
566 group,
567 columns,
568 projectHref,
569}: {
570 report: UsageReport;
571 group: GroupBy;
572 columns: Column[];
573 projectHref?: (project: string) => string;
574}) {
575 const header = (first: string) => (
576 <div className={cn(ROW, "border-b border-line py-2 text-xs text-muted")}>
577 <span>{first}</span>
578 <span className="hidden sm:block">Trend</span>
579 <span className="hidden text-right sm:block">Usage</span>
580 <span className="text-right">Charge at price</span>
581 </div>
582 );
583 if (group === "project") {
584 const rows = byProject(report);
585 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.586 <Card asChild className="overflow-hidden">
587 <section>
588 <div className="grid grid-cols-[minmax(0,1fr)_auto] border-b border-line px-4 py-2 text-xs text-muted">
589 <span>Project</span>
590 <span>Charge at price</span>
591 </div>
592 {rows.length === 0 ? (
593 <p className="px-4 py-6 text-center text-sm text-faint">Nothing used in this range.</p>
594 ) : (
595 <ul className="divide-y divide-line">
596 {rows.map((row) => (
597 <li key={row.project || "none"} className="px-4 py-2.5 text-sm">
598 <div className="flex justify-between gap-3">
599 <span className="truncate font-medium">
600 {row.project ? projectHref ? <Link to={projectHref(row.project)} className="hover:underline">{row.project}</Link> : row.project : "Not tied to a project"}
601 </span>
602 <span className="tabular-nums">{money(row.micros)}</span>
603 </div>
604 <p className="mt-0.5 text-xs text-faint">{row.meters.map((m) => `${m.label} ${money(m.micros)}`).join(" · ")}</p>
605 </li>
606 ))}
607 </ul>
608 )}
609 </section>
610 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit611 );
612 }
613 if (group === "day") {
614 const days = columns.filter((c) => c.total !== 0).reverse();
615 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.616 <Card asChild className="overflow-hidden">
617 <section>
618 <div className="grid grid-cols-[minmax(0,1fr)_auto] border-b border-line px-4 py-2 text-xs text-muted">
619 <span>Day</span>
620 <span>Charge at price</span>
621 </div>
622 {days.length === 0 ? (
623 <p className="px-4 py-6 text-center text-sm text-faint">Nothing used in this range.</p>
624 ) : (
625 <ul className="divide-y divide-line">
626 {days.map((column) => (
627 <li key={column.key} className="px-4 py-2.5 text-sm">
628 <div className="flex justify-between gap-3">
629 <span className="font-medium">{column.label}</span>
630 <span className="tabular-nums">{money(column.total)}</span>
631 </div>
632 <p className="mt-0.5 text-xs text-faint">
633 {Object.entries(column.parts)
634 .filter(([, micros]) => micros !== 0)
635 .map(([product, micros]) => `${productStyle(product).label} ${money(micros)}`)
636 .join(" · ")}
637 </p>
638 </li>
639 ))}
640 </ul>
641 )}
642 </section>
643 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit644 );
645 }
646 const products = report.products.filter((p) => shownLines(p.meters).length > 0);
647 return (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.648 <Card asChild className="overflow-hidden">
649 <section>
650 {header("Product")}
651 {products.length === 0 ? (
652 <p className="px-4 py-6 text-center text-sm text-faint">Nothing used in this range.</p>
653 ) : (
654 products.map((product) => {
655 const color = productStyle(product.key).color;
656 return (
657 <div key={product.key} className="border-b border-line last:border-b-0">
658 <div className="flex items-baseline justify-between gap-3 bg-raised/40 px-4 py-2">
659 <h3 className="text-sm font-medium">{product.label}</h3>
660 <span className="text-sm font-medium tabular-nums">{money(product.micros)}</span>
661 </div>
662 {product.features && product.features.length > 0 && (
663 <p className="px-4 pt-2 text-xs text-faint">
664 {product.features.map((f) => `${f.label}${f.count ? ` (${f.count})` : ""} ${money(f.micros)}`).join(" · ")}
665 </p>
666 )}
667 {product.key === "agent" && report.models && report.models.length > 0 && (
668 <p className="px-4 pt-1 text-xs text-faint">
669 Tokens by model:{" "}
670 {report.models.map((m) => `${m.model} ${quantity(m.input + m.output + m.cacheRead + m.cacheWrite, "tokens")}`).join(" · ")}
671 </p>
672 )}
673 <ul className="divide-y divide-line/60">
674 {shownLines(product.meters).map((meter) => (
675 <MeterRow key={meter.key} meter={meter} color={color} projectHref={projectHref} />
676 ))}
677 </ul>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit678 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.679 );
680 })
681 )}
682 </section>
683 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit684 );
685}
686
687// --- Loading --------------------------------------------------------------------------------
688
689/** Each region's shape while the report is on its way: the same heights, so nothing moves. */
690export function UsageSkeleton({ children }: { children?: ReactNode }) {
691 return (
692 <div className="space-y-6" aria-busy="true">
693 <span role="status" className="sr-only">
694 Loading usage…
695 </span>
696 {children}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.697 <Card tone="plain" className="grid gap-px overflow-hidden bg-line sm:grid-cols-3" aria-hidden="true">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit698 {[0, 1, 2].map((i) => (
699 <div key={i} className="h-[7.5rem] bg-surface p-4">
700 <Skeleton className="h-3 w-28" />
701 <Skeleton className="mt-3 h-5 w-20" />
702 <Skeleton className="mt-3 h-1.5 w-full" />
703 </div>
704 ))}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.705 </Card>
706 <Card className="p-5" aria-hidden="true">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit707 <Skeleton className="h-4 w-28" />
708 <div className="mt-6 flex h-[200px] items-end gap-1">
709 {Array.from({ length: 24 }, (_, i) => (
710 <Skeleton key={i} className="flex-1 rounded-t" style={{ height: `${20 + ((i * 37) % 60)}%` }} />
711 ))}
712 </div>
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.713 </Card>
714 <Card aria-hidden="true">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit715 {[0, 1, 2, 3, 4].map((i) => (
716 <div key={i} className="flex items-center justify-between border-b border-line px-4 py-3 last:border-b-0">
717 <Skeleton className="h-3 w-40" />
718 <Skeleton className="h-3 w-16" />
719 </div>
720 ))}
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.721 </Card>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit722 </div>
723 );
724}

This file's history is long; its oldest lines are credited to the oldest commit read.