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
3 files+310−2200/3 viewed
| 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'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'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'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 | + | } |
| 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> }; |
| 1 | − | import { ArrowRight, Bot, Box, ShieldCheck, Users } from "lucide-react"; | |
| 1 | + | import { ArrowRight, Box, ShieldCheck, Users } from "lucide-react"; | |
| 2 | 2 | import { type ReactNode, Suspense } from "react"; | |
| 3 | 3 | import { Await, Link, data } from "react-router"; | |
| 4 | 4 | ||
| 5 | − | import { COST_PROVIDERS, type CostProvider, type User, hasCodeAccess, isCostProvider } from "@g1t/contracts"; | |
| 5 | + | import { type User, hasCodeAccess } from "@g1t/contracts"; | |
| 6 | 6 | ||
| 7 | 7 | import type { Route } from "./+types/workspace-overview"; | |
| 8 | 8 | ||
| ⋯ | |||
| 17 | 17 | import { Avatar } from "../../components/ui/avatar"; | |
| 18 | 18 | import { Badge } from "../../components/ui/badge"; | |
| 19 | 19 | 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"; | |
| 25 | 22 | import { isFailed, part } from "../../lib/partial"; | |
| 26 | 23 | 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"; | |
| 29 | 27 | ||
| 30 | 28 | export function meta({ params, ...args }: Route.MetaArgs) { | |
| 31 | 29 | return page(args, { title: `Workspace · ${params.owner} · g1t` }); | |
| ⋯ | |||
| 33 | 31 | ||
| 34 | 32 | /** Most members listed before "and N more". */ | |
| 35 | 33 | const MAX_FACES = 10; | |
| 36 | − | /** Entries each quiet change lists. */ | |
| 37 | − | const QUIET_ROWS = 4; | |
| 38 | 34 | ||
| 39 | 35 | /** | |
| 40 | 36 | * The workspace's ground truth, from what its work says (lib/signals.ts), | |
| ⋯ | |||
| 86 | 82 | .catch(() => null), | |
| 87 | 83 | ]); | |
| 88 | 84 | 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 | − | }; | |
| 131 | 85 | } | |
| 132 | 86 | ||
| 133 | − | type Ground = ReturnType<typeof shapeGround> & { providers: ReturnType<typeof providerQuarter> }; | |
| 134 | 87 | ||
| 135 | 88 | function Tile({ icon, label, value, to, action }: { icon: ReactNode; label: string; value: string; to: string; action: string }) { | |
| 136 | 89 | return ( | |
| ⋯ | |||
| 236 | 189 | </section> | |
| 237 | 190 | </Card> | |
| 238 | 191 | </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'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'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'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> | |
| 405 | 192 | </div> | |
| 406 | 193 | ); | |
| 407 | 194 | } | |
Comments
Sign in to comment.