Skip to content

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

1,835 lines75,181 bytesCodeBlameRaw

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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { 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, KanbanSquare, KeyRound, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
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
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all5import type { Abilities, InboxCounts, Membership, Spike, User } 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";
Merge branch 'worktree-agent-ae1299e92e4462012'11import { Hint } from "./ui/hint";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit12import { 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 quotas13import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height14import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit15import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents16import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell17import {
18 DropdownMenu,
19 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas20 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell21 DropdownMenuItem,
22 DropdownMenuLabel,
23 DropdownMenuSeparator,
24 DropdownMenuTrigger,
25} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales26import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
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,
Agents as a team: lifecycle, merge queue, billing and a new shell86 children,
87}: {
88 to: string;
89 icon: ReactNode;
90 end?: boolean;
91 count?: number;
Projects: what a workspace builds and runs, first on every page92 /** Other path prefixes under which this link is the current one. */
93 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look94 /** It opens a list of its own: a chevron says so, always or on hover. */
95 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell96 children: ReactNode;
97}) {
98 const { pathname } = useLocation();
99 return (
100 <NavLink
101 to={to}
102 end={end}
103 prefetch="intent"
104 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page105 const current =
106 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark107 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 shell108 current
109 ? "bg-raised font-medium text-fg"
110 : isPending
111 ? "bg-raised/60 text-fg"
112 : "text-muted hover:bg-raised/60 hover:text-fg"
113 }`;
114 }}
115 >
116 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
117 <span className="min-w-0 grow truncate">{children}</span>
118 {count != null && count > 0 && (
119 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
120 {count}
121 </span>
122 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123 {drill && (
124 <ChevronRight
125 size={14}
126 aria-hidden="true"
127 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
128 />
129 )}
Agents as a team: lifecycle, merge queue, billing and a new shell130 </NavLink>
131 );
132}
133
Pull requests say where they are live; the project menu shows what is coming134/**
135 * Something a project will have, shown where it will live so people can
136 * see where g1t is going. Not a link: nothing is behind it yet.
137 */
138function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
139 return (
Merge branch 'worktree-agent-ae1299e92e4462012'140 <Hint label={about} side="right">
141 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
142 <span className="shrink-0 opacity-70">{icon}</span>
143 <span className="grow truncate">{children}</span>
144 <span className="sr-only">{about}</span>
145 <SoonPill />
146 </div>
147 </Hint>
Pull requests say where they are live; the project menu shows what is coming148 );
149}
150
The project menu opens by section, and the workspace has its own panel151/**
A flat project menu with Code first, views as tabs, and Stripe for every owner152 * A page that is coming, as a link to its roadmap page: faint, with Soon,
153 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel154 */
A flat project menu with Code first, views as tabs, and Stripe for every owner155function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel156 to,
157 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner158 icon,
159 about,
The project menu opens by section, and the workspace has its own panel160 children,
161}: {
162 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner163 also?: string[];
164 icon: ReactNode;
165 about: string;
The project menu opens by section, and the workspace has its own panel166 children: ReactNode;
167}) {
168 const { pathname } = useLocation();
169 return (
Merge branch 'worktree-agent-ae1299e92e4462012'170 <Hint label={about} side="right">
The project menu opens by section, and the workspace has its own panel171 <NavLink
172 to={to}
173 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner174 className={({ isActive }) => {
175 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
176 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
177 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 panel178 }`;
179 }}
180 >
A flat project menu with Code first, views as tabs, and Stripe for every owner181 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel182 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit183 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales184 </NavLink>
Merge branch 'worktree-agent-ae1299e92e4462012'185 </Hint>
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/** Icons for what spans projects. */
190const WORKSPACE_ICONS: Record<string, ReactNode> = {
191 board: <KanbanSquare size={15} />,
192 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member193 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner194 packages: <Package size={15} />,
195 fleet: <Bot size={15} />,
196};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales197
A flat project menu with Code first, views as tabs, and Stripe for every owner198/** The Soon pages of a section, as paths, so its link is current on them. */
199function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
200 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales201}
202
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look203function SidebarGroup({
204 title,
205 action,
206 className = "",
207 children,
208}: {
209 title: string;
210 action?: ReactNode;
211 className?: string;
212 children: ReactNode;
213}) {
Agents as a team: lifecycle, merge queue, billing and a new shell214 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 <section className={className}>
A Vercel-like sidebar and a new mark216 <div className="mb-1 flex h-6 items-center justify-between px-2">
217 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell218 {action}
219 </div>
220 <div className="space-y-px">{children}</div>
221 </section>
222 );
223}
224
Workspace names and icons, and a component kit for every control225/** What a workspace is called where people read it: its display name, else its slug. */
226function displayName(membership: Membership): string {
227 return membership.name?.trim() || membership.slug;
228}
229
Agents as a team: lifecycle, merge queue, billing and a new shell230function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control231 const workspace = shell.workspace;
232 const current = workspace?.slug;
233 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell234 return (
235 <DropdownMenu>
Workspace names and icons, and a component kit for every control236 {/* The name goes to the workspace; only the arrows switch it. The
237 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'238 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark239 <Link
240 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents241 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 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 mark243 >
Workspace names and icons, and a component kit for every control244 {workspace ? (
245 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell246 ) : (
Workspace names and icons, and a component kit for every control247 <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 shell248 )}
Workspace names and icons, and a component kit for every control249 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark250 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'251 </Hint>
A Vercel-like sidebar and a new mark252 <DropdownMenuTrigger
253 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit254 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 mark255 >
256 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell257 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control258 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar259 {workspace && (
260 <>
261 {/* The workspace's own page: its projects, packages and people. */}
262 <DropdownMenuItem asChild>
263 <Link to={`/${workspace.slug}`}>
264 <LayoutGrid />
265 Workspace overview
266 </Link>
267 </DropdownMenuItem>
268 <DropdownMenuItem asChild>
269 <Link to={`/${workspace.slug}/-/projects`}>
270 <Box />
271 All projects
272 </Link>
273 </DropdownMenuItem>
274 <DropdownMenuSeparator />
275 </>
276 )}
Agents as a team: lifecycle, merge queue, billing and a new shell277 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
278 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'279 <Hint key={membership.slug} label={displayName(membership)} side="right">
280 <DropdownMenuItem asChild>
281 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control282 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
283 <span className="flex min-w-0 grow flex-col leading-tight">
284 <span className="truncate">{displayName(membership)}</span>
285 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
286 </span>
287 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
288 {membership.slug === current ? (
289 <Check className="shrink-0 text-accent" />
290 ) : (
291 <span className="size-4 shrink-0" aria-hidden="true" />
292 )}
Agents as a team: lifecycle, merge queue, billing and a new shell293 </Link>
294 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'295 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell296 ))}
297 <DropdownMenuSeparator />
298 <DropdownMenuItem asChild>
299 <Link to="/workspaces/new">
300 <Plus />
301 New workspace
302 </Link>
303 </DropdownMenuItem>
304 </DropdownMenuContent>
305 </DropdownMenu>
A Vercel-like sidebar and a new mark306 );
307}
308
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas309/**
310 * The fine print, as one quiet row at the foot of the account menu and the
311 * visitor's panel: each link a full-height target, left-aligned. In the
312 * menu each is a menu item, so the arrow keys reach it.
313 */
314const LEGAL_LINKS: [string, string][] = [
315 ["Policies", "/policies"],
316 ["Privacy", "/policies/privacy"],
317 ["Security", "/security"],
318];
319const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
320
321function MenuLegalRow() {
322 return (
323 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
324 {LEGAL_LINKS.map(([label, to], index) => (
325 <span key={to} className="flex items-center gap-0.5">
326 {index > 0 && (
327 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
328 ·
329 </span>
330 )}
331 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
332 <Link to={to}>{label}</Link>
333 </DropdownMenuItem>
334 </span>
335 ))}
336 </div>
337 );
338}
339
340/** Status's row: the live dot and two or three words, fetched when the menu opens. */
341function StatusSummary({ open }: { open: boolean }) {
342 const status = useSiteStatus(open);
343 const words = statusTitle(status);
344 return (
345 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
346 {/* The words say it; the dot's own words for screen readers would repeat them. */}
347 <span aria-hidden="true" className="flex">
348 <StatusDot state={status?.overall.state ?? null} />
349 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents350 {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 quotas351 </span>
352 );
353}
354
355/** A menu row: an icon, words, and whatever sits at its end. */
356const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
357
A Vercel-like sidebar and a new mark358function AccountMenu({ user }: { user: User }) {
359 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas360 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents361 // Name, primary address and invites left: asked for once, as soon as the
362 // 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 quotas363 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents364 const prefetch = () => {
365 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
366 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas367 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents368 if (open) prefetch();
369 // eslint-disable-next-line react-hooks/exhaustive-deps
370 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas371 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents372 // Still on its way: shapes where the words will be, the same size.
373 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 quotas374 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark375 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas376 <DropdownMenu open={open} onOpenChange={setOpen}>
377 <DropdownMenuTrigger
378 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents379 onPointerEnter={prefetch}
380 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas381 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-merged/60 data-[state=open]:bg-raised"
382 >
Workspace names and icons, and a component kit for every control383 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark384 <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 quotas385 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark386 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas387 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
388 {/* Who is signed in, and a way to their profile. */}
389 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
390 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
391 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents392 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
393 {loading ? (
394 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
395 ) : (
396 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
397 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas398 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents399 {loading ? (
400 <Skeleton className="mt-1 h-3 w-40" />
401 ) : (
402 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
403 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas404 </span>
A Vercel-like sidebar and a new mark405 </Link>
406 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas407 <DropdownMenuSeparator className="my-1.5" />
408 <DropdownMenuGroup>
409 <DropdownMenuItem asChild className={MENU_ROW}>
410 <Link to={profile}>
411 <CircleUserRound />
412 Your profile
413 </Link>
414 </DropdownMenuItem>
415 <DropdownMenuItem asChild className={MENU_ROW}>
416 <Link to={FIRST_SETTINGS_PAGE}>
417 <Settings />
418 Your settings
419 </Link>
420 </DropdownMenuItem>
421 <DropdownMenuItem asChild className={MENU_ROW}>
422 <Link to="/settings/invites">
423 <Ticket />
424 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents425 {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 quotas426 {me?.invites_left != null && (
427 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
428 {me.invites_left} left
429 </span>
430 )}
431 </Link>
432 </DropdownMenuItem>
433 </DropdownMenuGroup>
434 <DropdownMenuSeparator className="my-1.5" />
435 <DropdownMenuGroup>
436 <DropdownMenuItem asChild className={MENU_ROW}>
437 <a href="https://docs.g1t.sh/">
438 <BookOpen />
439 Documentation
440 </a>
441 </DropdownMenuItem>
442 <DropdownMenuItem asChild className={MENU_ROW}>
443 <Link to="/support">
444 <LifeBuoy />
445 Support
446 </Link>
447 </DropdownMenuItem>
448 <DropdownMenuItem asChild className={MENU_ROW}>
449 <a href={STATUS_URL}>
450 <Activity />
451 Status
452 <StatusSummary open={open} />
453 </a>
454 </DropdownMenuItem>
455 </DropdownMenuGroup>
456 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark457 {/* Submitted from here: the menu closes on select, and a button
458 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 quotas459 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark460 <LogOut />
461 Sign out
462 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas463 <DropdownMenuSeparator className="my-1.5" />
464 <MenuLegalRow />
A Vercel-like sidebar and a new mark465 </DropdownMenuContent>
466 </DropdownMenu>
467 );
468}
469
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look470/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar471 * A workspace's settings pages, which the sidebar drills into: how it is
472 * set up and connected (guardrails, secrets, runners, integrations,
473 * webhooks), what it pays, its repositories, tokens and record. The main
474 * list keeps the places work happens and who belongs; every member can
475 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look476 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar477const SETTINGS_PAGE =
478 /^\/([^/]+)\/-\/(settings|repositories|tokens|guardrails|secrets|runners|integrations|webhooks|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look479/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page480const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings481
Polish: phones, copy boxes, the plan page, the landing page, a real glide482/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look483 * The sidebar's lists are a stack, the way a phone pushes a screen: one
484 * drilled into slides in from the right over the full width, while the one
485 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide486 * ease-out, the fade quicker than the move so the two never blur together.
487 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide488 */
Sidebar: no settings flash on the way into a repository, and a better slide489const LAYER =
Sidebar: the panels really slide490 "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 look491/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide492const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings493
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look494/** One list in the stack: what it is, and its rows. */
495type Level = { key: string; node: ReactNode };
496
497/** True once the page has hydrated, so what the server drew never animates in. */
498function useHydrated(): boolean {
499 const [hydrated, setHydrated] = useState(false);
500 useEffect(() => setHydrated(true), []);
501 return hydrated;
502}
503
504/**
505 * Drilling in and out of the sidebar's lists. `trail` is the way from the
506 * main list to the one shown, worked out from the address, so a link
507 * straight to a settings page opens the sidebar already drilled in, and
508 * leaving it comes back out. Lists deeper than the one shown stay in
509 * place, off to the right, so going back slides them away rather than
510 * dropping them. Moving to a list at the same depth (a workspace's settings
511 * to a project) fades the new one in where it is.
512 */
513function Drill({ trail }: { trail: Level[] }) {
514 const depth = trail.length - 1;
515 const hydrated = useHydrated();
516 const stack = useRef<Level[]>(trail);
517 const before = stack.current;
518 const within = trail.every((level, index) => before[index]?.key === level.key);
519 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
520 const levels = stack.current;
521 // What was showing last time, to tell drilling in from moving across.
522 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
523 useEffect(() => {
524 shown.current = { depth, keys: levels.map((level) => level.key) };
525 });
A catalogue of model providers, and settings that feel like settings526 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look527 <div className="relative min-h-0 grow overflow-hidden">
528 {levels.map((level, index) => {
529 const current = index === depth;
530 const place =
531 index < depth
532 ? "pointer-events-none -translate-x-1/4 opacity-0"
533 : current
534 ? "translate-x-0 opacity-100"
535 : "pointer-events-none translate-x-full opacity-0";
536 // A list that was not there before arrives: from the right when
537 // drilling in, in place when moving across.
538 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
539 const enter = arriving
540 ? depth > shown.current.depth
541 ? "starting:translate-x-full starting:opacity-0"
542 : "starting:opacity-0"
543 : "";
544 return (
545 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
546 {level.node}
547 </div>
548 );
549 })}
550 </div>
551 );
552}
553
554/**
555 * The row at the top of a list drilled into: the list's name, which goes
556 * back out, and what it belongs to, faint at the end.
557 */
558function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
559 return (
560 <Link
561 to={to}
562 prefetch="intent"
563 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 settings564 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look565 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
566 <span className="min-w-0 shrink-0 truncate">{label}</span>
567 {context && (
568 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
569 )}
570 </Link>
571 );
572}
573
574/** A thin rule between groups of a list. */
575function Rule() {
576 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
577}
578
Navigation: the workspace's own page with tabs, pins and recent in the sidebar579/** A quiet heading inside a group: Pinned, Recent. */
580function SidebarSubhead({ children }: { children: ReactNode }) {
581 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
582}
583
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look584/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar585 * The workspace's projects as the sidebar keeps them, however many there
586 * are: the person's pins in their order, what they opened last, and the
587 * way to all of them. Pins move by dragging, or with Alt and the arrow
588 * keys; the projects service keeps the order.
589 */
590function SidebarProjects({ slug, shell }: { slug: string; shell: ShellData }) {
591 const reorder = useFetcher({ key: `pins:${slug}` });
592 const saved = shell.pinned ?? [];
593 // While a new order is on its way, it shows as made.
594 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
595 const pinned = asked
596 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
597 : saved;
598 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
599 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
600 const [dragging, setDragging] = useState<number | null>(null);
601 const move = (from: number, by: number) => {
602 const next = movedPin(pinned, from, by);
603 if (!next) return;
604 const form = new FormData();
605 form.set("intent", "reorder");
606 for (const project of next) form.append("slug", project.name);
607 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
608 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar609 // Each row pins or unpins in place: the pin shows on hover or focus, and
610 // stays shown on a pinned row's hover so it reads as "unpin".
611 const row = (project: ShortcutProject, isPinned: boolean) => (
612 <div className="group/row relative">
613 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
614 <span className="block truncate pr-6">{project.title ?? project.name}</span>
615 </SidebarLink>
616 <PinButton
617 workspace={slug}
618 slug={project.name}
619 name={project.title ?? project.name}
620 pinned={isPinned}
621 small
622 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"
623 />
624 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar625 );
626 return (
627 <SidebarGroup
628 title="Projects"
629 action={
630 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
631 <Plus size={13} />
632 </Link>
633 }
634 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar635 {/* Only there once something is pinned: each row below has its own pin. */}
636 {pinned.length > 0 && (
637 <>
638 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar639 <ul aria-label="Pinned projects" className="space-y-px">
640 {pinned.map((project, index) => (
641 <li
642 key={project.name}
643 draggable={pinned.length > 1}
644 onDragStart={(event) => {
645 setDragging(index);
646 event.dataTransfer.effectAllowed = "move";
647 }}
648 onDragOver={(event) => {
649 if (dragging != null) event.preventDefault();
650 }}
651 onDrop={(event) => {
652 event.preventDefault();
653 if (dragging != null && dragging !== index) move(dragging, index - dragging);
654 setDragging(null);
655 }}
656 onDragEnd={() => setDragging(null)}
657 onKeyDown={(event) => {
658 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
659 event.preventDefault();
660 move(index, event.key === "ArrowUp" ? -1 : 1);
661 }}
662 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
663 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar664 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar665 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'666 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
667 <span className="absolute top-1/2 -left-1.5 -translate-y-1/2 cursor-grab text-faint opacity-0 transition-opacity group-hover/pin:opacity-100">
668 <GripVertical size={12} aria-hidden="true" />
669 </span>
670 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar671 )}
672 </li>
673 ))}
674 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar675 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar676 )}
677 {recent.length > 0 && (
678 <>
679 <SidebarSubhead>Recent</SidebarSubhead>
680 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar681 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar682 ))}
683 </>
684 )}
685 <div className="pt-1">
686 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length}>
687 All projects
688 </SidebarLink>
689 </div>
690 </SidebarGroup>
691 );
692}
693
694/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look695 * The main list: where you go, the projects, what the workspace builds and
696 * runs with across them, then its usage, support and settings.
697 */
698function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
699 const ws = shell.workspace;
700 // A visitor browses: no workspace, no projects of their own.
701 if (!user) {
702 return (
703 <nav aria-label="g1t" className={PANEL}>
704 <div className="mt-3 space-y-px">
705 {VISITOR_LINKS.map((link) => (
706 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
707 {link.label}
708 </SidebarLink>
709 ))}
710 </div>
711 </nav>
712 );
713 }
714 return (
715 <nav aria-label="g1t" className={PANEL}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar716 {/* Everything here is the workspace the switcher names: its home first.
717 The workspace's own page is the switcher's; Explore, all of g1t,
718 is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look719 <div className="mt-3 space-y-px">
720 <SidebarLink to="/" end icon={<House size={15} />}>
721 Mission control
722 </SidebarLink>
723 </div>
724
725 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar726 {ws ? (
727 <SidebarProjects slug={ws.slug} shell={shell} />
728 ) : (
729 <SidebarGroup title="Projects">
730 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
731 </SidebarGroup>
732 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look733 {(shell.shared ?? []).length > 0 && (
734 <SidebarGroup title="Shared with you" className="mt-3">
735 {(shell.shared ?? []).map((repo) => (
736 <SidebarLink
737 key={`${repo.namespace}/${repo.name}`}
738 to={`/${repo.namespace}/${repo.name}`}
739 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
740 drill="hover"
741 >
742 <span className="font-mono text-faint">{repo.namespace}/</span>
743 {repo.name}
744 </SidebarLink>
745 ))}
746 </SidebarGroup>
747 )}
748
749 {ws && (
750 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member751 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar752 <SidebarGroup title="Workspace">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look753 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
754 Agent fleet
755 </SidebarLink>
756 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
757 Context
758 </SidebarLink>
759 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
760 Memory
761 </SidebarLink>
762 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
763 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents764 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member765 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
766 Packages
767 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar768 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look769 <SidebarSoonLink
770 key={item.key}
771 to={`/${ws.slug}/-/soon/${item.key}`}
772 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
773 about={item.summary}
774 >
775 {item.title === "Board" ? "Boards" : item.title}
776 </SidebarSoonLink>
777 ))}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar778 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look779
780 <Rule />
781 <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 bar782 {/* Who belongs, for every member to see; owners invite and manage there. */}
783 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar784 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar785 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar786 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams}>
787 Teams
788 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look789 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
790 Usage
791 </SidebarLink>
792 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
793 Support
794 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar795 {/* 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 look796 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member797 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 look798 icon={<Settings size={15} />}
799 drill
800 >
801 Settings
802 </SidebarLink>
803 </div>
804 </>
805 )}
806 </nav>
807 );
808}
809
810/** A workspace's settings, drilled into from Settings in the main list. */
811function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
812 return (
813 <nav aria-label="Workspace settings" className={PANEL}>
814 <BackRow to={`/${slug}`} label="Settings" context={slug} />
815 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings816 {owner && (
817 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
818 General
819 </SidebarLink>
820 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look821 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
822 Repositories
823 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments824 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
825 Access tokens
826 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look827 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar828 <SidebarGroup title="Agents and runs" className="mt-3">
829 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
830 Guardrails
831 </SidebarLink>
832 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
833 Secrets and variables
834 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it835 {owner && (
836 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
837 Runners
838 </SidebarLink>
839 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar840 </SidebarGroup>
841 <SidebarGroup title="Connections" className="mt-3">
842 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
843 Integrations
844 </SidebarLink>
845 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
846 Webhooks
847 </SidebarLink>
848 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look849 <Rule />
850 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings851 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments852 Billing and plans
A catalogue of model providers, and settings that feel like settings853 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API854 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
855 Audit log
856 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look857 </div>
A catalogue of model providers, and settings that feel like settings858 </nav>
859 );
860}
861
Sidebar: no settings flash on the way into a repository, and a better slide862/**
863 * What the repository menu needs. Known from the address before the page's
864 * data arrives, so the menu is right from the first frame; the counts fill
865 * in once it does.
866 */
867type MenuRepo = {
868 namespace: string;
869 name: string;
870 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871 /** Whether they see its settings; from the repository's page once it loads. */
872 settings?: boolean;
873 /** What they may do there, once the repository's page loads. */
874 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide875 issues?: number;
876 pulls?: number;
877};
878
879function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
880 return (
881 a != null &&
882 b != null &&
883 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
884 a.name.toLowerCase() === b.name.toLowerCase()
885 );
886}
A repository has its own sidebar, as settings do887
888/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look889 * A project's own list, drilled into while you are in it: everything about
890 * the project, running and its code, and nothing else, with the way back
891 * out to all of them.
A repository has its own sidebar, as settings do892 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put893function RepoMenu({
894 repo,
895 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look896 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put897}: {
898 repo: MenuRepo;
899 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look900 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
901 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put902}) {
A repository has its own sidebar, as settings do903 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 put904 // 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 look905 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do906 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look907 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
908 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'909 <Hint label="Overview" side="right">
A flat project menu with Code first, views as tabs, and Stripe for every owner910 <NavLink
A repository has its own sidebar, as settings do911 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner912 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents913 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner914 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look915 `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 owner916 }
A repository has its own sidebar, as settings do917 >
918 <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 page919 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do920 </span>
921 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
922 <span className="text-faint">{repo.namespace}/</span>
923 <span className="font-semibold text-fg">{repo.name}</span>
924 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner925 </NavLink>
Merge branch 'worktree-agent-ae1299e92e4462012'926 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look927 {/* A project's pages, in the order people use them. A page with more
928 than one view shows them as tabs across its top. */}
929 <Rule />
930 <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 request931 <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 owner932 Code
933 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit934 <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 owner935 Issues
936 </SidebarLink>
937 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
938 Pull requests
939 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains940 <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 owner941 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains942 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner943 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
944 Workflows
Projects: what a workspace builds and runs, first on every page945 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look946 </div>
947 <Rule />
948 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put949 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner950 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
951 Deployments
952 </SidebarLink>
953 ) : null}
Observability gets its own page; tabs never overflow954 <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.">
955 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner956 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put957 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API958 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
959 Security
960 </SidebarLink>
961 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner962 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
963 Insights
964 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look965 </div>
966 {(repo.settings ?? shows.has("settings")) && (
967 <>
968 <Rule />
969 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner970 Settings
971 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look972 </>
973 )}
A repository has its own sidebar, as settings do974 </nav>
975 );
976}
977
Project settings move into the sidebar, grouped, with a heading per page978/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look979 * A project's settings, drilled into from its list: what is about running
980 * it, then its agents, then its code and who can reach it. The way back
981 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page982 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look983function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page984 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look985 // Each page shows to the roles that can use it, once the role is known.
986 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
987 const running = shows("deployments");
988 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page989 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look990 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
991 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
992 {shows("") && (
993 <div className="mt-2 space-y-px">
994 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
995 General
996 </SidebarLink>
997 </div>
998 )}
999 {running && (
1000 <>
1001 {shows("") && <Rule />}
1002 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1003 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1004 Deployments
1005 </SidebarLink>
1006 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1007 Domains
1008 </SidebarLink>
1009 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1010 Dependencies
1011 </SidebarLink>
1012 </div>
1013 </>
1014 )}
1015 {agents && (
1016 <>
1017 <Rule />
1018 <div className="space-y-px">
1019 {shows("agents") && (
1020 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1021 Agents
1022 </SidebarLink>
1023 )}
1024 {shows("guardrails") && (
1025 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1026 Guardrails
1027 </SidebarLink>
1028 )}
1029 </div>
1030 </>
1031 )}
1032 <Rule />
1033 <div className="space-y-px">
1034 {shows("repository") && (
1035 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1036 Repository
1037 </SidebarLink>
1038 )}
1039 {shows("access") && (
1040 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1041 Access
1042 </SidebarLink>
1043 )}
1044 {shows("branches") && (
1045 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1046 Branches and merging
1047 </SidebarLink>
1048 )}
1049 {shows("secrets") && (
1050 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1051 Secrets and variables
1052 </SidebarLink>
1053 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1054 {shows("runners") && (
1055 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1056 Runners
1057 </SidebarLink>
1058 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1059 {shows("webhooks") && (
1060 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1061 Webhooks
1062 </SidebarLink>
1063 )}
1064 </div>
Project settings move into the sidebar, grouped, with a heading per page1065 </nav>
1066 );
1067}
1068
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1069/** Your own settings, drilled into from Your settings: one page each. */
1070function AccountSettingsMenu({ username }: { username: string }) {
1071 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1072 <SidebarLink to={`/settings/${page}`} icon={icon}>
1073 {ACCOUNT_SETTINGS[page].title}
1074 </SidebarLink>
Account settings in the same shape as a workspace's1075 );
1076 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1077 <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 quotas1078 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1079 <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 quotas1080 {link("profile", <CircleUserRound size={15} />)}
1081 {link("emails", <Mail size={15} />)}
1082 {link("invites", <Ticket size={15} />)}
1083 </div>
1084 <Rule />
1085 <div className="space-y-px">
1086 {link("keys", <Fingerprint size={15} />)}
1087 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1088 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1089 <Rule />
1090 <div className="space-y-px">
1091 {link("github", <GithubMark className="size-[15px]" />)}
1092 {link("applications", <Plug size={15} />)}
1093 </div>
1094 <Rule />
1095 <div className="space-y-px">
1096 {link("security-log", <History size={15} />)}
1097 </div>
Account settings in the same shape as a workspace's1098 </nav>
1099 );
1100}
1101
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1102/**
1103 * Signing in and signing up, in place of the account for a visitor. Signing
1104 * in brings them back to the page they are on.
1105 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1106/** Help, status and the fine print under a visitor's sign-in buttons. */
1107function VisitorLinks() {
1108 const status = useSiteStatus();
1109 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1110 const dot = (
1111 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1112 ·
1113 </span>
1114 );
1115 return (
1116 // Two short rows, so a narrow sidebar never starts a line with a dot.
1117 <nav aria-label="About g1t" className="pt-0.5">
1118 <div className="flex items-center gap-0.5">
1119 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1120 <StatusDot state={status?.overall.state ?? null} />
1121 Status
1122 </a>
1123 {dot}
1124 <Link to="/support" className={quiet}>
1125 Support
1126 </Link>
1127 </div>
1128 <div className="flex items-center gap-0.5">
1129 {LEGAL_LINKS.map(([label, to], index) => (
1130 <span key={to} className="flex items-center gap-0.5">
1131 {index > 0 && dot}
1132 <Link to={to} className={quiet}>
1133 {label}
1134 </Link>
1135 </span>
1136 ))}
1137 </div>
1138 </nav>
1139 );
1140}
1141
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1142function VisitorPanel() {
1143 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1144 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1145 return (
1146 <div className="space-y-2">
1147 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1148 <div className="grid grid-cols-2 gap-2">
1149 <Link
1150 to={withNext("/login", pathname + search)}
1151 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"
1152 >
1153 <LogIn size={14} />
1154 Sign in
1155 </Link>
1156 <Link
1157 to={withNext("/register", pathname + search)}
1158 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1159 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1160 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1161 </Link>
1162 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1163 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1164 </div>
1165 );
1166}
1167
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1168function Sidebar({
1169 user,
1170 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1171 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1172 onFind,
1173 onClose,
1174}: {
1175 user: User | null;
1176 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1177 /** The page is a 404: the address names nothing the viewer can see. */
1178 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1179 onFind: () => void;
1180 /** In the sheet on a small screen: closing it, at the end of the top row. */
1181 onClose?: () => void;
1182}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1183 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1184 const { pathname } = useLocation();
1185 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1186 // Where the sidebar is drilled to follows the page being gone to, so it
1187 // moves as the link is followed, not once the page arrives.
1188 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1189 // On a 404, the address is not a place: no list is drilled into from it,
1190 // except the project's own when the project is real and visible and only
1191 // something inside it is missing (a file, a commit, an issue).
1192 const lost = missing && going == null;
1193 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1194 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1195 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1196 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1197 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1198 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 look1199 // An invitation to a repository is answered before its menu means anything.
1200 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1201 repoPath != null &&
1202 !reserved.has(repoPath[1]) &&
1203 repoPath[2] !== "-" &&
1204 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1205 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1206 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1207 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1208 // The repository the menu is for: the one loaded if it is the one being
1209 // gone to, else what the address says, at once.
1210 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 look1211 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1212 ? sameRepo(active, targetRepo)
1213 ? active
1214 : {
1215 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1216 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1217 }
1218 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1219 // The viewer's role there, from the repository's page once it has loaded.
1220 const page = useRouteLoaderData("routes/repo/layout") as
1221 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1222 | undefined;
1223 const menuRepo: MenuRepo | null =
1224 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1225 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1226 : guessed;
1227
1228 // The way from the main list to the one shown.
1229 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1230 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1231 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1232 } else if (ws && inSettings) {
1233 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1234 } else if (menuRepo) {
1235 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1236 // Out of a project: to its workspace's projects when they are yours.
1237 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1238 const back = !user
1239 ? { to: "/explore", label: "Explore" }
1240 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1241 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1242 : { to: "/", label: "Mission control" };
1243 trail.push({
1244 key,
1245 node: (
1246 <RepoMenu
1247 repo={menuRepo}
1248 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1249 back={back}
1250 />
1251 ),
1252 });
1253 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1254 }
1255
Agents as a team: lifecycle, merge queue, billing and a new shell1256 return (
1257 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1258 {/* 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 bar1259 <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 put1260 {user ? (
1261 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1262 {/* The 1 alone beside the workspace: a square hover the height of the
1263 switcher, the mark as tall as the workspace avatar. */}
1264 <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 bar1265 <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 put1266 </Link>
1267 <span className="shrink-0 text-line-strong" aria-hidden="true">
1268 /
1269 </span>
1270 <WorkspaceSwitcher user={user} shell={shell} />
1271 </>
1272 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1273 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1274 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1275 </Link>
1276 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1277 {onClose && (
1278 <button
1279 type="button"
1280 aria-label="Close menu"
1281 onClick={onClose}
1282 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1283 >
1284 <X size={16} />
1285 </button>
1286 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1287 </div>
A Vercel-like sidebar and a new mark1288 <div className="px-2 pt-3">
1289 <button
1290 type="button"
1291 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1292 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 mark1293 >
1294 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1295 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1296 <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 mark1297 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1298 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1299 <Drill trail={trail} />
1300 {/* The account, or signing in, as one row at the very bottom. */}
1301 <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 put1302 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1303 </div>
1304 </div>
1305 );
1306}
1307
1308/** Words for the sections a path can end in. */
1309const SECTIONS: Record<string, string> = {
1310 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1311 milestones: "Milestones",
1312 labels: "Labels",
A Vercel-like sidebar and a new mark1313 pulls: "Pull requests",
1314 queue: "Merge queue",
1315 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1316 plans: "Outcomes",
1317 actions: "Workflows",
1318 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1319 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1320 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1321 access: "Access",
1322 invitations: "Invitation",
1323 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1324 branches: "Branches",
1325 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1326 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1327 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1328 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1329 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1330 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1331 people: "People",
1332 projects: "Projects",
1333 teams: "Teams",
A Vercel-like sidebar and a new mark1334 tokens: "Access tokens",
1335 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1336 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1337 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1338 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1339 domains: "Domains",
1340 guardrails: "Guardrails",
1341 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1342 tree: "Files",
1343 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1344 agents: "Agents",
1345 context: "Context",
1346 memory: "Memory",
1347 security: "Security",
1348 packages: "Packages",
1349 runners: "Runners",
1350 workflows: "Workflows",
1351 observability: "Observability",
1352 insights: "Insights",
1353 sessions: "Sessions",
A Vercel-like sidebar and a new mark1354};
1355
1356/** 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 quotas1357function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1358 // A 404 names nothing from the address, unless the project is real and
1359 // visible and only something inside it is missing.
1360 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1361 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1362 return <span className="text-sm font-medium">Not found</span>;
1363 }
A Vercel-like sidebar and a new mark1364 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1365 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1366 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 quotas1367 // Your settings: Settings / Emails.
1368 if (parts[0] === "settings") {
1369 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1370 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 quotas1371 }
A Vercel-like sidebar and a new mark1372 if (reserved.includes(parts[0]!)) {
1373 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1374 settings: "Account",
A Vercel-like sidebar and a new mark1375 explore: "Explore",
Projects live: fixes from walking them end to end1376 new: "New project",
A Vercel-like sidebar and a new mark1377 search: "Search",
1378 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1379 policies: "Policies",
1380 security: "Security",
1381 support: "Support",
1382 status: "Status",
1383 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1384 inbox: "Inbox",
A Vercel-like sidebar and a new mark1385 };
1386 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1387 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1388 // A person's profile, by their handle.
1389 if (parts[0] === "u" && parts[1]) {
1390 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1391 }
A Vercel-like sidebar and a new mark1392 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1393 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1394 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1395 const page = `/${owner}/-/${third}`;
1396 if (third && SETTINGS_PAGE.test(page)) {
1397 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1398 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1399 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1400 } else if (second) {
1401 const repo = `/${owner}/${second}`;
1402 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1403 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1404 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 mark1405 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 deployments1406 else if (third && SECTIONS[third]) {
1407 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1408 // A settings page names which one: Settings / Secrets and variables.
1409 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1410 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 deployments1411 }
1412 }
A Vercel-like sidebar and a new mark1413 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1414 return <Trail trail={trail} />;
1415}
1416
1417/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1418type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1419
1420/**
1421 * The trail of links. On a phone there is room for one: the page itself,
1422 * with a way back to where it sits; the whole trail from a wider screen up.
1423 */
1424function Trail({ trail }: { trail: Crumb[] }) {
1425 const up = trail.length > 1 ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1426 return (
1427 <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 audit1428 {up && (
1429 <Link
1430 to={up.to}
1431 prefetch="intent"
1432 aria-label={`Back to ${up.label}`}
1433 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"
1434 >
1435 <ChevronLeft size={16} />
1436 </Link>
1437 )}
1438 {trail.map((crumb, index) => {
1439 const last = index === trail.length - 1;
1440 return (
1441 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1442 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1443 <Link
1444 to={crumb.to}
1445 prefetch="intent"
1446 aria-current={last ? "page" : undefined}
1447 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1448 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1449 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1450 >
1451 {last && crumb.short ? (
1452 <>
1453 <span className="sm:hidden">{crumb.short}</span>
1454 <span className="hidden sm:inline">{crumb.label}</span>
1455 </>
1456 ) : (
1457 crumb.label
1458 )}
1459 </Link>
1460 </span>
1461 );
1462 })}
A Vercel-like sidebar and a new mark1463 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1464 );
1465}
1466
Search across all of g1t, Explore, and a command palette1467type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1468
1469/** 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 look1470function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1471 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1472 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1473 { 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 step1474 ...(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-agent1475 ? [{ 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 step1476 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1477 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1478 { 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 all1479 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1480 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1481 { 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 quotas1482 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1483 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1484 .filter((page) => page !== "profile")
1485 .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 shell1486 ];
1487 const repo = shell.repo;
1488 if (repo) {
1489 const base = `/${repo.namespace}/${repo.name}`;
1490 const name = `${repo.namespace}/${repo.name}`;
1491 commands.unshift(
Projects: what a workspace builds and runs, first on every page1492 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1493 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1494 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1495 { 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 step1496 ...(repo.member
1497 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1498 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1499 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1500 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1501 ...(repo.member
1502 ? [
1503 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1504 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1505 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1506 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1507 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1508 ]
1509 : []),
1510 );
1511 }
1512 for (const membership of user.workspaces ?? []) {
1513 commands.push(
Workspace names and icons, and a component kit for every control1514 { 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 sidebar1515 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1516 { 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 request1517 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1518 { 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 settings1519 { 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 look1520 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1521 );
1522 }
1523 for (const listed of shell.repos) {
1524 commands.push({
Projects: what a workspace builds and runs, first on every page1525 label: listed.title ?? listed.name,
1526 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1527 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1528 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1529 });
1530 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1531 // "Put an agent on …": one per project, after the projects themselves.
1532 for (const listed of shell.repos) {
1533 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1534 commands.push({
1535 label: `Put an agent on ${listed.title ?? listed.name}`,
1536 hint: `${listed.namespace}/${listed.name}`,
1537 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1538 icon: <Sparkles size={15} />,
1539 });
1540 }
Agents as a team: lifecycle, merge queue, billing and a new shell1541 return commands;
1542}
1543
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1544/** 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 look1545function 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 put1546 const commands: Command[] = [];
1547 const repo = shell.repo;
1548 if (repo) {
1549 const base = `/${repo.namespace}/${repo.name}`;
1550 const name = `${repo.namespace}/${repo.name}`;
1551 commands.push(
1552 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1553 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1554 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1555 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1556 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1557 );
1558 }
1559 commands.push(
1560 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1561 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1562 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1563 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1564 { 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 look1565 { 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 put1566 );
1567 return commands;
1568}
1569
Agents as a team: lifecycle, merge queue, billing and a new shell1570/**
Merge branch 'worktree-agent-aca4790ea223aa235'1571 * The page while the next one loads: after a moment it dims, under the
1572 * bar below, so it is plainly on its way out rather than frozen, and the
1573 * next page fades up from there. Sending a form does not dim it: the
1574 * form's button says it is working. Spread on the element holding the page.
1575 */
1576export function useLeaving() {
1577 const navigation = useNavigation();
1578 const method = navigation.formMethod?.toUpperCase();
1579 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1580 return {
1581 "aria-busy": leaving || undefined,
1582 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1583 };
1584}
1585
1586/**
Agents as a team: lifecycle, merge queue, billing and a new shell1587 * A bar across the top of the page while the next one loads, as GitHub
1588 * has: it appears at once, creeps towards the end while waiting, then fills
1589 * and fades when the page arrives.
1590 */
1591export function Progress() {
1592 const navigation = useNavigation();
1593 const busy = navigation.state !== "idle";
1594 const [width, setWidth] = useState(0);
1595 const [visible, setVisible] = useState(false);
1596 useEffect(() => {
1597 if (busy) {
1598 setVisible(true);
1599 setWidth(12);
1600 // Slows as it goes, never quite arriving until the page does.
1601 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1602 return () => clearInterval(timer);
1603 }
1604 setWidth((now) => (now > 0 ? 100 : 0));
1605 const fade = setTimeout(() => setVisible(false), 250);
1606 const reset = setTimeout(() => setWidth(0), 550);
1607 return () => {
1608 clearTimeout(fade);
1609 clearTimeout(reset);
1610 };
1611 }, [busy]);
1612 return (
1613 <div
1614 aria-hidden="true"
1615 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1616 visible ? "opacity-100" : "opacity-0"
1617 }`}
1618 >
1619 <div
1620 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1621 style={{ width: `${width}%` }}
1622 />
1623 </div>
1624 );
1625}
1626
1627/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1628 * The app: a sidebar with the workspace, its repositories and the sections
1629 * of the one being looked at; a slim bar with search and the account; and
1630 * the page. A visitor who is not signed in gets the same frame, with
1631 * Explore and Search in place of the workspace, and signing in in place of
1632 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1633 */
1634export function AppShell({
1635 user,
1636 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1637 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1638 banner,
1639 children,
1640}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1641 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1642 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1643 /** The page is a 404 (see Sidebar). */
1644 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1645 banner?: ReactNode;
1646 children: ReactNode;
1647}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1648 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1649 const [drawer, setDrawer] = useState(false);
1650 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1651 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1652 const signUpLabel = useSignUpCopy().primary;
1653 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1654
1655 // A new page closes the drawer on small screens.
1656 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1657 // Mission control's code, fetched while the browser is idle, so going
1658 // home never waits on it (routes/home.tsx).
1659 useEffect(() => {
1660 if (!user) return;
1661 const load = () => void import("./mission-control").catch(() => undefined);
1662 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1663 if (idle) idle(load);
1664 else setTimeout(load, 1500);
1665 }, [user]);
Search across all of g1t, Explore, and a command palette1666 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1667 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1668
1669 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1670 // The phone's menu is a sheet: a dialog that holds focus, closes on
1671 // Escape or a tap outside, and gives focus back to the menu button.
1672 <Sheet open={drawer} onOpenChange={setDrawer}>
Agents as a team: lifecycle, merge queue, billing and a new shell1673 <div className="min-h-screen">
1674 <Progress />
1675 <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 quotas1676 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1677 </aside>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1678 <SheetContent side="left" showClose={false} aria-describedby={undefined} className="w-72 max-w-[85vw] sm:max-w-72 lg:hidden">
1679 <SheetTitle className="sr-only">Menu</SheetTitle>
1680 <Sidebar
1681 user={user}
1682 shell={shell}
1683 missing={missing}
1684 onFind={() => {
1685 setDrawer(false);
1686 setPalette(true);
1687 }}
1688 onClose={() => setDrawer(false)}
1689 />
1690 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell1691
Polish: phones, copy boxes, the plan page, the landing page, a real glide1692 <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 bar1693 <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 audit1694 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell1695 aria-label="Open menu"
1696 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1697 >
1698 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1699 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1700 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1701 <div className="ml-auto flex items-center gap-1.5">
1702 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1703 <button
1704 type="button"
1705 aria-label="Search or jump to"
1706 onClick={() => setPalette(true)}
1707 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1708 >
1709 <Search size={16} />
1710 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1711 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1712 <NavLink
1713 to="/explore"
1714 prefetch="intent"
1715 aria-label="Explore"
1716 className={({ isActive }) =>
1717 `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"}`
1718 }
1719 >
1720 <Compass size={16} className="sm:hidden" />
1721 <span className="hidden sm:inline">Explore</span>
1722 </NavLink>
A Vercel-like sidebar and a new mark1723 <a
1724 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1725 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 mark1726 >
1727 Docs
1728 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1729 {user && (
1730 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1731 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1732 <InboxBell counts={shell.inbox ?? null} />
1733 </>
1734 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1735 {!user ? (
1736 // The sidebar has these too, but on a phone it is folded away.
1737 <Link
1738 to={withNext("/login", here)}
1739 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"
1740 >
1741 Sign in
1742 </Link>
1743 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1744 <>
1745 {/* Room between what comes in (the inbox) and what you make. */}
1746 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1747 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1748 <Tooltip>
1749 <TooltipTrigger asChild>
1750 <DropdownMenuTrigger
1751 aria-label="Create new"
1752 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"
1753 >
1754 <Plus size={16} />
1755 <ChevronDown size={13} className="text-muted" />
1756 </DropdownMenuTrigger>
1757 </TooltipTrigger>
1758 <TooltipContent>Create new…</TooltipContent>
1759 </Tooltip>
1760 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1761 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1762 <>
1763 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1764 <DropdownMenuItem asChild>
1765 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1766 <CircleDot />
1767 New issue
1768 </Link>
1769 </DropdownMenuItem>
1770 {shell.repo.member && (
1771 <DropdownMenuItem asChild>
1772 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1773 <ListTree />
1774 Plan work
1775 </Link>
1776 </DropdownMenuItem>
1777 )}
1778 <DropdownMenuSeparator />
1779 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1780 )}
1781 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1782 <Link to="/?agent=new">
1783 <Sparkles />
1784 Put an agent on it
1785 </Link>
1786 </DropdownMenuItem>
1787 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1788 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1789 <Box />
1790 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1791 </Link>
1792 </DropdownMenuItem>
1793 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1794 <Link to="/new/github">
1795 <GitBranch />
1796 Import from GitHub
1797 </Link>
1798 </DropdownMenuItem>
1799 <DropdownMenuSeparator />
1800 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1801 <Link to="/workspaces/new">
1802 <Users />
1803 New workspace
1804 </Link>
1805 </DropdownMenuItem>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1806 {/* A team in the workspace the sidebar is about. */}
1807 {shell.workspace && (
1808 <DropdownMenuItem asChild>
1809 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1810 <UsersRound />
1811 New team
1812 </Link>
1813 </DropdownMenuItem>
1814 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1815 </DropdownMenuContent>
1816 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1817 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1818 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1819 </div>
1820 </header>
1821 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1822 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'1823 {children}
1824 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1825 </div>
Search across all of g1t, Explore, and a command palette1826 <CommandPalette
1827 open={palette}
1828 onOpenChange={setPalette}
1829 commands={commands}
1830 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1831 />
Agents as a team: lifecycle, merge queue, billing and a new shell1832 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1833 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell1834 );
1835}

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