g1t/apps/web/app/components/mission-control.tsx

1,115 lines46,298 bytesCodeBlame

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.

Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1import { ArrowDownWideNarrow, ArrowRight, ArrowUpRight, Check, ChevronDown, ChevronRight, LoaderCircle, Plus } from "lucide-react";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2import { type ReactNode, useEffect, useState } from "react";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms3import { Link, useFetcher, useRouteLoaderData, useSearchParams } from "react-router";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look4
5import { trialClosed } from "../lib/trial";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms6import { type ActivityGroup, type Verb, greetingFor, isAgent } from "../lib/mission";
7import {
8 BLOCKING,
9 type Fact,
10 type LandedRow,
11 type NeedRow,
12 type QuickAction,
13 REASON_LABEL,
14 type Reason,
15 type Sort,
16 type Tab,
17 type WaitingRow,
18 type Week,
19 type Who,
20 change,
21 dateLine,
22 parseSort,
23 parseTab,
24 signedPercent,
25 sortRows,
26 usd,
27 whyFor,
28} from "../lib/mission-control";
29import { cn } from "../lib/cn";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30import { AgentSetup } from "./agent-setup";
31import type { ShellData } from "./shell";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms32import { useLiveRefresh } from "./agents";
33import { Unavailable } from "./mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look34import { Avatar, TimeAgo } from "./ui";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms35import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look36import type { Loaded } from "../routes/home";
37
38/*
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms39 * Mission control: the home page of someone signed in. It answers where
40 * the viewer is needed, what agents are doing, and what landed without
41 * them. It is a chunk of its own, loaded only for them, so the signed-out
42 * home page does not carry it. routes/home.tsx loads its data;
43 * lib/mission-control.ts shapes it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44 */
45
46/** The viewer's time zone, set by the page itself, so the greeting fits their day. */
47const TZ_COOKIE = "g1t_tz";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms48/** Rows a tab shows before "See all". */
49const ROWS = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50
51const dollars = (micros: number) => `$${(Math.max(0, micros) / 1_000_000).toFixed(2)}`;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms52const plural = (n: number, one: string, many = `${one}s`) => `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms54// --- Small pieces -----------------------------------------------------------------
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look55
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms56const CHIP_TONE: Record<Reason, string> = {
57 blocking: "border-danger/35 bg-danger/10 text-danger",
58 checks_failing: "border-danger/35 bg-danger/10 text-danger",
59 outside_guardrails: "border-warn/35 bg-warn/10 text-warn",
60 stalled: "border-warn/35 bg-warn/10 text-warn",
61 asked_for_you: "border-merged/35 bg-merged/10 text-merged",
62 needs_review: "border-info/35 bg-info/10 text-info",
63 ready_to_merge: "border-accent/35 bg-accent/10 text-accent",
64};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look65
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms66function Chip({ children, tone }: { children: ReactNode; tone: string }) {
67 return (
68 <span
69 className={cn(
70 "inline-flex shrink-0 items-center rounded border px-1.5 py-px font-mono text-[0.625rem] font-medium tracking-wider whitespace-nowrap uppercase",
71 tone,
72 )}
73 >
74 {children}
75 </span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms77}
78
79/** A person or an agent, by name, wearing the mark that tells them apart. */
80function Person({ who, size = 16, className }: { who: Who; size?: number; className?: string }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms82 <span className={cn("inline-flex min-w-0 items-center gap-1.5", className)}>
83 <Avatar name={who.name} size={size} />
84 <span className="truncate">{who.name}</span>
85 </span>
86 );
87}
88
89function Eyebrow({ children }: { children: ReactNode }) {
90 return <p className="font-mono text-[0.625rem] font-medium tracking-[0.12em] text-faint uppercase">{children}</p>;
91}
92
93const FACT_TONE = { good: "text-accent", warn: "text-warn", bad: "text-danger" } as const;
94
95function Facts({ facts }: { facts: Fact[] }) {
96 if (facts.length === 0) return <p className="text-sm text-muted">Nothing more is known about it yet.</p>;
97 return (
98 <dl className="grid grid-cols-2 gap-x-4 gap-y-2.5">
99 {facts.map((fact) => (
100 <div key={fact.label} className="min-w-0">
101 <dt className="text-xs text-faint">{fact.label}</dt>
102 <dd className={cn("mt-0.5 truncate text-sm font-medium tabular-nums", fact.tone ? FACT_TONE[fact.tone] : "text-fg-soft")}>
103 {fact.value}
104 </dd>
105 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look106 ))}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms107 </dl>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look108 );
109}
110
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms111/** A form that acts on a pull request from here, saying so before the page catches up. */
112function QuickForm({ quick, variant = "quiet" }: { quick: QuickAction; variant?: "quiet" | "accent" }) {
113 const fetcher = useFetcher<{ error?: string } | null>();
114 const [sent, setSent] = useState(false);
115 const busy = fetcher.state !== "idle";
116 const done = sent && fetcher.state === "idle" && !fetcher.data?.error;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look117 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms118 <fetcher.Form method="post" action={quick.to} onSubmit={() => setSent(true)} className="contents">
119 {Object.entries(quick.fields).map(([name, value]) => (
120 <input key={name} type="hidden" name={name} value={value} />
121 ))}
122 <button
123 type="submit"
124 disabled={busy || done}
125 className={cn(
126 "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-default",
127 variant === "accent"
128 ? "bg-merged text-bg hover:bg-[#c9bfff]"
129 : "border border-line-strong text-fg/90 hover:bg-raised hover:text-fg",
130 done && "border-accent/40 text-accent",
131 )}
132 >
133 {busy ? <LoaderCircle size={14} className="animate-spin" /> : done ? <Check size={14} /> : null}
134 {done ? quick.done : quick.label}
135 </button>
136 {fetcher.data?.error && <span className="basis-full text-xs text-danger">{fetcher.data.error}</span>}
137 </fetcher.Form>
138 );
139}
140
141function ProjectMark({ repo }: { repo: { namespace: string; name: string } | null }) {
142 return <Avatar name={repo ? repo.name : "g1t"} size={32} square />;
143}
144
145/** A row that opens to what is known about it; the first one starts open. */
146function Row({
147 open,
148 repo,
149 by,
150 title,
151 sub,
152 chip,
153 at,
154 children,
155}: {
156 open: boolean;
157 repo: { namespace: string; name: string } | null;
158 by: Who | null;
159 title: ReactNode;
160 sub: ReactNode;
161 chip: ReactNode;
162 at: number;
163 children: ReactNode;
164}) {
165 return (
166 <li>
167 <details open={open} className="group">
168 <summary className="flex cursor-pointer list-none items-center gap-3 px-4 py-3 transition-colors select-none hover:bg-raised/60 sm:px-5 [&::-webkit-details-marker]:hidden">
169 <ProjectMark repo={repo} />
170 <span className="min-w-0 grow">
171 <span className="block truncate text-sm">{title}</span>
172 <span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted">
173 <span className="shrink-0 font-medium text-fg-soft">{repo ? repo.name : "Workspace"}</span>
174 {by && (
175 <>
176 <span className="text-faint">·</span>
177 <Person who={by} size={14} className={cn("shrink-0", by.agent ? "text-merged" : "")} />
178 </>
179 )}
180 {sub && (
181 <>
182 <span className="text-faint">—</span>
183 <span className="truncate">{sub}</span>
184 </>
185 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look186 </span>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms187 </span>
188 <span className="hidden sm:inline-flex">{chip}</span>
189 <span className="hidden w-16 shrink-0 text-right text-xs text-faint sm:block">
190 <TimeAgo at={at} />
191 </span>
192 <ChevronDown size={16} className="shrink-0 text-faint transition-transform group-open:rotate-180" />
193 </summary>
194 <div className="px-4 pb-4 sm:px-5 sm:pb-5">
195 <div className="mb-3 flex items-center gap-2 sm:hidden">
196 {chip}
197 <span className="text-xs text-faint">
198 <TimeAgo at={at} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look199 </span>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms200 </div>
201 {children}
202 </div>
203 </details>
204 </li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look205 );
206}
207
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms208function NeedCard({ row, first }: { row: NeedRow; first: boolean }) {
209 const isPull = row.to.includes("/pull/");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look210 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms211 <Row
212 open={first}
213 repo={row.repo}
214 by={row.by}
215 title={
216 <>
217 <span className="font-medium">{row.title}</span>
218 {row.ref && <span className="ml-1 font-mono text-xs text-faint">{row.ref}</span>}
219 </>
220 }
221 sub={row.ask}
222 chip={<Chip tone={CHIP_TONE[row.reason]}>{REASON_LABEL[row.reason]}</Chip>}
223 at={row.at}
224 >
225 <div className="grid gap-4 rounded-xl border border-line bg-bg/50 p-4 md:grid-cols-2 md:gap-x-6 2xl:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)_minmax(0,0.95fr)]">
226 <div className="min-w-0">
227 <Eyebrow>The ask</Eyebrow>
228 <p className="mt-2 text-sm leading-6 text-fg-soft">{row.ask}</p>
229 <p className="mt-2 text-xs leading-5 text-muted">
230 <Link to={row.to} className="font-medium text-fg hover:underline">
231 {row.repo ? `${row.repo.name}${row.ref ?? ""}` : row.title}
232 </Link>
233 {row.for && (
234 <>
235 {" "}
236 · started for <span className="text-fg-soft">{row.for}</span>
237 </>
238 )}
239 </p>
240 </div>
241 <div className="min-w-0">
242 <Eyebrow>What the agent already knows</Eyebrow>
243 <div className="mt-2">
244 <Facts facts={row.facts} />
245 </div>
246 </div>
247 <div className="rounded-lg border border-warn/20 bg-warn/[0.06] p-3.5 md:col-span-2 2xl:col-span-1">
248 <p className="text-xs font-semibold text-warn">Why this needs you</p>
249 <p className="mt-1.5 text-sm leading-6 text-fg-soft">{row.why}</p>
250 <p className="mt-2 text-xs text-faint">
251 Waiting <TimeAgo at={row.at} />
252 </p>
253 </div>
254 </div>
255 <div className="mt-3 flex flex-wrap items-center gap-2">
256 <Link
257 to={row.to}
258 prefetch="intent"
259 className="inline-flex items-center gap-1.5 rounded-md bg-fg px-3 py-1.5 text-sm font-medium text-bg transition-colors hover:bg-white"
260 >
261 Review and respond <ArrowUpRight size={14} />
262 </Link>
263 {row.quick && <QuickForm quick={row.quick} />}
264 {row.link && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look265 <Link
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms266 to={row.link.to}
267 className="inline-flex items-center rounded-md border border-line-strong px-3 py-1.5 text-sm font-medium text-fg/90 hover:bg-raised hover:text-fg"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look268 >
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms269 {row.link.label}
270 </Link>
271 )}
272 {row.open !== row.to && (
273 <Link to={row.open} prefetch="intent" className="px-2 py-1.5 text-sm text-muted hover:text-fg">
274 {row.open.includes("/issues/") ? "Open issue" : isPull ? "See the changes" : "Open pull request"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look275 </Link>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms276 )}
277 </div>
278 </Row>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look279 );
280}
281
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms282function WaitingCard({ row, first }: { row: WaitingRow; first: boolean }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look283 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms284 <Row
285 open={first}
286 repo={row.repo}
287 by={row.by}
288 title={
289 <>
290 <span className="font-medium">{row.title}</span>
291 {row.ref && <span className="ml-1 font-mono text-xs text-faint">{row.ref}</span>}
292 </>
293 }
294 sub={row.detail}
295 chip={
296 <Chip tone="border-line-strong bg-raised text-muted">
297 {row.live && <span className="mr-1.5 size-1.5 animate-pulse rounded-full bg-accent" />}
298 {row.chip}
299 </Chip>
300 }
301 at={row.at}
302 >
303 <div className="grid gap-4 rounded-xl border border-line bg-bg/50 p-4 md:grid-cols-2 md:gap-6">
304 <div className="min-w-0">
305 <Eyebrow>{row.live ? "Doing now" : "Where it stands"}</Eyebrow>
306 <p className="mt-2 text-sm leading-6 text-fg-soft">{row.detail}</p>
307 </div>
308 <div className="min-w-0">
309 <Eyebrow>What is known</Eyebrow>
310 <div className="mt-2">
311 <Facts facts={row.facts} />
312 </div>
313 </div>
314 </div>
315 <div className="mt-3 flex flex-wrap gap-2">
316 <Link
317 to={row.to}
318 prefetch="intent"
319 className="inline-flex items-center gap-1.5 rounded-md border border-line-strong px-3 py-1.5 text-sm font-medium text-fg/90 hover:bg-raised hover:text-fg"
320 >
321 {row.to.includes("/runs/") ? "Watch the run" : "Open pull request"} <ArrowUpRight size={14} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 </Link>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms323 {row.run && row.run !== row.to && (
324 <Link to={row.run} prefetch="intent" className="px-2 py-1.5 text-sm text-muted hover:text-fg">
325 Watch the run
326 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look327 )}
328 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms329 </Row>
330 );
331}
332
333function LandedCard({ row, first }: { row: LandedRow; first: boolean }) {
334 return (
335 <Row
336 open={first}
337 repo={row.repo}
338 by={row.byAgents ? { name: row.agent, agent: isAgent(row.agent) } : row.by}
339 title={
340 <>
341 <span className="font-medium">{row.title}</span>
342 <span className="ml-1 font-mono text-xs text-faint">{row.ref}</span>
343 </>
344 }
345 sub={row.byAgents ? "landed without a person" : `merged by ${row.by?.name ?? "a person"}`}
346 chip={
347 row.byAgents ? (
348 <Chip tone="border-merged/35 bg-merged/10 text-merged">By agents</Chip>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look349 ) : (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms350 <Chip tone="border-warn/35 bg-warn/10 text-warn">Needed a person</Chip>
351 )
352 }
353 at={row.at}
354 >
355 <div className="grid gap-4 rounded-xl border border-line bg-bg/50 p-4 md:grid-cols-2 md:gap-6">
356 <div className="min-w-0">
357 <Eyebrow>How it landed</Eyebrow>
358 <p className="mt-2 text-sm leading-6 text-fg-soft">
359 {row.byAgents
360 ? `g1t merged it once everything the repository asks for was met, with no one pressing merge.`
361 : `${row.by?.name ?? "A person"} merged it.`}
362 {row.agent && isAgent(row.agent) && ` ${row.agent} made the change.`}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look363 </p>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms364 </div>
365 <div className="min-w-0">
366 <Eyebrow>What changed</Eyebrow>
367 <div className="mt-2">
368 <Facts facts={row.facts} />
369 </div>
370 </div>
371 </div>
372 <div className="mt-3">
373 <Link
374 to={row.to}
375 prefetch="intent"
376 className="inline-flex items-center gap-1.5 rounded-md border border-line-strong px-3 py-1.5 text-sm font-medium text-fg/90 hover:bg-raised hover:text-fg"
377 >
378 Open pull request <ArrowUpRight size={14} />
379 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look380 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms381 </Row>
382 );
383}
384
385function List({ children }: { children: ReactNode }) {
386 return <ul className="divide-y divide-line">{children}</ul>;
387}
388
389function Empty({ children, action }: { children: ReactNode; action?: ReactNode }) {
390 return (
391 <div className="px-5 py-10 text-center">
392 <div className="mx-auto max-w-md text-sm leading-6 text-muted">{children}</div>
393 {action && <div className="mt-4 flex flex-wrap justify-center gap-2">{action}</div>}
394 </div>
395 );
396}
397
398// --- The stat strip -----------------------------------------------------------------
399
400function Stat({ label, value, hint, dot, title }: { label: string; value: string; hint: ReactNode; dot?: string; title?: string }) {
401 return (
402 <div className="min-w-0 bg-surface px-4 py-3.5 sm:px-5 sm:py-4" title={title}>
403 <p className="flex items-center gap-1.5 truncate text-xs text-muted">
404 {dot && <span className={cn("size-1.5 shrink-0 rounded-full", dot)} />}
405 {label}
406 </p>
407 <p className="mt-1 font-display text-2xl font-semibold tracking-tight tabular-nums sm:text-[1.75rem]">{value}</p>
408 <p className="mt-0.5 truncate text-xs text-faint">{hint}</p>
409 </div>
410 );
411}
412
413// --- This week ----------------------------------------------------------------------
414
415/** Seven days of landed changes, each split into what agents landed alone and what a person merged. */
416function WeekChart({ week }: { week: Week }) {
417 const max = Math.max(1, ...week.days.map((d) => d.agents + d.people));
418 const height = 112;
419 const delta = change(week.total, week.previous);
420 return (
421 <div>
422 <div className="flex items-end justify-between gap-3">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look423 <div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms424 <p className="font-display text-3xl font-semibold tracking-tight tabular-nums">{week.total.toLocaleString("en-US")}</p>
425 <p className="text-xs text-muted">changes landed in 7 days</p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look426 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms427 {delta != null ? (
428 <span
429 className={cn(
430 "rounded-full px-2 py-0.5 text-xs font-medium tabular-nums ring-1",
431 delta >= 0 ? "text-accent ring-accent/30" : "text-warn ring-warn/30",
432 )}
433 >
434 {signedPercent(delta)} vs last week
435 </span>
436 ) : week.previous === 0 && week.total > 0 ? (
437 <span className="text-xs text-faint">none the week before</span>
438 ) : null}
439 </div>
440 {week.total === 0 ? (
441 <p className="mt-4 rounded-lg border border-dashed border-line px-4 py-6 text-center text-sm leading-6 text-muted">
442 Nothing landed in the last 7 days. Each change that does shows here, by day, split by whether a person had to merge it.
443 </p>
444 ) : (
445 <div className="relative mt-5" style={{ height: height + 20 }}>
446 <div className="absolute inset-x-0 border-t border-line" style={{ top: height }} />
447 <div className="absolute inset-x-0 top-0 flex items-end justify-between gap-1" style={{ height }}>
448 {week.days.map((day, index) => {
449 const total = day.agents + day.people;
450 const agentsH = Math.round((day.agents / max) * (height - 4));
451 const peopleH = Math.round((day.people / max) * (height - 4));
452 const today = index === week.days.length - 1;
453 return (
454 <div key={day.key} className="group relative flex h-full flex-1 flex-col items-center justify-end">
455 {total > 0 && (
456 <span
457 className={cn(
458 "mb-1 text-[0.625rem] tabular-nums",
459 today ? "text-fg-soft" : "text-faint opacity-0 group-hover:opacity-100",
460 )}
461 >
462 {total}
463 </span>
464 )}
465 <div className="flex w-full max-w-6 flex-col items-stretch gap-[2px]">
466 {day.people > 0 && <span className="block rounded-t bg-warn" style={{ height: Math.max(3, peopleH) }} />}
467 {day.agents > 0 && (
468 <span className={cn("block bg-merged", day.people > 0 ? "" : "rounded-t")} style={{ height: Math.max(3, agentsH) }} />
469 )}
470 {total === 0 && <span className="block h-[2px] rounded-full bg-line-strong" />}
471 </div>
472 <div className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 hidden -translate-x-1/2 rounded-md border border-line-strong bg-raised px-2.5 py-1.5 text-xs whitespace-nowrap shadow-lg shadow-black/40 group-hover:block">
473 <p className="font-medium text-fg">{day.label}</p>
474 <p className="mt-0.5 flex items-center gap-1.5 text-muted">
475 <span className="size-1.5 rounded-full bg-merged" /> {day.agents} by agents
476 </p>
477 <p className="flex items-center gap-1.5 text-muted">
478 <span className="size-1.5 rounded-full bg-warn" /> {day.people} needed a person
479 </p>
480 </div>
481 </div>
482 );
483 })}
484 </div>
485 <div className="absolute inset-x-0 bottom-0 flex justify-between gap-1">
486 {week.days.map((day, index) => (
487 <span
488 key={day.key}
489 className={cn(
490 "flex-1 text-center text-[0.6875rem]",
491 index === week.days.length - 1 ? "font-medium text-fg-soft" : "text-faint",
492 )}
493 >
494 {day.label}
495 </span>
496 ))}
497 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look498 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms499 )}
500 <div className={cn("mt-3 flex-wrap gap-x-4 gap-y-1 text-xs text-muted", week.total === 0 ? "hidden" : "flex")}>
501 <span className="inline-flex items-center gap-1.5">
502 <span className="size-2 rounded-sm bg-merged" /> Landed by agents
503 </span>
504 <span className="inline-flex items-center gap-1.5">
505 <span className="size-2 rounded-sm bg-warn" /> Needed a person
506 </span>
507 </div>
508 <table className="sr-only">
509 <caption>Changes landed each day</caption>
510 <thead>
511 <tr>
512 <th>Day</th>
513 <th>Landed by agents</th>
514 <th>Needed a person</th>
515 </tr>
516 </thead>
517 <tbody>
518 {week.days.map((day) => (
519 <tr key={day.key}>
520 <td>{day.key}</td>
521 <td>{day.agents}</td>
522 <td>{day.people}</td>
523 </tr>
524 ))}
525 </tbody>
526 </table>
527 </div>
528 );
529}
530
531// --- Activity -----------------------------------------------------------------------
532
533const VERB: Record<Verb, string> = {
534 landed: "landed",
535 opened_issue: "opened",
536 closed_issue: "closed",
537 started: "started on",
538 ready: "marked ready",
539 checks_passed: "checks passed on",
540 checks_failed: "checks failed on",
541 approved: "approved",
542 changes_requested: "asked for changes on",
543 commented: "commented on",
544 asked: "was asked about",
545 deployed: "deployed production",
546 deploy_failed: "production build failed",
547 learned: "learned",
548};
549
550/** One line of the feed: who, what, and the one thing it was about, by name. */
551function FeedLine({ group, titles }: { group: ActivityGroup; titles: Record<string, string> }) {
552 const part = group.parts[0];
553 const base = `/${group.repo.namespace}/${group.repo.name}`;
554 const actor = group.actor ?? "g1t";
555 const agent = group.actor == null || isAgent(group.actor);
556 const number = part.numbers[0];
557 const title = number != null ? titles[`${group.repo.namespace}/${group.repo.name}#${number}`.toLowerCase()] : undefined;
558 const more = group.parts.length - 1 + Math.max(0, part.numbers.length - 1);
559 return (
560 <li className="flex gap-2.5 py-2">
561 <span className="mt-px">
562 <Avatar name={actor} size={22} />
563 </span>
564 <span className="min-w-0 grow text-[0.8125rem] leading-5 text-muted">
565 <span className={cn("font-medium", agent ? "text-merged" : "text-fg")}>{actor}</span> {VERB[part.verb]}{" "}
566 {part.verb === "learned" ? (
567 <Link to={part.to ?? `${base}/memory`} className="text-fg-soft hover:text-fg">
568 “{part.texts[0]}”
569 </Link>
570 ) : number != null ? (
571 <Link to={`${base}/issues/${number}`} prefetch="intent" className="text-fg-soft hover:text-fg">
572 <span className="font-mono text-xs">#{number}</span>
573 {title && <> {title}</>}
574 </Link>
575 ) : part.to ? (
576 <Link to={part.to} className="text-fg-soft hover:text-fg">
577 {group.repo.name}
578 </Link>
579 ) : null}
580 {more > 0 && <span className="text-faint"> and {more} more</span>}
581 <span className="block text-xs text-faint">
582 <span className="font-mono">{group.repo.name}</span> · <TimeAgo at={group.at} />
583 </span>
584 </span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look585 </li>
586 );
587}
588
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms589// --- Get started --------------------------------------------------------------------
590
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look591type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
592
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms593/** The first things to do, ticked off as they are done, until work has been handed to agents. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look594function GetStarted({ steps }: { steps: Step[] }) {
595 const left = steps.filter((step) => !step.done).length;
596 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms597 <section className="rounded-xl border border-line bg-surface p-5">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look598 <div className="flex items-baseline justify-between gap-3">
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms599 <h2 className="text-base font-semibold tracking-tight">Get started</h2>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look600 <span className="text-xs text-muted">
601 {steps.length - left} of {steps.length} done
602 </span>
603 </div>
604 <ol className="mt-4 space-y-2">
605 {steps.map((step, index) => (
606 <li
607 key={step.title}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms608 className={cn("flex items-start gap-3 rounded-lg px-3 py-2.5", step.done ? "" : "bg-bg/50 ring-1 ring-line")}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look609 >
610 <span
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms611 className={cn(
612 "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium",
613 step.done ? "bg-accent text-bg" : "text-muted ring-1 ring-line-strong",
614 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look615 >
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms616 {step.done ? <Check size={12} /> : index + 1}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look617 </span>
618 <span className="min-w-0 grow">
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms619 <span className={cn("block text-sm font-medium", step.done && "text-muted line-through decoration-faint")}>{step.title}</span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look620 {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
621 </span>
622 {!step.done && step.to && (
623 <Link
624 to={step.to}
625 className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-white"
626 >
627 {step.action}
628 </Link>
629 )}
630 </li>
631 ))}
632 </ol>
633 </section>
634 );
635}
636
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms637// --- The page -----------------------------------------------------------------------
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look638
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms639const TAB_LABEL: Record<Tab, string> = { needs: "Needs you", waiting: "Waiting on agents", landed: "Landed today" };
640const TAB_SHORT: Record<Tab, string> = { needs: "Needs you", waiting: "Waiting", landed: "Today" };
641const SORT_LABEL: Record<Sort, string> = { impact: "By impact", newest: "Newest" };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look642
643export default function MissionControl({ loaderData }: { loaderData: Loaded }) {
644 const shell = useRouteLoaderData("root")?.shell as ShellData | null | undefined;
645 const loaded: Loaded = loaderData;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms646 const [params] = useSearchParams();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look647 // Agents are at work, so the page changes without anyone touching it.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms648 useLiveRefresh(loaded.changing);
649 const [greeting, setGreeting] = useState(loaded.greeting);
650 const [date, setDate] = useState(loaded.date);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look651 useEffect(() => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms652 // The greeting and date follow the viewer's own clock, and the server
653 // learns their zone for next time.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look654 setGreeting(greetingFor(new Date().getHours()));
655 try {
656 const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms657 setDate(dateLine(Date.now(), zone ?? null));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look658 if (zone) document.cookie = `${TZ_COOKIE}=${encodeURIComponent(zone)}; Path=/; Max-Age=31536000; SameSite=Lax`;
659 } catch {
660 // Nothing to remember.
661 }
662 }, []);
663
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms664 const { viewer, repos, waiting, landed, liveTotal, week, groups, titles, stats, canRunAgents, trial } = loaded;
665 const workspace = shell?.workspace?.slug ?? loaded.workspace ?? viewer.workspaces?.[0]?.slug ?? null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look666
667 // The usage limit, as the sidebar knows it, heads the list when it bites.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms668 const limit = shell?.limit && !shell.limit.comped && shell.limit.state !== "ok" && workspace ? shell.limit : null;
669 const limitRow: NeedRow[] = limit
670 ? [
671 (() => {
672 const detail =
673 limit.state === "stopped"
674 ? "No new agent starts until the limit is raised or a card is added."
675 : `${dollars(limit.exposureMicros)} of ${dollars(limit.ceilingMicros ?? 0)} used. Add a card so work keeps going.`;
676 return {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look677 key: "limit",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms678 reason: "blocking" as const,
679 repo: null,
680 ref: null,
681 title: limit.state === "stopped" ? "Agents are stopped: the usage limit was reached" : "Usage is nearing its limit",
682 ask: detail,
683 by: null,
684 for: null,
685 at: Date.now(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look686 to: `/${workspace}/-/billing`,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms687 open: `/${workspace}/-/billing`,
688 facts: [
689 { label: "Used", value: `${dollars(limit.exposureMicros)} of ${dollars(limit.ceilingMicros ?? 0)}`, tone: "warn" as const },
690 ],
691 why: whyFor("blocking", { kind: "limit", detail }),
692 quick: null,
693 link: null,
694 };
695 })(),
696 ]
697 : [];
698 const needs = [...limitRow, ...loaded.needs];
699 const blocking = needs.filter((row) => BLOCKING.has(row.reason)).length;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look700
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms701 const counts: Record<Tab, number> = { needs: needs.length, waiting: waiting.length, landed: landed.length };
702 const tab = parseTab(params.get("tab")) ?? "needs";
703 const sort = parseSort(params.get("sort"));
704 const all = params.get("all") === "1";
705 const everyActivity = params.get("activity") === "all";
706 const link = (changes: Record<string, string | null>) => {
707 const next = new URLSearchParams(params);
708 for (const [key, value] of Object.entries(changes)) {
709 if (value == null) next.delete(key);
710 else next.set(key, value);
711 }
712 const text = next.toString();
713 return text ? `?${text}` : "/";
714 };
715
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look716 const steps: Step[] = [
717 {
718 done: Boolean(workspace),
719 title: "Create a workspace",
720 about: "Projects, people and agent credit live in one.",
721 to: "/workspaces/new",
722 action: "Create",
723 },
724 {
725 done: repos.length > 0,
726 title: "Add a project",
727 about: "Create a repository, or import one by its address. Push to it with git as usual.",
728 to: workspace ? `/new?workspace=${workspace}` : "/new",
729 action: "Add",
730 },
731 trial?.open
732 ? {
733 done: true,
734 title: "Try g1t's agents free",
735 about: trial.granted
736 ? `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its ${dollars(trial.limitMicros)} trial credit left, for g1t's models and sandboxes.`
737 : `This workspace gets ${dollars(trial.limitMicros)} of trial credit, for g1t's models and sandboxes, the first time its agents work.`,
738 to: workspace ? `/${workspace}/-/integrations` : null,
739 action: "Connect",
740 }
741 : canRunAgents
742 ? {
743 done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
744 title: "Keep work running",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms745 about:
746 "Usage is charged after it runs. Add a card under Billing, so g1t charges it as you near your limit instead of stopping work.",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look747 to: workspace ? `/${workspace}/-/billing` : null,
748 action: "Billing",
749 }
750 : {
751 done: false,
752 title: "Connect a model",
753 about: `${
754 trialClosed(trial, workspace ?? "This workspace") ? `${trialClosed(trial, workspace ?? "This workspace")} ` : ""
755 }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
756 to: workspace ? `/${workspace}/-/integrations` : null,
757 action: "Connect",
758 },
759 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms760 done: loaded.handedOff,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look761 title: "Hand off an outcome",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms762 about:
763 "Open an issue and assign it to g1t-agent, or write an outcome on a project's Plans page and let a planner split it into issues.",
764 to: repos[0] ? `/${repos[0].namespace}/${repos[0].name}/plans` : null,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look765 action: "Write one",
766 },
767 ];
768 const starting = steps.some((step) => !step.done);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms769 const noProjects = repos.length === 0;
770
771 const rows = tab === "needs" ? sortRows(needs, sort) : tab === "waiting" ? sortRows(waiting, sort) : sortRows(landed, sort);
772 const shown = all ? rows : rows.slice(0, ROWS);
773 const share = week.total > 0 ? week.byAgents / week.total : null;
774 const delta = change(week.total, week.previous);
775 const feed = everyActivity ? groups : groups.slice(0, 8);
776
777 const newIssue =
778 repos.length > 0 ? (
779 <DropdownMenu>
780 <DropdownMenuTrigger className="inline-flex items-center gap-1.5 rounded-md border border-line-strong px-3 py-2 text-sm font-medium text-fg/90 transition-colors hover:bg-raised hover:text-fg">
781 <Plus size={14} /> New issue
782 </DropdownMenuTrigger>
783 <DropdownMenuContent align="end" className="max-h-80 overflow-y-auto">
784 <DropdownMenuLabel>In which project?</DropdownMenuLabel>
785 {repos.map((repo) => (
786 <DropdownMenuItem key={repo.name} asChild>
787 <Link to={`/${repo.namespace}/${repo.name}/issues/new`}>
788 <Avatar name={repo.name} size={18} square />
789 {repo.name}
790 </Link>
791 </DropdownMenuItem>
792 ))}
793 </DropdownMenuContent>
794 </DropdownMenu>
795 ) : (
796 <Link
797 to={workspace ? `/new?workspace=${workspace}` : "/new"}
798 className="inline-flex items-center gap-1.5 rounded-md border border-line-strong px-3 py-2 text-sm font-medium text-fg/90 hover:bg-raised hover:text-fg"
799 >
800 <Plus size={14} /> New project
801 </Link>
802 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look803
804 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms805 <main className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
806 <header className="flex flex-wrap items-end justify-between gap-x-6 gap-y-4">
807 <div className="min-w-0">
808 <h1 className="text-[1.75rem] leading-tight font-semibold tracking-tight sm:text-[2rem]" suppressHydrationWarning>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look809 {greeting}, {loaded.name}
810 </h1>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms811 <p className="mt-1.5 text-sm text-muted" suppressHydrationWarning>
812 <span className="text-fg-soft">{date}</span> · {loaded.summary}
813 </p>
814 </div>
815 <div className="flex flex-wrap items-center gap-2">
816 {newIssue}
817 {needs.length > 0 && (
818 <Link
819 to={`${link({ tab: "needs", all: null })}#work`}
820 preventScrollReset
821 className="inline-flex items-center gap-1.5 rounded-md bg-merged px-3.5 py-2 text-sm font-semibold text-bg transition-colors hover:bg-[#c9bfff]"
822 >
823 Review {needs.length} that need{needs.length === 1 ? "s" : ""} you <ArrowRight size={14} />
824 </Link>
825 )}
826 </div>
827 </header>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look828
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms829 {starting && <GetStarted steps={steps} />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look830
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms831 <section
832 aria-label="At a glance"
833 className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5"
834 >
835 <Stat
836 label="Projects"
837 value={stats.projects == null ? "—" : String(stats.projects)}
838 hint={
839 stats.projectsThisMonth == null
840 ? "in this workspace"
841 : stats.projectsThisMonth > 0
842 ? `+${stats.projectsThisMonth} this month`
843 : "none new this month"
844 }
845 />
846 <Stat
847 label="Agents"
848 value={loaded.runsLoaded || !workspace ? String(liveTotal) : "—"}
849 dot={liveTotal > 0 ? "bg-accent animate-pulse" : undefined}
850 hint={
851 liveTotal > 0
852 ? `live now · ${stats.agentHours < 10 ? stats.agentHours.toFixed(1) : Math.round(stats.agentHours)}h this week`
853 : stats.agentHours > 0
854 ? `none live · ${stats.agentHours < 10 ? stats.agentHours.toFixed(1) : Math.round(stats.agentHours)}h this week`
855 : "none live now"
856 }
857 />
858 <Stat
859 label="Changes this week"
860 value={loaded.perRepoLoaded ? String(week.total) : "—"}
861 hint={
862 delta != null ? `${signedPercent(delta)} vs last week` : week.previous === 0 ? "none the week before" : "merged pull requests"
863 }
864 />
865 <Stat
866 label="Landed without you"
867 dot="bg-merged"
868 value={share == null ? "—" : `${Math.round(share * 100)}%`}
869 hint={share == null ? "nothing landed yet" : `${week.byAgents} of ${week.total}`}
870 title="Merged by g1t, by auto-merge or the merge queue, with no person pressing merge."
871 />
872 <div className="col-span-2 lg:col-span-1">
873 <Stat
874 label="Need you"
875 dot="bg-warn"
876 value={String(needs.length)}
877 hint={blocking > 0 ? `${blocking} blocking` : needs.length > 0 ? "nothing blocking" : "all clear"}
878 />
879 </div>
880 </section>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look881
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms882 <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_21rem] xl:grid-cols-[minmax(0,1fr)_23rem]">
883 <section id="work" className="min-w-0 scroll-mt-20 self-start overflow-hidden rounded-xl border border-line bg-surface">
884 <div className="flex items-center gap-2 border-b border-line px-2 sm:px-3">
885 <nav className="-mb-px flex min-w-0 grow gap-1 overflow-x-auto [scrollbar-width:none]" aria-label="Mission control">
886 {(["needs", "waiting", "landed"] as const).map((value) => (
887 <Link
888 key={value}
889 to={link({ tab: value, all: null })}
890 preventScrollReset
891 aria-current={tab === value ? "page" : undefined}
892 className={cn(
893 "flex shrink-0 items-center gap-1.5 border-b-2 px-2 py-3 text-sm whitespace-nowrap transition-colors sm:px-2.5",
894 tab === value ? "border-merged font-medium text-fg" : "border-transparent text-muted hover:text-fg",
895 )}
896 >
897 <span className="sm:hidden">{TAB_SHORT[value]}</span>
898 <span className="hidden sm:inline">{TAB_LABEL[value]}</span>
899 <span
900 className={cn(
901 "rounded-full px-1.5 text-[0.6875rem] tabular-nums",
902 tab === value && value === "needs" && counts.needs > 0 ? "bg-warn/15 text-warn" : "bg-line text-muted",
903 )}
904 >
905 {counts[value]}
906 </span>
907 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look908 ))}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms909 </nav>
910 <DropdownMenu>
911 <DropdownMenuTrigger
912 className="inline-flex shrink-0 items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-muted hover:bg-raised hover:text-fg"
913 aria-label="Sort"
914 >
915 <ArrowDownWideNarrow size={14} />
916 <span className="hidden sm:inline">{SORT_LABEL[sort]}</span>
917 </DropdownMenuTrigger>
918 <DropdownMenuContent align="end">
919 {(["impact", "newest"] as const).map((value) => (
920 <DropdownMenuItem key={value} asChild>
921 <Link to={link({ sort: value === "impact" ? null : value })} preventScrollReset>
922 {sort === value ? <Check /> : <span className="size-4" />}
923 {SORT_LABEL[value]}
924 </Link>
925 </DropdownMenuItem>
926 ))}
927 </DropdownMenuContent>
928 </DropdownMenu>
929 </div>
930
931 {!loaded.perRepoLoaded && tab !== "waiting" ? (
932 <div className="p-4">
933 <Unavailable what={tab === "needs" ? "Some of what needs you" : "What landed"} />
934 </div>
935 ) : null}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look936
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms937 {tab === "needs" &&
938 (needs.length === 0 ? (
939 noProjects ? (
940 <Empty
941 action={
942 <Link
943 to={workspace ? `/new?workspace=${workspace}` : "/new"}
944 className="rounded-md bg-fg px-3 py-1.5 text-sm font-medium text-bg hover:bg-white"
945 >
946 Create or import a project
947 </Link>
948 }
949 >
950 <p className="font-medium text-fg">No projects yet</p>
951 <p className="mt-1">
952 Create a repository or import one. Assign its issues to g1t-agent, and what needs you shows up here.
953 </p>
954 </Empty>
955 ) : (
956 <>
957 <Empty>
958 <p className="font-medium text-fg">Nothing needs you</p>
959 <p className="mt-1">
960 Agents are handling everything.{" "}
961 {landed.length > 0
962 ? "Here's what they landed today."
963 : "Reviews, stopped work and failed deploys show up here first."}
964 </p>
965 </Empty>
966 {landed.length > 0 && (
967 <div className="border-t border-line">
968 <List>
969 {landed.slice(0, ROWS).map((row) => (
970 <LandedCard key={row.key} row={row} first={false} />
971 ))}
972 </List>
973 </div>
974 )}
975 </>
976 )
977 ) : (
978 <List>
979 {(shown as NeedRow[]).map((row, index) => (
980 <NeedCard key={row.key} row={row} first={index === 0} />
981 ))}
982 </List>
983 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look984
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms985 {tab === "waiting" &&
986 (!loaded.runsLoaded && workspace ? (
987 <div className="p-4">
988 <Unavailable what="Agent runs" />
989 </div>
990 ) : waiting.length === 0 ? (
991 <Empty>
992 <p className="font-medium text-fg">No agent is at work right now</p>
993 <p className="mt-1">Assign an issue to g1t-agent and one starts on it in seconds. Its run shows here while it works.</p>
994 </Empty>
995 ) : (
996 <List>
997 {(shown as WaitingRow[]).map((row, index) => (
998 <WaitingCard key={row.key} row={row} first={index === 0} />
999 ))}
1000 </List>
1001 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1002
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1003 {tab === "landed" &&
1004 loaded.perRepoLoaded &&
1005 (landed.length === 0 ? (
1006 <Empty>
1007 <p className="font-medium text-fg">Nothing has landed today yet</p>
1008 <p className="mt-1">
1009 {week.total > 0
1010 ? `${plural(week.total, "change")} landed in the last 7 days.`
1011 : "Merged pull requests show up here the moment they land."}
1012 </p>
1013 </Empty>
1014 ) : (
1015 <List>
1016 {(shown as LandedRow[]).map((row, index) => (
1017 <LandedCard key={row.key} row={row} first={index === 0} />
1018 ))}
1019 </List>
1020 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1021
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1022 {rows.length > ROWS && (
1023 <div className="flex items-center justify-between border-t border-line px-4 py-2.5 text-xs text-muted sm:px-5">
1024 <span>
1025 Showing {shown.length} of {rows.length}
1026 </span>
1027 <Link
1028 to={link({ all: all ? null : "1" })}
1029 preventScrollReset
1030 className="inline-flex items-center gap-1 font-medium text-fg-soft hover:text-fg"
1031 >
1032 {all ? "Show fewer" : "See all"} <ChevronRight size={12} />
1033 </Link>
1034 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1035 )}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1036 </section>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1037
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1038 <aside className="min-w-0 space-y-6">
1039 <section className="rounded-xl border border-line bg-surface p-5">
1040 <div className="flex items-center justify-between">
1041 <h2 className="text-base font-semibold tracking-tight">This week</h2>
1042 {workspace && (
1043 <Link to={`/${workspace}/-/agents`} className="text-xs text-muted hover:text-fg">
1044 Fleet
1045 </Link>
1046 )}
1047 </div>
1048 <div className="mt-4">{loaded.perRepoLoaded ? <WeekChart week={week} /> : <Unavailable what="The week" />}</div>
1049 {workspace && stats.weekCost > 0 && (
1050 <Link
1051 to={`/${workspace}/-/usage`}
1052 className="mt-4 flex items-center justify-between border-t border-line pt-3 text-xs text-muted hover:text-fg"
1053 >
1054 <span>Agents and sandboxes cost {usd(stats.weekCost)} this week</span>
1055 <ChevronRight size={12} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1056 </Link>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1057 )}
1058 </section>
1059
1060 <section id="activity" className="scroll-mt-20 rounded-xl border border-line bg-surface p-5">
1061 <div className="flex items-center justify-between">
1062 <h2 className="text-base font-semibold tracking-tight">Activity</h2>
1063 {groups.length > 8 && (
1064 <Link
1065 to={`${link({ activity: everyActivity ? null : "all" })}#activity`}
1066 preventScrollReset
1067 className="inline-flex items-center gap-0.5 text-xs text-muted hover:text-fg"
1068 >
1069 {everyActivity ? "Less" : "All activity"} <ChevronRight size={12} />
1070 </Link>
1071 )}
1072 </div>
1073 <p className="mt-1 flex items-center gap-3 text-[0.6875rem] text-faint">
1074 <span className="inline-flex items-center gap-1.5">
1075 <Avatar name="g1t-agent" size={12} /> agents
1076 </span>
1077 <span className="inline-flex items-center gap-1.5">
1078 <Avatar name={viewer.username} size={12} /> people
1079 </span>
1080 </p>
1081 {!loaded.perRepoLoaded ? (
1082 <div className="mt-3">
1083 <Unavailable what="Activity" />
1084 </div>
1085 ) : feed.length === 0 ? (
1086 <p className="mt-3 text-sm leading-6 text-muted">
1087 Nothing has moved yet. Merges, deploys, checks, reviews and what agents learn show up here.
1088 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1089 ) : (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1090 <ol className="mt-2 divide-y divide-line/70">
1091 {feed.map((group) => (
1092 <FeedLine key={group.id} group={group} titles={titles} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1093 ))}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1094 </ol>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1095 )}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1096 </section>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1097
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1098 <section className="rounded-xl border border-line bg-surface p-5">
1099 <h2 className="text-sm font-semibold">Connect your own agent</h2>
1100 <p className="mt-1.5 text-xs leading-5 text-muted">
1101 Add g1t to your coding agent. It signs in through your browser; there is no token to copy.
1102 </p>
1103 <AgentSetup className="mt-3" />
1104 <Link
1105 to="https://docs.g1t.sh/guides/bring-your-own-agent/"
1106 className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg"
1107 >
1108 How it works <ArrowRight size={12} />
1109 </Link>
1110 </section>
1111 </aside>
1112 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1113 </main>
1114 );
1115}