Skip to content
1,876 linesCodeBlameRaw

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.

The sidebar says where you are; the workspace page loses its tabs1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, Inbox, KanbanSquare, KeyRound, LayoutDashboard, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, TrendingUp, 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";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Owners choose who can create teams5import { type Abilities, type InboxCounts, type Membership, type Spike, type User, mayCreateTeams } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Usage, Billing settings and prepaid AI credit; fixes from the UX audit7import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar8import { AgentButton, InboxBell } from "./inbox";
9import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit11import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height13import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit14import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents15import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell16import {
17 DropdownMenu,
18 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas19 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell20 DropdownMenuItem,
21 DropdownMenuLabel,
22 DropdownMenuSeparator,
23 DropdownMenuTrigger,
24} from "./ui/dropdown-menu";
The sidebar says where you are; the workspace page loses its tabs25import { type RoadmapItem, roadmapIn, roadmapItem } from "../lib/roadmap";
26import { SETTINGS_PAGES, sidebarCurrent } from "../lib/workspace-nav";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27import { 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 put28import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas29import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
30import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put31import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas33import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar34import { type ShortcutProject, movedPin, recentWith } from "../lib/pins";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas35import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell36
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put37/**
38 * What the sidebar needs, worked out by the root loader. For a visitor who
39 * is not signed in there is no workspace, no projects and no usage: only
40 * the project being looked at, if they can see it.
41 */
Agents as a team: lifecycle, merge queue, billing and a new shell42export type ShellData = {
43 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control44 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar45 /** All its projects, by name, for the palette: `name` is the slug in their address. */
46 repos: ShortcutProject[];
47 /** The person's pinned projects in it, in their order (lib/pins.ts). */
48 pinned?: ShortcutProject[];
49 /** What they opened there last, latest first, leaving out the pinned. */
50 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51 /** Repositories shared with them in workspaces they do not belong to. */
52 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page53 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell54 repo: {
55 namespace: string;
56 name: string;
57 member: boolean;
58 issues: number;
59 pulls: number;
60 } | null;
Billing accounts, terms and enterprises; g1t is no longer free61 /** Where the workspace stands against its usage limit, if billing is on. */
62 limit: {
63 exposureMicros: number;
64 ceilingMicros: number | null;
65 state: "ok" | "warning" | "stopped";
66 comped: boolean;
67 } | null;
Free while g1t is being built out; agents can check out their own forks68 /** Whether g1t charges nothing for now, while it is being built out. */
69 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request70 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy71 /** This month's usage: charged, or at cost while g1t is free. */
72 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
74 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all75 /** What is unread in their inbox, for the bell. Absent for a visitor. */
76 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell77};
78
79function SidebarLink({
80 to,
81 icon,
82 end,
83 count,
84 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 drill,
The sidebar says where you are; the workspace page loses its tabs86 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell87 children,
88}: {
89 to: string;
90 icon: ReactNode;
91 end?: boolean;
92 count?: number;
Projects: what a workspace builds and runs, first on every page93 /** Other path prefixes under which this link is the current one. */
94 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look95 /** It opens a list of its own: a chevron says so, always or on hover. */
96 drill?: boolean | "hover";
The sidebar says where you are; the workspace page loses its tabs97 /** Whether it is the current row, when the list works that out itself. */
98 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell99 children: ReactNode;
100}) {
101 const { pathname } = useLocation();
102 return (
103 <NavLink
104 to={to}
105 end={end}
106 prefetch="intent"
107 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page108 const current =
The sidebar says where you are; the workspace page loses its tabs109 lit ??
110 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark111 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 shell112 current
113 ? "bg-raised font-medium text-fg"
114 : isPending
115 ? "bg-raised/60 text-fg"
116 : "text-muted hover:bg-raised/60 hover:text-fg"
117 }`;
118 }}
119 >
120 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
121 <span className="min-w-0 grow truncate">{children}</span>
122 {count != null && count > 0 && (
123 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
124 {count}
125 </span>
126 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look127 {drill && (
128 <ChevronRight
129 size={14}
130 aria-hidden="true"
131 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
132 />
133 )}
Agents as a team: lifecycle, merge queue, billing and a new shell134 </NavLink>
135 );
136}
137
Pull requests say where they are live; the project menu shows what is coming138/**
139 * Something a project will have, shown where it will live so people can
140 * see where g1t is going. Not a link: nothing is behind it yet.
141 */
142function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
143 return (
144 <div
145 title={about}
146 aria-disabled="true"
147 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
148 >
149 <span className="shrink-0 opacity-70">{icon}</span>
150 <span className="grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit151 <SoonPill />
Pull requests say where they are live; the project menu shows what is coming152 </div>
153 );
154}
155
The project menu opens by section, and the workspace has its own panel156/**
A flat project menu with Code first, views as tabs, and Stripe for every owner157 * A page that is coming, as a link to its roadmap page: faint, with Soon,
158 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel159 */
A flat project menu with Code first, views as tabs, and Stripe for every owner160function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel161 to,
162 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner163 icon,
164 about,
The sidebar says where you are; the workspace page loses its tabs165 current: lit,
The project menu opens by section, and the workspace has its own panel166 children,
167}: {
168 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner169 also?: string[];
170 icon: ReactNode;
171 about: string;
The sidebar says where you are; the workspace page loses its tabs172 /** Whether it is the current row, when the list works that out itself. */
173 current?: boolean;
The project menu opens by section, and the workspace has its own panel174 children: ReactNode;
175}) {
176 const { pathname } = useLocation();
177 return (
178 <NavLink
179 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner180 title={about}
The project menu opens by section, and the workspace has its own panel181 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner182 className={({ isActive }) => {
The sidebar says where you are; the workspace page loses its tabs183 const current = lit ?? (isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A flat project menu with Code first, views as tabs, and Stripe for every owner184 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
185 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 panel186 }`;
187 }}
188 >
A flat project menu with Code first, views as tabs, and Stripe for every owner189 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel190 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit191 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales192 </NavLink>
The project menu opens by section, and the workspace has its own panel193 );
194}
195
A flat project menu with Code first, views as tabs, and Stripe for every owner196/** Icons for what spans projects. */
197const WORKSPACE_ICONS: Record<string, ReactNode> = {
198 board: <KanbanSquare size={15} />,
199 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member200 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner201 packages: <Package size={15} />,
202 fleet: <Bot size={15} />,
203};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales204
A flat project menu with Code first, views as tabs, and Stripe for every owner205/** The Soon pages of a section, as paths, so its link is current on them. */
206function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
207 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales208}
209
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look210function SidebarGroup({
211 title,
212 action,
213 className = "",
214 children,
215}: {
216 title: string;
217 action?: ReactNode;
218 className?: string;
219 children: ReactNode;
220}) {
Agents as a team: lifecycle, merge queue, billing and a new shell221 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look222 <section className={className}>
A Vercel-like sidebar and a new mark223 <div className="mb-1 flex h-6 items-center justify-between px-2">
224 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell225 {action}
226 </div>
227 <div className="space-y-px">{children}</div>
228 </section>
229 );
230}
231
Workspace names and icons, and a component kit for every control232/** What a workspace is called where people read it: its display name, else its slug. */
233function displayName(membership: Membership): string {
234 return membership.name?.trim() || membership.slug;
235}
236
Agents as a team: lifecycle, merge queue, billing and a new shell237function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control238 const workspace = shell.workspace;
239 const current = workspace?.slug;
240 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell241 return (
242 <DropdownMenu>
Workspace names and icons, and a component kit for every control243 {/* The name goes to the workspace; only the arrows switch it. The
244 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark245 <Link
246 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents247 prefetch="intent"
Workspace names and icons, and a component kit for every control248 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look249 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 mark250 >
Workspace names and icons, and a component kit for every control251 {workspace ? (
252 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell253 ) : (
Workspace names and icons, and a component kit for every control254 <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 shell255 )}
Workspace names and icons, and a component kit for every control256 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark257 </Link>
258 <DropdownMenuTrigger
259 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit260 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 focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark261 >
262 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell263 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control264 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar265 {workspace && (
266 <>
267 {/* The workspace's own page: its projects, packages and people. */}
268 <DropdownMenuItem asChild>
269 <Link to={`/${workspace.slug}`}>
270 <LayoutGrid />
271 Workspace overview
272 </Link>
273 </DropdownMenuItem>
274 <DropdownMenuItem asChild>
275 <Link to={`/${workspace.slug}/-/projects`}>
276 <Box />
277 All projects
278 </Link>
279 </DropdownMenuItem>
280 <DropdownMenuSeparator />
281 </>
282 )}
Agents as a team: lifecycle, merge queue, billing and a new shell283 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
284 {(user.workspaces ?? []).map((membership) => (
285 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control286 <Link to={`/${membership.slug}`} title={displayName(membership)}>
287 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
288 <span className="flex min-w-0 grow flex-col leading-tight">
289 <span className="truncate">{displayName(membership)}</span>
290 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
291 </span>
292 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
293 {membership.slug === current ? (
294 <Check className="shrink-0 text-accent" />
295 ) : (
296 <span className="size-4 shrink-0" aria-hidden="true" />
297 )}
Agents as a team: lifecycle, merge queue, billing and a new shell298 </Link>
299 </DropdownMenuItem>
300 ))}
301 <DropdownMenuSeparator />
302 <DropdownMenuItem asChild>
303 <Link to="/workspaces/new">
304 <Plus />
305 New workspace
306 </Link>
307 </DropdownMenuItem>
308 </DropdownMenuContent>
309 </DropdownMenu>
A Vercel-like sidebar and a new mark310 );
311}
312
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas313/**
314 * The fine print, as one quiet row at the foot of the account menu and the
315 * visitor's panel: each link a full-height target, left-aligned. In the
316 * menu each is a menu item, so the arrow keys reach it.
317 */
318const LEGAL_LINKS: [string, string][] = [
319 ["Policies", "/policies"],
320 ["Privacy", "/policies/privacy"],
321 ["Security", "/security"],
322];
323const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
324
325function MenuLegalRow() {
326 return (
327 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
328 {LEGAL_LINKS.map(([label, to], index) => (
329 <span key={to} className="flex items-center gap-0.5">
330 {index > 0 && (
331 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
332 ·
333 </span>
334 )}
335 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
336 <Link to={to}>{label}</Link>
337 </DropdownMenuItem>
338 </span>
339 ))}
340 </div>
341 );
342}
343
344/** Status's row: the live dot and two or three words, fetched when the menu opens. */
345function StatusSummary({ open }: { open: boolean }) {
346 const status = useSiteStatus(open);
347 const words = statusTitle(status);
348 return (
349 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
350 {/* The words say it; the dot's own words for screen readers would repeat them. */}
351 <span aria-hidden="true" className="flex">
352 <StatusDot state={status?.overall.state ?? null} />
353 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents354 {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 quotas355 </span>
356 );
357}
358
359/** A menu row: an icon, words, and whatever sits at its end. */
360const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
361
A Vercel-like sidebar and a new mark362function AccountMenu({ user }: { user: User }) {
363 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas364 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents365 // Name, primary address and invites left: asked for once, as soon as the
366 // 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 quotas367 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents368 const prefetch = () => {
369 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
370 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas371 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents372 if (open) prefetch();
373 // eslint-disable-next-line react-hooks/exhaustive-deps
374 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas375 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents376 // Still on its way: shapes where the words will be, the same size.
377 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 quotas378 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark379 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas380 <DropdownMenu open={open} onOpenChange={setOpen}>
381 <DropdownMenuTrigger
382 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents383 onPointerEnter={prefetch}
384 onFocus={prefetch}
Lavender is the accent; mint means success385 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-accent/60 data-[state=open]:bg-raised"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas386 >
Workspace names and icons, and a component kit for every control387 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark388 <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 quotas389 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark390 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas391 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
392 {/* Who is signed in, and a way to their profile. */}
393 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
394 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
395 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
397 {loading ? (
398 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
399 ) : (
400 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
401 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas402 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents403 {loading ? (
404 <Skeleton className="mt-1 h-3 w-40" />
405 ) : (
406 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
407 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas408 </span>
A Vercel-like sidebar and a new mark409 </Link>
410 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas411 <DropdownMenuSeparator className="my-1.5" />
412 <DropdownMenuGroup>
413 <DropdownMenuItem asChild className={MENU_ROW}>
414 <Link to={profile}>
415 <CircleUserRound />
416 Your profile
417 </Link>
418 </DropdownMenuItem>
419 <DropdownMenuItem asChild className={MENU_ROW}>
420 <Link to={FIRST_SETTINGS_PAGE}>
421 <Settings />
422 Your settings
423 </Link>
424 </DropdownMenuItem>
425 <DropdownMenuItem asChild className={MENU_ROW}>
426 <Link to="/settings/invites">
427 <Ticket />
428 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents429 {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 quotas430 {me?.invites_left != null && (
431 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
432 {me.invites_left} left
433 </span>
434 )}
435 </Link>
436 </DropdownMenuItem>
437 </DropdownMenuGroup>
438 <DropdownMenuSeparator className="my-1.5" />
439 <DropdownMenuGroup>
440 <DropdownMenuItem asChild className={MENU_ROW}>
441 <a href="https://docs.g1t.sh/">
442 <BookOpen />
443 Documentation
444 </a>
445 </DropdownMenuItem>
446 <DropdownMenuItem asChild className={MENU_ROW}>
447 <Link to="/support">
448 <LifeBuoy />
449 Support
450 </Link>
451 </DropdownMenuItem>
452 <DropdownMenuItem asChild className={MENU_ROW}>
453 <a href={STATUS_URL}>
454 <Activity />
455 Status
456 <StatusSummary open={open} />
457 </a>
458 </DropdownMenuItem>
459 </DropdownMenuGroup>
460 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark461 {/* Submitted from here: the menu closes on select, and a button
462 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 quotas463 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark464 <LogOut />
465 Sign out
466 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas467 <DropdownMenuSeparator className="my-1.5" />
468 <MenuLegalRow />
A Vercel-like sidebar and a new mark469 </DropdownMenuContent>
470 </DropdownMenu>
471 );
472}
473
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look474/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar475 * A workspace's settings pages, which the sidebar drills into: how it is
476 * set up and connected (guardrails, secrets, runners, integrations,
477 * webhooks), what it pays, its repositories, tokens and record. The main
478 * list keeps the places work happens and who belongs; every member can
479 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look480 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar481const SETTINGS_PAGE =
The sidebar says where you are; the workspace page loses its tabs482 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look483/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page484const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings485
Polish: phones, copy boxes, the plan page, the landing page, a real glide486/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look487 * The sidebar's lists are a stack, the way a phone pushes a screen: one
488 * drilled into slides in from the right over the full width, while the one
489 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide490 * ease-out, the fade quicker than the move so the two never blur together.
491 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide492 */
Sidebar: no settings flash on the way into a repository, and a better slide493const LAYER =
Sidebar: the panels really slide494 "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 look495/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide496const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings497
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look498/** One list in the stack: what it is, and its rows. */
499type Level = { key: string; node: ReactNode };
500
501/** True once the page has hydrated, so what the server drew never animates in. */
502function useHydrated(): boolean {
503 const [hydrated, setHydrated] = useState(false);
504 useEffect(() => setHydrated(true), []);
505 return hydrated;
506}
507
508/**
509 * Drilling in and out of the sidebar's lists. `trail` is the way from the
510 * main list to the one shown, worked out from the address, so a link
511 * straight to a settings page opens the sidebar already drilled in, and
512 * leaving it comes back out. Lists deeper than the one shown stay in
513 * place, off to the right, so going back slides them away rather than
514 * dropping them. Moving to a list at the same depth (a workspace's settings
515 * to a project) fades the new one in where it is.
516 */
517function Drill({ trail }: { trail: Level[] }) {
518 const depth = trail.length - 1;
519 const hydrated = useHydrated();
520 const stack = useRef<Level[]>(trail);
521 const before = stack.current;
522 const within = trail.every((level, index) => before[index]?.key === level.key);
523 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
524 const levels = stack.current;
525 // What was showing last time, to tell drilling in from moving across.
526 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
527 useEffect(() => {
528 shown.current = { depth, keys: levels.map((level) => level.key) };
529 });
A catalogue of model providers, and settings that feel like settings530 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look531 <div className="relative min-h-0 grow overflow-hidden">
532 {levels.map((level, index) => {
533 const current = index === depth;
534 const place =
535 index < depth
536 ? "pointer-events-none -translate-x-1/4 opacity-0"
537 : current
538 ? "translate-x-0 opacity-100"
539 : "pointer-events-none translate-x-full opacity-0";
540 // A list that was not there before arrives: from the right when
541 // drilling in, in place when moving across.
542 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
543 const enter = arriving
544 ? depth > shown.current.depth
545 ? "starting:translate-x-full starting:opacity-0"
546 : "starting:opacity-0"
547 : "";
548 return (
549 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
550 {level.node}
551 </div>
552 );
553 })}
554 </div>
555 );
556}
557
558/**
559 * The row at the top of a list drilled into: the list's name, which goes
560 * back out, and what it belongs to, faint at the end.
561 */
562function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
563 return (
564 <Link
565 to={to}
566 prefetch="intent"
567 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 settings568 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look569 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
570 <span className="min-w-0 shrink-0 truncate">{label}</span>
571 {context && (
572 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
573 )}
574 </Link>
575 );
576}
577
578/** A thin rule between groups of a list. */
579function Rule() {
580 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
581}
582
Navigation: the workspace's own page with tabs, pins and recent in the sidebar583/** A quiet heading inside a group: Pinned, Recent. */
584function SidebarSubhead({ children }: { children: ReactNode }) {
585 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
586}
587
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look588/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar589 * The workspace's projects as the sidebar keeps them, however many there
590 * are: the person's pins in their order, what they opened last, and the
591 * way to all of them. Pins move by dragging, or with Alt and the arrow
592 * keys; the projects service keeps the order.
593 */
The sidebar says where you are; the workspace page loses its tabs594function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar595 const reorder = useFetcher({ key: `pins:${slug}` });
596 const saved = shell.pinned ?? [];
597 // While a new order is on its way, it shows as made.
598 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
599 const pinned = asked
600 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
601 : saved;
602 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
603 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
604 const [dragging, setDragging] = useState<number | null>(null);
605 const move = (from: number, by: number) => {
606 const next = movedPin(pinned, from, by);
607 if (!next) return;
608 const form = new FormData();
609 form.set("intent", "reorder");
610 for (const project of next) form.append("slug", project.name);
611 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
612 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar613 // Each row pins or unpins in place: the pin shows on hover or focus, and
614 // stays shown on a pinned row's hover so it reads as "unpin".
615 const row = (project: ShortcutProject, isPinned: boolean) => (
616 <div className="group/row relative">
617 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
618 <span className="block truncate pr-6">{project.title ?? project.name}</span>
619 </SidebarLink>
620 <PinButton
621 workspace={slug}
622 slug={project.name}
623 name={project.title ?? project.name}
624 pinned={isPinned}
625 small
626 className="absolute top-1/2 right-6 -translate-y-1/2 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
627 />
628 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar629 );
630 return (
631 <SidebarGroup
632 title="Projects"
633 action={
634 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
635 <Plus size={13} />
636 </Link>
637 }
638 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar639 {/* Only there once something is pinned: each row below has its own pin. */}
640 {pinned.length > 0 && (
641 <>
642 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar643 <ul aria-label="Pinned projects" className="space-y-px">
644 {pinned.map((project, index) => (
645 <li
646 key={project.name}
647 draggable={pinned.length > 1}
648 onDragStart={(event) => {
649 setDragging(index);
650 event.dataTransfer.effectAllowed = "move";
651 }}
652 onDragOver={(event) => {
653 if (dragging != null) event.preventDefault();
654 }}
655 onDrop={(event) => {
656 event.preventDefault();
657 if (dragging != null && dragging !== index) move(dragging, index - dragging);
658 setDragging(null);
659 }}
660 onDragEnd={() => setDragging(null)}
661 onKeyDown={(event) => {
662 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
663 event.preventDefault();
664 move(index, event.key === "ArrowUp" ? -1 : 1);
665 }}
666 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
667 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
668 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar669 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar670 {pinned.length > 1 && (
671 <GripVertical
672 size={12}
673 aria-hidden="true"
674 className="pointer-events-none absolute top-1/2 -left-1.5 -translate-y-1/2 text-faint opacity-0 transition-opacity group-hover/pin:opacity-100"
675 />
676 )}
677 </li>
678 ))}
679 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar680 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar681 )}
682 {recent.length > 0 && (
683 <>
684 <SidebarSubhead>Recent</SidebarSubhead>
685 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar686 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar687 ))}
688 </>
689 )}
690 <div className="pt-1">
The sidebar says where you are; the workspace page loses its tabs691 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length} current={current}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar692 All projects
693 </SidebarLink>
694 </div>
695 </SidebarGroup>
696 );
697}
698
699/**
The sidebar says where you are; the workspace page loses its tabs700 * The main list, in two parts a rule apart. Above it, what is yours
701 * whichever workspace you are in: Mission control, your inbox, and what
702 * others have shared with you. Below it, under the workspace's name, the
703 * workspace: its overview, its projects, what it builds and runs with
704 * across them, then its people, usage, support and settings. One row is
705 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look706 */
707function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
708 const ws = shell.workspace;
The sidebar says where you are; the workspace page loses its tabs709 const { pathname } = useLocation();
710 const going = useNavigation().location?.pathname;
711 // The row lights as the link is followed, not once the page arrives.
712 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look713 // A visitor browses: no workspace, no projects of their own.
714 if (!user) {
715 return (
716 <nav aria-label="g1t" className={PANEL}>
717 <div className="mt-3 space-y-px">
718 {VISITOR_LINKS.map((link) => (
719 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
720 {link.label}
721 </SidebarLink>
722 ))}
723 </div>
724 </nav>
725 );
726 }
The sidebar says where you are; the workspace page loses its tabs727 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look728 return (
729 <nav aria-label="g1t" className={PANEL}>
The sidebar says where you are; the workspace page loses its tabs730 {/* Yours, in every workspace. Explore, all of g1t, is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look731 <div className="mt-3 space-y-px">
The sidebar says where you are; the workspace page loses its tabs732 <SidebarLink to="/" end icon={<House size={15} />} current={here === "mission"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look733 Mission control
734 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs735 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
736 Inbox
737 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look738 </div>
The sidebar says where you are; the workspace page loses its tabs739 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look740 <SidebarGroup title="Shared with you" className="mt-3">
The sidebar says where you are; the workspace page loses its tabs741 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look742 <SidebarLink
743 key={`${repo.namespace}/${repo.name}`}
744 to={`/${repo.namespace}/${repo.name}`}
745 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
746 drill="hover"
747 >
748 <span className="font-mono text-faint">{repo.namespace}/</span>
749 {repo.name}
750 </SidebarLink>
751 ))}
752 </SidebarGroup>
753 )}
754
The sidebar says where you are; the workspace page loses its tabs755 <Rule />
756 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look757 <>
The sidebar says where you are; the workspace page loses its tabs758 {/* The workspace the switcher names: its own page first. */}
759 <SidebarGroup title={displayName(ws)}>
760 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
761 Overview
762 </SidebarLink>
763 </SidebarGroup>
764 <div className="mt-3">
765 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
766 </div>
767
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member768 <Rule />
The sidebar says where you are; the workspace page loses its tabs769 <div className="space-y-px">
770 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />} current={here === "agents"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look771 Agent fleet
772 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs773 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />} current={here === "context"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look774 Context
775 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs776 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />} current={here === "memory"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look777 Memory
778 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs779 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />} current={here === "security"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look780 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents781 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs782 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />} current={here === "packages"}>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member783 Packages
784 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs785 {INSIGHTS && (
786 <SidebarSoonLink
787 to={`/${ws.slug}/-/insights`}
788 icon={<TrendingUp size={15} />}
789 about={INSIGHTS.summary}
790 current={here === "insights"}
791 >
792 {INSIGHTS.title}
793 </SidebarSoonLink>
794 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar795 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look796 <SidebarSoonLink
797 key={item.key}
798 to={`/${ws.slug}/-/soon/${item.key}`}
799 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
800 about={item.summary}
801 >
802 {item.title === "Board" ? "Boards" : item.title}
803 </SidebarSoonLink>
804 ))}
The sidebar says where you are; the workspace page loses its tabs805 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look806
807 <Rule />
808 <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 bar809 {/* Who belongs, for every member to see; owners invite and manage there. */}
The sidebar says where you are; the workspace page loses its tabs810 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />} current={here === "people"}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar811 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar812 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs813 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams} current={here === "teams"}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar814 Teams
815 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs816 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />} current={here === "usage"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look817 Usage
818 </SidebarLink>
The sidebar says where you are; the workspace page loses its tabs819 <SidebarLink to="/support" icon={<LifeBuoy size={15} />} current={here === "support"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look820 Support
821 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar822 {/* 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 look823 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member824 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 look825 icon={<Settings size={15} />}
826 drill
The sidebar says where you are; the workspace page loses its tabs827 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look828 >
829 Settings
830 </SidebarLink>
831 </div>
832 </>
The sidebar says where you are; the workspace page loses its tabs833 ) : (
834 <SidebarGroup title="Projects">
835 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
836 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look837 )}
838 </nav>
839 );
840}
841
The sidebar says where you are; the workspace page loses its tabs842/** Insights, coming: the workspace's own page says what it will be. */
843const INSIGHTS = roadmapItem("insights");
844
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look845/** A workspace's settings, drilled into from Settings in the main list. */
846function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
847 return (
848 <nav aria-label="Workspace settings" className={PANEL}>
849 <BackRow to={`/${slug}`} label="Settings" context={slug} />
850 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings851 {owner && (
852 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
853 General
854 </SidebarLink>
855 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look856 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
857 Repositories
858 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments859 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
860 Access tokens
861 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look862 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar863 <SidebarGroup title="Agents and runs" className="mt-3">
864 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
865 Guardrails
866 </SidebarLink>
867 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
868 Secrets and variables
869 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it870 {owner && (
871 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
872 Runners
873 </SidebarLink>
874 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar875 </SidebarGroup>
876 <SidebarGroup title="Connections" className="mt-3">
877 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
878 Integrations
879 </SidebarLink>
880 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
881 Webhooks
882 </SidebarLink>
883 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look884 <Rule />
885 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings886 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments887 Billing and plans
A catalogue of model providers, and settings that feel like settings888 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API889 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
890 Audit log
891 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look892 </div>
A catalogue of model providers, and settings that feel like settings893 </nav>
894 );
895}
896
Sidebar: no settings flash on the way into a repository, and a better slide897/**
898 * What the repository menu needs. Known from the address before the page's
899 * data arrives, so the menu is right from the first frame; the counts fill
900 * in once it does.
901 */
902type MenuRepo = {
903 namespace: string;
904 name: string;
905 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look906 /** Whether they see its settings; from the repository's page once it loads. */
907 settings?: boolean;
908 /** What they may do there, once the repository's page loads. */
909 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide910 issues?: number;
911 pulls?: number;
912};
913
914function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
915 return (
916 a != null &&
917 b != null &&
918 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
919 a.name.toLowerCase() === b.name.toLowerCase()
920 );
921}
A repository has its own sidebar, as settings do922
923/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look924 * A project's own list, drilled into while you are in it: everything about
925 * the project, running and its code, and nothing else, with the way back
926 * out to all of them.
A repository has its own sidebar, as settings do927 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put928function RepoMenu({
929 repo,
930 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look931 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put932}: {
933 repo: MenuRepo;
934 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look935 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
936 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put937}) {
A repository has its own sidebar, as settings do938 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 put939 // 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 look940 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do941 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look942 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
943 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner944 <NavLink
A repository has its own sidebar, as settings do945 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner946 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents947 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner948 title="Overview"
949 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look950 `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 owner951 }
A repository has its own sidebar, as settings do952 >
953 <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 page954 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do955 </span>
956 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
957 <span className="text-faint">{repo.namespace}/</span>
958 <span className="font-semibold text-fg">{repo.name}</span>
959 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner960 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look961 {/* A project's pages, in the order people use them. A page with more
962 than one view shows them as tabs across its top. */}
963 <Rule />
964 <div className="space-y-px">
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request965 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, `${base}/branches`, `${base}/tags`, `${base}/compare`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
A flat project menu with Code first, views as tabs, and Stripe for every owner966 Code
967 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit968 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, `${base}/milestones`, `${base}/labels`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
A flat project menu with Code first, views as tabs, and Stripe for every owner969 Issues
970 </SidebarLink>
971 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
972 Pull requests
973 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains974 <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 owner975 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains976 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner977 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
978 Workflows
Projects: what a workspace builds and runs, first on every page979 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look980 </div>
981 <Rule />
982 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put983 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner984 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
985 Deployments
986 </SidebarLink>
987 ) : null}
Observability gets its own page; tabs never overflow988 <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.">
989 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner990 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put991 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API992 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
993 Security
994 </SidebarLink>
995 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner996 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
997 Insights
998 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look999 </div>
1000 {(repo.settings ?? shows.has("settings")) && (
1001 <>
1002 <Rule />
1003 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1004 Settings
1005 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1006 </>
1007 )}
A repository has its own sidebar, as settings do1008 </nav>
1009 );
1010}
1011
Project settings move into the sidebar, grouped, with a heading per page1012/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1013 * A project's settings, drilled into from its list: what is about running
1014 * it, then its agents, then its code and who can reach it. The way back
1015 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1016 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1017function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1018 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1019 // Each page shows to the roles that can use it, once the role is known.
1020 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1021 const running = shows("deployments");
1022 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1023 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1024 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1025 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1026 {shows("") && (
1027 <div className="mt-2 space-y-px">
1028 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1029 General
1030 </SidebarLink>
1031 </div>
1032 )}
1033 {running && (
1034 <>
1035 {shows("") && <Rule />}
1036 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1037 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1038 Deployments
1039 </SidebarLink>
1040 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1041 Domains
1042 </SidebarLink>
1043 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1044 Dependencies
1045 </SidebarLink>
1046 </div>
1047 </>
1048 )}
1049 {agents && (
1050 <>
1051 <Rule />
1052 <div className="space-y-px">
1053 {shows("agents") && (
1054 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1055 Agents
1056 </SidebarLink>
1057 )}
1058 {shows("guardrails") && (
1059 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1060 Guardrails
1061 </SidebarLink>
1062 )}
1063 </div>
1064 </>
1065 )}
1066 <Rule />
1067 <div className="space-y-px">
1068 {shows("repository") && (
1069 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1070 Repository
1071 </SidebarLink>
1072 )}
1073 {shows("access") && (
1074 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1075 Access
1076 </SidebarLink>
1077 )}
1078 {shows("branches") && (
1079 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1080 Branches and merging
1081 </SidebarLink>
1082 )}
1083 {shows("secrets") && (
1084 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1085 Secrets and variables
1086 </SidebarLink>
1087 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1088 {shows("runners") && (
1089 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1090 Runners
1091 </SidebarLink>
1092 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1093 {shows("webhooks") && (
1094 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1095 Webhooks
1096 </SidebarLink>
1097 )}
1098 </div>
Project settings move into the sidebar, grouped, with a heading per page1099 </nav>
1100 );
1101}
1102
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1103/** Your own settings, drilled into from Your settings: one page each. */
1104function AccountSettingsMenu({ username }: { username: string }) {
1105 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1106 <SidebarLink to={`/settings/${page}`} icon={icon}>
1107 {ACCOUNT_SETTINGS[page].title}
1108 </SidebarLink>
Account settings in the same shape as a workspace's1109 );
1110 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1111 <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 quotas1112 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1113 <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 quotas1114 {link("profile", <CircleUserRound size={15} />)}
1115 {link("emails", <Mail size={15} />)}
1116 {link("invites", <Ticket size={15} />)}
1117 </div>
1118 <Rule />
1119 <div className="space-y-px">
1120 {link("keys", <Fingerprint size={15} />)}
1121 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1122 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1123 <Rule />
1124 <div className="space-y-px">
1125 {link("github", <GithubMark className="size-[15px]" />)}
1126 {link("applications", <Plug size={15} />)}
1127 </div>
1128 <Rule />
1129 <div className="space-y-px">
1130 {link("security-log", <History size={15} />)}
1131 </div>
Account settings in the same shape as a workspace's1132 </nav>
1133 );
1134}
1135
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1136/**
1137 * Signing in and signing up, in place of the account for a visitor. Signing
1138 * in brings them back to the page they are on.
1139 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1140/** Help, status and the fine print under a visitor's sign-in buttons. */
1141function VisitorLinks() {
1142 const status = useSiteStatus();
Lavender is the accent; mint means success1143 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/60`;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1144 const dot = (
1145 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1146 ·
1147 </span>
1148 );
1149 return (
1150 // Two short rows, so a narrow sidebar never starts a line with a dot.
1151 <nav aria-label="About g1t" className="pt-0.5">
1152 <div className="flex items-center gap-0.5">
1153 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1154 <StatusDot state={status?.overall.state ?? null} />
1155 Status
1156 </a>
1157 {dot}
1158 <Link to="/support" className={quiet}>
1159 Support
1160 </Link>
1161 </div>
1162 <div className="flex items-center gap-0.5">
1163 {LEGAL_LINKS.map(([label, to], index) => (
1164 <span key={to} className="flex items-center gap-0.5">
1165 {index > 0 && dot}
1166 <Link to={to} className={quiet}>
1167 {label}
1168 </Link>
1169 </span>
1170 ))}
1171 </div>
1172 </nav>
1173 );
1174}
1175
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1176function VisitorPanel() {
1177 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1178 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1179 return (
1180 <div className="space-y-2">
1181 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1182 <div className="grid grid-cols-2 gap-2">
1183 <Link
1184 to={withNext("/login", pathname + search)}
1185 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"
1186 >
1187 <LogIn size={14} />
1188 Sign in
1189 </Link>
1190 <Link
1191 to={withNext("/register", pathname + search)}
1192 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1193 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1194 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1195 </Link>
1196 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1197 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1198 </div>
1199 );
1200}
1201
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1202function Sidebar({
1203 user,
1204 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1205 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1206 onFind,
1207 onClose,
1208}: {
1209 user: User | null;
1210 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1211 /** The page is a 404: the address names nothing the viewer can see. */
1212 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1213 onFind: () => void;
1214 /** In the sheet on a small screen: closing it, at the end of the top row. */
1215 onClose?: () => void;
1216}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1217 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1218 const { pathname } = useLocation();
1219 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1220 // Where the sidebar is drilled to follows the page being gone to, so it
1221 // moves as the link is followed, not once the page arrives.
1222 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1223 // On a 404, the address is not a place: no list is drilled into from it,
1224 // except the project's own when the project is real and visible and only
1225 // something inside it is missing (a file, a commit, an issue).
1226 const lost = missing && going == null;
1227 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1228 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1229 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1230 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1231 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1232 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", "inbox", ".well-known"]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1233 // An invitation to a repository is answered before its menu means anything.
1234 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1235 repoPath != null &&
1236 !reserved.has(repoPath[1]) &&
1237 repoPath[2] !== "-" &&
1238 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1239 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1240 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1241 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1242 // The repository the menu is for: the one loaded if it is the one being
1243 // gone to, else what the address says, at once.
1244 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 look1245 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1246 ? sameRepo(active, targetRepo)
1247 ? active
1248 : {
1249 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1250 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1251 }
1252 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1253 // The viewer's role there, from the repository's page once it has loaded.
1254 const page = useRouteLoaderData("routes/repo/layout") as
1255 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1256 | undefined;
1257 const menuRepo: MenuRepo | null =
1258 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1259 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1260 : guessed;
1261
1262 // The way from the main list to the one shown.
1263 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1264 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1265 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1266 } else if (ws && inSettings) {
1267 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1268 } else if (menuRepo) {
1269 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1270 // Out of a project: to its workspace's projects when they are yours.
1271 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1272 const back = !user
1273 ? { to: "/explore", label: "Explore" }
1274 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1275 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1276 : { to: "/", label: "Mission control" };
1277 trail.push({
1278 key,
1279 node: (
1280 <RepoMenu
1281 repo={menuRepo}
1282 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1283 back={back}
1284 />
1285 ),
1286 });
1287 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1288 }
1289
Agents as a team: lifecycle, merge queue, billing and a new shell1290 return (
1291 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1292 {/* 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 bar1293 <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 put1294 {user ? (
1295 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1296 {/* The 1 alone beside the workspace: a square hover the height of the
1297 switcher, the mark as tall as the workspace avatar. */}
1298 <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 bar1299 <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 put1300 </Link>
1301 <span className="shrink-0 text-line-strong" aria-hidden="true">
1302 /
1303 </span>
1304 <WorkspaceSwitcher user={user} shell={shell} />
1305 </>
1306 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1307 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1308 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1309 </Link>
1310 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1311 {onClose && (
1312 <button
1313 type="button"
1314 aria-label="Close menu"
1315 onClick={onClose}
1316 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1317 >
1318 <X size={16} />
1319 </button>
1320 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1321 </div>
A Vercel-like sidebar and a new mark1322 <div className="px-2 pt-3">
1323 <button
1324 type="button"
1325 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1326 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 mark1327 >
1328 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1329 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1330 <PaletteKey className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" />
A Vercel-like sidebar and a new mark1331 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1332 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1333 <Drill trail={trail} />
1334 {/* The account, or signing in, as one row at the very bottom. */}
1335 <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 put1336 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1337 </div>
1338 </div>
1339 );
1340}
1341
1342/** Words for the sections a path can end in. */
1343const SECTIONS: Record<string, string> = {
1344 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1345 milestones: "Milestones",
1346 labels: "Labels",
A Vercel-like sidebar and a new mark1347 pulls: "Pull requests",
1348 queue: "Merge queue",
1349 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1350 plans: "Outcomes",
1351 actions: "Workflows",
1352 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1353 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1354 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1355 access: "Access",
1356 invitations: "Invitation",
1357 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1358 branches: "Branches",
1359 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1360 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1361 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1362 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1363 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1364 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1365 people: "People",
1366 projects: "Projects",
1367 teams: "Teams",
A Vercel-like sidebar and a new mark1368 tokens: "Access tokens",
1369 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1370 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1371 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1372 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1373 domains: "Domains",
1374 guardrails: "Guardrails",
1375 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1376 tree: "Files",
1377 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1378 agents: "Agents",
1379 context: "Context",
1380 memory: "Memory",
1381 security: "Security",
1382 packages: "Packages",
1383 runners: "Runners",
1384 workflows: "Workflows",
1385 observability: "Observability",
1386 insights: "Insights",
1387 sessions: "Sessions",
A Vercel-like sidebar and a new mark1388};
1389
1390/** 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 quotas1391function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1392 // A 404 names nothing from the address, unless the project is real and
1393 // visible and only something inside it is missing.
1394 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1395 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1396 return <span className="text-sm font-medium">Not found</span>;
1397 }
A Vercel-like sidebar and a new mark1398 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1399 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1400 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 quotas1401 // Your settings: Settings / Emails.
1402 if (parts[0] === "settings") {
1403 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1404 return <Trail trail={[{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])]} />;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1405 }
A Vercel-like sidebar and a new mark1406 if (reserved.includes(parts[0]!)) {
1407 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1408 settings: "Account",
A Vercel-like sidebar and a new mark1409 explore: "Explore",
Projects live: fixes from walking them end to end1410 new: "New project",
A Vercel-like sidebar and a new mark1411 search: "Search",
1412 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1413 policies: "Policies",
1414 security: "Security",
1415 support: "Support",
1416 status: "Status",
1417 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1418 inbox: "Inbox",
A Vercel-like sidebar and a new mark1419 };
1420 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1421 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1422 // A person's profile, by their handle.
1423 if (parts[0] === "u" && parts[1]) {
1424 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1425 }
A Vercel-like sidebar and a new mark1426 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1427 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1428 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1429 const page = `/${owner}/-/${third}`;
1430 if (third && SETTINGS_PAGE.test(page)) {
1431 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1432 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1433 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1434 } else if (second) {
1435 const repo = `/${owner}/${second}`;
1436 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1437 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1438 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, short: `#${fourth}`, to: `${repo}/issues/${fourth}` });
A Vercel-like sidebar and a new mark1439 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 deployments1440 else if (third && SECTIONS[third]) {
1441 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1442 // A settings page names which one: Settings / Secrets and variables.
1443 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1444 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 deployments1445 }
1446 }
A Vercel-like sidebar and a new mark1447 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1448 return <Trail trail={trail} />;
1449}
1450
1451/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1452type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1453
1454/**
1455 * The trail of links. On a phone there is room for one: the page itself,
1456 * with a way back to where it sits; the whole trail from a wider screen up.
1457 */
1458function Trail({ trail }: { trail: Crumb[] }) {
1459 const up = trail.length > 1 ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1460 return (
1461 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1462 {up && (
1463 <Link
1464 to={up.to}
1465 prefetch="intent"
1466 aria-label={`Back to ${up.label}`}
1467 className="-ml-1 flex size-8 shrink-0 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg sm:hidden"
1468 >
1469 <ChevronLeft size={16} />
1470 </Link>
1471 )}
1472 {trail.map((crumb, index) => {
1473 const last = index === trail.length - 1;
1474 return (
1475 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1476 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1477 <Link
1478 to={crumb.to}
1479 prefetch="intent"
1480 aria-current={last ? "page" : undefined}
1481 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1482 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1483 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1484 >
1485 {last && crumb.short ? (
1486 <>
1487 <span className="sm:hidden">{crumb.short}</span>
1488 <span className="hidden sm:inline">{crumb.label}</span>
1489 </>
1490 ) : (
1491 crumb.label
1492 )}
1493 </Link>
1494 </span>
1495 );
1496 })}
A Vercel-like sidebar and a new mark1497 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1498 );
1499}
1500
Search across all of g1t, Explore, and a command palette1501type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1502
1503/** 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 look1504function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1505 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1506 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1507 { 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 step1508 ...(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-agent1509 ? [{ 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 step1510 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1511 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1512 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1513 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1514 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1515 { 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 quotas1516 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1517 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1518 .filter((page) => page !== "profile")
1519 .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 shell1520 ];
1521 const repo = shell.repo;
1522 if (repo) {
1523 const base = `/${repo.namespace}/${repo.name}`;
1524 const name = `${repo.namespace}/${repo.name}`;
1525 commands.unshift(
Projects: what a workspace builds and runs, first on every page1526 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1527 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1528 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1529 { 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 step1530 ...(repo.member
1531 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1532 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1533 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1534 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1535 ...(repo.member
1536 ? [
1537 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1538 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1539 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1540 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1541 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1542 ]
1543 : []),
1544 );
1545 }
1546 for (const membership of user.workspaces ?? []) {
1547 commands.push(
Workspace names and icons, and a component kit for every control1548 { label: displayName(membership), hint: `Workspace · ${membership.slug}`, to: `/${membership.slug}`, icon: <Avatar name={membership.slug} image={membership.avatar} size={15} square /> },
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1549 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1550 { label: "People", hint: membership.slug, to: `/${membership.slug}/-/people`, icon: <Users size={15} /> },
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1551 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1552 { 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 settings1553 { 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 look1554 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1555 );
1556 }
1557 for (const listed of shell.repos) {
1558 commands.push({
Projects: what a workspace builds and runs, first on every page1559 label: listed.title ?? listed.name,
1560 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1561 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1562 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1563 });
1564 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1565 // "Put an agent on …": one per project, after the projects themselves.
1566 for (const listed of shell.repos) {
1567 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1568 commands.push({
1569 label: `Put an agent on ${listed.title ?? listed.name}`,
1570 hint: `${listed.namespace}/${listed.name}`,
1571 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1572 icon: <Sparkles size={15} />,
1573 });
1574 }
Agents as a team: lifecycle, merge queue, billing and a new shell1575 return commands;
1576}
1577
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1578/** 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 look1579function 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 put1580 const commands: Command[] = [];
1581 const repo = shell.repo;
1582 if (repo) {
1583 const base = `/${repo.namespace}/${repo.name}`;
1584 const name = `${repo.namespace}/${repo.name}`;
1585 commands.push(
1586 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1587 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1588 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1589 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1590 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1591 );
1592 }
1593 commands.push(
1594 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1595 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1596 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1597 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1598 { 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 look1599 { 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 put1600 );
1601 return commands;
1602}
1603
Agents as a team: lifecycle, merge queue, billing and a new shell1604/**
Merge branch 'worktree-agent-aca4790ea223aa235'1605 * The page while the next one loads: after a moment it dims, under the
1606 * bar below, so it is plainly on its way out rather than frozen, and the
1607 * next page fades up from there. Sending a form does not dim it: the
1608 * form's button says it is working. Spread on the element holding the page.
1609 */
1610export function useLeaving() {
1611 const navigation = useNavigation();
1612 const method = navigation.formMethod?.toUpperCase();
1613 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1614 return {
1615 "aria-busy": leaving || undefined,
1616 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1617 };
1618}
1619
1620/**
Agents as a team: lifecycle, merge queue, billing and a new shell1621 * A bar across the top of the page while the next one loads, as GitHub
1622 * has: it appears at once, creeps towards the end while waiting, then fills
1623 * and fades when the page arrives.
1624 */
1625export function Progress() {
1626 const navigation = useNavigation();
1627 const busy = navigation.state !== "idle";
1628 const [width, setWidth] = useState(0);
1629 const [visible, setVisible] = useState(false);
1630 useEffect(() => {
1631 if (busy) {
1632 setVisible(true);
1633 setWidth(12);
1634 // Slows as it goes, never quite arriving until the page does.
1635 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1636 return () => clearInterval(timer);
1637 }
1638 setWidth((now) => (now > 0 ? 100 : 0));
1639 const fade = setTimeout(() => setVisible(false), 250);
1640 const reset = setTimeout(() => setWidth(0), 550);
1641 return () => {
1642 clearTimeout(fade);
1643 clearTimeout(reset);
1644 };
1645 }, [busy]);
1646 return (
1647 <div
1648 aria-hidden="true"
1649 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1650 visible ? "opacity-100" : "opacity-0"
1651 }`}
1652 >
1653 <div
1654 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1655 style={{ width: `${width}%` }}
1656 />
1657 </div>
1658 );
1659}
1660
1661/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1662 * The app: a sidebar with the workspace, its repositories and the sections
1663 * of the one being looked at; a slim bar with search and the account; and
1664 * the page. A visitor who is not signed in gets the same frame, with
1665 * Explore and Search in place of the workspace, and signing in in place of
1666 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1667 */
1668export function AppShell({
1669 user,
1670 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1671 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1672 banner,
1673 children,
1674}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1675 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1676 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1677 /** The page is a 404 (see Sidebar). */
1678 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1679 banner?: ReactNode;
1680 children: ReactNode;
1681}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1682 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1683 const [drawer, setDrawer] = useState(false);
1684 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1685 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1686 const signUpLabel = useSignUpCopy().primary;
1687 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1688
1689 // A new page closes the drawer on small screens.
1690 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1691 // Mission control's code, fetched while the browser is idle, so going
1692 // home never waits on it (routes/home.tsx).
1693 useEffect(() => {
1694 if (!user) return;
1695 const load = () => void import("./mission-control").catch(() => undefined);
1696 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1697 if (idle) idle(load);
1698 else setTimeout(load, 1500);
1699 }, [user]);
Search across all of g1t, Explore, and a command palette1700 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1701 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1702
1703 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1704 // The phone's menu is a sheet: a dialog that holds focus, closes on
1705 // Escape or a tap outside, and gives focus back to the menu button.
1706 <Sheet open={drawer} onOpenChange={setDrawer}>
Agents as a team: lifecycle, merge queue, billing and a new shell1707 <div className="min-h-screen">
1708 <Progress />
1709 <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 quotas1710 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1711 </aside>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1712 <SheetContent side="left" showClose={false} aria-describedby={undefined} className="w-72 max-w-[85vw] sm:max-w-72 lg:hidden">
1713 <SheetTitle className="sr-only">Menu</SheetTitle>
1714 <Sidebar
1715 user={user}
1716 shell={shell}
1717 missing={missing}
1718 onFind={() => {
1719 setDrawer(false);
1720 setPalette(true);
1721 }}
1722 onClose={() => setDrawer(false)}
1723 />
1724 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell1725
Polish: phones, copy boxes, the plan page, the landing page, a real glide1726 <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 bar1727 <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">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1728 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell1729 aria-label="Open menu"
1730 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1731 >
1732 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1733 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1734 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1735 <div className="ml-auto flex items-center gap-1.5">
1736 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1737 <button
1738 type="button"
1739 aria-label="Search or jump to"
1740 onClick={() => setPalette(true)}
1741 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1742 >
1743 <Search size={16} />
1744 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1745 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1746 <NavLink
1747 to="/explore"
1748 prefetch="intent"
1749 aria-label="Explore"
1750 className={({ isActive }) =>
1751 `flex h-9 items-center gap-1.5 rounded-md px-2 text-sm transition-colors hover:bg-raised hover:text-fg sm:px-2.5 ${isActive ? "text-fg" : "text-muted"}`
1752 }
1753 >
1754 <Compass size={16} className="sm:hidden" />
1755 <span className="hidden sm:inline">Explore</span>
1756 </NavLink>
A Vercel-like sidebar and a new mark1757 <a
1758 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1759 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 mark1760 >
1761 Docs
1762 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1763 {user && (
1764 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1765 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1766 <InboxBell counts={shell.inbox ?? null} />
1767 </>
1768 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1769 {!user ? (
1770 // The sidebar has these too, but on a phone it is folded away.
1771 <Link
1772 to={withNext("/login", here)}
1773 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"
1774 >
1775 Sign in
1776 </Link>
1777 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1778 <>
1779 {/* Room between what comes in (the inbox) and what you make. */}
1780 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1781 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1782 <Tooltip>
1783 <TooltipTrigger asChild>
1784 <DropdownMenuTrigger
1785 aria-label="Create new"
1786 className="flex h-9 items-center gap-1 rounded-md border border-line px-2 text-fg/90 outline-none transition-colors hover:border-line-strong hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised"
1787 >
1788 <Plus size={16} />
1789 <ChevronDown size={13} className="text-muted" />
1790 </DropdownMenuTrigger>
1791 </TooltipTrigger>
1792 <TooltipContent>Create new…</TooltipContent>
1793 </Tooltip>
1794 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1795 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1796 <>
1797 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1798 <DropdownMenuItem asChild>
1799 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1800 <CircleDot />
1801 New issue
1802 </Link>
1803 </DropdownMenuItem>
1804 {shell.repo.member && (
1805 <DropdownMenuItem asChild>
1806 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1807 <ListTree />
1808 Plan work
1809 </Link>
1810 </DropdownMenuItem>
1811 )}
1812 <DropdownMenuSeparator />
1813 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1814 )}
1815 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1816 <Link to="/?agent=new">
1817 <Sparkles />
1818 Put an agent on it
1819 </Link>
1820 </DropdownMenuItem>
1821 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1822 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1823 <Box />
1824 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1825 </Link>
1826 </DropdownMenuItem>
1827 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1828 <Link to="/new/github">
1829 <GitBranch />
1830 Import from GitHub
1831 </Link>
1832 </DropdownMenuItem>
1833 <DropdownMenuSeparator />
1834 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1835 <Link to="/workspaces/new">
1836 <Users />
1837 New workspace
1838 </Link>
1839 </DropdownMenuItem>
Owners choose who can create teams1840 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
1841 {shell.workspace &&
1842 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
1843 <DropdownMenuItem asChild>
1844 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1845 <UsersRound />
1846 New team
1847 </Link>
1848 </DropdownMenuItem>
1849 ) : (
1850 <DropdownMenuItem disabled title="Only owners can create teams in this workspace.">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1851 <UsersRound />
1852 New team
Owners choose who can create teams1853 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
1854 </DropdownMenuItem>
1855 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell1856 </DropdownMenuContent>
1857 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1858 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1859 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1860 </div>
1861 </header>
1862 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1863 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'1864 {children}
1865 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1866 </div>
Search across all of g1t, Explore, and a command palette1867 <CommandPalette
1868 open={palette}
1869 onOpenChange={setPalette}
1870 commands={commands}
1871 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1872 />
Agents as a team: lifecycle, merge queue, billing and a new shell1873 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1874 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell1875 );
1876}

This file's history is long; its oldest lines are credited to the oldest commit read.