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

1,631 lines66,158 bytesCodeBlame

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

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