Skip to content

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

1,831 lines74,957 bytesCodeBlame

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

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";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit11import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height13import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit14import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents15import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell16import {
17 DropdownMenu,
18 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas19 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell20 DropdownMenuItem,
21 DropdownMenuLabel,
22 DropdownMenuSeparator,
23 DropdownMenuTrigger,
24} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales25import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26import { 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 put27import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas28import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
29import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put30import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas32import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar33import { 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 quotas34import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell35
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put36/**
37 * What the sidebar needs, worked out by the root loader. For a visitor who
38 * is not signed in there is no workspace, no projects and no usage: only
39 * the project being looked at, if they can see it.
40 */
Agents as a team: lifecycle, merge queue, billing and a new shell41export type ShellData = {
42 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control43 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar44 /** All its projects, by name, for the palette: `name` is the slug in their address. */
45 repos: ShortcutProject[];
46 /** The person's pinned projects in it, in their order (lib/pins.ts). */
47 pinned?: ShortcutProject[];
48 /** What they opened there last, latest first, leaving out the pinned. */
49 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50 /** Repositories shared with them in workspaces they do not belong to. */
51 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page52 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell53 repo: {
54 namespace: string;
55 name: string;
56 member: boolean;
57 issues: number;
58 pulls: number;
59 } | null;
Billing accounts, terms and enterprises; g1t is no longer free60 /** Where the workspace stands against its usage limit, if billing is on. */
61 limit: {
62 exposureMicros: number;
63 ceilingMicros: number | null;
64 state: "ok" | "warning" | "stopped";
65 comped: boolean;
66 } | null;
Free while g1t is being built out; agents can check out their own forks67 /** Whether g1t charges nothing for now, while it is being built out. */
68 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request69 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy70 /** This month's usage: charged, or at cost while g1t is free. */
71 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
73 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 all74 /** What is unread in their inbox, for the bell. Absent for a visitor. */
75 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell76};
77
78function SidebarLink({
79 to,
80 icon,
81 end,
82 count,
83 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell85 children,
86}: {
87 to: string;
88 icon: ReactNode;
89 end?: boolean;
90 count?: number;
Projects: what a workspace builds and runs, first on every page91 /** Other path prefixes under which this link is the current one. */
92 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93 /** It opens a list of its own: a chevron says so, always or on hover. */
94 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell95 children: ReactNode;
96}) {
97 const { pathname } = useLocation();
98 return (
99 <NavLink
100 to={to}
101 end={end}
102 prefetch="intent"
103 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page104 const current =
105 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark106 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 shell107 current
108 ? "bg-raised font-medium text-fg"
109 : isPending
110 ? "bg-raised/60 text-fg"
111 : "text-muted hover:bg-raised/60 hover:text-fg"
112 }`;
113 }}
114 >
115 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
116 <span className="min-w-0 grow truncate">{children}</span>
117 {count != null && count > 0 && (
118 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
119 {count}
120 </span>
121 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look122 {drill && (
123 <ChevronRight
124 size={14}
125 aria-hidden="true"
126 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
127 />
128 )}
Agents as a team: lifecycle, merge queue, billing and a new shell129 </NavLink>
130 );
131}
132
Pull requests say where they are live; the project menu shows what is coming133/**
134 * Something a project will have, shown where it will live so people can
135 * see where g1t is going. Not a link: nothing is behind it yet.
136 */
137function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
138 return (
139 <div
140 title={about}
141 aria-disabled="true"
142 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
143 >
144 <span className="shrink-0 opacity-70">{icon}</span>
145 <span className="grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit146 <SoonPill />
Pull requests say where they are live; the project menu shows what is coming147 </div>
148 );
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 (
170 <NavLink
171 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner172 title={about}
The project menu opens by section, and the workspace has its own panel173 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>
The project menu opens by section, and the workspace has its own panel185 );
186}
187
A flat project menu with Code first, views as tabs, and Stripe for every owner188/** Icons for what spans projects. */
189const WORKSPACE_ICONS: Record<string, ReactNode> = {
190 board: <KanbanSquare size={15} />,
191 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member192 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner193 packages: <Package size={15} />,
194 fleet: <Bot size={15} />,
195};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales196
A flat project menu with Code first, views as tabs, and Stripe for every owner197/** The Soon pages of a section, as paths, so its link is current on them. */
198function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
199 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales200}
201
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look202function SidebarGroup({
203 title,
204 action,
205 className = "",
206 children,
207}: {
208 title: string;
209 action?: ReactNode;
210 className?: string;
211 children: ReactNode;
212}) {
Agents as a team: lifecycle, merge queue, billing and a new shell213 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look214 <section className={className}>
A Vercel-like sidebar and a new mark215 <div className="mb-1 flex h-6 items-center justify-between px-2">
216 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell217 {action}
218 </div>
219 <div className="space-y-px">{children}</div>
220 </section>
221 );
222}
223
Workspace names and icons, and a component kit for every control224/** What a workspace is called where people read it: its display name, else its slug. */
225function displayName(membership: Membership): string {
226 return membership.name?.trim() || membership.slug;
227}
228
Agents as a team: lifecycle, merge queue, billing and a new shell229function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control230 const workspace = shell.workspace;
231 const current = workspace?.slug;
232 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell233 return (
234 <DropdownMenu>
Workspace names and icons, and a component kit for every control235 {/* The name goes to the workspace; only the arrows switch it. The
236 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark237 <Link
238 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents239 prefetch="intent"
Workspace names and icons, and a component kit for every control240 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look241 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 mark242 >
Workspace names and icons, and a component kit for every control243 {workspace ? (
244 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell245 ) : (
Workspace names and icons, and a component kit for every control246 <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 shell247 )}
Workspace names and icons, and a component kit for every control248 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark249 </Link>
250 <DropdownMenuTrigger
251 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit252 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 mark253 >
254 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell255 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control256 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar257 {workspace && (
258 <>
259 {/* The workspace's own page: its projects, packages and people. */}
260 <DropdownMenuItem asChild>
261 <Link to={`/${workspace.slug}`}>
262 <LayoutGrid />
263 Workspace overview
264 </Link>
265 </DropdownMenuItem>
266 <DropdownMenuItem asChild>
267 <Link to={`/${workspace.slug}/-/projects`}>
268 <Box />
269 All projects
270 </Link>
271 </DropdownMenuItem>
272 <DropdownMenuSeparator />
273 </>
274 )}
Agents as a team: lifecycle, merge queue, billing and a new shell275 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
276 {(user.workspaces ?? []).map((membership) => (
277 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control278 <Link to={`/${membership.slug}`} title={displayName(membership)}>
279 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
280 <span className="flex min-w-0 grow flex-col leading-tight">
281 <span className="truncate">{displayName(membership)}</span>
282 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
283 </span>
284 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
285 {membership.slug === current ? (
286 <Check className="shrink-0 text-accent" />
287 ) : (
288 <span className="size-4 shrink-0" aria-hidden="true" />
289 )}
Agents as a team: lifecycle, merge queue, billing and a new shell290 </Link>
291 </DropdownMenuItem>
292 ))}
293 <DropdownMenuSeparator />
294 <DropdownMenuItem asChild>
295 <Link to="/workspaces/new">
296 <Plus />
297 New workspace
298 </Link>
299 </DropdownMenuItem>
300 </DropdownMenuContent>
301 </DropdownMenu>
A Vercel-like sidebar and a new mark302 );
303}
304
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas305/**
306 * The fine print, as one quiet row at the foot of the account menu and the
307 * visitor's panel: each link a full-height target, left-aligned. In the
308 * menu each is a menu item, so the arrow keys reach it.
309 */
310const LEGAL_LINKS: [string, string][] = [
311 ["Policies", "/policies"],
312 ["Privacy", "/policies/privacy"],
313 ["Security", "/security"],
314];
315const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
316
317function MenuLegalRow() {
318 return (
319 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
320 {LEGAL_LINKS.map(([label, to], index) => (
321 <span key={to} className="flex items-center gap-0.5">
322 {index > 0 && (
323 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
324 ·
325 </span>
326 )}
327 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
328 <Link to={to}>{label}</Link>
329 </DropdownMenuItem>
330 </span>
331 ))}
332 </div>
333 );
334}
335
336/** Status's row: the live dot and two or three words, fetched when the menu opens. */
337function StatusSummary({ open }: { open: boolean }) {
338 const status = useSiteStatus(open);
339 const words = statusTitle(status);
340 return (
341 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
342 {/* The words say it; the dot's own words for screen readers would repeat them. */}
343 <span aria-hidden="true" className="flex">
344 <StatusDot state={status?.overall.state ?? null} />
345 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents346 {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 quotas347 </span>
348 );
349}
350
351/** A menu row: an icon, words, and whatever sits at its end. */
352const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
353
A Vercel-like sidebar and a new mark354function AccountMenu({ user }: { user: User }) {
355 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas356 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents357 // Name, primary address and invites left: asked for once, as soon as the
358 // 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 quotas359 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents360 const prefetch = () => {
361 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
362 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas363 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents364 if (open) prefetch();
365 // eslint-disable-next-line react-hooks/exhaustive-deps
366 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas367 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents368 // Still on its way: shapes where the words will be, the same size.
369 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 quotas370 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark371 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas372 <DropdownMenu open={open} onOpenChange={setOpen}>
373 <DropdownMenuTrigger
374 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents375 onPointerEnter={prefetch}
376 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas377 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"
378 >
Workspace names and icons, and a component kit for every control379 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark380 <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 quotas381 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark382 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas383 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
384 {/* Who is signed in, and a way to their profile. */}
385 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
386 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
387 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents388 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
389 {loading ? (
390 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
391 ) : (
392 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
393 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas394 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents395 {loading ? (
396 <Skeleton className="mt-1 h-3 w-40" />
397 ) : (
398 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
399 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas400 </span>
A Vercel-like sidebar and a new mark401 </Link>
402 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas403 <DropdownMenuSeparator className="my-1.5" />
404 <DropdownMenuGroup>
405 <DropdownMenuItem asChild className={MENU_ROW}>
406 <Link to={profile}>
407 <CircleUserRound />
408 Your profile
409 </Link>
410 </DropdownMenuItem>
411 <DropdownMenuItem asChild className={MENU_ROW}>
412 <Link to={FIRST_SETTINGS_PAGE}>
413 <Settings />
414 Your settings
415 </Link>
416 </DropdownMenuItem>
417 <DropdownMenuItem asChild className={MENU_ROW}>
418 <Link to="/settings/invites">
419 <Ticket />
420 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents421 {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 quotas422 {me?.invites_left != null && (
423 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
424 {me.invites_left} left
425 </span>
426 )}
427 </Link>
428 </DropdownMenuItem>
429 </DropdownMenuGroup>
430 <DropdownMenuSeparator className="my-1.5" />
431 <DropdownMenuGroup>
432 <DropdownMenuItem asChild className={MENU_ROW}>
433 <a href="https://docs.g1t.sh/">
434 <BookOpen />
435 Documentation
436 </a>
437 </DropdownMenuItem>
438 <DropdownMenuItem asChild className={MENU_ROW}>
439 <Link to="/support">
440 <LifeBuoy />
441 Support
442 </Link>
443 </DropdownMenuItem>
444 <DropdownMenuItem asChild className={MENU_ROW}>
445 <a href={STATUS_URL}>
446 <Activity />
447 Status
448 <StatusSummary open={open} />
449 </a>
450 </DropdownMenuItem>
451 </DropdownMenuGroup>
452 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark453 {/* Submitted from here: the menu closes on select, and a button
454 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 quotas455 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark456 <LogOut />
457 Sign out
458 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas459 <DropdownMenuSeparator className="my-1.5" />
460 <MenuLegalRow />
A Vercel-like sidebar and a new mark461 </DropdownMenuContent>
462 </DropdownMenu>
463 );
464}
465
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look466/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar467 * A workspace's settings pages, which the sidebar drills into: how it is
468 * set up and connected (guardrails, secrets, runners, integrations,
469 * webhooks), what it pays, its repositories, tokens and record. The main
470 * list keeps the places work happens and who belongs; every member can
471 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look472 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar473const SETTINGS_PAGE =
474 /^\/([^/]+)\/-\/(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 look475/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page476const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings477
Polish: phones, copy boxes, the plan page, the landing page, a real glide478/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look479 * The sidebar's lists are a stack, the way a phone pushes a screen: one
480 * drilled into slides in from the right over the full width, while the one
481 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide482 * ease-out, the fade quicker than the move so the two never blur together.
483 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide484 */
Sidebar: no settings flash on the way into a repository, and a better slide485const LAYER =
Sidebar: the panels really slide486 "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 look487/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide488const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings489
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look490/** One list in the stack: what it is, and its rows. */
491type Level = { key: string; node: ReactNode };
492
493/** True once the page has hydrated, so what the server drew never animates in. */
494function useHydrated(): boolean {
495 const [hydrated, setHydrated] = useState(false);
496 useEffect(() => setHydrated(true), []);
497 return hydrated;
498}
499
500/**
501 * Drilling in and out of the sidebar's lists. `trail` is the way from the
502 * main list to the one shown, worked out from the address, so a link
503 * straight to a settings page opens the sidebar already drilled in, and
504 * leaving it comes back out. Lists deeper than the one shown stay in
505 * place, off to the right, so going back slides them away rather than
506 * dropping them. Moving to a list at the same depth (a workspace's settings
507 * to a project) fades the new one in where it is.
508 */
509function Drill({ trail }: { trail: Level[] }) {
510 const depth = trail.length - 1;
511 const hydrated = useHydrated();
512 const stack = useRef<Level[]>(trail);
513 const before = stack.current;
514 const within = trail.every((level, index) => before[index]?.key === level.key);
515 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
516 const levels = stack.current;
517 // What was showing last time, to tell drilling in from moving across.
518 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
519 useEffect(() => {
520 shown.current = { depth, keys: levels.map((level) => level.key) };
521 });
A catalogue of model providers, and settings that feel like settings522 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look523 <div className="relative min-h-0 grow overflow-hidden">
524 {levels.map((level, index) => {
525 const current = index === depth;
526 const place =
527 index < depth
528 ? "pointer-events-none -translate-x-1/4 opacity-0"
529 : current
530 ? "translate-x-0 opacity-100"
531 : "pointer-events-none translate-x-full opacity-0";
532 // A list that was not there before arrives: from the right when
533 // drilling in, in place when moving across.
534 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
535 const enter = arriving
536 ? depth > shown.current.depth
537 ? "starting:translate-x-full starting:opacity-0"
538 : "starting:opacity-0"
539 : "";
540 return (
541 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
542 {level.node}
543 </div>
544 );
545 })}
546 </div>
547 );
548}
549
550/**
551 * The row at the top of a list drilled into: the list's name, which goes
552 * back out, and what it belongs to, faint at the end.
553 */
554function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
555 return (
556 <Link
557 to={to}
558 prefetch="intent"
559 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 settings560 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look561 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
562 <span className="min-w-0 shrink-0 truncate">{label}</span>
563 {context && (
564 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
565 )}
566 </Link>
567 );
568}
569
570/** A thin rule between groups of a list. */
571function Rule() {
572 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
573}
574
Navigation: the workspace's own page with tabs, pins and recent in the sidebar575/** A quiet heading inside a group: Pinned, Recent. */
576function SidebarSubhead({ children }: { children: ReactNode }) {
577 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
578}
579
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look580/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar581 * The workspace's projects as the sidebar keeps them, however many there
582 * are: the person's pins in their order, what they opened last, and the
583 * way to all of them. Pins move by dragging, or with Alt and the arrow
584 * keys; the projects service keeps the order.
585 */
586function SidebarProjects({ slug, shell }: { slug: string; shell: ShellData }) {
587 const reorder = useFetcher({ key: `pins:${slug}` });
588 const saved = shell.pinned ?? [];
589 // While a new order is on its way, it shows as made.
590 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
591 const pinned = asked
592 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
593 : saved;
594 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
595 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
596 const [dragging, setDragging] = useState<number | null>(null);
597 const move = (from: number, by: number) => {
598 const next = movedPin(pinned, from, by);
599 if (!next) return;
600 const form = new FormData();
601 form.set("intent", "reorder");
602 for (const project of next) form.append("slug", project.name);
603 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
604 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar605 // Each row pins or unpins in place: the pin shows on hover or focus, and
606 // stays shown on a pinned row's hover so it reads as "unpin".
607 const row = (project: ShortcutProject, isPinned: boolean) => (
608 <div className="group/row relative">
609 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
610 <span className="block truncate pr-6">{project.title ?? project.name}</span>
611 </SidebarLink>
612 <PinButton
613 workspace={slug}
614 slug={project.name}
615 name={project.title ?? project.name}
616 pinned={isPinned}
617 small
618 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"
619 />
620 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar621 );
622 return (
623 <SidebarGroup
624 title="Projects"
625 action={
626 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
627 <Plus size={13} />
628 </Link>
629 }
630 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar631 {/* Only there once something is pinned: each row below has its own pin. */}
632 {pinned.length > 0 && (
633 <>
634 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar635 <ul aria-label="Pinned projects" className="space-y-px">
636 {pinned.map((project, index) => (
637 <li
638 key={project.name}
639 draggable={pinned.length > 1}
640 onDragStart={(event) => {
641 setDragging(index);
642 event.dataTransfer.effectAllowed = "move";
643 }}
644 onDragOver={(event) => {
645 if (dragging != null) event.preventDefault();
646 }}
647 onDrop={(event) => {
648 event.preventDefault();
649 if (dragging != null && dragging !== index) move(dragging, index - dragging);
650 setDragging(null);
651 }}
652 onDragEnd={() => setDragging(null)}
653 onKeyDown={(event) => {
654 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
655 event.preventDefault();
656 move(index, event.key === "ArrowUp" ? -1 : 1);
657 }}
658 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
659 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
660 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar661 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar662 {pinned.length > 1 && (
663 <GripVertical
664 size={12}
665 aria-hidden="true"
666 className="pointer-events-none absolute top-1/2 -left-1.5 -translate-y-1/2 text-faint opacity-0 transition-opacity group-hover/pin:opacity-100"
667 />
668 )}
669 </li>
670 ))}
671 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar672 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar673 )}
674 {recent.length > 0 && (
675 <>
676 <SidebarSubhead>Recent</SidebarSubhead>
677 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar678 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar679 ))}
680 </>
681 )}
682 <div className="pt-1">
683 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length}>
684 All projects
685 </SidebarLink>
686 </div>
687 </SidebarGroup>
688 );
689}
690
691/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look692 * The main list: where you go, the projects, what the workspace builds and
693 * runs with across them, then its usage, support and settings.
694 */
695function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
696 const ws = shell.workspace;
697 // A visitor browses: no workspace, no projects of their own.
698 if (!user) {
699 return (
700 <nav aria-label="g1t" className={PANEL}>
701 <div className="mt-3 space-y-px">
702 {VISITOR_LINKS.map((link) => (
703 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
704 {link.label}
705 </SidebarLink>
706 ))}
707 </div>
708 </nav>
709 );
710 }
711 return (
712 <nav aria-label="g1t" className={PANEL}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar713 {/* Everything here is the workspace the switcher names: its home first.
714 The workspace's own page is the switcher's; Explore, all of g1t,
715 is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look716 <div className="mt-3 space-y-px">
717 <SidebarLink to="/" end icon={<House size={15} />}>
718 Mission control
719 </SidebarLink>
720 </div>
721
722 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar723 {ws ? (
724 <SidebarProjects slug={ws.slug} shell={shell} />
725 ) : (
726 <SidebarGroup title="Projects">
727 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
728 </SidebarGroup>
729 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look730 {(shell.shared ?? []).length > 0 && (
731 <SidebarGroup title="Shared with you" className="mt-3">
732 {(shell.shared ?? []).map((repo) => (
733 <SidebarLink
734 key={`${repo.namespace}/${repo.name}`}
735 to={`/${repo.namespace}/${repo.name}`}
736 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
737 drill="hover"
738 >
739 <span className="font-mono text-faint">{repo.namespace}/</span>
740 {repo.name}
741 </SidebarLink>
742 ))}
743 </SidebarGroup>
744 )}
745
746 {ws && (
747 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member748 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar749 <SidebarGroup title="Workspace">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look750 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
751 Agent fleet
752 </SidebarLink>
753 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
754 Context
755 </SidebarLink>
756 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
757 Memory
758 </SidebarLink>
759 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
760 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents761 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member762 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
763 Packages
764 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar765 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look766 <SidebarSoonLink
767 key={item.key}
768 to={`/${ws.slug}/-/soon/${item.key}`}
769 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
770 about={item.summary}
771 >
772 {item.title === "Board" ? "Boards" : item.title}
773 </SidebarSoonLink>
774 ))}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar775 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look776
777 <Rule />
778 <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 bar779 {/* Who belongs, for every member to see; owners invite and manage there. */}
780 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar781 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar782 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar783 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams}>
784 Teams
785 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look786 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
787 Usage
788 </SidebarLink>
789 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
790 Support
791 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar792 {/* 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 look793 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member794 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 look795 icon={<Settings size={15} />}
796 drill
797 >
798 Settings
799 </SidebarLink>
800 </div>
801 </>
802 )}
803 </nav>
804 );
805}
806
807/** A workspace's settings, drilled into from Settings in the main list. */
808function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
809 return (
810 <nav aria-label="Workspace settings" className={PANEL}>
811 <BackRow to={`/${slug}`} label="Settings" context={slug} />
812 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings813 {owner && (
814 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
815 General
816 </SidebarLink>
817 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look818 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
819 Repositories
820 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments821 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
822 Access tokens
823 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look824 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar825 <SidebarGroup title="Agents and runs" className="mt-3">
826 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
827 Guardrails
828 </SidebarLink>
829 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
830 Secrets and variables
831 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it832 {owner && (
833 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
834 Runners
835 </SidebarLink>
836 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar837 </SidebarGroup>
838 <SidebarGroup title="Connections" className="mt-3">
839 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
840 Integrations
841 </SidebarLink>
842 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
843 Webhooks
844 </SidebarLink>
845 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look846 <Rule />
847 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings848 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments849 Billing and plans
A catalogue of model providers, and settings that feel like settings850 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API851 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
852 Audit log
853 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look854 </div>
A catalogue of model providers, and settings that feel like settings855 </nav>
856 );
857}
858
Sidebar: no settings flash on the way into a repository, and a better slide859/**
860 * What the repository menu needs. Known from the address before the page's
861 * data arrives, so the menu is right from the first frame; the counts fill
862 * in once it does.
863 */
864type MenuRepo = {
865 namespace: string;
866 name: string;
867 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look868 /** Whether they see its settings; from the repository's page once it loads. */
869 settings?: boolean;
870 /** What they may do there, once the repository's page loads. */
871 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide872 issues?: number;
873 pulls?: number;
874};
875
876function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
877 return (
878 a != null &&
879 b != null &&
880 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
881 a.name.toLowerCase() === b.name.toLowerCase()
882 );
883}
A repository has its own sidebar, as settings do884
885/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look886 * A project's own list, drilled into while you are in it: everything about
887 * the project, running and its code, and nothing else, with the way back
888 * out to all of them.
A repository has its own sidebar, as settings do889 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put890function RepoMenu({
891 repo,
892 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look893 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put894}: {
895 repo: MenuRepo;
896 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look897 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
898 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put899}) {
A repository has its own sidebar, as settings do900 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 put901 // 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 look902 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do903 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look904 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
905 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner906 <NavLink
A repository has its own sidebar, as settings do907 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner908 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents909 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner910 title="Overview"
911 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look912 `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 owner913 }
A repository has its own sidebar, as settings do914 >
915 <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 page916 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do917 </span>
918 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
919 <span className="text-faint">{repo.namespace}/</span>
920 <span className="font-semibold text-fg">{repo.name}</span>
921 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner922 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look923 {/* A project's pages, in the order people use them. A page with more
924 than one view shows them as tabs across its top. */}
925 <Rule />
926 <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 request927 <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 owner928 Code
929 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit930 <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 owner931 Issues
932 </SidebarLink>
933 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
934 Pull requests
935 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains936 <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 owner937 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains938 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner939 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
940 Workflows
Projects: what a workspace builds and runs, first on every page941 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look942 </div>
943 <Rule />
944 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put945 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner946 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
947 Deployments
948 </SidebarLink>
949 ) : null}
Observability gets its own page; tabs never overflow950 <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.">
951 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner952 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put953 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API954 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
955 Security
956 </SidebarLink>
957 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner958 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
959 Insights
960 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look961 </div>
962 {(repo.settings ?? shows.has("settings")) && (
963 <>
964 <Rule />
965 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner966 Settings
967 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look968 </>
969 )}
A repository has its own sidebar, as settings do970 </nav>
971 );
972}
973
Project settings move into the sidebar, grouped, with a heading per page974/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look975 * A project's settings, drilled into from its list: what is about running
976 * it, then its agents, then its code and who can reach it. The way back
977 * leads to the project.
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 look979function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page980 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look981 // Each page shows to the roles that can use it, once the role is known.
982 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
983 const running = shows("deployments");
984 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page985 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look986 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
987 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
988 {shows("") && (
989 <div className="mt-2 space-y-px">
990 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
991 General
992 </SidebarLink>
993 </div>
994 )}
995 {running && (
996 <>
997 {shows("") && <Rule />}
998 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
999 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1000 Deployments
1001 </SidebarLink>
1002 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1003 Domains
1004 </SidebarLink>
1005 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1006 Dependencies
1007 </SidebarLink>
1008 </div>
1009 </>
1010 )}
1011 {agents && (
1012 <>
1013 <Rule />
1014 <div className="space-y-px">
1015 {shows("agents") && (
1016 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1017 Agents
1018 </SidebarLink>
1019 )}
1020 {shows("guardrails") && (
1021 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1022 Guardrails
1023 </SidebarLink>
1024 )}
1025 </div>
1026 </>
1027 )}
1028 <Rule />
1029 <div className="space-y-px">
1030 {shows("repository") && (
1031 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1032 Repository
1033 </SidebarLink>
1034 )}
1035 {shows("access") && (
1036 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1037 Access
1038 </SidebarLink>
1039 )}
1040 {shows("branches") && (
1041 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1042 Branches and merging
1043 </SidebarLink>
1044 )}
1045 {shows("secrets") && (
1046 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1047 Secrets and variables
1048 </SidebarLink>
1049 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1050 {shows("runners") && (
1051 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1052 Runners
1053 </SidebarLink>
1054 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1055 {shows("webhooks") && (
1056 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1057 Webhooks
1058 </SidebarLink>
1059 )}
1060 </div>
Project settings move into the sidebar, grouped, with a heading per page1061 </nav>
1062 );
1063}
1064
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1065/** Your own settings, drilled into from Your settings: one page each. */
1066function AccountSettingsMenu({ username }: { username: string }) {
1067 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1068 <SidebarLink to={`/settings/${page}`} icon={icon}>
1069 {ACCOUNT_SETTINGS[page].title}
1070 </SidebarLink>
Account settings in the same shape as a workspace's1071 );
1072 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1073 <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 quotas1074 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1075 <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 quotas1076 {link("profile", <CircleUserRound size={15} />)}
1077 {link("emails", <Mail size={15} />)}
1078 {link("invites", <Ticket size={15} />)}
1079 </div>
1080 <Rule />
1081 <div className="space-y-px">
1082 {link("keys", <Fingerprint size={15} />)}
1083 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1084 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1085 <Rule />
1086 <div className="space-y-px">
1087 {link("github", <GithubMark className="size-[15px]" />)}
1088 {link("applications", <Plug size={15} />)}
1089 </div>
1090 <Rule />
1091 <div className="space-y-px">
1092 {link("security-log", <History size={15} />)}
1093 </div>
Account settings in the same shape as a workspace's1094 </nav>
1095 );
1096}
1097
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1098/**
1099 * Signing in and signing up, in place of the account for a visitor. Signing
1100 * in brings them back to the page they are on.
1101 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1102/** Help, status and the fine print under a visitor's sign-in buttons. */
1103function VisitorLinks() {
1104 const status = useSiteStatus();
1105 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1106 const dot = (
1107 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1108 ·
1109 </span>
1110 );
1111 return (
1112 // Two short rows, so a narrow sidebar never starts a line with a dot.
1113 <nav aria-label="About g1t" className="pt-0.5">
1114 <div className="flex items-center gap-0.5">
1115 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1116 <StatusDot state={status?.overall.state ?? null} />
1117 Status
1118 </a>
1119 {dot}
1120 <Link to="/support" className={quiet}>
1121 Support
1122 </Link>
1123 </div>
1124 <div className="flex items-center gap-0.5">
1125 {LEGAL_LINKS.map(([label, to], index) => (
1126 <span key={to} className="flex items-center gap-0.5">
1127 {index > 0 && dot}
1128 <Link to={to} className={quiet}>
1129 {label}
1130 </Link>
1131 </span>
1132 ))}
1133 </div>
1134 </nav>
1135 );
1136}
1137
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1138function VisitorPanel() {
1139 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1140 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1141 return (
1142 <div className="space-y-2">
1143 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1144 <div className="grid grid-cols-2 gap-2">
1145 <Link
1146 to={withNext("/login", pathname + search)}
1147 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"
1148 >
1149 <LogIn size={14} />
1150 Sign in
1151 </Link>
1152 <Link
1153 to={withNext("/register", pathname + search)}
1154 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1155 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1156 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1157 </Link>
1158 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1159 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1160 </div>
1161 );
1162}
1163
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1164function Sidebar({
1165 user,
1166 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1167 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1168 onFind,
1169 onClose,
1170}: {
1171 user: User | null;
1172 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1173 /** The page is a 404: the address names nothing the viewer can see. */
1174 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1175 onFind: () => void;
1176 /** In the sheet on a small screen: closing it, at the end of the top row. */
1177 onClose?: () => void;
1178}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1179 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1180 const { pathname } = useLocation();
1181 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1182 // Where the sidebar is drilled to follows the page being gone to, so it
1183 // moves as the link is followed, not once the page arrives.
1184 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1185 // On a 404, the address is not a place: no list is drilled into from it,
1186 // except the project's own when the project is real and visible and only
1187 // something inside it is missing (a file, a commit, an issue).
1188 const lost = missing && going == null;
1189 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1190 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1191 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1192 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1193 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1194 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 look1195 // An invitation to a repository is answered before its menu means anything.
1196 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1197 repoPath != null &&
1198 !reserved.has(repoPath[1]) &&
1199 repoPath[2] !== "-" &&
1200 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1201 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1202 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1203 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1204 // The repository the menu is for: the one loaded if it is the one being
1205 // gone to, else what the address says, at once.
1206 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 look1207 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1208 ? sameRepo(active, targetRepo)
1209 ? active
1210 : {
1211 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1212 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1213 }
1214 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1215 // The viewer's role there, from the repository's page once it has loaded.
1216 const page = useRouteLoaderData("routes/repo/layout") as
1217 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1218 | undefined;
1219 const menuRepo: MenuRepo | null =
1220 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1221 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1222 : guessed;
1223
1224 // The way from the main list to the one shown.
1225 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1226 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1227 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1228 } else if (ws && inSettings) {
1229 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1230 } else if (menuRepo) {
1231 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1232 // Out of a project: to its workspace's projects when they are yours.
1233 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1234 const back = !user
1235 ? { to: "/explore", label: "Explore" }
1236 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1237 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1238 : { to: "/", label: "Mission control" };
1239 trail.push({
1240 key,
1241 node: (
1242 <RepoMenu
1243 repo={menuRepo}
1244 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1245 back={back}
1246 />
1247 ),
1248 });
1249 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1250 }
1251
Agents as a team: lifecycle, merge queue, billing and a new shell1252 return (
1253 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1254 {/* 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 bar1255 <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 put1256 {user ? (
1257 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1258 {/* The 1 alone beside the workspace: a square hover the height of the
1259 switcher, the mark as tall as the workspace avatar. */}
1260 <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 bar1261 <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 put1262 </Link>
1263 <span className="shrink-0 text-line-strong" aria-hidden="true">
1264 /
1265 </span>
1266 <WorkspaceSwitcher user={user} shell={shell} />
1267 </>
1268 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1269 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1270 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1271 </Link>
1272 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1273 {onClose && (
1274 <button
1275 type="button"
1276 aria-label="Close menu"
1277 onClick={onClose}
1278 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1279 >
1280 <X size={16} />
1281 </button>
1282 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1283 </div>
A Vercel-like sidebar and a new mark1284 <div className="px-2 pt-3">
1285 <button
1286 type="button"
1287 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1288 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 mark1289 >
1290 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1291 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1292 <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 mark1293 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1294 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1295 <Drill trail={trail} />
1296 {/* The account, or signing in, as one row at the very bottom. */}
1297 <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 put1298 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1299 </div>
1300 </div>
1301 );
1302}
1303
1304/** Words for the sections a path can end in. */
1305const SECTIONS: Record<string, string> = {
1306 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1307 milestones: "Milestones",
1308 labels: "Labels",
A Vercel-like sidebar and a new mark1309 pulls: "Pull requests",
1310 queue: "Merge queue",
1311 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1312 plans: "Outcomes",
1313 actions: "Workflows",
1314 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1315 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1316 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1317 access: "Access",
1318 invitations: "Invitation",
1319 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1320 branches: "Branches",
1321 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1322 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1323 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1324 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1325 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1326 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1327 people: "People",
1328 projects: "Projects",
1329 teams: "Teams",
A Vercel-like sidebar and a new mark1330 tokens: "Access tokens",
1331 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1332 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1333 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1334 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1335 domains: "Domains",
1336 guardrails: "Guardrails",
1337 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1338 tree: "Files",
1339 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1340 agents: "Agents",
1341 context: "Context",
1342 memory: "Memory",
1343 security: "Security",
1344 packages: "Packages",
1345 runners: "Runners",
1346 workflows: "Workflows",
1347 observability: "Observability",
1348 insights: "Insights",
1349 sessions: "Sessions",
A Vercel-like sidebar and a new mark1350};
1351
1352/** 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 quotas1353function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1354 // A 404 names nothing from the address, unless the project is real and
1355 // visible and only something inside it is missing.
1356 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1357 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1358 return <span className="text-sm font-medium">Not found</span>;
1359 }
A Vercel-like sidebar and a new mark1360 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1361 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1362 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 quotas1363 // Your settings: Settings / Emails.
1364 if (parts[0] === "settings") {
1365 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1366 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 quotas1367 }
A Vercel-like sidebar and a new mark1368 if (reserved.includes(parts[0]!)) {
1369 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1370 settings: "Account",
A Vercel-like sidebar and a new mark1371 explore: "Explore",
Projects live: fixes from walking them end to end1372 new: "New project",
A Vercel-like sidebar and a new mark1373 search: "Search",
1374 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1375 policies: "Policies",
1376 security: "Security",
1377 support: "Support",
1378 status: "Status",
1379 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1380 inbox: "Inbox",
A Vercel-like sidebar and a new mark1381 };
1382 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1383 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1384 // A person's profile, by their handle.
1385 if (parts[0] === "u" && parts[1]) {
1386 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1387 }
A Vercel-like sidebar and a new mark1388 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1389 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1390 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1391 const page = `/${owner}/-/${third}`;
1392 if (third && SETTINGS_PAGE.test(page)) {
1393 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1394 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1395 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1396 } else if (second) {
1397 const repo = `/${owner}/${second}`;
1398 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1399 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1400 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 mark1401 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 deployments1402 else if (third && SECTIONS[third]) {
1403 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1404 // A settings page names which one: Settings / Secrets and variables.
1405 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1406 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 deployments1407 }
1408 }
A Vercel-like sidebar and a new mark1409 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1410 return <Trail trail={trail} />;
1411}
1412
1413/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1414type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1415
1416/**
1417 * The trail of links. On a phone there is room for one: the page itself,
1418 * with a way back to where it sits; the whole trail from a wider screen up.
1419 */
1420function Trail({ trail }: { trail: Crumb[] }) {
1421 const up = trail.length > 1 ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1422 return (
1423 <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 audit1424 {up && (
1425 <Link
1426 to={up.to}
1427 prefetch="intent"
1428 aria-label={`Back to ${up.label}`}
1429 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"
1430 >
1431 <ChevronLeft size={16} />
1432 </Link>
1433 )}
1434 {trail.map((crumb, index) => {
1435 const last = index === trail.length - 1;
1436 return (
1437 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1438 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1439 <Link
1440 to={crumb.to}
1441 prefetch="intent"
1442 aria-current={last ? "page" : undefined}
1443 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1444 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1445 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1446 >
1447 {last && crumb.short ? (
1448 <>
1449 <span className="sm:hidden">{crumb.short}</span>
1450 <span className="hidden sm:inline">{crumb.label}</span>
1451 </>
1452 ) : (
1453 crumb.label
1454 )}
1455 </Link>
1456 </span>
1457 );
1458 })}
A Vercel-like sidebar and a new mark1459 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1460 );
1461}
1462
Search across all of g1t, Explore, and a command palette1463type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1464
1465/** 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 look1466function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1467 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1468 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1469 { 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 step1470 ...(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-agent1471 ? [{ 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 step1472 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1473 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1474 { 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 all1475 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1476 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1477 { 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 quotas1478 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1479 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1480 .filter((page) => page !== "profile")
1481 .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 shell1482 ];
1483 const repo = shell.repo;
1484 if (repo) {
1485 const base = `/${repo.namespace}/${repo.name}`;
1486 const name = `${repo.namespace}/${repo.name}`;
1487 commands.unshift(
Projects: what a workspace builds and runs, first on every page1488 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1489 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1490 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1491 { 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 step1492 ...(repo.member
1493 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1494 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1495 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1496 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1497 ...(repo.member
1498 ? [
1499 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1500 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1501 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1502 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1503 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1504 ]
1505 : []),
1506 );
1507 }
1508 for (const membership of user.workspaces ?? []) {
1509 commands.push(
Workspace names and icons, and a component kit for every control1510 { 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 sidebar1511 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1512 { 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 request1513 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1514 { 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 settings1515 { 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 look1516 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1517 );
1518 }
1519 for (const listed of shell.repos) {
1520 commands.push({
Projects: what a workspace builds and runs, first on every page1521 label: listed.title ?? listed.name,
1522 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1523 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1524 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1525 });
1526 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1527 // "Put an agent on …": one per project, after the projects themselves.
1528 for (const listed of shell.repos) {
1529 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1530 commands.push({
1531 label: `Put an agent on ${listed.title ?? listed.name}`,
1532 hint: `${listed.namespace}/${listed.name}`,
1533 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1534 icon: <Sparkles size={15} />,
1535 });
1536 }
Agents as a team: lifecycle, merge queue, billing and a new shell1537 return commands;
1538}
1539
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1540/** 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 look1541function 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 put1542 const commands: Command[] = [];
1543 const repo = shell.repo;
1544 if (repo) {
1545 const base = `/${repo.namespace}/${repo.name}`;
1546 const name = `${repo.namespace}/${repo.name}`;
1547 commands.push(
1548 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1549 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1550 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1551 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1552 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1553 );
1554 }
1555 commands.push(
1556 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1557 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1558 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1559 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1560 { 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 look1561 { 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 put1562 );
1563 return commands;
1564}
1565
Agents as a team: lifecycle, merge queue, billing and a new shell1566/**
Merge branch 'worktree-agent-aca4790ea223aa235'1567 * The page while the next one loads: after a moment it dims, under the
1568 * bar below, so it is plainly on its way out rather than frozen, and the
1569 * next page fades up from there. Sending a form does not dim it: the
1570 * form's button says it is working. Spread on the element holding the page.
1571 */
1572export function useLeaving() {
1573 const navigation = useNavigation();
1574 const method = navigation.formMethod?.toUpperCase();
1575 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1576 return {
1577 "aria-busy": leaving || undefined,
1578 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1579 };
1580}
1581
1582/**
Agents as a team: lifecycle, merge queue, billing and a new shell1583 * A bar across the top of the page while the next one loads, as GitHub
1584 * has: it appears at once, creeps towards the end while waiting, then fills
1585 * and fades when the page arrives.
1586 */
1587export function Progress() {
1588 const navigation = useNavigation();
1589 const busy = navigation.state !== "idle";
1590 const [width, setWidth] = useState(0);
1591 const [visible, setVisible] = useState(false);
1592 useEffect(() => {
1593 if (busy) {
1594 setVisible(true);
1595 setWidth(12);
1596 // Slows as it goes, never quite arriving until the page does.
1597 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1598 return () => clearInterval(timer);
1599 }
1600 setWidth((now) => (now > 0 ? 100 : 0));
1601 const fade = setTimeout(() => setVisible(false), 250);
1602 const reset = setTimeout(() => setWidth(0), 550);
1603 return () => {
1604 clearTimeout(fade);
1605 clearTimeout(reset);
1606 };
1607 }, [busy]);
1608 return (
1609 <div
1610 aria-hidden="true"
1611 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1612 visible ? "opacity-100" : "opacity-0"
1613 }`}
1614 >
1615 <div
1616 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1617 style={{ width: `${width}%` }}
1618 />
1619 </div>
1620 );
1621}
1622
1623/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1624 * The app: a sidebar with the workspace, its repositories and the sections
1625 * of the one being looked at; a slim bar with search and the account; and
1626 * the page. A visitor who is not signed in gets the same frame, with
1627 * Explore and Search in place of the workspace, and signing in in place of
1628 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1629 */
1630export function AppShell({
1631 user,
1632 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1633 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1634 banner,
1635 children,
1636}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1637 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1638 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1639 /** The page is a 404 (see Sidebar). */
1640 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1641 banner?: ReactNode;
1642 children: ReactNode;
1643}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1644 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1645 const [drawer, setDrawer] = useState(false);
1646 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1647 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1648 const signUpLabel = useSignUpCopy().primary;
1649 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1650
1651 // A new page closes the drawer on small screens.
1652 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1653 // Mission control's code, fetched while the browser is idle, so going
1654 // home never waits on it (routes/home.tsx).
1655 useEffect(() => {
1656 if (!user) return;
1657 const load = () => void import("./mission-control").catch(() => undefined);
1658 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1659 if (idle) idle(load);
1660 else setTimeout(load, 1500);
1661 }, [user]);
Search across all of g1t, Explore, and a command palette1662 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1663 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1664
1665 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1666 // The phone's menu is a sheet: a dialog that holds focus, closes on
1667 // Escape or a tap outside, and gives focus back to the menu button.
1668 <Sheet open={drawer} onOpenChange={setDrawer}>
Agents as a team: lifecycle, merge queue, billing and a new shell1669 <div className="min-h-screen">
1670 <Progress />
1671 <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 quotas1672 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1673 </aside>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1674 <SheetContent side="left" showClose={false} aria-describedby={undefined} className="w-72 max-w-[85vw] sm:max-w-72 lg:hidden">
1675 <SheetTitle className="sr-only">Menu</SheetTitle>
1676 <Sidebar
1677 user={user}
1678 shell={shell}
1679 missing={missing}
1680 onFind={() => {
1681 setDrawer(false);
1682 setPalette(true);
1683 }}
1684 onClose={() => setDrawer(false)}
1685 />
1686 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell1687
Polish: phones, copy boxes, the plan page, the landing page, a real glide1688 <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 bar1689 <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 audit1690 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell1691 aria-label="Open menu"
1692 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1693 >
1694 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1695 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1696 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1697 <div className="ml-auto flex items-center gap-1.5">
1698 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1699 <button
1700 type="button"
1701 aria-label="Search or jump to"
1702 onClick={() => setPalette(true)}
1703 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1704 >
1705 <Search size={16} />
1706 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1707 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1708 <NavLink
1709 to="/explore"
1710 prefetch="intent"
1711 aria-label="Explore"
1712 className={({ isActive }) =>
1713 `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"}`
1714 }
1715 >
1716 <Compass size={16} className="sm:hidden" />
1717 <span className="hidden sm:inline">Explore</span>
1718 </NavLink>
A Vercel-like sidebar and a new mark1719 <a
1720 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1721 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 mark1722 >
1723 Docs
1724 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1725 {user && (
1726 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1727 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1728 <InboxBell counts={shell.inbox ?? null} />
1729 </>
1730 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1731 {!user ? (
1732 // The sidebar has these too, but on a phone it is folded away.
1733 <Link
1734 to={withNext("/login", here)}
1735 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"
1736 >
1737 Sign in
1738 </Link>
1739 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1740 <>
1741 {/* Room between what comes in (the inbox) and what you make. */}
1742 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1743 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1744 <Tooltip>
1745 <TooltipTrigger asChild>
1746 <DropdownMenuTrigger
1747 aria-label="Create new"
1748 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"
1749 >
1750 <Plus size={16} />
1751 <ChevronDown size={13} className="text-muted" />
1752 </DropdownMenuTrigger>
1753 </TooltipTrigger>
1754 <TooltipContent>Create new…</TooltipContent>
1755 </Tooltip>
1756 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1757 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1758 <>
1759 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1760 <DropdownMenuItem asChild>
1761 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1762 <CircleDot />
1763 New issue
1764 </Link>
1765 </DropdownMenuItem>
1766 {shell.repo.member && (
1767 <DropdownMenuItem asChild>
1768 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1769 <ListTree />
1770 Plan work
1771 </Link>
1772 </DropdownMenuItem>
1773 )}
1774 <DropdownMenuSeparator />
1775 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1776 )}
1777 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1778 <Link to="/?agent=new">
1779 <Sparkles />
1780 Put an agent on it
1781 </Link>
1782 </DropdownMenuItem>
1783 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1784 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1785 <Box />
1786 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1787 </Link>
1788 </DropdownMenuItem>
1789 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1790 <Link to="/new/github">
1791 <GitBranch />
1792 Import from GitHub
1793 </Link>
1794 </DropdownMenuItem>
1795 <DropdownMenuSeparator />
1796 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1797 <Link to="/workspaces/new">
1798 <Users />
1799 New workspace
1800 </Link>
1801 </DropdownMenuItem>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1802 {/* A team in the workspace the sidebar is about. */}
1803 {shell.workspace && (
1804 <DropdownMenuItem asChild>
1805 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1806 <UsersRound />
1807 New team
1808 </Link>
1809 </DropdownMenuItem>
1810 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1811 </DropdownMenuContent>
1812 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1813 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1814 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1815 </div>
1816 </header>
1817 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1818 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'1819 {children}
1820 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1821 </div>
Search across all of g1t, Explore, and a command palette1822 <CommandPalette
1823 open={palette}
1824 onOpenChange={setPalette}
1825 commands={commands}
1826 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1827 />
Agents as a team: lifecycle, merge queue, billing and a new shell1828 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1829 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell1830 );
1831}

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