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