Skip to content
805 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.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1/**
2 * The billing page's sections: the spend-spike banner (also in the app
3 * shell), alerts, the g1t plan, the trial, the spend limit and its range,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging4 * Raise my limit, credits from g1t, Prepay, the run and issue caps, and "Spent more than you
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5 * meant to?". Each posts to the billing route's action with an `intent`.
6 */
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging7import { ArrowUpRight, CreditCard, Gauge, Gift, Landmark, ShieldCheck, Sparkles } from "lucide-react";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look8import type { ReactNode } from "react";
9import { Form } from "react-router";
10
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging11import type { Credits, Entitlements, FeatureState, Limit, LimitRequest, MeterUsage, UsageAlert } from "@g1t/contracts";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12
13import {
14 CAPS,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging15 CREDIT_KIND,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person16 PLUS_TAX,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17 PREPAY,
18 type PlanStatus,
19 alertText,
20 alertTone,
Merge Stripe Tax, the card fee on card payments, and one free workspace per person21 cardFeeCents,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging22 creditLine,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas23 gigabytes,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24 requestStatus,
25 share,
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging26 shortDay,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas27 shownMeters,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28 spendRange,
29} from "../lib/billing";
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.30import { money, wholeDollars } from "../lib/money";
Merge branch 'worktree-agent-a985c01f43538af01'31import { ErrorText, SubmitButton } from "./ui";
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.32import { Badge } from "./ui/badge";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
34
35/** An error the action returned, for the section it belongs to. */
36export type SectionError = { section: string; error: string } | null | undefined;
37
38const CONTROL =
39 "rounded-md border border-line bg-bg px-2.5 py-1.5 text-sm tabular-nums outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
40
41/** A dollar amount field: a `$` and a number. */
42function DollarInput({ name, defaultValue, placeholder, label }: { name: string; defaultValue?: string; placeholder?: string; label: string }) {
43 return (
44 <span className="flex items-center rounded-md border border-line bg-bg px-2 focus-within:border-accent-dim">
45 <span className="text-sm text-muted">$</span>
46 <input
47 name={name}
48 inputMode="decimal"
49 aria-label={label}
50 defaultValue={defaultValue}
51 placeholder={placeholder}
52 autoComplete="off"
53 data-1p-ignore
54 data-lpignore="true"
55 className="w-24 bg-transparent px-1 py-1.5 text-sm tabular-nums outline-none placeholder:text-faint"
56 />
57 </span>
58 );
59}
60
61/** A bordered card with a heading, a line about it, and a tone. */
62export function Card({
63 id,
64 title,
65 icon,
66 aside,
67 about,
68 tone = "plain",
69 children,
70}: {
71 id?: string;
72 title: ReactNode;
73 icon?: ReactNode;
74 aside?: ReactNode;
75 about?: ReactNode;
76 tone?: "plain" | "accent" | "warn" | "danger";
77 children?: ReactNode;
78}) {
79 const tones = {
80 plain: "border-line bg-surface",
81 accent: "border-accent/40 bg-accent/5",
82 warn: "border-warn/40 bg-warn/5",
83 danger: "border-danger/40 bg-danger/5",
84 };
85 return (
86 <section id={id} className={`mb-6 scroll-mt-20 rounded-xl border p-5 ${tones[tone]}`}>
87 <div className="flex flex-wrap items-start justify-between gap-3 sm:flex-nowrap">
88 <div className="min-w-0 basis-full sm:basis-0 sm:flex-1">
89 <h2 className="flex flex-wrap items-center gap-2 font-medium">
90 {icon && <span className="text-accent">{icon}</span>}
91 {title}
92 </h2>
93 {about && <div className="mt-1 max-w-2xl text-sm text-muted">{about}</div>}
94 </div>
95 {aside}
96 </div>
97 {children}
98 </section>
99 );
100}
101
102/** One meter: how much of something is used. */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas103export function Meter({
104 used,
105 of,
106 label,
107 detail,
108 calm = false,
109}: {
110 used: number;
111 of: number | null;
112 label: string;
113 detail?: ReactNode;
114 /** Reaching the end is ordinary, not a warning: the plan's included usage, past which usage simply goes on demand. */
115 calm?: boolean;
116}) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look117 const part = share(used, of);
Merge branch 'worktree-agent-ad7c6d88d93adc817'118 const bar = calm ? "bg-success" : of != null && used >= of ? "bg-danger" : part >= 0.75 ? "bg-warn" : "bg-success";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 return (
120 <div className="mt-4">
121 <div className="flex flex-wrap justify-between gap-x-4 text-sm">
122 <span className="text-muted">{label}</span>
123 <span className="tabular-nums">
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.124 {money(used)}
125 {of != null && <span className="text-faint"> of {money(of)}</span>}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look126 </span>
127 </div>
128 <div className="mt-1.5 h-1.5 overflow-hidden rounded-full bg-line" role="presentation">
129 <div className={`h-full ${bar}`} style={{ width: `${Math.max(part * 100, part > 0 ? 2 : 0)}%` }} />
130 </div>
131 {detail && <p className="mt-1.5 text-xs text-faint">{detail}</p>}
132 </div>
133 );
134}
135
136// --- Spike ------------------------------------------------------------------------
137
Fast pages, required checks on the branch, self-hosted runners, honest incidents138// In its own module, so the app shell carries it without the billing page.
139export { SpikeBanner } from "./spike-banner";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look140
141// --- Alerts -----------------------------------------------------------------------
142
143export function Alerts({ alerts }: { alerts: UsageAlert[] }) {
144 const shown = alerts.filter((alert) => alert.level >= 50).sort((a, b) => b.level - a.level);
145 if (shown.length === 0) return null;
146 return (
147 <ul className="mb-6 space-y-2">
148 {shown.map((alert) => {
149 const tone = alertTone(alert.level);
150 return (
151 <li
152 key={`${alert.meter}-${alert.level}`}
153 className={`flex gap-2.5 rounded-lg border px-4 py-2.5 text-sm ${
154 tone === "stopped" ? "border-danger/40 bg-danger/5" : tone === "warning" ? "border-warn/40 bg-warn/5" : "border-line bg-surface"
155 }`}
156 >
157 <Gauge size={15} className={`mt-0.5 shrink-0 ${tone === "stopped" ? "text-danger" : tone === "warning" ? "text-warn" : "text-muted"}`} />
158 <span>{alert.message || alertText(alert)}</span>
159 </li>
160 );
161 })}
162 </ul>
163 );
164}
165
166// --- The plan -----------------------------------------------------------------------
167
168const STATUS_TONE: Record<PlanStatus["kind"], "plain" | "accent" | "warn"> = {
169 free: "plain",
170 trial: "plain",
171 paid: "accent",
172 canceling: "warn",
173 past_due: "warn",
174 comped: "accent",
175 enterprise: "accent",
176};
177
178export function PlanCard({
179 state,
180 status,
181 entitlements,
182 owner,
183 enabled,
184 live,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas185 meters,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look186 error,
187}: {
188 state: FeatureState | null;
189 status: PlanStatus;
190 entitlements: Entitlements | null;
191 owner: boolean;
192 enabled: boolean;
193 live: boolean;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas194 /** This month's usage by meter, from billing's `usage_meters`. */
195 meters: MeterUsage[] | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look196 error?: string;
197}) {
198 const plan = state?.plan;
199 const subscription = state?.subscription ?? null;
200 const on = ["paid", "canceling", "comped", "enterprise"].includes(status.kind);
201 return (
202 <Card
203 id="plan"
204 tone={STATUS_TONE[status.kind]}
205 icon={<Sparkles size={16} />}
206 title={
207 <>
208 {plan?.title ?? "g1t"}
209 <span
210 className={`rounded-full px-2 py-0.5 text-xs ${
211 status.kind === "past_due" || status.kind === "canceling"
212 ? "bg-warn/15 text-warn"
213 : on
Merge branch 'worktree-agent-ad7c6d88d93adc817'214 ? "bg-success/15 text-success"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 : "border border-line text-muted"
216 }`}
217 >
218 {status.label}
219 {status.kind === "canceling" && subscription?.periodEnd ? `, ${new Date(subscription.periodEnd).toLocaleDateString()}` : ""}
220 </span>
221 </>
222 }
223 about={
224 on
225 ? "Everything on g1t for the workspace: agents, checks, workflows, the merge queue, deployments and semantic search."
226 : "The forge is free: repositories, issues, pull requests, reviews, search and the audit log. Anything that runs compute needs the plan, or the trial."
227 }
228 aside={
229 <p className="shrink-0 sm:text-right">
230 <span className="text-2xl font-semibold tabular-nums tracking-tight">${((plan?.monthlyCents ?? 2000) / 100).toFixed(0)}</span>
231 <span className="text-sm text-muted"> / month</span>
232 <span className="block text-xs text-faint">per workspace, never per seat</span>
Merge Stripe Tax, the card fee on card payments, and one free workspace per person233 <span className="block text-xs text-faint">
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.234 {plan?.cardFeeCents ? `+ ${money(plan.cardFeeCents * 10_000)} card processing fee, ${PLUS_TAX}` : "Plus tax where it applies"}
Merge Stripe Tax, the card fee on card payments, and one free workspace per person235 </span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look236 </p>
237 }
238 >
239 {plan && (
240 <ul className="mt-4 grid gap-1.5 text-sm text-muted sm:grid-cols-2">
241 {plan.includes.map((line) => (
242 <li key={line} className="flex gap-2">
Merge branch 'worktree-agent-ad7c6d88d93adc817'243 <span className="text-success">✓</span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look244 {line}
245 </li>
246 ))}
247 </ul>
248 )}
249 {plan && <p className="mt-3 text-xs text-faint">{plan.overage}</p>}
250
251 {on && entitlements?.includedMicros ? (
252 <Meter
253 label="Included usage this month"
254 used={entitlements.includedUsedMicros ?? 0}
255 of={entitlements.includedMicros}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas256 calm
257 detail="Used first, at cost plus 20%. Past it, usage goes on at the same prices. It starts again on the 1st; what is unused does not carry over."
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look258 />
259 ) : null}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas260 {meters && <MonthUsage meters={meters} on={on} comped={status.kind === "comped"} entitlements={entitlements} />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look261
262 {status.kind === "comped" || status.kind === "enterprise" ? (
263 <p className="mt-4 text-sm text-muted">
264 {status.kind === "comped"
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging265 ? "This workspace has a 100% discount from g1t: the plan and its usage are shown at their price, and nothing is charged."
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look266 : "An enterprise pays for this workspace, on its own invoice."}
267 </p>
268 ) : !enabled ? (
269 <p className="mt-4 text-sm text-muted">Payments are not set up on this g1t, so the plan cannot be started here.</p>
270 ) : !owner ? (
271 <p className="mt-4 text-sm text-muted">An owner manages the plan.</p>
272 ) : (
273 <Form method="post" className="mt-5 flex flex-wrap items-center gap-3">
274 <input type="hidden" name="feature" value={plan?.feature ?? "plan"} />
275 {status.kind === "free" || status.kind === "trial" ? (
Merge branch 'worktree-agent-a985c01f43538af01'276 <SubmitButton variant="accent" name="intent" value="subscribe" pending="Opening Stripe…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look277 <CreditCard size={14} />
278 Start the g1t plan
Merge branch 'worktree-agent-a985c01f43538af01'279 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look280 ) : status.kind === "canceling" ? (
Merge branch 'worktree-agent-a985c01f43538af01'281 <SubmitButton variant="accent" name="intent" value="resume" pending="Saving…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look282 Keep the plan
Merge branch 'worktree-agent-a985c01f43538af01'283 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 ) : null}
285 {(status.kind === "paid" || status.kind === "canceling" || status.kind === "past_due") && (
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.286 <SubmitButton variant={status.kind === "past_due" ? "accent" : "outline"} name="intent" value="portal" pending="Opening Stripe…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look287 {status.kind === "past_due" ? "Update payment on Stripe" : "Manage on Stripe"}
288 <ArrowUpRight size={14} />
Merge branch 'worktree-agent-a985c01f43538af01'289 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look290 )}
291 {status.kind === "paid" && subscription && (
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.292 <SubmitButton variant="outline" name="intent" value="cancel" pending="Saving…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look293 End at the end of the period
Merge branch 'worktree-agent-a985c01f43538af01'294 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look295 )}
296 {!live && (status.kind === "free" || status.kind === "trial") && (
297 <span className="text-xs text-faint">Test mode: card 4242 4242 4242 4242, any future date and code.</span>
298 )}
299 </Form>
300 )}
301 <ErrorText>{error}</ErrorText>
302 {(status.kind === "free" || status.kind === "trial") && (
303 <p className="mt-4 text-xs text-faint">
304 Your first month starts at a {wholeDollars(100_000_000)} limit on usage past what is included, so a stolen card cannot run
305 up a bill. It rises as payments clear, and you can prepay or ask to raise it at once.
306 </p>
307 )}
308 </Card>
309 );
310}
311
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas312/**
313 * This month's usage, one line per meter, in dollars at cost plus 20% and
314 * in what was used. No quotas: every line is metered from the first unit.
315 */
316function MonthUsage({
317 meters,
318 on,
319 comped,
320 entitlements,
321}: {
322 meters: MeterUsage[];
323 on: boolean;
324 comped: boolean;
325 entitlements: Entitlements | null;
326}) {
327 const freeStorage = gigabytes(entitlements?.freePrivateStorageBytes ?? 1_000_000_000);
328 const freeGit = (entitlements?.gitOperationsIncluded ?? 50_000).toLocaleString("en-US");
329 const rows = shownMeters(meters, on);
330 const total = rows.reduce((sum, row) => sum + row.micros, 0);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look331 return (
332 <div className="mt-4 rounded-lg border border-line bg-bg/40 p-4">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas333 <div className="flex items-baseline justify-between gap-4">
334 <p className="text-xs font-medium text-muted">This month's usage</p>
335 <p className="text-xs text-faint">At cost plus 20%</p>
336 </div>
337 <ul className="mt-2 divide-y divide-line/60">
338 {rows.map((row) => (
339 <li key={row.key} className="flex items-baseline justify-between gap-4 py-2 text-sm">
340 <span className="min-w-0">
341 <span>{row.label}</span>
342 {(row.quantity || row.micros === 0) && (
343 <span className="block truncate text-xs text-faint">{row.quantity ?? "None yet"}</span>
344 )}
345 </span>
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.346 <span className={`shrink-0 tabular-nums ${row.micros > 0 ? "" : "text-faint"}`}>{money(row.micros)}</span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look347 </li>
348 ))}
349 </ul>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas350 <div className="mt-1 flex items-baseline justify-between gap-4 border-t border-line pt-2.5 text-sm">
Billing's usage total is labeled at price, and Usage says what part of it was paid for351 {/* At price, before the plan's included usage, the trial and the pools paid
352 their part: Usage shows what was charged. */}
353 <span className="font-medium">Total at price</span>
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.354 <span className="font-medium tabular-nums">{money(total)}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas355 </div>
356 <p className="mt-2 text-xs text-faint">
357 {comped
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging358 ? "What this workspace's usage comes to at price. Its 100% discount takes all of it off."
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas359 : on
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.360 ? `Drawn from the included usage first, then charged up to your spend limit. Projects, previews and repositories are never charged, and the first ${freeStorage} of private storage and ${freeGit} git operations a month are free. App traffic, custom domains, storage and git operations are counted through the month and charged when it closes, after the included usage and any discount, like the rest.`
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging361 : `The forge is free: ${freeStorage} of private storage and ${freeGit} git operations a month. Agents run from the trial, and checks, workflows and the merge queue on public repositories from the open-source pool; together they pay part of this total: Usage shows what was charged.`}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas362 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look363 </div>
364 );
365}
366
367// --- The trial -----------------------------------------------------------------------
368
369export function TrialCard({
370 entitlements,
371 trialMicros,
372 owner,
373 enabled,
374 error,
375}: {
376 entitlements: Entitlements | null;
377 trialMicros: number;
378 owner: boolean;
379 enabled: boolean;
380 error?: string;
381}) {
382 const verified = entitlements?.trialVerified ?? false;
383 const left = entitlements?.trialMicrosLeft ?? 0;
384 return (
385 <Card
386 id="trial"
387 icon={<ShieldCheck size={16} />}
388 title={`Try it with ${wholeDollars(trialMicros)} of usage`}
389 about={
390 verified
391 ? left > 0
392 ? "Your trial pays for agents, checks and workflows until it is used. It is given once per workspace."
393 : "This workspace has no trial left. Start the plan to keep going."
394 : "Once per workspace, after a card check: Stripe saves and verifies the card with 3-D Secure and charges nothing. The trial needs a credit or debit card; prepaid cards can still pay for the plan."
395 }
396 >
397 {verified && left > 0 && <Meter label="Trial used" used={Math.max(0, trialMicros - left)} of={trialMicros} />}
398 {!verified && enabled && owner && (
399 <Form method="post" className="mt-4 flex flex-wrap items-center gap-3">
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.400 <SubmitButton variant="outline" name="intent" value="card-check" pending="Opening Stripe…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look401 <ShieldCheck size={14} />
402 Check a card
Merge branch 'worktree-agent-a985c01f43538af01'403 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look404 <span className="text-xs text-faint">Trials come from a monthly pool; when it is given out, new ones start on the 1st.</span>
405 </Form>
406 )}
407 {!verified && !owner && <p className="mt-3 text-sm text-muted">An owner can do the card check.</p>}
408 <ErrorText>{error}</ErrorText>
409 </Card>
410 );
411}
412
413// --- Spend limit -----------------------------------------------------------------------
414
Merge branch 'worktree-agent-a985c01f43538af01'415export function SpendLimitCard({ limit, owner, error }: { limit: Limit; owner: boolean; error?: string }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look416 const range = spendRange(limit);
417 const spent = limit.spentMicros ?? 0;
418 const spendLimit = limit.spendLimitMicros;
419 const mode = limit.defaultSpendLimit ? "automatic" : spendLimit == null ? "full" : "fixed";
420 const tone = limit.state === "stopped" ? "danger" : limit.state === "warning" ? "warn" : "plain";
421 return (
422 <Card
423 id="limit"
424 tone={tone}
425 icon={<Gauge size={16} />}
426 title="Spend limit"
427 about="What on-demand usage may reach this month, past what the plan includes. At the limit new work waits; runs already going finish."
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.428 aside={limit.firstMonth ? <Badge size="md">First month</Badge> : null}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look429 >
430 {limit.account.startsWith("ent_") && (
431 <p className="mt-2 text-sm text-muted">
432 Paid for by the <span className="font-medium text-fg">{limit.accountName}</span> enterprise: these figures are for all of its
433 workspaces together.
434 </p>
435 )}
436 <div className="grid gap-x-8 sm:grid-cols-2">
437 <Meter
438 label="Charged this month"
439 used={spent}
440 of={spendLimit}
441 detail={
442 mode === "automatic"
443 ? "Automatic: $200, or twice last month's spend, within what is available."
444 : mode === "fixed"
445 ? "A limit you set."
446 : "As high as is available."
447 }
448 />
449 <Meter
450 label="Not yet paid"
451 used={limit.exposureMicros}
452 of={limit.ceilingMicros}
453 detail={limit.growth ?? "Charged to the card as it nears the limit, so work keeps going."}
454 />
455 </div>
456 {limit.message && <p className="mt-4 text-sm">{limit.message}</p>}
457
458 <div className="mt-5 rounded-lg border border-line bg-bg/40 p-4 text-sm">
459 <p>
460 {range.selfServeMicros == null ? (
461 "No ceiling: set any limit."
462 ) : (
463 <>
464 You can set up to <span className="font-medium tabular-nums">{wholeDollars(range.selfServeMicros)}</span> yourself: the
465 highest limit this workspace has had{(limit.prepaidMicros ?? 0) > 0 ? ", plus what is prepaid" : ""}.
466 </>
467 )}
468 </p>
469 <p className="mt-1 text-muted">
470 {range.raiseOnceMicros != null ? (
471 <>
472 Once, you can raise it yourself to <span className="tabular-nums text-fg">{wholeDollars(range.raiseOnceMicros)}</span>.
473 Past that, prepay or use Raise my limit.
474 </>
475 ) : range.raisedAt ? (
476 <>The one-time raise was used on {new Date(range.raisedAt).toLocaleDateString()}. Prepay or use Raise my limit for more.</>
477 ) : range.selfServeMicros != null ? (
478 "For more, prepay or use Raise my limit."
479 ) : null}
480 </p>
481 </div>
482
483 {owner && (
484 <Form method="post" className="mt-4">
485 <input type="hidden" name="intent" value="spend-limit" />
486 <fieldset>
487 <legend className="text-xs text-muted">Your monthly spend limit</legend>
488 <RadioGroup name="mode" defaultValue={mode} className="mt-2 gap-2.5 text-sm">
489 <label className="flex cursor-pointer items-center gap-2">
490 <RadioGroupItem value="automatic" />
491 Automatic
492 </label>
493 <label className="flex cursor-pointer flex-wrap items-center gap-2">
494 <RadioGroupItem value="fixed" />
495 Fixed at
496 <DollarInput
497 name="limit"
498 label="Spend limit in dollars"
499 defaultValue={mode === "fixed" && spendLimit != null ? String(spendLimit / 1_000_000) : ""}
500 placeholder={range.selfServeMicros != null ? String(range.selfServeMicros / 1_000_000) : "500"}
501 />
502 </label>
503 <label className="flex cursor-pointer items-center gap-2">
504 <RadioGroupItem value="full" />
505 Everything available{range.selfServeMicros != null ? ` (${wholeDollars(range.selfServeMicros)})` : ""}
506 </label>
507 {range.raiseOnceMicros != null && (
508 <label className="flex cursor-pointer items-center gap-2">
509 <RadioGroupItem value="raise" />
510 Use my one-time raise, to the amount above or up to {wholeDollars(range.raiseOnceMicros)}
511 </label>
512 )}
513 </RadioGroup>
514 </fieldset>
515 <div className="mt-3 flex flex-wrap items-center gap-3">
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.516 <SubmitButton variant="outline" match={{ intent: "spend-limit" }} pending="Saving…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look517 Save limit
Merge branch 'worktree-agent-a985c01f43538af01'518 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look519 <a href="#raise" className="text-sm text-muted hover:text-fg">
520 Need more? Raise my limit
521 </a>
522 </div>
523 <ErrorText>{error}</ErrorText>
524 </Form>
525 )}
526 </Card>
527 );
528}
529
530// --- Raise my limit -----------------------------------------------------------------------
531
532function RequestList({ requests }: { requests: LimitRequest[] }) {
533 if (requests.length === 0) return null;
534 return (
535 <ul className="mt-4 divide-y divide-line rounded-lg border border-line text-sm">
536 {requests.map((request) => (
537 <li key={request.id} className="px-4 py-2.5">
538 <p className="flex flex-wrap items-center justify-between gap-2">
539 <span>
540 {request.kind === "limit" ? `Asked for ${wholeDollars(request.amountMicros)}` : "Told g1t about this month"}
541 <span className="text-faint"> · {new Date(request.createdAt).toLocaleDateString()}</span>
542 </span>
543 <span
544 className={`rounded-full px-2 py-0.5 text-xs ${
545 request.status === "approved"
Merge branch 'worktree-agent-ad7c6d88d93adc817'546 ? "bg-success/15 text-success"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look547 : request.status === "declined"
548 ? "bg-danger/15 text-danger"
549 : "border border-line text-muted"
550 }`}
551 >
552 {requestStatus(request)}
553 </span>
554 </p>
555 {request.answer && <p className="mt-1 text-muted">{request.answer}</p>}
556 </li>
557 ))}
558 </ul>
559 );
560}
561
Merge branch 'worktree-agent-a985c01f43538af01'562export function RaiseCard({ requests, owner, error }: { requests: LimitRequest[]; owner: boolean; error?: string }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look563 const mine = requests.filter((request) => request.kind === "limit");
564 const waiting = mine.some((request) => request.status === "open");
565 return (
566 <Card
567 id="raise"
568 title="Raise my limit"
569 about="Ask g1t for a higher limit than you can set yourself. A person reads it and answers within one business day, in the app and by email. Nothing about price is negotiated."
570 >
571 {owner && !waiting && (
572 <Form method="post" className="mt-4 space-y-3">
573 <input type="hidden" name="intent" value="request" />
574 <input type="hidden" name="kind" value="limit" />
575 <div className="flex flex-wrap gap-4">
576 <label className="text-sm">
577 <span className="mb-1 block text-xs text-muted">Limit you need</span>
578 <DollarInput name="amount" label="Limit you need in dollars" placeholder="2000" />
579 </label>
580 <label className="text-sm">
581 <span className="mb-1 block text-xs text-muted">What you expect to spend a month</span>
582 <DollarInput name="expected" label="Expected monthly spend in dollars" placeholder="1500" />
583 </label>
584 </div>
585 <label className="block text-sm">
586 <span className="mb-1 block text-xs text-muted">What it is for</span>
587 <textarea
588 name="reason"
589 rows={2}
590 maxLength={2000}
591 placeholder="A launch next week: agents on 40 issues across three projects."
592 className={`${CONTROL} w-full`}
593 />
594 </label>
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.595 <SubmitButton variant="outline" match={{ intent: "request", kind: "limit" }} pending="Sending…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look596 Send the request
Merge branch 'worktree-agent-a985c01f43538af01'597 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look598 <ErrorText>{error}</ErrorText>
599 </Form>
600 )}
601 {owner && waiting && <p className="mt-3 text-sm text-muted">Your request is with g1t. You can send another once it is answered.</p>}
602 {!owner && <p className="mt-3 text-sm text-muted">An owner can ask.</p>}
603 <RequestList requests={mine} />
604 </Card>
605 );
606}
607
608// --- Prepay -----------------------------------------------------------------------
609
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging610/**
611 * Credit g1t gave the workspace: what is left, each grant in a line with
612 * its expiry, and what each was for. Spent before anything prepaid.
613 */
614export function CreditsCard({ credits }: { credits: Credits }) {
615 const open = credits.grants.filter((grant) => grant.state === "open");
616 const past = credits.grants.filter((grant) => grant.state !== "open");
617 return (
618 <Card
619 id="credits"
620 icon={<Gift size={16} />}
621 title="Credits from g1t"
622 about="Credit g1t gave this workspace. It pays for usage before anything prepaid, the soonest-expiring first. Unused credit stops counting when it expires."
623 aside={
624 <p className="shrink-0 sm:text-right">
625 <span className="block text-xs text-muted">Credit left</span>
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.626 <span className="text-xl font-semibold tabular-nums">{money(credits.leftMicros)}</span>
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging627 </p>
628 }
629 >
630 <ul className="mt-4 divide-y divide-line rounded-lg border border-line">
631 {[...open, ...past].map((grant) => (
632 <li key={grant.id} className={`px-3.5 py-2.5 text-sm ${grant.state === "open" ? "" : "text-muted"}`}>
633 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
634 <span className={`tabular-nums ${grant.state === "open" ? "font-medium" : ""}`}>{creditLine(grant)}</span>
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.635 <Badge size="md" className="py-px">{CREDIT_KIND[grant.kind]}</Badge>
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging636 </p>
637 <p className="mt-0.5 text-xs text-faint">
638 {grant.kind === "refund" && grant.refundFor ? `For ${grant.refundFor}. ` : ""}
639 {grant.note} · given {shortDay(grant.createdAt)}
640 </p>
641 </li>
642 ))}
643 </ul>
644 </Card>
645 );
646}
647
Merge Stripe Tax, the card fee on card payments, and one free workspace per person648export function PrepayCard({
649 prepaidMicros,
650 owner,
651 live,
652 cardFee,
653 error,
654}: {
655 prepaidMicros: number;
656 owner: boolean;
657 live: boolean;
658 /** The card fee as billing charges it, to show it before paying. */
659 cardFee?: { on: boolean; percentMicros: number; fixedCents: number } | null;
660 error?: string;
661}) {
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.662 const fees = PREPAY.presets.map((amount) => `${money(cardFeeCents(amount * 100, cardFee) * 10_000)} on ${wholeDollars(amount * 1_000_000)}`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look663 return (
664 <Card
665 id="prepay"
666 icon={<Landmark size={16} />}
667 title="Prepay"
668 about="Pay for usage in advance. It raises what you can use before work stops by the same amount, at once, and usage draws it down."
669 aside={
670 <p className="shrink-0 sm:text-right">
671 <span className="block text-xs text-muted">Prepaid balance</span>
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.672 <span className="text-xl font-semibold tabular-nums">{money(prepaidMicros)}</span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look673 </p>
674 }
675 >
676 {owner ? (
677 <Form method="post" className="mt-4 space-y-3">
678 <input type="hidden" name="intent" value="prepay" />
679 <div className="flex flex-wrap items-center gap-2">
680 {PREPAY.presets.map((amount) => (
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.681 <SubmitButton key={amount} variant="outline" name="amount" value={amount} match={{ intent: "prepay" }} pending="Opening Stripe…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look682 {wholeDollars(amount * 1_000_000)}
Merge branch 'worktree-agent-a985c01f43538af01'683 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look684 ))}
685 <span className="text-sm text-muted">or</span>
686 <DollarInput name="custom" label="Amount to prepay in dollars" placeholder="250" />
Merge branch 'worktree-agent-a985c01f43538af01'687 {/* An empty amount, so a preset's press is not this button's: the typed amount is used. */}
688 <SubmitButton variant="accent" name="amount" value="" match={{ intent: "prepay" }} pending="Opening Stripe…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look689 <CreditCard size={14} />
690 Prepay
Merge branch 'worktree-agent-a985c01f43538af01'691 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look692 </div>
693 <RadioGroup name="method" defaultValue="card" className="flex flex-wrap gap-x-5 gap-y-2 text-sm">
694 <label className="flex cursor-pointer items-center gap-2">
695 <RadioGroupItem value="card" />
696 By card, with 3-D Secure ({wholeDollars(PREPAY.min * 1_000_000)} to {wholeDollars(PREPAY.maxCard * 1_000_000)})
697 </label>
698 <label className="flex cursor-pointer items-center gap-2">
699 <RadioGroupItem value="bank_transfer" />
700 By bank transfer (from {wholeDollars(PREPAY.bankFrom * 1_000_000)}; counted when it arrives)
701 </label>
702 </RadioGroup>
Merge Stripe Tax, the card fee on card payments, and one free workspace per person703 <p className="text-xs text-faint">
704 {cardFee?.on ? `By card, a card processing fee is its own line (${fees.join(", ")}); a bank transfer has none. ` : ""}
705 Tax is added where it applies, from your billing address; what you prepay is credited in full.
706 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look707 {!live && <p className="text-xs text-faint">Test mode: card 4242 4242 4242 4242, any future date and code.</p>}
708 <ErrorText>{error}</ErrorText>
709 </Form>
710 ) : (
711 <p className="mt-3 text-sm text-muted">An owner can prepay.</p>
712 )}
713 </Card>
714 );
715}
716
717// --- Caps -----------------------------------------------------------------------
718
Merge branch 'worktree-agent-a985c01f43538af01'719export function CapsCard({ entitlements, owner, error }: { entitlements: Entitlements; owner: boolean; error?: string }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look720 return (
721 <Card
722 id="caps"
723 title="Caps on agents"
724 about={
725 <>
726 The most one agent run may spend, and the most the agents on one issue may spend in all, so an agent stuck in a loop stops on
727 its own. Blank goes back to {wholeDollars(CAPS.run.default)} and {wholeDollars(CAPS.issue.default)}. A cap g1t set for this
728 workspace wins.
729 </>
730 }
731 >
732 <dl className="mt-4 grid grid-cols-2 gap-4 text-sm sm:max-w-md">
733 <div>
734 <dt className="text-xs text-muted">Per run</dt>
735 <dd className="mt-0.5 text-lg font-semibold tabular-nums">{wholeDollars(entitlements.runCapMicros)}</dd>
736 </div>
737 <div>
738 <dt className="text-xs text-muted">Per issue</dt>
739 <dd className="mt-0.5 text-lg font-semibold tabular-nums">{wholeDollars(entitlements.issueCapMicros)}</dd>
740 </div>
741 </dl>
742 {owner && (
743 <Form method="post" className="mt-4 flex flex-wrap items-end gap-4">
744 <input type="hidden" name="intent" value="caps" />
745 <label className="text-sm">
746 <span className="mb-1 block text-xs text-muted">
747 Per run ({wholeDollars(CAPS.run.min)} to {wholeDollars(CAPS.run.max)})
748 </span>
749 <DollarInput name="run" label="Run cap in dollars" defaultValue={String(entitlements.runCapMicros / 1_000_000)} />
750 </label>
751 <label className="text-sm">
752 <span className="mb-1 block text-xs text-muted">
753 Per issue ({wholeDollars(CAPS.issue.min)} to {wholeDollars(CAPS.issue.max)})
754 </span>
755 <DollarInput name="issue" label="Issue cap in dollars" defaultValue={String(entitlements.issueCapMicros / 1_000_000)} />
756 </label>
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.757 <SubmitButton variant="outline" match={{ intent: "caps" }} pending="Saving…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look758 Save caps
Merge branch 'worktree-agent-a985c01f43538af01'759 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look760 </Form>
761 )}
762 <ErrorText>{error}</ErrorText>
763 <p className="mt-3 text-xs text-faint">
764 {entitlements.maxConcurrentAgents} agents at a time{entitlements.maxRunMinutes ? `, ${entitlements.maxRunMinutes} minutes a run` : ""}
765 {entitlements.firstMonth ? " in the first month" : ""}.
766 </p>
767 </Card>
768 );
769}
770
771// --- Overage -----------------------------------------------------------------------
772
Merge branch 'worktree-agent-a985c01f43538af01'773export function OverageCard({ requests, owner, error }: { requests: LimitRequest[]; owner: boolean; error?: string }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look774 const mine = requests.filter((request) => request.kind === "overage");
775 const waiting = mine.some((request) => request.status === "open");
776 return (
777 <Card
778 id="overage"
779 title="Spent more than you meant to? Tell us."
780 about="A loop that ran all night, a workflow on every push: say what happened. g1t can credit usage you did not mean, once in 12 months; larger credits, or a second one within 12 months, are reviewed by a person."
781 >
782 {owner && !waiting && (
783 <Form method="post" className="mt-4 space-y-3">
784 <input type="hidden" name="intent" value="request" />
785 <input type="hidden" name="kind" value="overage" />
786 <textarea
787 name="reason"
788 rows={2}
789 maxLength={2000}
790 aria-label="What happened"
791 placeholder="An agent kept retrying a failing check on Tuesday night."
792 className={`${CONTROL} w-full`}
793 />
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.794 <SubmitButton variant="outline" match={{ intent: "request", kind: "overage" }} pending="Sending…">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look795 Tell g1t
Merge branch 'worktree-agent-a985c01f43538af01'796 </SubmitButton>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look797 <ErrorText>{error}</ErrorText>
798 </Form>
799 )}
800 {owner && waiting && <p className="mt-3 text-sm text-muted">g1t has your note and answers within one business day.</p>}
801 {!owner && <p className="mt-3 text-sm text-muted">An owner can tell g1t.</p>}
802 <RequestList requests={mine} />
803 </Card>
804 );
805}

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