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

1,620 lines65,827 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

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