Skip to content
1,840 linesCodeBlameRaw

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

Rules on the site: rulesets for a repository and a workspace, and the merge box1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, KanbanSquare, KeyRound, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Scale, Sparkles, Ticket, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all5import type { Abilities, InboxCounts, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Usage, Billing settings and prepaid AI credit; fixes from the UX audit7import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar8import { AgentButton, InboxBell } from "./inbox";
9import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit11import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height13import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit14import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents15import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell16import {
17 DropdownMenu,
18 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas19 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell20 DropdownMenuItem,
21 DropdownMenuLabel,
22 DropdownMenuSeparator,
23 DropdownMenuTrigger,
24} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales25import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26import { SETTINGS_CAPABILITY, type ViewerAccess, seesSettings } from "../lib/access";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put27import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas28import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
29import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put30import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look31import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas32import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar33import { type ShortcutProject, movedPin, recentWith } from "../lib/pins";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas34import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell35
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put36/**
37 * What the sidebar needs, worked out by the root loader. For a visitor who
38 * is not signed in there is no workspace, no projects and no usage: only
39 * the project being looked at, if they can see it.
40 */
Agents as a team: lifecycle, merge queue, billing and a new shell41export type ShellData = {
42 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control43 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar44 /** All its projects, by name, for the palette: `name` is the slug in their address. */
45 repos: ShortcutProject[];
46 /** The person's pinned projects in it, in their order (lib/pins.ts). */
47 pinned?: ShortcutProject[];
48 /** What they opened there last, latest first, leaving out the pinned. */
49 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50 /** Repositories shared with them in workspaces they do not belong to. */
51 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page52 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell53 repo: {
54 namespace: string;
55 name: string;
56 member: boolean;
57 issues: number;
58 pulls: number;
59 } | null;
Billing accounts, terms and enterprises; g1t is no longer free60 /** Where the workspace stands against its usage limit, if billing is on. */
61 limit: {
62 exposureMicros: number;
63 ceilingMicros: number | null;
64 state: "ok" | "warning" | "stopped";
65 comped: boolean;
66 } | null;
Free while g1t is being built out; agents can check out their own forks67 /** Whether g1t charges nothing for now, while it is being built out. */
68 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request69 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy70 /** This month's usage: charged, or at cost while g1t is free. */
71 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
73 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all74 /** What is unread in their inbox, for the bell. Absent for a visitor. */
75 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell76};
77
78function SidebarLink({
79 to,
80 icon,
81 end,
82 count,
83 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell85 children,
86}: {
87 to: string;
88 icon: ReactNode;
89 end?: boolean;
90 count?: number;
Projects: what a workspace builds and runs, first on every page91 /** Other path prefixes under which this link is the current one. */
92 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93 /** It opens a list of its own: a chevron says so, always or on hover. */
94 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell95 children: ReactNode;
96}) {
97 const { pathname } = useLocation();
98 return (
99 <NavLink
100 to={to}
101 end={end}
102 prefetch="intent"
103 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page104 const current =
105 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark106 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
Agents as a team: lifecycle, merge queue, billing and a new shell107 current
108 ? "bg-raised font-medium text-fg"
109 : isPending
110 ? "bg-raised/60 text-fg"
111 : "text-muted hover:bg-raised/60 hover:text-fg"
112 }`;
113 }}
114 >
115 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
116 <span className="min-w-0 grow truncate">{children}</span>
117 {count != null && count > 0 && (
118 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
119 {count}
120 </span>
121 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look122 {drill && (
123 <ChevronRight
124 size={14}
125 aria-hidden="true"
126 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
127 />
128 )}
Agents as a team: lifecycle, merge queue, billing and a new shell129 </NavLink>
130 );
131}
132
Pull requests say where they are live; the project menu shows what is coming133/**
134 * Something a project will have, shown where it will live so people can
135 * see where g1t is going. Not a link: nothing is behind it yet.
136 */
137function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
138 return (
139 <div
140 title={about}
141 aria-disabled="true"
142 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
143 >
144 <span className="shrink-0 opacity-70">{icon}</span>
145 <span className="grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit146 <SoonPill />
Pull requests say where they are live; the project menu shows what is coming147 </div>
148 );
149}
150
The project menu opens by section, and the workspace has its own panel151/**
A flat project menu with Code first, views as tabs, and Stripe for every owner152 * A page that is coming, as a link to its roadmap page: faint, with Soon,
153 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel154 */
A flat project menu with Code first, views as tabs, and Stripe for every owner155function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel156 to,
157 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner158 icon,
159 about,
The project menu opens by section, and the workspace has its own panel160 children,
161}: {
162 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner163 also?: string[];
164 icon: ReactNode;
165 about: string;
The project menu opens by section, and the workspace has its own panel166 children: ReactNode;
167}) {
168 const { pathname } = useLocation();
169 return (
170 <NavLink
171 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner172 title={about}
The project menu opens by section, and the workspace has its own panel173 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner174 className={({ isActive }) => {
175 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
176 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
177 current ? "bg-raised font-medium text-fg" : "text-faint hover:bg-raised/60 hover:text-muted"
The project menu opens by section, and the workspace has its own panel178 }`;
179 }}
180 >
A flat project menu with Code first, views as tabs, and Stripe for every owner181 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel182 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit183 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales184 </NavLink>
The project menu opens by section, and the workspace has its own panel185 );
186}
187
A flat project menu with Code first, views as tabs, and Stripe for every owner188/** Icons for what spans projects. */
189const WORKSPACE_ICONS: Record<string, ReactNode> = {
190 board: <KanbanSquare size={15} />,
191 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member192 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner193 packages: <Package size={15} />,
194 fleet: <Bot size={15} />,
195};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales196
A flat project menu with Code first, views as tabs, and Stripe for every owner197/** The Soon pages of a section, as paths, so its link is current on them. */
198function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
199 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales200}
201
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look202function SidebarGroup({
203 title,
204 action,
205 className = "",
206 children,
207}: {
208 title: string;
209 action?: ReactNode;
210 className?: string;
211 children: ReactNode;
212}) {
Agents as a team: lifecycle, merge queue, billing and a new shell213 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look214 <section className={className}>
A Vercel-like sidebar and a new mark215 <div className="mb-1 flex h-6 items-center justify-between px-2">
216 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell217 {action}
218 </div>
219 <div className="space-y-px">{children}</div>
220 </section>
221 );
222}
223
Workspace names and icons, and a component kit for every control224/** What a workspace is called where people read it: its display name, else its slug. */
225function displayName(membership: Membership): string {
226 return membership.name?.trim() || membership.slug;
227}
228
Agents as a team: lifecycle, merge queue, billing and a new shell229function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control230 const workspace = shell.workspace;
231 const current = workspace?.slug;
232 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell233 return (
234 <DropdownMenu>
Workspace names and icons, and a component kit for every control235 {/* The name goes to the workspace; only the arrows switch it. The
236 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark237 <Link
238 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents239 prefetch="intent"
Workspace names and icons, and a component kit for every control240 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look241 className="flex h-9 min-w-0 grow items-center gap-2 rounded-md px-2 transition-colors hover:bg-raised"
A Vercel-like sidebar and a new mark242 >
Workspace names and icons, and a component kit for every control243 {workspace ? (
244 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell245 ) : (
Workspace names and icons, and a component kit for every control246 <span className="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell247 )}
Workspace names and icons, and a component kit for every control248 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark249 </Link>
250 <DropdownMenuTrigger
251 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit252 className="flex h-9 w-7 shrink-0 items-center justify-center rounded-md text-faint outline-none transition-colors hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark253 >
254 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell255 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control256 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar257 {workspace && (
258 <>
259 {/* The workspace's own page: its projects, packages and people. */}
260 <DropdownMenuItem asChild>
261 <Link to={`/${workspace.slug}`}>
262 <LayoutGrid />
263 Workspace overview
264 </Link>
265 </DropdownMenuItem>
266 <DropdownMenuItem asChild>
267 <Link to={`/${workspace.slug}/-/projects`}>
268 <Box />
269 All projects
270 </Link>
271 </DropdownMenuItem>
272 <DropdownMenuSeparator />
273 </>
274 )}
Agents as a team: lifecycle, merge queue, billing and a new shell275 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
276 {(user.workspaces ?? []).map((membership) => (
277 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control278 <Link to={`/${membership.slug}`} title={displayName(membership)}>
279 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
280 <span className="flex min-w-0 grow flex-col leading-tight">
281 <span className="truncate">{displayName(membership)}</span>
282 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
283 </span>
284 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
285 {membership.slug === current ? (
286 <Check className="shrink-0 text-accent" />
287 ) : (
288 <span className="size-4 shrink-0" aria-hidden="true" />
289 )}
Agents as a team: lifecycle, merge queue, billing and a new shell290 </Link>
291 </DropdownMenuItem>
292 ))}
293 <DropdownMenuSeparator />
294 <DropdownMenuItem asChild>
295 <Link to="/workspaces/new">
296 <Plus />
297 New workspace
298 </Link>
299 </DropdownMenuItem>
300 </DropdownMenuContent>
301 </DropdownMenu>
A Vercel-like sidebar and a new mark302 );
303}
304
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas305/**
306 * The fine print, as one quiet row at the foot of the account menu and the
307 * visitor's panel: each link a full-height target, left-aligned. In the
308 * menu each is a menu item, so the arrow keys reach it.
309 */
310const LEGAL_LINKS: [string, string][] = [
311 ["Policies", "/policies"],
312 ["Privacy", "/policies/privacy"],
313 ["Security", "/security"],
314];
315const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
316
317function MenuLegalRow() {
318 return (
319 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
320 {LEGAL_LINKS.map(([label, to], index) => (
321 <span key={to} className="flex items-center gap-0.5">
322 {index > 0 && (
323 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
324 ·
325 </span>
326 )}
327 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
328 <Link to={to}>{label}</Link>
329 </DropdownMenuItem>
330 </span>
331 ))}
332 </div>
333 );
334}
335
336/** Status's row: the live dot and two or three words, fetched when the menu opens. */
337function StatusSummary({ open }: { open: boolean }) {
338 const status = useSiteStatus(open);
339 const words = statusTitle(status);
340 return (
341 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
342 {/* The words say it; the dot's own words for screen readers would repeat them. */}
343 <span aria-hidden="true" className="flex">
344 <StatusDot state={status?.overall.state ?? null} />
345 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents346 {words ? <span className="truncate">{words}</span> : <Skeleton className="h-3 w-20" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas347 </span>
348 );
349}
350
351/** A menu row: an icon, words, and whatever sits at its end. */
352const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
353
A Vercel-like sidebar and a new mark354function AccountMenu({ user }: { user: User }) {
355 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas356 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents357 // Name, primary address and invites left: asked for once, as soon as the
358 // pointer or focus reaches the button, so they are there when it opens.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas359 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents360 const prefetch = () => {
361 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
362 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas363 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents364 if (open) prefetch();
365 // eslint-disable-next-line react-hooks/exhaustive-deps
366 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas367 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents368 // Still on its way: shapes where the words will be, the same size.
369 const loading = details.data === undefined && details.state !== "idle";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas370 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark371 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas372 <DropdownMenu open={open} onOpenChange={setOpen}>
373 <DropdownMenuTrigger
374 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents375 onPointerEnter={prefetch}
376 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas377 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-merged/60 data-[state=open]:bg-raised"
378 >
Workspace names and icons, and a component kit for every control379 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark380 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas381 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark382 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas383 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
384 {/* Who is signed in, and a way to their profile. */}
385 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
386 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
387 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents388 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
389 {loading ? (
390 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
391 ) : (
392 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
393 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas394 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents395 {loading ? (
396 <Skeleton className="mt-1 h-3 w-40" />
397 ) : (
398 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
399 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas400 </span>
A Vercel-like sidebar and a new mark401 </Link>
402 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas403 <DropdownMenuSeparator className="my-1.5" />
404 <DropdownMenuGroup>
405 <DropdownMenuItem asChild className={MENU_ROW}>
406 <Link to={profile}>
407 <CircleUserRound />
408 Your profile
409 </Link>
410 </DropdownMenuItem>
411 <DropdownMenuItem asChild className={MENU_ROW}>
412 <Link to={FIRST_SETTINGS_PAGE}>
413 <Settings />
414 Your settings
415 </Link>
416 </DropdownMenuItem>
417 <DropdownMenuItem asChild className={MENU_ROW}>
418 <Link to="/settings/invites">
419 <Ticket />
420 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents421 {loading && <Skeleton className="ml-auto h-4 w-12 rounded-full" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas422 {me?.invites_left != null && (
423 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
424 {me.invites_left} left
425 </span>
426 )}
427 </Link>
428 </DropdownMenuItem>
429 </DropdownMenuGroup>
430 <DropdownMenuSeparator className="my-1.5" />
431 <DropdownMenuGroup>
432 <DropdownMenuItem asChild className={MENU_ROW}>
433 <a href="https://docs.g1t.sh/">
434 <BookOpen />
435 Documentation
436 </a>
437 </DropdownMenuItem>
438 <DropdownMenuItem asChild className={MENU_ROW}>
439 <Link to="/support">
440 <LifeBuoy />
441 Support
442 </Link>
443 </DropdownMenuItem>
444 <DropdownMenuItem asChild className={MENU_ROW}>
445 <a href={STATUS_URL}>
446 <Activity />
447 Status
448 <StatusSummary open={open} />
449 </a>
450 </DropdownMenuItem>
451 </DropdownMenuGroup>
452 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark453 {/* Submitted from here: the menu closes on select, and a button
454 that has left the page cannot submit a form. */}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas455 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark456 <LogOut />
457 Sign out
458 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas459 <DropdownMenuSeparator className="my-1.5" />
460 <MenuLegalRow />
A Vercel-like sidebar and a new mark461 </DropdownMenuContent>
462 </DropdownMenu>
463 );
464}
465
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look466/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar467 * A workspace's settings pages, which the sidebar drills into: how it is
468 * set up and connected (guardrails, secrets, runners, integrations,
469 * webhooks), what it pays, its repositories, tokens and record. The main
470 * list keeps the places work happens and who belongs; every member can
471 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look472 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar473const SETTINGS_PAGE =
Rules on the site: rulesets for a repository and a workspace, and the merge box474 /^\/([^/]+)\/-\/(settings|repositories|tokens|rules|guardrails|secrets|runners|integrations|webhooks|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look475/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page476const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings477
Polish: phones, copy boxes, the plan page, the landing page, a real glide478/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look479 * The sidebar's lists are a stack, the way a phone pushes a screen: one
480 * drilled into slides in from the right over the full width, while the one
481 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide482 * ease-out, the fade quicker than the move so the two never blur together.
483 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide484 */
Sidebar: no settings flash on the way into a repository, and a better slide485const LAYER =
Sidebar: the panels really slide486 "absolute inset-0 [transition:translate_380ms_cubic-bezier(0.32,0.72,0,1),opacity_220ms_ease-out] will-change-[translate,opacity] motion-reduce:transition-none";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look487/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide488const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings489
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look490/** One list in the stack: what it is, and its rows. */
491type Level = { key: string; node: ReactNode };
492
493/** True once the page has hydrated, so what the server drew never animates in. */
494function useHydrated(): boolean {
495 const [hydrated, setHydrated] = useState(false);
496 useEffect(() => setHydrated(true), []);
497 return hydrated;
498}
499
500/**
501 * Drilling in and out of the sidebar's lists. `trail` is the way from the
502 * main list to the one shown, worked out from the address, so a link
503 * straight to a settings page opens the sidebar already drilled in, and
504 * leaving it comes back out. Lists deeper than the one shown stay in
505 * place, off to the right, so going back slides them away rather than
506 * dropping them. Moving to a list at the same depth (a workspace's settings
507 * to a project) fades the new one in where it is.
508 */
509function Drill({ trail }: { trail: Level[] }) {
510 const depth = trail.length - 1;
511 const hydrated = useHydrated();
512 const stack = useRef<Level[]>(trail);
513 const before = stack.current;
514 const within = trail.every((level, index) => before[index]?.key === level.key);
515 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
516 const levels = stack.current;
517 // What was showing last time, to tell drilling in from moving across.
518 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
519 useEffect(() => {
520 shown.current = { depth, keys: levels.map((level) => level.key) };
521 });
A catalogue of model providers, and settings that feel like settings522 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look523 <div className="relative min-h-0 grow overflow-hidden">
524 {levels.map((level, index) => {
525 const current = index === depth;
526 const place =
527 index < depth
528 ? "pointer-events-none -translate-x-1/4 opacity-0"
529 : current
530 ? "translate-x-0 opacity-100"
531 : "pointer-events-none translate-x-full opacity-0";
532 // A list that was not there before arrives: from the right when
533 // drilling in, in place when moving across.
534 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
535 const enter = arriving
536 ? depth > shown.current.depth
537 ? "starting:translate-x-full starting:opacity-0"
538 : "starting:opacity-0"
539 : "";
540 return (
541 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
542 {level.node}
543 </div>
544 );
545 })}
546 </div>
547 );
548}
549
550/**
551 * The row at the top of a list drilled into: the list's name, which goes
552 * back out, and what it belongs to, faint at the end.
553 */
554function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
555 return (
556 <Link
557 to={to}
558 prefetch="intent"
559 className="group mt-3 flex h-8 items-center gap-1.5 rounded-md pr-2 pl-1 text-[0.8125rem] font-medium text-fg transition-colors hover:bg-raised/60"
A catalogue of model providers, and settings that feel like settings560 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look561 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
562 <span className="min-w-0 shrink-0 truncate">{label}</span>
563 {context && (
564 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
565 )}
566 </Link>
567 );
568}
569
570/** A thin rule between groups of a list. */
571function Rule() {
572 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
573}
574
Navigation: the workspace's own page with tabs, pins and recent in the sidebar575/** A quiet heading inside a group: Pinned, Recent. */
576function SidebarSubhead({ children }: { children: ReactNode }) {
577 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
578}
579
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look580/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar581 * The workspace's projects as the sidebar keeps them, however many there
582 * are: the person's pins in their order, what they opened last, and the
583 * way to all of them. Pins move by dragging, or with Alt and the arrow
584 * keys; the projects service keeps the order.
585 */
586function SidebarProjects({ slug, shell }: { slug: string; shell: ShellData }) {
587 const reorder = useFetcher({ key: `pins:${slug}` });
588 const saved = shell.pinned ?? [];
589 // While a new order is on its way, it shows as made.
590 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
591 const pinned = asked
592 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
593 : saved;
594 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
595 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
596 const [dragging, setDragging] = useState<number | null>(null);
597 const move = (from: number, by: number) => {
598 const next = movedPin(pinned, from, by);
599 if (!next) return;
600 const form = new FormData();
601 form.set("intent", "reorder");
602 for (const project of next) form.append("slug", project.name);
603 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
604 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar605 // Each row pins or unpins in place: the pin shows on hover or focus, and
606 // stays shown on a pinned row's hover so it reads as "unpin".
607 const row = (project: ShortcutProject, isPinned: boolean) => (
608 <div className="group/row relative">
609 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
610 <span className="block truncate pr-6">{project.title ?? project.name}</span>
611 </SidebarLink>
612 <PinButton
613 workspace={slug}
614 slug={project.name}
615 name={project.title ?? project.name}
616 pinned={isPinned}
617 small
618 className="absolute top-1/2 right-6 -translate-y-1/2 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
619 />
620 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar621 );
622 return (
623 <SidebarGroup
624 title="Projects"
625 action={
626 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
627 <Plus size={13} />
628 </Link>
629 }
630 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar631 {/* Only there once something is pinned: each row below has its own pin. */}
632 {pinned.length > 0 && (
633 <>
634 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar635 <ul aria-label="Pinned projects" className="space-y-px">
636 {pinned.map((project, index) => (
637 <li
638 key={project.name}
639 draggable={pinned.length > 1}
640 onDragStart={(event) => {
641 setDragging(index);
642 event.dataTransfer.effectAllowed = "move";
643 }}
644 onDragOver={(event) => {
645 if (dragging != null) event.preventDefault();
646 }}
647 onDrop={(event) => {
648 event.preventDefault();
649 if (dragging != null && dragging !== index) move(dragging, index - dragging);
650 setDragging(null);
651 }}
652 onDragEnd={() => setDragging(null)}
653 onKeyDown={(event) => {
654 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
655 event.preventDefault();
656 move(index, event.key === "ArrowUp" ? -1 : 1);
657 }}
658 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
659 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
660 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar661 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar662 {pinned.length > 1 && (
663 <GripVertical
664 size={12}
665 aria-hidden="true"
666 className="pointer-events-none absolute top-1/2 -left-1.5 -translate-y-1/2 text-faint opacity-0 transition-opacity group-hover/pin:opacity-100"
667 />
668 )}
669 </li>
670 ))}
671 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar672 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar673 )}
674 {recent.length > 0 && (
675 <>
676 <SidebarSubhead>Recent</SidebarSubhead>
677 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar678 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar679 ))}
680 </>
681 )}
682 <div className="pt-1">
683 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length}>
684 All projects
685 </SidebarLink>
686 </div>
687 </SidebarGroup>
688 );
689}
690
691/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look692 * The main list: where you go, the projects, what the workspace builds and
693 * runs with across them, then its usage, support and settings.
694 */
695function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
696 const ws = shell.workspace;
697 // A visitor browses: no workspace, no projects of their own.
698 if (!user) {
699 return (
700 <nav aria-label="g1t" className={PANEL}>
701 <div className="mt-3 space-y-px">
702 {VISITOR_LINKS.map((link) => (
703 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
704 {link.label}
705 </SidebarLink>
706 ))}
707 </div>
708 </nav>
709 );
710 }
711 return (
712 <nav aria-label="g1t" className={PANEL}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar713 {/* Everything here is the workspace the switcher names: its home first.
714 The workspace's own page is the switcher's; Explore, all of g1t,
715 is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look716 <div className="mt-3 space-y-px">
717 <SidebarLink to="/" end icon={<House size={15} />}>
718 Mission control
719 </SidebarLink>
720 </div>
721
722 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar723 {ws ? (
724 <SidebarProjects slug={ws.slug} shell={shell} />
725 ) : (
726 <SidebarGroup title="Projects">
727 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
728 </SidebarGroup>
729 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look730 {(shell.shared ?? []).length > 0 && (
731 <SidebarGroup title="Shared with you" className="mt-3">
732 {(shell.shared ?? []).map((repo) => (
733 <SidebarLink
734 key={`${repo.namespace}/${repo.name}`}
735 to={`/${repo.namespace}/${repo.name}`}
736 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
737 drill="hover"
738 >
739 <span className="font-mono text-faint">{repo.namespace}/</span>
740 {repo.name}
741 </SidebarLink>
742 ))}
743 </SidebarGroup>
744 )}
745
746 {ws && (
747 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member748 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar749 <SidebarGroup title="Workspace">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look750 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
751 Agent fleet
752 </SidebarLink>
753 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
754 Context
755 </SidebarLink>
756 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
757 Memory
758 </SidebarLink>
759 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
760 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents761 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member762 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
763 Packages
764 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar765 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look766 <SidebarSoonLink
767 key={item.key}
768 to={`/${ws.slug}/-/soon/${item.key}`}
769 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
770 about={item.summary}
771 >
772 {item.title === "Board" ? "Boards" : item.title}
773 </SidebarSoonLink>
774 ))}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar775 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look776
777 <Rule />
778 <div className="space-y-px">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar779 {/* Who belongs, for every member to see; owners invite and manage there. */}
780 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar781 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar782 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar783 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams}>
784 Teams
785 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look786 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
787 Usage
788 </SidebarLink>
789 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
790 Support
791 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar792 {/* How it is set up and connected, what it pays and its record: a list of their own. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look793 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member794 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/repositories`}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look795 icon={<Settings size={15} />}
796 drill
797 >
798 Settings
799 </SidebarLink>
800 </div>
801 </>
802 )}
803 </nav>
804 );
805}
806
807/** A workspace's settings, drilled into from Settings in the main list. */
808function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
809 return (
810 <nav aria-label="Workspace settings" className={PANEL}>
811 <BackRow to={`/${slug}`} label="Settings" context={slug} />
812 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings813 {owner && (
814 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
815 General
816 </SidebarLink>
817 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look818 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
819 Repositories
820 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments821 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
822 Access tokens
823 </SidebarLink>
Rules on the site: rulesets for a repository and a workspace, and the merge box824 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />}>
825 Rules
826 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look827 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar828 <SidebarGroup title="Agents and runs" className="mt-3">
829 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
830 Guardrails
831 </SidebarLink>
832 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
833 Secrets and variables
834 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it835 {owner && (
836 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
837 Runners
838 </SidebarLink>
839 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar840 </SidebarGroup>
841 <SidebarGroup title="Connections" className="mt-3">
842 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
843 Integrations
844 </SidebarLink>
845 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
846 Webhooks
847 </SidebarLink>
848 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look849 <Rule />
850 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings851 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments852 Billing and plans
A catalogue of model providers, and settings that feel like settings853 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API854 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
855 Audit log
856 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look857 </div>
A catalogue of model providers, and settings that feel like settings858 </nav>
859 );
860}
861
Sidebar: no settings flash on the way into a repository, and a better slide862/**
863 * What the repository menu needs. Known from the address before the page's
864 * data arrives, so the menu is right from the first frame; the counts fill
865 * in once it does.
866 */
867type MenuRepo = {
868 namespace: string;
869 name: string;
870 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871 /** Whether they see its settings; from the repository's page once it loads. */
872 settings?: boolean;
873 /** What they may do there, once the repository's page loads. */
874 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide875 issues?: number;
876 pulls?: number;
877};
878
879function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
880 return (
881 a != null &&
882 b != null &&
883 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
884 a.name.toLowerCase() === b.name.toLowerCase()
885 );
886}
A repository has its own sidebar, as settings do887
888/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look889 * A project's own list, drilled into while you are in it: everything about
890 * the project, running and its code, and nothing else, with the way back
891 * out to all of them.
A repository has its own sidebar, as settings do892 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put893function RepoMenu({
894 repo,
895 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look896 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put897}: {
898 repo: MenuRepo;
899 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look900 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
901 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put902}) {
A repository has its own sidebar, as settings do903 const base = `/${repo.namespace}/${repo.name}`;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put904 // What a member sees, and what everyone who can see the project does.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look905 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do906 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look907 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
908 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner909 <NavLink
A repository has its own sidebar, as settings do910 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner911 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents912 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner913 title="Overview"
914 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look915 `mt-2 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
A flat project menu with Code first, views as tabs, and Stripe for every owner916 }
A repository has its own sidebar, as settings do917 >
918 <span className="flex size-6 shrink-0 items-center justify-center rounded-md bg-raised text-muted ring-1 ring-line">
Projects: what a workspace builds and runs, first on every page919 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do920 </span>
921 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
922 <span className="text-faint">{repo.namespace}/</span>
923 <span className="font-semibold text-fg">{repo.name}</span>
924 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner925 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look926 {/* A project's pages, in the order people use them. A page with more
927 than one view shows them as tabs across its top. */}
928 <Rule />
929 <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 request930 <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 owner931 Code
932 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit933 <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 owner934 Issues
935 </SidebarLink>
936 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
937 Pull requests
938 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains939 <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 owner940 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains941 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner942 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
943 Workflows
Projects: what a workspace builds and runs, first on every page944 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look945 </div>
946 <Rule />
947 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put948 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner949 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
950 Deployments
951 </SidebarLink>
952 ) : null}
Observability gets its own page; tabs never overflow953 <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.">
954 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner955 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put956 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API957 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
958 Security
959 </SidebarLink>
960 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner961 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
962 Insights
963 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look964 </div>
965 {(repo.settings ?? shows.has("settings")) && (
966 <>
967 <Rule />
968 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner969 Settings
970 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look971 </>
972 )}
A repository has its own sidebar, as settings do973 </nav>
974 );
975}
976
Project settings move into the sidebar, grouped, with a heading per page977/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look978 * A project's settings, drilled into from its list: what is about running
979 * it, then its agents, then its code and who can reach it. The way back
980 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page981 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look982function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page983 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look984 // Each page shows to the roles that can use it, once the role is known.
985 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
986 const running = shows("deployments");
987 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page988 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look989 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
990 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
991 {shows("") && (
992 <div className="mt-2 space-y-px">
993 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
994 General
995 </SidebarLink>
996 </div>
997 )}
998 {running && (
999 <>
1000 {shows("") && <Rule />}
1001 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1002 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1003 Deployments
1004 </SidebarLink>
1005 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1006 Domains
1007 </SidebarLink>
1008 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1009 Dependencies
1010 </SidebarLink>
1011 </div>
1012 </>
1013 )}
1014 {agents && (
1015 <>
1016 <Rule />
1017 <div className="space-y-px">
1018 {shows("agents") && (
1019 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1020 Agents
1021 </SidebarLink>
1022 )}
1023 {shows("guardrails") && (
1024 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1025 Guardrails
1026 </SidebarLink>
1027 )}
1028 </div>
1029 </>
1030 )}
1031 <Rule />
1032 <div className="space-y-px">
1033 {shows("repository") && (
1034 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1035 Repository
1036 </SidebarLink>
1037 )}
1038 {shows("access") && (
1039 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1040 Access
1041 </SidebarLink>
1042 )}
1043 {shows("branches") && (
1044 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1045 Branches and merging
1046 </SidebarLink>
1047 )}
Rules on the site: rulesets for a repository and a workspace, and the merge box1048 {shows("rules") && (
1049 <SidebarLink to={`${base}/settings/rules`} icon={<Scale size={15} />}>
1050 Rules
1051 </SidebarLink>
1052 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1053 {shows("secrets") && (
1054 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1055 Secrets and variables
1056 </SidebarLink>
1057 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1058 {shows("runners") && (
1059 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1060 Runners
1061 </SidebarLink>
1062 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1063 {shows("webhooks") && (
1064 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1065 Webhooks
1066 </SidebarLink>
1067 )}
1068 </div>
Project settings move into the sidebar, grouped, with a heading per page1069 </nav>
1070 );
1071}
1072
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1073/** Your own settings, drilled into from Your settings: one page each. */
1074function AccountSettingsMenu({ username }: { username: string }) {
1075 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1076 <SidebarLink to={`/settings/${page}`} icon={icon}>
1077 {ACCOUNT_SETTINGS[page].title}
1078 </SidebarLink>
Account settings in the same shape as a workspace's1079 );
1080 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1081 <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 quotas1082 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1083 <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 quotas1084 {link("profile", <CircleUserRound size={15} />)}
1085 {link("emails", <Mail size={15} />)}
1086 {link("invites", <Ticket size={15} />)}
1087 </div>
1088 <Rule />
1089 <div className="space-y-px">
1090 {link("keys", <Fingerprint size={15} />)}
1091 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1092 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1093 <Rule />
1094 <div className="space-y-px">
1095 {link("github", <GithubMark className="size-[15px]" />)}
1096 {link("applications", <Plug size={15} />)}
1097 </div>
1098 <Rule />
1099 <div className="space-y-px">
1100 {link("security-log", <History size={15} />)}
1101 </div>
Account settings in the same shape as a workspace's1102 </nav>
1103 );
1104}
1105
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1106/**
1107 * Signing in and signing up, in place of the account for a visitor. Signing
1108 * in brings them back to the page they are on.
1109 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1110/** Help, status and the fine print under a visitor's sign-in buttons. */
1111function VisitorLinks() {
1112 const status = useSiteStatus();
1113 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1114 const dot = (
1115 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1116 ·
1117 </span>
1118 );
1119 return (
1120 // Two short rows, so a narrow sidebar never starts a line with a dot.
1121 <nav aria-label="About g1t" className="pt-0.5">
1122 <div className="flex items-center gap-0.5">
1123 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1124 <StatusDot state={status?.overall.state ?? null} />
1125 Status
1126 </a>
1127 {dot}
1128 <Link to="/support" className={quiet}>
1129 Support
1130 </Link>
1131 </div>
1132 <div className="flex items-center gap-0.5">
1133 {LEGAL_LINKS.map(([label, to], index) => (
1134 <span key={to} className="flex items-center gap-0.5">
1135 {index > 0 && dot}
1136 <Link to={to} className={quiet}>
1137 {label}
1138 </Link>
1139 </span>
1140 ))}
1141 </div>
1142 </nav>
1143 );
1144}
1145
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1146function VisitorPanel() {
1147 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1148 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1149 return (
1150 <div className="space-y-2">
1151 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1152 <div className="grid grid-cols-2 gap-2">
1153 <Link
1154 to={withNext("/login", pathname + search)}
1155 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"
1156 >
1157 <LogIn size={14} />
1158 Sign in
1159 </Link>
1160 <Link
1161 to={withNext("/register", pathname + search)}
1162 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1163 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1164 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1165 </Link>
1166 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1167 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1168 </div>
1169 );
1170}
1171
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1172function Sidebar({
1173 user,
1174 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1175 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1176 onFind,
1177 onClose,
1178}: {
1179 user: User | null;
1180 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1181 /** The page is a 404: the address names nothing the viewer can see. */
1182 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1183 onFind: () => void;
1184 /** In the sheet on a small screen: closing it, at the end of the top row. */
1185 onClose?: () => void;
1186}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1187 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1188 const { pathname } = useLocation();
1189 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1190 // Where the sidebar is drilled to follows the page being gone to, so it
1191 // moves as the link is followed, not once the page arrives.
1192 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1193 // On a 404, the address is not a place: no list is drilled into from it,
1194 // except the project's own when the project is real and visible and only
1195 // something inside it is missing (a file, a commit, an issue).
1196 const lost = missing && going == null;
1197 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1198 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1199 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1200 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1201 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1202 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 look1203 // An invitation to a repository is answered before its menu means anything.
1204 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1205 repoPath != null &&
1206 !reserved.has(repoPath[1]) &&
1207 repoPath[2] !== "-" &&
1208 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1209 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1210 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1211 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1212 // The repository the menu is for: the one loaded if it is the one being
1213 // gone to, else what the address says, at once.
1214 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 look1215 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1216 ? sameRepo(active, targetRepo)
1217 ? active
1218 : {
1219 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1220 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1221 }
1222 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1223 // The viewer's role there, from the repository's page once it has loaded.
1224 const page = useRouteLoaderData("routes/repo/layout") as
1225 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1226 | undefined;
1227 const menuRepo: MenuRepo | null =
1228 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1229 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1230 : guessed;
1231
1232 // The way from the main list to the one shown.
1233 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1234 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1235 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1236 } else if (ws && inSettings) {
1237 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1238 } else if (menuRepo) {
1239 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1240 // Out of a project: to its workspace's projects when they are yours.
1241 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1242 const back = !user
1243 ? { to: "/explore", label: "Explore" }
1244 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1245 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1246 : { to: "/", label: "Mission control" };
1247 trail.push({
1248 key,
1249 node: (
1250 <RepoMenu
1251 repo={menuRepo}
1252 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1253 back={back}
1254 />
1255 ),
1256 });
1257 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1258 }
1259
Agents as a team: lifecycle, merge queue, billing and a new shell1260 return (
1261 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1262 {/* 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 bar1263 <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 put1264 {user ? (
1265 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1266 {/* The 1 alone beside the workspace: a square hover the height of the
1267 switcher, the mark as tall as the workspace avatar. */}
1268 <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 bar1269 <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 put1270 </Link>
1271 <span className="shrink-0 text-line-strong" aria-hidden="true">
1272 /
1273 </span>
1274 <WorkspaceSwitcher user={user} shell={shell} />
1275 </>
1276 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1277 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1278 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1279 </Link>
1280 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1281 {onClose && (
1282 <button
1283 type="button"
1284 aria-label="Close menu"
1285 onClick={onClose}
1286 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1287 >
1288 <X size={16} />
1289 </button>
1290 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1291 </div>
A Vercel-like sidebar and a new mark1292 <div className="px-2 pt-3">
1293 <button
1294 type="button"
1295 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1296 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 mark1297 >
1298 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1299 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1300 <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 mark1301 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1302 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1303 <Drill trail={trail} />
1304 {/* The account, or signing in, as one row at the very bottom. */}
1305 <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 put1306 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1307 </div>
1308 </div>
1309 );
1310}
1311
1312/** Words for the sections a path can end in. */
1313const SECTIONS: Record<string, string> = {
1314 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1315 milestones: "Milestones",
1316 labels: "Labels",
A Vercel-like sidebar and a new mark1317 pulls: "Pull requests",
1318 queue: "Merge queue",
1319 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1320 plans: "Outcomes",
1321 actions: "Workflows",
1322 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1323 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1324 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1325 access: "Access",
1326 invitations: "Invitation",
1327 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1328 branches: "Branches",
1329 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1330 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1331 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1332 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1333 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1334 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1335 people: "People",
1336 projects: "Projects",
1337 teams: "Teams",
A Vercel-like sidebar and a new mark1338 tokens: "Access tokens",
1339 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1340 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1341 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1342 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1343 domains: "Domains",
1344 guardrails: "Guardrails",
Rules on the site: rulesets for a repository and a workspace, and the merge box1345 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1346 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1347 tree: "Files",
1348 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1349 agents: "Agents",
1350 context: "Context",
1351 memory: "Memory",
1352 security: "Security",
1353 packages: "Packages",
1354 runners: "Runners",
1355 workflows: "Workflows",
1356 observability: "Observability",
1357 insights: "Insights",
1358 sessions: "Sessions",
A Vercel-like sidebar and a new mark1359};
1360
1361/** 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 quotas1362function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1363 // A 404 names nothing from the address, unless the project is real and
1364 // visible and only something inside it is missing.
1365 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1366 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1367 return <span className="text-sm font-medium">Not found</span>;
1368 }
A Vercel-like sidebar and a new mark1369 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1370 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1371 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 quotas1372 // Your settings: Settings / Emails.
1373 if (parts[0] === "settings") {
1374 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1375 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 quotas1376 }
A Vercel-like sidebar and a new mark1377 if (reserved.includes(parts[0]!)) {
1378 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1379 settings: "Account",
A Vercel-like sidebar and a new mark1380 explore: "Explore",
Projects live: fixes from walking them end to end1381 new: "New project",
A Vercel-like sidebar and a new mark1382 search: "Search",
1383 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1384 policies: "Policies",
1385 security: "Security",
1386 support: "Support",
1387 status: "Status",
1388 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1389 inbox: "Inbox",
A Vercel-like sidebar and a new mark1390 };
1391 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1392 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1393 // A person's profile, by their handle.
1394 if (parts[0] === "u" && parts[1]) {
1395 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1396 }
A Vercel-like sidebar and a new mark1397 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1398 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1399 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1400 const page = `/${owner}/-/${third}`;
1401 if (third && SETTINGS_PAGE.test(page)) {
1402 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1403 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1404 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1405 } else if (second) {
1406 const repo = `/${owner}/${second}`;
1407 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1408 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1409 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 mark1410 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 deployments1411 else if (third && SECTIONS[third]) {
1412 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1413 // A settings page names which one: Settings / Secrets and variables.
1414 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1415 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 deployments1416 }
1417 }
A Vercel-like sidebar and a new mark1418 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1419 return <Trail trail={trail} />;
1420}
1421
1422/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1423type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1424
1425/**
1426 * The trail of links. On a phone there is room for one: the page itself,
1427 * with a way back to where it sits; the whole trail from a wider screen up.
1428 */
1429function Trail({ trail }: { trail: Crumb[] }) {
1430 const up = trail.length > 1 ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1431 return (
1432 <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 audit1433 {up && (
1434 <Link
1435 to={up.to}
1436 prefetch="intent"
1437 aria-label={`Back to ${up.label}`}
1438 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"
1439 >
1440 <ChevronLeft size={16} />
1441 </Link>
1442 )}
1443 {trail.map((crumb, index) => {
1444 const last = index === trail.length - 1;
1445 return (
1446 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1447 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1448 <Link
1449 to={crumb.to}
1450 prefetch="intent"
1451 aria-current={last ? "page" : undefined}
1452 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1453 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1454 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1455 >
1456 {last && crumb.short ? (
1457 <>
1458 <span className="sm:hidden">{crumb.short}</span>
1459 <span className="hidden sm:inline">{crumb.label}</span>
1460 </>
1461 ) : (
1462 crumb.label
1463 )}
1464 </Link>
1465 </span>
1466 );
1467 })}
A Vercel-like sidebar and a new mark1468 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1469 );
1470}
1471
Search across all of g1t, Explore, and a command palette1472type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1473
1474/** 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 look1475function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1476 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1477 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1478 { 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 step1479 ...(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-agent1480 ? [{ 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 step1481 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1482 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1483 { 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 all1484 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1485 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1486 { 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 quotas1487 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1488 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1489 .filter((page) => page !== "profile")
1490 .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 shell1491 ];
1492 const repo = shell.repo;
1493 if (repo) {
1494 const base = `/${repo.namespace}/${repo.name}`;
1495 const name = `${repo.namespace}/${repo.name}`;
1496 commands.unshift(
Projects: what a workspace builds and runs, first on every page1497 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1498 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1499 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1500 { 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 step1501 ...(repo.member
1502 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1503 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1504 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1505 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1506 ...(repo.member
1507 ? [
1508 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1509 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1510 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1511 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1512 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1513 ]
1514 : []),
1515 );
1516 }
1517 for (const membership of user.workspaces ?? []) {
1518 commands.push(
Workspace names and icons, and a component kit for every control1519 { 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 sidebar1520 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1521 { 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 request1522 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1523 { 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 settings1524 { 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 look1525 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1526 );
1527 }
1528 for (const listed of shell.repos) {
1529 commands.push({
Projects: what a workspace builds and runs, first on every page1530 label: listed.title ?? listed.name,
1531 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1532 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1533 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1534 });
1535 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1536 // "Put an agent on …": one per project, after the projects themselves.
1537 for (const listed of shell.repos) {
1538 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1539 commands.push({
1540 label: `Put an agent on ${listed.title ?? listed.name}`,
1541 hint: `${listed.namespace}/${listed.name}`,
1542 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1543 icon: <Sparkles size={15} />,
1544 });
1545 }
Agents as a team: lifecycle, merge queue, billing and a new shell1546 return commands;
1547}
1548
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1549/** 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 look1550function 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 put1551 const commands: Command[] = [];
1552 const repo = shell.repo;
1553 if (repo) {
1554 const base = `/${repo.namespace}/${repo.name}`;
1555 const name = `${repo.namespace}/${repo.name}`;
1556 commands.push(
1557 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1558 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1559 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1560 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1561 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1562 );
1563 }
1564 commands.push(
1565 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1566 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1567 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1568 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1569 { 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 look1570 { 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 put1571 );
1572 return commands;
1573}
1574
Agents as a team: lifecycle, merge queue, billing and a new shell1575/**
Merge branch 'worktree-agent-aca4790ea223aa235'1576 * The page while the next one loads: after a moment it dims, under the
1577 * bar below, so it is plainly on its way out rather than frozen, and the
1578 * next page fades up from there. Sending a form does not dim it: the
1579 * form's button says it is working. Spread on the element holding the page.
1580 */
1581export function useLeaving() {
1582 const navigation = useNavigation();
1583 const method = navigation.formMethod?.toUpperCase();
1584 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1585 return {
1586 "aria-busy": leaving || undefined,
1587 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1588 };
1589}
1590
1591/**
Agents as a team: lifecycle, merge queue, billing and a new shell1592 * A bar across the top of the page while the next one loads, as GitHub
1593 * has: it appears at once, creeps towards the end while waiting, then fills
1594 * and fades when the page arrives.
1595 */
1596export function Progress() {
1597 const navigation = useNavigation();
1598 const busy = navigation.state !== "idle";
1599 const [width, setWidth] = useState(0);
1600 const [visible, setVisible] = useState(false);
1601 useEffect(() => {
1602 if (busy) {
1603 setVisible(true);
1604 setWidth(12);
1605 // Slows as it goes, never quite arriving until the page does.
1606 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1607 return () => clearInterval(timer);
1608 }
1609 setWidth((now) => (now > 0 ? 100 : 0));
1610 const fade = setTimeout(() => setVisible(false), 250);
1611 const reset = setTimeout(() => setWidth(0), 550);
1612 return () => {
1613 clearTimeout(fade);
1614 clearTimeout(reset);
1615 };
1616 }, [busy]);
1617 return (
1618 <div
1619 aria-hidden="true"
1620 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1621 visible ? "opacity-100" : "opacity-0"
1622 }`}
1623 >
1624 <div
1625 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1626 style={{ width: `${width}%` }}
1627 />
1628 </div>
1629 );
1630}
1631
1632/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1633 * The app: a sidebar with the workspace, its repositories and the sections
1634 * of the one being looked at; a slim bar with search and the account; and
1635 * the page. A visitor who is not signed in gets the same frame, with
1636 * Explore and Search in place of the workspace, and signing in in place of
1637 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1638 */
1639export function AppShell({
1640 user,
1641 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1642 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1643 banner,
1644 children,
1645}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1646 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1647 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1648 /** The page is a 404 (see Sidebar). */
1649 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1650 banner?: ReactNode;
1651 children: ReactNode;
1652}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1653 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1654 const [drawer, setDrawer] = useState(false);
1655 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1656 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1657 const signUpLabel = useSignUpCopy().primary;
1658 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1659
1660 // A new page closes the drawer on small screens.
1661 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1662 // Mission control's code, fetched while the browser is idle, so going
1663 // home never waits on it (routes/home.tsx).
1664 useEffect(() => {
1665 if (!user) return;
1666 const load = () => void import("./mission-control").catch(() => undefined);
1667 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1668 if (idle) idle(load);
1669 else setTimeout(load, 1500);
1670 }, [user]);
Search across all of g1t, Explore, and a command palette1671 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1672 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1673
1674 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1675 // The phone's menu is a sheet: a dialog that holds focus, closes on
1676 // Escape or a tap outside, and gives focus back to the menu button.
1677 <Sheet open={drawer} onOpenChange={setDrawer}>
Agents as a team: lifecycle, merge queue, billing and a new shell1678 <div className="min-h-screen">
1679 <Progress />
1680 <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 quotas1681 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1682 </aside>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1683 <SheetContent side="left" showClose={false} aria-describedby={undefined} className="w-72 max-w-[85vw] sm:max-w-72 lg:hidden">
1684 <SheetTitle className="sr-only">Menu</SheetTitle>
1685 <Sidebar
1686 user={user}
1687 shell={shell}
1688 missing={missing}
1689 onFind={() => {
1690 setDrawer(false);
1691 setPalette(true);
1692 }}
1693 onClose={() => setDrawer(false)}
1694 />
1695 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell1696
Polish: phones, copy boxes, the plan page, the landing page, a real glide1697 <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 bar1698 <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 audit1699 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell1700 aria-label="Open menu"
1701 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1702 >
1703 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1704 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1705 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1706 <div className="ml-auto flex items-center gap-1.5">
1707 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1708 <button
1709 type="button"
1710 aria-label="Search or jump to"
1711 onClick={() => setPalette(true)}
1712 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1713 >
1714 <Search size={16} />
1715 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1716 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1717 <NavLink
1718 to="/explore"
1719 prefetch="intent"
1720 aria-label="Explore"
1721 className={({ isActive }) =>
1722 `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"}`
1723 }
1724 >
1725 <Compass size={16} className="sm:hidden" />
1726 <span className="hidden sm:inline">Explore</span>
1727 </NavLink>
A Vercel-like sidebar and a new mark1728 <a
1729 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1730 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 mark1731 >
1732 Docs
1733 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1734 {user && (
1735 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1736 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1737 <InboxBell counts={shell.inbox ?? null} />
1738 </>
1739 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1740 {!user ? (
1741 // The sidebar has these too, but on a phone it is folded away.
1742 <Link
1743 to={withNext("/login", here)}
1744 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"
1745 >
1746 Sign in
1747 </Link>
1748 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1749 <>
1750 {/* Room between what comes in (the inbox) and what you make. */}
1751 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1752 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1753 <Tooltip>
1754 <TooltipTrigger asChild>
1755 <DropdownMenuTrigger
1756 aria-label="Create new"
1757 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"
1758 >
1759 <Plus size={16} />
1760 <ChevronDown size={13} className="text-muted" />
1761 </DropdownMenuTrigger>
1762 </TooltipTrigger>
1763 <TooltipContent>Create new…</TooltipContent>
1764 </Tooltip>
1765 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1766 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1767 <>
1768 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1769 <DropdownMenuItem asChild>
1770 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1771 <CircleDot />
1772 New issue
1773 </Link>
1774 </DropdownMenuItem>
1775 {shell.repo.member && (
1776 <DropdownMenuItem asChild>
1777 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1778 <ListTree />
1779 Plan work
1780 </Link>
1781 </DropdownMenuItem>
1782 )}
1783 <DropdownMenuSeparator />
1784 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1785 )}
1786 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1787 <Link to="/?agent=new">
1788 <Sparkles />
1789 Put an agent on it
1790 </Link>
1791 </DropdownMenuItem>
1792 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1793 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1794 <Box />
1795 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1796 </Link>
1797 </DropdownMenuItem>
1798 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1799 <Link to="/new/github">
1800 <GitBranch />
1801 Import from GitHub
1802 </Link>
1803 </DropdownMenuItem>
1804 <DropdownMenuSeparator />
1805 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1806 <Link to="/workspaces/new">
1807 <Users />
1808 New workspace
1809 </Link>
1810 </DropdownMenuItem>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1811 {/* A team in the workspace the sidebar is about. */}
1812 {shell.workspace && (
1813 <DropdownMenuItem asChild>
1814 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1815 <UsersRound />
1816 New team
1817 </Link>
1818 </DropdownMenuItem>
1819 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1820 </DropdownMenuContent>
1821 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1822 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1823 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1824 </div>
1825 </header>
1826 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1827 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'1828 {children}
1829 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1830 </div>
Search across all of g1t, Explore, and a command palette1831 <CommandPalette
1832 open={palette}
1833 onOpenChange={setPalette}
1834 commands={commands}
1835 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1836 />
Agents as a team: lifecycle, merge queue, billing and a new shell1837 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1838 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell1839 );
1840}

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