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

1,151 lines47,779 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.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1import { ArrowDownWideNarrow, ArrowRight, ArrowUpRight, Check, ChevronDown, ChevronRight, Plus, Sparkles } 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";
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step30import { AgentComposer, type ComposerResult } from "./agent-composer";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31import { AgentSetup } from "./agent-setup";
32import type { ShellData } from "./shell";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms33import { useLiveRefresh } from "./agents";
34import { Unavailable } from "./mission";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look35import { Avatar, TimeAgo } from "./ui";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms36import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look37import type { Loaded } from "../routes/home";
38
39/*
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms40 * Mission control: the home page of someone signed in. It answers where
41 * the viewer is needed, what agents are doing, and what landed without
42 * them. It is a chunk of its own, loaded only for them, so the signed-out
43 * home page does not carry it. routes/home.tsx loads its data;
44 * lib/mission-control.ts shapes it.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look45 */
46
47/** The viewer's time zone, set by the page itself, so the greeting fits their day. */
48const TZ_COOKIE = "g1t_tz";
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms49/** Rows a tab shows before "See all". */
50const ROWS = 6;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51
52const 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 200ms53const 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 look54
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms55// --- Small pieces -----------------------------------------------------------------
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look56
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms57const CHIP_TONE: Record<Reason, string> = {
58 blocking: "border-danger/35 bg-danger/10 text-danger",
59 checks_failing: "border-danger/35 bg-danger/10 text-danger",
60 outside_guardrails: "border-warn/35 bg-warn/10 text-warn",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step61 low_confidence: "border-warn/35 bg-warn/10 text-warn",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms62 stalled: "border-warn/35 bg-warn/10 text-warn",
63 asked_for_you: "border-merged/35 bg-merged/10 text-merged",
64 needs_review: "border-info/35 bg-info/10 text-info",
65 ready_to_merge: "border-accent/35 bg-accent/10 text-accent",
66};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms68function Chip({ children, tone }: { children: ReactNode; tone: string }) {
69 return (
70 <span
71 className={cn(
72 "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",
73 tone,
74 )}
75 >
76 {children}
77 </span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look78 );
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms79}
80
81/** A person or an agent, by name, wearing the mark that tells them apart. */
82function 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 look83 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms84 <span className={cn("inline-flex min-w-0 items-center gap-1.5", className)}>
85 <Avatar name={who.name} size={size} />
86 <span className="truncate">{who.name}</span>
87 </span>
88 );
89}
90
91function Eyebrow({ children }: { children: ReactNode }) {
92 return <p className="font-mono text-[0.625rem] font-medium tracking-[0.12em] text-faint uppercase">{children}</p>;
93}
94
95const FACT_TONE = { good: "text-accent", warn: "text-warn", bad: "text-danger" } as const;
96
97function Facts({ facts }: { facts: Fact[] }) {
98 if (facts.length === 0) return <p className="text-sm text-muted">Nothing more is known about it yet.</p>;
99 return (
100 <dl className="grid grid-cols-2 gap-x-4 gap-y-2.5">
101 {facts.map((fact) => (
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step102 <div key={fact.label} className={cn("min-w-0", fact.wide && "col-span-2")}>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms103 <dt className="text-xs text-faint">{fact.label}</dt>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step104 <dd
105 className={cn(
106 "mt-0.5 text-sm font-medium tabular-nums",
107 fact.wide ? "leading-5" : "truncate",
108 fact.tone ? FACT_TONE[fact.tone] : "text-fg-soft",
109 )}
110 >
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms111 {fact.value}
112 </dd>
113 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 ))}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms115 </dl>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look116 );
117}
118
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms119/** A form that acts on a pull request from here, saying so before the page catches up. */
120function QuickForm({ quick, variant = "quiet" }: { quick: QuickAction; variant?: "quiet" | "accent" }) {
121 const fetcher = useFetcher<{ error?: string } | null>();
122 const [sent, setSent] = useState(false);
123 const busy = fetcher.state !== "idle";
Fast pages, required checks on the branch, self-hosted runners, honest incidents124 // Shown as done the moment it is sent, and undone if it fails: the page
125 // reloads behind it, which takes longer than the action.
126 const done = sent && (busy || !fetcher.data?.error);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look127 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms128 <fetcher.Form method="post" action={quick.to} onSubmit={() => setSent(true)} className="contents">
129 {Object.entries(quick.fields).map(([name, value]) => (
130 <input key={name} type="hidden" name={name} value={value} />
131 ))}
132 <button
133 type="submit"
134 disabled={busy || done}
135 className={cn(
136 "inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium transition-colors disabled:cursor-default",
137 variant === "accent"
138 ? "bg-merged text-bg hover:bg-[#c9bfff]"
139 : "border border-line-strong text-fg/90 hover:bg-raised hover:text-fg",
140 done && "border-accent/40 text-accent",
141 )}
142 >
Fast pages, required checks on the branch, self-hosted runners, honest incidents143 {done ? <Check size={14} /> : null}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms144 {done ? quick.done : quick.label}
145 </button>
146 {fetcher.data?.error && <span className="basis-full text-xs text-danger">{fetcher.data.error}</span>}
147 </fetcher.Form>
148 );
149}
150
151function ProjectMark({ repo }: { repo: { namespace: string; name: string } | null }) {
152 return <Avatar name={repo ? repo.name : "g1t"} size={32} square />;
153}
154
155/** A row that opens to what is known about it; the first one starts open. */
156function Row({
157 open,
158 repo,
159 by,
160 title,
161 sub,
162 chip,
163 at,
164 children,
165}: {
166 open: boolean;
167 repo: { namespace: string; name: string } | null;
168 by: Who | null;
169 title: ReactNode;
170 sub: ReactNode;
171 chip: ReactNode;
172 at: number;
173 children: ReactNode;
174}) {
175 return (
176 <li>
177 <details open={open} className="group">
178 <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">
179 <ProjectMark repo={repo} />
180 <span className="min-w-0 grow">
181 <span className="block truncate text-sm">{title}</span>
182 <span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted">
183 <span className="shrink-0 font-medium text-fg-soft">{repo ? repo.name : "Workspace"}</span>
184 {by && (
185 <>
186 <span className="text-faint">·</span>
187 <Person who={by} size={14} className={cn("shrink-0", by.agent ? "text-merged" : "")} />
188 </>
189 )}
190 {sub && (
191 <>
192 <span className="text-faint">—</span>
193 <span className="truncate">{sub}</span>
194 </>
195 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look196 </span>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms197 </span>
198 <span className="hidden sm:inline-flex">{chip}</span>
199 <span className="hidden w-16 shrink-0 text-right text-xs text-faint sm:block">
200 <TimeAgo at={at} />
201 </span>
202 <ChevronDown size={16} className="shrink-0 text-faint transition-transform group-open:rotate-180" />
203 </summary>
204 <div className="px-4 pb-4 sm:px-5 sm:pb-5">
205 <div className="mb-3 flex items-center gap-2 sm:hidden">
206 {chip}
207 <span className="text-xs text-faint">
208 <TimeAgo at={at} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209 </span>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms210 </div>
211 {children}
212 </div>
213 </details>
214 </li>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 );
216}
217
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms218function NeedCard({ row, first }: { row: NeedRow; first: boolean }) {
219 const isPull = row.to.includes("/pull/");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look220 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms221 <Row
222 open={first}
223 repo={row.repo}
224 by={row.by}
225 title={
226 <>
227 <span className="font-medium">{row.title}</span>
228 {row.ref && <span className="ml-1 font-mono text-xs text-faint">{row.ref}</span>}
229 </>
230 }
231 sub={row.ask}
232 chip={<Chip tone={CHIP_TONE[row.reason]}>{REASON_LABEL[row.reason]}</Chip>}
233 at={row.at}
234 >
235 <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)]">
236 <div className="min-w-0">
237 <Eyebrow>The ask</Eyebrow>
238 <p className="mt-2 text-sm leading-6 text-fg-soft">{row.ask}</p>
239 <p className="mt-2 text-xs leading-5 text-muted">
240 <Link to={row.to} className="font-medium text-fg hover:underline">
241 {row.repo ? `${row.repo.name}${row.ref ?? ""}` : row.title}
242 </Link>
243 {row.for && (
244 <>
245 {" "}
246 · started for <span className="text-fg-soft">{row.for}</span>
247 </>
248 )}
249 </p>
250 </div>
251 <div className="min-w-0">
252 <Eyebrow>What the agent already knows</Eyebrow>
253 <div className="mt-2">
254 <Facts facts={row.facts} />
255 </div>
256 </div>
257 <div className="rounded-lg border border-warn/20 bg-warn/[0.06] p-3.5 md:col-span-2 2xl:col-span-1">
258 <p className="text-xs font-semibold text-warn">Why this needs you</p>
259 <p className="mt-1.5 text-sm leading-6 text-fg-soft">{row.why}</p>
260 <p className="mt-2 text-xs text-faint">
261 Waiting <TimeAgo at={row.at} />
262 </p>
263 </div>
264 </div>
265 <div className="mt-3 flex flex-wrap items-center gap-2">
266 <Link
267 to={row.to}
268 prefetch="intent"
269 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"
270 >
271 Review and respond <ArrowUpRight size={14} />
272 </Link>
273 {row.quick && <QuickForm quick={row.quick} />}
274 {row.link && (
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 to={row.link.to}
277 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 look278 >
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms279 {row.link.label}
280 </Link>
281 )}
282 {row.open !== row.to && (
283 <Link to={row.open} prefetch="intent" className="px-2 py-1.5 text-sm text-muted hover:text-fg">
284 {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 look285 </Link>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms286 )}
287 </div>
288 </Row>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look289 );
290}
291
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms292function WaitingCard({ row, first }: { row: WaitingRow; first: boolean }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look293 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms294 <Row
295 open={first}
296 repo={row.repo}
297 by={row.by}
298 title={
299 <>
300 <span className="font-medium">{row.title}</span>
301 {row.ref && <span className="ml-1 font-mono text-xs text-faint">{row.ref}</span>}
302 </>
303 }
304 sub={row.detail}
305 chip={
306 <Chip tone="border-line-strong bg-raised text-muted">
307 {row.live && <span className="mr-1.5 size-1.5 animate-pulse rounded-full bg-accent" />}
308 {row.chip}
309 </Chip>
310 }
311 at={row.at}
312 >
313 <div className="grid gap-4 rounded-xl border border-line bg-bg/50 p-4 md:grid-cols-2 md:gap-6">
314 <div className="min-w-0">
315 <Eyebrow>{row.live ? "Doing now" : "Where it stands"}</Eyebrow>
316 <p className="mt-2 text-sm leading-6 text-fg-soft">{row.detail}</p>
317 </div>
318 <div className="min-w-0">
319 <Eyebrow>What is known</Eyebrow>
320 <div className="mt-2">
321 <Facts facts={row.facts} />
322 </div>
323 </div>
324 </div>
325 <div className="mt-3 flex flex-wrap gap-2">
326 <Link
327 to={row.to}
328 prefetch="intent"
329 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"
330 >
331 {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 look332 </Link>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms333 {row.run && row.run !== row.to && (
334 <Link to={row.run} prefetch="intent" className="px-2 py-1.5 text-sm text-muted hover:text-fg">
335 Watch the run
336 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look337 )}
338 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms339 </Row>
340 );
341}
342
343function LandedCard({ row, first }: { row: LandedRow; first: boolean }) {
344 return (
345 <Row
346 open={first}
347 repo={row.repo}
348 by={row.byAgents ? { name: row.agent, agent: isAgent(row.agent) } : row.by}
349 title={
350 <>
351 <span className="font-medium">{row.title}</span>
352 <span className="ml-1 font-mono text-xs text-faint">{row.ref}</span>
353 </>
354 }
355 sub={row.byAgents ? "landed without a person" : `merged by ${row.by?.name ?? "a person"}`}
356 chip={
357 row.byAgents ? (
358 <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 look359 ) : (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms360 <Chip tone="border-warn/35 bg-warn/10 text-warn">Needed a person</Chip>
361 )
362 }
363 at={row.at}
364 >
365 <div className="grid gap-4 rounded-xl border border-line bg-bg/50 p-4 md:grid-cols-2 md:gap-6">
366 <div className="min-w-0">
367 <Eyebrow>How it landed</Eyebrow>
368 <p className="mt-2 text-sm leading-6 text-fg-soft">
369 {row.byAgents
370 ? `g1t merged it once everything the repository asks for was met, with no one pressing merge.`
371 : `${row.by?.name ?? "A person"} merged it.`}
372 {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 look373 </p>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms374 </div>
375 <div className="min-w-0">
376 <Eyebrow>What changed</Eyebrow>
377 <div className="mt-2">
378 <Facts facts={row.facts} />
379 </div>
380 </div>
381 </div>
382 <div className="mt-3">
383 <Link
384 to={row.to}
385 prefetch="intent"
386 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"
387 >
388 Open pull request <ArrowUpRight size={14} />
389 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look390 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms391 </Row>
392 );
393}
394
395function List({ children }: { children: ReactNode }) {
396 return <ul className="divide-y divide-line">{children}</ul>;
397}
398
399function Empty({ children, action }: { children: ReactNode; action?: ReactNode }) {
400 return (
401 <div className="px-5 py-10 text-center">
402 <div className="mx-auto max-w-md text-sm leading-6 text-muted">{children}</div>
403 {action && <div className="mt-4 flex flex-wrap justify-center gap-2">{action}</div>}
404 </div>
405 );
406}
407
408// --- The stat strip -----------------------------------------------------------------
409
410function Stat({ label, value, hint, dot, title }: { label: string; value: string; hint: ReactNode; dot?: string; title?: string }) {
411 return (
412 <div className="min-w-0 bg-surface px-4 py-3.5 sm:px-5 sm:py-4" title={title}>
413 <p className="flex items-center gap-1.5 truncate text-xs text-muted">
414 {dot && <span className={cn("size-1.5 shrink-0 rounded-full", dot)} />}
415 {label}
416 </p>
417 <p className="mt-1 font-display text-2xl font-semibold tracking-tight tabular-nums sm:text-[1.75rem]">{value}</p>
418 <p className="mt-0.5 truncate text-xs text-faint">{hint}</p>
419 </div>
420 );
421}
422
423// --- This week ----------------------------------------------------------------------
424
425/** Seven days of landed changes, each split into what agents landed alone and what a person merged. */
426function WeekChart({ week }: { week: Week }) {
427 const max = Math.max(1, ...week.days.map((d) => d.agents + d.people));
428 const height = 112;
429 const delta = change(week.total, week.previous);
430 return (
431 <div>
432 <div className="flex items-end justify-between gap-3">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look433 <div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms434 <p className="font-display text-3xl font-semibold tracking-tight tabular-nums">{week.total.toLocaleString("en-US")}</p>
435 <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 look436 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms437 {delta != null ? (
438 <span
439 className={cn(
440 "rounded-full px-2 py-0.5 text-xs font-medium tabular-nums ring-1",
441 delta >= 0 ? "text-accent ring-accent/30" : "text-warn ring-warn/30",
442 )}
443 >
444 {signedPercent(delta)} vs last week
445 </span>
446 ) : week.previous === 0 && week.total > 0 ? (
447 <span className="text-xs text-faint">none the week before</span>
448 ) : null}
449 </div>
450 {week.total === 0 ? (
451 <p className="mt-4 rounded-lg border border-dashed border-line px-4 py-6 text-center text-sm leading-6 text-muted">
452 Nothing landed in the last 7 days. Each change that does shows here, by day, split by whether a person had to merge it.
453 </p>
454 ) : (
455 <div className="relative mt-5" style={{ height: height + 20 }}>
456 <div className="absolute inset-x-0 border-t border-line" style={{ top: height }} />
457 <div className="absolute inset-x-0 top-0 flex items-end justify-between gap-1" style={{ height }}>
458 {week.days.map((day, index) => {
459 const total = day.agents + day.people;
460 const agentsH = Math.round((day.agents / max) * (height - 4));
461 const peopleH = Math.round((day.people / max) * (height - 4));
462 const today = index === week.days.length - 1;
463 return (
464 <div key={day.key} className="group relative flex h-full flex-1 flex-col items-center justify-end">
465 {total > 0 && (
466 <span
467 className={cn(
468 "mb-1 text-[0.625rem] tabular-nums",
469 today ? "text-fg-soft" : "text-faint opacity-0 group-hover:opacity-100",
470 )}
471 >
472 {total}
473 </span>
474 )}
475 <div className="flex w-full max-w-6 flex-col items-stretch gap-[2px]">
476 {day.people > 0 && <span className="block rounded-t bg-warn" style={{ height: Math.max(3, peopleH) }} />}
477 {day.agents > 0 && (
478 <span className={cn("block bg-merged", day.people > 0 ? "" : "rounded-t")} style={{ height: Math.max(3, agentsH) }} />
479 )}
480 {total === 0 && <span className="block h-[2px] rounded-full bg-line-strong" />}
481 </div>
482 <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">
483 <p className="font-medium text-fg">{day.label}</p>
484 <p className="mt-0.5 flex items-center gap-1.5 text-muted">
485 <span className="size-1.5 rounded-full bg-merged" /> {day.agents} by agents
486 </p>
487 <p className="flex items-center gap-1.5 text-muted">
488 <span className="size-1.5 rounded-full bg-warn" /> {day.people} needed a person
489 </p>
490 </div>
491 </div>
492 );
493 })}
494 </div>
495 <div className="absolute inset-x-0 bottom-0 flex justify-between gap-1">
496 {week.days.map((day, index) => (
497 <span
498 key={day.key}
499 className={cn(
500 "flex-1 text-center text-[0.6875rem]",
501 index === week.days.length - 1 ? "font-medium text-fg-soft" : "text-faint",
502 )}
503 >
504 {day.label}
505 </span>
506 ))}
507 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look508 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms509 )}
510 <div className={cn("mt-3 flex-wrap gap-x-4 gap-y-1 text-xs text-muted", week.total === 0 ? "hidden" : "flex")}>
511 <span className="inline-flex items-center gap-1.5">
512 <span className="size-2 rounded-sm bg-merged" /> Landed by agents
513 </span>
514 <span className="inline-flex items-center gap-1.5">
515 <span className="size-2 rounded-sm bg-warn" /> Needed a person
516 </span>
517 </div>
518 <table className="sr-only">
519 <caption>Changes landed each day</caption>
520 <thead>
521 <tr>
522 <th>Day</th>
523 <th>Landed by agents</th>
524 <th>Needed a person</th>
525 </tr>
526 </thead>
527 <tbody>
528 {week.days.map((day) => (
529 <tr key={day.key}>
530 <td>{day.key}</td>
531 <td>{day.agents}</td>
532 <td>{day.people}</td>
533 </tr>
534 ))}
535 </tbody>
536 </table>
537 </div>
538 );
539}
540
541// --- Activity -----------------------------------------------------------------------
542
543const VERB: Record<Verb, string> = {
544 landed: "landed",
545 opened_issue: "opened",
546 closed_issue: "closed",
547 started: "started on",
548 ready: "marked ready",
549 checks_passed: "checks passed on",
550 checks_failed: "checks failed on",
551 approved: "approved",
552 changes_requested: "asked for changes on",
553 commented: "commented on",
554 asked: "was asked about",
555 deployed: "deployed production",
556 deploy_failed: "production build failed",
557 learned: "learned",
558};
559
560/** One line of the feed: who, what, and the one thing it was about, by name. */
561function FeedLine({ group, titles }: { group: ActivityGroup; titles: Record<string, string> }) {
562 const part = group.parts[0];
563 const base = `/${group.repo.namespace}/${group.repo.name}`;
564 const actor = group.actor ?? "g1t";
565 const agent = group.actor == null || isAgent(group.actor);
566 const number = part.numbers[0];
567 const title = number != null ? titles[`${group.repo.namespace}/${group.repo.name}#${number}`.toLowerCase()] : undefined;
568 const more = group.parts.length - 1 + Math.max(0, part.numbers.length - 1);
569 return (
570 <li className="flex gap-2.5 py-2">
571 <span className="mt-px">
572 <Avatar name={actor} size={22} />
573 </span>
574 <span className="min-w-0 grow text-[0.8125rem] leading-5 text-muted">
575 <span className={cn("font-medium", agent ? "text-merged" : "text-fg")}>{actor}</span> {VERB[part.verb]}{" "}
576 {part.verb === "learned" ? (
577 <Link to={part.to ?? `${base}/memory`} className="text-fg-soft hover:text-fg">
578 “{part.texts[0]}”
579 </Link>
580 ) : number != null ? (
581 <Link to={`${base}/issues/${number}`} prefetch="intent" className="text-fg-soft hover:text-fg">
582 <span className="font-mono text-xs">#{number}</span>
583 {title && <> {title}</>}
584 </Link>
585 ) : part.to ? (
586 <Link to={part.to} className="text-fg-soft hover:text-fg">
587 {group.repo.name}
588 </Link>
589 ) : null}
590 {more > 0 && <span className="text-faint"> and {more} more</span>}
591 <span className="block text-xs text-faint">
592 <span className="font-mono">{group.repo.name}</span> · <TimeAgo at={group.at} />
593 </span>
594 </span>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look595 </li>
596 );
597}
598
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms599// --- Get started --------------------------------------------------------------------
600
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look601type Step = { done: boolean; title: string; about: string; to: string | null; action: string };
602
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms603/** 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 look604function GetStarted({ steps }: { steps: Step[] }) {
605 const left = steps.filter((step) => !step.done).length;
606 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms607 <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 look608 <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 200ms609 <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 look610 <span className="text-xs text-muted">
611 {steps.length - left} of {steps.length} done
612 </span>
613 </div>
614 <ol className="mt-4 space-y-2">
615 {steps.map((step, index) => (
616 <li
617 key={step.title}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms618 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 look619 >
620 <span
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms621 className={cn(
622 "mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full text-[0.6875rem] font-medium",
623 step.done ? "bg-accent text-bg" : "text-muted ring-1 ring-line-strong",
624 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look625 >
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms626 {step.done ? <Check size={12} /> : index + 1}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look627 </span>
628 <span className="min-w-0 grow">
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms629 <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 look630 {!step.done && <span className="mt-0.5 block text-xs leading-5 text-muted">{step.about}</span>}
631 </span>
632 {!step.done && step.to && (
633 <Link
634 to={step.to}
635 className="shrink-0 rounded-md bg-fg px-2.5 py-1 text-xs font-medium text-bg transition-colors hover:bg-white"
636 >
637 {step.action}
638 </Link>
639 )}
640 </li>
641 ))}
642 </ol>
643 </section>
644 );
645}
646
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms647// --- The page -----------------------------------------------------------------------
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look648
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms649const TAB_LABEL: Record<Tab, string> = { needs: "Needs you", waiting: "Waiting on agents", landed: "Landed today" };
650const TAB_SHORT: Record<Tab, string> = { needs: "Needs you", waiting: "Waiting", landed: "Today" };
651const 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 look652
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step653export default function MissionControl({ loaderData, delegated = null }: { loaderData: Loaded; delegated?: ComposerResult }) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look654 const shell = useRouteLoaderData("root")?.shell as ShellData | null | undefined;
655 const loaded: Loaded = loaderData;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms656 const [params] = useSearchParams();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look657 // 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 200ms658 useLiveRefresh(loaded.changing);
659 const [greeting, setGreeting] = useState(loaded.greeting);
660 const [date, setDate] = useState(loaded.date);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look661 useEffect(() => {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms662 // The greeting and date follow the viewer's own clock, and the server
663 // learns their zone for next time.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look664 setGreeting(greetingFor(new Date().getHours()));
665 try {
666 const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms667 setDate(dateLine(Date.now(), zone ?? null));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look668 if (zone) document.cookie = `${TZ_COOKIE}=${encodeURIComponent(zone)}; Path=/; Max-Age=31536000; SameSite=Lax`;
669 } catch {
670 // Nothing to remember.
671 }
672 }, []);
673
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms674 const { viewer, repos, waiting, landed, liveTotal, week, groups, titles, stats, canRunAgents, trial } = loaded;
675 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 look676
677 // 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 200ms678 const limit = shell?.limit && !shell.limit.comped && shell.limit.state !== "ok" && workspace ? shell.limit : null;
679 const limitRow: NeedRow[] = limit
680 ? [
681 (() => {
682 const detail =
683 limit.state === "stopped"
684 ? "No new agent starts until the limit is raised or a card is added."
685 : `${dollars(limit.exposureMicros)} of ${dollars(limit.ceilingMicros ?? 0)} used. Add a card so work keeps going.`;
686 return {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look687 key: "limit",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms688 reason: "blocking" as const,
689 repo: null,
690 ref: null,
691 title: limit.state === "stopped" ? "Agents are stopped: the usage limit was reached" : "Usage is nearing its limit",
692 ask: detail,
693 by: null,
694 for: null,
695 at: Date.now(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look696 to: `/${workspace}/-/billing`,
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms697 open: `/${workspace}/-/billing`,
698 facts: [
699 { label: "Used", value: `${dollars(limit.exposureMicros)} of ${dollars(limit.ceilingMicros ?? 0)}`, tone: "warn" as const },
700 ],
701 why: whyFor("blocking", { kind: "limit", detail }),
702 quick: null,
703 link: null,
704 };
705 })(),
706 ]
707 : [];
708 const needs = [...limitRow, ...loaded.needs];
709 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 look710
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms711 const counts: Record<Tab, number> = { needs: needs.length, waiting: waiting.length, landed: landed.length };
712 const tab = parseTab(params.get("tab")) ?? "needs";
713 const sort = parseSort(params.get("sort"));
714 const all = params.get("all") === "1";
715 const everyActivity = params.get("activity") === "all";
716 const link = (changes: Record<string, string | null>) => {
717 const next = new URLSearchParams(params);
718 for (const [key, value] of Object.entries(changes)) {
719 if (value == null) next.delete(key);
720 else next.set(key, value);
721 }
722 const text = next.toString();
723 return text ? `?${text}` : "/";
724 };
725
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look726 const steps: Step[] = [
727 {
728 done: Boolean(workspace),
729 title: "Create a workspace",
730 about: "Projects, people and agent credit live in one.",
731 to: "/workspaces/new",
732 action: "Create",
733 },
734 {
735 done: repos.length > 0,
736 title: "Add a project",
737 about: "Create a repository, or import one by its address. Push to it with git as usual.",
738 to: workspace ? `/new?workspace=${workspace}` : "/new",
739 action: "Add",
740 },
741 trial?.open
742 ? {
743 done: true,
744 title: "Try g1t's agents free",
745 about: trial.granted
746 ? `This workspace has ${dollars(trial.limitMicros - trial.usedMicros)} of its ${dollars(trial.limitMicros)} trial credit left, for g1t's models and sandboxes.`
747 : `This workspace gets ${dollars(trial.limitMicros)} of trial credit, for g1t's models and sandboxes, the first time its agents work.`,
748 to: workspace ? `/${workspace}/-/integrations` : null,
749 action: "Connect",
750 }
751 : canRunAgents
752 ? {
753 done: Boolean(shell?.free) || shell?.limit == null || shell.limit.comped || shell.limit.state === "ok",
754 title: "Keep work running",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms755 about:
756 "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 look757 to: workspace ? `/${workspace}/-/billing` : null,
758 action: "Billing",
759 }
760 : {
761 done: false,
762 title: "Connect a model",
763 about: `${
764 trialClosed(trial, workspace ?? "This workspace") ? `${trialClosed(trial, workspace ?? "This workspace")} ` : ""
765 }Agents need a model to think with. Connect your Anthropic or OpenAI key, or any compatible endpoint.`,
766 to: workspace ? `/${workspace}/-/integrations` : null,
767 action: "Connect",
768 },
769 {
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms770 done: loaded.handedOff,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look771 title: "Hand off an outcome",
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms772 about:
773 "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.",
774 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 look775 action: "Write one",
776 },
777 ];
778 const starting = steps.some((step) => !step.done);
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms779 const noProjects = repos.length === 0;
780
781 const rows = tab === "needs" ? sortRows(needs, sort) : tab === "waiting" ? sortRows(waiting, sort) : sortRows(landed, sort);
782 const shown = all ? rows : rows.slice(0, ROWS);
783 const share = week.total > 0 ? week.byAgents / week.total : null;
784 const delta = change(week.total, week.previous);
785 const feed = everyActivity ? groups : groups.slice(0, 8);
786
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step787 // "Put an agent on it" first, with "New issue" beside it as before: one
788 // split control, the agent the main way in.
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms789 const newIssue =
790 repos.length > 0 ? (
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step791 <div className="flex items-stretch">
792 <AgentComposer
793 repos={repos}
794 open={params.get("agent") === "new" || delegated != null}
795 result={delegated}
796 note={
797 canRunAgents ? null : (
798 <>
799 Agents need a model first.{" "}
800 {workspace && (
801 <Link to={`/${workspace}/-/integrations`} className="font-medium text-fg hover:underline">
802 Connect one
803 </Link>
804 )}
805 . The issue still opens.
806 </>
807 )
808 }
809 >
810 <span className="inline-flex cursor-pointer items-center gap-1.5 rounded-l-md border border-line-strong bg-raised px-3 py-2 text-sm font-medium text-fg transition-colors hover:bg-line/60 group-open/composer:bg-line/60">
811 <Sparkles size={14} className="text-merged" /> Put an agent on it
812 </span>
813 </AgentComposer>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms814 <DropdownMenu>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step815 <DropdownMenuTrigger className="-ml-px inline-flex items-center gap-1.5 rounded-r-md border border-line-strong px-3 py-2 text-sm font-medium text-fg/90 transition-colors hover:bg-raised hover:text-fg">
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms816 <Plus size={14} /> New issue
817 </DropdownMenuTrigger>
818 <DropdownMenuContent align="end" className="max-h-80 overflow-y-auto">
819 <DropdownMenuLabel>In which project?</DropdownMenuLabel>
820 {repos.map((repo) => (
821 <DropdownMenuItem key={repo.name} asChild>
822 <Link to={`/${repo.namespace}/${repo.name}/issues/new`}>
823 <Avatar name={repo.name} size={18} square />
824 {repo.name}
825 </Link>
826 </DropdownMenuItem>
827 ))}
828 </DropdownMenuContent>
829 </DropdownMenu>
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step830 </div>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms831 ) : (
832 <Link
833 to={workspace ? `/new?workspace=${workspace}` : "/new"}
834 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"
835 >
836 <Plus size={14} /> New project
837 </Link>
838 );
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look839
840 return (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms841 <main className="mx-auto max-w-7xl space-y-6 px-4 py-8 sm:px-6 sm:py-10">
842 <header className="flex flex-wrap items-end justify-between gap-x-6 gap-y-4">
843 <div className="min-w-0">
844 <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 look845 {greeting}, {loaded.name}
846 </h1>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms847 <p className="mt-1.5 text-sm text-muted" suppressHydrationWarning>
848 <span className="text-fg-soft">{date}</span> · {loaded.summary}
849 </p>
850 </div>
851 <div className="flex flex-wrap items-center gap-2">
852 {newIssue}
853 {needs.length > 0 && (
854 <Link
855 to={`${link({ tab: "needs", all: null })}#work`}
856 preventScrollReset
857 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]"
858 >
859 Review {needs.length} that need{needs.length === 1 ? "s" : ""} you <ArrowRight size={14} />
860 </Link>
861 )}
862 </div>
863 </header>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms865 {starting && <GetStarted steps={steps} />}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look866
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms867 <section
868 aria-label="At a glance"
869 className="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-line bg-line lg:grid-cols-5"
870 >
871 <Stat
872 label="Projects"
873 value={stats.projects == null ? "—" : String(stats.projects)}
874 hint={
875 stats.projectsThisMonth == null
876 ? "in this workspace"
877 : stats.projectsThisMonth > 0
878 ? `+${stats.projectsThisMonth} this month`
879 : "none new this month"
880 }
881 />
882 <Stat
883 label="Agents"
884 value={loaded.runsLoaded || !workspace ? String(liveTotal) : "—"}
885 dot={liveTotal > 0 ? "bg-accent animate-pulse" : undefined}
886 hint={
887 liveTotal > 0
888 ? `live now · ${stats.agentHours < 10 ? stats.agentHours.toFixed(1) : Math.round(stats.agentHours)}h this week`
889 : stats.agentHours > 0
890 ? `none live · ${stats.agentHours < 10 ? stats.agentHours.toFixed(1) : Math.round(stats.agentHours)}h this week`
891 : "none live now"
892 }
893 />
894 <Stat
895 label="Changes this week"
896 value={loaded.perRepoLoaded ? String(week.total) : "—"}
897 hint={
898 delta != null ? `${signedPercent(delta)} vs last week` : week.previous === 0 ? "none the week before" : "merged pull requests"
899 }
900 />
901 <Stat
902 label="Landed without you"
903 dot="bg-merged"
904 value={share == null ? "—" : `${Math.round(share * 100)}%`}
905 hint={share == null ? "nothing landed yet" : `${week.byAgents} of ${week.total}`}
906 title="Merged by g1t, by auto-merge or the merge queue, with no person pressing merge."
907 />
908 <div className="col-span-2 lg:col-span-1">
909 <Stat
910 label="Need you"
911 dot="bg-warn"
912 value={String(needs.length)}
913 hint={blocking > 0 ? `${blocking} blocking` : needs.length > 0 ? "nothing blocking" : "all clear"}
914 />
915 </div>
916 </section>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look917
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms918 <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_21rem] xl:grid-cols-[minmax(0,1fr)_23rem]">
919 <section id="work" className="min-w-0 scroll-mt-20 self-start overflow-hidden rounded-xl border border-line bg-surface">
920 <div className="flex items-center gap-2 border-b border-line px-2 sm:px-3">
921 <nav className="-mb-px flex min-w-0 grow gap-1 overflow-x-auto [scrollbar-width:none]" aria-label="Mission control">
922 {(["needs", "waiting", "landed"] as const).map((value) => (
923 <Link
924 key={value}
925 to={link({ tab: value, all: null })}
926 preventScrollReset
927 aria-current={tab === value ? "page" : undefined}
928 className={cn(
929 "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",
930 tab === value ? "border-merged font-medium text-fg" : "border-transparent text-muted hover:text-fg",
931 )}
932 >
933 <span className="sm:hidden">{TAB_SHORT[value]}</span>
934 <span className="hidden sm:inline">{TAB_LABEL[value]}</span>
935 <span
936 className={cn(
937 "rounded-full px-1.5 text-[0.6875rem] tabular-nums",
938 tab === value && value === "needs" && counts.needs > 0 ? "bg-warn/15 text-warn" : "bg-line text-muted",
939 )}
940 >
941 {counts[value]}
942 </span>
943 </Link>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look944 ))}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms945 </nav>
946 <DropdownMenu>
947 <DropdownMenuTrigger
948 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"
949 aria-label="Sort"
950 >
951 <ArrowDownWideNarrow size={14} />
952 <span className="hidden sm:inline">{SORT_LABEL[sort]}</span>
953 </DropdownMenuTrigger>
954 <DropdownMenuContent align="end">
955 {(["impact", "newest"] as const).map((value) => (
956 <DropdownMenuItem key={value} asChild>
957 <Link to={link({ sort: value === "impact" ? null : value })} preventScrollReset>
958 {sort === value ? <Check /> : <span className="size-4" />}
959 {SORT_LABEL[value]}
960 </Link>
961 </DropdownMenuItem>
962 ))}
963 </DropdownMenuContent>
964 </DropdownMenu>
965 </div>
966
967 {!loaded.perRepoLoaded && tab !== "waiting" ? (
968 <div className="p-4">
969 <Unavailable what={tab === "needs" ? "Some of what needs you" : "What landed"} />
970 </div>
971 ) : null}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look972
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms973 {tab === "needs" &&
974 (needs.length === 0 ? (
975 noProjects ? (
976 <Empty
977 action={
978 <Link
979 to={workspace ? `/new?workspace=${workspace}` : "/new"}
980 className="rounded-md bg-fg px-3 py-1.5 text-sm font-medium text-bg hover:bg-white"
981 >
982 Create or import a project
983 </Link>
984 }
985 >
986 <p className="font-medium text-fg">No projects yet</p>
987 <p className="mt-1">
988 Create a repository or import one. Assign its issues to g1t-agent, and what needs you shows up here.
989 </p>
990 </Empty>
991 ) : (
992 <>
993 <Empty>
994 <p className="font-medium text-fg">Nothing needs you</p>
995 <p className="mt-1">
996 Agents are handling everything.{" "}
997 {landed.length > 0
998 ? "Here's what they landed today."
999 : "Reviews, stopped work and failed deploys show up here first."}
1000 </p>
1001 </Empty>
1002 {landed.length > 0 && (
1003 <div className="border-t border-line">
1004 <List>
1005 {landed.slice(0, ROWS).map((row) => (
1006 <LandedCard key={row.key} row={row} first={false} />
1007 ))}
1008 </List>
1009 </div>
1010 )}
1011 </>
1012 )
1013 ) : (
1014 <List>
1015 {(shown as NeedRow[]).map((row, index) => (
1016 <NeedCard key={row.key} row={row} first={index === 0} />
1017 ))}
1018 </List>
1019 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1020
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1021 {tab === "waiting" &&
1022 (!loaded.runsLoaded && workspace ? (
1023 <div className="p-4">
1024 <Unavailable what="Agent runs" />
1025 </div>
1026 ) : waiting.length === 0 ? (
1027 <Empty>
1028 <p className="font-medium text-fg">No agent is at work right now</p>
1029 <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>
1030 </Empty>
1031 ) : (
1032 <List>
1033 {(shown as WaitingRow[]).map((row, index) => (
1034 <WaitingCard key={row.key} row={row} first={index === 0} />
1035 ))}
1036 </List>
1037 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1038
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1039 {tab === "landed" &&
1040 loaded.perRepoLoaded &&
1041 (landed.length === 0 ? (
1042 <Empty>
1043 <p className="font-medium text-fg">Nothing has landed today yet</p>
1044 <p className="mt-1">
1045 {week.total > 0
1046 ? `${plural(week.total, "change")} landed in the last 7 days.`
1047 : "Merged pull requests show up here the moment they land."}
1048 </p>
1049 </Empty>
1050 ) : (
1051 <List>
1052 {(shown as LandedRow[]).map((row, index) => (
1053 <LandedCard key={row.key} row={row} first={index === 0} />
1054 ))}
1055 </List>
1056 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1057
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1058 {rows.length > ROWS && (
1059 <div className="flex items-center justify-between border-t border-line px-4 py-2.5 text-xs text-muted sm:px-5">
1060 <span>
1061 Showing {shown.length} of {rows.length}
1062 </span>
1063 <Link
1064 to={link({ all: all ? null : "1" })}
1065 preventScrollReset
1066 className="inline-flex items-center gap-1 font-medium text-fg-soft hover:text-fg"
1067 >
1068 {all ? "Show fewer" : "See all"} <ChevronRight size={12} />
1069 </Link>
1070 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1071 )}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1072 </section>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1073
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1074 <aside className="min-w-0 space-y-6">
1075 <section className="rounded-xl border border-line bg-surface p-5">
1076 <div className="flex items-center justify-between">
1077 <h2 className="text-base font-semibold tracking-tight">This week</h2>
1078 {workspace && (
1079 <Link to={`/${workspace}/-/agents`} className="text-xs text-muted hover:text-fg">
1080 Fleet
1081 </Link>
1082 )}
1083 </div>
1084 <div className="mt-4">{loaded.perRepoLoaded ? <WeekChart week={week} /> : <Unavailable what="The week" />}</div>
1085 {workspace && stats.weekCost > 0 && (
1086 <Link
1087 to={`/${workspace}/-/usage`}
1088 className="mt-4 flex items-center justify-between border-t border-line pt-3 text-xs text-muted hover:text-fg"
1089 >
1090 <span>Agents and sandboxes cost {usd(stats.weekCost)} this week</span>
1091 <ChevronRight size={12} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1092 </Link>
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1093 )}
1094 </section>
1095
1096 <section id="activity" className="scroll-mt-20 rounded-xl border border-line bg-surface p-5">
1097 <div className="flex items-center justify-between">
1098 <h2 className="text-base font-semibold tracking-tight">Activity</h2>
1099 {groups.length > 8 && (
1100 <Link
1101 to={`${link({ activity: everyActivity ? null : "all" })}#activity`}
1102 preventScrollReset
1103 className="inline-flex items-center gap-0.5 text-xs text-muted hover:text-fg"
1104 >
1105 {everyActivity ? "Less" : "All activity"} <ChevronRight size={12} />
1106 </Link>
1107 )}
1108 </div>
1109 <p className="mt-1 flex items-center gap-3 text-[0.6875rem] text-faint">
1110 <span className="inline-flex items-center gap-1.5">
1111 <Avatar name="g1t-agent" size={12} /> agents
1112 </span>
1113 <span className="inline-flex items-center gap-1.5">
1114 <Avatar name={viewer.username} size={12} /> people
1115 </span>
1116 </p>
1117 {!loaded.perRepoLoaded ? (
1118 <div className="mt-3">
1119 <Unavailable what="Activity" />
1120 </div>
1121 ) : feed.length === 0 ? (
1122 <p className="mt-3 text-sm leading-6 text-muted">
1123 Nothing has moved yet. Merges, deploys, checks, reviews and what agents learn show up here.
1124 </p>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1125 ) : (
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1126 <ol className="mt-2 divide-y divide-line/70">
1127 {feed.map((group) => (
1128 <FeedLine key={group.id} group={group} titles={titles} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1129 ))}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1130 </ol>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1131 )}
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1132 </section>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1133
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms1134 <section className="rounded-xl border border-line bg-surface p-5">
1135 <h2 className="text-sm font-semibold">Connect your own agent</h2>
1136 <p className="mt-1.5 text-xs leading-5 text-muted">
1137 Add g1t to your coding agent. It signs in through your browser; there is no token to copy.
1138 </p>
1139 <AgentSetup className="mt-3" />
1140 <Link
1141 to="https://docs.g1t.sh/guides/bring-your-own-agent/"
1142 className="mt-3 inline-flex items-center gap-1 text-xs text-muted hover:text-fg"
1143 >
1144 How it works <ArrowRight size={12} />
1145 </Link>
1146 </section>
1147 </aside>
1148 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1149 </main>
1150 );
1151}