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

1,632 lines66,199 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.

Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1import { Activity, BarChart3, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, 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
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5import type { Abilities, 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";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas8import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height9import { Logo, Mark } from "./logo";
Search across all of g1t, Explore, and a command palette10import { Avatar, notACredential } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents11import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell12import {
13 DropdownMenu,
14 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas15 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell16 DropdownMenuItem,
17 DropdownMenuLabel,
18 DropdownMenuSeparator,
19 DropdownMenuTrigger,
20} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales21import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look22import { 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 put23import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas24import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
25import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put26import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas28import { STATUS_URL, statusTitle } from "../lib/status";
29import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell30
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put31/**
32 * What the sidebar needs, worked out by the root loader. For a visitor who
33 * is not signed in there is no workspace, no projects and no usage: only
34 * the project being looked at, if they can see it.
35 */
Agents as a team: lifecycle, merge queue, billing and a new shell36export type ShellData = {
37 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control38 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page39 /** Its projects, by name: `name` is the slug in their address. */
40 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look41 /** Repositories shared with them in workspaces they do not belong to. */
42 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page43 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell44 repo: {
45 namespace: string;
46 name: string;
47 member: boolean;
48 issues: number;
49 pulls: number;
50 } | null;
Billing accounts, terms and enterprises; g1t is no longer free51 /** Where the workspace stands against its usage limit, if billing is on. */
52 limit: {
53 exposureMicros: number;
54 ceilingMicros: number | null;
55 state: "ok" | "warning" | "stopped";
56 comped: boolean;
57 } | null;
Free while g1t is being built out; agents can check out their own forks58 /** Whether g1t charges nothing for now, while it is being built out. */
59 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request60 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy61 /** This month's usage: charged, or at cost while g1t is free. */
62 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
64 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Agents as a team: lifecycle, merge queue, billing and a new shell65};
66
67function SidebarLink({
68 to,
69 icon,
70 end,
71 count,
72 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell74 children,
75}: {
76 to: string;
77 icon: ReactNode;
78 end?: boolean;
79 count?: number;
Projects: what a workspace builds and runs, first on every page80 /** Other path prefixes under which this link is the current one. */
81 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 /** It opens a list of its own: a chevron says so, always or on hover. */
83 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell84 children: ReactNode;
85}) {
86 const { pathname } = useLocation();
87 return (
88 <NavLink
89 to={to}
90 end={end}
91 prefetch="intent"
92 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page93 const current =
94 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark95 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 shell96 current
97 ? "bg-raised font-medium text-fg"
98 : isPending
99 ? "bg-raised/60 text-fg"
100 : "text-muted hover:bg-raised/60 hover:text-fg"
101 }`;
102 }}
103 >
104 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
105 <span className="min-w-0 grow truncate">{children}</span>
106 {count != null && count > 0 && (
107 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
108 {count}
109 </span>
110 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look111 {drill && (
112 <ChevronRight
113 size={14}
114 aria-hidden="true"
115 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
116 />
117 )}
Agents as a team: lifecycle, merge queue, billing and a new shell118 </NavLink>
119 );
120}
121
Pull requests say where they are live; the project menu shows what is coming122/**
123 * Something a project will have, shown where it will live so people can
124 * see where g1t is going. Not a link: nothing is behind it yet.
125 */
126function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
127 return (
128 <div
129 title={about}
130 aria-disabled="true"
131 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
132 >
133 <span className="shrink-0 opacity-70">{icon}</span>
134 <span className="grow truncate">{children}</span>
135 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
136 Soon
137 </span>
138 </div>
139 );
140}
141
The project menu opens by section, and the workspace has its own panel142/**
A flat project menu with Code first, views as tabs, and Stripe for every owner143 * A page that is coming, as a link to its roadmap page: faint, with Soon,
144 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel145 */
A flat project menu with Code first, views as tabs, and Stripe for every owner146function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel147 to,
148 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner149 icon,
150 about,
The project menu opens by section, and the workspace has its own panel151 children,
152}: {
153 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner154 also?: string[];
155 icon: ReactNode;
156 about: string;
The project menu opens by section, and the workspace has its own panel157 children: ReactNode;
158}) {
159 const { pathname } = useLocation();
160 return (
161 <NavLink
162 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner163 title={about}
The project menu opens by section, and the workspace has its own panel164 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner165 className={({ isActive }) => {
166 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
167 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
168 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 panel169 }`;
170 }}
171 >
A flat project menu with Code first, views as tabs, and Stripe for every owner172 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel173 <span className="min-w-0 grow truncate">{children}</span>
174 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
175 Soon
176 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales177 </NavLink>
The project menu opens by section, and the workspace has its own panel178 );
179}
180
A flat project menu with Code first, views as tabs, and Stripe for every owner181/** Icons for what spans projects. */
182const WORKSPACE_ICONS: Record<string, ReactNode> = {
183 board: <KanbanSquare size={15} />,
184 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member185 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner186 packages: <Package size={15} />,
187 fleet: <Bot size={15} />,
188};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales189
A flat project menu with Code first, views as tabs, and Stripe for every owner190/** The Soon pages of a section, as paths, so its link is current on them. */
191function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
192 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales193}
194
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look195function SidebarGroup({
196 title,
197 action,
198 className = "",
199 children,
200}: {
201 title: string;
202 action?: ReactNode;
203 className?: string;
204 children: ReactNode;
205}) {
Agents as a team: lifecycle, merge queue, billing and a new shell206 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look207 <section className={className}>
A Vercel-like sidebar and a new mark208 <div className="mb-1 flex h-6 items-center justify-between px-2">
209 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell210 {action}
211 </div>
212 <div className="space-y-px">{children}</div>
213 </section>
214 );
215}
216
Workspace names and icons, and a component kit for every control217/** What a workspace is called where people read it: its display name, else its slug. */
218function displayName(membership: Membership): string {
219 return membership.name?.trim() || membership.slug;
220}
221
Agents as a team: lifecycle, merge queue, billing and a new shell222function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control223 const workspace = shell.workspace;
224 const current = workspace?.slug;
225 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell226 return (
227 <DropdownMenu>
Workspace names and icons, and a component kit for every control228 {/* The name goes to the workspace; only the arrows switch it. The
229 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark230 <Link
231 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents232 prefetch="intent"
Workspace names and icons, and a component kit for every control233 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look234 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 mark235 >
Workspace names and icons, and a component kit for every control236 {workspace ? (
237 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell238 ) : (
Workspace names and icons, and a component kit for every control239 <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 shell240 )}
Workspace names and icons, and a component kit for every control241 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark242 </Link>
243 <DropdownMenuTrigger
244 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look245 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 mark246 >
247 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell248 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control249 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell250 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
251 {(user.workspaces ?? []).map((membership) => (
252 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control253 <Link to={`/${membership.slug}`} title={displayName(membership)}>
254 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
255 <span className="flex min-w-0 grow flex-col leading-tight">
256 <span className="truncate">{displayName(membership)}</span>
257 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
258 </span>
259 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
260 {membership.slug === current ? (
261 <Check className="shrink-0 text-accent" />
262 ) : (
263 <span className="size-4 shrink-0" aria-hidden="true" />
264 )}
Agents as a team: lifecycle, merge queue, billing and a new shell265 </Link>
266 </DropdownMenuItem>
267 ))}
268 <DropdownMenuSeparator />
269 <DropdownMenuItem asChild>
270 <Link to="/workspaces/new">
271 <Plus />
272 New workspace
273 </Link>
274 </DropdownMenuItem>
275 </DropdownMenuContent>
276 </DropdownMenu>
A Vercel-like sidebar and a new mark277 );
278}
279
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas280/**
281 * The fine print, as one quiet row at the foot of the account menu and the
282 * visitor's panel: each link a full-height target, left-aligned. In the
283 * menu each is a menu item, so the arrow keys reach it.
284 */
285const LEGAL_LINKS: [string, string][] = [
286 ["Policies", "/policies"],
287 ["Privacy", "/policies/privacy"],
288 ["Security", "/security"],
289];
290const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
291
292function MenuLegalRow() {
293 return (
294 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
295 {LEGAL_LINKS.map(([label, to], index) => (
296 <span key={to} className="flex items-center gap-0.5">
297 {index > 0 && (
298 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
299 ·
300 </span>
301 )}
302 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
303 <Link to={to}>{label}</Link>
304 </DropdownMenuItem>
305 </span>
306 ))}
307 </div>
308 );
309}
310
311/** Status's row: the live dot and two or three words, fetched when the menu opens. */
312function StatusSummary({ open }: { open: boolean }) {
313 const status = useSiteStatus(open);
314 const words = statusTitle(status);
315 return (
316 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
317 {/* The words say it; the dot's own words for screen readers would repeat them. */}
318 <span aria-hidden="true" className="flex">
319 <StatusDot state={status?.overall.state ?? null} />
320 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents321 {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 quotas322 </span>
323 );
324}
325
326/** A menu row: an icon, words, and whatever sits at its end. */
327const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
328
A Vercel-like sidebar and a new mark329function AccountMenu({ user }: { user: User }) {
330 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas331 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents332 // Name, primary address and invites left: asked for once, as soon as the
333 // 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 quotas334 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents335 const prefetch = () => {
336 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
337 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas338 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents339 if (open) prefetch();
340 // eslint-disable-next-line react-hooks/exhaustive-deps
341 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas342 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents343 // Still on its way: shapes where the words will be, the same size.
344 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 quotas345 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark346 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas347 <DropdownMenu open={open} onOpenChange={setOpen}>
348 <DropdownMenuTrigger
349 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents350 onPointerEnter={prefetch}
351 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas352 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"
353 >
Workspace names and icons, and a component kit for every control354 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark355 <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 quotas356 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark357 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas358 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
359 {/* Who is signed in, and a way to their profile. */}
360 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
361 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
362 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents363 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
364 {loading ? (
365 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
366 ) : (
367 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
368 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas369 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents370 {loading ? (
371 <Skeleton className="mt-1 h-3 w-40" />
372 ) : (
373 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
374 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas375 </span>
A Vercel-like sidebar and a new mark376 </Link>
377 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas378 <DropdownMenuSeparator className="my-1.5" />
379 <DropdownMenuGroup>
380 <DropdownMenuItem asChild className={MENU_ROW}>
381 <Link to={profile}>
382 <CircleUserRound />
383 Your profile
384 </Link>
385 </DropdownMenuItem>
386 <DropdownMenuItem asChild className={MENU_ROW}>
387 <Link to={FIRST_SETTINGS_PAGE}>
388 <Settings />
389 Your settings
390 </Link>
391 </DropdownMenuItem>
392 <DropdownMenuItem asChild className={MENU_ROW}>
393 <Link to="/settings/invites">
394 <Ticket />
395 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 {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 quotas397 {me?.invites_left != null && (
398 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
399 {me.invites_left} left
400 </span>
401 )}
402 </Link>
403 </DropdownMenuItem>
404 </DropdownMenuGroup>
405 <DropdownMenuSeparator className="my-1.5" />
406 <DropdownMenuGroup>
407 <DropdownMenuItem asChild className={MENU_ROW}>
408 <a href="https://docs.g1t.sh/">
409 <BookOpen />
410 Documentation
411 </a>
412 </DropdownMenuItem>
413 <DropdownMenuItem asChild className={MENU_ROW}>
414 <Link to="/support">
415 <LifeBuoy />
416 Support
417 </Link>
418 </DropdownMenuItem>
419 <DropdownMenuItem asChild className={MENU_ROW}>
420 <a href={STATUS_URL}>
421 <Activity />
422 Status
423 <StatusSummary open={open} />
424 </a>
425 </DropdownMenuItem>
426 </DropdownMenuGroup>
427 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark428 {/* Submitted from here: the menu closes on select, and a button
429 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 quotas430 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark431 <LogOut />
432 Sign out
433 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas434 <DropdownMenuSeparator className="my-1.5" />
435 <MenuLegalRow />
A Vercel-like sidebar and a new mark436 </DropdownMenuContent>
437 </DropdownMenu>
438 );
439}
440
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look441/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar442 * A workspace's settings pages, which the sidebar drills into: how it is
443 * set up and connected (guardrails, secrets, runners, integrations,
444 * webhooks), what it pays, its repositories, tokens and record. The main
445 * list keeps the places work happens and who belongs; every member can
446 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look447 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar448const SETTINGS_PAGE =
449 /^\/([^/]+)\/-\/(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 look450/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page451const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings452
Polish: phones, copy boxes, the plan page, the landing page, a real glide453/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look454 * The sidebar's lists are a stack, the way a phone pushes a screen: one
455 * drilled into slides in from the right over the full width, while the one
456 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide457 * ease-out, the fade quicker than the move so the two never blur together.
458 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide459 */
Sidebar: no settings flash on the way into a repository, and a better slide460const LAYER =
Sidebar: the panels really slide461 "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 look462/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide463const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings464
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look465/** One list in the stack: what it is, and its rows. */
466type Level = { key: string; node: ReactNode };
467
468/** True once the page has hydrated, so what the server drew never animates in. */
469function useHydrated(): boolean {
470 const [hydrated, setHydrated] = useState(false);
471 useEffect(() => setHydrated(true), []);
472 return hydrated;
473}
474
475/**
476 * Drilling in and out of the sidebar's lists. `trail` is the way from the
477 * main list to the one shown, worked out from the address, so a link
478 * straight to a settings page opens the sidebar already drilled in, and
479 * leaving it comes back out. Lists deeper than the one shown stay in
480 * place, off to the right, so going back slides them away rather than
481 * dropping them. Moving to a list at the same depth (a workspace's settings
482 * to a project) fades the new one in where it is.
483 */
484function Drill({ trail }: { trail: Level[] }) {
485 const depth = trail.length - 1;
486 const hydrated = useHydrated();
487 const stack = useRef<Level[]>(trail);
488 const before = stack.current;
489 const within = trail.every((level, index) => before[index]?.key === level.key);
490 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
491 const levels = stack.current;
492 // What was showing last time, to tell drilling in from moving across.
493 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
494 useEffect(() => {
495 shown.current = { depth, keys: levels.map((level) => level.key) };
496 });
A catalogue of model providers, and settings that feel like settings497 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look498 <div className="relative min-h-0 grow overflow-hidden">
499 {levels.map((level, index) => {
500 const current = index === depth;
501 const place =
502 index < depth
503 ? "pointer-events-none -translate-x-1/4 opacity-0"
504 : current
505 ? "translate-x-0 opacity-100"
506 : "pointer-events-none translate-x-full opacity-0";
507 // A list that was not there before arrives: from the right when
508 // drilling in, in place when moving across.
509 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
510 const enter = arriving
511 ? depth > shown.current.depth
512 ? "starting:translate-x-full starting:opacity-0"
513 : "starting:opacity-0"
514 : "";
515 return (
516 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
517 {level.node}
518 </div>
519 );
520 })}
521 </div>
522 );
523}
524
525/**
526 * The row at the top of a list drilled into: the list's name, which goes
527 * back out, and what it belongs to, faint at the end.
528 */
529function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
530 return (
531 <Link
532 to={to}
533 prefetch="intent"
534 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 settings535 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look536 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
537 <span className="min-w-0 shrink-0 truncate">{label}</span>
538 {context && (
539 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
540 )}
541 </Link>
542 );
543}
544
545/** A thin rule between groups of a list. */
546function Rule() {
547 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
548}
549
550/**
551 * The main list: where you go, the projects, what the workspace builds and
552 * runs with across them, then its usage, support and settings.
553 */
554function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
555 const ws = shell.workspace;
556 const active = shell.repo;
557 // The repository being looked at is listed even when it is someone else's.
558 const listed =
559 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
560 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
561 : shell.repos;
562 // A visitor browses: no workspace, no projects of their own.
563 if (!user) {
564 return (
565 <nav aria-label="g1t" className={PANEL}>
566 <div className="mt-3 space-y-px">
567 {VISITOR_LINKS.map((link) => (
568 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
569 {link.label}
570 </SidebarLink>
571 ))}
572 </div>
573 </nav>
574 );
575 }
576 return (
577 <nav aria-label="g1t" className={PANEL}>
578 <div className="mt-3 space-y-px">
579 <SidebarLink to="/" end icon={<House size={15} />}>
580 Mission control
581 </SidebarLink>
582 {ws && (
583 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutGrid size={15} />}>
584 Overview
585 </SidebarLink>
586 )}
587 <SidebarLink to="/explore" icon={<Compass size={15} />}>
588 Explore
589 </SidebarLink>
590 </div>
591
592 <Rule />
593 <SidebarGroup
594 title="Projects"
595 action={
596 ws && (
597 <Link
598 to={`/new?workspace=${ws.slug}`}
599 aria-label="New project"
600 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
601 >
602 <Plus size={13} />
603 </Link>
604 )
605 }
A catalogue of model providers, and settings that feel like settings606 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look607 {listed.length === 0 && <p className="px-2 py-1 text-xs text-faint">None yet.</p>}
608 {listed.map((repo) => (
609 <SidebarLink
610 key={`${repo.namespace}/${repo.name}`}
611 to={`/${repo.namespace}/${repo.name}`}
612 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
613 drill="hover"
614 >
615 {repo.namespace !== ws?.slug && <span className="font-mono text-faint">{repo.namespace}/</span>}
616 {"title" in repo && repo.title ? repo.title : repo.name}
617 </SidebarLink>
618 ))}
619 </SidebarGroup>
620 {(shell.shared ?? []).length > 0 && (
621 <SidebarGroup title="Shared with you" className="mt-3">
622 {(shell.shared ?? []).map((repo) => (
623 <SidebarLink
624 key={`${repo.namespace}/${repo.name}`}
625 to={`/${repo.namespace}/${repo.name}`}
626 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
627 drill="hover"
628 >
629 <span className="font-mono text-faint">{repo.namespace}/</span>
630 {repo.name}
631 </SidebarLink>
632 ))}
633 </SidebarGroup>
634 )}
635
636 {ws && (
637 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member638 <Rule />
639 <div className="space-y-px">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look640 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
641 Agent fleet
642 </SidebarLink>
643 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
644 Context
645 </SidebarLink>
646 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
647 Memory
648 </SidebarLink>
649 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
650 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents651 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member652 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
653 Packages
654 </SidebarLink>
655 {roadmapIn("Workspace").filter((item) => item.key !== "teams").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look656 <SidebarSoonLink
657 key={item.key}
658 to={`/${ws.slug}/-/soon/${item.key}`}
659 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
660 about={item.summary}
661 >
662 {item.title === "Board" ? "Boards" : item.title}
663 </SidebarSoonLink>
664 ))}
665 </div>
666
667 <Rule />
668 <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 bar669 {/* Who belongs, for every member to see; owners invite and manage there. */}
670 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
671 Members
672 </SidebarLink>
673 {roadmapIn("Workspace")
674 .filter((item) => item.key === "teams")
675 .map((item) => (
676 <SidebarSoonLink key={item.key} to={`/${ws.slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key]} about={item.summary}>
677 {item.title}
678 </SidebarSoonLink>
679 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look680 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
681 Usage
682 </SidebarLink>
683 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
684 Support
685 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar686 {/* 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 look687 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member688 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 look689 icon={<Settings size={15} />}
690 drill
691 >
692 Settings
693 </SidebarLink>
694 </div>
695 </>
696 )}
697 </nav>
698 );
699}
700
701/** A workspace's settings, drilled into from Settings in the main list. */
702function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
703 return (
704 <nav aria-label="Workspace settings" className={PANEL}>
705 <BackRow to={`/${slug}`} label="Settings" context={slug} />
706 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings707 {owner && (
708 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
709 General
710 </SidebarLink>
711 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look712 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
713 Repositories
714 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments715 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
716 Access tokens
717 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look718 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar719 <SidebarGroup title="Agents and runs" className="mt-3">
720 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
721 Guardrails
722 </SidebarLink>
723 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
724 Secrets and variables
725 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it726 {owner && (
727 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
728 Runners
729 </SidebarLink>
730 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar731 </SidebarGroup>
732 <SidebarGroup title="Connections" className="mt-3">
733 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
734 Integrations
735 </SidebarLink>
736 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
737 Webhooks
738 </SidebarLink>
739 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look740 <Rule />
741 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings742 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments743 Billing and plans
A catalogue of model providers, and settings that feel like settings744 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API745 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
746 Audit log
747 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look748 </div>
A catalogue of model providers, and settings that feel like settings749 </nav>
750 );
751}
752
Sidebar: no settings flash on the way into a repository, and a better slide753/**
754 * What the repository menu needs. Known from the address before the page's
755 * data arrives, so the menu is right from the first frame; the counts fill
756 * in once it does.
757 */
758type MenuRepo = {
759 namespace: string;
760 name: string;
761 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look762 /** Whether they see its settings; from the repository's page once it loads. */
763 settings?: boolean;
764 /** What they may do there, once the repository's page loads. */
765 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide766 issues?: number;
767 pulls?: number;
768};
769
770function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
771 return (
772 a != null &&
773 b != null &&
774 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
775 a.name.toLowerCase() === b.name.toLowerCase()
776 );
777}
A repository has its own sidebar, as settings do778
779/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look780 * A project's own list, drilled into while you are in it: everything about
781 * the project, running and its code, and nothing else, with the way back
782 * out to all of them.
A repository has its own sidebar, as settings do783 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put784function RepoMenu({
785 repo,
786 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put788}: {
789 repo: MenuRepo;
790 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look791 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
792 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put793}) {
A repository has its own sidebar, as settings do794 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 put795 // 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 look796 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do797 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look798 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
799 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner800 <NavLink
A repository has its own sidebar, as settings do801 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner802 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents803 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner804 title="Overview"
805 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look806 `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 owner807 }
A repository has its own sidebar, as settings do808 >
809 <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 page810 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do811 </span>
812 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
813 <span className="text-faint">{repo.namespace}/</span>
814 <span className="font-semibold text-fg">{repo.name}</span>
815 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner816 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look817 {/* A project's pages, in the order people use them. A page with more
818 than one view shows them as tabs across its top. */}
819 <Rule />
820 <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 request821 <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 owner822 Code
823 </SidebarLink>
824 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
825 Issues
826 </SidebarLink>
827 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
828 Pull requests
829 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains830 <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 owner831 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains832 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner833 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
834 Workflows
Projects: what a workspace builds and runs, first on every page835 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look836 </div>
837 <Rule />
838 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put839 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner840 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
841 Deployments
842 </SidebarLink>
843 ) : null}
Observability gets its own page; tabs never overflow844 <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.">
845 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner846 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put847 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API848 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
849 Security
850 </SidebarLink>
851 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner852 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
853 Insights
854 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look855 </div>
856 {(repo.settings ?? shows.has("settings")) && (
857 <>
858 <Rule />
859 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner860 Settings
861 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look862 </>
863 )}
A repository has its own sidebar, as settings do864 </nav>
865 );
866}
867
Project settings move into the sidebar, grouped, with a heading per page868/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look869 * A project's settings, drilled into from its list: what is about running
870 * it, then its agents, then its code and who can reach it. The way back
871 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page872 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look873function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page874 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look875 // Each page shows to the roles that can use it, once the role is known.
876 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
877 const running = shows("deployments");
878 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page879 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look880 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
881 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
882 {shows("") && (
883 <div className="mt-2 space-y-px">
884 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
885 General
886 </SidebarLink>
887 </div>
888 )}
889 {running && (
890 <>
891 {shows("") && <Rule />}
892 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
893 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
894 Deployments
895 </SidebarLink>
896 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
897 Domains
898 </SidebarLink>
899 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
900 Dependencies
901 </SidebarLink>
902 </div>
903 </>
904 )}
905 {agents && (
906 <>
907 <Rule />
908 <div className="space-y-px">
909 {shows("agents") && (
910 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
911 Agents
912 </SidebarLink>
913 )}
914 {shows("guardrails") && (
915 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
916 Guardrails
917 </SidebarLink>
918 )}
919 </div>
920 </>
921 )}
922 <Rule />
923 <div className="space-y-px">
924 {shows("repository") && (
925 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
926 Repository
927 </SidebarLink>
928 )}
929 {shows("access") && (
930 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
931 Access
932 </SidebarLink>
933 )}
934 {shows("branches") && (
935 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
936 Branches and merging
937 </SidebarLink>
938 )}
939 {shows("secrets") && (
940 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
941 Secrets and variables
942 </SidebarLink>
943 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents944 {shows("runners") && (
945 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
946 Runners
947 </SidebarLink>
948 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look949 {shows("webhooks") && (
950 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
951 Webhooks
952 </SidebarLink>
953 )}
954 </div>
Project settings move into the sidebar, grouped, with a heading per page955 </nav>
956 );
957}
958
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas959/** Your own settings, drilled into from Your settings: one page each. */
960function AccountSettingsMenu({ username }: { username: string }) {
961 const link = (page: AccountSettingsPage, icon: ReactNode) => (
962 <SidebarLink to={`/settings/${page}`} icon={icon}>
963 {ACCOUNT_SETTINGS[page].title}
964 </SidebarLink>
Account settings in the same shape as a workspace's965 );
966 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look967 <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 quotas968 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look969 <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 quotas970 {link("profile", <CircleUserRound size={15} />)}
971 {link("emails", <Mail size={15} />)}
972 {link("invites", <Ticket size={15} />)}
973 </div>
974 <Rule />
975 <div className="space-y-px">
976 {link("keys", <Fingerprint size={15} />)}
977 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look978 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas979 <Rule />
980 <div className="space-y-px">
981 {link("github", <GithubMark className="size-[15px]" />)}
982 {link("applications", <Plug size={15} />)}
983 </div>
984 <Rule />
985 <div className="space-y-px">
986 {link("security-log", <History size={15} />)}
987 </div>
Account settings in the same shape as a workspace's988 </nav>
989 );
990}
991
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put992/**
993 * Signing in and signing up, in place of the account for a visitor. Signing
994 * in brings them back to the page they are on.
995 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas996/** Help, status and the fine print under a visitor's sign-in buttons. */
997function VisitorLinks() {
998 const status = useSiteStatus();
999 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1000 const dot = (
1001 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1002 ·
1003 </span>
1004 );
1005 return (
1006 // Two short rows, so a narrow sidebar never starts a line with a dot.
1007 <nav aria-label="About g1t" className="pt-0.5">
1008 <div className="flex items-center gap-0.5">
1009 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1010 <StatusDot state={status?.overall.state ?? null} />
1011 Status
1012 </a>
1013 {dot}
1014 <Link to="/support" className={quiet}>
1015 Support
1016 </Link>
1017 </div>
1018 <div className="flex items-center gap-0.5">
1019 {LEGAL_LINKS.map(([label, to], index) => (
1020 <span key={to} className="flex items-center gap-0.5">
1021 {index > 0 && dot}
1022 <Link to={to} className={quiet}>
1023 {label}
1024 </Link>
1025 </span>
1026 ))}
1027 </div>
1028 </nav>
1029 );
1030}
1031
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1032function VisitorPanel() {
1033 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1034 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1035 return (
1036 <div className="space-y-2">
1037 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1038 <div className="grid grid-cols-2 gap-2">
1039 <Link
1040 to={withNext("/login", pathname + search)}
1041 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"
1042 >
1043 <LogIn size={14} />
1044 Sign in
1045 </Link>
1046 <Link
1047 to={withNext("/register", pathname + search)}
1048 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1049 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1050 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1051 </Link>
1052 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1053 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1054 </div>
1055 );
1056}
1057
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1058function Sidebar({
1059 user,
1060 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1061 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1062 onFind,
1063 onClose,
1064}: {
1065 user: User | null;
1066 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1067 /** The page is a 404: the address names nothing the viewer can see. */
1068 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1069 onFind: () => void;
1070 /** In the sheet on a small screen: closing it, at the end of the top row. */
1071 onClose?: () => void;
1072}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1073 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1074 const { pathname } = useLocation();
1075 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1076 // Where the sidebar is drilled to follows the page being gone to, so it
1077 // moves as the link is followed, not once the page arrives.
1078 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1079 // On a 404, the address is not a place: no list is drilled into from it,
1080 // except the project's own when the project is real and visible and only
1081 // something inside it is missing (a file, a commit, an issue).
1082 const lost = missing && going == null;
1083 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1084 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1085 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1086 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1087 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1088 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", ".well-known"]);
1089 // An invitation to a repository is answered before its menu means anything.
1090 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1091 repoPath != null &&
1092 !reserved.has(repoPath[1]) &&
1093 repoPath[2] !== "-" &&
1094 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1095 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1096 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1097 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1098 // The repository the menu is for: the one loaded if it is the one being
1099 // gone to, else what the address says, at once.
1100 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 look1101 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1102 ? sameRepo(active, targetRepo)
1103 ? active
1104 : {
1105 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1106 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1107 }
1108 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1109 // The viewer's role there, from the repository's page once it has loaded.
1110 const page = useRouteLoaderData("routes/repo/layout") as
1111 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1112 | undefined;
1113 const menuRepo: MenuRepo | null =
1114 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1115 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1116 : guessed;
1117
1118 // The way from the main list to the one shown.
1119 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1120 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1121 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1122 } else if (ws && inSettings) {
1123 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1124 } else if (menuRepo) {
1125 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1126 // Out of a project: to its workspace's projects when they are yours.
1127 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1128 const back = !user
1129 ? { to: "/explore", label: "Explore" }
1130 : home
1131 ? { to: `/${home.slug}`, label: "All projects" }
1132 : { to: "/", label: "Mission control" };
1133 trail.push({
1134 key,
1135 node: (
1136 <RepoMenu
1137 repo={menuRepo}
1138 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1139 back={back}
1140 />
1141 ),
1142 });
1143 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1144 }
1145
Agents as a team: lifecycle, merge queue, billing and a new shell1146 return (
1147 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1148 {/* 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 bar1149 <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 put1150 {user ? (
1151 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1152 {/* The 1 alone beside the workspace: a square hover the height of the
1153 switcher, the mark as tall as the workspace avatar. */}
1154 <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 bar1155 <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 put1156 </Link>
1157 <span className="shrink-0 text-line-strong" aria-hidden="true">
1158 /
1159 </span>
1160 <WorkspaceSwitcher user={user} shell={shell} />
1161 </>
1162 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1163 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1164 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1165 </Link>
1166 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1167 {onClose && (
1168 <button
1169 type="button"
1170 aria-label="Close menu"
1171 onClick={onClose}
1172 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1173 >
1174 <X size={16} />
1175 </button>
1176 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1177 </div>
A Vercel-like sidebar and a new mark1178 <div className="px-2 pt-3">
1179 <button
1180 type="button"
1181 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1182 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 mark1183 >
1184 <Search size={14} />
1185 <span className="grow text-left">Find…</span>
1186 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1187 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1188 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1189 <Drill trail={trail} />
1190 {/* The account, or signing in, as one row at the very bottom. */}
1191 <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 put1192 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1193 </div>
1194 </div>
1195 );
1196}
1197
1198/** Words for the sections a path can end in. */
1199const SECTIONS: Record<string, string> = {
1200 issues: "Issues",
1201 pulls: "Pull requests",
1202 queue: "Merge queue",
1203 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1204 plans: "Outcomes",
1205 actions: "Workflows",
1206 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1207 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1208 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1209 access: "Access",
1210 invitations: "Invitation",
1211 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1212 branches: "Branches",
1213 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1214 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1215 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1216 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1217 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1218 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1219 people: "Members",
A Vercel-like sidebar and a new mark1220 tokens: "Access tokens",
1221 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1222 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1223 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1224 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1225 domains: "Domains",
1226 guardrails: "Guardrails",
1227 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1228 tree: "Files",
1229 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1230 agents: "Agents",
1231 context: "Context",
1232 memory: "Memory",
1233 security: "Security",
1234 packages: "Packages",
1235 runners: "Runners",
1236 workflows: "Workflows",
1237 observability: "Observability",
1238 insights: "Insights",
1239 sessions: "Sessions",
A Vercel-like sidebar and a new mark1240};
1241
1242/** 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 quotas1243function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1244 // A 404 names nothing from the address, unless the project is real and
1245 // visible and only something inside it is missing.
1246 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1247 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1248 return <span className="text-sm font-medium">Not found</span>;
1249 }
A Vercel-like sidebar and a new mark1250 const parts = pathname.split("/").filter(Boolean);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1251 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite"];
A Vercel-like sidebar and a new mark1252 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 quotas1253 // Your settings: Settings / Emails.
1254 if (parts[0] === "settings") {
1255 const page = accountSettingsPage(pathname);
1256 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1257 return (
1258 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1259 {trail.map((crumb, index) => (
1260 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1261 {index > 0 && <span className="text-line-strong">/</span>}
1262 <Link
1263 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1264 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1265 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1266 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1267 }`}
1268 >
1269 {crumb.label}
1270 </Link>
1271 </span>
1272 ))}
1273 </nav>
1274 );
1275 }
A Vercel-like sidebar and a new mark1276 if (reserved.includes(parts[0]!)) {
1277 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1278 settings: "Account",
A Vercel-like sidebar and a new mark1279 explore: "Explore",
Projects live: fixes from walking them end to end1280 new: "New project",
A Vercel-like sidebar and a new mark1281 search: "Search",
1282 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1283 policies: "Policies",
1284 security: "Security",
1285 support: "Support",
1286 status: "Status",
1287 invite: "Invite",
A Vercel-like sidebar and a new mark1288 };
1289 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1290 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1291 // A person's profile, by their handle.
1292 if (parts[0] === "u" && parts[1]) {
1293 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1294 }
A Vercel-like sidebar and a new mark1295 const [owner, second, third, fourth] = parts;
1296 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1297 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1298 const page = `/${owner}/-/${third}`;
1299 if (third && SETTINGS_PAGE.test(page)) {
1300 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1301 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1302 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1303 } else if (second) {
1304 const repo = `/${owner}/${second}`;
1305 trail.push({ label: second, to: repo, mono: true });
1306 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1307 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1308 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 deployments1309 else if (third && SECTIONS[third]) {
1310 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1311 // A settings page names which one: Settings / Secrets and variables.
1312 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1313 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 deployments1314 }
1315 }
A Vercel-like sidebar and a new mark1316 }
1317 return (
1318 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1319 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1320 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1321 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1322 <Link
A Vercel-like sidebar and a new mark1323 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1324 prefetch="intent"
A Vercel-like sidebar and a new mark1325 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1326 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1327 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1328 >
A Vercel-like sidebar and a new mark1329 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1330 </Link>
A Vercel-like sidebar and a new mark1331 </span>
1332 ))}
1333 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1334 );
1335}
1336
Search across all of g1t, Explore, and a command palette1337type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1338
1339/** 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 look1340function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1341 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1342 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1343 { 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 step1344 ...(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-agent1345 ? [{ 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 step1346 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1347 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1348 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page1349 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1350 { 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 quotas1351 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1352 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1353 .filter((page) => page !== "profile")
1354 .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 shell1355 ];
1356 const repo = shell.repo;
1357 if (repo) {
1358 const base = `/${repo.namespace}/${repo.name}`;
1359 const name = `${repo.namespace}/${repo.name}`;
1360 commands.unshift(
Projects: what a workspace builds and runs, first on every page1361 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1362 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1363 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1364 { 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 step1365 ...(repo.member
1366 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1367 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1368 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1369 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1370 ...(repo.member
1371 ? [
1372 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1373 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1374 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1375 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1376 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1377 ]
1378 : []),
1379 );
1380 }
1381 for (const membership of user.workspaces ?? []) {
1382 commands.push(
Workspace names and icons, and a component kit for every control1383 { label: displayName(membership), hint: `Workspace · ${membership.slug}`, to: `/${membership.slug}`, icon: <Avatar name={membership.slug} image={membership.avatar} size={15} square /> },
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1384 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1385 { 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 settings1386 { 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 look1387 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1388 );
1389 }
1390 for (const listed of shell.repos) {
1391 commands.push({
Projects: what a workspace builds and runs, first on every page1392 label: listed.title ?? listed.name,
1393 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1394 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1395 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1396 });
1397 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1398 // "Put an agent on …": one per project, after the projects themselves.
1399 for (const listed of shell.repos) {
1400 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1401 commands.push({
1402 label: `Put an agent on ${listed.title ?? listed.name}`,
1403 hint: `${listed.namespace}/${listed.name}`,
1404 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1405 icon: <Sparkles size={15} />,
1406 });
1407 }
Agents as a team: lifecycle, merge queue, billing and a new shell1408 return commands;
1409}
1410
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1411/** 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 look1412function 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 put1413 const commands: Command[] = [];
1414 const repo = shell.repo;
1415 if (repo) {
1416 const base = `/${repo.namespace}/${repo.name}`;
1417 const name = `${repo.namespace}/${repo.name}`;
1418 commands.push(
1419 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1420 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1421 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1422 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1423 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1424 );
1425 }
1426 commands.push(
1427 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1428 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1429 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1430 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1431 { 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 look1432 { 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 put1433 );
1434 return commands;
1435}
1436
Agents as a team: lifecycle, merge queue, billing and a new shell1437/**
1438 * A bar across the top of the page while the next one loads, as GitHub
1439 * has: it appears at once, creeps towards the end while waiting, then fills
1440 * and fades when the page arrives.
1441 */
1442export function Progress() {
1443 const navigation = useNavigation();
1444 const busy = navigation.state !== "idle";
1445 const [width, setWidth] = useState(0);
1446 const [visible, setVisible] = useState(false);
1447 useEffect(() => {
1448 if (busy) {
1449 setVisible(true);
1450 setWidth(12);
1451 // Slows as it goes, never quite arriving until the page does.
1452 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1453 return () => clearInterval(timer);
1454 }
1455 setWidth((now) => (now > 0 ? 100 : 0));
1456 const fade = setTimeout(() => setVisible(false), 250);
1457 const reset = setTimeout(() => setWidth(0), 550);
1458 return () => {
1459 clearTimeout(fade);
1460 clearTimeout(reset);
1461 };
1462 }, [busy]);
1463 return (
1464 <div
1465 aria-hidden="true"
1466 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1467 visible ? "opacity-100" : "opacity-0"
1468 }`}
1469 >
1470 <div
1471 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1472 style={{ width: `${width}%` }}
1473 />
1474 </div>
1475 );
1476}
1477
1478/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1479 * The app: a sidebar with the workspace, its repositories and the sections
1480 * of the one being looked at; a slim bar with search and the account; and
1481 * the page. A visitor who is not signed in gets the same frame, with
1482 * Explore and Search in place of the workspace, and signing in in place of
1483 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1484 */
1485export function AppShell({
1486 user,
1487 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1488 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1489 banner,
1490 children,
1491}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1492 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1493 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1494 /** The page is a 404 (see Sidebar). */
1495 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1496 banner?: ReactNode;
1497 children: ReactNode;
1498}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1499 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1500 const [drawer, setDrawer] = useState(false);
1501 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1502 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1503 const signUpLabel = useSignUpCopy().primary;
1504 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1505
1506 // A new page closes the drawer on small screens.
1507 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1508 // Mission control's code, fetched while the browser is idle, so going
1509 // home never waits on it (routes/home.tsx).
1510 useEffect(() => {
1511 if (!user) return;
1512 const load = () => void import("./mission-control").catch(() => undefined);
1513 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1514 if (idle) idle(load);
1515 else setTimeout(load, 1500);
1516 }, [user]);
Search across all of g1t, Explore, and a command palette1517 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1518
1519 return (
1520 <div className="min-h-screen">
1521 <Progress />
1522 <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 quotas1523 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1524 </aside>
1525 {drawer && (
1526 <div className="fixed inset-0 z-50 lg:hidden">
1527 <button
1528 type="button"
1529 aria-label="Close menu"
1530 className="absolute inset-0 bg-black/60"
1531 onClick={() => setDrawer(false)}
1532 />
1533 <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 quotas1534 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1535 </aside>
1536 </div>
1537 )}
1538
Polish: phones, copy boxes, the plan page, the landing page, a real glide1539 <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 bar1540 <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 shell1541 <button
1542 type="button"
1543 aria-label="Open menu"
1544 onClick={() => setDrawer(true)}
1545 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1546 >
1547 <Menu size={18} />
1548 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1549 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1550 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1551 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1552 <input
1553 name="q"
1554 {...notACredential()}
1555 placeholder="Search g1t"
1556 aria-label="Search g1t"
1557 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"
1558 />
1559 <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">
1560 ⌘K
1561 </kbd>
1562 </Form>
1563 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1564 <a
1565 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1566 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 mark1567 >
1568 Docs
1569 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1570 {!user ? (
1571 // The sidebar has these too, but on a phone it is folded away.
1572 <Link
1573 to={withNext("/login", here)}
1574 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"
1575 >
1576 Sign in
1577 </Link>
1578 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1579 <DropdownMenu>
1580 <DropdownMenuTrigger
1581 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1582 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 shell1583 >
A Vercel-like sidebar and a new mark1584 <Plus size={14} />
1585 New
Agents as a team: lifecycle, merge queue, billing and a new shell1586 </DropdownMenuTrigger>
1587 <DropdownMenuContent align="end">
1588 {shell.repo && (
1589 <DropdownMenuItem asChild>
1590 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1591 <CircleDot />
1592 New issue
1593 </Link>
1594 </DropdownMenuItem>
1595 )}
1596 {shell.repo?.member && (
1597 <DropdownMenuItem asChild>
1598 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1599 <ListTree />
1600 Plan work
1601 </Link>
1602 </DropdownMenuItem>
1603 )}
1604 <DropdownMenuItem asChild>
1605 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1606 <Box />
1607 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1608 </Link>
1609 </DropdownMenuItem>
1610 <DropdownMenuItem asChild>
1611 <Link to="/workspaces/new">
1612 <Users />
1613 New workspace
1614 </Link>
1615 </DropdownMenuItem>
1616 </DropdownMenuContent>
1617 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1618 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1619 </div>
1620 </header>
1621 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1622 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1623 </div>
Search across all of g1t, Explore, and a command palette1624 <CommandPalette
1625 open={palette}
1626 onOpenChange={setPalette}
1627 commands={commands}
1628 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1629 />
Agents as a team: lifecycle, merge queue, billing and a new shell1630 </div>
1631 );
1632}