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

1,615 lines65,675 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/**
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member442 * A workspace's settings pages, which the sidebar drills into: what it
443 * pays, its repositories, tokens and record. Who belongs (Members, Teams)
444 * and what it builds and runs with (secrets, integrations, webhooks,
445 * guardrails) sit in the main list, for every member to see.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look446 */
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member447const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|repositories|tokens|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look448/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page449const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings450
Polish: phones, copy boxes, the plan page, the landing page, a real glide451/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look452 * The sidebar's lists are a stack, the way a phone pushes a screen: one
453 * drilled into slides in from the right over the full width, while the one
454 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide455 * ease-out, the fade quicker than the move so the two never blur together.
456 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide457 */
Sidebar: no settings flash on the way into a repository, and a better slide458const LAYER =
Sidebar: the panels really slide459 "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 look460/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide461const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings462
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look463/** One list in the stack: what it is, and its rows. */
464type Level = { key: string; node: ReactNode };
465
466/** True once the page has hydrated, so what the server drew never animates in. */
467function useHydrated(): boolean {
468 const [hydrated, setHydrated] = useState(false);
469 useEffect(() => setHydrated(true), []);
470 return hydrated;
471}
472
473/**
474 * Drilling in and out of the sidebar's lists. `trail` is the way from the
475 * main list to the one shown, worked out from the address, so a link
476 * straight to a settings page opens the sidebar already drilled in, and
477 * leaving it comes back out. Lists deeper than the one shown stay in
478 * place, off to the right, so going back slides them away rather than
479 * dropping them. Moving to a list at the same depth (a workspace's settings
480 * to a project) fades the new one in where it is.
481 */
482function Drill({ trail }: { trail: Level[] }) {
483 const depth = trail.length - 1;
484 const hydrated = useHydrated();
485 const stack = useRef<Level[]>(trail);
486 const before = stack.current;
487 const within = trail.every((level, index) => before[index]?.key === level.key);
488 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
489 const levels = stack.current;
490 // What was showing last time, to tell drilling in from moving across.
491 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
492 useEffect(() => {
493 shown.current = { depth, keys: levels.map((level) => level.key) };
494 });
A catalogue of model providers, and settings that feel like settings495 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look496 <div className="relative min-h-0 grow overflow-hidden">
497 {levels.map((level, index) => {
498 const current = index === depth;
499 const place =
500 index < depth
501 ? "pointer-events-none -translate-x-1/4 opacity-0"
502 : current
503 ? "translate-x-0 opacity-100"
504 : "pointer-events-none translate-x-full opacity-0";
505 // A list that was not there before arrives: from the right when
506 // drilling in, in place when moving across.
507 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
508 const enter = arriving
509 ? depth > shown.current.depth
510 ? "starting:translate-x-full starting:opacity-0"
511 : "starting:opacity-0"
512 : "";
513 return (
514 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
515 {level.node}
516 </div>
517 );
518 })}
519 </div>
520 );
521}
522
523/**
524 * The row at the top of a list drilled into: the list's name, which goes
525 * back out, and what it belongs to, faint at the end.
526 */
527function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
528 return (
529 <Link
530 to={to}
531 prefetch="intent"
532 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 settings533 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look534 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
535 <span className="min-w-0 shrink-0 truncate">{label}</span>
536 {context && (
537 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
538 )}
539 </Link>
540 );
541}
542
543/** A thin rule between groups of a list. */
544function Rule() {
545 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
546}
547
548/**
549 * The main list: where you go, the projects, what the workspace builds and
550 * runs with across them, then its usage, support and settings.
551 */
552function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
553 const ws = shell.workspace;
554 const active = shell.repo;
555 // The repository being looked at is listed even when it is someone else's.
556 const listed =
557 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
558 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
559 : shell.repos;
560 // A visitor browses: no workspace, no projects of their own.
561 if (!user) {
562 return (
563 <nav aria-label="g1t" className={PANEL}>
564 <div className="mt-3 space-y-px">
565 {VISITOR_LINKS.map((link) => (
566 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
567 {link.label}
568 </SidebarLink>
569 ))}
570 </div>
571 </nav>
572 );
573 }
574 return (
575 <nav aria-label="g1t" className={PANEL}>
576 <div className="mt-3 space-y-px">
577 <SidebarLink to="/" end icon={<House size={15} />}>
578 Mission control
579 </SidebarLink>
580 {ws && (
581 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutGrid size={15} />}>
582 Overview
583 </SidebarLink>
584 )}
585 <SidebarLink to="/explore" icon={<Compass size={15} />}>
586 Explore
587 </SidebarLink>
588 </div>
589
590 <Rule />
591 <SidebarGroup
592 title="Projects"
593 action={
594 ws && (
595 <Link
596 to={`/new?workspace=${ws.slug}`}
597 aria-label="New project"
598 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
599 >
600 <Plus size={13} />
601 </Link>
602 )
603 }
A catalogue of model providers, and settings that feel like settings604 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look605 {listed.length === 0 && <p className="px-2 py-1 text-xs text-faint">None yet.</p>}
606 {listed.map((repo) => (
607 <SidebarLink
608 key={`${repo.namespace}/${repo.name}`}
609 to={`/${repo.namespace}/${repo.name}`}
610 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
611 drill="hover"
612 >
613 {repo.namespace !== ws?.slug && <span className="font-mono text-faint">{repo.namespace}/</span>}
614 {"title" in repo && repo.title ? repo.title : repo.name}
615 </SidebarLink>
616 ))}
617 </SidebarGroup>
618 {(shell.shared ?? []).length > 0 && (
619 <SidebarGroup title="Shared with you" className="mt-3">
620 {(shell.shared ?? []).map((repo) => (
621 <SidebarLink
622 key={`${repo.namespace}/${repo.name}`}
623 to={`/${repo.namespace}/${repo.name}`}
624 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
625 drill="hover"
626 >
627 <span className="font-mono text-faint">{repo.namespace}/</span>
628 {repo.name}
629 </SidebarLink>
630 ))}
631 </SidebarGroup>
632 )}
633
634 {ws && (
635 <>
636 <Rule />
637 <div className="space-y-px">
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member638 {/* Who belongs, for every member to see; owners invite and manage there. */}
639 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
640 Members
641 </SidebarLink>
642 {roadmapIn("Workspace")
643 .filter((item) => item.key === "teams")
644 .map((item) => (
645 <SidebarSoonLink key={item.key} to={`/${ws.slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key]} about={item.summary}>
646 {item.title}
647 </SidebarSoonLink>
648 ))}
649 </div>
650 <Rule />
651 <div className="space-y-px">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look652 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
653 Agent fleet
654 </SidebarLink>
655 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
656 Context
657 </SidebarLink>
658 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
659 Memory
660 </SidebarLink>
661 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
662 Security
663 </SidebarLink>
664 <SidebarLink to={`/${ws.slug}/-/guardrails`} icon={<Gauge size={15} />}>
665 Guardrails
666 </SidebarLink>
667 <SidebarLink to={`/${ws.slug}/-/secrets`} icon={<Lock size={15} />}>
668 Secrets and variables
669 </SidebarLink>
Fast pages, required checks on the branch, self-hosted runners, honest incidents670 <SidebarLink to={`/${ws.slug}/-/runners`} icon={<ServerCog size={15} />}>
671 Runners
672 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member673 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
674 Packages
675 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look676 <SidebarLink to={`/${ws.slug}/-/integrations`} icon={<Plug size={15} />}>
677 Integrations
678 </SidebarLink>
679 <SidebarLink to={`/${ws.slug}/-/webhooks`} icon={<Webhook size={15} />}>
680 Webhooks
681 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member682 {roadmapIn("Workspace").filter((item) => item.key !== "teams").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look683 <SidebarSoonLink
684 key={item.key}
685 to={`/${ws.slug}/-/soon/${item.key}`}
686 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
687 about={item.summary}
688 >
689 {item.title === "Board" ? "Boards" : item.title}
690 </SidebarSoonLink>
691 ))}
692 </div>
693
694 <Rule />
695 <div className="space-y-px">
696 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
697 Usage
698 </SidebarLink>
699 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
700 Support
701 </SidebarLink>
702 {/* Who belongs, what it pays and its record: a list of their own. */}
703 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member704 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 look705 icon={<Settings size={15} />}
706 drill
707 >
708 Settings
709 </SidebarLink>
710 </div>
711 </>
712 )}
713 </nav>
714 );
715}
716
717/** A workspace's settings, drilled into from Settings in the main list. */
718function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
719 return (
720 <nav aria-label="Workspace settings" className={PANEL}>
721 <BackRow to={`/${slug}`} label="Settings" context={slug} />
722 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings723 {owner && (
724 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
725 General
726 </SidebarLink>
727 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look728 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
729 Repositories
730 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments731 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
732 Access tokens
733 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look734 </div>
735 <Rule />
736 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings737 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments738 Billing and plans
A catalogue of model providers, and settings that feel like settings739 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API740 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
741 Audit log
742 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look743 </div>
A catalogue of model providers, and settings that feel like settings744 </nav>
745 );
746}
747
Sidebar: no settings flash on the way into a repository, and a better slide748/**
749 * What the repository menu needs. Known from the address before the page's
750 * data arrives, so the menu is right from the first frame; the counts fill
751 * in once it does.
752 */
753type MenuRepo = {
754 namespace: string;
755 name: string;
756 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look757 /** Whether they see its settings; from the repository's page once it loads. */
758 settings?: boolean;
759 /** What they may do there, once the repository's page loads. */
760 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide761 issues?: number;
762 pulls?: number;
763};
764
765function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
766 return (
767 a != null &&
768 b != null &&
769 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
770 a.name.toLowerCase() === b.name.toLowerCase()
771 );
772}
A repository has its own sidebar, as settings do773
774/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look775 * A project's own list, drilled into while you are in it: everything about
776 * the project, running and its code, and nothing else, with the way back
777 * out to all of them.
A repository has its own sidebar, as settings do778 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put779function RepoMenu({
780 repo,
781 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look782 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put783}: {
784 repo: MenuRepo;
785 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look786 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
787 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put788}) {
A repository has its own sidebar, as settings do789 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 put790 // 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 look791 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do792 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look793 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
794 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner795 <NavLink
A repository has its own sidebar, as settings do796 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner797 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents798 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner799 title="Overview"
800 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look801 `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 owner802 }
A repository has its own sidebar, as settings do803 >
804 <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 page805 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do806 </span>
807 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
808 <span className="text-faint">{repo.namespace}/</span>
809 <span className="font-semibold text-fg">{repo.name}</span>
810 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner811 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look812 {/* A project's pages, in the order people use them. A page with more
813 than one view shows them as tabs across its top. */}
814 <Rule />
815 <div className="space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner816 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
817 Code
818 </SidebarLink>
819 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
820 Issues
821 </SidebarLink>
822 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
823 Pull requests
824 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains825 <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 owner826 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains827 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner828 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
829 Workflows
Projects: what a workspace builds and runs, first on every page830 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look831 </div>
832 <Rule />
833 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put834 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner835 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
836 Deployments
837 </SidebarLink>
838 ) : null}
Observability gets its own page; tabs never overflow839 <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.">
840 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner841 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put842 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API843 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
844 Security
845 </SidebarLink>
846 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner847 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
848 Insights
849 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look850 </div>
851 {(repo.settings ?? shows.has("settings")) && (
852 <>
853 <Rule />
854 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner855 Settings
856 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look857 </>
858 )}
A repository has its own sidebar, as settings do859 </nav>
860 );
861}
862
Project settings move into the sidebar, grouped, with a heading per page863/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864 * A project's settings, drilled into from its list: what is about running
865 * it, then its agents, then its code and who can reach it. The way back
866 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page867 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look868function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page869 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look870 // Each page shows to the roles that can use it, once the role is known.
871 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
872 const running = shows("deployments");
873 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page874 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look875 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
876 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
877 {shows("") && (
878 <div className="mt-2 space-y-px">
879 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
880 General
881 </SidebarLink>
882 </div>
883 )}
884 {running && (
885 <>
886 {shows("") && <Rule />}
887 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
888 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
889 Deployments
890 </SidebarLink>
891 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
892 Domains
893 </SidebarLink>
894 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
895 Dependencies
896 </SidebarLink>
897 </div>
898 </>
899 )}
900 {agents && (
901 <>
902 <Rule />
903 <div className="space-y-px">
904 {shows("agents") && (
905 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
906 Agents
907 </SidebarLink>
908 )}
909 {shows("guardrails") && (
910 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
911 Guardrails
912 </SidebarLink>
913 )}
914 </div>
915 </>
916 )}
917 <Rule />
918 <div className="space-y-px">
919 {shows("repository") && (
920 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
921 Repository
922 </SidebarLink>
923 )}
924 {shows("access") && (
925 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
926 Access
927 </SidebarLink>
928 )}
929 {shows("branches") && (
930 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
931 Branches and merging
932 </SidebarLink>
933 )}
934 {shows("secrets") && (
935 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
936 Secrets and variables
937 </SidebarLink>
938 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents939 {shows("runners") && (
940 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
941 Runners
942 </SidebarLink>
943 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look944 {shows("webhooks") && (
945 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
946 Webhooks
947 </SidebarLink>
948 )}
949 </div>
Project settings move into the sidebar, grouped, with a heading per page950 </nav>
951 );
952}
953
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas954/** Your own settings, drilled into from Your settings: one page each. */
955function AccountSettingsMenu({ username }: { username: string }) {
956 const link = (page: AccountSettingsPage, icon: ReactNode) => (
957 <SidebarLink to={`/settings/${page}`} icon={icon}>
958 {ACCOUNT_SETTINGS[page].title}
959 </SidebarLink>
Account settings in the same shape as a workspace's960 );
961 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look962 <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 quotas963 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look964 <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 quotas965 {link("profile", <CircleUserRound size={15} />)}
966 {link("emails", <Mail size={15} />)}
967 {link("invites", <Ticket size={15} />)}
968 </div>
969 <Rule />
970 <div className="space-y-px">
971 {link("keys", <Fingerprint size={15} />)}
972 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look973 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas974 <Rule />
975 <div className="space-y-px">
976 {link("github", <GithubMark className="size-[15px]" />)}
977 {link("applications", <Plug size={15} />)}
978 </div>
979 <Rule />
980 <div className="space-y-px">
981 {link("security-log", <History size={15} />)}
982 </div>
Account settings in the same shape as a workspace's983 </nav>
984 );
985}
986
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put987/**
988 * Signing in and signing up, in place of the account for a visitor. Signing
989 * in brings them back to the page they are on.
990 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas991/** Help, status and the fine print under a visitor's sign-in buttons. */
992function VisitorLinks() {
993 const status = useSiteStatus();
994 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
995 const dot = (
996 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
997 ·
998 </span>
999 );
1000 return (
1001 // Two short rows, so a narrow sidebar never starts a line with a dot.
1002 <nav aria-label="About g1t" className="pt-0.5">
1003 <div className="flex items-center gap-0.5">
1004 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1005 <StatusDot state={status?.overall.state ?? null} />
1006 Status
1007 </a>
1008 {dot}
1009 <Link to="/support" className={quiet}>
1010 Support
1011 </Link>
1012 </div>
1013 <div className="flex items-center gap-0.5">
1014 {LEGAL_LINKS.map(([label, to], index) => (
1015 <span key={to} className="flex items-center gap-0.5">
1016 {index > 0 && dot}
1017 <Link to={to} className={quiet}>
1018 {label}
1019 </Link>
1020 </span>
1021 ))}
1022 </div>
1023 </nav>
1024 );
1025}
1026
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1027function VisitorPanel() {
1028 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1029 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1030 return (
1031 <div className="space-y-2">
1032 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1033 <div className="grid grid-cols-2 gap-2">
1034 <Link
1035 to={withNext("/login", pathname + search)}
1036 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"
1037 >
1038 <LogIn size={14} />
1039 Sign in
1040 </Link>
1041 <Link
1042 to={withNext("/register", pathname + search)}
1043 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1044 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1045 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1046 </Link>
1047 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1048 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1049 </div>
1050 );
1051}
1052
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1053function Sidebar({
1054 user,
1055 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1056 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1057 onFind,
1058 onClose,
1059}: {
1060 user: User | null;
1061 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1062 /** The page is a 404: the address names nothing the viewer can see. */
1063 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1064 onFind: () => void;
1065 /** In the sheet on a small screen: closing it, at the end of the top row. */
1066 onClose?: () => void;
1067}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1068 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1069 const { pathname } = useLocation();
1070 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1071 // Where the sidebar is drilled to follows the page being gone to, so it
1072 // moves as the link is followed, not once the page arrives.
1073 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1074 // On a 404, the address is not a place: no list is drilled into from it,
1075 // except the project's own when the project is real and visible and only
1076 // something inside it is missing (a file, a commit, an issue).
1077 const lost = missing && going == null;
1078 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1079 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1080 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1081 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1082 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1083 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"]);
1084 // An invitation to a repository is answered before its menu means anything.
1085 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1086 repoPath != null &&
1087 !reserved.has(repoPath[1]) &&
1088 repoPath[2] !== "-" &&
1089 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1090 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1091 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1092 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1093 // The repository the menu is for: the one loaded if it is the one being
1094 // gone to, else what the address says, at once.
1095 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 look1096 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1097 ? sameRepo(active, targetRepo)
1098 ? active
1099 : {
1100 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1101 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1102 }
1103 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1104 // The viewer's role there, from the repository's page once it has loaded.
1105 const page = useRouteLoaderData("routes/repo/layout") as
1106 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1107 | undefined;
1108 const menuRepo: MenuRepo | null =
1109 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1110 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1111 : guessed;
1112
1113 // The way from the main list to the one shown.
1114 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1115 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1116 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1117 } else if (ws && inSettings) {
1118 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1119 } else if (menuRepo) {
1120 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1121 // Out of a project: to its workspace's projects when they are yours.
1122 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1123 const back = !user
1124 ? { to: "/explore", label: "Explore" }
1125 : home
1126 ? { to: `/${home.slug}`, label: "All projects" }
1127 : { to: "/", label: "Mission control" };
1128 trail.push({
1129 key,
1130 node: (
1131 <RepoMenu
1132 repo={menuRepo}
1133 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1134 back={back}
1135 />
1136 ),
1137 });
1138 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1139 }
1140
Agents as a team: lifecycle, merge queue, billing and a new shell1141 return (
1142 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1143 {/* The same height and rule as the top bar, so the two read as one line. */}
Workspace names and icons, and a component kit for every control1144 <div className="flex h-16 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 put1145 {user ? (
1146 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1147 {/* The 1 alone beside the workspace: a square hover the height of the
1148 switcher, the mark as tall as the workspace avatar. */}
1149 <Link to="/" aria-label="g1t home" className="flex size-9 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-raised">
1150 <Mark tight className="h-5 w-auto" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1151 </Link>
1152 <span className="shrink-0 text-line-strong" aria-hidden="true">
1153 /
1154 </span>
1155 <WorkspaceSwitcher user={user} shell={shell} />
1156 </>
1157 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1158 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1159 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1160 </Link>
1161 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1162 {onClose && (
1163 <button
1164 type="button"
1165 aria-label="Close menu"
1166 onClick={onClose}
1167 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1168 >
1169 <X size={16} />
1170 </button>
1171 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1172 </div>
A Vercel-like sidebar and a new mark1173 <div className="px-2 pt-3">
1174 <button
1175 type="button"
1176 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1177 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 mark1178 >
1179 <Search size={14} />
1180 <span className="grow text-left">Find…</span>
1181 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1182 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1183 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1184 <Drill trail={trail} />
1185 {/* The account, or signing in, as one row at the very bottom. */}
1186 <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 put1187 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1188 </div>
1189 </div>
1190 );
1191}
1192
1193/** Words for the sections a path can end in. */
1194const SECTIONS: Record<string, string> = {
1195 issues: "Issues",
1196 pulls: "Pull requests",
1197 queue: "Merge queue",
1198 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1199 plans: "Outcomes",
1200 actions: "Workflows",
1201 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1202 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1203 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1204 access: "Access",
1205 invitations: "Invitation",
1206 repositories: "Repositories",
1207 branches: "Branches and merging",
Project dependencies: addresses, preview stacks, Affects, and agents who know1208 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1209 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1210 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1211 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1212 people: "Members",
A Vercel-like sidebar and a new mark1213 tokens: "Access tokens",
1214 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1215 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1216 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1217 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1218 domains: "Domains",
1219 guardrails: "Guardrails",
1220 audit: "Audit log",
A Vercel-like sidebar and a new mark1221 tree: "Code",
1222 blob: "Code",
1223};
1224
1225/** 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 quotas1226function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1227 // A 404 names nothing from the address, unless the project is real and
1228 // visible and only something inside it is missing.
1229 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1230 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1231 return <span className="text-sm font-medium">Not found</span>;
1232 }
A Vercel-like sidebar and a new mark1233 const parts = pathname.split("/").filter(Boolean);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1234 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite"];
A Vercel-like sidebar and a new mark1235 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 quotas1236 // Your settings: Settings / Emails.
1237 if (parts[0] === "settings") {
1238 const page = accountSettingsPage(pathname);
1239 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1240 return (
1241 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1242 {trail.map((crumb, index) => (
1243 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1244 {index > 0 && <span className="text-line-strong">/</span>}
1245 <Link
1246 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1247 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1248 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1249 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1250 }`}
1251 >
1252 {crumb.label}
1253 </Link>
1254 </span>
1255 ))}
1256 </nav>
1257 );
1258 }
A Vercel-like sidebar and a new mark1259 if (reserved.includes(parts[0]!)) {
1260 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1261 settings: "Account",
A Vercel-like sidebar and a new mark1262 explore: "Explore",
Projects live: fixes from walking them end to end1263 new: "New project",
A Vercel-like sidebar and a new mark1264 search: "Search",
1265 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1266 policies: "Policies",
1267 security: "Security",
1268 support: "Support",
1269 status: "Status",
1270 invite: "Invite",
A Vercel-like sidebar and a new mark1271 };
1272 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1273 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1274 // A person's profile, by their handle.
1275 if (parts[0] === "u" && parts[1]) {
1276 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1277 }
A Vercel-like sidebar and a new mark1278 const [owner, second, third, fourth] = parts;
1279 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1280 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1281 const page = `/${owner}/-/${third}`;
1282 if (third && SETTINGS_PAGE.test(page)) {
1283 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1284 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1285 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1286 } else if (second) {
1287 const repo = `/${owner}/${second}`;
1288 trail.push({ label: second, to: repo, mono: true });
1289 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1290 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1291 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 deployments1292 else if (third && SECTIONS[third]) {
1293 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1294 // A settings page names which one: Settings / Secrets and variables.
1295 if (third === "settings" && fourth && SECTIONS[fourth]) {
1296 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
1297 }
1298 }
A Vercel-like sidebar and a new mark1299 }
1300 return (
1301 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1302 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1303 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1304 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1305 <Link
A Vercel-like sidebar and a new mark1306 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1307 prefetch="intent"
A Vercel-like sidebar and a new mark1308 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1309 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1310 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1311 >
A Vercel-like sidebar and a new mark1312 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1313 </Link>
A Vercel-like sidebar and a new mark1314 </span>
1315 ))}
1316 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1317 );
1318}
1319
Search across all of g1t, Explore, and a command palette1320type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1321
1322/** 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 look1323function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1324 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1325 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1326 { 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 step1327 ...(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-agent1328 ? [{ 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 step1329 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1330 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1331 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page1332 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1333 { 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 quotas1334 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1335 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1336 .filter((page) => page !== "profile")
1337 .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 shell1338 ];
1339 const repo = shell.repo;
1340 if (repo) {
1341 const base = `/${repo.namespace}/${repo.name}`;
1342 const name = `${repo.namespace}/${repo.name}`;
1343 commands.unshift(
Projects: what a workspace builds and runs, first on every page1344 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1345 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1346 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1347 { 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 step1348 ...(repo.member
1349 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1350 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1351 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1352 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1353 ...(repo.member
1354 ? [
1355 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1356 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1357 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1358 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1359 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1360 ]
1361 : []),
1362 );
1363 }
1364 for (const membership of user.workspaces ?? []) {
1365 commands.push(
Workspace names and icons, and a component kit for every control1366 { 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 request1367 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1368 { 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 settings1369 { 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 look1370 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1371 );
1372 }
1373 for (const listed of shell.repos) {
1374 commands.push({
Projects: what a workspace builds and runs, first on every page1375 label: listed.title ?? listed.name,
1376 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1377 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1378 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1379 });
1380 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1381 // "Put an agent on …": one per project, after the projects themselves.
1382 for (const listed of shell.repos) {
1383 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1384 commands.push({
1385 label: `Put an agent on ${listed.title ?? listed.name}`,
1386 hint: `${listed.namespace}/${listed.name}`,
1387 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1388 icon: <Sparkles size={15} />,
1389 });
1390 }
Agents as a team: lifecycle, merge queue, billing and a new shell1391 return commands;
1392}
1393
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1394/** 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 look1395function 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 put1396 const commands: Command[] = [];
1397 const repo = shell.repo;
1398 if (repo) {
1399 const base = `/${repo.namespace}/${repo.name}`;
1400 const name = `${repo.namespace}/${repo.name}`;
1401 commands.push(
1402 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1403 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1404 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1405 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1406 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1407 );
1408 }
1409 commands.push(
1410 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1411 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1412 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1413 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1414 { 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 look1415 { 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 put1416 );
1417 return commands;
1418}
1419
Agents as a team: lifecycle, merge queue, billing and a new shell1420/**
1421 * A bar across the top of the page while the next one loads, as GitHub
1422 * has: it appears at once, creeps towards the end while waiting, then fills
1423 * and fades when the page arrives.
1424 */
1425export function Progress() {
1426 const navigation = useNavigation();
1427 const busy = navigation.state !== "idle";
1428 const [width, setWidth] = useState(0);
1429 const [visible, setVisible] = useState(false);
1430 useEffect(() => {
1431 if (busy) {
1432 setVisible(true);
1433 setWidth(12);
1434 // Slows as it goes, never quite arriving until the page does.
1435 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1436 return () => clearInterval(timer);
1437 }
1438 setWidth((now) => (now > 0 ? 100 : 0));
1439 const fade = setTimeout(() => setVisible(false), 250);
1440 const reset = setTimeout(() => setWidth(0), 550);
1441 return () => {
1442 clearTimeout(fade);
1443 clearTimeout(reset);
1444 };
1445 }, [busy]);
1446 return (
1447 <div
1448 aria-hidden="true"
1449 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1450 visible ? "opacity-100" : "opacity-0"
1451 }`}
1452 >
1453 <div
1454 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1455 style={{ width: `${width}%` }}
1456 />
1457 </div>
1458 );
1459}
1460
1461/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1462 * The app: a sidebar with the workspace, its repositories and the sections
1463 * of the one being looked at; a slim bar with search and the account; and
1464 * the page. A visitor who is not signed in gets the same frame, with
1465 * Explore and Search in place of the workspace, and signing in in place of
1466 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1467 */
1468export function AppShell({
1469 user,
1470 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1471 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1472 banner,
1473 children,
1474}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1475 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1476 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1477 /** The page is a 404 (see Sidebar). */
1478 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1479 banner?: ReactNode;
1480 children: ReactNode;
1481}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1482 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1483 const [drawer, setDrawer] = useState(false);
1484 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1485 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1486 const signUpLabel = useSignUpCopy().primary;
1487 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1488
1489 // A new page closes the drawer on small screens.
1490 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1491 // Mission control's code, fetched while the browser is idle, so going
1492 // home never waits on it (routes/home.tsx).
1493 useEffect(() => {
1494 if (!user) return;
1495 const load = () => void import("./mission-control").catch(() => undefined);
1496 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1497 if (idle) idle(load);
1498 else setTimeout(load, 1500);
1499 }, [user]);
Search across all of g1t, Explore, and a command palette1500 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1501
1502 return (
1503 <div className="min-h-screen">
1504 <Progress />
1505 <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 quotas1506 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1507 </aside>
1508 {drawer && (
1509 <div className="fixed inset-0 z-50 lg:hidden">
1510 <button
1511 type="button"
1512 aria-label="Close menu"
1513 className="absolute inset-0 bg-black/60"
1514 onClick={() => setDrawer(false)}
1515 />
1516 <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 quotas1517 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1518 </aside>
1519 </div>
1520 )}
1521
Polish: phones, copy boxes, the plan page, the landing page, a real glide1522 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals1523 <header className="sticky top-0 z-30 flex h-16 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 shell1524 <button
1525 type="button"
1526 aria-label="Open menu"
1527 onClick={() => setDrawer(true)}
1528 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1529 >
1530 <Menu size={18} />
1531 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1532 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1533 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1534 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1535 <input
1536 name="q"
1537 {...notACredential()}
1538 placeholder="Search g1t"
1539 aria-label="Search g1t"
1540 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"
1541 />
1542 <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">
1543 ⌘K
1544 </kbd>
1545 </Form>
1546 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1547 <a
1548 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1549 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 mark1550 >
1551 Docs
1552 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1553 {!user ? (
1554 // The sidebar has these too, but on a phone it is folded away.
1555 <Link
1556 to={withNext("/login", here)}
1557 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"
1558 >
1559 Sign in
1560 </Link>
1561 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1562 <DropdownMenu>
1563 <DropdownMenuTrigger
1564 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1565 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 shell1566 >
A Vercel-like sidebar and a new mark1567 <Plus size={14} />
1568 New
Agents as a team: lifecycle, merge queue, billing and a new shell1569 </DropdownMenuTrigger>
1570 <DropdownMenuContent align="end">
1571 {shell.repo && (
1572 <DropdownMenuItem asChild>
1573 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1574 <CircleDot />
1575 New issue
1576 </Link>
1577 </DropdownMenuItem>
1578 )}
1579 {shell.repo?.member && (
1580 <DropdownMenuItem asChild>
1581 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1582 <ListTree />
1583 Plan work
1584 </Link>
1585 </DropdownMenuItem>
1586 )}
1587 <DropdownMenuItem asChild>
1588 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1589 <Box />
1590 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1591 </Link>
1592 </DropdownMenuItem>
1593 <DropdownMenuItem asChild>
1594 <Link to="/workspaces/new">
1595 <Users />
1596 New workspace
1597 </Link>
1598 </DropdownMenuItem>
1599 </DropdownMenuContent>
1600 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1601 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1602 </div>
1603 </header>
1604 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1605 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1606 </div>
Search across all of g1t, Explore, and a command palette1607 <CommandPalette
1608 open={palette}
1609 onOpenChange={setPalette}
1610 commands={commands}
1611 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1612 />
Agents as a team: lifecycle, merge queue, billing and a new shell1613 </div>
1614 );
1615}