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 audit | 1 | /** |
| 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 | */ | |
| 9 | import { ChevronDown, Download, Ellipsis, FileCode } from "lucide-react"; | |
| 10 | import { type ReactNode, useId, useRef } from "react"; | |
| 11 | import { Form, Link } from "react-router"; | |
| 12 | ||
| 13 | import type { MeterLine, ProductUsage, UsageReport } from "@g1t/contracts"; | |
| 14 | ||
| 15 | import { | |
| 16 | type Column, | |
| 17 | type Grain, | |
| 18 | type GroupBy, | |
| 19 | PERIODS, | |
| 20 | type Period, | |
| 21 | byProject, | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 22 | pendingSentence, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 23 | productStyle, |
| 24 | quantity, | |
| 25 | rangeLabel, | |
| 26 | receipt, | |
| 27 | shownLines, | |
| 28 | ticks, | |
| 29 | usageCsv, | |
| 30 | } from "../lib/usage"; | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 31 | import { money } from "../lib/money"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 32 | import { 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. | 33 | import { Button } from "./ui/button"; |
| 34 | import { 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. | 35 | import { Checkbox } from "./ui/checkbox"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 36 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; |
| Usage: a meter's note is the site's tooltip, caught by the no-native-title test | 37 | import { 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. | 38 | import { Input } from "./ui/input"; |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 39 | import { SelectField } from "./ui/select"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 40 | import { Skeleton } from "./ui/skeleton"; |
| 41 | ||
| 42 | const 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 | ||
| 47 | export 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. */ | |
| 59 | function 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 audit | 69 | {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 audit | 77 | <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 audit | 84 | 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 audit | 87 | </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 | */ | |
| 96 | export function UsageFilterBar({ | |
| 97 | filters, | |
| 98 | products, | |
| 99 | projects, | |
| 100 | report, | |
| 101 | apiHref, | |
| 102 | fixedProject, | |
| 103 | }: { | |
| 104 | filters: UsageFilters; | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 105 | products: readonly { key: string; label: string }[]; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 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 project | 125 | <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 audit | 135 | {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 audit | 138 | <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 audit | 140 | </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 project | 146 | <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 audit | 160 | <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 audit | 164 | 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 audit | 166 | </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. */ | |
| 190 | function 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 audit | 195 | </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 | */ | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 204 | export function IncludedAndCredit({ report, billingHref }: { report: UsageReport; billingHref: string; owner?: boolean }) { |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 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 ? ( | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 237 | <p className="mt-1 text-sm">Usage is free under the {report.discountPercent}% discount.</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. | 238 | ) : ( |
| 239 | <dl className="mt-1 space-y-1 text-sm"> | |
| 240 | <div className="flex justify-between gap-3"> | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 241 | <dt className="text-muted">Credit from g1t</dt> |
| 242 | <dd className="font-medium tabular-nums">{money(report.creditMicros)}</dd> | |
| 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. | 243 | </div> |
| 244 | </dl> | |
| 245 | )} | |
| 246 | </div> | |
| 247 | <div className="bg-surface p-4"> | |
| 248 | <p className="text-sm text-muted">This range</p> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 249 | <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. | 250 | {lines.map((line, i) => ( |
| 251 | <div key={line.label} className={cn("flex justify-between gap-3", i === lines.length - 1 && "border-t border-line pt-1 font-medium")}> | |
| 252 | <dt className={i === lines.length - 1 ? "" : "text-muted"}>{line.label}</dt> | |
| 253 | <dd className="tabular-nums"> | |
| 254 | {line.minus ? "−" : ""} | |
| 255 | {money(line.micros)} | |
| 256 | </dd> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 257 | </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. | 258 | ))} |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 259 | </dl> |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 260 | {report.totals.pendingMicros > 0 && <p className="mt-1 text-xs text-faint">{pendingSentence(report.totals)}</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. | 261 | </div> |
| 262 | </section> | |
| 263 | </Card> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 264 | ); |
| 265 | } | |
| 266 | ||
| 267 | // --- The chart ----------------------------------------------------------------------------- | |
| 268 | ||
| 269 | const CHART_HEIGHT = 200; | |
| 270 | ||
| 271 | /** | |
| 272 | * Consumption: one column per day (or week, or month), stacked by product, | |
| 273 | * money on the one axis. Hovering or focusing a column shows its products; | |
| 274 | * the legend names them; the table view has every number. | |
| 275 | */ | |
| 276 | export function UsageChart({ | |
| 277 | columns, | |
| 278 | products, | |
| 279 | grain, | |
| 280 | cumulative, | |
| 281 | grainHref, | |
| 282 | cumulativeHref, | |
| 283 | }: { | |
| 284 | columns: Column[]; | |
| 285 | products: ProductUsage[]; | |
| 286 | grain: Grain; | |
| 287 | cumulative: boolean; | |
| 288 | grainHref: (grain: Grain) => string; | |
| 289 | cumulativeHref: string; | |
| 290 | }) { | |
| 291 | const used = products.filter((p) => p.micros !== 0 && columns.some((c) => (c.parts[p.key] ?? 0) !== 0)); | |
| 292 | const max = Math.max(0, ...columns.map((c) => c.total)); | |
| 293 | const scale = ticks(max); | |
| 294 | const top = scale[scale.length - 1] || 1; | |
| 295 | const id = useId(); | |
| 296 | const labelEvery = Math.max(1, Math.ceil(columns.length / 6)); | |
| 297 | 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. | 298 | <Card asChild className="p-4 sm:p-5"> |
| 299 | <section aria-labelledby={`${id}-title`}> | |
| 300 | <div className="flex flex-wrap items-center justify-between gap-3"> | |
| 301 | <h2 id={`${id}-title`} className="font-medium"> | |
| 302 | Consumption | |
| 303 | </h2> | |
| 304 | <div className="flex flex-wrap items-center gap-2 text-sm"> | |
| 305 | <nav className="flex rounded-md border border-line p-0.5" aria-label="Columns"> | |
| 306 | {(["day", "week", "month"] as Grain[]).map((g) => ( | |
| 307 | <Link | |
| 308 | key={g} | |
| 309 | to={grainHref(g)} | |
| 310 | preventScrollReset | |
| 311 | aria-current={g === grain ? "true" : undefined} | |
| 312 | className={cn("rounded px-2 py-0.5 capitalize", g === grain ? "bg-raised text-fg" : "text-muted hover:text-fg")} | |
| 313 | > | |
| 314 | {g === "day" ? "Daily" : g === "week" ? "Weekly" : "Monthly"} | |
| 315 | </Link> | |
| 316 | ))} | |
| 317 | </nav> | |
| 318 | <Link to={cumulativeHref} preventScrollReset className="flex items-center gap-1.5 text-muted hover:text-fg" role="checkbox" aria-checked={cumulative}> | |
| 319 | <span className={cn("grid size-3.5 place-items-center rounded-sm border", cumulative ? "border-accent bg-accent text-bg" : "border-line-strong")}> | |
| 320 | {cumulative && <span className="text-[0.6rem] leading-none">✓</span>} | |
| 321 | </span> | |
| 322 | Cumulative | |
| 323 | </Link> | |
| 324 | </div> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 325 | </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. | 326 | {used.length > 0 && ( |
| 327 | <ul className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted" aria-label="Legend"> | |
| 328 | {used.map((p) => ( | |
| 329 | <li key={p.key} className="flex items-center gap-1.5"> | |
| 330 | <span className="size-2.5 rounded-sm" style={{ background: productStyle(p.key).color }} /> | |
| 331 | {p.label} | |
| 332 | </li> | |
| 333 | ))} | |
| 334 | </ul> | |
| 335 | )} | |
| 336 | <div className="mt-4 flex gap-2"> | |
| 337 | {/* The one money axis: clean ticks, every label different. */} | |
| 338 | <div className="relative w-12 shrink-0 text-right text-[0.6875rem] text-faint tabular-nums" style={{ height: CHART_HEIGHT }} aria-hidden="true"> | |
| 339 | {scale.map((t) => ( | |
| 340 | <span key={t} className="absolute right-0 -translate-y-1/2" style={{ top: `${100 - (t / top) * 100}%` }}> | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 341 | {money(t, { compact: true })} |
| 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. | 342 | </span> |
| 343 | ))} | |
| 344 | </div> | |
| 345 | <div className="relative min-w-0 flex-1" style={{ height: CHART_HEIGHT }}> | |
| 346 | {scale.map((t) => ( | |
| 347 | <div key={t} className="absolute inset-x-0 border-t border-line" style={{ top: `${100 - (t / top) * 100}%` }} aria-hidden="true" /> | |
| 348 | ))} | |
| 349 | <div className="absolute inset-0 flex items-end" role="list" aria-label="Usage per column"> | |
| 350 | {columns.map((column, i) => ( | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 351 | <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. | 352 | key={column.key} |
| 353 | role="listitem" | |
| 354 | tabIndex={0} | |
| 355 | aria-label={`${column.label}: ${money(column.total)}`} | |
| 356 | 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 audit | 357 | > |
| 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. | 358 | <div className="flex w-full max-w-6 flex-col-reverse gap-[2px] px-[1px]" style={{ height: `${(column.total / top) * 100}%` }}> |
| 359 | {used | |
| 360 | .filter((p) => (column.parts[p.key] ?? 0) > 0) | |
| 361 | .map((p, at, all) => ( | |
| 362 | <div | |
| 363 | key={p.key} | |
| 364 | className={cn("w-full min-h-[2px]", at === all.length - 1 && "rounded-t")} | |
| 365 | style={{ flexGrow: column.parts[p.key] ?? 0, flexBasis: 0, background: productStyle(p.key).color }} | |
| 366 | /> | |
| 367 | ))} | |
| 368 | </div> | |
| 369 | <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" /> | |
| 370 | <div | |
| 371 | className={cn( | |
| 372 | "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", | |
| 373 | i < columns.length / 2 ? "left-0" : "right-0", | |
| 374 | )} | |
| 375 | role="tooltip" | |
| 376 | > | |
| 377 | <p className="flex justify-between font-medium"> | |
| 378 | <span>{column.label}</span> | |
| 379 | <span className="tabular-nums">{money(column.total)}</span> | |
| 380 | </p> | |
| 381 | <ul className="mt-1.5 space-y-1"> | |
| 382 | {used.map((p) => ( | |
| 383 | <li key={p.key} className="flex items-center justify-between gap-2 text-muted"> | |
| 384 | <span className="flex items-center gap-1.5"> | |
| 385 | <span className="size-2 rounded-sm" style={{ background: productStyle(p.key).color }} /> | |
| 386 | {p.label} | |
| 387 | </span> | |
| 388 | <span className="tabular-nums text-fg-soft">{money(column.parts[p.key] ?? 0)}</span> | |
| 389 | </li> | |
| 390 | ))} | |
| 391 | </ul> | |
| 392 | </div> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 393 | </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. | 394 | ))} |
| 395 | </div> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 396 | </div> |
| 397 | </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. | 398 | <div className="ml-14 mt-1.5 flex text-[0.6875rem] text-faint" aria-hidden="true"> |
| 399 | {columns.map((column, i) => ( | |
| 400 | <span key={column.key} className="min-w-0 flex-1 truncate text-center"> | |
| 401 | {i % labelEvery === 0 ? column.label : ""} | |
| 402 | </span> | |
| 403 | ))} | |
| 404 | </div> | |
| 405 | {max === 0 && <p className="mt-3 text-center text-sm text-faint">Nothing used in this range.</p>} | |
| 406 | <details className="mt-3 text-sm"> | |
| 407 | <summary className="cursor-pointer text-xs text-muted hover:text-fg">Show as a table</summary> | |
| 408 | <div className="mt-2 max-h-72 overflow-auto"> | |
| 409 | <table className="w-full text-left text-xs tabular-nums"> | |
| 410 | <thead className="sticky top-0 bg-surface text-muted"> | |
| 411 | <tr> | |
| 412 | <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 audit | 413 | {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. | 414 | <th key={p.key} className="py-1 pr-3 text-right font-medium"> |
| 415 | {p.label} | |
| 416 | </th> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 417 | ))} |
| 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. | 418 | <th className="py-1 text-right font-medium">{cumulative ? "Running total" : "Total"}</th> |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 419 | </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. | 420 | </thead> |
| 421 | <tbody className="divide-y divide-line"> | |
| 422 | {columns.map((column) => ( | |
| 423 | <tr key={column.key}> | |
| 424 | <td className="py-1 pr-3">{column.label}</td> | |
| 425 | {used.map((p) => ( | |
| 426 | <td key={p.key} className="py-1 pr-3 text-right"> | |
| 427 | {money(column.parts[p.key] ?? 0)} | |
| 428 | </td> | |
| 429 | ))} | |
| 430 | <td className="py-1 text-right">{money(column.total)}</td> | |
| 431 | </tr> | |
| 432 | ))} | |
| 433 | </tbody> | |
| 434 | </table> | |
| 435 | </div> | |
| 436 | </details> | |
| 437 | </section> | |
| 438 | </Card> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 439 | ); |
| 440 | } | |
| 441 | ||
| 442 | // --- The breakdown --------------------------------------------------------------------------- | |
| 443 | ||
| 444 | /** A meter's days as a small line, in the product's color. */ | |
| 445 | export function Sparkline({ values, color }: { values: number[]; color: string }) { | |
| 446 | const max = Math.max(...values, 0); | |
| 447 | if (values.length < 2 || max <= 0) return <span className="block h-5 w-20" aria-hidden="true" />; | |
| 448 | const points = values.map((v, i) => `${(i / (values.length - 1)) * 78 + 1},${19 - (v / max) * 17}`).join(" "); | |
| 449 | return ( | |
| 450 | <svg viewBox="0 0 80 20" className="h-5 w-20" aria-hidden="true"> | |
| 451 | <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" /> | |
| 452 | </svg> | |
| 453 | ); | |
| 454 | } | |
| 455 | ||
| 456 | /** How much of an allowance is used, as a ring. */ | |
| 457 | export function AllowanceRing({ used, of }: { used: number; of: number }) { | |
| 458 | const part = of > 0 ? Math.min(1, used / of) : 0; | |
| 459 | const r = 7; | |
| 460 | const c = 2 * Math.PI * r; | |
| 461 | return ( | |
| 462 | <svg viewBox="0 0 18 18" className="size-4 shrink-0 -rotate-90" aria-hidden="true"> | |
| 463 | <circle cx="9" cy="9" r={r} fill="none" stroke="var(--g1t-line-strong)" strokeWidth="2.5" /> | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 464 | <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 audit | 465 | </svg> |
| 466 | ); | |
| 467 | } | |
| 468 | ||
| 469 | function MeterUsed({ meter }: { meter: MeterLine }) { | |
| 470 | if (meter.allowance) { | |
| 471 | const unit = meter.allowance.unit; | |
| 472 | return ( | |
| 473 | <span className="flex items-center gap-1.5 sm:justify-end"> | |
| 474 | <AllowanceRing used={meter.allowance.used} of={meter.allowance.of} /> | |
| 475 | <span> | |
| 476 | {quantity(meter.allowance.used, unit)} <span className="text-faint">/ {quantity(meter.allowance.of, unit).replace(/ (operations|entries)$/, "")} free</span> | |
| 477 | </span> | |
| 478 | </span> | |
| 479 | ); | |
| 480 | } | |
| 481 | return <span>{meter.quantity ? quantity(meter.quantity, meter.unit) : <span className="text-faint">—</span>}</span>; | |
| 482 | } | |
| 483 | ||
| 484 | const 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]"; | |
| 485 | ||
| 486 | /** One meter: its days, how much was used and its charge; open, its projects. */ | |
| 487 | function MeterRow({ meter, color, projectHref }: { meter: MeterLine; color: string; projectHref?: (project: string) => string }) { | |
| 488 | const parts = meter.byProject.filter((p) => p.micros !== 0 || p.quantity !== 0); | |
| 489 | const body = ( | |
| 490 | <> | |
| 491 | <span className="flex min-w-0 items-center gap-2"> | |
| 492 | <span className="size-2 shrink-0 rounded-sm" style={{ background: color }} /> | |
| Merge branch 'model-routing' | 493 | <span className="min-w-0"> |
| 494 | <span className="block truncate">{meter.label}</span> | |
| Usage: a meter's note is the site's tooltip, caught by the no-native-title test | 495 | {meter.note && ( |
| 496 | <Hint label={meter.note}> | |
| 497 | <span className="block truncate text-[0.6875rem] text-faint">{meter.note}</span> | |
| 498 | </Hint> | |
| 499 | )} | |
| Merge branch 'model-routing' | 500 | </span> |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 501 | {parts.length > 0 && <ChevronDown size={13} className="shrink-0 text-faint transition-transform group-open:rotate-180" />} |
| 502 | </span> | |
| 503 | <span className="hidden sm:block"> | |
| 504 | <Sparkline values={meter.daily} color={color} /> | |
| 505 | </span> | |
| 506 | <span className="order-3 col-span-2 text-sm text-muted tabular-nums sm:order-none sm:col-span-1 sm:text-right"> | |
| 507 | <MeterUsed meter={meter} /> | |
| 508 | </span> | |
| 509 | <span className="text-right tabular-nums"> | |
| 510 | {money(meter.micros)} | |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 511 | {(meter.pendingMicros ?? 0) > 0 && <span className="block text-[0.6875rem] text-faint">{money(meter.pendingMicros ?? 0, { precise: true })} not yet closed</span>} |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 512 | </span> |
| 513 | </> | |
| 514 | ); | |
| 515 | if (parts.length === 0) return <li className={cn(ROW, "text-sm")}>{body}</li>; | |
| 516 | return ( | |
| 517 | <li> | |
| 518 | <details className="group"> | |
| 519 | <summary className={cn(ROW, "cursor-pointer list-none text-sm hover:bg-raised/50 [&::-webkit-details-marker]:hidden")}>{body}</summary> | |
| 520 | <ul className="border-t border-line bg-bg/40 py-1"> | |
| 521 | {parts.map((part) => ( | |
| 522 | <li key={part.project || "none"} className="flex justify-between gap-3 py-1.5 pl-10 pr-4 text-sm text-muted"> | |
| 523 | <span className="truncate"> | |
| 524 | {part.project ? ( | |
| 525 | projectHref ? ( | |
| 526 | <Link to={projectHref(part.project)} className="hover:text-fg hover:underline"> | |
| 527 | {part.project} | |
| 528 | </Link> | |
| 529 | ) : ( | |
| 530 | part.project | |
| 531 | ) | |
| 532 | ) : ( | |
| 533 | "Not tied to a project" | |
| 534 | )} | |
| 535 | </span> | |
| 536 | <span className="flex gap-4 tabular-nums"> | |
| 537 | {part.quantity > 0 && meter.unit !== "entries" && <span className="hidden sm:inline">{quantity(part.quantity, meter.unit)}</span>} | |
| 538 | <span className="text-fg-soft">{money(part.micros)}</span> | |
| 539 | </span> | |
| 540 | </li> | |
| 541 | ))} | |
| 542 | </ul> | |
| 543 | </details> | |
| 544 | </li> | |
| 545 | ); | |
| 546 | } | |
| 547 | ||
| 548 | /** The breakdown, by product family (or by project, or by day). */ | |
| 549 | export function Breakdown({ | |
| 550 | report, | |
| 551 | group, | |
| 552 | columns, | |
| 553 | projectHref, | |
| 554 | }: { | |
| 555 | report: UsageReport; | |
| 556 | group: GroupBy; | |
| 557 | columns: Column[]; | |
| 558 | projectHref?: (project: string) => string; | |
| 559 | }) { | |
| 560 | const header = (first: string) => ( | |
| 561 | <div className={cn(ROW, "border-b border-line py-2 text-xs text-muted")}> | |
| 562 | <span>{first}</span> | |
| 563 | <span className="hidden sm:block">Trend</span> | |
| 564 | <span className="hidden text-right sm:block">Usage</span> | |
| 565 | <span className="text-right">Charge at price</span> | |
| 566 | </div> | |
| 567 | ); | |
| 568 | if (group === "project") { | |
| 569 | const rows = byProject(report); | |
| 570 | 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. | 571 | <Card asChild className="overflow-hidden"> |
| 572 | <section> | |
| 573 | <div className="grid grid-cols-[minmax(0,1fr)_auto] border-b border-line px-4 py-2 text-xs text-muted"> | |
| 574 | <span>Project</span> | |
| 575 | <span>Charge at price</span> | |
| 576 | </div> | |
| 577 | {rows.length === 0 ? ( | |
| 578 | <p className="px-4 py-6 text-center text-sm text-faint">Nothing used in this range.</p> | |
| 579 | ) : ( | |
| 580 | <ul className="divide-y divide-line"> | |
| 581 | {rows.map((row) => ( | |
| 582 | <li key={row.project || "none"} className="px-4 py-2.5 text-sm"> | |
| 583 | <div className="flex justify-between gap-3"> | |
| 584 | <span className="truncate font-medium"> | |
| 585 | {row.project ? projectHref ? <Link to={projectHref(row.project)} className="hover:underline">{row.project}</Link> : row.project : "Not tied to a project"} | |
| 586 | </span> | |
| 587 | <span className="tabular-nums">{money(row.micros)}</span> | |
| 588 | </div> | |
| 589 | <p className="mt-0.5 text-xs text-faint">{row.meters.map((m) => `${m.label} ${money(m.micros)}`).join(" · ")}</p> | |
| 590 | </li> | |
| 591 | ))} | |
| 592 | </ul> | |
| 593 | )} | |
| 594 | </section> | |
| 595 | </Card> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 596 | ); |
| 597 | } | |
| 598 | if (group === "day") { | |
| 599 | const days = columns.filter((c) => c.total !== 0).reverse(); | |
| 600 | 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. | 601 | <Card asChild className="overflow-hidden"> |
| 602 | <section> | |
| 603 | <div className="grid grid-cols-[minmax(0,1fr)_auto] border-b border-line px-4 py-2 text-xs text-muted"> | |
| 604 | <span>Day</span> | |
| 605 | <span>Charge at price</span> | |
| 606 | </div> | |
| 607 | {days.length === 0 ? ( | |
| 608 | <p className="px-4 py-6 text-center text-sm text-faint">Nothing used in this range.</p> | |
| 609 | ) : ( | |
| 610 | <ul className="divide-y divide-line"> | |
| 611 | {days.map((column) => ( | |
| 612 | <li key={column.key} className="px-4 py-2.5 text-sm"> | |
| 613 | <div className="flex justify-between gap-3"> | |
| 614 | <span className="font-medium">{column.label}</span> | |
| 615 | <span className="tabular-nums">{money(column.total)}</span> | |
| 616 | </div> | |
| 617 | <p className="mt-0.5 text-xs text-faint"> | |
| 618 | {Object.entries(column.parts) | |
| 619 | .filter(([, micros]) => micros !== 0) | |
| 620 | .map(([product, micros]) => `${productStyle(product).label} ${money(micros)}`) | |
| 621 | .join(" · ")} | |
| 622 | </p> | |
| 623 | </li> | |
| 624 | ))} | |
| 625 | </ul> | |
| 626 | )} | |
| 627 | </section> | |
| 628 | </Card> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 629 | ); |
| 630 | } | |
| 631 | const products = report.products.filter((p) => shownLines(p.meters).length > 0); | |
| 632 | 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. | 633 | <Card asChild className="overflow-hidden"> |
| 634 | <section> | |
| 635 | {header("Product")} | |
| 636 | {products.length === 0 ? ( | |
| 637 | <p className="px-4 py-6 text-center text-sm text-faint">Nothing used in this range.</p> | |
| 638 | ) : ( | |
| 639 | products.map((product) => { | |
| 640 | const color = productStyle(product.key).color; | |
| 641 | return ( | |
| 642 | <div key={product.key} className="border-b border-line last:border-b-0"> | |
| 643 | <div className="flex items-baseline justify-between gap-3 bg-raised/40 px-4 py-2"> | |
| 644 | <h3 className="text-sm font-medium">{product.label}</h3> | |
| 645 | <span className="text-sm font-medium tabular-nums">{money(product.micros)}</span> | |
| 646 | </div> | |
| 647 | <ul className="divide-y divide-line/60"> | |
| 648 | {shownLines(product.meters).map((meter) => ( | |
| 649 | <MeterRow key={meter.key} meter={meter} color={color} projectHref={projectHref} /> | |
| 650 | ))} | |
| 651 | </ul> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 652 | </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. | 653 | ); |
| 654 | }) | |
| 655 | )} | |
| 656 | </section> | |
| 657 | </Card> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 658 | ); |
| 659 | } | |
| 660 | ||
| 661 | // --- Loading -------------------------------------------------------------------------------- | |
| 662 | ||
| 663 | /** Each region's shape while the report is on its way: the same heights, so nothing moves. */ | |
| 664 | export function UsageSkeleton({ children }: { children?: ReactNode }) { | |
| 665 | return ( | |
| 666 | <div className="space-y-6" aria-busy="true"> | |
| 667 | <span role="status" className="sr-only"> | |
| 668 | Loading usage… | |
| 669 | </span> | |
| 670 | {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. | 671 | <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 audit | 672 | {[0, 1, 2].map((i) => ( |
| 673 | <div key={i} className="h-[7.5rem] bg-surface p-4"> | |
| 674 | <Skeleton className="h-3 w-28" /> | |
| 675 | <Skeleton className="mt-3 h-5 w-20" /> | |
| 676 | <Skeleton className="mt-3 h-1.5 w-full" /> | |
| 677 | </div> | |
| 678 | ))} | |
| 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 | </Card> |
| 680 | <Card className="p-5" aria-hidden="true"> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 681 | <Skeleton className="h-4 w-28" /> |
| 682 | <div className="mt-6 flex h-[200px] items-end gap-1"> | |
| 683 | {Array.from({ length: 24 }, (_, i) => ( | |
| 684 | <Skeleton key={i} className="flex-1 rounded-t" style={{ height: `${20 + ((i * 37) % 60)}%` }} /> | |
| 685 | ))} | |
| 686 | </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. | 687 | </Card> |
| 688 | <Card aria-hidden="true"> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 689 | {[0, 1, 2, 3, 4].map((i) => ( |
| 690 | <div key={i} className="flex items-center justify-between border-b border-line px-4 py-3 last:border-b-0"> | |
| 691 | <Skeleton className="h-3 w-40" /> | |
| 692 | <Skeleton className="h-3 w-16" /> | |
| 693 | </div> | |
| 694 | ))} | |
| 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. | 695 | </Card> |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 696 | </div> |
| 697 | ); | |
| 698 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.