Skip to content

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

1,880 lines77,754 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.

Merge branch 'worktree-agent-ad7c6d88d93adc817'1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, Inbox, KanbanSquare, KeyRound, LayoutDashboard, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, TrendingUp, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Merge branch 'worktree-agent-ad7c6d88d93adc817'5import { type Abilities, type InboxCounts, type Membership, type Spike, type User, mayCreateTeams } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Usage, Billing settings and prepaid AI credit; fixes from the UX audit7import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar8import { AgentButton, InboxBell } from "./inbox";
9import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Merge branch 'worktree-agent-ae1299e92e4462012'11import { Hint } from "./ui/hint";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit12import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas13import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height14import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit15import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents16import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell17import {
18 DropdownMenu,
19 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas20 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell21 DropdownMenuItem,
22 DropdownMenuLabel,
23 DropdownMenuSeparator,
24 DropdownMenuTrigger,
25} from "./ui/dropdown-menu";
Merge branch 'worktree-agent-ad7c6d88d93adc817'26import { type RoadmapItem, roadmapIn, roadmapItem } from "../lib/roadmap";
27import { SETTINGS_PAGES, sidebarCurrent } from "../lib/workspace-nav";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28import { 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 put29import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas30import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
31import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put32import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look33import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas34import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar35import { 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 quotas36import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell37
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put38/**
39 * What the sidebar needs, worked out by the root loader. For a visitor who
40 * is not signed in there is no workspace, no projects and no usage: only
41 * the project being looked at, if they can see it.
42 */
Agents as a team: lifecycle, merge queue, billing and a new shell43export type ShellData = {
44 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control45 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar46 /** All its projects, by name, for the palette: `name` is the slug in their address. */
47 repos: ShortcutProject[];
48 /** The person's pinned projects in it, in their order (lib/pins.ts). */
49 pinned?: ShortcutProject[];
50 /** What they opened there last, latest first, leaving out the pinned. */
51 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look52 /** Repositories shared with them in workspaces they do not belong to. */
53 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page54 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell55 repo: {
56 namespace: string;
57 name: string;
58 member: boolean;
59 issues: number;
60 pulls: number;
61 } | null;
Billing accounts, terms and enterprises; g1t is no longer free62 /** Where the workspace stands against its usage limit, if billing is on. */
63 limit: {
64 exposureMicros: number;
65 ceilingMicros: number | null;
66 state: "ok" | "warning" | "stopped";
67 comped: boolean;
68 } | null;
Free while g1t is being built out; agents can check out their own forks69 /** Whether g1t charges nothing for now, while it is being built out. */
70 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request71 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy72 /** This month's usage: charged, or at cost while g1t is free. */
73 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look74 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
75 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 all76 /** What is unread in their inbox, for the bell. Absent for a visitor. */
77 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell78};
79
80function SidebarLink({
81 to,
82 icon,
83 end,
84 count,
85 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look86 drill,
Merge branch 'worktree-agent-ad7c6d88d93adc817'87 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell88 children,
89}: {
90 to: string;
91 icon: ReactNode;
92 end?: boolean;
93 count?: number;
Projects: what a workspace builds and runs, first on every page94 /** Other path prefixes under which this link is the current one. */
95 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look96 /** It opens a list of its own: a chevron says so, always or on hover. */
97 drill?: boolean | "hover";
Merge branch 'worktree-agent-ad7c6d88d93adc817'98 /** Whether it is the current row, when the list works that out itself. */
99 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell100 children: ReactNode;
101}) {
102 const { pathname } = useLocation();
103 return (
104 <NavLink
105 to={to}
106 end={end}
107 prefetch="intent"
108 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page109 const current =
Merge branch 'worktree-agent-ad7c6d88d93adc817'110 lit ??
111 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark112 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 shell113 current
114 ? "bg-raised font-medium text-fg"
115 : isPending
116 ? "bg-raised/60 text-fg"
117 : "text-muted hover:bg-raised/60 hover:text-fg"
118 }`;
119 }}
120 >
121 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
122 <span className="min-w-0 grow truncate">{children}</span>
123 {count != null && count > 0 && (
124 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
125 {count}
126 </span>
127 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look128 {drill && (
129 <ChevronRight
130 size={14}
131 aria-hidden="true"
132 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
133 />
134 )}
Agents as a team: lifecycle, merge queue, billing and a new shell135 </NavLink>
136 );
137}
138
Pull requests say where they are live; the project menu shows what is coming139/**
140 * Something a project will have, shown where it will live so people can
141 * see where g1t is going. Not a link: nothing is behind it yet.
142 */
143function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
144 return (
Merge branch 'worktree-agent-ae1299e92e4462012'145 <Hint label={about} side="right">
146 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
147 <span className="shrink-0 opacity-70">{icon}</span>
148 <span className="grow truncate">{children}</span>
149 <span className="sr-only">{about}</span>
150 <SoonPill />
151 </div>
152 </Hint>
Pull requests say where they are live; the project menu shows what is coming153 );
154}
155
The project menu opens by section, and the workspace has its own panel156/**
A flat project menu with Code first, views as tabs, and Stripe for every owner157 * A page that is coming, as a link to its roadmap page: faint, with Soon,
158 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel159 */
A flat project menu with Code first, views as tabs, and Stripe for every owner160function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel161 to,
162 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner163 icon,
164 about,
Merge branch 'worktree-agent-ad7c6d88d93adc817'165 current: lit,
The project menu opens by section, and the workspace has its own panel166 children,
167}: {
168 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner169 also?: string[];
170 icon: ReactNode;
171 about: string;
Merge branch 'worktree-agent-ad7c6d88d93adc817'172 /** Whether it is the current row, when the list works that out itself. */
173 current?: boolean;
The project menu opens by section, and the workspace has its own panel174 children: ReactNode;
175}) {
176 const { pathname } = useLocation();
177 return (
Merge branch 'worktree-agent-ae1299e92e4462012'178 <Hint label={about} side="right">
The project menu opens by section, and the workspace has its own panel179 <NavLink
180 to={to}
181 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner182 className={({ isActive }) => {
Merge branch 'worktree-agent-ad7c6d88d93adc817'183 const current = lit ?? (isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A flat project menu with Code first, views as tabs, and Stripe for every owner184 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
185 current ? "bg-raised font-medium text-fg" : "text-faint hover:bg-raised/60 hover:text-muted"
The project menu opens by section, and the workspace has its own panel186 }`;
187 }}
188 >
A flat project menu with Code first, views as tabs, and Stripe for every owner189 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel190 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit191 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales192 </NavLink>
Merge branch 'worktree-agent-ae1299e92e4462012'193 </Hint>
The project menu opens by section, and the workspace has its own panel194 );
195}
196
A flat project menu with Code first, views as tabs, and Stripe for every owner197/** Icons for what spans projects. */
198const WORKSPACE_ICONS: Record<string, ReactNode> = {
199 board: <KanbanSquare size={15} />,
200 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member201 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner202 packages: <Package size={15} />,
203 fleet: <Bot size={15} />,
204};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales205
A flat project menu with Code first, views as tabs, and Stripe for every owner206/** The Soon pages of a section, as paths, so its link is current on them. */
207function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
208 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales209}
210
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look211function SidebarGroup({
212 title,
213 action,
214 className = "",
215 children,
216}: {
217 title: string;
218 action?: ReactNode;
219 className?: string;
220 children: ReactNode;
221}) {
Agents as a team: lifecycle, merge queue, billing and a new shell222 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look223 <section className={className}>
A Vercel-like sidebar and a new mark224 <div className="mb-1 flex h-6 items-center justify-between px-2">
225 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell226 {action}
227 </div>
228 <div className="space-y-px">{children}</div>
229 </section>
230 );
231}
232
Workspace names and icons, and a component kit for every control233/** What a workspace is called where people read it: its display name, else its slug. */
234function displayName(membership: Membership): string {
235 return membership.name?.trim() || membership.slug;
236}
237
Agents as a team: lifecycle, merge queue, billing and a new shell238function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control239 const workspace = shell.workspace;
240 const current = workspace?.slug;
241 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell242 return (
243 <DropdownMenu>
Workspace names and icons, and a component kit for every control244 {/* The name goes to the workspace; only the arrows switch it. The
245 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'246 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark247 <Link
248 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents249 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look250 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 mark251 >
Workspace names and icons, and a component kit for every control252 {workspace ? (
253 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell254 ) : (
Workspace names and icons, and a component kit for every control255 <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 shell256 )}
Workspace names and icons, and a component kit for every control257 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark258 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'259 </Hint>
A Vercel-like sidebar and a new mark260 <DropdownMenuTrigger
261 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit262 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 mark263 >
264 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell265 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control266 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar267 {workspace && (
268 <>
269 {/* The workspace's own page: its projects, packages and people. */}
270 <DropdownMenuItem asChild>
271 <Link to={`/${workspace.slug}`}>
272 <LayoutGrid />
273 Workspace overview
274 </Link>
275 </DropdownMenuItem>
276 <DropdownMenuItem asChild>
277 <Link to={`/${workspace.slug}/-/projects`}>
278 <Box />
279 All projects
280 </Link>
281 </DropdownMenuItem>
282 <DropdownMenuSeparator />
283 </>
284 )}
Agents as a team: lifecycle, merge queue, billing and a new shell285 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
286 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'287 <Hint key={membership.slug} label={displayName(membership)} side="right">
288 <DropdownMenuItem asChild>
289 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control290 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
291 <span className="flex min-w-0 grow flex-col leading-tight">
292 <span className="truncate">{displayName(membership)}</span>
293 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
294 </span>
295 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
296 {membership.slug === current ? (
297 <Check className="shrink-0 text-accent" />
298 ) : (
299 <span className="size-4 shrink-0" aria-hidden="true" />
300 )}
Agents as a team: lifecycle, merge queue, billing and a new shell301 </Link>
302 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'303 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell304 ))}
305 <DropdownMenuSeparator />
306 <DropdownMenuItem asChild>
307 <Link to="/workspaces/new">
308 <Plus />
309 New workspace
310 </Link>
311 </DropdownMenuItem>
312 </DropdownMenuContent>
313 </DropdownMenu>
A Vercel-like sidebar and a new mark314 );
315}
316
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas317/**
318 * The fine print, as one quiet row at the foot of the account menu and the
319 * visitor's panel: each link a full-height target, left-aligned. In the
320 * menu each is a menu item, so the arrow keys reach it.
321 */
322const LEGAL_LINKS: [string, string][] = [
323 ["Policies", "/policies"],
324 ["Privacy", "/policies/privacy"],
325 ["Security", "/security"],
326];
327const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
328
329function MenuLegalRow() {
330 return (
331 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
332 {LEGAL_LINKS.map(([label, to], index) => (
333 <span key={to} className="flex items-center gap-0.5">
334 {index > 0 && (
335 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
336 ·
337 </span>
338 )}
339 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
340 <Link to={to}>{label}</Link>
341 </DropdownMenuItem>
342 </span>
343 ))}
344 </div>
345 );
346}
347
348/** Status's row: the live dot and two or three words, fetched when the menu opens. */
349function StatusSummary({ open }: { open: boolean }) {
350 const status = useSiteStatus(open);
351 const words = statusTitle(status);
352 return (
353 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
354 {/* The words say it; the dot's own words for screen readers would repeat them. */}
355 <span aria-hidden="true" className="flex">
356 <StatusDot state={status?.overall.state ?? null} />
357 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents358 {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 quotas359 </span>
360 );
361}
362
363/** A menu row: an icon, words, and whatever sits at its end. */
364const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
365
A Vercel-like sidebar and a new mark366function AccountMenu({ user }: { user: User }) {
367 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas368 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents369 // Name, primary address and invites left: asked for once, as soon as the
370 // 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 quotas371 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents372 const prefetch = () => {
373 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
374 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas375 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents376 if (open) prefetch();
377 // eslint-disable-next-line react-hooks/exhaustive-deps
378 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas379 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents380 // Still on its way: shapes where the words will be, the same size.
381 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 quotas382 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark383 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas384 <DropdownMenu open={open} onOpenChange={setOpen}>
385 <DropdownMenuTrigger
386 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents387 onPointerEnter={prefetch}
388 onFocus={prefetch}
Merge branch 'worktree-agent-ad7c6d88d93adc817'389 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent/60 data-[state=open]:bg-raised"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas390 >
Workspace names and icons, and a component kit for every control391 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark392 <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 quotas393 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark394 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas395 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
396 {/* Who is signed in, and a way to their profile. */}
397 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
398 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
399 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents400 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
401 {loading ? (
402 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
403 ) : (
404 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
405 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas406 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents407 {loading ? (
408 <Skeleton className="mt-1 h-3 w-40" />
409 ) : (
410 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
411 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas412 </span>
A Vercel-like sidebar and a new mark413 </Link>
414 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas415 <DropdownMenuSeparator className="my-1.5" />
416 <DropdownMenuGroup>
417 <DropdownMenuItem asChild className={MENU_ROW}>
418 <Link to={profile}>
419 <CircleUserRound />
420 Your profile
421 </Link>
422 </DropdownMenuItem>
423 <DropdownMenuItem asChild className={MENU_ROW}>
424 <Link to={FIRST_SETTINGS_PAGE}>
425 <Settings />
426 Your settings
427 </Link>
428 </DropdownMenuItem>
429 <DropdownMenuItem asChild className={MENU_ROW}>
430 <Link to="/settings/invites">
431 <Ticket />
432 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents433 {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 quotas434 {me?.invites_left != null && (
435 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
436 {me.invites_left} left
437 </span>
438 )}
439 </Link>
440 </DropdownMenuItem>
441 </DropdownMenuGroup>
442 <DropdownMenuSeparator className="my-1.5" />
443 <DropdownMenuGroup>
444 <DropdownMenuItem asChild className={MENU_ROW}>
445 <a href="https://docs.g1t.sh/">
446 <BookOpen />
447 Documentation
448 </a>
449 </DropdownMenuItem>
450 <DropdownMenuItem asChild className={MENU_ROW}>
451 <Link to="/support">
452 <LifeBuoy />
453 Support
454 </Link>
455 </DropdownMenuItem>
456 <DropdownMenuItem asChild className={MENU_ROW}>
457 <a href={STATUS_URL}>
458 <Activity />
459 Status
460 <StatusSummary open={open} />
461 </a>
462 </DropdownMenuItem>
463 </DropdownMenuGroup>
464 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark465 {/* Submitted from here: the menu closes on select, and a button
466 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 quotas467 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark468 <LogOut />
469 Sign out
470 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas471 <DropdownMenuSeparator className="my-1.5" />
472 <MenuLegalRow />
A Vercel-like sidebar and a new mark473 </DropdownMenuContent>
474 </DropdownMenu>
475 );
476}
477
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look478/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar479 * A workspace's settings pages, which the sidebar drills into: how it is
480 * set up and connected (guardrails, secrets, runners, integrations,
481 * webhooks), what it pays, its repositories, tokens and record. The main
482 * list keeps the places work happens and who belongs; every member can
483 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look484 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar485const SETTINGS_PAGE =
Merge branch 'worktree-agent-ad7c6d88d93adc817'486 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look487/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page488const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings489
Polish: phones, copy boxes, the plan page, the landing page, a real glide490/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look491 * The sidebar's lists are a stack, the way a phone pushes a screen: one
492 * drilled into slides in from the right over the full width, while the one
493 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide494 * ease-out, the fade quicker than the move so the two never blur together.
495 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide496 */
Sidebar: no settings flash on the way into a repository, and a better slide497const LAYER =
Sidebar: the panels really slide498 "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 look499/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide500const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings501
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look502/** One list in the stack: what it is, and its rows. */
503type Level = { key: string; node: ReactNode };
504
505/** True once the page has hydrated, so what the server drew never animates in. */
506function useHydrated(): boolean {
507 const [hydrated, setHydrated] = useState(false);
508 useEffect(() => setHydrated(true), []);
509 return hydrated;
510}
511
512/**
513 * Drilling in and out of the sidebar's lists. `trail` is the way from the
514 * main list to the one shown, worked out from the address, so a link
515 * straight to a settings page opens the sidebar already drilled in, and
516 * leaving it comes back out. Lists deeper than the one shown stay in
517 * place, off to the right, so going back slides them away rather than
518 * dropping them. Moving to a list at the same depth (a workspace's settings
519 * to a project) fades the new one in where it is.
520 */
521function Drill({ trail }: { trail: Level[] }) {
522 const depth = trail.length - 1;
523 const hydrated = useHydrated();
524 const stack = useRef<Level[]>(trail);
525 const before = stack.current;
526 const within = trail.every((level, index) => before[index]?.key === level.key);
527 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
528 const levels = stack.current;
529 // What was showing last time, to tell drilling in from moving across.
530 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
531 useEffect(() => {
532 shown.current = { depth, keys: levels.map((level) => level.key) };
533 });
A catalogue of model providers, and settings that feel like settings534 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look535 <div className="relative min-h-0 grow overflow-hidden">
536 {levels.map((level, index) => {
537 const current = index === depth;
538 const place =
539 index < depth
540 ? "pointer-events-none -translate-x-1/4 opacity-0"
541 : current
542 ? "translate-x-0 opacity-100"
543 : "pointer-events-none translate-x-full opacity-0";
544 // A list that was not there before arrives: from the right when
545 // drilling in, in place when moving across.
546 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
547 const enter = arriving
548 ? depth > shown.current.depth
549 ? "starting:translate-x-full starting:opacity-0"
550 : "starting:opacity-0"
551 : "";
552 return (
553 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
554 {level.node}
555 </div>
556 );
557 })}
558 </div>
559 );
560}
561
562/**
563 * The row at the top of a list drilled into: the list's name, which goes
564 * back out, and what it belongs to, faint at the end.
565 */
566function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
567 return (
568 <Link
569 to={to}
570 prefetch="intent"
571 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 settings572 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look573 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
574 <span className="min-w-0 shrink-0 truncate">{label}</span>
575 {context && (
576 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
577 )}
578 </Link>
579 );
580}
581
582/** A thin rule between groups of a list. */
583function Rule() {
584 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
585}
586
Navigation: the workspace's own page with tabs, pins and recent in the sidebar587/** A quiet heading inside a group: Pinned, Recent. */
588function SidebarSubhead({ children }: { children: ReactNode }) {
589 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
590}
591
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look592/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar593 * The workspace's projects as the sidebar keeps them, however many there
594 * are: the person's pins in their order, what they opened last, and the
595 * way to all of them. Pins move by dragging, or with Alt and the arrow
596 * keys; the projects service keeps the order.
597 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'598function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar599 const reorder = useFetcher({ key: `pins:${slug}` });
600 const saved = shell.pinned ?? [];
601 // While a new order is on its way, it shows as made.
602 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
603 const pinned = asked
604 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
605 : saved;
606 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
607 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
608 const [dragging, setDragging] = useState<number | null>(null);
609 const move = (from: number, by: number) => {
610 const next = movedPin(pinned, from, by);
611 if (!next) return;
612 const form = new FormData();
613 form.set("intent", "reorder");
614 for (const project of next) form.append("slug", project.name);
615 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
616 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar617 // Each row pins or unpins in place: the pin shows on hover or focus, and
618 // stays shown on a pinned row's hover so it reads as "unpin".
619 const row = (project: ShortcutProject, isPinned: boolean) => (
620 <div className="group/row relative">
621 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
622 <span className="block truncate pr-6">{project.title ?? project.name}</span>
623 </SidebarLink>
624 <PinButton
625 workspace={slug}
626 slug={project.name}
627 name={project.title ?? project.name}
628 pinned={isPinned}
629 small
630 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"
631 />
632 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar633 );
634 return (
635 <SidebarGroup
636 title="Projects"
637 action={
638 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
639 <Plus size={13} />
640 </Link>
641 }
642 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar643 {/* Only there once something is pinned: each row below has its own pin. */}
644 {pinned.length > 0 && (
645 <>
646 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar647 <ul aria-label="Pinned projects" className="space-y-px">
648 {pinned.map((project, index) => (
649 <li
650 key={project.name}
651 draggable={pinned.length > 1}
652 onDragStart={(event) => {
653 setDragging(index);
654 event.dataTransfer.effectAllowed = "move";
655 }}
656 onDragOver={(event) => {
657 if (dragging != null) event.preventDefault();
658 }}
659 onDrop={(event) => {
660 event.preventDefault();
661 if (dragging != null && dragging !== index) move(dragging, index - dragging);
662 setDragging(null);
663 }}
664 onDragEnd={() => setDragging(null)}
665 onKeyDown={(event) => {
666 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
667 event.preventDefault();
668 move(index, event.key === "ArrowUp" ? -1 : 1);
669 }}
670 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
671 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar672 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar673 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'674 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
675 <span className="absolute top-1/2 -left-1.5 -translate-y-1/2 cursor-grab text-faint opacity-0 transition-opacity group-hover/pin:opacity-100">
676 <GripVertical size={12} aria-hidden="true" />
677 </span>
678 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar679 )}
680 </li>
681 ))}
682 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar683 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar684 )}
685 {recent.length > 0 && (
686 <>
687 <SidebarSubhead>Recent</SidebarSubhead>
688 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar689 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar690 ))}
691 </>
692 )}
693 <div className="pt-1">
Merge branch 'worktree-agent-ad7c6d88d93adc817'694 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length} current={current}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar695 All projects
696 </SidebarLink>
697 </div>
698 </SidebarGroup>
699 );
700}
701
702/**
Merge branch 'worktree-agent-ad7c6d88d93adc817'703 * The main list, in two parts a rule apart. Above it, what is yours
704 * whichever workspace you are in: Mission control, your inbox, and what
705 * others have shared with you. Below it, under the workspace's name, the
706 * workspace: its overview, its projects, what it builds and runs with
707 * across them, then its people, usage, support and settings. One row is
708 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look709 */
710function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
711 const ws = shell.workspace;
Merge branch 'worktree-agent-ad7c6d88d93adc817'712 const { pathname } = useLocation();
713 const going = useNavigation().location?.pathname;
714 // The row lights as the link is followed, not once the page arrives.
715 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look716 // A visitor browses: no workspace, no projects of their own.
717 if (!user) {
718 return (
719 <nav aria-label="g1t" className={PANEL}>
720 <div className="mt-3 space-y-px">
721 {VISITOR_LINKS.map((link) => (
722 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
723 {link.label}
724 </SidebarLink>
725 ))}
726 </div>
727 </nav>
728 );
729 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'730 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look731 return (
732 <nav aria-label="g1t" className={PANEL}>
Merge branch 'worktree-agent-ad7c6d88d93adc817'733 {/* Yours, in every workspace. Explore, all of g1t, is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look734 <div className="mt-3 space-y-px">
Merge branch 'worktree-agent-ad7c6d88d93adc817'735 <SidebarLink to="/" end icon={<House size={15} />} current={here === "mission"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look736 Mission control
737 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'738 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
739 Inbox
740 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look741 </div>
Merge branch 'worktree-agent-ad7c6d88d93adc817'742 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look743 <SidebarGroup title="Shared with you" className="mt-3">
Merge branch 'worktree-agent-ad7c6d88d93adc817'744 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look745 <SidebarLink
746 key={`${repo.namespace}/${repo.name}`}
747 to={`/${repo.namespace}/${repo.name}`}
748 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
749 drill="hover"
750 >
751 <span className="font-mono text-faint">{repo.namespace}/</span>
752 {repo.name}
753 </SidebarLink>
754 ))}
755 </SidebarGroup>
756 )}
757
Merge branch 'worktree-agent-ad7c6d88d93adc817'758 <Rule />
759 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look760 <>
Merge branch 'worktree-agent-ad7c6d88d93adc817'761 {/* The workspace the switcher names: its own page first. */}
762 <SidebarGroup title={displayName(ws)}>
763 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
764 Overview
765 </SidebarLink>
766 </SidebarGroup>
767 <div className="mt-3">
768 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
769 </div>
770
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member771 <Rule />
Merge branch 'worktree-agent-ad7c6d88d93adc817'772 <div className="space-y-px">
773 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />} current={here === "agents"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look774 Agent fleet
775 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'776 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />} current={here === "context"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look777 Context
778 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'779 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />} current={here === "memory"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look780 Memory
781 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'782 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />} current={here === "security"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look783 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents784 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'785 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />} current={here === "packages"}>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member786 Packages
787 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'788 {INSIGHTS && (
789 <SidebarSoonLink
790 to={`/${ws.slug}/-/insights`}
791 icon={<TrendingUp size={15} />}
792 about={INSIGHTS.summary}
793 current={here === "insights"}
794 >
795 {INSIGHTS.title}
796 </SidebarSoonLink>
797 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar798 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look799 <SidebarSoonLink
800 key={item.key}
801 to={`/${ws.slug}/-/soon/${item.key}`}
802 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
803 about={item.summary}
804 >
805 {item.title === "Board" ? "Boards" : item.title}
806 </SidebarSoonLink>
807 ))}
Merge branch 'worktree-agent-ad7c6d88d93adc817'808 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look809
810 <Rule />
811 <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 bar812 {/* Who belongs, for every member to see; owners invite and manage there. */}
Merge branch 'worktree-agent-ad7c6d88d93adc817'813 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />} current={here === "people"}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar814 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar815 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'816 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams} current={here === "teams"}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar817 Teams
818 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'819 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />} current={here === "usage"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look820 Usage
821 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'822 <SidebarLink to="/support" icon={<LifeBuoy size={15} />} current={here === "support"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look823 Support
824 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar825 {/* 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 look826 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member827 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 look828 icon={<Settings size={15} />}
829 drill
Merge branch 'worktree-agent-ad7c6d88d93adc817'830 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look831 >
832 Settings
833 </SidebarLink>
834 </div>
835 </>
Merge branch 'worktree-agent-ad7c6d88d93adc817'836 ) : (
837 <SidebarGroup title="Projects">
838 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
839 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look840 )}
841 </nav>
842 );
843}
844
Merge branch 'worktree-agent-ad7c6d88d93adc817'845/** Insights, coming: the workspace's own page says what it will be. */
846const INSIGHTS = roadmapItem("insights");
847
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look848/** A workspace's settings, drilled into from Settings in the main list. */
849function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
850 return (
851 <nav aria-label="Workspace settings" className={PANEL}>
852 <BackRow to={`/${slug}`} label="Settings" context={slug} />
853 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings854 {owner && (
855 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
856 General
857 </SidebarLink>
858 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look859 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
860 Repositories
861 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments862 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
863 Access tokens
864 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look865 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar866 <SidebarGroup title="Agents and runs" className="mt-3">
867 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
868 Guardrails
869 </SidebarLink>
870 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
871 Secrets and variables
872 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it873 {owner && (
874 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
875 Runners
876 </SidebarLink>
877 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar878 </SidebarGroup>
879 <SidebarGroup title="Connections" className="mt-3">
880 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
881 Integrations
882 </SidebarLink>
883 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
884 Webhooks
885 </SidebarLink>
886 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look887 <Rule />
888 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings889 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments890 Billing and plans
A catalogue of model providers, and settings that feel like settings891 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API892 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
893 Audit log
894 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look895 </div>
A catalogue of model providers, and settings that feel like settings896 </nav>
897 );
898}
899
Sidebar: no settings flash on the way into a repository, and a better slide900/**
901 * What the repository menu needs. Known from the address before the page's
902 * data arrives, so the menu is right from the first frame; the counts fill
903 * in once it does.
904 */
905type MenuRepo = {
906 namespace: string;
907 name: string;
908 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look909 /** Whether they see its settings; from the repository's page once it loads. */
910 settings?: boolean;
911 /** What they may do there, once the repository's page loads. */
912 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide913 issues?: number;
914 pulls?: number;
915};
916
917function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
918 return (
919 a != null &&
920 b != null &&
921 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
922 a.name.toLowerCase() === b.name.toLowerCase()
923 );
924}
A repository has its own sidebar, as settings do925
926/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look927 * A project's own list, drilled into while you are in it: everything about
928 * the project, running and its code, and nothing else, with the way back
929 * out to all of them.
A repository has its own sidebar, as settings do930 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put931function RepoMenu({
932 repo,
933 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look934 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put935}: {
936 repo: MenuRepo;
937 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look938 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
939 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put940}) {
A repository has its own sidebar, as settings do941 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 put942 // 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 look943 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do944 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look945 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
946 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'947 <Hint label="Overview" side="right">
A flat project menu with Code first, views as tabs, and Stripe for every owner948 <NavLink
A repository has its own sidebar, as settings do949 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner950 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents951 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner952 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look953 `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 owner954 }
A repository has its own sidebar, as settings do955 >
956 <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 page957 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do958 </span>
959 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
960 <span className="text-faint">{repo.namespace}/</span>
961 <span className="font-semibold text-fg">{repo.name}</span>
962 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner963 </NavLink>
Merge branch 'worktree-agent-ae1299e92e4462012'964 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look965 {/* A project's pages, in the order people use them. A page with more
966 than one view shows them as tabs across its top. */}
967 <Rule />
968 <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 request969 <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 owner970 Code
971 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit972 <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 owner973 Issues
974 </SidebarLink>
975 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
976 Pull requests
977 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains978 <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 owner979 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains980 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner981 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
982 Workflows
Projects: what a workspace builds and runs, first on every page983 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look984 </div>
985 <Rule />
986 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put987 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner988 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
989 Deployments
990 </SidebarLink>
991 ) : null}
Observability gets its own page; tabs never overflow992 <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.">
993 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner994 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put995 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API996 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
997 Security
998 </SidebarLink>
999 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner1000 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
1001 Insights
1002 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1003 </div>
1004 {(repo.settings ?? shows.has("settings")) && (
1005 <>
1006 <Rule />
1007 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1008 Settings
1009 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1010 </>
1011 )}
A repository has its own sidebar, as settings do1012 </nav>
1013 );
1014}
1015
Project settings move into the sidebar, grouped, with a heading per page1016/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1017 * A project's settings, drilled into from its list: what is about running
1018 * it, then its agents, then its code and who can reach it. The way back
1019 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1020 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1021function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1022 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1023 // Each page shows to the roles that can use it, once the role is known.
1024 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1025 const running = shows("deployments");
1026 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1027 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1028 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1029 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1030 {shows("") && (
1031 <div className="mt-2 space-y-px">
1032 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1033 General
1034 </SidebarLink>
1035 </div>
1036 )}
1037 {running && (
1038 <>
1039 {shows("") && <Rule />}
1040 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1041 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1042 Deployments
1043 </SidebarLink>
1044 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1045 Domains
1046 </SidebarLink>
1047 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1048 Dependencies
1049 </SidebarLink>
1050 </div>
1051 </>
1052 )}
1053 {agents && (
1054 <>
1055 <Rule />
1056 <div className="space-y-px">
1057 {shows("agents") && (
1058 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1059 Agents
1060 </SidebarLink>
1061 )}
1062 {shows("guardrails") && (
1063 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1064 Guardrails
1065 </SidebarLink>
1066 )}
1067 </div>
1068 </>
1069 )}
1070 <Rule />
1071 <div className="space-y-px">
1072 {shows("repository") && (
1073 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1074 Repository
1075 </SidebarLink>
1076 )}
1077 {shows("access") && (
1078 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1079 Access
1080 </SidebarLink>
1081 )}
1082 {shows("branches") && (
1083 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1084 Branches and merging
1085 </SidebarLink>
1086 )}
1087 {shows("secrets") && (
1088 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1089 Secrets and variables
1090 </SidebarLink>
1091 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1092 {shows("runners") && (
1093 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1094 Runners
1095 </SidebarLink>
1096 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1097 {shows("webhooks") && (
1098 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1099 Webhooks
1100 </SidebarLink>
1101 )}
1102 </div>
Project settings move into the sidebar, grouped, with a heading per page1103 </nav>
1104 );
1105}
1106
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1107/** Your own settings, drilled into from Your settings: one page each. */
1108function AccountSettingsMenu({ username }: { username: string }) {
1109 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1110 <SidebarLink to={`/settings/${page}`} icon={icon}>
1111 {ACCOUNT_SETTINGS[page].title}
1112 </SidebarLink>
Account settings in the same shape as a workspace's1113 );
1114 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1115 <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 quotas1116 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1117 <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 quotas1118 {link("profile", <CircleUserRound size={15} />)}
1119 {link("emails", <Mail size={15} />)}
1120 {link("invites", <Ticket size={15} />)}
1121 </div>
1122 <Rule />
1123 <div className="space-y-px">
1124 {link("keys", <Fingerprint size={15} />)}
1125 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1126 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1127 <Rule />
1128 <div className="space-y-px">
1129 {link("github", <GithubMark className="size-[15px]" />)}
1130 {link("applications", <Plug size={15} />)}
1131 </div>
1132 <Rule />
1133 <div className="space-y-px">
1134 {link("security-log", <History size={15} />)}
1135 </div>
Account settings in the same shape as a workspace's1136 </nav>
1137 );
1138}
1139
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1140/**
1141 * Signing in and signing up, in place of the account for a visitor. Signing
1142 * in brings them back to the page they are on.
1143 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1144/** Help, status and the fine print under a visitor's sign-in buttons. */
1145function VisitorLinks() {
1146 const status = useSiteStatus();
Merge branch 'worktree-agent-ad7c6d88d93adc817'1147 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/60`;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1148 const dot = (
1149 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1150 ·
1151 </span>
1152 );
1153 return (
1154 // Two short rows, so a narrow sidebar never starts a line with a dot.
1155 <nav aria-label="About g1t" className="pt-0.5">
1156 <div className="flex items-center gap-0.5">
1157 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1158 <StatusDot state={status?.overall.state ?? null} />
1159 Status
1160 </a>
1161 {dot}
1162 <Link to="/support" className={quiet}>
1163 Support
1164 </Link>
1165 </div>
1166 <div className="flex items-center gap-0.5">
1167 {LEGAL_LINKS.map(([label, to], index) => (
1168 <span key={to} className="flex items-center gap-0.5">
1169 {index > 0 && dot}
1170 <Link to={to} className={quiet}>
1171 {label}
1172 </Link>
1173 </span>
1174 ))}
1175 </div>
1176 </nav>
1177 );
1178}
1179
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1180function VisitorPanel() {
1181 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1182 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1183 return (
1184 <div className="space-y-2">
1185 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1186 <div className="grid grid-cols-2 gap-2">
1187 <Link
1188 to={withNext("/login", pathname + search)}
1189 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"
1190 >
1191 <LogIn size={14} />
1192 Sign in
1193 </Link>
1194 <Link
1195 to={withNext("/register", pathname + search)}
1196 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1197 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1198 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1199 </Link>
1200 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1201 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1202 </div>
1203 );
1204}
1205
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1206function Sidebar({
1207 user,
1208 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1209 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1210 onFind,
1211 onClose,
1212}: {
1213 user: User | null;
1214 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1215 /** The page is a 404: the address names nothing the viewer can see. */
1216 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1217 onFind: () => void;
1218 /** In the sheet on a small screen: closing it, at the end of the top row. */
1219 onClose?: () => void;
1220}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1221 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1222 const { pathname } = useLocation();
1223 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1224 // Where the sidebar is drilled to follows the page being gone to, so it
1225 // moves as the link is followed, not once the page arrives.
1226 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1227 // On a 404, the address is not a place: no list is drilled into from it,
1228 // except the project's own when the project is real and visible and only
1229 // something inside it is missing (a file, a commit, an issue).
1230 const lost = missing && going == null;
1231 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1232 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1233 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1234 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1235 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1236 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 look1237 // An invitation to a repository is answered before its menu means anything.
1238 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1239 repoPath != null &&
1240 !reserved.has(repoPath[1]) &&
1241 repoPath[2] !== "-" &&
1242 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1243 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1244 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1245 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1246 // The repository the menu is for: the one loaded if it is the one being
1247 // gone to, else what the address says, at once.
1248 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 look1249 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1250 ? sameRepo(active, targetRepo)
1251 ? active
1252 : {
1253 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1254 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1255 }
1256 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1257 // The viewer's role there, from the repository's page once it has loaded.
1258 const page = useRouteLoaderData("routes/repo/layout") as
1259 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1260 | undefined;
1261 const menuRepo: MenuRepo | null =
1262 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1263 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1264 : guessed;
1265
1266 // The way from the main list to the one shown.
1267 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1268 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1269 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1270 } else if (ws && inSettings) {
1271 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1272 } else if (menuRepo) {
1273 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1274 // Out of a project: to its workspace's projects when they are yours.
1275 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1276 const back = !user
1277 ? { to: "/explore", label: "Explore" }
1278 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1279 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1280 : { to: "/", label: "Mission control" };
1281 trail.push({
1282 key,
1283 node: (
1284 <RepoMenu
1285 repo={menuRepo}
1286 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1287 back={back}
1288 />
1289 ),
1290 });
1291 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1292 }
1293
Agents as a team: lifecycle, merge queue, billing and a new shell1294 return (
1295 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1296 {/* 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 bar1297 <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 put1298 {user ? (
1299 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1300 {/* The 1 alone beside the workspace: a square hover the height of the
1301 switcher, the mark as tall as the workspace avatar. */}
1302 <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 bar1303 <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 put1304 </Link>
1305 <span className="shrink-0 text-line-strong" aria-hidden="true">
1306 /
1307 </span>
1308 <WorkspaceSwitcher user={user} shell={shell} />
1309 </>
1310 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1311 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1312 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1313 </Link>
1314 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1315 {onClose && (
1316 <button
1317 type="button"
1318 aria-label="Close menu"
1319 onClick={onClose}
1320 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1321 >
1322 <X size={16} />
1323 </button>
1324 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1325 </div>
A Vercel-like sidebar and a new mark1326 <div className="px-2 pt-3">
1327 <button
1328 type="button"
1329 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1330 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 mark1331 >
1332 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1333 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1334 <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 mark1335 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1336 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1337 <Drill trail={trail} />
1338 {/* The account, or signing in, as one row at the very bottom. */}
1339 <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 put1340 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1341 </div>
1342 </div>
1343 );
1344}
1345
1346/** Words for the sections a path can end in. */
1347const SECTIONS: Record<string, string> = {
1348 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1349 milestones: "Milestones",
1350 labels: "Labels",
A Vercel-like sidebar and a new mark1351 pulls: "Pull requests",
1352 queue: "Merge queue",
1353 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1354 plans: "Outcomes",
1355 actions: "Workflows",
1356 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1357 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1358 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1359 access: "Access",
1360 invitations: "Invitation",
1361 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1362 branches: "Branches",
1363 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1364 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1365 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1366 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1367 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1368 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1369 people: "People",
1370 projects: "Projects",
1371 teams: "Teams",
A Vercel-like sidebar and a new mark1372 tokens: "Access tokens",
1373 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1374 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1375 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1376 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1377 domains: "Domains",
1378 guardrails: "Guardrails",
1379 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1380 tree: "Files",
1381 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1382 agents: "Agents",
1383 context: "Context",
1384 memory: "Memory",
1385 security: "Security",
1386 packages: "Packages",
1387 runners: "Runners",
1388 workflows: "Workflows",
1389 observability: "Observability",
1390 insights: "Insights",
1391 sessions: "Sessions",
A Vercel-like sidebar and a new mark1392};
1393
1394/** 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 quotas1395function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1396 // A 404 names nothing from the address, unless the project is real and
1397 // visible and only something inside it is missing.
1398 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1399 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1400 return <span className="text-sm font-medium">Not found</span>;
1401 }
A Vercel-like sidebar and a new mark1402 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1403 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1404 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 quotas1405 // Your settings: Settings / Emails.
1406 if (parts[0] === "settings") {
1407 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1408 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 quotas1409 }
A Vercel-like sidebar and a new mark1410 if (reserved.includes(parts[0]!)) {
1411 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1412 settings: "Account",
A Vercel-like sidebar and a new mark1413 explore: "Explore",
Projects live: fixes from walking them end to end1414 new: "New project",
A Vercel-like sidebar and a new mark1415 search: "Search",
1416 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1417 policies: "Policies",
1418 security: "Security",
1419 support: "Support",
1420 status: "Status",
1421 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1422 inbox: "Inbox",
A Vercel-like sidebar and a new mark1423 };
1424 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1425 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1426 // A person's profile, by their handle.
1427 if (parts[0] === "u" && parts[1]) {
1428 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1429 }
A Vercel-like sidebar and a new mark1430 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1431 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1432 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1433 const page = `/${owner}/-/${third}`;
1434 if (third && SETTINGS_PAGE.test(page)) {
1435 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1436 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1437 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1438 } else if (second) {
1439 const repo = `/${owner}/${second}`;
1440 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1441 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1442 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 mark1443 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 deployments1444 else if (third && SECTIONS[third]) {
1445 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1446 // A settings page names which one: Settings / Secrets and variables.
1447 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1448 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 deployments1449 }
1450 }
A Vercel-like sidebar and a new mark1451 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1452 return <Trail trail={trail} />;
1453}
1454
1455/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1456type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1457
1458/**
1459 * The trail of links. On a phone there is room for one: the page itself,
1460 * with a way back to where it sits; the whole trail from a wider screen up.
1461 */
1462function Trail({ trail }: { trail: Crumb[] }) {
1463 const up = trail.length > 1 ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1464 return (
1465 <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 audit1466 {up && (
1467 <Link
1468 to={up.to}
1469 prefetch="intent"
1470 aria-label={`Back to ${up.label}`}
1471 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"
1472 >
1473 <ChevronLeft size={16} />
1474 </Link>
1475 )}
1476 {trail.map((crumb, index) => {
1477 const last = index === trail.length - 1;
1478 return (
1479 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1480 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1481 <Link
1482 to={crumb.to}
1483 prefetch="intent"
1484 aria-current={last ? "page" : undefined}
1485 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1486 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1487 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1488 >
1489 {last && crumb.short ? (
1490 <>
1491 <span className="sm:hidden">{crumb.short}</span>
1492 <span className="hidden sm:inline">{crumb.label}</span>
1493 </>
1494 ) : (
1495 crumb.label
1496 )}
1497 </Link>
1498 </span>
1499 );
1500 })}
A Vercel-like sidebar and a new mark1501 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1502 );
1503}
1504
Search across all of g1t, Explore, and a command palette1505type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1506
1507/** 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 look1508function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1509 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1510 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1511 { 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 step1512 ...(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-agent1513 ? [{ 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 step1514 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1515 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1516 { 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 all1517 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1518 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1519 { 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 quotas1520 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1521 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1522 .filter((page) => page !== "profile")
1523 .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 shell1524 ];
1525 const repo = shell.repo;
1526 if (repo) {
1527 const base = `/${repo.namespace}/${repo.name}`;
1528 const name = `${repo.namespace}/${repo.name}`;
1529 commands.unshift(
Projects: what a workspace builds and runs, first on every page1530 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1531 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1532 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1533 { 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 step1534 ...(repo.member
1535 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1536 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1537 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1538 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1539 ...(repo.member
1540 ? [
1541 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1542 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1543 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1544 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1545 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1546 ]
1547 : []),
1548 );
1549 }
1550 for (const membership of user.workspaces ?? []) {
1551 commands.push(
Workspace names and icons, and a component kit for every control1552 { 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 sidebar1553 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1554 { 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 request1555 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1556 { 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 settings1557 { 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 look1558 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1559 );
1560 }
1561 for (const listed of shell.repos) {
1562 commands.push({
Projects: what a workspace builds and runs, first on every page1563 label: listed.title ?? listed.name,
1564 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1565 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1566 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1567 });
1568 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1569 // "Put an agent on …": one per project, after the projects themselves.
1570 for (const listed of shell.repos) {
1571 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1572 commands.push({
1573 label: `Put an agent on ${listed.title ?? listed.name}`,
1574 hint: `${listed.namespace}/${listed.name}`,
1575 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1576 icon: <Sparkles size={15} />,
1577 });
1578 }
Agents as a team: lifecycle, merge queue, billing and a new shell1579 return commands;
1580}
1581
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1582/** 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 look1583function 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 put1584 const commands: Command[] = [];
1585 const repo = shell.repo;
1586 if (repo) {
1587 const base = `/${repo.namespace}/${repo.name}`;
1588 const name = `${repo.namespace}/${repo.name}`;
1589 commands.push(
1590 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1591 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1592 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1593 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1594 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1595 );
1596 }
1597 commands.push(
1598 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1599 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1600 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1601 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1602 { 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 look1603 { 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 put1604 );
1605 return commands;
1606}
1607
Agents as a team: lifecycle, merge queue, billing and a new shell1608/**
Merge branch 'worktree-agent-aca4790ea223aa235'1609 * The page while the next one loads: after a moment it dims, under the
1610 * bar below, so it is plainly on its way out rather than frozen, and the
1611 * next page fades up from there. Sending a form does not dim it: the
1612 * form's button says it is working. Spread on the element holding the page.
1613 */
1614export function useLeaving() {
1615 const navigation = useNavigation();
1616 const method = navigation.formMethod?.toUpperCase();
1617 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1618 return {
1619 "aria-busy": leaving || undefined,
1620 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1621 };
1622}
1623
1624/**
Agents as a team: lifecycle, merge queue, billing and a new shell1625 * A bar across the top of the page while the next one loads, as GitHub
1626 * has: it appears at once, creeps towards the end while waiting, then fills
1627 * and fades when the page arrives.
1628 */
1629export function Progress() {
1630 const navigation = useNavigation();
1631 const busy = navigation.state !== "idle";
1632 const [width, setWidth] = useState(0);
1633 const [visible, setVisible] = useState(false);
1634 useEffect(() => {
1635 if (busy) {
1636 setVisible(true);
1637 setWidth(12);
1638 // Slows as it goes, never quite arriving until the page does.
1639 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1640 return () => clearInterval(timer);
1641 }
1642 setWidth((now) => (now > 0 ? 100 : 0));
1643 const fade = setTimeout(() => setVisible(false), 250);
1644 const reset = setTimeout(() => setWidth(0), 550);
1645 return () => {
1646 clearTimeout(fade);
1647 clearTimeout(reset);
1648 };
1649 }, [busy]);
1650 return (
1651 <div
1652 aria-hidden="true"
1653 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1654 visible ? "opacity-100" : "opacity-0"
1655 }`}
1656 >
1657 <div
1658 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1659 style={{ width: `${width}%` }}
1660 />
1661 </div>
1662 );
1663}
1664
1665/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1666 * The app: a sidebar with the workspace, its repositories and the sections
1667 * of the one being looked at; a slim bar with search and the account; and
1668 * the page. A visitor who is not signed in gets the same frame, with
1669 * Explore and Search in place of the workspace, and signing in in place of
1670 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1671 */
1672export function AppShell({
1673 user,
1674 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1675 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1676 banner,
1677 children,
1678}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1679 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1680 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1681 /** The page is a 404 (see Sidebar). */
1682 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1683 banner?: ReactNode;
1684 children: ReactNode;
1685}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1686 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1687 const [drawer, setDrawer] = useState(false);
1688 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1689 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1690 const signUpLabel = useSignUpCopy().primary;
1691 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1692
1693 // A new page closes the drawer on small screens.
1694 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1695 // Mission control's code, fetched while the browser is idle, so going
1696 // home never waits on it (routes/home.tsx).
1697 useEffect(() => {
1698 if (!user) return;
1699 const load = () => void import("./mission-control").catch(() => undefined);
1700 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1701 if (idle) idle(load);
1702 else setTimeout(load, 1500);
1703 }, [user]);
Search across all of g1t, Explore, and a command palette1704 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1705 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1706
1707 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1708 // The phone's menu is a sheet: a dialog that holds focus, closes on
1709 // Escape or a tap outside, and gives focus back to the menu button.
1710 <Sheet open={drawer} onOpenChange={setDrawer}>
Agents as a team: lifecycle, merge queue, billing and a new shell1711 <div className="min-h-screen">
1712 <Progress />
1713 <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 quotas1714 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1715 </aside>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1716 <SheetContent side="left" showClose={false} aria-describedby={undefined} className="w-72 max-w-[85vw] sm:max-w-72 lg:hidden">
1717 <SheetTitle className="sr-only">Menu</SheetTitle>
1718 <Sidebar
1719 user={user}
1720 shell={shell}
1721 missing={missing}
1722 onFind={() => {
1723 setDrawer(false);
1724 setPalette(true);
1725 }}
1726 onClose={() => setDrawer(false)}
1727 />
1728 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell1729
Polish: phones, copy boxes, the plan page, the landing page, a real glide1730 <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 bar1731 <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 audit1732 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell1733 aria-label="Open menu"
1734 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1735 >
1736 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1737 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1738 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1739 <div className="ml-auto flex items-center gap-1.5">
1740 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1741 <button
1742 type="button"
1743 aria-label="Search or jump to"
1744 onClick={() => setPalette(true)}
1745 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1746 >
1747 <Search size={16} />
1748 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1749 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1750 <NavLink
1751 to="/explore"
1752 prefetch="intent"
1753 aria-label="Explore"
1754 className={({ isActive }) =>
1755 `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"}`
1756 }
1757 >
1758 <Compass size={16} className="sm:hidden" />
1759 <span className="hidden sm:inline">Explore</span>
1760 </NavLink>
A Vercel-like sidebar and a new mark1761 <a
1762 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1763 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 mark1764 >
1765 Docs
1766 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1767 {user && (
1768 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1769 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1770 <InboxBell counts={shell.inbox ?? null} />
1771 </>
1772 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1773 {!user ? (
1774 // The sidebar has these too, but on a phone it is folded away.
1775 <Link
1776 to={withNext("/login", here)}
1777 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"
1778 >
1779 Sign in
1780 </Link>
1781 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1782 <>
1783 {/* Room between what comes in (the inbox) and what you make. */}
1784 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1785 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1786 <Tooltip>
1787 <TooltipTrigger asChild>
1788 <DropdownMenuTrigger
1789 aria-label="Create new"
1790 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"
1791 >
1792 <Plus size={16} />
1793 <ChevronDown size={13} className="text-muted" />
1794 </DropdownMenuTrigger>
1795 </TooltipTrigger>
1796 <TooltipContent>Create new…</TooltipContent>
1797 </Tooltip>
1798 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1799 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1800 <>
1801 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1802 <DropdownMenuItem asChild>
1803 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1804 <CircleDot />
1805 New issue
1806 </Link>
1807 </DropdownMenuItem>
1808 {shell.repo.member && (
1809 <DropdownMenuItem asChild>
1810 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1811 <ListTree />
1812 Plan work
1813 </Link>
1814 </DropdownMenuItem>
1815 )}
1816 <DropdownMenuSeparator />
1817 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1818 )}
1819 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1820 <Link to="/?agent=new">
1821 <Sparkles />
1822 Put an agent on it
1823 </Link>
1824 </DropdownMenuItem>
1825 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1826 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1827 <Box />
1828 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1829 </Link>
1830 </DropdownMenuItem>
1831 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1832 <Link to="/new/github">
1833 <GitBranch />
1834 Import from GitHub
1835 </Link>
1836 </DropdownMenuItem>
1837 <DropdownMenuSeparator />
1838 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1839 <Link to="/workspaces/new">
1840 <Users />
1841 New workspace
1842 </Link>
1843 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'1844 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
1845 {shell.workspace &&
1846 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
1847 <DropdownMenuItem asChild>
1848 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1849 <UsersRound />
1850 New team
1851 </Link>
1852 </DropdownMenuItem>
1853 ) : (
1854 <DropdownMenuItem disabled title="Only owners can create teams in this workspace.">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1855 <UsersRound />
1856 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'1857 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
1858 </DropdownMenuItem>
1859 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell1860 </DropdownMenuContent>
1861 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1862 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1863 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1864 </div>
1865 </header>
1866 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1867 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'1868 {children}
1869 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1870 </div>
Search across all of g1t, Explore, and a command palette1871 <CommandPalette
1872 open={palette}
1873 onOpenChange={setPalette}
1874 commands={commands}
1875 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1876 />
Agents as a team: lifecycle, merge queue, billing and a new shell1877 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1878 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell1879 );
1880}

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