Skip to content

g1t/apps/web/app/components/shell.tsx

1,773 lines72,461 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.

Navigation: the workspace's own page with tabs, pins and recent in the sidebar1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, KanbanSquare, KeyRound, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas3import { Form, Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all5import type { Abilities, InboxCounts, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Search across all of g1t, Explore, and a command palette7import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all8import { AskAi, InboxBell } from "./inbox";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas9import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height10import { Logo, Mark } from "./logo";
Search across all of g1t, Explore, and a command palette11import { Avatar, notACredential } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents12import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell13import {
14 DropdownMenu,
15 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas16 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell17 DropdownMenuItem,
18 DropdownMenuLabel,
19 DropdownMenuSeparator,
20 DropdownMenuTrigger,
21} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales22import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23import { SETTINGS_CAPABILITY, type ViewerAccess, seesSettings } from "../lib/access";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put24import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas25import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
26import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put27import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas29import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar30import { type ShortcutProject, movedPin, recentWith } from "../lib/pins";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas31import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell32
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put33/**
34 * What the sidebar needs, worked out by the root loader. For a visitor who
35 * is not signed in there is no workspace, no projects and no usage: only
36 * the project being looked at, if they can see it.
37 */
Agents as a team: lifecycle, merge queue, billing and a new shell38export type ShellData = {
39 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control40 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar41 /** All its projects, by name, for the palette: `name` is the slug in their address. */
42 repos: ShortcutProject[];
43 /** The person's pinned projects in it, in their order (lib/pins.ts). */
44 pinned?: ShortcutProject[];
45 /** What they opened there last, latest first, leaving out the pinned. */
46 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47 /** Repositories shared with them in workspaces they do not belong to. */
48 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page49 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell50 repo: {
51 namespace: string;
52 name: string;
53 member: boolean;
54 issues: number;
55 pulls: number;
56 } | null;
Billing accounts, terms and enterprises; g1t is no longer free57 /** Where the workspace stands against its usage limit, if billing is on. */
58 limit: {
59 exposureMicros: number;
60 ceilingMicros: number | null;
61 state: "ok" | "warning" | "stopped";
62 comped: boolean;
63 } | null;
Free while g1t is being built out; agents can check out their own forks64 /** Whether g1t charges nothing for now, while it is being built out. */
65 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request66 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy67 /** This month's usage: charged, or at cost while g1t is free. */
68 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
70 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all71 /** What is unread in their inbox, for the bell. Absent for a visitor. */
72 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell73};
74
75function SidebarLink({
76 to,
77 icon,
78 end,
79 count,
80 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell82 children,
83}: {
84 to: string;
85 icon: ReactNode;
86 end?: boolean;
87 count?: number;
Projects: what a workspace builds and runs, first on every page88 /** Other path prefixes under which this link is the current one. */
89 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look90 /** It opens a list of its own: a chevron says so, always or on hover. */
91 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell92 children: ReactNode;
93}) {
94 const { pathname } = useLocation();
95 return (
96 <NavLink
97 to={to}
98 end={end}
99 prefetch="intent"
100 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page101 const current =
102 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark103 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
Agents as a team: lifecycle, merge queue, billing and a new shell104 current
105 ? "bg-raised font-medium text-fg"
106 : isPending
107 ? "bg-raised/60 text-fg"
108 : "text-muted hover:bg-raised/60 hover:text-fg"
109 }`;
110 }}
111 >
112 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
113 <span className="min-w-0 grow truncate">{children}</span>
114 {count != null && count > 0 && (
115 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
116 {count}
117 </span>
118 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 {drill && (
120 <ChevronRight
121 size={14}
122 aria-hidden="true"
123 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
124 />
125 )}
Agents as a team: lifecycle, merge queue, billing and a new shell126 </NavLink>
127 );
128}
129
Pull requests say where they are live; the project menu shows what is coming130/**
131 * Something a project will have, shown where it will live so people can
132 * see where g1t is going. Not a link: nothing is behind it yet.
133 */
134function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
135 return (
136 <div
137 title={about}
138 aria-disabled="true"
139 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
140 >
141 <span className="shrink-0 opacity-70">{icon}</span>
142 <span className="grow truncate">{children}</span>
143 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
144 Soon
145 </span>
146 </div>
147 );
148}
149
The project menu opens by section, and the workspace has its own panel150/**
A flat project menu with Code first, views as tabs, and Stripe for every owner151 * A page that is coming, as a link to its roadmap page: faint, with Soon,
152 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel153 */
A flat project menu with Code first, views as tabs, and Stripe for every owner154function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel155 to,
156 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner157 icon,
158 about,
The project menu opens by section, and the workspace has its own panel159 children,
160}: {
161 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner162 also?: string[];
163 icon: ReactNode;
164 about: string;
The project menu opens by section, and the workspace has its own panel165 children: ReactNode;
166}) {
167 const { pathname } = useLocation();
168 return (
169 <NavLink
170 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner171 title={about}
The project menu opens by section, and the workspace has its own panel172 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner173 className={({ isActive }) => {
174 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
175 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
176 current ? "bg-raised font-medium text-fg" : "text-faint hover:bg-raised/60 hover:text-muted"
The project menu opens by section, and the workspace has its own panel177 }`;
178 }}
179 >
A flat project menu with Code first, views as tabs, and Stripe for every owner180 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel181 <span className="min-w-0 grow truncate">{children}</span>
182 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
183 Soon
184 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales185 </NavLink>
The project menu opens by section, and the workspace has its own panel186 );
187}
188
A flat project menu with Code first, views as tabs, and Stripe for every owner189/** Icons for what spans projects. */
190const WORKSPACE_ICONS: Record<string, ReactNode> = {
191 board: <KanbanSquare size={15} />,
192 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member193 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner194 packages: <Package size={15} />,
195 fleet: <Bot size={15} />,
196};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales197
A flat project menu with Code first, views as tabs, and Stripe for every owner198/** The Soon pages of a section, as paths, so its link is current on them. */
199function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
200 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales201}
202
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look203function SidebarGroup({
204 title,
205 action,
206 className = "",
207 children,
208}: {
209 title: string;
210 action?: ReactNode;
211 className?: string;
212 children: ReactNode;
213}) {
Agents as a team: lifecycle, merge queue, billing and a new shell214 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 <section className={className}>
A Vercel-like sidebar and a new mark216 <div className="mb-1 flex h-6 items-center justify-between px-2">
217 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell218 {action}
219 </div>
220 <div className="space-y-px">{children}</div>
221 </section>
222 );
223}
224
Workspace names and icons, and a component kit for every control225/** What a workspace is called where people read it: its display name, else its slug. */
226function displayName(membership: Membership): string {
227 return membership.name?.trim() || membership.slug;
228}
229
Agents as a team: lifecycle, merge queue, billing and a new shell230function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control231 const workspace = shell.workspace;
232 const current = workspace?.slug;
233 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell234 return (
235 <DropdownMenu>
Workspace names and icons, and a component kit for every control236 {/* The name goes to the workspace; only the arrows switch it. The
237 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark238 <Link
239 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents240 prefetch="intent"
Workspace names and icons, and a component kit for every control241 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 className="flex h-9 min-w-0 grow items-center gap-2 rounded-md px-2 transition-colors hover:bg-raised"
A Vercel-like sidebar and a new mark243 >
Workspace names and icons, and a component kit for every control244 {workspace ? (
245 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell246 ) : (
Workspace names and icons, and a component kit for every control247 <span className="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell248 )}
Workspace names and icons, and a component kit for every control249 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark250 </Link>
251 <DropdownMenuTrigger
252 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look253 className="flex h-9 w-7 shrink-0 items-center justify-center rounded-md text-faint outline-none transition-colors hover:bg-raised hover:text-fg data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark254 >
255 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell256 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control257 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar258 {workspace && (
259 <>
260 {/* The workspace's own page: its projects, packages and people. */}
261 <DropdownMenuItem asChild>
262 <Link to={`/${workspace.slug}`}>
263 <LayoutGrid />
264 Workspace overview
265 </Link>
266 </DropdownMenuItem>
267 <DropdownMenuItem asChild>
268 <Link to={`/${workspace.slug}/-/projects`}>
269 <Box />
270 All projects
271 </Link>
272 </DropdownMenuItem>
273 <DropdownMenuSeparator />
274 </>
275 )}
Agents as a team: lifecycle, merge queue, billing and a new shell276 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
277 {(user.workspaces ?? []).map((membership) => (
278 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control279 <Link to={`/${membership.slug}`} title={displayName(membership)}>
280 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
281 <span className="flex min-w-0 grow flex-col leading-tight">
282 <span className="truncate">{displayName(membership)}</span>
283 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
284 </span>
285 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
286 {membership.slug === current ? (
287 <Check className="shrink-0 text-accent" />
288 ) : (
289 <span className="size-4 shrink-0" aria-hidden="true" />
290 )}
Agents as a team: lifecycle, merge queue, billing and a new shell291 </Link>
292 </DropdownMenuItem>
293 ))}
294 <DropdownMenuSeparator />
295 <DropdownMenuItem asChild>
296 <Link to="/workspaces/new">
297 <Plus />
298 New workspace
299 </Link>
300 </DropdownMenuItem>
301 </DropdownMenuContent>
302 </DropdownMenu>
A Vercel-like sidebar and a new mark303 );
304}
305
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas306/**
307 * The fine print, as one quiet row at the foot of the account menu and the
308 * visitor's panel: each link a full-height target, left-aligned. In the
309 * menu each is a menu item, so the arrow keys reach it.
310 */
311const LEGAL_LINKS: [string, string][] = [
312 ["Policies", "/policies"],
313 ["Privacy", "/policies/privacy"],
314 ["Security", "/security"],
315];
316const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
317
318function MenuLegalRow() {
319 return (
320 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
321 {LEGAL_LINKS.map(([label, to], index) => (
322 <span key={to} className="flex items-center gap-0.5">
323 {index > 0 && (
324 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
325 ·
326 </span>
327 )}
328 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
329 <Link to={to}>{label}</Link>
330 </DropdownMenuItem>
331 </span>
332 ))}
333 </div>
334 );
335}
336
337/** Status's row: the live dot and two or three words, fetched when the menu opens. */
338function StatusSummary({ open }: { open: boolean }) {
339 const status = useSiteStatus(open);
340 const words = statusTitle(status);
341 return (
342 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
343 {/* The words say it; the dot's own words for screen readers would repeat them. */}
344 <span aria-hidden="true" className="flex">
345 <StatusDot state={status?.overall.state ?? null} />
346 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents347 {words ? <span className="truncate">{words}</span> : <Skeleton className="h-3 w-20" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas348 </span>
349 );
350}
351
352/** A menu row: an icon, words, and whatever sits at its end. */
353const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
354
A Vercel-like sidebar and a new mark355function AccountMenu({ user }: { user: User }) {
356 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas357 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents358 // Name, primary address and invites left: asked for once, as soon as the
359 // pointer or focus reaches the button, so they are there when it opens.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas360 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents361 const prefetch = () => {
362 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
363 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas364 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents365 if (open) prefetch();
366 // eslint-disable-next-line react-hooks/exhaustive-deps
367 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas368 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents369 // Still on its way: shapes where the words will be, the same size.
370 const loading = details.data === undefined && details.state !== "idle";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas371 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark372 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas373 <DropdownMenu open={open} onOpenChange={setOpen}>
374 <DropdownMenuTrigger
375 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents376 onPointerEnter={prefetch}
377 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas378 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-merged/60 data-[state=open]:bg-raised"
379 >
Workspace names and icons, and a component kit for every control380 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark381 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas382 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark383 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas384 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
385 {/* Who is signed in, and a way to their profile. */}
386 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
387 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
388 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents389 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
390 {loading ? (
391 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
392 ) : (
393 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
394 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas395 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 {loading ? (
397 <Skeleton className="mt-1 h-3 w-40" />
398 ) : (
399 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
400 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas401 </span>
A Vercel-like sidebar and a new mark402 </Link>
403 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas404 <DropdownMenuSeparator className="my-1.5" />
405 <DropdownMenuGroup>
406 <DropdownMenuItem asChild className={MENU_ROW}>
407 <Link to={profile}>
408 <CircleUserRound />
409 Your profile
410 </Link>
411 </DropdownMenuItem>
412 <DropdownMenuItem asChild className={MENU_ROW}>
413 <Link to={FIRST_SETTINGS_PAGE}>
414 <Settings />
415 Your settings
416 </Link>
417 </DropdownMenuItem>
418 <DropdownMenuItem asChild className={MENU_ROW}>
419 <Link to="/settings/invites">
420 <Ticket />
421 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents422 {loading && <Skeleton className="ml-auto h-4 w-12 rounded-full" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas423 {me?.invites_left != null && (
424 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
425 {me.invites_left} left
426 </span>
427 )}
428 </Link>
429 </DropdownMenuItem>
430 </DropdownMenuGroup>
431 <DropdownMenuSeparator className="my-1.5" />
432 <DropdownMenuGroup>
433 <DropdownMenuItem asChild className={MENU_ROW}>
434 <a href="https://docs.g1t.sh/">
435 <BookOpen />
436 Documentation
437 </a>
438 </DropdownMenuItem>
439 <DropdownMenuItem asChild className={MENU_ROW}>
440 <Link to="/support">
441 <LifeBuoy />
442 Support
443 </Link>
444 </DropdownMenuItem>
445 <DropdownMenuItem asChild className={MENU_ROW}>
446 <a href={STATUS_URL}>
447 <Activity />
448 Status
449 <StatusSummary open={open} />
450 </a>
451 </DropdownMenuItem>
452 </DropdownMenuGroup>
453 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark454 {/* Submitted from here: the menu closes on select, and a button
455 that has left the page cannot submit a form. */}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas456 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark457 <LogOut />
458 Sign out
459 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas460 <DropdownMenuSeparator className="my-1.5" />
461 <MenuLegalRow />
A Vercel-like sidebar and a new mark462 </DropdownMenuContent>
463 </DropdownMenu>
464 );
465}
466
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look467/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar468 * A workspace's settings pages, which the sidebar drills into: how it is
469 * set up and connected (guardrails, secrets, runners, integrations,
470 * webhooks), what it pays, its repositories, tokens and record. The main
471 * list keeps the places work happens and who belongs; every member can
472 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look473 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar474const SETTINGS_PAGE =
475 /^\/([^/]+)\/-\/(settings|repositories|tokens|guardrails|secrets|runners|integrations|webhooks|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look476/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page477const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings478
Polish: phones, copy boxes, the plan page, the landing page, a real glide479/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look480 * The sidebar's lists are a stack, the way a phone pushes a screen: one
481 * drilled into slides in from the right over the full width, while the one
482 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide483 * ease-out, the fade quicker than the move so the two never blur together.
484 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide485 */
Sidebar: no settings flash on the way into a repository, and a better slide486const LAYER =
Sidebar: the panels really slide487 "absolute inset-0 [transition:translate_380ms_cubic-bezier(0.32,0.72,0,1),opacity_220ms_ease-out] will-change-[translate,opacity] motion-reduce:transition-none";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look488/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide489const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings490
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look491/** One list in the stack: what it is, and its rows. */
492type Level = { key: string; node: ReactNode };
493
494/** True once the page has hydrated, so what the server drew never animates in. */
495function useHydrated(): boolean {
496 const [hydrated, setHydrated] = useState(false);
497 useEffect(() => setHydrated(true), []);
498 return hydrated;
499}
500
501/**
502 * Drilling in and out of the sidebar's lists. `trail` is the way from the
503 * main list to the one shown, worked out from the address, so a link
504 * straight to a settings page opens the sidebar already drilled in, and
505 * leaving it comes back out. Lists deeper than the one shown stay in
506 * place, off to the right, so going back slides them away rather than
507 * dropping them. Moving to a list at the same depth (a workspace's settings
508 * to a project) fades the new one in where it is.
509 */
510function Drill({ trail }: { trail: Level[] }) {
511 const depth = trail.length - 1;
512 const hydrated = useHydrated();
513 const stack = useRef<Level[]>(trail);
514 const before = stack.current;
515 const within = trail.every((level, index) => before[index]?.key === level.key);
516 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
517 const levels = stack.current;
518 // What was showing last time, to tell drilling in from moving across.
519 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
520 useEffect(() => {
521 shown.current = { depth, keys: levels.map((level) => level.key) };
522 });
A catalogue of model providers, and settings that feel like settings523 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look524 <div className="relative min-h-0 grow overflow-hidden">
525 {levels.map((level, index) => {
526 const current = index === depth;
527 const place =
528 index < depth
529 ? "pointer-events-none -translate-x-1/4 opacity-0"
530 : current
531 ? "translate-x-0 opacity-100"
532 : "pointer-events-none translate-x-full opacity-0";
533 // A list that was not there before arrives: from the right when
534 // drilling in, in place when moving across.
535 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
536 const enter = arriving
537 ? depth > shown.current.depth
538 ? "starting:translate-x-full starting:opacity-0"
539 : "starting:opacity-0"
540 : "";
541 return (
542 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
543 {level.node}
544 </div>
545 );
546 })}
547 </div>
548 );
549}
550
551/**
552 * The row at the top of a list drilled into: the list's name, which goes
553 * back out, and what it belongs to, faint at the end.
554 */
555function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
556 return (
557 <Link
558 to={to}
559 prefetch="intent"
560 className="group mt-3 flex h-8 items-center gap-1.5 rounded-md pr-2 pl-1 text-[0.8125rem] font-medium text-fg transition-colors hover:bg-raised/60"
A catalogue of model providers, and settings that feel like settings561 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look562 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
563 <span className="min-w-0 shrink-0 truncate">{label}</span>
564 {context && (
565 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
566 )}
567 </Link>
568 );
569}
570
571/** A thin rule between groups of a list. */
572function Rule() {
573 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
574}
575
Navigation: the workspace's own page with tabs, pins and recent in the sidebar576/** A quiet heading inside a group: Pinned, Recent. */
577function SidebarSubhead({ children }: { children: ReactNode }) {
578 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
579}
580
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look581/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar582 * The workspace's projects as the sidebar keeps them, however many there
583 * are: the person's pins in their order, what they opened last, and the
584 * way to all of them. Pins move by dragging, or with Alt and the arrow
585 * keys; the projects service keeps the order.
586 */
587function SidebarProjects({ slug, shell }: { slug: string; shell: ShellData }) {
588 const reorder = useFetcher({ key: `pins:${slug}` });
589 const saved = shell.pinned ?? [];
590 // While a new order is on its way, it shows as made.
591 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
592 const pinned = asked
593 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
594 : saved;
595 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
596 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
597 const [dragging, setDragging] = useState<number | null>(null);
598 const move = (from: number, by: number) => {
599 const next = movedPin(pinned, from, by);
600 if (!next) return;
601 const form = new FormData();
602 form.set("intent", "reorder");
603 for (const project of next) form.append("slug", project.name);
604 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
605 };
606 const row = (project: ShortcutProject) => (
607 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
608 {project.title ?? project.name}
609 </SidebarLink>
610 );
611 return (
612 <SidebarGroup
613 title="Projects"
614 action={
615 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
616 <Plus size={13} />
617 </Link>
618 }
619 >
620 <SidebarSubhead>Pinned</SidebarSubhead>
621 {pinned.length === 0 ? (
622 <p className="px-2 pb-1 text-xs text-faint">Pin projects to keep them here.</p>
623 ) : (
624 <ul aria-label="Pinned projects" className="space-y-px">
625 {pinned.map((project, index) => (
626 <li
627 key={project.name}
628 draggable={pinned.length > 1}
629 onDragStart={(event) => {
630 setDragging(index);
631 event.dataTransfer.effectAllowed = "move";
632 }}
633 onDragOver={(event) => {
634 if (dragging != null) event.preventDefault();
635 }}
636 onDrop={(event) => {
637 event.preventDefault();
638 if (dragging != null && dragging !== index) move(dragging, index - dragging);
639 setDragging(null);
640 }}
641 onDragEnd={() => setDragging(null)}
642 onKeyDown={(event) => {
643 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
644 event.preventDefault();
645 move(index, event.key === "ArrowUp" ? -1 : 1);
646 }}
647 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
648 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
649 >
650 {row(project)}
651 {pinned.length > 1 && (
652 <GripVertical
653 size={12}
654 aria-hidden="true"
655 className="pointer-events-none absolute top-1/2 -left-1.5 -translate-y-1/2 text-faint opacity-0 transition-opacity group-hover/pin:opacity-100"
656 />
657 )}
658 </li>
659 ))}
660 </ul>
661 )}
662 {recent.length > 0 && (
663 <>
664 <SidebarSubhead>Recent</SidebarSubhead>
665 {recent.map((project) => (
666 <div key={`${project.namespace}/${project.name}`}>{row(project)}</div>
667 ))}
668 </>
669 )}
670 <div className="pt-1">
671 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length}>
672 All projects
673 </SidebarLink>
674 </div>
675 </SidebarGroup>
676 );
677}
678
679/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look680 * The main list: where you go, the projects, what the workspace builds and
681 * runs with across them, then its usage, support and settings.
682 */
683function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
684 const ws = shell.workspace;
685 // A visitor browses: no workspace, no projects of their own.
686 if (!user) {
687 return (
688 <nav aria-label="g1t" className={PANEL}>
689 <div className="mt-3 space-y-px">
690 {VISITOR_LINKS.map((link) => (
691 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
692 {link.label}
693 </SidebarLink>
694 ))}
695 </div>
696 </nav>
697 );
698 }
699 return (
700 <nav aria-label="g1t" className={PANEL}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar701 {/* Everything here is the workspace the switcher names: its home first.
702 The workspace's own page is the switcher's; Explore, all of g1t,
703 is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look704 <div className="mt-3 space-y-px">
705 <SidebarLink to="/" end icon={<House size={15} />}>
706 Mission control
707 </SidebarLink>
708 </div>
709
710 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar711 {ws ? (
712 <SidebarProjects slug={ws.slug} shell={shell} />
713 ) : (
714 <SidebarGroup title="Projects">
715 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
716 </SidebarGroup>
717 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look718 {(shell.shared ?? []).length > 0 && (
719 <SidebarGroup title="Shared with you" className="mt-3">
720 {(shell.shared ?? []).map((repo) => (
721 <SidebarLink
722 key={`${repo.namespace}/${repo.name}`}
723 to={`/${repo.namespace}/${repo.name}`}
724 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
725 drill="hover"
726 >
727 <span className="font-mono text-faint">{repo.namespace}/</span>
728 {repo.name}
729 </SidebarLink>
730 ))}
731 </SidebarGroup>
732 )}
733
734 {ws && (
735 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member736 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar737 <SidebarGroup title="Workspace">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look738 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
739 Agent fleet
740 </SidebarLink>
741 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
742 Context
743 </SidebarLink>
744 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
745 Memory
746 </SidebarLink>
747 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
748 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents749 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member750 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
751 Packages
752 </SidebarLink>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar753 {roadmapIn("Workspace").filter((item) => item.key !== "teams" && item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look754 <SidebarSoonLink
755 key={item.key}
756 to={`/${ws.slug}/-/soon/${item.key}`}
757 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
758 about={item.summary}
759 >
760 {item.title === "Board" ? "Boards" : item.title}
761 </SidebarSoonLink>
762 ))}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar763 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look764
765 <Rule />
766 <div className="space-y-px">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar767 {/* Who belongs, for every member to see; owners invite and manage there. */}
768 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar769 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar770 </SidebarLink>
771 {roadmapIn("Workspace")
772 .filter((item) => item.key === "teams")
773 .map((item) => (
Navigation: the workspace's own page with tabs, pins and recent in the sidebar774 <SidebarSoonLink key={item.key} to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS[item.key]} about={item.summary}>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar775 {item.title}
776 </SidebarSoonLink>
777 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look778 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
779 Usage
780 </SidebarLink>
781 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
782 Support
783 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar784 {/* How it is set up and connected, what it pays and its record: a list of their own. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member786 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/repositories`}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 icon={<Settings size={15} />}
788 drill
789 >
790 Settings
791 </SidebarLink>
792 </div>
793 </>
794 )}
795 </nav>
796 );
797}
798
799/** A workspace's settings, drilled into from Settings in the main list. */
800function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
801 return (
802 <nav aria-label="Workspace settings" className={PANEL}>
803 <BackRow to={`/${slug}`} label="Settings" context={slug} />
804 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings805 {owner && (
806 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
807 General
808 </SidebarLink>
809 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look810 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
811 Repositories
812 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments813 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
814 Access tokens
815 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look816 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar817 <SidebarGroup title="Agents and runs" className="mt-3">
818 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
819 Guardrails
820 </SidebarLink>
821 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
822 Secrets and variables
823 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it824 {owner && (
825 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
826 Runners
827 </SidebarLink>
828 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar829 </SidebarGroup>
830 <SidebarGroup title="Connections" className="mt-3">
831 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
832 Integrations
833 </SidebarLink>
834 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
835 Webhooks
836 </SidebarLink>
837 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look838 <Rule />
839 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings840 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments841 Billing and plans
A catalogue of model providers, and settings that feel like settings842 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API843 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
844 Audit log
845 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look846 </div>
A catalogue of model providers, and settings that feel like settings847 </nav>
848 );
849}
850
Sidebar: no settings flash on the way into a repository, and a better slide851/**
852 * What the repository menu needs. Known from the address before the page's
853 * data arrives, so the menu is right from the first frame; the counts fill
854 * in once it does.
855 */
856type MenuRepo = {
857 namespace: string;
858 name: string;
859 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look860 /** Whether they see its settings; from the repository's page once it loads. */
861 settings?: boolean;
862 /** What they may do there, once the repository's page loads. */
863 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide864 issues?: number;
865 pulls?: number;
866};
867
868function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
869 return (
870 a != null &&
871 b != null &&
872 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
873 a.name.toLowerCase() === b.name.toLowerCase()
874 );
875}
A repository has its own sidebar, as settings do876
877/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look878 * A project's own list, drilled into while you are in it: everything about
879 * the project, running and its code, and nothing else, with the way back
880 * out to all of them.
A repository has its own sidebar, as settings do881 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put882function RepoMenu({
883 repo,
884 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look885 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put886}: {
887 repo: MenuRepo;
888 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look889 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
890 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put891}) {
A repository has its own sidebar, as settings do892 const base = `/${repo.namespace}/${repo.name}`;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put893 // What a member sees, and what everyone who can see the project does.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look894 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do895 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look896 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
897 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner898 <NavLink
A repository has its own sidebar, as settings do899 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner900 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents901 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner902 title="Overview"
903 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look904 `mt-2 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
A flat project menu with Code first, views as tabs, and Stripe for every owner905 }
A repository has its own sidebar, as settings do906 >
907 <span className="flex size-6 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
Projects: what a workspace builds and runs, first on every page908 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do909 </span>
910 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
911 <span className="text-faint">{repo.namespace}/</span>
912 <span className="font-semibold text-fg">{repo.name}</span>
913 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner914 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look915 {/* A project's pages, in the order people use them. A page with more
916 than one view shows them as tabs across its top. */}
917 <Rule />
918 <div className="space-y-px">
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request919 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, `${base}/branches`, `${base}/tags`, `${base}/compare`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
A flat project menu with Code first, views as tabs, and Stripe for every owner920 Code
921 </SidebarLink>
922 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
923 Issues
924 </SidebarLink>
925 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
926 Pull requests
927 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains928 <SidebarLink to={`${base}/agents`} also={[`${base}/sessions`, `${base}/memory`, ...soonPaths(base, "Agents")]} icon={<Bot size={15} />}>
A flat project menu with Code first, views as tabs, and Stripe for every owner929 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains930 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner931 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
932 Workflows
Projects: what a workspace builds and runs, first on every page933 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look934 </div>
935 <Rule />
936 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put937 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner938 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
939 Deployments
940 </SidebarLink>
941 ) : null}
Observability gets its own page; tabs never overflow942 <SidebarSoonLink to={`${base}/soon/logs`} also={soonPaths(base, "Observability")} icon={<Activity size={15} />} about="Logs, errors, uptime and analytics of the project's deployed apps.">
943 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner944 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put945 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API946 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
947 Security
948 </SidebarLink>
949 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner950 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
951 Insights
952 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look953 </div>
954 {(repo.settings ?? shows.has("settings")) && (
955 <>
956 <Rule />
957 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner958 Settings
959 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look960 </>
961 )}
A repository has its own sidebar, as settings do962 </nav>
963 );
964}
965
Project settings move into the sidebar, grouped, with a heading per page966/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look967 * A project's settings, drilled into from its list: what is about running
968 * it, then its agents, then its code and who can reach it. The way back
969 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page970 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look971function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page972 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look973 // Each page shows to the roles that can use it, once the role is known.
974 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
975 const running = shows("deployments");
976 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page977 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look978 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
979 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
980 {shows("") && (
981 <div className="mt-2 space-y-px">
982 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
983 General
984 </SidebarLink>
985 </div>
986 )}
987 {running && (
988 <>
989 {shows("") && <Rule />}
990 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
991 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
992 Deployments
993 </SidebarLink>
994 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
995 Domains
996 </SidebarLink>
997 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
998 Dependencies
999 </SidebarLink>
1000 </div>
1001 </>
1002 )}
1003 {agents && (
1004 <>
1005 <Rule />
1006 <div className="space-y-px">
1007 {shows("agents") && (
1008 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1009 Agents
1010 </SidebarLink>
1011 )}
1012 {shows("guardrails") && (
1013 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1014 Guardrails
1015 </SidebarLink>
1016 )}
1017 </div>
1018 </>
1019 )}
1020 <Rule />
1021 <div className="space-y-px">
1022 {shows("repository") && (
1023 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1024 Repository
1025 </SidebarLink>
1026 )}
1027 {shows("access") && (
1028 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1029 Access
1030 </SidebarLink>
1031 )}
1032 {shows("branches") && (
1033 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1034 Branches and merging
1035 </SidebarLink>
1036 )}
1037 {shows("secrets") && (
1038 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1039 Secrets and variables
1040 </SidebarLink>
1041 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1042 {shows("runners") && (
1043 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1044 Runners
1045 </SidebarLink>
1046 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1047 {shows("webhooks") && (
1048 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1049 Webhooks
1050 </SidebarLink>
1051 )}
1052 </div>
Project settings move into the sidebar, grouped, with a heading per page1053 </nav>
1054 );
1055}
1056
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1057/** Your own settings, drilled into from Your settings: one page each. */
1058function AccountSettingsMenu({ username }: { username: string }) {
1059 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1060 <SidebarLink to={`/settings/${page}`} icon={icon}>
1061 {ACCOUNT_SETTINGS[page].title}
1062 </SidebarLink>
Account settings in the same shape as a workspace's1063 );
1064 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1065 <nav aria-label="Your settings" className={PANEL}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1066 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1067 <div className="mt-2 space-y-px">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1068 {link("profile", <CircleUserRound size={15} />)}
1069 {link("emails", <Mail size={15} />)}
1070 {link("invites", <Ticket size={15} />)}
1071 </div>
1072 <Rule />
1073 <div className="space-y-px">
1074 {link("keys", <Fingerprint size={15} />)}
1075 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1076 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1077 <Rule />
1078 <div className="space-y-px">
1079 {link("github", <GithubMark className="size-[15px]" />)}
1080 {link("applications", <Plug size={15} />)}
1081 </div>
1082 <Rule />
1083 <div className="space-y-px">
1084 {link("security-log", <History size={15} />)}
1085 </div>
Account settings in the same shape as a workspace's1086 </nav>
1087 );
1088}
1089
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1090/**
1091 * Signing in and signing up, in place of the account for a visitor. Signing
1092 * in brings them back to the page they are on.
1093 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1094/** Help, status and the fine print under a visitor's sign-in buttons. */
1095function VisitorLinks() {
1096 const status = useSiteStatus();
1097 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1098 const dot = (
1099 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1100 ·
1101 </span>
1102 );
1103 return (
1104 // Two short rows, so a narrow sidebar never starts a line with a dot.
1105 <nav aria-label="About g1t" className="pt-0.5">
1106 <div className="flex items-center gap-0.5">
1107 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1108 <StatusDot state={status?.overall.state ?? null} />
1109 Status
1110 </a>
1111 {dot}
1112 <Link to="/support" className={quiet}>
1113 Support
1114 </Link>
1115 </div>
1116 <div className="flex items-center gap-0.5">
1117 {LEGAL_LINKS.map(([label, to], index) => (
1118 <span key={to} className="flex items-center gap-0.5">
1119 {index > 0 && dot}
1120 <Link to={to} className={quiet}>
1121 {label}
1122 </Link>
1123 </span>
1124 ))}
1125 </div>
1126 </nav>
1127 );
1128}
1129
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1130function VisitorPanel() {
1131 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1132 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1133 return (
1134 <div className="space-y-2">
1135 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1136 <div className="grid grid-cols-2 gap-2">
1137 <Link
1138 to={withNext("/login", pathname + search)}
1139 className="flex h-9 items-center justify-center gap-1.5 rounded-md border border-line text-[0.8125rem] font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg"
1140 >
1141 <LogIn size={14} />
1142 Sign in
1143 </Link>
1144 <Link
1145 to={withNext("/register", pathname + search)}
1146 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1147 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1148 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1149 </Link>
1150 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1151 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1152 </div>
1153 );
1154}
1155
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1156function Sidebar({
1157 user,
1158 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1159 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1160 onFind,
1161 onClose,
1162}: {
1163 user: User | null;
1164 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1165 /** The page is a 404: the address names nothing the viewer can see. */
1166 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1167 onFind: () => void;
1168 /** In the sheet on a small screen: closing it, at the end of the top row. */
1169 onClose?: () => void;
1170}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1171 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1172 const { pathname } = useLocation();
1173 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1174 // Where the sidebar is drilled to follows the page being gone to, so it
1175 // moves as the link is followed, not once the page arrives.
1176 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1177 // On a 404, the address is not a place: no list is drilled into from it,
1178 // except the project's own when the project is real and visible and only
1179 // something inside it is missing (a file, a commit, an issue).
1180 const lost = missing && going == null;
1181 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1182 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1183 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1184 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1185 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1186 const reserved = new Set(["settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth", "policies", "security", "support", "status", "invite", "inbox", ".well-known"]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1187 // An invitation to a repository is answered before its menu means anything.
1188 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1189 repoPath != null &&
1190 !reserved.has(repoPath[1]) &&
1191 repoPath[2] !== "-" &&
1192 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1193 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1194 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1195 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1196 // The repository the menu is for: the one loaded if it is the one being
1197 // gone to, else what the address says, at once.
1198 const targetRepo = inRepo && repoPath ? { namespace: repoPath[1], name: repoPath[2] } : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1199 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1200 ? sameRepo(active, targetRepo)
1201 ? active
1202 : {
1203 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1204 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1205 }
1206 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1207 // The viewer's role there, from the repository's page once it has loaded.
1208 const page = useRouteLoaderData("routes/repo/layout") as
1209 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1210 | undefined;
1211 const menuRepo: MenuRepo | null =
1212 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1213 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1214 : guessed;
1215
1216 // The way from the main list to the one shown.
1217 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1218 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1219 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1220 } else if (ws && inSettings) {
1221 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1222 } else if (menuRepo) {
1223 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1224 // Out of a project: to its workspace's projects when they are yours.
1225 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1226 const back = !user
1227 ? { to: "/explore", label: "Explore" }
1228 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1229 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1230 : { to: "/", label: "Mission control" };
1231 trail.push({
1232 key,
1233 node: (
1234 <RepoMenu
1235 repo={menuRepo}
1236 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1237 back={back}
1238 />
1239 ),
1240 });
1241 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1242 }
1243
Agents as a team: lifecycle, merge queue, billing and a new shell1244 return (
1245 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1246 {/* The same height and rule as the top bar, so the two read as one line. */}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1247 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2 pl-2.5">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1248 {user ? (
1249 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1250 {/* The 1 alone beside the workspace: a square hover the height of the
1251 switcher, the mark as tall as the workspace avatar. */}
1252 <Link to="/" aria-label="g1t home" className="flex size-9 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-raised">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1253 <Mark tight className="h-4 w-auto" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1254 </Link>
1255 <span className="shrink-0 text-line-strong" aria-hidden="true">
1256 /
1257 </span>
1258 <WorkspaceSwitcher user={user} shell={shell} />
1259 </>
1260 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1261 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1262 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1263 </Link>
1264 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1265 {onClose && (
1266 <button
1267 type="button"
1268 aria-label="Close menu"
1269 onClick={onClose}
1270 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1271 >
1272 <X size={16} />
1273 </button>
1274 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1275 </div>
A Vercel-like sidebar and a new mark1276 <div className="px-2 pt-3">
1277 <button
1278 type="button"
1279 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1280 className="flex h-9 w-full items-center gap-2 rounded-md bg-surface px-2.5 text-[0.8125rem] text-faint ring-1 ring-line transition-colors hover:text-muted hover:ring-line-strong"
A Vercel-like sidebar and a new mark1281 >
1282 <Search size={14} />
1283 <span className="grow text-left">Find…</span>
1284 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1285 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1286 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1287 <Drill trail={trail} />
1288 {/* The account, or signing in, as one row at the very bottom. */}
1289 <div className="shrink-0 border-t border-line p-2">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1290 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1291 </div>
1292 </div>
1293 );
1294}
1295
1296/** Words for the sections a path can end in. */
1297const SECTIONS: Record<string, string> = {
1298 issues: "Issues",
1299 pulls: "Pull requests",
1300 queue: "Merge queue",
1301 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1302 plans: "Outcomes",
1303 actions: "Workflows",
1304 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1305 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1306 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1307 access: "Access",
1308 invitations: "Invitation",
1309 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1310 branches: "Branches",
1311 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1312 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1313 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1314 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1315 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1316 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1317 people: "People",
1318 projects: "Projects",
1319 teams: "Teams",
A Vercel-like sidebar and a new mark1320 tokens: "Access tokens",
1321 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1322 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1323 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1324 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1325 domains: "Domains",
1326 guardrails: "Guardrails",
1327 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1328 tree: "Files",
1329 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1330 agents: "Agents",
1331 context: "Context",
1332 memory: "Memory",
1333 security: "Security",
1334 packages: "Packages",
1335 runners: "Runners",
1336 workflows: "Workflows",
1337 observability: "Observability",
1338 insights: "Insights",
1339 sessions: "Sessions",
A Vercel-like sidebar and a new mark1340};
1341
1342/** Where the page is, as a trail of links: workspace / repository / section. */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1343function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1344 // A 404 names nothing from the address, unless the project is real and
1345 // visible and only something inside it is missing.
1346 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1347 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1348 return <span className="text-sm font-medium">Not found</span>;
1349 }
A Vercel-like sidebar and a new mark1350 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1351 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1352 if (parts.length === 0) return <span className="text-sm font-medium">Mission control</span>;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1353 // Your settings: Settings / Emails.
1354 if (parts[0] === "settings") {
1355 const page = accountSettingsPage(pathname);
1356 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1357 return (
1358 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1359 {trail.map((crumb, index) => (
1360 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1361 {index > 0 && <span className="text-line-strong">/</span>}
1362 <Link
1363 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1364 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1365 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1366 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1367 }`}
1368 >
1369 {crumb.label}
1370 </Link>
1371 </span>
1372 ))}
1373 </nav>
1374 );
1375 }
A Vercel-like sidebar and a new mark1376 if (reserved.includes(parts[0]!)) {
1377 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1378 settings: "Account",
A Vercel-like sidebar and a new mark1379 explore: "Explore",
Projects live: fixes from walking them end to end1380 new: "New project",
A Vercel-like sidebar and a new mark1381 search: "Search",
1382 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1383 policies: "Policies",
1384 security: "Security",
1385 support: "Support",
1386 status: "Status",
1387 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1388 inbox: "Inbox",
A Vercel-like sidebar and a new mark1389 };
1390 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1391 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1392 // A person's profile, by their handle.
1393 if (parts[0] === "u" && parts[1]) {
1394 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1395 }
A Vercel-like sidebar and a new mark1396 const [owner, second, third, fourth] = parts;
1397 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1398 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1399 const page = `/${owner}/-/${third}`;
1400 if (third && SETTINGS_PAGE.test(page)) {
1401 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1402 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1403 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1404 } else if (second) {
1405 const repo = `/${owner}/${second}`;
1406 trail.push({ label: second, to: repo, mono: true });
1407 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1408 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1409 else if (third === "commit" && fourth) trail.push({ label: fourth.slice(0, 7), to: `${repo}/commit/${fourth}`, mono: true });
Secrets and variables: one list, rows per environment, for workflows and deployments1410 else if (third && SECTIONS[third]) {
1411 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1412 // A settings page names which one: Settings / Secrets and variables.
1413 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1414 trail.push({ label: fourth === "branches" ? "Branches and merging" : SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1415 }
1416 }
A Vercel-like sidebar and a new mark1417 }
1418 return (
1419 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1420 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1421 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1422 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1423 <Link
A Vercel-like sidebar and a new mark1424 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1425 prefetch="intent"
A Vercel-like sidebar and a new mark1426 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1427 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1428 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1429 >
A Vercel-like sidebar and a new mark1430 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1431 </Link>
A Vercel-like sidebar and a new mark1432 </span>
1433 ))}
1434 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1435 );
1436}
1437
Search across all of g1t, Explore, and a command palette1438type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1439
1440/** Everything the palette can jump to, from what the sidebar already knows. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1441function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1442 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1443 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1444 { label: "Mission control", to: "/", icon: <House size={15} /> },
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1445 ...(shell.repos.length > 0
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1446 ? [{ label: "Put an agent on it", hint: "Open an issue and assign g1t", to: "/?agent=new", icon: <Sparkles size={15} /> }]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1447 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1448 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1449 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1450 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1451 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1452 { label: "New workspace", to: "/workspaces/new", icon: <Plus size={15} /> },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1453 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1454 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1455 .filter((page) => page !== "profile")
1456 .map((page) => ({ label: ACCOUNT_SETTINGS[page].title, hint: "Your settings", to: `/settings/${page}`, icon: <Settings size={15} /> })),
Agents as a team: lifecycle, merge queue, billing and a new shell1457 ];
1458 const repo = shell.repo;
1459 if (repo) {
1460 const base = `/${repo.namespace}/${repo.name}`;
1461 const name = `${repo.namespace}/${repo.name}`;
1462 commands.unshift(
Projects: what a workspace builds and runs, first on every page1463 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1464 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1465 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1466 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1467 ...(repo.member
1468 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1469 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1470 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1471 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1472 ...(repo.member
1473 ? [
1474 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1475 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1476 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1477 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1478 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1479 ]
1480 : []),
1481 );
1482 }
1483 for (const membership of user.workspaces ?? []) {
1484 commands.push(
Workspace names and icons, and a component kit for every control1485 { label: displayName(membership), hint: `Workspace · ${membership.slug}`, to: `/${membership.slug}`, icon: <Avatar name={membership.slug} image={membership.avatar} size={15} square /> },
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1486 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1487 { label: "People", hint: membership.slug, to: `/${membership.slug}/-/people`, icon: <Users size={15} /> },
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1488 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1489 { label: "Billing and plans", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/billing`, icon: <CreditCard size={15} /> },
A catalogue of model providers, and settings that feel like settings1490 { label: "Access tokens", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/tokens`, icon: <KeyRound size={15} /> },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1491 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1492 );
1493 }
1494 for (const listed of shell.repos) {
1495 commands.push({
Projects: what a workspace builds and runs, first on every page1496 label: listed.title ?? listed.name,
1497 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1498 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1499 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1500 });
1501 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1502 // "Put an agent on …": one per project, after the projects themselves.
1503 for (const listed of shell.repos) {
1504 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1505 commands.push({
1506 label: `Put an agent on ${listed.title ?? listed.name}`,
1507 hint: `${listed.namespace}/${listed.name}`,
1508 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1509 icon: <Sparkles size={15} />,
1510 });
1511 }
Agents as a team: lifecycle, merge queue, billing and a new shell1512 return commands;
1513}
1514
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1515/** What the palette offers a visitor: browsing, the project they are in, and signing in. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1516function visitorCommands(shell: ShellData, here: string, signUpLabel: string): Command[] {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1517 const commands: Command[] = [];
1518 const repo = shell.repo;
1519 if (repo) {
1520 const base = `/${repo.namespace}/${repo.name}`;
1521 const name = `${repo.namespace}/${repo.name}`;
1522 commands.push(
1523 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1524 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1525 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1526 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1527 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1528 );
1529 }
1530 commands.push(
1531 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1532 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1533 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1534 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1535 { label: "Sign in", to: withNext("/login", here), icon: <LogIn size={15} /> },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1536 { label: signUpLabel, to: withNext("/register", here), icon: <Plus size={15} /> },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1537 );
1538 return commands;
1539}
1540
Agents as a team: lifecycle, merge queue, billing and a new shell1541/**
Merge branch 'worktree-agent-aca4790ea223aa235'1542 * The page while the next one loads: after a moment it dims, under the
1543 * bar below, so it is plainly on its way out rather than frozen, and the
1544 * next page fades up from there. Sending a form does not dim it: the
1545 * form's button says it is working. Spread on the element holding the page.
1546 */
1547export function useLeaving() {
1548 const navigation = useNavigation();
1549 const method = navigation.formMethod?.toUpperCase();
1550 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1551 return {
1552 "aria-busy": leaving || undefined,
1553 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1554 };
1555}
1556
1557/**
Agents as a team: lifecycle, merge queue, billing and a new shell1558 * A bar across the top of the page while the next one loads, as GitHub
1559 * has: it appears at once, creeps towards the end while waiting, then fills
1560 * and fades when the page arrives.
1561 */
1562export function Progress() {
1563 const navigation = useNavigation();
1564 const busy = navigation.state !== "idle";
1565 const [width, setWidth] = useState(0);
1566 const [visible, setVisible] = useState(false);
1567 useEffect(() => {
1568 if (busy) {
1569 setVisible(true);
1570 setWidth(12);
1571 // Slows as it goes, never quite arriving until the page does.
1572 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1573 return () => clearInterval(timer);
1574 }
1575 setWidth((now) => (now > 0 ? 100 : 0));
1576 const fade = setTimeout(() => setVisible(false), 250);
1577 const reset = setTimeout(() => setWidth(0), 550);
1578 return () => {
1579 clearTimeout(fade);
1580 clearTimeout(reset);
1581 };
1582 }, [busy]);
1583 return (
1584 <div
1585 aria-hidden="true"
1586 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1587 visible ? "opacity-100" : "opacity-0"
1588 }`}
1589 >
1590 <div
1591 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1592 style={{ width: `${width}%` }}
1593 />
1594 </div>
1595 );
1596}
1597
1598/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1599 * The app: a sidebar with the workspace, its repositories and the sections
1600 * of the one being looked at; a slim bar with search and the account; and
1601 * the page. A visitor who is not signed in gets the same frame, with
1602 * Explore and Search in place of the workspace, and signing in in place of
1603 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1604 */
1605export function AppShell({
1606 user,
1607 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1608 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1609 banner,
1610 children,
1611}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1612 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1613 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1614 /** The page is a 404 (see Sidebar). */
1615 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1616 banner?: ReactNode;
1617 children: ReactNode;
1618}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1619 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1620 const [drawer, setDrawer] = useState(false);
1621 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1622 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1623 const signUpLabel = useSignUpCopy().primary;
1624 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1625
1626 // A new page closes the drawer on small screens.
1627 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1628 // Mission control's code, fetched while the browser is idle, so going
1629 // home never waits on it (routes/home.tsx).
1630 useEffect(() => {
1631 if (!user) return;
1632 const load = () => void import("./mission-control").catch(() => undefined);
1633 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1634 if (idle) idle(load);
1635 else setTimeout(load, 1500);
1636 }, [user]);
Search across all of g1t, Explore, and a command palette1637 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1638 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1639
1640 return (
1641 <div className="min-h-screen">
1642 <Progress />
1643 <aside className="fixed inset-y-0 left-0 z-40 hidden w-64 border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))] lg:block">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1644 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1645 </aside>
1646 {drawer && (
1647 <div className="fixed inset-0 z-50 lg:hidden">
1648 <button
1649 type="button"
1650 aria-label="Close menu"
1651 className="absolute inset-0 bg-black/60"
1652 onClick={() => setDrawer(false)}
1653 />
1654 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1655 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1656 </aside>
1657 </div>
1658 )}
1659
Polish: phones, copy boxes, the plan page, the landing page, a real glide1660 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1661 <header className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-line bg-bg/85 px-4 backdrop-blur sm:px-6">
Agents as a team: lifecycle, merge queue, billing and a new shell1662 <button
1663 type="button"
1664 aria-label="Open menu"
1665 onClick={() => setDrawer(true)}
1666 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1667 >
1668 <Menu size={18} />
1669 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1670 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1671 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1672 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1673 <input
1674 name="q"
1675 {...notACredential()}
1676 placeholder="Search g1t"
1677 aria-label="Search g1t"
1678 className="h-9 w-full rounded-md border border-line bg-surface pr-12 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
1679 />
1680 <kbd className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">
1681 ⌘K
1682 </kbd>
1683 </Form>
1684 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1685 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1686 <NavLink
1687 to="/explore"
1688 prefetch="intent"
1689 aria-label="Explore"
1690 className={({ isActive }) =>
1691 `flex h-9 items-center gap-1.5 rounded-md px-2 text-sm transition-colors hover:bg-raised hover:text-fg sm:px-2.5 ${isActive ? "text-fg" : "text-muted"}`
1692 }
1693 >
1694 <Compass size={16} className="sm:hidden" />
1695 <span className="hidden sm:inline">Explore</span>
1696 </NavLink>
A Vercel-like sidebar and a new mark1697 <a
1698 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1699 className="hidden rounded-md px-2.5 py-1.5 text-sm text-muted transition-colors hover:bg-raised hover:text-fg sm:block"
A Vercel-like sidebar and a new mark1700 >
1701 Docs
1702 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1703 {user && (
1704 <>
1705 <AskAi />
1706 <InboxBell counts={shell.inbox ?? null} />
1707 </>
1708 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1709 {!user ? (
1710 // The sidebar has these too, but on a phone it is folded away.
1711 <Link
1712 to={withNext("/login", here)}
1713 className="flex h-9 items-center rounded-md bg-fg px-3 text-sm font-medium text-bg transition-colors hover:bg-white lg:hidden"
1714 >
1715 Sign in
1716 </Link>
1717 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1718 <DropdownMenu>
1719 <DropdownMenuTrigger
1720 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1721 className="flex h-9 items-center gap-1.5 rounded-md bg-fg px-3 text-sm font-medium text-bg outline-none transition-colors hover:bg-white"
Agents as a team: lifecycle, merge queue, billing and a new shell1722 >
A Vercel-like sidebar and a new mark1723 <Plus size={14} />
1724 New
Agents as a team: lifecycle, merge queue, billing and a new shell1725 </DropdownMenuTrigger>
1726 <DropdownMenuContent align="end">
1727 {shell.repo && (
1728 <DropdownMenuItem asChild>
1729 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1730 <CircleDot />
1731 New issue
1732 </Link>
1733 </DropdownMenuItem>
1734 )}
1735 {shell.repo?.member && (
1736 <DropdownMenuItem asChild>
1737 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1738 <ListTree />
1739 Plan work
1740 </Link>
1741 </DropdownMenuItem>
1742 )}
1743 <DropdownMenuItem asChild>
1744 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1745 <Box />
1746 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1747 </Link>
1748 </DropdownMenuItem>
1749 <DropdownMenuItem asChild>
1750 <Link to="/workspaces/new">
1751 <Users />
1752 New workspace
1753 </Link>
1754 </DropdownMenuItem>
1755 </DropdownMenuContent>
1756 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1757 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1758 </div>
1759 </header>
1760 {banner}
Merge branch 'worktree-agent-aca4790ea223aa235'1761 <main {...leaving} className={`min-w-0 grow ${leaving.className}`}>
1762 {children}
1763 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1764 </div>
Search across all of g1t, Explore, and a command palette1765 <CommandPalette
1766 open={palette}
1767 onOpenChange={setPalette}
1768 commands={commands}
1769 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1770 />
Agents as a team: lifecycle, merge queue, billing and a new shell1771 </div>
1772 );
1773}

This file's history is long; its oldest lines are credited to the oldest commit read.