Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

The workspace overview's ground truth is drawn in its own component

Its shaping moves to lib/ground.ts and its drawing to components/ground-truth.tsx, so the route only reads, and the parts can be drawn and looked at without the services behind them. Part of #157

G1t-Session:
ses_01m57128wre9bvvj1y4z4b8dk0
syntaqxcommitted Parentae80d0bBrowse files
3 files+310−2200/3 viewed
+213−0
1+/**
2+ * The workspace overview's ground truth (lib/ground.ts): the past week,
3+ * the quarter's spend threaded to the work beside what providers reported,
4+ * each team's last two weeks with what waits on a first review, and the
5+ * quiet changes in who works where.
6+ */
7+import { ArrowRight, Bot } from "lucide-react";
8+import type { ReactNode } from "react";
9+import { Link } from "react-router";
10+
11+import { COST_PROVIDERS, type CostProvider, isCostProvider } from "@g1t/contracts";
12+
13+import type { Ground } from "../lib/ground";
14+import { dollars } from "../lib/signals";
15+import { Boundary, Unavailable } from "./boundary";
16+import { Rhythm } from "./rhythm";
17+import { PulseSkeleton, PulseStrip } from "./signals";
18+import { SpendFlow } from "./spend-flow";
19+import { Skeleton } from "./ui/skeleton";
20+
21+export function GroundSkeleton() {
22+ return (
23+ <div className="space-y-6">
24+ <PulseSkeleton />
25+ <Skeleton className="h-112 w-full rounded-2xl" />
26+ </div>
27+ );
28+}
29+
30+/** A part of the ground truth: a mono eyebrow, a sentence for a heading, and what it shows. */
31+function Section({ id, eyebrow, title, hint, children }: { id: string; eyebrow: string; title: string; hint?: ReactNode; children: ReactNode }) {
32+ return (
33+ <section id={id} aria-labelledby={`${id}-title`} className="scroll-mt-6 rounded-2xl border border-line bg-surface px-5 py-6 md:px-8 md:py-7">
34+ <p className="font-mono text-[0.6875rem] tracking-[0.16em] text-accent uppercase">{eyebrow}</p>
35+ <h2 id={`${id}-title`} className="mt-2 font-display text-2xl leading-tight tracking-tight text-fg">
36+ {title}
37+ </h2>
38+ {hint && <p className="mt-1.5 max-w-3xl text-sm text-muted">{hint}</p>}
39+ <div className="mt-6">{children}</div>
40+ </section>
41+ );
42+}
43+
44+type QuietRow = Ground["silos"][number];
45+
46+function QuietList({ title, hint, rows, empty }: { title: string; hint: string; rows: QuietRow[]; empty: string }) {
47+ return (
48+ <div className="min-w-0">
49+ <h3 className="text-sm font-semibold text-fg">{title}</h3>
50+ <p className="mt-0.5 text-xs text-muted">{hint}</p>
51+ {rows.length === 0 ? (
52+ <p className="mt-3 text-sm text-faint">{empty}</p>
53+ ) : (
54+ <ul className="mt-3 space-y-3">
55+ {rows.map((row) => (
56+ <li key={row.key} className="text-sm">
57+ {row.to ? (
58+ <Link to={row.to} className="font-medium text-fg hover:text-accent">
59+ {row.where}
60+ </Link>
61+ ) : (
62+ <span className="font-medium text-fg">{row.where}</span>
63+ )}
64+ <p className="text-fg-soft">{row.line}</p>
65+ <p className="font-mono text-[0.6875rem] text-faint">{row.more}</p>
66+ </li>
67+ ))}
68+ </ul>
69+ )}
70+ </div>
71+ );
72+}
73+
74+export function GroundTruth({ slug, ground }: { slug: string; ground: Ground }) {
75+ return (
76+ <div className="space-y-6">
77+ <Boundary what="The past week">
78+ <PulseStrip tiles={ground.tiles} />
79+ </Boundary>
80+ {!ground.complete && <p className="text-xs text-faint">Some projects didn&apos;t answer, so what follows leaves them out.</p>}
81+ <Section
82+ id="spend"
83+ eyebrow="Spend, threaded to the work"
84+ title={
85+ ground.quarterSpend
86+ ? `${dollars(ground.quarterSpend)} of agent spend this quarter, and what it went into`
87+ : "This quarter's work, and how much of it agents did"
88+ }
89+ hint={
90+ ground.sized === "spend"
91+ ? `Each milestone, its work items, their change set in each project and its pull requests, sized by what agents cost on them${ground.sessionSpend > 0 ? ` (g1t's metered usage, and ${dollars(ground.sessionSpend)} of harness sessions estimated at list price)` : ""} and shaded by how much was an agent's. Choose a box to open what is under it.`
92+ : "Usage isn't known for this quarter, so each box is sized by its pull requests and shaded by how many were an agent's. Choose a box to open what is under it."
93+ }
94+ >
95+ <ProvidersLine slug={slug} providers={ground.providers} threaded={ground.quarterSpend} />
96+ <Boundary what="Spend, threaded to the work">
97+ <SpendFlow flow={ground.flow} sized={ground.sized} />
98+ </Boundary>
99+ {ground.capped && <p className="mt-3 text-xs text-faint">There were more pull requests this quarter than this reads, so the newest are shown.</p>}
100+ </Section>
101+ <Section
102+ id="rhythm"
103+ eyebrow="What's moving and what's waiting"
104+ title={ground.rhythm?.by === "project" ? "Each project's last two weeks" : "Each team's last two weeks"}
105+ hint="Pull requests and agent sessions above the line; reviews, comments and requests for changes below it. Each row is drawn to its own scale: point at a day for its numbers."
106+ >
107+ <div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_18rem]">
108+ <Boundary what="What's moving">
109+ {ground.rhythm ? <Rhythm rows={ground.rhythm.rows} by={ground.rhythm.by} /> : <Unavailable what="The teams" />}
110+ </Boundary>
111+ <div className="min-w-0">
112+ <h3 className="text-sm font-semibold text-fg">Waiting on a first review</h3>
113+ <p className="mt-0.5 text-xs text-muted">Open, and no one has reviewed them yet: the oldest first.</p>
114+ {ground.waiting.length === 0 ? (
115+ <p className="mt-3 text-sm text-faint">Nothing is waiting for a first review.</p>
116+ ) : (
117+ <ul className="mt-3 divide-y divide-line">
118+ {ground.waiting.map((pull) => (
119+ <li key={`${pull.repoId}#${pull.number}`} className="py-2.5 text-sm">
120+ <Link to={pull.to} className="flex items-start gap-2 hover:text-accent">
121+ <span className="shrink-0 pt-px font-mono text-xs text-muted">#{pull.number}</span>
122+ <span className="min-w-0 grow">
123+ <span className="line-clamp-2 font-medium text-fg">{pull.title}</span>
124+ <span className="font-mono text-[0.6875rem] text-faint">
125+ {pull.repo.name} · open {pull.age}
126+ {pull.blocks.length > 0 && ` · holds up ${pull.blocks.length}`}
127+ </span>
128+ </span>
129+ {pull.agent && <Bot size={13} className="mt-0.5 shrink-0 text-accent" aria-label="An agent's" />}
130+ </Link>
131+ </li>
132+ ))}
133+ </ul>
134+ )}
135+ <Link to={`/${slug}/-/overview?tab=needs#work`} className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
136+ The review queue <ArrowRight size={11} />
137+ </Link>
138+ </div>
139+ </div>
140+ </Section>
141+ <Section
142+ id="quiet"
143+ eyebrow="Quiet changes"
144+ title="Who works where, and what is spreading"
145+ hint="What no roll-up shows: knowledge resting with one person, people moving into new ground, and new ways of working taking hold. Each is a place to look, not a verdict."
146+ >
147+ <div className="grid gap-8 md:grid-cols-2 xl:grid-cols-4">
148+ <QuietList
149+ title="Knowledge silos"
150+ hint="Projects where one person has done most of the last 90 days' work"
151+ rows={ground.silos}
152+ empty="No project rests on one person right now."
153+ />
154+ <QuietList
155+ title="New ground"
156+ hint="People who merged to a project for the first time in the last two weeks"
157+ rows={ground.newGround}
158+ empty="No one has moved into a new project in the last two weeks."
159+ />
160+ <QuietList
161+ title="Spreading"
162+ hint="A harness more people took up this month, after someone started with it"
163+ rows={ground.spreading}
164+ empty="No harness has caught on in a team this month."
165+ />
166+ <QuietList
167+ title="New habits"
168+ hint="People whose first agent-made change merged in the last two weeks"
169+ rows={ground.habits}
170+ empty="No one new started merging agents' work in the last two weeks."
171+ />
172+ </div>
173+ </Section>
174+ </div>
175+ );
176+}
177+
178+const providerName = (provider: string) => (isCostProvider(provider) ? COST_PROVIDERS[provider as CostProvider].label : provider);
179+
180+/**
181+ * What the connected providers reported for the quarter, beside what g1t
182+ * threads to the work: their bill holds everything the organization ran,
183+ * so it is the larger figure, never added to the other.
184+ */
185+function ProvidersLine({ slug, providers, threaded }: { slug: string; providers: Ground["providers"]; threaded: number | null }) {
186+ if (!providers) {
187+ return (
188+ <p className="-mt-2 mb-5 text-sm text-muted">
189+ <Link to={`/${slug}/-/provider-costs`} className="text-accent hover:underline">
190+ Connect a provider&apos;s cost report
191+ </Link>{" "}
192+ to see everything your organization spent with Anthropic or OpenAI, not only what g1t can tie to the work.
193+ </p>
194+ );
195+ }
196+ return (
197+ <div className="-mt-2 mb-5 flex flex-wrap items-baseline gap-x-5 gap-y-1 rounded-xl border border-line bg-bg px-4 py-3 text-sm">
198+ <span className="text-fg">
199+ <span className="font-semibold">{dollars(providers.total)}</span> reported by your providers this quarter
200+ </span>
201+ <span className="font-mono text-xs text-muted">
202+ {providers.providers.map((row) => `${providerName(row.provider)} ${dollars(row.micros)}`).join(" · ")}
203+ </span>
204+ {threaded != null && threaded > 0 && providers.total > 0 && (
205+ <span className="text-xs text-muted">{Math.min(100, Math.round((threaded / providers.total) * 100))}% of it is threaded to the work below</span>
206+ )}
207+ {!providers.complete && <span className="text-xs text-faint">Some of the quarter isn&apos;t read yet</span>}
208+ <Link to={`/${slug}/-/provider-costs`} className="ml-auto text-xs text-muted hover:text-fg">
209+ Provider costs
210+ </Link>
211+ </div>
212+ );
213+}
+90−0
1+/**
2+ * The workspace overview's ground truth, shaped from one reading of the
3+ * work (lib/signals.ts) for components/ground-truth.tsx: the pulse, spend
4+ * threaded to the work, each team's last two weeks, what waits on a first
5+ * review, and the quiet changes, each list in sentences. Imports only types
6+ * and pure functions, so the page's parts can be drawn and tested alone.
7+ */
8+import type { SignalData } from "./signals.server";
9+import {
10+ type Group,
11+ dailySignal,
12+ dayList,
13+ harnessWords,
14+ namesWords,
15+ newGround,
16+ newHabits,
17+ providerQuarter,
18+ quarterSpend,
19+ rhythm,
20+ silos,
21+ spendFlow,
22+ spoken,
23+ spreading,
24+ waitingRows,
25+} from "./signals";
26+
27+/** Entries each quiet change lists. */
28+export const QUIET_ROWS = 4;
29+
30+/** What the rhythm's rows are: teams by their projects, else the projects. Null when identity did not answer. */
31+export type Groups = { groups: Group[]; by: "team" | "project" } | null;
32+
33+export function shapeGround(found: SignalData, groups: Groups, now: number) {
34+ const name = (id: string) => found.repos[id]?.name ?? "a project";
35+ const path = (id: string) => (found.repos[id] ? `/${found.repos[id]!.namespace}/${found.repos[id]!.name}` : null);
36+ const quarter = quarterSpend({ repos: found.repos, signals: found.signals, quarterUsage: found.quarterUsage, quarter: found.range.quarter });
37+ // Practices catching on in each team (else across the workspace), the newest first.
38+ const scopes =
39+ groups?.by === "team"
40+ ? groups.groups.map((group) => ({ key: group.key, label: group.label, only: new Set(group.repoIds) as ReadonlySet<string> | undefined }))
41+ : [{ key: "all", label: "the workspace", only: undefined }];
42+ const spreads = scopes.flatMap((scope) => spreading(found.signals.harnesses, now, { only: scope.only }).map((spread) => ({ scope, spread })));
43+ return {
44+ tiles: dailySignal(found).tiles,
45+ complete: found.complete,
46+ capped: found.signals.pullsCapped,
47+ flow: spendFlow({ slug: found.slug, repos: found.repos, signals: found.signals, quarterUsage: found.quarterUsage, quarter: found.range.quarter }),
48+ sized: (quarter?.total ?? 0) > 0 ? ("spend" as const) : ("pulls" as const),
49+ quarterSpend: quarter?.total ?? null,
50+ sessionSpend: quarter?.sessions ?? 0,
51+ rhythm: groups ? { by: groups.by, rows: rhythm(found.signals.days, dayList(found.range.until, found.range.days), groups.groups) } : null,
52+ waiting: waitingRows(found.signals.waiting, found.repos).map((pull) => ({ ...pull, age: spoken(Math.max(0, (now - Date.parse(pull.createdAt)) / 1000)) })),
53+ silos: silos(found.signals.authors, now)
54+ .slice(0, QUIET_ROWS)
55+ .map((silo) => ({
56+ key: silo.repoId,
57+ to: path(silo.repoId),
58+ where: name(silo.repoId),
59+ line: `${silo.name} merged ${silo.theirs} of its ${silo.recent} changes in 90 days`,
60+ more: silo.joining.length > 0 ? `${silo.joining.map((who) => who.name).join(", ")} started this month` : "No one else has started there",
61+ })),
62+ newGround: newGround(found.signals.authors, now)
63+ .slice(0, QUIET_ROWS)
64+ .map((row) => ({
65+ key: `${row.ownerId}:${row.repoId}`,
66+ to: path(row.repoId),
67+ where: name(row.repoId),
68+ line: `${row.name} merged a first change here`,
69+ more: `${row.elsewhere} merges elsewhere first`,
70+ })),
71+ spreading: spreads.slice(0, QUIET_ROWS).map(({ scope, spread }) => ({
72+ key: `${scope.key}:${spread.harness}`,
73+ to: null,
74+ where: `${harnessWords(spread.harness)} in ${scope.label}`,
75+ line: `${namesWords(spread.recent.map((person) => person.name))} started using it this month, after ${spread.first.name}`,
76+ more: `${spread.people} people use it now`,
77+ })),
78+ habits: newHabits(found.signals.pulls, now)
79+ .slice(0, QUIET_ROWS)
80+ .map((row) => ({
81+ key: row.ownerId,
82+ to: path(row.repoId),
83+ where: name(row.repoId),
84+ line: `${row.name} merged a first agent-made change`,
85+ more: `after ${row.byHand} by hand`,
86+ })),
87+ };
88+}
89+
90+export type Ground = ReturnType<typeof shapeGround> & { providers: ReturnType<typeof providerQuarter> };
+7−220
1−import { ArrowRight, Bot, Box, ShieldCheck, Users } from "lucide-react";
1+import { ArrowRight, Box, ShieldCheck, Users } from "lucide-react";
22 import { type ReactNode, Suspense } from "react";
33 import { Await, Link, data } from "react-router";
44
5−import { COST_PROVIDERS, type CostProvider, type User, hasCodeAccess, isCostProvider } from "@g1t/contracts";
5+import { type User, hasCodeAccess } from "@g1t/contracts";
66
77 import type { Route } from "./+types/workspace-overview";
88
1717 import { Avatar } from "../../components/ui/avatar";
1818 import { Badge } from "../../components/ui/badge";
1919 import { Card } from "../../components/ui/card";
20−import { Skeleton } from "../../components/ui/skeleton";
21−import { Boundary, Unavailable } from "../../components/boundary";
22−import { Rhythm } from "../../components/rhythm";
23−import { PulseSkeleton, PulseStrip } from "../../components/signals";
24−import { SpendFlow } from "../../components/spend-flow";
20+import { Unavailable } from "../../components/boundary";
21+import { GroundSkeleton, GroundTruth } from "../../components/ground-truth";
2522 import { isFailed, part } from "../../lib/partial";
2623 import { loadRepos } from "../../lib/home.server";
27−import { dailySignal, dayList, dollars, harnessWords, namesWords, newGround, newHabits, providerQuarter, quarterSpend, signalsRange, rhythm, silos, spendFlow, spoken, spreading, waitingRows } from "../../lib/signals";
28−import { type SignalData, loadGroups, loadSignalData } from "../../lib/signals.server";
24+import { shapeGround } from "../../lib/ground";
25+import { providerQuarter, signalsRange } from "../../lib/signals";
26+import { loadGroups, loadSignalData } from "../../lib/signals.server";
2927
3028 export function meta({ params, ...args }: Route.MetaArgs) {
3129 return page(args, { title: `Workspace · ${params.owner} · g1t` });
3331
3432 /** Most members listed before "and N more". */
3533 const MAX_FACES = 10;
36−/** Entries each quiet change lists. */
37−const QUIET_ROWS = 4;
3834
3935 /**
4036 * The workspace's ground truth, from what its work says (lib/signals.ts),
8682 .catch(() => null),
8783 ]);
8884 return found ? { ...shapeGround(found, groups, now), providers: providerQuarter(providers, found.range.quarter) } : null;
89−}
90−
91−function shapeGround(found: SignalData, groups: Awaited<ReturnType<typeof loadGroups>>, now: number) {
92− const name = (id: string) => found.repos[id]?.name ?? "a project";
93− const path = (id: string) => (found.repos[id] ? `/${found.repos[id]!.namespace}/${found.repos[id]!.name}` : null);
94− const quarter = quarterSpend({ repos: found.repos, signals: found.signals, quarterUsage: found.quarterUsage, quarter: found.range.quarter });
95− // Practices catching on in each team (else across the workspace), the newest first.
96− const scopes = groups?.by === "team" ? groups.groups.map((group) => ({ key: group.key, label: group.label, only: new Set(group.repoIds) as ReadonlySet<string> | undefined })) : [{ key: "all", label: "the workspace", only: undefined }];
97− const spreads = scopes.flatMap((scope) => spreading(found.signals.harnesses, now, { only: scope.only }).map((spread) => ({ scope, spread })));
98− return {
99− tiles: dailySignal(found).tiles,
100− complete: found.complete,
101− capped: found.signals.pullsCapped,
102− flow: spendFlow({ slug: found.slug, repos: found.repos, signals: found.signals, quarterUsage: found.quarterUsage, quarter: found.range.quarter }),
103− sized: (quarter?.total ?? 0) > 0 ? ("spend" as const) : ("pulls" as const),
104− quarterSpend: quarter?.total ?? null,
105− sessionSpend: quarter?.sessions ?? 0,
106− rhythm: groups ? { by: groups.by, rows: rhythm(found.signals.days, dayList(found.range.until, found.range.days), groups.groups) } : null,
107− waiting: waitingRows(found.signals.waiting, found.repos).map((pull) => ({ ...pull, age: spoken(Math.max(0, (now - Date.parse(pull.createdAt)) / 1000)) })),
108− silos: silos(found.signals.authors, now)
109− .slice(0, QUIET_ROWS)
110− .map((silo) => ({
111− key: silo.repoId,
112− to: path(silo.repoId),
113− where: name(silo.repoId),
114− line: `${silo.name} merged ${silo.theirs} of its ${silo.recent} changes in 90 days`,
115− more: silo.joining.length > 0 ? `${silo.joining.map((who) => who.name).join(", ")} started this month` : "No one else has started there",
116− })),
117− newGround: newGround(found.signals.authors, now)
118− .slice(0, QUIET_ROWS)
119− .map((row) => ({ key: `${row.ownerId}:${row.repoId}`, to: path(row.repoId), where: name(row.repoId), line: `${row.name} merged a first change here`, more: `${row.elsewhere} merges elsewhere first` })),
120− spreading: spreads.slice(0, QUIET_ROWS).map(({ scope, spread }) => ({
121− key: `${scope.key}:${spread.harness}`,
122− to: null,
123− where: `${harnessWords(spread.harness)} in ${scope.label}`,
124− line: `${namesWords(spread.recent.map((person) => person.name))} started using it this month, after ${spread.first.name}`,
125− more: `${spread.people} people use it now`,
126− })),
127− habits: newHabits(found.signals.pulls, now)
128− .slice(0, QUIET_ROWS)
129− .map((row) => ({ key: row.ownerId, to: path(row.repoId), where: name(row.repoId), line: `${row.name} merged a first agent-made change`, more: `after ${row.byHand} by hand` })),
130− };
13185 }
13286
133−type Ground = ReturnType<typeof shapeGround> & { providers: ReturnType<typeof providerQuarter> };
13487
13588 function Tile({ icon, label, value, to, action }: { icon: ReactNode; label: string; value: string; to: string; action: string }) {
13689 return (
236189 </section>
237190 </Card>
238191 </div>
239− </div>
240− );
241−}
242−
243−function GroundSkeleton() {
244− return (
245− <div className="space-y-6">
246− <PulseSkeleton />
247− <Skeleton className="h-112 w-full rounded-2xl" />
248− </div>
249− );
250−}
251−
252−/** A part of the ground truth: a mono eyebrow, a sentence for a heading, and what it shows. */
253−function Section({ id, eyebrow, title, hint, children }: { id: string; eyebrow: string; title: string; hint?: ReactNode; children: ReactNode }) {
254− return (
255− <section id={id} aria-labelledby={`${id}-title`} className="scroll-mt-6 rounded-2xl border border-line bg-surface px-5 py-6 md:px-8 md:py-7">
256− <p className="font-mono text-[0.6875rem] tracking-[0.16em] text-accent uppercase">{eyebrow}</p>
257− <h2 id={`${id}-title`} className="mt-2 font-display text-2xl leading-tight tracking-tight text-fg">
258− {title}
259− </h2>
260− {hint && <p className="mt-1.5 max-w-3xl text-sm text-muted">{hint}</p>}
261− <div className="mt-6">{children}</div>
262− </section>
263− );
264−}
265−
266−type QuietRow = Ground["silos"][number];
267−
268−function QuietList({ title, hint, rows, empty }: { title: string; hint: string; rows: QuietRow[]; empty: string }) {
269− return (
270− <div className="min-w-0">
271− <h3 className="text-sm font-semibold text-fg">{title}</h3>
272− <p className="mt-0.5 text-xs text-muted">{hint}</p>
273− {rows.length === 0 ? (
274− <p className="mt-3 text-sm text-faint">{empty}</p>
275− ) : (
276− <ul className="mt-3 space-y-3">
277− {rows.map((row) => (
278− <li key={row.key} className="text-sm">
279− {row.to ? (
280− <Link to={row.to} className="font-medium text-fg hover:text-accent">
281− {row.where}
282− </Link>
283− ) : (
284− <span className="font-medium text-fg">{row.where}</span>
285− )}
286− <p className="text-fg-soft">{row.line}</p>
287− <p className="font-mono text-[0.6875rem] text-faint">{row.more}</p>
288− </li>
289− ))}
290− </ul>
291− )}
292− </div>
293− );
294−}
295−
296−function GroundTruth({ slug, ground }: { slug: string; ground: Ground }) {
297− return (
298− <div className="space-y-6">
299− <Boundary what="The past week">
300− <PulseStrip tiles={ground.tiles} />
301− </Boundary>
302− {!ground.complete && <p className="text-xs text-faint">Some projects didn&apos;t answer, so what follows leaves them out.</p>}
303− <Section
304− id="spend"
305− eyebrow="Spend, threaded to the work"
306− title={ground.quarterSpend ? `${dollars(ground.quarterSpend)} of agent spend this quarter, and what it went into` : "This quarter's work, and how much of it agents did"}
307− hint={
308− ground.sized === "spend"
309− ? `Each milestone, its work items, their change set in each project and its pull requests, sized by what agents cost on them${ground.sessionSpend > 0 ? ` (g1t's metered usage, and ${dollars(ground.sessionSpend)} of harness sessions estimated at list price)` : ""} and shaded by how much was an agent's. Choose a box to open what is under it.`
310− : "Usage isn't known for this quarter, so each box is sized by its pull requests and shaded by how many were an agent's. Choose a box to open what is under it."
311− }
312− >
313− <ProvidersLine slug={slug} providers={ground.providers} threaded={ground.quarterSpend} />
314− <Boundary what="Spend, threaded to the work">
315− <SpendFlow flow={ground.flow} sized={ground.sized} />
316− </Boundary>
317− {ground.capped && <p className="mt-3 text-xs text-faint">There were more pull requests this quarter than this reads, so the newest are shown.</p>}
318− </Section>
319− <Section
320− id="rhythm"
321− eyebrow="What's moving and what's waiting"
322− title={ground.rhythm?.by === "project" ? "Each project's last two weeks" : "Each team's last two weeks"}
323− hint="Pull requests and agent sessions above the line; reviews, comments and requests for changes below it. Each row is drawn to its own scale: point at a day for its numbers."
324− >
325− <div className="grid gap-8 xl:grid-cols-[minmax(0,1fr)_18rem]">
326− <Boundary what="What's moving">{ground.rhythm ? <Rhythm rows={ground.rhythm.rows} by={ground.rhythm.by} /> : <Unavailable what="The teams" />}</Boundary>
327− <div className="min-w-0">
328− <h3 className="text-sm font-semibold text-fg">Waiting on a first review</h3>
329− <p className="mt-0.5 text-xs text-muted">Open, and no one has reviewed them yet: the oldest first.</p>
330− {ground.waiting.length === 0 ? (
331− <p className="mt-3 text-sm text-faint">Nothing is waiting for a first review.</p>
332− ) : (
333− <ul className="mt-3 divide-y divide-line">
334− {ground.waiting.map((pull) => (
335− <li key={`${pull.repoId}#${pull.number}`} className="py-2.5 text-sm">
336− <Link to={pull.to} className="flex items-start gap-2 hover:text-accent">
337− <span className="shrink-0 pt-px font-mono text-xs text-muted">#{pull.number}</span>
338− <span className="min-w-0 grow">
339− <span className="line-clamp-2 font-medium text-fg">{pull.title}</span>
340− <span className="font-mono text-[0.6875rem] text-faint">
341− {pull.repo.name} · open {pull.age}
342− {pull.blocks.length > 0 && ` · holds up ${pull.blocks.length}`}
343− </span>
344− </span>
345− {pull.agent && <Bot size={13} className="mt-0.5 shrink-0 text-accent" aria-label="An agent's" />}
346− </Link>
347− </li>
348− ))}
349− </ul>
350− )}
351− <Link to={`/${slug}/-/overview?tab=needs#work`} className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
352− The review queue <ArrowRight size={11} />
353− </Link>
354− </div>
355− </div>
356− </Section>
357− <Section
358− id="quiet"
359− eyebrow="Quiet changes"
360− title="Who works where, and what is spreading"
361− hint="What no roll-up shows: knowledge resting with one person, people moving into new ground, and new ways of working taking hold. Each is a place to look, not a verdict."
362− >
363− <div className="grid gap-8 md:grid-cols-2 xl:grid-cols-4">
364− <QuietList title="Knowledge silos" hint="Projects where one person has done most of the last 90 days' work" rows={ground.silos} empty="No project rests on one person right now." />
365− <QuietList title="New ground" hint="People who merged to a project for the first time in the last two weeks" rows={ground.newGround} empty="No one has moved into a new project in the last two weeks." />
366− <QuietList title="Spreading" hint="A harness more people took up this month, after someone started with it" rows={ground.spreading} empty="No harness has caught on in a team this month." />
367− <QuietList title="New habits" hint="People whose first agent-made change merged in the last two weeks" rows={ground.habits} empty="No one new started merging agents' work in the last two weeks." />
368− </div>
369− </Section>
370− </div>
371− );
372−}
373−
374−const providerName = (provider: string) => (isCostProvider(provider) ? COST_PROVIDERS[provider as CostProvider].label : provider);
375−
376−/**
377− * What the connected providers reported for the quarter, beside what g1t
378− * threads to the work: their bill holds everything the organization ran,
379− * so it is the larger figure, never added to the other.
380− */
381−function ProvidersLine({ slug, providers, threaded }: { slug: string; providers: Ground["providers"]; threaded: number | null }) {
382− if (!providers) {
383− return (
384− <p className="-mt-2 mb-5 text-sm text-muted">
385− <Link to={`/${slug}/-/provider-costs`} className="text-accent hover:underline">
386− Connect a provider&apos;s cost report
387− </Link>{" "}
388− to see everything your organization spent with Anthropic or OpenAI, not only what g1t can tie to the work.
389− </p>
390− );
391− }
392− return (
393− <div className="-mt-2 mb-5 flex flex-wrap items-baseline gap-x-5 gap-y-1 rounded-xl border border-line bg-bg px-4 py-3 text-sm">
394− <span className="text-fg">
395− <span className="font-semibold">{dollars(providers.total)}</span> reported by your providers this quarter
396− </span>
397− <span className="font-mono text-xs text-muted">{providers.providers.map((row) => `${providerName(row.provider)} ${dollars(row.micros)}`).join(" · ")}</span>
398− {threaded != null && threaded > 0 && providers.total > 0 && (
399− <span className="text-xs text-muted">{Math.min(100, Math.round((threaded / providers.total) * 100))}% of it is threaded to the work below</span>
400− )}
401− {!providers.complete && <span className="text-xs text-faint">Some of the quarter isn&apos;t read yet</span>}
402− <Link to={`/${slug}/-/provider-costs`} className="ml-auto text-xs text-muted hover:text-fg">
403− Provider costs
404− </Link>
405192 </div>
406193 );
407194 }

Comments

Sign in to comment.