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