Skip to content
96 linesCodeBlameRaw
1import { data, useOutletContext } from "react-router";
2
3import type { AgentSpendBreakdown, WorkspaceAgent } from "@g1t/contracts";
4
5import type { Route } from "./+types/spend";
6import { readOrNull } from "../../../components/agents/actions.server";
7import { meterTone, monthName, shareOf } from "../../../components/agents/format";
8import { DailyBars, Meter, Panel, Quiet, SessionRow, SliceList } from "../../../components/agents/parts";
9import { requireUser, roleIn } from "../../../lib/session.server";
10import { workspaceAgents } from "../../../lib/services.server";
11import { money } from "../../../lib/usage";
12
13/** Where this agent's month went. */
14export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ spend: AgentSpendBreakdown | null }> {
15 const viewer = requireUser(context, request);
16 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
17 return { spend: await readOrNull(workspaceAgents.spend(params.owner.toLowerCase(), viewer, params.handle.toLowerCase())) };
18}
19
20/**
21 * Spend this month against the agent's budget: by day, by kind of work, by
22 * model, by who asked, and its costliest sessions. A session's helpers and
23 * subagents are paid here when this agent started it.
24 */
25export default function Spend({ loaderData, params }: Route.ComponentProps) {
26 const agent = useOutletContext<WorkspaceAgent>();
27 const { spend } = loaderData;
28 const spent = spend?.total_micros ?? agent.spent_month_micros ?? 0;
29 const cap = agent.budget.monthly_micros;
30 const share = shareOf(spent, cap);
31 const tone = meterTone(share);
32 return (
33 <div className="space-y-6">
34 <section className="rounded-2xl border border-line bg-surface p-6">
35 <p className="text-sm text-muted">Spent in {monthName(spend?.period ?? new Date().toISOString().slice(0, 7))}</p>
36 <p className="mt-1 flex flex-wrap items-baseline gap-x-2">
37 <span className="text-3xl font-semibold tracking-tight tabular-nums">{money(spent)}</span>
38 <span className="text-sm text-muted">{cap != null ? `of ${money(cap)}` : "No monthly budget of its own: the workspace's agent budget applies"}</span>
39 </p>
40 {cap != null && (
41 <>
42 <Meter spent={spent} cap={cap} label="Spent of its monthly budget" className="mt-4" />
43 <p className={`mt-2 text-xs ${tone === "danger" ? "text-danger" : tone === "warn" ? "text-warn" : "text-faint"}`}>
44 {Math.round((share ?? 0) * 100)}% used. At 100% it takes no new work until the 1st, or until an owner raises its budget on its profile.
45 </p>
46 </>
47 )}
48 <dl className="mt-6 grid grid-cols-2 gap-4 border-t border-line pt-5 sm:grid-cols-4">
49 <Stat label="Daily cap" value={agent.budget.daily_micros != null ? money(agent.budget.daily_micros) : "None"} />
50 <Stat label="Per session" value={agent.budget.task_micros != null ? money(agent.budget.task_micros) : "Workspace default"} />
51 <Stat label="While idle" value="Costs nothing" />
52 <Stat label="Rolls up to" value="The workspace's bill" />
53 </dl>
54 </section>
55
56 {!spend ? (
57 <Quiet title="The breakdown can't be shown right now">The agents service didn&apos;t answer. Reload in a moment.</Quiet>
58 ) : (
59 <>
60 <Panel title="By day" aside="UTC">
61 <DailyBars period={spend.period} days={spend.days} className="px-4 pt-4 pb-3" />
62 </Panel>
63 <div className="grid gap-4 lg:grid-cols-2">
64 <Panel title="By kind of work">
65 <SliceList slices={spend.by_kind} unit="runs" />
66 </Panel>
67 <Panel title="By model">
68 <SliceList slices={spend.by_model} unit="runs" />
69 </Panel>
70 <Panel title="By who asked" className="lg:col-span-2">
71 <SliceList slices={spend.by_person} unit="runs" />
72 </Panel>
73 </div>
74 {spend.top_sessions.length > 0 && (
75 <Panel title="Costliest sessions" aside="With everything they brought in">
76 <ul className="divide-y divide-line/60">
77 {spend.top_sessions.map((session) => (
78 <SessionRow key={session.id} slug={params.owner} session={session} showAgent={session.agent_id !== agent.id} />
79 ))}
80 </ul>
81 </Panel>
82 )}
83 </>
84 )}
85 </div>
86 );
87}
88
89function Stat({ label, value }: { label: string; value: string }) {
90 return (
91 <div>
92 <dt className="text-xs text-faint">{label}</dt>
93 <dd className="mt-0.5 text-sm tabular-nums">{value}</dd>
94 </div>
95 );
96}