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

1,618 lines65,790 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>
726 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
727 Runners
728 </SidebarLink>
729 </SidebarGroup>
730 <SidebarGroup title="Connections" className="mt-3">
731 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
732 Integrations
733 </SidebarLink>
734 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
735 Webhooks
736 </SidebarLink>
737 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look738 <Rule />
739 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings740 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments741 Billing and plans
A catalogue of model providers, and settings that feel like settings742 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API743 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
744 Audit log
745 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look746 </div>
A catalogue of model providers, and settings that feel like settings747 </nav>
748 );
749}
750
Sidebar: no settings flash on the way into a repository, and a better slide751/**
752 * What the repository menu needs. Known from the address before the page's
753 * data arrives, so the menu is right from the first frame; the counts fill
754 * in once it does.
755 */
756type MenuRepo = {
757 namespace: string;
758 name: string;
759 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look760 /** Whether they see its settings; from the repository's page once it loads. */
761 settings?: boolean;
762 /** What they may do there, once the repository's page loads. */
763 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide764 issues?: number;
765 pulls?: number;
766};
767
768function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
769 return (
770 a != null &&
771 b != null &&
772 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
773 a.name.toLowerCase() === b.name.toLowerCase()
774 );
775}
A repository has its own sidebar, as settings do776
777/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look778 * A project's own list, drilled into while you are in it: everything about
779 * the project, running and its code, and nothing else, with the way back
780 * out to all of them.
A repository has its own sidebar, as settings do781 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put782function RepoMenu({
783 repo,
784 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put786}: {
787 repo: MenuRepo;
788 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look789 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
790 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put791}) {
A repository has its own sidebar, as settings do792 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 put793 // 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 look794 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do795 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look796 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
797 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner798 <NavLink
A repository has its own sidebar, as settings do799 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner800 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents801 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner802 title="Overview"
803 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look804 `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 owner805 }
A repository has its own sidebar, as settings do806 >
807 <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 page808 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do809 </span>
810 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
811 <span className="text-faint">{repo.namespace}/</span>
812 <span className="font-semibold text-fg">{repo.name}</span>
813 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner814 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look815 {/* A project's pages, in the order people use them. A page with more
816 than one view shows them as tabs across its top. */}
817 <Rule />
818 <div className="space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner819 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
820 Code
821 </SidebarLink>
822 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
823 Issues
824 </SidebarLink>
825 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
826 Pull requests
827 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains828 <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 owner829 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains830 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner831 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
832 Workflows
Projects: what a workspace builds and runs, first on every page833 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look834 </div>
835 <Rule />
836 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put837 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner838 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
839 Deployments
840 </SidebarLink>
841 ) : null}
Observability gets its own page; tabs never overflow842 <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.">
843 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner844 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put845 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API846 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
847 Security
848 </SidebarLink>
849 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner850 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
851 Insights
852 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look853 </div>
854 {(repo.settings ?? shows.has("settings")) && (
855 <>
856 <Rule />
857 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner858 Settings
859 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look860 </>
861 )}
A repository has its own sidebar, as settings do862 </nav>
863 );
864}
865
Project settings move into the sidebar, grouped, with a heading per page866/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look867 * A project's settings, drilled into from its list: what is about running
868 * it, then its agents, then its code and who can reach it. The way back
869 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page870 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page872 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look873 // Each page shows to the roles that can use it, once the role is known.
874 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
875 const running = shows("deployments");
876 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page877 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look878 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
879 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
880 {shows("") && (
881 <div className="mt-2 space-y-px">
882 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
883 General
884 </SidebarLink>
885 </div>
886 )}
887 {running && (
888 <>
889 {shows("") && <Rule />}
890 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
891 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
892 Deployments
893 </SidebarLink>
894 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
895 Domains
896 </SidebarLink>
897 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
898 Dependencies
899 </SidebarLink>
900 </div>
901 </>
902 )}
903 {agents && (
904 <>
905 <Rule />
906 <div className="space-y-px">
907 {shows("agents") && (
908 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
909 Agents
910 </SidebarLink>
911 )}
912 {shows("guardrails") && (
913 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
914 Guardrails
915 </SidebarLink>
916 )}
917 </div>
918 </>
919 )}
920 <Rule />
921 <div className="space-y-px">
922 {shows("repository") && (
923 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
924 Repository
925 </SidebarLink>
926 )}
927 {shows("access") && (
928 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
929 Access
930 </SidebarLink>
931 )}
932 {shows("branches") && (
933 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
934 Branches and merging
935 </SidebarLink>
936 )}
937 {shows("secrets") && (
938 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
939 Secrets and variables
940 </SidebarLink>
941 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents942 {shows("runners") && (
943 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
944 Runners
945 </SidebarLink>
946 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look947 {shows("webhooks") && (
948 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
949 Webhooks
950 </SidebarLink>
951 )}
952 </div>
Project settings move into the sidebar, grouped, with a heading per page953 </nav>
954 );
955}
956
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas957/** Your own settings, drilled into from Your settings: one page each. */
958function AccountSettingsMenu({ username }: { username: string }) {
959 const link = (page: AccountSettingsPage, icon: ReactNode) => (
960 <SidebarLink to={`/settings/${page}`} icon={icon}>
961 {ACCOUNT_SETTINGS[page].title}
962 </SidebarLink>
Account settings in the same shape as a workspace's963 );
964 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look965 <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 quotas966 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look967 <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 quotas968 {link("profile", <CircleUserRound size={15} />)}
969 {link("emails", <Mail size={15} />)}
970 {link("invites", <Ticket size={15} />)}
971 </div>
972 <Rule />
973 <div className="space-y-px">
974 {link("keys", <Fingerprint size={15} />)}
975 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look976 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas977 <Rule />
978 <div className="space-y-px">
979 {link("github", <GithubMark className="size-[15px]" />)}
980 {link("applications", <Plug size={15} />)}
981 </div>
982 <Rule />
983 <div className="space-y-px">
984 {link("security-log", <History size={15} />)}
985 </div>
Account settings in the same shape as a workspace's986 </nav>
987 );
988}
989
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put990/**
991 * Signing in and signing up, in place of the account for a visitor. Signing
992 * in brings them back to the page they are on.
993 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas994/** Help, status and the fine print under a visitor's sign-in buttons. */
995function VisitorLinks() {
996 const status = useSiteStatus();
997 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
998 const dot = (
999 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1000 ·
1001 </span>
1002 );
1003 return (
1004 // Two short rows, so a narrow sidebar never starts a line with a dot.
1005 <nav aria-label="About g1t" className="pt-0.5">
1006 <div className="flex items-center gap-0.5">
1007 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1008 <StatusDot state={status?.overall.state ?? null} />
1009 Status
1010 </a>
1011 {dot}
1012 <Link to="/support" className={quiet}>
1013 Support
1014 </Link>
1015 </div>
1016 <div className="flex items-center gap-0.5">
1017 {LEGAL_LINKS.map(([label, to], index) => (
1018 <span key={to} className="flex items-center gap-0.5">
1019 {index > 0 && dot}
1020 <Link to={to} className={quiet}>
1021 {label}
1022 </Link>
1023 </span>
1024 ))}
1025 </div>
1026 </nav>
1027 );
1028}
1029
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1030function VisitorPanel() {
1031 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1032 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1033 return (
1034 <div className="space-y-2">
1035 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1036 <div className="grid grid-cols-2 gap-2">
1037 <Link
1038 to={withNext("/login", pathname + search)}
1039 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"
1040 >
1041 <LogIn size={14} />
1042 Sign in
1043 </Link>
1044 <Link
1045 to={withNext("/register", pathname + search)}
1046 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1047 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1048 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1049 </Link>
1050 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1051 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1052 </div>
1053 );
1054}
1055
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1056function Sidebar({
1057 user,
1058 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1059 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1060 onFind,
1061 onClose,
1062}: {
1063 user: User | null;
1064 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1065 /** The page is a 404: the address names nothing the viewer can see. */
1066 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1067 onFind: () => void;
1068 /** In the sheet on a small screen: closing it, at the end of the top row. */
1069 onClose?: () => void;
1070}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1071 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1072 const { pathname } = useLocation();
1073 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1074 // Where the sidebar is drilled to follows the page being gone to, so it
1075 // moves as the link is followed, not once the page arrives.
1076 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1077 // On a 404, the address is not a place: no list is drilled into from it,
1078 // except the project's own when the project is real and visible and only
1079 // something inside it is missing (a file, a commit, an issue).
1080 const lost = missing && going == null;
1081 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1082 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1083 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1084 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1085 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1086 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"]);
1087 // An invitation to a repository is answered before its menu means anything.
1088 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1089 repoPath != null &&
1090 !reserved.has(repoPath[1]) &&
1091 repoPath[2] !== "-" &&
1092 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1093 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1094 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1095 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1096 // The repository the menu is for: the one loaded if it is the one being
1097 // gone to, else what the address says, at once.
1098 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 look1099 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1100 ? sameRepo(active, targetRepo)
1101 ? active
1102 : {
1103 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1104 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1105 }
1106 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1107 // The viewer's role there, from the repository's page once it has loaded.
1108 const page = useRouteLoaderData("routes/repo/layout") as
1109 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1110 | undefined;
1111 const menuRepo: MenuRepo | null =
1112 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1113 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1114 : guessed;
1115
1116 // The way from the main list to the one shown.
1117 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1118 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1119 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1120 } else if (ws && inSettings) {
1121 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1122 } else if (menuRepo) {
1123 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1124 // Out of a project: to its workspace's projects when they are yours.
1125 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1126 const back = !user
1127 ? { to: "/explore", label: "Explore" }
1128 : home
1129 ? { to: `/${home.slug}`, label: "All projects" }
1130 : { to: "/", label: "Mission control" };
1131 trail.push({
1132 key,
1133 node: (
1134 <RepoMenu
1135 repo={menuRepo}
1136 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1137 back={back}
1138 />
1139 ),
1140 });
1141 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1142 }
1143
Agents as a team: lifecycle, merge queue, billing and a new shell1144 return (
1145 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1146 {/* 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 bar1147 <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 put1148 {user ? (
1149 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1150 {/* The 1 alone beside the workspace: a square hover the height of the
1151 switcher, the mark as tall as the workspace avatar. */}
1152 <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 bar1153 <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 put1154 </Link>
1155 <span className="shrink-0 text-line-strong" aria-hidden="true">
1156 /
1157 </span>
1158 <WorkspaceSwitcher user={user} shell={shell} />
1159 </>
1160 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1161 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1162 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1163 </Link>
1164 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1165 {onClose && (
1166 <button
1167 type="button"
1168 aria-label="Close menu"
1169 onClick={onClose}
1170 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1171 >
1172 <X size={16} />
1173 </button>
1174 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1175 </div>
A Vercel-like sidebar and a new mark1176 <div className="px-2 pt-3">
1177 <button
1178 type="button"
1179 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1180 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 mark1181 >
1182 <Search size={14} />
1183 <span className="grow text-left">Find…</span>
1184 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1185 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1186 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1187 <Drill trail={trail} />
1188 {/* The account, or signing in, as one row at the very bottom. */}
1189 <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 put1190 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1191 </div>
1192 </div>
1193 );
1194}
1195
1196/** Words for the sections a path can end in. */
1197const SECTIONS: Record<string, string> = {
1198 issues: "Issues",
1199 pulls: "Pull requests",
1200 queue: "Merge queue",
1201 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1202 plans: "Outcomes",
1203 actions: "Workflows",
1204 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1205 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1206 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1207 access: "Access",
1208 invitations: "Invitation",
1209 repositories: "Repositories",
1210 branches: "Branches and merging",
Project dependencies: addresses, preview stacks, Affects, and agents who know1211 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1212 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1213 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1214 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1215 people: "Members",
A Vercel-like sidebar and a new mark1216 tokens: "Access tokens",
1217 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1218 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1219 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1220 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1221 domains: "Domains",
1222 guardrails: "Guardrails",
1223 audit: "Audit log",
A Vercel-like sidebar and a new mark1224 tree: "Code",
1225 blob: "Code",
1226};
1227
1228/** 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 quotas1229function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1230 // A 404 names nothing from the address, unless the project is real and
1231 // visible and only something inside it is missing.
1232 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1233 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1234 return <span className="text-sm font-medium">Not found</span>;
1235 }
A Vercel-like sidebar and a new mark1236 const parts = pathname.split("/").filter(Boolean);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1237 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite"];
A Vercel-like sidebar and a new mark1238 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 quotas1239 // Your settings: Settings / Emails.
1240 if (parts[0] === "settings") {
1241 const page = accountSettingsPage(pathname);
1242 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1243 return (
1244 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1245 {trail.map((crumb, index) => (
1246 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1247 {index > 0 && <span className="text-line-strong">/</span>}
1248 <Link
1249 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1250 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1251 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1252 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1253 }`}
1254 >
1255 {crumb.label}
1256 </Link>
1257 </span>
1258 ))}
1259 </nav>
1260 );
1261 }
A Vercel-like sidebar and a new mark1262 if (reserved.includes(parts[0]!)) {
1263 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1264 settings: "Account",
A Vercel-like sidebar and a new mark1265 explore: "Explore",
Projects live: fixes from walking them end to end1266 new: "New project",
A Vercel-like sidebar and a new mark1267 search: "Search",
1268 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1269 policies: "Policies",
1270 security: "Security",
1271 support: "Support",
1272 status: "Status",
1273 invite: "Invite",
A Vercel-like sidebar and a new mark1274 };
1275 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1276 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1277 // A person's profile, by their handle.
1278 if (parts[0] === "u" && parts[1]) {
1279 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1280 }
A Vercel-like sidebar and a new mark1281 const [owner, second, third, fourth] = parts;
1282 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1283 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1284 const page = `/${owner}/-/${third}`;
1285 if (third && SETTINGS_PAGE.test(page)) {
1286 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1287 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1288 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1289 } else if (second) {
1290 const repo = `/${owner}/${second}`;
1291 trail.push({ label: second, to: repo, mono: true });
1292 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1293 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1294 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 deployments1295 else if (third && SECTIONS[third]) {
1296 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1297 // A settings page names which one: Settings / Secrets and variables.
1298 if (third === "settings" && fourth && SECTIONS[fourth]) {
1299 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
1300 }
1301 }
A Vercel-like sidebar and a new mark1302 }
1303 return (
1304 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1305 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1306 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1307 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1308 <Link
A Vercel-like sidebar and a new mark1309 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1310 prefetch="intent"
A Vercel-like sidebar and a new mark1311 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1312 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1313 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1314 >
A Vercel-like sidebar and a new mark1315 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1316 </Link>
A Vercel-like sidebar and a new mark1317 </span>
1318 ))}
1319 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1320 );
1321}
1322
Search across all of g1t, Explore, and a command palette1323type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1324
1325/** 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 look1326function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1327 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1328 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1329 { 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 step1330 ...(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-agent1331 ? [{ 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 step1332 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1333 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1334 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page1335 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1336 { 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 quotas1337 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1338 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1339 .filter((page) => page !== "profile")
1340 .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 shell1341 ];
1342 const repo = shell.repo;
1343 if (repo) {
1344 const base = `/${repo.namespace}/${repo.name}`;
1345 const name = `${repo.namespace}/${repo.name}`;
1346 commands.unshift(
Projects: what a workspace builds and runs, first on every page1347 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1348 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1349 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1350 { 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 step1351 ...(repo.member
1352 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1353 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1354 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1355 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1356 ...(repo.member
1357 ? [
1358 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1359 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1360 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1361 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1362 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1363 ]
1364 : []),
1365 );
1366 }
1367 for (const membership of user.workspaces ?? []) {
1368 commands.push(
Workspace names and icons, and a component kit for every control1369 { 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 request1370 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1371 { 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 settings1372 { 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 look1373 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1374 );
1375 }
1376 for (const listed of shell.repos) {
1377 commands.push({
Projects: what a workspace builds and runs, first on every page1378 label: listed.title ?? listed.name,
1379 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1380 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1381 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1382 });
1383 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1384 // "Put an agent on …": one per project, after the projects themselves.
1385 for (const listed of shell.repos) {
1386 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1387 commands.push({
1388 label: `Put an agent on ${listed.title ?? listed.name}`,
1389 hint: `${listed.namespace}/${listed.name}`,
1390 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1391 icon: <Sparkles size={15} />,
1392 });
1393 }
Agents as a team: lifecycle, merge queue, billing and a new shell1394 return commands;
1395}
1396
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1397/** 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 look1398function 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 put1399 const commands: Command[] = [];
1400 const repo = shell.repo;
1401 if (repo) {
1402 const base = `/${repo.namespace}/${repo.name}`;
1403 const name = `${repo.namespace}/${repo.name}`;
1404 commands.push(
1405 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1406 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1407 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1408 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1409 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1410 );
1411 }
1412 commands.push(
1413 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1414 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1415 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1416 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1417 { 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 look1418 { 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 put1419 );
1420 return commands;
1421}
1422
Agents as a team: lifecycle, merge queue, billing and a new shell1423/**
1424 * A bar across the top of the page while the next one loads, as GitHub
1425 * has: it appears at once, creeps towards the end while waiting, then fills
1426 * and fades when the page arrives.
1427 */
1428export function Progress() {
1429 const navigation = useNavigation();
1430 const busy = navigation.state !== "idle";
1431 const [width, setWidth] = useState(0);
1432 const [visible, setVisible] = useState(false);
1433 useEffect(() => {
1434 if (busy) {
1435 setVisible(true);
1436 setWidth(12);
1437 // Slows as it goes, never quite arriving until the page does.
1438 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1439 return () => clearInterval(timer);
1440 }
1441 setWidth((now) => (now > 0 ? 100 : 0));
1442 const fade = setTimeout(() => setVisible(false), 250);
1443 const reset = setTimeout(() => setWidth(0), 550);
1444 return () => {
1445 clearTimeout(fade);
1446 clearTimeout(reset);
1447 };
1448 }, [busy]);
1449 return (
1450 <div
1451 aria-hidden="true"
1452 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1453 visible ? "opacity-100" : "opacity-0"
1454 }`}
1455 >
1456 <div
1457 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1458 style={{ width: `${width}%` }}
1459 />
1460 </div>
1461 );
1462}
1463
1464/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1465 * The app: a sidebar with the workspace, its repositories and the sections
1466 * of the one being looked at; a slim bar with search and the account; and
1467 * the page. A visitor who is not signed in gets the same frame, with
1468 * Explore and Search in place of the workspace, and signing in in place of
1469 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1470 */
1471export function AppShell({
1472 user,
1473 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1474 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1475 banner,
1476 children,
1477}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1478 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1479 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1480 /** The page is a 404 (see Sidebar). */
1481 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1482 banner?: ReactNode;
1483 children: ReactNode;
1484}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1485 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1486 const [drawer, setDrawer] = useState(false);
1487 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1488 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1489 const signUpLabel = useSignUpCopy().primary;
1490 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1491
1492 // A new page closes the drawer on small screens.
1493 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1494 // Mission control's code, fetched while the browser is idle, so going
1495 // home never waits on it (routes/home.tsx).
1496 useEffect(() => {
1497 if (!user) return;
1498 const load = () => void import("./mission-control").catch(() => undefined);
1499 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1500 if (idle) idle(load);
1501 else setTimeout(load, 1500);
1502 }, [user]);
Search across all of g1t, Explore, and a command palette1503 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1504
1505 return (
1506 <div className="min-h-screen">
1507 <Progress />
1508 <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 quotas1509 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1510 </aside>
1511 {drawer && (
1512 <div className="fixed inset-0 z-50 lg:hidden">
1513 <button
1514 type="button"
1515 aria-label="Close menu"
1516 className="absolute inset-0 bg-black/60"
1517 onClick={() => setDrawer(false)}
1518 />
1519 <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 quotas1520 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1521 </aside>
1522 </div>
1523 )}
1524
Polish: phones, copy boxes, the plan page, the landing page, a real glide1525 <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 bar1526 <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 shell1527 <button
1528 type="button"
1529 aria-label="Open menu"
1530 onClick={() => setDrawer(true)}
1531 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1532 >
1533 <Menu size={18} />
1534 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1535 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1536 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1537 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1538 <input
1539 name="q"
1540 {...notACredential()}
1541 placeholder="Search g1t"
1542 aria-label="Search g1t"
1543 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"
1544 />
1545 <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">
1546 ⌘K
1547 </kbd>
1548 </Form>
1549 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1550 <a
1551 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1552 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 mark1553 >
1554 Docs
1555 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1556 {!user ? (
1557 // The sidebar has these too, but on a phone it is folded away.
1558 <Link
1559 to={withNext("/login", here)}
1560 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"
1561 >
1562 Sign in
1563 </Link>
1564 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1565 <DropdownMenu>
1566 <DropdownMenuTrigger
1567 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1568 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 shell1569 >
A Vercel-like sidebar and a new mark1570 <Plus size={14} />
1571 New
Agents as a team: lifecycle, merge queue, billing and a new shell1572 </DropdownMenuTrigger>
1573 <DropdownMenuContent align="end">
1574 {shell.repo && (
1575 <DropdownMenuItem asChild>
1576 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1577 <CircleDot />
1578 New issue
1579 </Link>
1580 </DropdownMenuItem>
1581 )}
1582 {shell.repo?.member && (
1583 <DropdownMenuItem asChild>
1584 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1585 <ListTree />
1586 Plan work
1587 </Link>
1588 </DropdownMenuItem>
1589 )}
1590 <DropdownMenuItem asChild>
1591 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1592 <Box />
1593 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1594 </Link>
1595 </DropdownMenuItem>
1596 <DropdownMenuItem asChild>
1597 <Link to="/workspaces/new">
1598 <Users />
1599 New workspace
1600 </Link>
1601 </DropdownMenuItem>
1602 </DropdownMenuContent>
1603 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1604 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1605 </div>
1606 </header>
1607 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1608 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1609 </div>
Search across all of g1t, Explore, and a command palette1610 <CommandPalette
1611 open={palette}
1612 onOpenChange={setPalette}
1613 commands={commands}
1614 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1615 />
Agents as a team: lifecycle, merge queue, billing and a new shell1616 </div>
1617 );
1618}