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.
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 1 | /** |
| 2 | * Spend's parts: the tiles at the top, spend by day, where it went, the | |
| 3 | * budgets from the workspace down to a task with what happens at each, | |
| 4 | * the costliest tasks with their receipts, pricing, and the top bar's | |
| 5 | * spend pill. Every figure is one a service returned (lib/spend.server.ts). | |
| 6 | */ | |
| 7 | import { ArrowRight, Building2, Coins, Cpu, KeyRound, MessagesSquare, Puzzle, ReceiptText, Sparkles, UserRound, Users } from "lucide-react"; | |
| 8 | import { type ReactNode, useEffect, useId, useState } from "react"; | |
| 9 | import { Link, useFetcher } from "react-router"; | |
| 10 | ||
| 11 | import type { AgentPolicy, AgentSession, PersonBudget } from "@g1t/contracts"; | |
| 12 | ||
| 13 | import { AgentAvatar } from "./agent-avatar"; | |
| 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. | 14 | import { DollarsInput, useDialogFetcher } from "./agents/dialogs"; |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 15 | import { Meter, PrivateTitle, SliceList, stepsLine } from "./agents/parts"; |
| 16 | import { monthName, shortDay } from "./agents/format"; | |
| 17 | import { Badge } from "./ui/badge"; | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 18 | import { Button } from "./ui/button"; |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 19 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; |
| 20 | import { Field, FieldDescription, FieldError, FieldLabel } from "./ui/field"; | |
| 21 | import { Hint } from "./ui/hint"; | |
| 22 | import { Input } from "./ui/input"; | |
| 23 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | |
| 24 | import { Skeleton } from "./ui/skeleton"; | |
| 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. | 25 | import { ButtonLink, TimeAgo } from "./ui"; |
| 26 | import { Card } from "./ui/card"; | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 27 | import { dollarsField } from "../lib/agent-form"; |
| 28 | import { cn } from "../lib/cn"; | |
| 29 | import type { AgentBudgetRow, Budgets, PillData } from "../lib/spend.server"; | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 30 | import { AT_LIMIT, type Pricing, type SpendScope, agentRateLabel, markupLabel, percentLabel, shareOfBudget } from "../lib/spend"; |
| Money is written one way. A single formatter turns millionths of a dollar into dollars, rounding half up on whole micros rather than on a float, so the same sum reads the same on every page: under a cent reads <$0.01 on a total and exactly nothing is $0.00, while the statement's lines, a session's receipt, the price book and an agent's effort costs carry up to four places where the fraction of a cent is the point; the six formatters that each rounded their own way are gone. This month is the calendar month in UTC from its first day to today everywhere, and billing counts the month's not-yet-closed usage in any range that reaches into the current month, so the top bar's pill, Spend, Home and Usage ask for the same days and get the same figure; Home now reads the usage report the others read instead of adding up statements. Usage's pending sentence says what of that usage the close will charge after the discount and included usage, which the billing API returns as pending_charged_micros. A reconciliation test holds the pill, Spend, Home and Usage to one number for one month. The usage and billing guide says how amounts are written and what this month means. | 31 | import { money } from "../lib/money"; |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 32 | |
| 33 | ||
| 34 | /** A figure at the top of the page: what it is, the amount, and a line under it. */ | |
| 35 | export function Tile({ label, value, sub, children }: { label: ReactNode; value: ReactNode; sub?: ReactNode; children?: ReactNode }) { | |
| 36 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 37 | <Card className="min-w-0 p-4"> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 38 | <p className="flex items-center gap-1.5 text-xs text-muted">{label}</p> |
| 39 | <p className="mt-1.5 truncate text-2xl font-semibold tracking-tight tabular-nums">{value}</p> | |
| 40 | {sub && <p className="mt-0.5 text-xs text-faint">{sub}</p>} | |
| 41 | {children} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 42 | </Card> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 43 | ); |
| 44 | } | |
| 45 | ||
| 46 | export function TileSkeleton() { | |
| 47 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 48 | <Card className="p-4"> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 49 | <Skeleton className="h-3 w-24" /> |
| 50 | <Skeleton className="mt-3 h-7 w-28" /> | |
| 51 | <Skeleton className="mt-2 h-3 w-36" /> | |
| 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. | 52 | </Card> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 53 | ); |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Spend by day over any span, one bar a day with its amount on hover or | |
| 58 | * focus, and a table for screen readers. | |
| 59 | */ | |
| 60 | export function SpendBars({ days, className }: { days: { day: string; micros: number }[]; className?: string }) { | |
| 61 | const max = Math.max(0, ...days.map((d) => d.micros)); | |
| 62 | return ( | |
| 63 | <div className={className}> | |
| 64 | <div className="flex h-28 items-end gap-0.5" aria-hidden="true"> | |
| 65 | {days.map((d) => ( | |
| 66 | <Hint key={d.day} label={`${shortDay(d.day)} · ${money(d.micros)}`}> | |
| 67 | <span className="flex h-full min-w-0 flex-1 items-end rounded-t-[3px] hover:bg-raised/60"> | |
| 68 | <span | |
| 69 | className={cn("block w-full rounded-t-[3px]", d.micros > 0 ? "bg-accent/80" : "bg-line/70")} | |
| 70 | style={{ height: max > 0 && d.micros > 0 ? `${Math.max(4, (d.micros / max) * 100)}%` : "2px" }} | |
| 71 | /> | |
| 72 | </span> | |
| 73 | </Hint> | |
| 74 | ))} | |
| 75 | </div> | |
| 76 | <div className="mt-1.5 flex justify-between text-[0.6875rem] text-faint" aria-hidden="true"> | |
| 77 | <span>{days[0] ? shortDay(days[0].day) : ""}</span> | |
| 78 | <span>{days.length > 1 ? shortDay(days[days.length - 1]!.day) : ""}</span> | |
| 79 | </div> | |
| 80 | <table className="sr-only"> | |
| 81 | <caption>Spend by day</caption> | |
| 82 | <tbody> | |
| 83 | {days.map((d) => ( | |
| 84 | <tr key={d.day}> | |
| 85 | <th scope="row">{shortDay(d.day)}</th> | |
| 86 | <td>{money(d.micros)}</td> | |
| 87 | </tr> | |
| 88 | ))} | |
| 89 | </tbody> | |
| 90 | </table> | |
| 91 | </div> | |
| 92 | ); | |
| 93 | } | |
| 94 | ||
| 95 | /** A section with a heading, an aside, and what it holds. */ | |
| 96 | export function Section({ id, title, aside, children, className }: { id: string; title: ReactNode; aside?: ReactNode; children: ReactNode; className?: string }) { | |
| 97 | return ( | |
| 98 | <section aria-labelledby={id} className={cn("space-y-3", className)}> | |
| 99 | <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"> | |
| 100 | <h2 id={id} className="text-base font-semibold tracking-tight"> | |
| 101 | {title} | |
| 102 | </h2> | |
| 103 | {aside && <div className="text-xs text-faint">{aside}</div>} | |
| 104 | </div> | |
| 105 | {children} | |
| 106 | </section> | |
| 107 | ); | |
| 108 | } | |
| 109 | ||
| 110 | /** The ways to slice spend, as the page's tabs. */ | |
| 111 | export type SliceKey = "agent" | "person" | "channel" | "model" | "kind" | "product" | "extension"; | |
| 112 | ||
| 113 | export const SLICES: { key: SliceKey; label: string; icon: ReactNode; workspaceOnly?: boolean }[] = [ | |
| 114 | { key: "agent", label: "Agents", icon: <Sparkles size={13} /> }, | |
| 115 | { key: "person", label: "People", icon: <Users size={13} />, workspaceOnly: true }, | |
| 116 | { key: "channel", label: "Channels", icon: <MessagesSquare size={13} /> }, | |
| 117 | { key: "model", label: "Models", icon: <Cpu size={13} /> }, | |
| 118 | { key: "kind", label: "Kind of work", icon: <ReceiptText size={13} /> }, | |
| 119 | { key: "product", label: "Products", icon: <Building2 size={13} />, workspaceOnly: true }, | |
| 120 | { key: "extension", label: "Extensions", icon: <Puzzle size={13} />, workspaceOnly: true }, | |
| 121 | ]; | |
| 122 | ||
| 123 | /** The tabs over the breakdown: each a link, so a slice is an address. */ | |
| 124 | export function SliceTabs({ current, scope, href }: { current: SliceKey; scope: SpendScope; href: (key: SliceKey) => string }) { | |
| 125 | const shown = SLICES.filter((s) => scope === "workspace" || !s.workspaceOnly); | |
| 126 | return ( | |
| 127 | <nav aria-label="Slice spend by" className="-mx-1 flex gap-1 overflow-x-auto px-1 pb-1 [scrollbar-width:none]"> | |
| 128 | {shown.map((s) => ( | |
| 129 | <Link | |
| 130 | key={s.key} | |
| 131 | to={href(s.key)} | |
| 132 | preventScrollReset | |
| 133 | replace | |
| 134 | aria-current={s.key === current ? "page" : undefined} | |
| 135 | className={cn( | |
| 136 | "inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md px-2.5 text-[0.8125rem] transition-colors", | |
| 137 | s.key === current ? "bg-raised text-fg ring-1 ring-line-strong" : "text-muted hover:bg-raised/60 hover:text-fg", | |
| 138 | )} | |
| 139 | > | |
| 140 | <span className="text-faint">{s.icon}</span> | |
| 141 | {s.label} | |
| 142 | {s.key === "extension" && <span className="ml-0.5 text-[0.625rem] tracking-wide text-faint uppercase">Coming</span>} | |
| 143 | </Link> | |
| 144 | ))} | |
| 145 | </nav> | |
| 146 | ); | |
| 147 | } | |
| 148 | ||
| 149 | /** Spend in a panel, the slices ranked. */ | |
| 150 | export function SlicePanel({ children, foot }: { children: ReactNode; foot?: ReactNode }) { | |
| 151 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 152 | <Card className="overflow-hidden"> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 153 | {children} |
| 154 | {foot && <p className="border-t border-line px-4 py-2.5 text-xs text-faint">{foot}</p>} | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 155 | </Card> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 156 | ); |
| 157 | } | |
| 158 | ||
| 159 | export { SliceList }; | |
| 160 | ||
| 161 | /** Something not built yet, said plainly where it will be. */ | |
| 162 | export function ComingNote({ title, children }: { title: string; children: ReactNode }) { | |
| 163 | return ( | |
| 164 | <div className="px-4 py-8 text-center"> | |
| 165 | <p className="flex items-center justify-center gap-2 text-sm font-medium"> | |
| 166 | {title} | |
| 167 | <Badge>Coming</Badge> | |
| 168 | </p> | |
| 169 | <p className="mx-auto mt-1.5 max-w-md text-sm text-muted">{children}</p> | |
| 170 | </div> | |
| 171 | ); | |
| 172 | } | |
| 173 | ||
| 174 | // ── Budgets ────────────────────────────────────────────────────────────── | |
| 175 | ||
| 176 | /** One level of budget: its name, its amount, what is spent against it, and what happens at 100%. */ | |
| 177 | function Level({ | |
| 178 | icon, | |
| 179 | title, | |
| 180 | amount, | |
| 181 | spent, | |
| 182 | budget, | |
| 183 | atLimit, | |
| 184 | action, | |
| 185 | children, | |
| 186 | }: { | |
| 187 | icon: ReactNode; | |
| 188 | title: string; | |
| 189 | amount: ReactNode; | |
| 190 | spent?: number | null; | |
| 191 | budget?: number | null; | |
| 192 | atLimit: string; | |
| 193 | action?: ReactNode; | |
| 194 | children?: ReactNode; | |
| 195 | }) { | |
| 196 | const share = spent != null ? shareOfBudget(spent, budget) : null; | |
| 197 | return ( | |
| 198 | <li className="px-4 py-4"> | |
| 199 | <div className="flex flex-wrap items-start gap-x-3 gap-y-2"> | |
| 200 | <span className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-lg bg-raised text-muted ring-1 ring-line">{icon}</span> | |
| 201 | <div className="min-w-0 grow basis-56"> | |
| 202 | <p className="text-sm font-medium">{title}</p> | |
| 203 | <p className="mt-0.5 text-xs text-muted"> | |
| 204 | <span className="font-medium text-fg-soft">At 100%:</span> {atLimit} | |
| 205 | </p> | |
| 206 | </div> | |
| 207 | <div className="flex shrink-0 items-center gap-3 max-sm:w-full max-sm:justify-between max-sm:pl-10"> | |
| 208 | <div className="sm:text-right"> | |
| 209 | <p className="text-sm tabular-nums">{amount}</p> | |
| 210 | {spent != null && ( | |
| 211 | <p className="text-xs text-faint tabular-nums"> | |
| 212 | {money(spent)} spent{share != null ? ` · ${percentLabel(spent, budget)}` : ""} | |
| 213 | </p> | |
| 214 | )} | |
| 215 | </div> | |
| 216 | {action} | |
| 217 | </div> | |
| 218 | </div> | |
| 219 | {share != null && <Meter spent={spent!} cap={budget} label={`${title}: spent of its budget`} size="sm" className="mt-3 ml-10" />} | |
| 220 | {children} | |
| 221 | </li> | |
| 222 | ); | |
| 223 | } | |
| 224 | ||
| 225 | /** | |
| 226 | * The budgets, widest first: the workspace's spend limit, every agent | |
| 227 | * together, each person, each agent, and each task. Owners change the | |
| 228 | * agent levels here; the spend limit is on Billing, an agent's own on its | |
| 229 | * profile. | |
| 230 | */ | |
| 231 | export function BudgetLadder({ slug, budgets, owner, me, action, workspaceFigures }: { slug: string; budgets: Budgets; owner: boolean; me: string; action: string; workspaceFigures: boolean }) { | |
| 232 | const { limit, caps, policy, people, agents } = budgets; | |
| 233 | const limitMicros = limit ? (limit.spendLimitMicros ?? limit.ceilingMicros) : null; | |
| 234 | const mine = people?.people.find((p) => p.username === me) ?? null; | |
| 235 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 236 | <Card asChild divided className="overflow-hidden"> |
| 237 | <ol> | |
| 238 | <Level | |
| 239 | icon={<Building2 size={14} />} | |
| 240 | title="The workspace" | |
| 241 | amount={limit ? (limitMicros != null ? `${money(limitMicros)} a month` : "No limit") : <span className="text-faint">Unavailable</span>} | |
| 242 | spent={limit && workspaceFigures ? (limit.spentMicros ?? null) : null} | |
| 243 | budget={limitMicros} | |
| 244 | atLimit={AT_LIMIT.workspace(limit?.pauseAtLimit ?? true)} | |
| 245 | action={ | |
| 246 | <ButtonLink to={`/${slug}/-/billing`} variant="outline" size="sm"> | |
| 247 | {owner ? "Change" : "Billing"} | |
| 248 | </ButtonLink> | |
| 249 | } | |
| 250 | > | |
| 251 | {limit?.defaultSpendLimit && <p className="mt-2 ml-10 text-xs text-faint">Automatic until owners set one: $200, or twice last month's spend, whichever is more.</p>} | |
| 252 | </Level> | |
| 253 | <Level | |
| 254 | icon={<Sparkles size={14} />} | |
| 255 | title="All agents together" | |
| 256 | amount={policy ? (policy.monthly_micros != null ? `${money(policy.monthly_micros)} a month` : "No budget of its own") : <span className="text-faint">Unavailable</span>} | |
| 257 | spent={workspaceFigures ? budgets.agentsMonthMicros : null} | |
| 258 | budget={policy?.monthly_micros ?? null} | |
| 259 | atLimit={AT_LIMIT.agents} | |
| 260 | action={owner && policy ? <PolicyDialog policy={policy} action={action} trigger={<Button type="button" variant="outline" size="sm">Change</Button>} /> : null} | |
| 261 | /> | |
| 262 | <Level | |
| 263 | icon={<UserRound size={14} />} | |
| 264 | title="Each person" | |
| 265 | amount={policy ? (policy.person_monthly_micros != null ? `${money(policy.person_monthly_micros)} a month each` : "No budget per person") : <span className="text-faint">Unavailable</span>} | |
| 266 | spent={!owner && mine ? mine.spent_micros : null} | |
| 267 | budget={!owner && mine ? mine.monthly_micros : null} | |
| 268 | atLimit={AT_LIMIT.person} | |
| 269 | action={owner && policy ? <PolicyDialog policy={policy} action={action} trigger={<Button type="button" variant="outline" size="sm">Change</Button>} /> : null} | |
| 270 | > | |
| 271 | <p className="mt-2 ml-10 text-xs text-faint">What agents spend on work a person asks for: their chats with agents and the sessions they start.</p> | |
| 272 | {owner && people && <PeopleBudgets people={people.people} defaultMicros={people.default_micros} action={action} />} | |
| 273 | </Level> | |
| 274 | <Level | |
| 275 | icon={<Sparkles size={14} />} | |
| 276 | title="Each agent" | |
| 277 | amount={policy ? (policy.default_agent_monthly_micros != null ? `${money(policy.default_agent_monthly_micros)} a month for a new agent` : "Set on each agent") : <span className="text-faint">Unavailable</span>} | |
| 278 | atLimit={AT_LIMIT.agent} | |
| 279 | > | |
| 280 | {agents && agents.length > 0 && <AgentBudgets slug={slug} agents={agents} />} | |
| 281 | </Level> | |
| 282 | <Level | |
| 283 | icon={<ReceiptText size={14} />} | |
| 284 | title="Each task" | |
| 285 | amount={policy ? `${money(policy.default_session_micros)} a session` : <span className="text-faint">Unavailable</span>} | |
| 286 | atLimit={AT_LIMIT.session} | |
| 287 | action={owner && policy ? <PolicyDialog policy={policy} action={action} trigger={<Button type="button" variant="outline" size="sm">Change</Button>} /> : null} | |
| 288 | > | |
| 289 | <dl className="mt-3 ml-10 grid grid-cols-1 gap-x-6 gap-y-2 text-xs sm:grid-cols-3"> | |
| 290 | <Fact label="A session" value={policy ? money(policy.default_session_micros) : "—"} hint="Unless its agent's own per-task cap is lower." /> | |
| 291 | <Fact label="One run of the plan" value={caps ? money(caps.runMicros) : "—"} hint="Set under Caps on agents, on Billing." /> | |
| 292 | <Fact label="Agents on one issue" value={caps ? money(caps.issueMicros) : "—"} hint="Every run on the issue together." /> | |
| 293 | </dl> | |
| 294 | </Level> | |
| 295 | </ol> | |
| 296 | </Card> | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 297 | ); |
| 298 | } | |
| 299 | ||
| 300 | function Fact({ label, value, hint }: { label: string; value: string; hint: string }) { | |
| 301 | return ( | |
| 302 | <div> | |
| 303 | <dt className="text-faint">{label}</dt> | |
| 304 | <dd className="mt-0.5 text-sm tabular-nums">{value}</dd> | |
| 305 | <dd className="text-faint">{hint}</dd> | |
| 306 | </div> | |
| 307 | ); | |
| 308 | } | |
| 309 | ||
| 310 | /** Each agent against its own monthly budget, a link to its profile. */ | |
| 311 | function AgentBudgets({ slug, agents }: { slug: string; agents: AgentBudgetRow[] }) { | |
| 312 | const [all, setAll] = useState(false); | |
| 313 | const shown = all ? agents : agents.slice(0, 5); | |
| 314 | return ( | |
| 315 | <ul className="mt-3 ml-10 space-y-2.5"> | |
| 316 | {shown.map((agent) => ( | |
| 317 | <li key={agent.id} className="flex items-center gap-2.5 text-sm"> | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 318 | <AgentAvatar agent={{ handle: agent.handle, avatar_seed: agent.avatar_seed, look: agent.look }} size={20} /> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 319 | <Link to={`/${slug}/-/agents/${agent.handle}/spend`} className="min-w-0 grow truncate hover:underline"> |
| 320 | {agent.display_name} | |
| 321 | </Link> | |
| 322 | <span className="shrink-0 text-xs text-faint tabular-nums"> | |
| 323 | {money(agent.spent_month_micros)} | |
| 324 | {agent.budget.monthly_micros != null ? ` of ${money(agent.budget.monthly_micros)}` : " · no cap"} | |
| 325 | </span> | |
| 326 | <span className="hidden w-24 shrink-0 sm:block"> | |
| 327 | {agent.budget.monthly_micros != null && <Meter spent={agent.spent_month_micros} cap={agent.budget.monthly_micros} label={`${agent.display_name}: spent of its budget`} size="sm" />} | |
| 328 | </span> | |
| 329 | </li> | |
| 330 | ))} | |
| 331 | {agents.length > 5 && ( | |
| 332 | <li> | |
| 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. | 333 | <Button type="button" onClick={() => setAll((v) => !v)} variant="link" size="inline" className="text-xs font-normal"> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 334 | {all ? "Show fewer" : `Show all ${agents.length}`} |
| 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. | 335 | </Button> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 336 | </li> |
| 337 | )} | |
| 338 | </ul> | |
| 339 | ); | |
| 340 | } | |
| 341 | ||
| 342 | /** Each person who has a budget of their own or spent this month, for owners, with the way to set one. */ | |
| 343 | function PeopleBudgets({ people, defaultMicros, action }: { people: PersonBudget[]; defaultMicros: number | null; action: string }) { | |
| 344 | return ( | |
| 345 | <div className="mt-3 ml-10"> | |
| 346 | {people.length > 0 && ( | |
| 347 | <ul className="space-y-2.5"> | |
| 348 | {people.slice(0, 12).map((person) => ( | |
| 349 | <li key={person.username} className="flex items-center gap-2.5 text-sm"> | |
| 350 | <span className="min-w-0 grow truncate"> | |
| 351 | @{person.username} | |
| 352 | {person.own && <span className="ml-1.5 text-xs text-faint">own budget</span>} | |
| 353 | </span> | |
| 354 | <span className="shrink-0 text-xs text-faint tabular-nums"> | |
| 355 | {money(person.spent_micros)} | |
| 356 | {person.monthly_micros != null ? ` of ${money(person.monthly_micros)}` : " · no budget"} | |
| 357 | </span> | |
| 358 | <span className="hidden w-24 shrink-0 sm:block"> | |
| 359 | {person.monthly_micros != null && <Meter spent={person.spent_micros} cap={person.monthly_micros} label={`@${person.username}: spent of their budget`} size="sm" />} | |
| 360 | </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. | 361 | <PersonDialog action={action} person={person} defaultMicros={defaultMicros} trigger={<Button type="button" variant="link" size="inline" className="text-xs">Set</Button>} /> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 362 | </li> |
| 363 | ))} | |
| 364 | </ul> | |
| 365 | )} | |
| 366 | <div className="mt-2.5"> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 367 | <PersonDialog action={action} person={null} defaultMicros={defaultMicros} trigger={<Button type="button" variant="link" size="inline" className="text-xs">Give someone their own budget</Button>} /> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 368 | </div> |
| 369 | </div> | |
| 370 | ); | |
| 371 | } | |
| 372 | ||
| 373 | /** The workspace's agent policy: every agent together, each person, a new agent, a session. Owners only. */ | |
| 374 | function PolicyDialog({ policy, action, trigger }: { policy: AgentPolicy; action: string; trigger: ReactNode }) { | |
| 375 | const { fetcher, open, setOpen, error, busy } = useDialogFetcher("spend-policy"); | |
| 376 | const id = useId(); | |
| 377 | return ( | |
| 378 | <Dialog open={open} onOpenChange={setOpen}> | |
| 379 | <DialogTrigger asChild>{trigger}</DialogTrigger> | |
| 380 | <DialogContent> | |
| 381 | <DialogHeader> | |
| 382 | <DialogTitle>Agent budgets</DialogTitle> | |
| 383 | <DialogDescription>Monthly, reset on the 1st (UTC). The workspace's spend limit sits above all of them. Leave a budget empty for none.</DialogDescription> | |
| 384 | </DialogHeader> | |
| 385 | <fetcher.Form method="post" action={action} className="grid gap-5"> | |
| 386 | <input type="hidden" name="intent" value="policy" /> | |
| 387 | <Field> | |
| 388 | <FieldLabel htmlFor={`${id}-monthly`}>All agents together</FieldLabel> | |
| 389 | <DollarsInput id={`${id}-monthly`} name="monthly" defaultValue={dollarsField(policy.monthly_micros)} placeholder="No budget of its own" /> | |
| 390 | </Field> | |
| 391 | <Field> | |
| 392 | <FieldLabel htmlFor={`${id}-person`}>Each person</FieldLabel> | |
| 393 | <DollarsInput id={`${id}-person`} name="person" defaultValue={dollarsField(policy.person_monthly_micros)} placeholder="No budget per person" /> | |
| 394 | <FieldDescription>What agents may spend on the work one person asks for. Give someone more or less from their row.</FieldDescription> | |
| 395 | </Field> | |
| 396 | <Field> | |
| 397 | <FieldLabel htmlFor={`${id}-agent`}>A new agent</FieldLabel> | |
| 398 | <DollarsInput id={`${id}-agent`} name="default_agent" defaultValue={dollarsField(policy.default_agent_monthly_micros)} placeholder="None" /> | |
| 399 | <FieldDescription>Filled in when an agent is hired; change each one on its profile.</FieldDescription> | |
| 400 | </Field> | |
| 401 | <Field> | |
| 402 | <FieldLabel htmlFor={`${id}-session`}>A session</FieldLabel> | |
| 403 | <DollarsInput id={`${id}-session`} name="default_session" defaultValue={dollarsField(policy.default_session_micros)} required /> | |
| 404 | <FieldDescription>Between $0.10 and $500. A session stops here and asks before spending more.</FieldDescription> | |
| 405 | </Field> | |
| 406 | <FieldError>{error}</FieldError> | |
| 407 | <DialogFooter> | |
| 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. | 408 | <Button type="button" variant="outline" onClick={() => setOpen(false)}> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 409 | Cancel |
| 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. | 410 | </Button> |
| 411 | <Button type="submit" variant="accent" disabled={busy}> | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 412 | {busy ? "Saving…" : "Save budgets"} |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 413 | </Button> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 414 | </DialogFooter> |
| 415 | </fetcher.Form> | |
| 416 | </DialogContent> | |
| 417 | </Dialog> | |
| 418 | ); | |
| 419 | } | |
| 420 | ||
| 421 | /** One person's own budget: an amount, none at all, or back to the default. Owners only. */ | |
| 422 | function PersonDialog({ action, person, defaultMicros, trigger }: { action: string; person: PersonBudget | null; defaultMicros: number | null; trigger: ReactNode }) { | |
| 423 | const { fetcher, open, setOpen, error, busy } = useDialogFetcher(`spend-person-${person?.username ?? "new"}`); | |
| 424 | const id = useId(); | |
| 425 | return ( | |
| 426 | <Dialog open={open} onOpenChange={setOpen}> | |
| 427 | <DialogTrigger asChild>{trigger}</DialogTrigger> | |
| 428 | <DialogContent> | |
| 429 | <DialogHeader> | |
| 430 | <DialogTitle>{person ? `@${person.username}'s budget` : "A person's own budget"}</DialogTitle> | |
| 431 | <DialogDescription> | |
| 432 | What agents may spend on work this person asks for each month, in place of the default | |
| 433 | {defaultMicros != null ? ` of ${money(defaultMicros)}` : " (none now)"}. 0 means no budget at all. | |
| 434 | </DialogDescription> | |
| 435 | </DialogHeader> | |
| 436 | <fetcher.Form method="post" action={action} className="grid gap-5"> | |
| 437 | <input type="hidden" name="intent" value="person" /> | |
| 438 | {person ? ( | |
| 439 | <input type="hidden" name="username" value={person.username} /> | |
| 440 | ) : ( | |
| 441 | <Field> | |
| 442 | <FieldLabel htmlFor={`${id}-username`}>Username</FieldLabel> | |
| 443 | <Input id={`${id}-username`} name="username" required autoComplete="off" placeholder="ana" /> | |
| 444 | </Field> | |
| 445 | )} | |
| 446 | <Field> | |
| 447 | <FieldLabel htmlFor={`${id}-amount`}>Monthly budget</FieldLabel> | |
| 448 | <DollarsInput id={`${id}-amount`} name="amount" defaultValue={person?.own ? dollarsField(person.monthly_micros ?? 0) : ""} placeholder="The default" /> | |
| 449 | <FieldDescription>Empty puts them back on the default.</FieldDescription> | |
| 450 | </Field> | |
| 451 | <FieldError>{error}</FieldError> | |
| 452 | <DialogFooter> | |
| 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. | 453 | <Button type="button" variant="outline" onClick={() => setOpen(false)}> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 454 | Cancel |
| 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. | 455 | </Button> |
| 456 | <Button type="submit" variant="accent" disabled={busy}> | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 457 | {busy ? "Saving…" : "Save"} |
| 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. | 458 | </Button> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 459 | </DialogFooter> |
| 460 | </fetcher.Form> | |
| 461 | </DialogContent> | |
| 462 | </Dialog> | |
| 463 | ); | |
| 464 | } | |
| 465 | ||
| 466 | // ── Receipts ───────────────────────────────────────────────────────────── | |
| 467 | ||
| 468 | /** Where a task's receipt is. */ | |
| 469 | export function receiptHref(slug: string, session: Pick<AgentSession, "id">): string { | |
| 470 | return `/${slug}/-/spend/receipts/${session.id}`; | |
| 471 | } | |
| 472 | ||
| 473 | /** The costliest tasks, each a link to its receipt. */ | |
| 474 | export function TaskList({ slug, sessions }: { slug: string; sessions: AgentSession[] }) { | |
| 475 | if (sessions.length === 0) return <p className="px-4 py-6 text-center text-sm text-faint">No tasks in this period.</p>; | |
| 476 | return ( | |
| 477 | <ul className="divide-y divide-line/60"> | |
| 478 | {sessions.map((session) => ( | |
| 479 | <li key={session.id} className="group relative flex items-center gap-3 px-4 py-3 transition-colors hover:bg-raised/40"> | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 480 | <AgentAvatar agent={{ handle: session.agent_handle, avatar_seed: session.agent_avatar_seed, look: session.agent_look }} size={24} /> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 481 | <div className="min-w-0 grow"> |
| 482 | <Link to={receiptHref(slug, session)} className="block min-w-0 truncate text-sm font-medium after:absolute after:inset-0 hover:underline"> | |
| 483 | {session.visible ? session.title || "Untitled session" : <PrivateTitle />} | |
| 484 | </Link> | |
| 485 | <p className="mt-0.5 truncate text-xs text-faint"> | |
| 486 | {session.agent_name} | |
| 487 | {session.visible && session.asked_by_username ? ` · for @${session.asked_by_username}` : ""} · {stepsLine(session)} · <TimeAgo at={session.created_at} /> | |
| 488 | </p> | |
| 489 | </div> | |
| 490 | <span className="shrink-0 text-sm tabular-nums">{money(session.charged_micros)}</span> | |
| 491 | <ArrowRight size={14} className="shrink-0 text-faint group-hover:text-muted" aria-hidden="true" /> | |
| 492 | </li> | |
| 493 | ))} | |
| 494 | </ul> | |
| 495 | ); | |
| 496 | } | |
| 497 | ||
| 498 | // ── Pricing ────────────────────────────────────────────────────────────── | |
| 499 | ||
| 500 | /** How g1t prices what it runs, from the price book. */ | |
| 501 | export function PricingCard({ pricing }: { pricing: Pricing | null }) { | |
| 502 | if (!pricing) return <p className="rounded-xl border border-line bg-surface px-4 py-6 text-center text-sm text-muted">Prices couldn't be read right now.</p>; | |
| 503 | const rows: { icon: ReactNode; title: string; value: string; about: string }[] = [ | |
| 504 | { | |
| 505 | icon: <Cpu size={15} />, | |
| 506 | title: "Models", | |
| 507 | value: pricing.modelMarkupPercent > 0 ? `Provider price + ${pricing.modelMarkupPercent}%` : "Provider price", | |
| 508 | about: pricing.modelMarkupPercent > 0 ? "What the model provider charges, with the markup shown." : "What the model provider charges, with no markup.", | |
| 509 | }, | |
| 510 | { | |
| 511 | icon: <Sparkles size={15} />, | |
| 512 | title: "Agent rate", | |
| 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. | 513 | value: agentRateLabel(pricing, (micros) => money(micros, { precise: true })), |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 514 | about: "For what g1t runs around every model call: the model gateway, secrets, routing, context and pass-through to your own provider.", |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 515 | }, |
| 516 | { | |
| 517 | icon: <Coins size={15} />, | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 518 | title: "Everything else g1t runs", |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 519 | value: pricing.markup ? `At cost + ${markupLabel(pricing.markup)}` : "At cost", |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 520 | about: "Sandboxes, builds, hosting, storage, search and security scans, at what they cost g1t.", |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 521 | }, |
| 522 | { | |
| 523 | icon: <KeyRound size={15} />, | |
| 524 | title: "Your own model keys", | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 525 | value: "Agent rate only", |
| 526 | about: "Your provider bills the model; g1t charges the agent rate for passing it through. Local models the same.", | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 527 | }, |
| 528 | { icon: <Building2 size={15} />, title: "Your own runners", value: "$0", about: "Time on your own hardware is never charged." }, | |
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 529 | { icon: <Users size={15} />, title: "People", value: "No seats", about: "Invite everyone; people chatting costs nothing. Effort and weekly suggestions help agents spend less." }, |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 530 | ]; |
| 531 | return ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 532 | <Card asChild tone="plain" className="grid gap-px overflow-hidden bg-line sm:grid-cols-2 lg:grid-cols-3"> |
| 533 | <ul> | |
| 534 | {rows.map((row) => ( | |
| 535 | <li key={row.title} className="bg-surface p-4"> | |
| 536 | <p className="flex items-center gap-2 text-xs text-muted"> | |
| 537 | <span className="text-faint">{row.icon}</span> | |
| 538 | {row.title} | |
| 539 | </p> | |
| 540 | <p className="mt-1 text-sm font-medium">{row.value}</p> | |
| 541 | <p className="mt-0.5 text-xs text-faint">{row.about}</p> | |
| 542 | </li> | |
| 543 | ))} | |
| 544 | </ul> | |
| 545 | </Card> | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 546 | ); |
| 547 | } | |
| 548 | ||
| 549 | // ── The top bar's pill ─────────────────────────────────────────────────── | |
| 550 | ||
| 551 | const SCOPE_KEY = "g1t_spend_pill"; | |
| 552 | ||
| 553 | function storedScope(): SpendScope { | |
| 554 | try { | |
| 555 | return window.localStorage.getItem(SCOPE_KEY) === "workspace" ? "workspace" : "me"; | |
| 556 | } catch { | |
| 557 | return "me"; | |
| 558 | } | |
| 559 | } | |
| 560 | ||
| 561 | /** | |
| The top bar's spend pill counts both tabs the same way, at price: You is what agents did for you, Workspace is everything the workspace used this month, the figure Home and Spend show, with what it was charged after its plan and credit underneath, against its spend limit; so a comped workspace reads as what it used and nothing charged, not the other way round. The spend guide says so. | 562 | * Your spend this month in the top bar: what agents did for you, at price, |
| 563 | * against your budget, or, for owners and billing managers who switch it, | |
| 564 | * everything the workspace used at price, with what it was charged against | |
| 565 | * its spend limit underneath. Both tabs count the same way, at price, so | |
| 566 | * the numbers add up across them. A click opens where it went. Read after | |
| 567 | * the page draws, so it adds nothing to the page's time. | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 568 | */ |
| 569 | export function SpendPill({ slug, mayWorkspace }: { slug: string; mayWorkspace: boolean }) { | |
| 570 | const pill = useFetcher<PillData>({ key: `spend-pill:${slug}` }); | |
| 571 | const [scope, setScope] = useState<SpendScope>("me"); | |
| 572 | const [open, setOpen] = useState(false); | |
| 573 | useEffect(() => { | |
| 574 | setScope(mayWorkspace ? storedScope() : "me"); | |
| 575 | pill.load(`/${slug}/-/spend/pill`); | |
| 576 | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 577 | }, [slug, mayWorkspace]); | |
| 578 | useEffect(() => { | |
| 579 | if (open) pill.load(`/${slug}/-/spend/pill`); | |
| 580 | // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 581 | }, [open]); | |
| 582 | const choose = (next: SpendScope) => { | |
| 583 | setScope(next); | |
| 584 | try { | |
| 585 | window.localStorage.setItem(SCOPE_KEY, next); | |
| 586 | } catch { | |
| 587 | // Kept for this page only. | |
| 588 | } | |
| 589 | }; | |
| 590 | const data = pill.data; | |
| 591 | const ws = scope === "workspace" ? data?.workspace : null; | |
| 592 | const spent = ws ? ws.spentMicros : (data?.me?.spentMicros ?? null); | |
| The top bar's spend pill counts both tabs the same way, at price: You is what agents did for you, Workspace is everything the workspace used this month, the figure Home and Spend show, with what it was charged after its plan and credit underneath, against its spend limit; so a comped workspace reads as what it used and nothing charged, not the other way round. The spend guide says so. | 593 | // The limit governs what is charged, so the meter measures that. |
| 594 | const against = ws ? ws.chargedMicros : (data?.me?.spentMicros ?? null); | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 595 | const budget = ws ? ws.limitMicros : (data?.me?.budgetMicros ?? null); |
| The top bar's spend pill counts both tabs the same way, at price: You is what agents did for you, Workspace is everything the workspace used this month, the figure Home and Spend show, with what it was charged after its plan and credit underneath, against its spend limit; so a comped workspace reads as what it used and nothing charged, not the other way round. The spend guide says so. | 596 | const share = against != null ? shareOfBudget(against, budget) : null; |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 597 | const label = spent == null ? (data ? "Spend" : "") : money(spent); |
| 598 | return ( | |
| 599 | <Popover open={open} onOpenChange={setOpen}> | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 600 | <PopoverTrigger asChild> |
| 601 | <Button | |
| 602 | variant="ghost" | |
| 603 | size="sm" | |
| 604 | aria-label={spent == null ? "Spend this month" : `${ws ? "The workspace's" : "Your"} spend this month: ${money(spent)}${budget != null ? ` of ${money(budget)}` : ""}`} | |
| 605 | className="gap-2 text-fg-soft tabular-nums" | |
| 606 | > | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 607 | {ws ? <Building2 size={14} className="shrink-0 text-faint" /> : <Coins size={14} className="shrink-0 text-faint" />} |
| 608 | {data ? <span>{label}</span> : <Skeleton className="h-3 w-10" />} | |
| 609 | {share != null && ( | |
| 610 | <span className="hidden w-9 lg:block"> | |
| The top bar's spend pill counts both tabs the same way, at price: You is what agents did for you, Workspace is everything the workspace used this month, the figure Home and Spend show, with what it was charged after its plan and credit underneath, against its spend limit; so a comped workspace reads as what it used and nothing charged, not the other way round. The spend guide says so. | 611 | <Meter spent={against!} cap={budget} label={ws ? "Charged of the spend limit" : "Spent of your budget"} size="sm" /> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 612 | </span> |
| 613 | )} | |
| The top of every page is one surface: the breadcrumb, search, spend, Ask g1t, bell and + share one height, corners and spacing, Ask g1t is a plain tinted button with the g1t mark drawn on whole pixels, a page's own header sits directly under the bar at the same left edge, and every section, conversations and open artifacts included, draws the same rounded panel. Tables written in chat render as tables, and the site's file, checkbox, radio, number and date controls are the design system's. Button, Separator, Breadcrumb, Table and FileDrop join the shared components; the chat guide says how tables work. | 614 | </Button> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 615 | </PopoverTrigger> |
| 616 | <PopoverContent align="end" className="w-80 p-0"> | |
| 617 | <SpendPopover slug={slug} data={data ?? null} scope={scope} mayWorkspace={mayWorkspace} onScope={choose} onClose={() => setOpen(false)} /> | |
| 618 | </PopoverContent> | |
| 619 | </Popover> | |
| 620 | ); | |
| 621 | } | |
| 622 | ||
| 623 | function SpendPopover({ | |
| 624 | slug, | |
| 625 | data, | |
| 626 | scope, | |
| 627 | mayWorkspace, | |
| 628 | onScope, | |
| 629 | onClose, | |
| 630 | }: { | |
| 631 | slug: string; | |
| 632 | data: PillData | null; | |
| 633 | scope: SpendScope; | |
| 634 | mayWorkspace: boolean; | |
| 635 | onScope: (scope: SpendScope) => void; | |
| 636 | onClose: () => void; | |
| 637 | }) { | |
| 638 | const month = data ? monthName(data.month).split(" ")[0] : ""; | |
| 639 | const ws = scope === "workspace" ? data?.workspace : null; | |
| 640 | return ( | |
| 641 | <div> | |
| 642 | <div className="flex items-center gap-1 border-b border-line p-1.5"> | |
| 643 | {(["me", "workspace"] as const).map((key) => | |
| 644 | key === "workspace" && !mayWorkspace ? ( | |
| 645 | <Hint key={key} label="The workspace's spend is for owners and billing managers."> | |
| 646 | <span tabIndex={0} className="flex h-7 grow cursor-default items-center justify-center rounded-md text-xs text-faint"> | |
| 647 | Workspace | |
| 648 | </span> | |
| 649 | </Hint> | |
| 650 | ) : ( | |
| 651 | <button | |
| 652 | key={key} | |
| 653 | type="button" | |
| 654 | aria-pressed={scope === key} | |
| 655 | onClick={() => onScope(key)} | |
| 656 | className={cn("h-7 grow rounded-md text-xs transition-colors", scope === key ? "bg-surface text-fg ring-1 ring-line-strong" : "text-muted hover:text-fg")} | |
| 657 | > | |
| 658 | {key === "me" ? "You" : "Workspace"} | |
| 659 | </button> | |
| 660 | ), | |
| 661 | )} | |
| 662 | </div> | |
| 663 | {!data ? ( | |
| 664 | <div className="space-y-2 p-4"> | |
| 665 | <Skeleton className="h-3 w-32" /> | |
| 666 | <Skeleton className="h-6 w-24" /> | |
| 667 | <Skeleton className="h-3 w-full" /> | |
| 668 | </div> | |
| 669 | ) : ws ? ( | |
| 670 | <div className="p-4"> | |
| The top bar's spend pill counts both tabs the same way, at price: You is what agents did for you, Workspace is everything the workspace used this month, the figure Home and Spend show, with what it was charged after its plan and credit underneath, against its spend limit; so a comped workspace reads as what it used and nothing charged, not the other way round. The spend guide says so. | 671 | <p className="text-xs text-muted">The workspace · {month}, everything it used, at price</p> |
| 672 | <p className="mt-1 text-xl font-semibold tabular-nums">{ws.spentMicros != null ? money(ws.spentMicros) : "—"}</p> | |
| 673 | {ws.chargedMicros != null && ( | |
| 674 | <p className="mt-1 text-xs text-muted"> | |
| 675 | Charged after its plan and credit: <span className="text-fg tabular-nums">{money(ws.chargedMicros)}</span> | |
| 676 | {ws.limitMicros != null ? ` of a ${money(ws.limitMicros)} spend limit` : " · no spend limit set"} | |
| 677 | </p> | |
| 678 | )} | |
| 679 | {ws.chargedMicros != null && ws.limitMicros != null && <Meter spent={ws.chargedMicros} cap={ws.limitMicros} label="Charged of the spend limit" size="sm" className="mt-2" />} | |
| 680 | <p className="mt-3 text-xs text-muted">Of that, agents{ws.agentsMicros != null ? `: ${money(ws.agentsMicros)}` : ""}</p> | |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 681 | <PopoverSlices slices={ws.byAgent} /> |
| 682 | </div> | |
| 683 | ) : data.me ? ( | |
| 684 | <div className="p-4"> | |
| The top bar's spend pill counts both tabs the same way, at price: You is what agents did for you, Workspace is everything the workspace used this month, the figure Home and Spend show, with what it was charged after its plan and credit underneath, against its spend limit; so a comped workspace reads as what it used and nothing charged, not the other way round. The spend guide says so. | 685 | <p className="text-xs text-muted">You · {month}, what agents did for you, at price</p> |
| The Spend page says what a workspace spent, where it went and the budgets that hold it, in Workspace under Money: spent at price, agents' spend and the spend limit; by day; by agent, person, channel, model, kind of work and product; budgets from the workspace down to each person, agent and task, with what happens at 100%; the costliest tasks, each with a receipt of every session at the provider's price and what was charged; and how it's priced. Everyone sees what agents spent for them, and owners and billing managers the whole workspace; each person can have a monthly budget agents keep to, and the spend pill in the top bar shows your month or the workspace's against its limit. The spend guide says how. | 686 | <p className="mt-1 text-xl font-semibold tabular-nums"> |
| 687 | {money(data.me.spentMicros)} | |
| 688 | {data.me.budgetMicros != null && <span className="ml-1.5 text-sm font-normal text-muted">of {money(data.me.budgetMicros)}</span>} | |
| 689 | </p> | |
| 690 | {data.me.budgetMicros != null ? ( | |
| 691 | <Meter spent={data.me.spentMicros} cap={data.me.budgetMicros} label="Spent of your budget" size="sm" className="mt-2" /> | |
| 692 | ) : ( | |
| 693 | <p className="mt-0.5 text-xs text-faint">No budget of your own; the workspace's limits apply.</p> | |
| 694 | )} | |
| 695 | <PopoverSlices slices={data.me.byKind} /> | |
| 696 | {data.me.byAgent.length > 0 && ( | |
| 697 | <p className="mt-2 truncate text-xs text-faint"> | |
| 698 | {data.me.byAgent | |
| 699 | .slice(0, 3) | |
| 700 | .map((s) => `${s.label.replace(/ \(@[^)]+\)$/, "")} ${money(s.micros)}`) | |
| 701 | .join(" · ")} | |
| 702 | </p> | |
| 703 | )} | |
| 704 | </div> | |
| 705 | ) : ( | |
| 706 | <p className="p-4 text-sm text-muted">Your spend couldn't be read right now.</p> | |
| 707 | )} | |
| 708 | <div className="border-t border-line p-1.5"> | |
| 709 | <Link | |
| 710 | to={`/${slug}/-/spend${scope === "workspace" ? "" : "?scope=me"}`} | |
| 711 | onClick={onClose} | |
| 712 | className="flex h-8 items-center justify-between rounded-md px-2.5 text-sm text-fg/90 hover:bg-surface hover:text-fg" | |
| 713 | > | |
| 714 | {scope === "workspace" ? "Open workspace spend" : "Your spend and budget"} | |
| 715 | <ArrowRight size={14} className="text-faint" /> | |
| 716 | </Link> | |
| 717 | </div> | |
| 718 | </div> | |
| 719 | ); | |
| 720 | } | |
| 721 | ||
| 722 | function PopoverSlices({ slices }: { slices: { key: string; label: string; micros: number }[] }) { | |
| 723 | const shown = slices.filter((s) => s.micros > 0).slice(0, 4); | |
| 724 | if (shown.length === 0) return <p className="mt-2 text-xs text-faint">Nothing spent yet this month.</p>; | |
| 725 | return ( | |
| 726 | <ul className="mt-2 space-y-1"> | |
| 727 | {shown.map((s) => ( | |
| 728 | <li key={s.key || "none"} className="flex items-center justify-between gap-3 text-sm"> | |
| 729 | <span className="min-w-0 truncate text-fg-soft">{s.label}</span> | |
| 730 | <span className="shrink-0 tabular-nums">{money(s.micros)}</span> | |
| 731 | </li> | |
| 732 | ))} | |
| 733 | </ul> | |
| 734 | ); | |
| 735 | } |