Skip to content
1,931 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.

Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, Inbox, KanbanSquare, KeyRound, Layers, LayoutDashboard, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Scale, Sparkles, Ticket, TrendingUp, UserRoundKey, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Merge branch 'worktree-agent-ad7c6d88d93adc817'5import { type Abilities, type InboxCounts, type Membership, type Spike, type User, mayCreateTeams } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in7import { InMain } from "./landmark";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit8import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar9import { AgentButton, InboxBell } from "./inbox";
10import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar11import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Merge branch 'worktree-agent-ae1299e92e4462012'12import { Hint } from "./ui/hint";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13import { 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 quotas14import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height15import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit16import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents17import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell18import {
19 DropdownMenu,
20 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas21 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell22 DropdownMenuItem,
23 DropdownMenuLabel,
24 DropdownMenuSeparator,
25 DropdownMenuTrigger,
26} from "./ui/dropdown-menu";
Merge branch 'worktree-agent-ad7c6d88d93adc817'27import { type RoadmapItem, roadmapIn, roadmapItem } from "../lib/roadmap";
28import { SETTINGS_PAGES, sidebarCurrent } from "../lib/workspace-nav";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29import { 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 put30import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas31import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
32import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put33import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look34import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas35import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar36import { 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 quotas37import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell38
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put39/**
40 * What the sidebar needs, worked out by the root loader. For a visitor who
41 * is not signed in there is no workspace, no projects and no usage: only
42 * the project being looked at, if they can see it.
43 */
Agents as a team: lifecycle, merge queue, billing and a new shell44export type ShellData = {
45 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control46 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar47 /** All its projects, by name, for the palette: `name` is the slug in their address. */
48 repos: ShortcutProject[];
49 /** The person's pinned projects in it, in their order (lib/pins.ts). */
50 pinned?: ShortcutProject[];
51 /** What they opened there last, latest first, leaving out the pinned. */
52 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53 /** Repositories shared with them in workspaces they do not belong to. */
54 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page55 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell56 repo: {
57 namespace: string;
58 name: string;
59 member: boolean;
60 issues: number;
61 pulls: number;
62 } | null;
Billing accounts, terms and enterprises; g1t is no longer free63 /** Where the workspace stands against its usage limit, if billing is on. */
64 limit: {
65 exposureMicros: number;
66 ceilingMicros: number | null;
67 state: "ok" | "warning" | "stopped";
68 comped: boolean;
69 } | null;
Free while g1t is being built out; agents can check out their own forks70 /** Whether g1t charges nothing for now, while it is being built out. */
71 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request72 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy73 /** This month's usage: charged, or at cost while g1t is free. */
74 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
76 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 all77 /** What is unread in their inbox, for the bell. Absent for a visitor. */
78 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell79};
80
81function SidebarLink({
82 to,
83 icon,
84 end,
85 count,
86 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look87 drill,
Merge branch 'worktree-agent-ad7c6d88d93adc817'88 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell89 children,
90}: {
91 to: string;
92 icon: ReactNode;
93 end?: boolean;
94 count?: number;
Projects: what a workspace builds and runs, first on every page95 /** Other path prefixes under which this link is the current one. */
96 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 /** It opens a list of its own: a chevron says so, always or on hover. */
98 drill?: boolean | "hover";
Merge branch 'worktree-agent-ad7c6d88d93adc817'99 /** Whether it is the current row, when the list works that out itself. */
100 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell101 children: ReactNode;
102}) {
103 const { pathname } = useLocation();
104 return (
105 <NavLink
106 to={to}
107 end={end}
108 prefetch="intent"
109 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page110 const current =
Merge branch 'worktree-agent-ad7c6d88d93adc817'111 lit ??
112 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark113 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 shell114 current
115 ? "bg-raised font-medium text-fg"
116 : isPending
117 ? "bg-raised/60 text-fg"
118 : "text-muted hover:bg-raised/60 hover:text-fg"
119 }`;
120 }}
121 >
122 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
123 <span className="min-w-0 grow truncate">{children}</span>
124 {count != null && count > 0 && (
125 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
126 {count}
127 </span>
128 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look129 {drill && (
130 <ChevronRight
131 size={14}
132 aria-hidden="true"
133 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
134 />
135 )}
Agents as a team: lifecycle, merge queue, billing and a new shell136 </NavLink>
137 );
138}
139
Pull requests say where they are live; the project menu shows what is coming140/**
141 * Something a project will have, shown where it will live so people can
142 * see where g1t is going. Not a link: nothing is behind it yet.
143 */
144function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
145 return (
Merge branch 'worktree-agent-ae1299e92e4462012'146 <Hint label={about} side="right">
147 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
148 <span className="shrink-0 opacity-70">{icon}</span>
149 <span className="grow truncate">{children}</span>
150 <span className="sr-only">{about}</span>
151 <SoonPill />
152 </div>
153 </Hint>
Pull requests say where they are live; the project menu shows what is coming154 );
155}
156
The project menu opens by section, and the workspace has its own panel157/**
A flat project menu with Code first, views as tabs, and Stripe for every owner158 * A page that is coming, as a link to its roadmap page: faint, with Soon,
159 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel160 */
A flat project menu with Code first, views as tabs, and Stripe for every owner161function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel162 to,
163 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner164 icon,
165 about,
Merge branch 'worktree-agent-ad7c6d88d93adc817'166 current: lit,
The project menu opens by section, and the workspace has its own panel167 children,
168}: {
169 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner170 also?: string[];
171 icon: ReactNode;
172 about: string;
Merge branch 'worktree-agent-ad7c6d88d93adc817'173 /** Whether it is the current row, when the list works that out itself. */
174 current?: boolean;
The project menu opens by section, and the workspace has its own panel175 children: ReactNode;
176}) {
177 const { pathname } = useLocation();
178 return (
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero179 // The hint wraps a plain box: Radix's asChild merges className as a
180 // string, which would break NavLink's className function.
Merge branch 'worktree-agent-ae1299e92e4462012'181 <Hint label={about} side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero182 <div>
The project menu opens by section, and the workspace has its own panel183 <NavLink
184 to={to}
185 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner186 className={({ isActive }) => {
Merge branch 'worktree-agent-ad7c6d88d93adc817'187 const current = lit ?? (isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A flat project menu with Code first, views as tabs, and Stripe for every owner188 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
189 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 panel190 }`;
191 }}
192 >
A flat project menu with Code first, views as tabs, and Stripe for every owner193 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel194 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit195 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales196 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero197 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'198 </Hint>
The project menu opens by section, and the workspace has its own panel199 );
200}
201
A flat project menu with Code first, views as tabs, and Stripe for every owner202/** Icons for what spans projects. */
203const WORKSPACE_ICONS: Record<string, ReactNode> = {
204 board: <KanbanSquare size={15} />,
205 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member206 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner207 packages: <Package size={15} />,
208 fleet: <Bot size={15} />,
209};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales210
A flat project menu with Code first, views as tabs, and Stripe for every owner211/** The Soon pages of a section, as paths, so its link is current on them. */
212function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
213 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales214}
215
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look216function SidebarGroup({
217 title,
218 action,
219 className = "",
220 children,
221}: {
222 title: string;
223 action?: ReactNode;
224 className?: string;
225 children: ReactNode;
226}) {
Agents as a team: lifecycle, merge queue, billing and a new shell227 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look228 <section className={className}>
A Vercel-like sidebar and a new mark229 <div className="mb-1 flex h-6 items-center justify-between px-2">
230 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell231 {action}
232 </div>
233 <div className="space-y-px">{children}</div>
234 </section>
235 );
236}
237
Workspace names and icons, and a component kit for every control238/** What a workspace is called where people read it: its display name, else its slug. */
239function displayName(membership: Membership): string {
240 return membership.name?.trim() || membership.slug;
241}
242
Agents as a team: lifecycle, merge queue, billing and a new shell243function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control244 const workspace = shell.workspace;
245 const current = workspace?.slug;
246 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell247 return (
248 <DropdownMenu>
Workspace names and icons, and a component kit for every control249 {/* The name goes to the workspace; only the arrows switch it. The
250 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'251 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark252 <Link
253 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents254 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look255 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 mark256 >
Workspace names and icons, and a component kit for every control257 {workspace ? (
258 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell259 ) : (
Workspace names and icons, and a component kit for every control260 <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 shell261 )}
Workspace names and icons, and a component kit for every control262 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark263 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'264 </Hint>
A Vercel-like sidebar and a new mark265 <DropdownMenuTrigger
266 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit267 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 mark268 >
269 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell270 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control271 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar272 {workspace && (
273 <>
274 {/* The workspace's own page: its projects, packages and people. */}
275 <DropdownMenuItem asChild>
276 <Link to={`/${workspace.slug}`}>
277 <LayoutGrid />
278 Workspace overview
279 </Link>
280 </DropdownMenuItem>
281 <DropdownMenuItem asChild>
282 <Link to={`/${workspace.slug}/-/projects`}>
283 <Box />
284 All projects
285 </Link>
286 </DropdownMenuItem>
287 <DropdownMenuSeparator />
288 </>
289 )}
Agents as a team: lifecycle, merge queue, billing and a new shell290 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
291 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'292 <Hint key={membership.slug} label={displayName(membership)} side="right">
293 <DropdownMenuItem asChild>
294 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control295 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
296 <span className="flex min-w-0 grow flex-col leading-tight">
297 <span className="truncate">{displayName(membership)}</span>
298 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
299 </span>
300 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
301 {membership.slug === current ? (
302 <Check className="shrink-0 text-accent" />
303 ) : (
304 <span className="size-4 shrink-0" aria-hidden="true" />
305 )}
Agents as a team: lifecycle, merge queue, billing and a new shell306 </Link>
307 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'308 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell309 ))}
310 <DropdownMenuSeparator />
311 <DropdownMenuItem asChild>
312 <Link to="/workspaces/new">
313 <Plus />
314 New workspace
315 </Link>
316 </DropdownMenuItem>
317 </DropdownMenuContent>
318 </DropdownMenu>
A Vercel-like sidebar and a new mark319 );
320}
321
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas322/**
323 * The fine print, as one quiet row at the foot of the account menu and the
324 * visitor's panel: each link a full-height target, left-aligned. In the
325 * menu each is a menu item, so the arrow keys reach it.
326 */
327const LEGAL_LINKS: [string, string][] = [
328 ["Policies", "/policies"],
329 ["Privacy", "/policies/privacy"],
330 ["Security", "/security"],
331];
332const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
333
334function MenuLegalRow() {
335 return (
336 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
337 {LEGAL_LINKS.map(([label, to], index) => (
338 <span key={to} className="flex items-center gap-0.5">
339 {index > 0 && (
340 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
341 ·
342 </span>
343 )}
344 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
345 <Link to={to}>{label}</Link>
346 </DropdownMenuItem>
347 </span>
348 ))}
349 </div>
350 );
351}
352
353/** Status's row: the live dot and two or three words, fetched when the menu opens. */
354function StatusSummary({ open }: { open: boolean }) {
355 const status = useSiteStatus(open);
356 const words = statusTitle(status);
357 return (
358 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
359 {/* The words say it; the dot's own words for screen readers would repeat them. */}
360 <span aria-hidden="true" className="flex">
361 <StatusDot state={status?.overall.state ?? null} />
362 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents363 {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 quotas364 </span>
365 );
366}
367
368/** A menu row: an icon, words, and whatever sits at its end. */
369const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
370
A Vercel-like sidebar and a new mark371function AccountMenu({ user }: { user: User }) {
372 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas373 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents374 // Name, primary address and invites left: asked for once, as soon as the
375 // 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 quotas376 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents377 const prefetch = () => {
378 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
379 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas380 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents381 if (open) prefetch();
382 // eslint-disable-next-line react-hooks/exhaustive-deps
383 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas384 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents385 // Still on its way: shapes where the words will be, the same size.
386 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 quotas387 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark388 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas389 <DropdownMenu open={open} onOpenChange={setOpen}>
390 <DropdownMenuTrigger
391 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents392 onPointerEnter={prefetch}
393 onFocus={prefetch}
Merge branch 'worktree-agent-ad7c6d88d93adc817'394 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent/60 data-[state=open]:bg-raised"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas395 >
Workspace names and icons, and a component kit for every control396 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark397 <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 quotas398 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark399 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas400 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
401 {/* Who is signed in, and a way to their profile. */}
402 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
403 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
404 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents405 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
406 {loading ? (
407 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
408 ) : (
409 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
410 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas411 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents412 {loading ? (
413 <Skeleton className="mt-1 h-3 w-40" />
414 ) : (
415 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
416 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas417 </span>
A Vercel-like sidebar and a new mark418 </Link>
419 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas420 <DropdownMenuSeparator className="my-1.5" />
421 <DropdownMenuGroup>
422 <DropdownMenuItem asChild className={MENU_ROW}>
423 <Link to={profile}>
424 <CircleUserRound />
425 Your profile
426 </Link>
427 </DropdownMenuItem>
428 <DropdownMenuItem asChild className={MENU_ROW}>
429 <Link to={FIRST_SETTINGS_PAGE}>
430 <Settings />
431 Your settings
432 </Link>
433 </DropdownMenuItem>
434 <DropdownMenuItem asChild className={MENU_ROW}>
435 <Link to="/settings/invites">
436 <Ticket />
437 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents438 {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 quotas439 {me?.invites_left != null && (
440 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
441 {me.invites_left} left
442 </span>
443 )}
444 </Link>
445 </DropdownMenuItem>
446 </DropdownMenuGroup>
447 <DropdownMenuSeparator className="my-1.5" />
448 <DropdownMenuGroup>
449 <DropdownMenuItem asChild className={MENU_ROW}>
450 <a href="https://docs.g1t.sh/">
451 <BookOpen />
452 Documentation
453 </a>
454 </DropdownMenuItem>
455 <DropdownMenuItem asChild className={MENU_ROW}>
456 <Link to="/support">
457 <LifeBuoy />
458 Support
459 </Link>
460 </DropdownMenuItem>
461 <DropdownMenuItem asChild className={MENU_ROW}>
462 <a href={STATUS_URL}>
463 <Activity />
464 Status
465 <StatusSummary open={open} />
466 </a>
467 </DropdownMenuItem>
468 </DropdownMenuGroup>
469 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark470 {/* Submitted from here: the menu closes on select, and a button
471 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 quotas472 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark473 <LogOut />
474 Sign out
475 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas476 <DropdownMenuSeparator className="my-1.5" />
477 <MenuLegalRow />
A Vercel-like sidebar and a new mark478 </DropdownMenuContent>
479 </DropdownMenu>
480 );
481}
482
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look483/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar484 * A workspace's settings pages, which the sidebar drills into: how it is
485 * set up and connected (guardrails, secrets, runners, integrations,
486 * webhooks), what it pays, its repositories, tokens and record. The main
487 * list keeps the places work happens and who belongs; every member can
488 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look489 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar490const SETTINGS_PAGE =
Merge branch 'worktree-agent-ad7c6d88d93adc817'491 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look492/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page493const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings494
Polish: phones, copy boxes, the plan page, the landing page, a real glide495/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look496 * The sidebar's lists are a stack, the way a phone pushes a screen: one
497 * drilled into slides in from the right over the full width, while the one
498 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide499 * ease-out, the fade quicker than the move so the two never blur together.
500 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide501 */
Sidebar: no settings flash on the way into a repository, and a better slide502const LAYER =
Sidebar: the panels really slide503 "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 look504/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide505const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings506
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look507/** One list in the stack: what it is, and its rows. */
508type Level = { key: string; node: ReactNode };
509
510/** True once the page has hydrated, so what the server drew never animates in. */
511function useHydrated(): boolean {
512 const [hydrated, setHydrated] = useState(false);
513 useEffect(() => setHydrated(true), []);
514 return hydrated;
515}
516
517/**
518 * Drilling in and out of the sidebar's lists. `trail` is the way from the
519 * main list to the one shown, worked out from the address, so a link
520 * straight to a settings page opens the sidebar already drilled in, and
521 * leaving it comes back out. Lists deeper than the one shown stay in
522 * place, off to the right, so going back slides them away rather than
523 * dropping them. Moving to a list at the same depth (a workspace's settings
524 * to a project) fades the new one in where it is.
525 */
526function Drill({ trail }: { trail: Level[] }) {
527 const depth = trail.length - 1;
528 const hydrated = useHydrated();
529 const stack = useRef<Level[]>(trail);
530 const before = stack.current;
531 const within = trail.every((level, index) => before[index]?.key === level.key);
532 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
533 const levels = stack.current;
534 // What was showing last time, to tell drilling in from moving across.
535 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
536 useEffect(() => {
537 shown.current = { depth, keys: levels.map((level) => level.key) };
538 });
A catalogue of model providers, and settings that feel like settings539 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look540 <div className="relative min-h-0 grow overflow-hidden">
541 {levels.map((level, index) => {
542 const current = index === depth;
543 const place =
544 index < depth
545 ? "pointer-events-none -translate-x-1/4 opacity-0"
546 : current
547 ? "translate-x-0 opacity-100"
548 : "pointer-events-none translate-x-full opacity-0";
549 // A list that was not there before arrives: from the right when
550 // drilling in, in place when moving across.
551 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
552 const enter = arriving
553 ? depth > shown.current.depth
554 ? "starting:translate-x-full starting:opacity-0"
555 : "starting:opacity-0"
556 : "";
557 return (
558 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
559 {level.node}
560 </div>
561 );
562 })}
563 </div>
564 );
565}
566
567/**
568 * The row at the top of a list drilled into: the list's name, which goes
569 * back out, and what it belongs to, faint at the end.
570 */
571function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
572 return (
573 <Link
574 to={to}
575 prefetch="intent"
576 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 settings577 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look578 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
579 <span className="min-w-0 shrink-0 truncate">{label}</span>
580 {context && (
581 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
582 )}
583 </Link>
584 );
585}
586
587/** A thin rule between groups of a list. */
588function Rule() {
589 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
590}
591
Navigation: the workspace's own page with tabs, pins and recent in the sidebar592/** A quiet heading inside a group: Pinned, Recent. */
593function SidebarSubhead({ children }: { children: ReactNode }) {
594 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
595}
596
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look597/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar598 * The workspace's projects as the sidebar keeps them, however many there
599 * are: the person's pins in their order, what they opened last, and the
600 * way to all of them. Pins move by dragging, or with Alt and the arrow
601 * keys; the projects service keeps the order.
602 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'603function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar604 const reorder = useFetcher({ key: `pins:${slug}` });
605 const saved = shell.pinned ?? [];
606 // While a new order is on its way, it shows as made.
607 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
608 const pinned = asked
609 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
610 : saved;
611 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
612 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
613 const [dragging, setDragging] = useState<number | null>(null);
614 const move = (from: number, by: number) => {
615 const next = movedPin(pinned, from, by);
616 if (!next) return;
617 const form = new FormData();
618 form.set("intent", "reorder");
619 for (const project of next) form.append("slug", project.name);
620 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
621 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar622 // Each row pins or unpins in place: the pin shows on hover or focus, and
623 // stays shown on a pinned row's hover so it reads as "unpin".
624 const row = (project: ShortcutProject, isPinned: boolean) => (
625 <div className="group/row relative">
626 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
627 <span className="block truncate pr-6">{project.title ?? project.name}</span>
628 </SidebarLink>
629 <PinButton
630 workspace={slug}
631 slug={project.name}
632 name={project.title ?? project.name}
633 pinned={isPinned}
634 small
635 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"
636 />
637 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar638 );
639 return (
640 <SidebarGroup
641 title="Projects"
642 action={
643 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
644 <Plus size={13} />
645 </Link>
646 }
647 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar648 {/* Only there once something is pinned: each row below has its own pin. */}
649 {pinned.length > 0 && (
650 <>
651 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar652 <ul aria-label="Pinned projects" className="space-y-px">
653 {pinned.map((project, index) => (
654 <li
655 key={project.name}
656 draggable={pinned.length > 1}
657 onDragStart={(event) => {
658 setDragging(index);
659 event.dataTransfer.effectAllowed = "move";
660 }}
661 onDragOver={(event) => {
662 if (dragging != null) event.preventDefault();
663 }}
664 onDrop={(event) => {
665 event.preventDefault();
666 if (dragging != null && dragging !== index) move(dragging, index - dragging);
667 setDragging(null);
668 }}
669 onDragEnd={() => setDragging(null)}
670 onKeyDown={(event) => {
671 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
672 event.preventDefault();
673 move(index, event.key === "ArrowUp" ? -1 : 1);
674 }}
675 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
676 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar677 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar678 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'679 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
680 <span className="absolute top-1/2 -left-1.5 -translate-y-1/2 cursor-grab text-faint opacity-0 transition-opacity group-hover/pin:opacity-100">
681 <GripVertical size={12} aria-hidden="true" />
682 </span>
683 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar684 )}
685 </li>
686 ))}
687 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar688 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar689 )}
690 {recent.length > 0 && (
691 <>
692 <SidebarSubhead>Recent</SidebarSubhead>
693 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar694 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar695 ))}
696 </>
697 )}
698 <div className="pt-1">
Merge branch 'worktree-agent-ad7c6d88d93adc817'699 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length} current={current}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar700 All projects
701 </SidebarLink>
702 </div>
703 </SidebarGroup>
704 );
705}
706
707/**
Merge branch 'worktree-agent-ad7c6d88d93adc817'708 * The main list, in two parts a rule apart. Above it, what is yours
709 * whichever workspace you are in: Mission control, your inbox, and what
710 * others have shared with you. Below it, under the workspace's name, the
711 * workspace: its overview, its projects, what it builds and runs with
712 * across them, then its people, usage, support and settings. One row is
713 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look714 */
715function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
716 const ws = shell.workspace;
Merge branch 'worktree-agent-ad7c6d88d93adc817'717 const { pathname } = useLocation();
718 const going = useNavigation().location?.pathname;
719 // The row lights as the link is followed, not once the page arrives.
720 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look721 // A visitor browses: no workspace, no projects of their own.
722 if (!user) {
723 return (
724 <nav aria-label="g1t" className={PANEL}>
725 <div className="mt-3 space-y-px">
726 {VISITOR_LINKS.map((link) => (
727 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
728 {link.label}
729 </SidebarLink>
730 ))}
731 </div>
732 </nav>
733 );
734 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'735 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look736 return (
737 <nav aria-label="g1t" className={PANEL}>
Merge branch 'worktree-agent-ad7c6d88d93adc817'738 {/* Yours, in every workspace. Explore, all of g1t, is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look739 <div className="mt-3 space-y-px">
Merge branch 'worktree-agent-ad7c6d88d93adc817'740 <SidebarLink to="/" end icon={<House size={15} />} current={here === "mission"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look741 Mission control
742 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'743 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
744 Inbox
745 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look746 </div>
Merge branch 'worktree-agent-ad7c6d88d93adc817'747 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look748 <SidebarGroup title="Shared with you" className="mt-3">
Merge branch 'worktree-agent-ad7c6d88d93adc817'749 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look750 <SidebarLink
751 key={`${repo.namespace}/${repo.name}`}
752 to={`/${repo.namespace}/${repo.name}`}
753 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
754 drill="hover"
755 >
756 <span className="font-mono text-faint">{repo.namespace}/</span>
757 {repo.name}
758 </SidebarLink>
759 ))}
760 </SidebarGroup>
761 )}
762
Merge branch 'worktree-agent-ad7c6d88d93adc817'763 <Rule />
764 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look765 <>
Merge branch 'worktree-agent-ad7c6d88d93adc817'766 {/* The workspace the switcher names: its own page first. */}
767 <SidebarGroup title={displayName(ws)}>
768 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
769 Overview
770 </SidebarLink>
771 </SidebarGroup>
772 <div className="mt-3">
773 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
774 </div>
775
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member776 <Rule />
Merge branch 'worktree-agent-ad7c6d88d93adc817'777 <div className="space-y-px">
778 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />} current={here === "agents"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look779 Agent fleet
780 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'781 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />} current={here === "context"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look782 Context
783 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'784 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />} current={here === "memory"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 Memory
786 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'787 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />} current={here === "security"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look788 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents789 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'790 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />} current={here === "packages"}>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member791 Packages
792 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'793 {INSIGHTS && (
794 <SidebarSoonLink
795 to={`/${ws.slug}/-/insights`}
796 icon={<TrendingUp size={15} />}
797 about={INSIGHTS.summary}
798 current={here === "insights"}
799 >
800 {INSIGHTS.title}
801 </SidebarSoonLink>
802 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar803 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look804 <SidebarSoonLink
805 key={item.key}
806 to={`/${ws.slug}/-/soon/${item.key}`}
807 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
808 about={item.summary}
809 >
810 {item.title === "Board" ? "Boards" : item.title}
811 </SidebarSoonLink>
812 ))}
Merge branch 'worktree-agent-ad7c6d88d93adc817'813 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look814
815 <Rule />
816 <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 bar817 {/* Who belongs, for every member to see; owners invite and manage there. */}
Merge branch 'worktree-agent-ad7c6d88d93adc817'818 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />} current={here === "people"}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar819 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar820 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'821 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams} current={here === "teams"}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar822 Teams
823 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'824 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />} current={here === "usage"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look825 Usage
826 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'827 <SidebarLink to="/support" icon={<LifeBuoy size={15} />} current={here === "support"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look828 Support
829 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar830 {/* 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 look831 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member832 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 look833 icon={<Settings size={15} />}
834 drill
Merge branch 'worktree-agent-ad7c6d88d93adc817'835 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look836 >
837 Settings
838 </SidebarLink>
839 </div>
840 </>
Merge branch 'worktree-agent-ad7c6d88d93adc817'841 ) : (
842 <SidebarGroup title="Projects">
843 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
844 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look845 )}
846 </nav>
847 );
848}
849
Merge branch 'worktree-agent-ad7c6d88d93adc817'850/** Insights, coming: the workspace's own page says what it will be. */
851const INSIGHTS = roadmapItem("insights");
852
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look853/** A workspace's settings, drilled into from Settings in the main list. */
854function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
855 return (
856 <nav aria-label="Workspace settings" className={PANEL}>
857 <BackRow to={`/${slug}`} label="Settings" context={slug} />
858 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings859 {owner && (
860 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
861 General
862 </SidebarLink>
863 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
865 Repositories
866 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments867 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
868 Access tokens
869 </SidebarLink>
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules870 {owner && (
871 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
872 Personal access tokens
873 </SidebarLink>
874 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge875 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />}>
876 Rules
877 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look878 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar879 <SidebarGroup title="Agents and runs" className="mt-3">
880 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
881 Guardrails
882 </SidebarLink>
883 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
884 Secrets and variables
885 </SidebarLink>
Merge branch 'worktree-agent-a3abfcce648e87dca'886 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
887 Actions
888 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it889 {owner && (
890 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
891 Runners
892 </SidebarLink>
893 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar894 </SidebarGroup>
895 <SidebarGroup title="Connections" className="mt-3">
896 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
897 Integrations
898 </SidebarLink>
899 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
900 Webhooks
901 </SidebarLink>
902 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look903 <Rule />
904 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings905 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments906 Billing and plans
A catalogue of model providers, and settings that feel like settings907 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API908 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
909 Audit log
910 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look911 </div>
A catalogue of model providers, and settings that feel like settings912 </nav>
913 );
914}
915
Sidebar: no settings flash on the way into a repository, and a better slide916/**
917 * What the repository menu needs. Known from the address before the page's
918 * data arrives, so the menu is right from the first frame; the counts fill
919 * in once it does.
920 */
921type MenuRepo = {
922 namespace: string;
923 name: string;
924 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look925 /** Whether they see its settings; from the repository's page once it loads. */
926 settings?: boolean;
927 /** What they may do there, once the repository's page loads. */
928 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide929 issues?: number;
930 pulls?: number;
931};
932
933function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
934 return (
935 a != null &&
936 b != null &&
937 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
938 a.name.toLowerCase() === b.name.toLowerCase()
939 );
940}
A repository has its own sidebar, as settings do941
942/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look943 * A project's own list, drilled into while you are in it: everything about
944 * the project, running and its code, and nothing else, with the way back
945 * out to all of them.
A repository has its own sidebar, as settings do946 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put947function RepoMenu({
948 repo,
949 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look950 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put951}: {
952 repo: MenuRepo;
953 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look954 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
955 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put956}) {
A repository has its own sidebar, as settings do957 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 put958 // 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 look959 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do960 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look961 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
962 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'963 <Hint label="Overview" side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero964 <div>
A flat project menu with Code first, views as tabs, and Stripe for every owner965 <NavLink
A repository has its own sidebar, as settings do966 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner967 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents968 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner969 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look970 `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 owner971 }
A repository has its own sidebar, as settings do972 >
973 <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 page974 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do975 </span>
976 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
977 <span className="text-faint">{repo.namespace}/</span>
978 <span className="font-semibold text-fg">{repo.name}</span>
979 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner980 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero981 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'982 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look983 {/* A project's pages, in the order people use them. A page with more
984 than one view shows them as tabs across its top. */}
985 <Rule />
986 <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 request987 <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 owner988 Code
989 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit990 <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 owner991 Issues
992 </SidebarLink>
993 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
994 Pull requests
995 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains996 <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 owner997 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains998 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner999 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
1000 Workflows
Projects: what a workspace builds and runs, first on every page1001 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1002 </div>
1003 <Rule />
1004 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1005 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner1006 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
1007 Deployments
1008 </SidebarLink>
1009 ) : null}
Observability gets its own page; tabs never overflow1010 <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.">
1011 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner1012 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1013 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1014 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
1015 Security
1016 </SidebarLink>
1017 ) : null}
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971018 <SidebarLink
1019 to={`${base}/contributors`}
1020 also={[`${base}/activity`, `${base}/stargazers`, ...soonPaths(base, "Insights")]}
1021 icon={<BarChart3 size={15} />}
1022 >
A flat project menu with Code first, views as tabs, and Stripe for every owner1023 Insights
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971024 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1025 </div>
1026 {(repo.settings ?? shows.has("settings")) && (
1027 <>
1028 <Rule />
1029 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1030 Settings
1031 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1032 </>
1033 )}
A repository has its own sidebar, as settings do1034 </nav>
1035 );
1036}
1037
Project settings move into the sidebar, grouped, with a heading per page1038/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1039 * A project's settings, drilled into from its list: what is about running
1040 * it, then its agents, then its code and who can reach it. The way back
1041 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1042 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1043function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1044 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1045 // Each page shows to the roles that can use it, once the role is known.
1046 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1047 const running = shows("deployments");
1048 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1049 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1050 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1051 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1052 {shows("") && (
1053 <div className="mt-2 space-y-px">
1054 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1055 General
1056 </SidebarLink>
1057 </div>
1058 )}
1059 {running && (
1060 <>
1061 {shows("") && <Rule />}
1062 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1063 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1064 Deployments
1065 </SidebarLink>
1066 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1067 Domains
1068 </SidebarLink>
1069 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1070 Dependencies
1071 </SidebarLink>
1072 </div>
1073 </>
1074 )}
1075 {agents && (
1076 <>
1077 <Rule />
1078 <div className="space-y-px">
1079 {shows("agents") && (
1080 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1081 Agents
1082 </SidebarLink>
1083 )}
1084 {shows("guardrails") && (
1085 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1086 Guardrails
1087 </SidebarLink>
1088 )}
1089 </div>
1090 </>
1091 )}
1092 <Rule />
1093 <div className="space-y-px">
1094 {shows("repository") && (
1095 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1096 Repository
1097 </SidebarLink>
1098 )}
1099 {shows("access") && (
1100 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1101 Access
1102 </SidebarLink>
1103 )}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1104 {shows("keys") && (
1105 <SidebarLink to={`${base}/settings/keys`} icon={<KeyRound size={15} />}>
1106 Deploy keys
1107 </SidebarLink>
1108 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1109 {shows("branches") && (
1110 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1111 Branches and merging
1112 </SidebarLink>
1113 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1114 {shows("rules") && (
1115 <SidebarLink to={`${base}/settings/rules`} icon={<Scale size={15} />}>
1116 Rules
1117 </SidebarLink>
1118 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1119 {shows("secrets") && (
1120 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1121 Secrets and variables
1122 </SidebarLink>
1123 )}
Merge branch 'worktree-agent-a3abfcce648e87dca'1124 {shows("actions") && (
1125 <SidebarLink to={`${base}/settings/actions`} icon={<PlayCircle size={15} />}>
1126 Actions
1127 </SidebarLink>
1128 )}
1129 {shows("environments") && (
1130 <SidebarLink to={`${base}/settings/environments`} icon={<Layers size={15} />}>
1131 Environments
1132 </SidebarLink>
1133 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1134 {shows("runners") && (
1135 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1136 Runners
1137 </SidebarLink>
1138 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1139 {shows("webhooks") && (
1140 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1141 Webhooks
1142 </SidebarLink>
1143 )}
1144 </div>
Project settings move into the sidebar, grouped, with a heading per page1145 </nav>
1146 );
1147}
1148
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1149/** Your own settings, drilled into from Your settings: one page each. */
1150function AccountSettingsMenu({ username }: { username: string }) {
1151 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1152 <SidebarLink to={`/settings/${page}`} icon={icon}>
1153 {ACCOUNT_SETTINGS[page].title}
1154 </SidebarLink>
Account settings in the same shape as a workspace's1155 );
1156 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1157 <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 quotas1158 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1159 <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 quotas1160 {link("profile", <CircleUserRound size={15} />)}
1161 {link("emails", <Mail size={15} />)}
1162 {link("invites", <Ticket size={15} />)}
1163 </div>
1164 <Rule />
1165 <div className="space-y-px">
1166 {link("keys", <Fingerprint size={15} />)}
1167 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1168 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1169 <Rule />
1170 <div className="space-y-px">
1171 {link("github", <GithubMark className="size-[15px]" />)}
1172 {link("applications", <Plug size={15} />)}
1173 </div>
1174 <Rule />
1175 <div className="space-y-px">
Merge main (membership, two-factor, GitHub repo roles) into tokens1176 {link("two-factor", <ShieldCheck size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1177 {link("security-log", <History size={15} />)}
1178 </div>
Account settings in the same shape as a workspace's1179 </nav>
1180 );
1181}
1182
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1183/**
1184 * Signing in and signing up, in place of the account for a visitor. Signing
1185 * in brings them back to the page they are on.
1186 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1187/** Help, status and the fine print under a visitor's sign-in buttons. */
1188function VisitorLinks() {
1189 const status = useSiteStatus();
Merge branch 'worktree-agent-ad7c6d88d93adc817'1190 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/60`;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1191 const dot = (
1192 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1193 ·
1194 </span>
1195 );
1196 return (
1197 // Two short rows, so a narrow sidebar never starts a line with a dot.
1198 <nav aria-label="About g1t" className="pt-0.5">
1199 <div className="flex items-center gap-0.5">
1200 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1201 <StatusDot state={status?.overall.state ?? null} />
1202 Status
1203 </a>
1204 {dot}
1205 <Link to="/support" className={quiet}>
1206 Support
1207 </Link>
1208 </div>
1209 <div className="flex items-center gap-0.5">
1210 {LEGAL_LINKS.map(([label, to], index) => (
1211 <span key={to} className="flex items-center gap-0.5">
1212 {index > 0 && dot}
1213 <Link to={to} className={quiet}>
1214 {label}
1215 </Link>
1216 </span>
1217 ))}
1218 </div>
1219 </nav>
1220 );
1221}
1222
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1223function VisitorPanel() {
1224 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1225 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1226 return (
1227 <div className="space-y-2">
1228 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1229 <div className="grid grid-cols-2 gap-2">
1230 <Link
1231 to={withNext("/login", pathname + search)}
1232 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"
1233 >
1234 <LogIn size={14} />
1235 Sign in
1236 </Link>
1237 <Link
1238 to={withNext("/register", pathname + search)}
1239 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1240 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1241 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1242 </Link>
1243 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1244 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1245 </div>
1246 );
1247}
1248
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1249function Sidebar({
1250 user,
1251 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1252 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1253 onFind,
1254 onClose,
1255}: {
1256 user: User | null;
1257 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1258 /** The page is a 404: the address names nothing the viewer can see. */
1259 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1260 onFind: () => void;
1261 /** In the sheet on a small screen: closing it, at the end of the top row. */
1262 onClose?: () => void;
1263}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1264 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1265 const { pathname } = useLocation();
1266 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1267 // Where the sidebar is drilled to follows the page being gone to, so it
1268 // moves as the link is followed, not once the page arrives.
1269 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1270 // On a 404, the address is not a place: no list is drilled into from it,
1271 // except the project's own when the project is real and visible and only
1272 // something inside it is missing (a file, a commit, an issue).
1273 const lost = missing && going == null;
1274 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1275 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1276 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1277 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1278 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1279 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 look1280 // An invitation to a repository is answered before its menu means anything.
1281 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1282 repoPath != null &&
1283 !reserved.has(repoPath[1]) &&
1284 repoPath[2] !== "-" &&
1285 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1286 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1287 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1288 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1289 // The repository the menu is for: the one loaded if it is the one being
1290 // gone to, else what the address says, at once.
1291 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 look1292 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1293 ? sameRepo(active, targetRepo)
1294 ? active
1295 : {
1296 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1297 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1298 }
1299 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1300 // The viewer's role there, from the repository's page once it has loaded.
1301 const page = useRouteLoaderData("routes/repo/layout") as
1302 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1303 | undefined;
1304 const menuRepo: MenuRepo | null =
1305 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1306 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1307 : guessed;
1308
1309 // The way from the main list to the one shown.
1310 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1311 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1312 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1313 } else if (ws && inSettings) {
1314 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1315 } else if (menuRepo) {
1316 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1317 // Out of a project: to its workspace's projects when they are yours.
1318 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1319 const back = !user
1320 ? { to: "/explore", label: "Explore" }
1321 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1322 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1323 : { to: "/", label: "Mission control" };
1324 trail.push({
1325 key,
1326 node: (
1327 <RepoMenu
1328 repo={menuRepo}
1329 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1330 back={back}
1331 />
1332 ),
1333 });
1334 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1335 }
1336
Agents as a team: lifecycle, merge queue, billing and a new shell1337 return (
1338 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1339 {/* 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 bar1340 <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 put1341 {user ? (
1342 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1343 {/* The 1 alone beside the workspace: a square hover the height of the
1344 switcher, the mark as tall as the workspace avatar. */}
1345 <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 bar1346 <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 put1347 </Link>
1348 <span className="shrink-0 text-line-strong" aria-hidden="true">
1349 /
1350 </span>
1351 <WorkspaceSwitcher user={user} shell={shell} />
1352 </>
1353 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1354 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1355 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1356 </Link>
1357 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1358 {onClose && (
1359 <button
1360 type="button"
1361 aria-label="Close menu"
1362 onClick={onClose}
1363 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1364 >
1365 <X size={16} />
1366 </button>
1367 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1368 </div>
A Vercel-like sidebar and a new mark1369 <div className="px-2 pt-3">
1370 <button
1371 type="button"
1372 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1373 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 mark1374 >
1375 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1376 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1377 <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 mark1378 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1379 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1380 <Drill trail={trail} />
1381 {/* The account, or signing in, as one row at the very bottom. */}
1382 <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 put1383 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1384 </div>
1385 </div>
1386 );
1387}
1388
1389/** Words for the sections a path can end in. */
1390const SECTIONS: Record<string, string> = {
1391 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1392 milestones: "Milestones",
1393 labels: "Labels",
A Vercel-like sidebar and a new mark1394 pulls: "Pull requests",
1395 queue: "Merge queue",
1396 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1397 plans: "Outcomes",
1398 actions: "Workflows",
1399 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1400 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1401 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1402 access: "Access",
1403 invitations: "Invitation",
1404 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1405 branches: "Branches",
1406 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1407 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1408 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1409 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1410 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1411 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1412 people: "People",
1413 projects: "Projects",
1414 teams: "Teams",
A Vercel-like sidebar and a new mark1415 tokens: "Access tokens",
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1416 "personal-access-tokens": "Personal access tokens",
A Vercel-like sidebar and a new mark1417 usage: "Usage",
Site: the AI Gateway page's breadcrumb reads "AI Gateway"1418 gateway: "AI Gateway",
Secrets and variables: one list, rows per environment, for workflows and deployments1419 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1420 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1421 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1422 domains: "Domains",
1423 guardrails: "Guardrails",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1424 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1425 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1426 tree: "Files",
1427 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1428 agents: "Agents",
1429 context: "Context",
1430 memory: "Memory",
1431 security: "Security",
1432 packages: "Packages",
1433 runners: "Runners",
Merge branch 'worktree-agent-a3abfcce648e87dca'1434 environments: "Environments",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1435 workflows: "Workflows",
1436 observability: "Observability",
1437 insights: "Insights",
1438 sessions: "Sessions",
A Vercel-like sidebar and a new mark1439};
1440
Merge branch 'worktree-agent-a3abfcce648e87dca'1441/** Settings pages whose name differs from the section's of the same word. */
1442const SETTINGS_SECTIONS: Record<string, string> = { branches: "Branches and merging", actions: "Actions" };
1443
A Vercel-like sidebar and a new mark1444/** 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 quotas1445function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1446 // A 404 names nothing from the address, unless the project is real and
1447 // visible and only something inside it is missing.
1448 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1449 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1450 return <span className="text-sm font-medium">Not found</span>;
1451 }
A Vercel-like sidebar and a new mark1452 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1453 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1454 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 quotas1455 // Your settings: Settings / Emails.
1456 if (parts[0] === "settings") {
1457 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1458 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 quotas1459 }
A Vercel-like sidebar and a new mark1460 if (reserved.includes(parts[0]!)) {
1461 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1462 settings: "Account",
A Vercel-like sidebar and a new mark1463 explore: "Explore",
Projects live: fixes from walking them end to end1464 new: "New project",
A Vercel-like sidebar and a new mark1465 search: "Search",
1466 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1467 policies: "Policies",
1468 security: "Security",
1469 support: "Support",
1470 status: "Status",
1471 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1472 inbox: "Inbox",
A Vercel-like sidebar and a new mark1473 };
1474 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1475 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1476 // A person's profile, by their handle.
1477 if (parts[0] === "u" && parts[1]) {
1478 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1479 }
A Vercel-like sidebar and a new mark1480 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1481 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1482 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1483 const page = `/${owner}/-/${third}`;
1484 if (third && SETTINGS_PAGE.test(page)) {
1485 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1486 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1487 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1488 } else if (second) {
1489 const repo = `/${owner}/${second}`;
1490 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1491 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1492 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 mark1493 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 deployments1494 else if (third && SECTIONS[third]) {
1495 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1496 // A settings page names which one: Settings / Secrets and variables.
1497 if (third === "settings" && fourth && SECTIONS[fourth]) {
Merge branch 'worktree-agent-a3abfcce648e87dca'1498 trail.push({ label: SETTINGS_SECTIONS[fourth] ?? SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1499 }
1500 }
A Vercel-like sidebar and a new mark1501 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1502 return <Trail trail={trail} />;
1503}
1504
1505/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1506type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1507
1508/**
1509 * The trail of links. On a phone there is room for one: the page itself,
1510 * with a way back to where it sits; the whole trail from a wider screen up.
1511 */
1512function Trail({ trail }: { trail: Crumb[] }) {
1513 const up = trail.length > 1 ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1514 return (
1515 <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 audit1516 {up && (
1517 <Link
1518 to={up.to}
1519 prefetch="intent"
1520 aria-label={`Back to ${up.label}`}
1521 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"
1522 >
1523 <ChevronLeft size={16} />
1524 </Link>
1525 )}
1526 {trail.map((crumb, index) => {
1527 const last = index === trail.length - 1;
1528 return (
1529 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1530 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1531 <Link
1532 to={crumb.to}
1533 prefetch="intent"
1534 aria-current={last ? "page" : undefined}
1535 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1536 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1537 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1538 >
1539 {last && crumb.short ? (
1540 <>
1541 <span className="sm:hidden">{crumb.short}</span>
1542 <span className="hidden sm:inline">{crumb.label}</span>
1543 </>
1544 ) : (
1545 crumb.label
1546 )}
1547 </Link>
1548 </span>
1549 );
1550 })}
A Vercel-like sidebar and a new mark1551 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1552 );
1553}
1554
Search across all of g1t, Explore, and a command palette1555type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1556
1557/** 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 look1558function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1559 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1560 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1561 { 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 step1562 ...(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-agent1563 ? [{ 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 step1564 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1565 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1566 { 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 all1567 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1568 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1569 { 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 quotas1570 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1571 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1572 .filter((page) => page !== "profile")
1573 .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 shell1574 ];
1575 const repo = shell.repo;
1576 if (repo) {
1577 const base = `/${repo.namespace}/${repo.name}`;
1578 const name = `${repo.namespace}/${repo.name}`;
1579 commands.unshift(
Projects: what a workspace builds and runs, first on every page1580 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1581 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1582 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1583 { 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 step1584 ...(repo.member
1585 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1586 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1587 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1588 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1589 ...(repo.member
1590 ? [
1591 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1592 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1593 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1594 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1595 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1596 ]
1597 : []),
1598 );
1599 }
1600 for (const membership of user.workspaces ?? []) {
1601 commands.push(
Workspace names and icons, and a component kit for every control1602 { 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 sidebar1603 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1604 { 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 request1605 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1606 { label: "AI Gateway", hint: `${membership.slug} · Usage`, to: `/${membership.slug}/-/gateway`, icon: <Network size={15} /> },
Projects: what a workspace builds and runs, first on every page1607 { 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 settings1608 { 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 look1609 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1610 );
1611 }
1612 for (const listed of shell.repos) {
1613 commands.push({
Projects: what a workspace builds and runs, first on every page1614 label: listed.title ?? listed.name,
1615 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1616 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1617 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1618 });
1619 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1620 // "Put an agent on …": one per project, after the projects themselves.
1621 for (const listed of shell.repos) {
1622 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1623 commands.push({
1624 label: `Put an agent on ${listed.title ?? listed.name}`,
1625 hint: `${listed.namespace}/${listed.name}`,
1626 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1627 icon: <Sparkles size={15} />,
1628 });
1629 }
Agents as a team: lifecycle, merge queue, billing and a new shell1630 return commands;
1631}
1632
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1633/** 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 look1634function 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 put1635 const commands: Command[] = [];
1636 const repo = shell.repo;
1637 if (repo) {
1638 const base = `/${repo.namespace}/${repo.name}`;
1639 const name = `${repo.namespace}/${repo.name}`;
1640 commands.push(
1641 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1642 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1643 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1644 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1645 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1646 );
1647 }
1648 commands.push(
1649 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1650 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1651 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1652 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1653 { 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 look1654 { 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 put1655 );
1656 return commands;
1657}
1658
Agents as a team: lifecycle, merge queue, billing and a new shell1659/**
Merge branch 'worktree-agent-aca4790ea223aa235'1660 * The page while the next one loads: after a moment it dims, under the
1661 * bar below, so it is plainly on its way out rather than frozen, and the
1662 * next page fades up from there. Sending a form does not dim it: the
1663 * form's button says it is working. Spread on the element holding the page.
1664 */
1665export function useLeaving() {
1666 const navigation = useNavigation();
1667 const method = navigation.formMethod?.toUpperCase();
1668 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1669 return {
1670 "aria-busy": leaving || undefined,
1671 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1672 };
1673}
1674
1675/**
Agents as a team: lifecycle, merge queue, billing and a new shell1676 * A bar across the top of the page while the next one loads, as GitHub
1677 * has: it appears at once, creeps towards the end while waiting, then fills
1678 * and fades when the page arrives.
1679 */
1680export function Progress() {
1681 const navigation = useNavigation();
1682 const busy = navigation.state !== "idle";
1683 const [width, setWidth] = useState(0);
1684 const [visible, setVisible] = useState(false);
1685 useEffect(() => {
1686 if (busy) {
1687 setVisible(true);
1688 setWidth(12);
1689 // Slows as it goes, never quite arriving until the page does.
1690 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1691 return () => clearInterval(timer);
1692 }
1693 setWidth((now) => (now > 0 ? 100 : 0));
1694 const fade = setTimeout(() => setVisible(false), 250);
1695 const reset = setTimeout(() => setWidth(0), 550);
1696 return () => {
1697 clearTimeout(fade);
1698 clearTimeout(reset);
1699 };
1700 }, [busy]);
1701 return (
1702 <div
1703 aria-hidden="true"
1704 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1705 visible ? "opacity-100" : "opacity-0"
1706 }`}
1707 >
1708 <div
1709 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1710 style={{ width: `${width}%` }}
1711 />
1712 </div>
1713 );
1714}
1715
1716/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1717 * The app: a sidebar with the workspace, its repositories and the sections
1718 * of the one being looked at; a slim bar with search and the account; and
1719 * the page. A visitor who is not signed in gets the same frame, with
1720 * Explore and Search in place of the workspace, and signing in in place of
1721 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1722 */
1723export function AppShell({
1724 user,
1725 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1726 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1727 banner,
1728 children,
1729}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1730 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1731 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1732 /** The page is a 404 (see Sidebar). */
1733 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1734 banner?: ReactNode;
1735 children: ReactNode;
1736}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1737 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1738 const [drawer, setDrawer] = useState(false);
1739 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1740 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1741 const signUpLabel = useSignUpCopy().primary;
1742 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1743
1744 // A new page closes the drawer on small screens.
1745 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1746 // Mission control's code, fetched while the browser is idle, so going
1747 // home never waits on it (routes/home.tsx).
1748 useEffect(() => {
1749 if (!user) return;
1750 const load = () => void import("./mission-control").catch(() => undefined);
1751 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1752 if (idle) idle(load);
1753 else setTimeout(load, 1500);
1754 }, [user]);
Search across all of g1t, Explore, and a command palette1755 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1756 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1757
1758 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1759 // The phone's menu is a sheet: a dialog that holds focus, closes on
1760 // Escape or a tap outside, and gives focus back to the menu button.
1761 <Sheet open={drawer} onOpenChange={setDrawer}>
Agents as a team: lifecycle, merge queue, billing and a new shell1762 <div className="min-h-screen">
1763 <Progress />
1764 <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 quotas1765 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1766 </aside>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1767 <SheetContent side="left" showClose={false} aria-describedby={undefined} className="w-72 max-w-[85vw] sm:max-w-72 lg:hidden">
1768 <SheetTitle className="sr-only">Menu</SheetTitle>
1769 <Sidebar
1770 user={user}
1771 shell={shell}
1772 missing={missing}
1773 onFind={() => {
1774 setDrawer(false);
1775 setPalette(true);
1776 }}
1777 onClose={() => setDrawer(false)}
1778 />
1779 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell1780
Polish: phones, copy boxes, the plan page, the landing page, a real glide1781 <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 bar1782 <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 audit1783 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell1784 aria-label="Open menu"
1785 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1786 >
1787 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1788 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1789 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1790 <div className="ml-auto flex items-center gap-1.5">
1791 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1792 <button
1793 type="button"
1794 aria-label="Search or jump to"
1795 onClick={() => setPalette(true)}
1796 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1797 >
1798 <Search size={16} />
1799 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1800 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1801 <NavLink
1802 to="/explore"
1803 prefetch="intent"
1804 aria-label="Explore"
1805 className={({ isActive }) =>
1806 `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"}`
1807 }
1808 >
1809 <Compass size={16} className="sm:hidden" />
1810 <span className="hidden sm:inline">Explore</span>
1811 </NavLink>
A Vercel-like sidebar and a new mark1812 <a
1813 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1814 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 mark1815 >
1816 Docs
1817 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1818 {user && (
1819 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1820 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1821 <InboxBell counts={shell.inbox ?? null} />
1822 </>
1823 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1824 {!user ? (
1825 // The sidebar has these too, but on a phone it is folded away.
1826 <Link
1827 to={withNext("/login", here)}
1828 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"
1829 >
1830 Sign in
1831 </Link>
1832 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1833 <>
1834 {/* Room between what comes in (the inbox) and what you make. */}
1835 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1836 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1837 <Tooltip>
1838 <TooltipTrigger asChild>
1839 <DropdownMenuTrigger
1840 aria-label="Create new"
1841 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"
1842 >
1843 <Plus size={16} />
1844 <ChevronDown size={13} className="text-muted" />
1845 </DropdownMenuTrigger>
1846 </TooltipTrigger>
1847 <TooltipContent>Create new…</TooltipContent>
1848 </Tooltip>
1849 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1850 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1851 <>
1852 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1853 <DropdownMenuItem asChild>
1854 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1855 <CircleDot />
1856 New issue
1857 </Link>
1858 </DropdownMenuItem>
1859 {shell.repo.member && (
1860 <DropdownMenuItem asChild>
1861 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1862 <ListTree />
1863 Plan work
1864 </Link>
1865 </DropdownMenuItem>
1866 )}
1867 <DropdownMenuSeparator />
1868 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1869 )}
1870 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1871 <Link to="/?agent=new">
1872 <Sparkles />
1873 Put an agent on it
1874 </Link>
1875 </DropdownMenuItem>
1876 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1877 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1878 <Box />
1879 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1880 </Link>
1881 </DropdownMenuItem>
1882 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1883 <Link to="/new/github">
1884 <GitBranch />
1885 Import from GitHub
1886 </Link>
1887 </DropdownMenuItem>
1888 <DropdownMenuSeparator />
1889 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1890 <Link to="/workspaces/new">
1891 <Users />
1892 New workspace
1893 </Link>
1894 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'1895 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
1896 {shell.workspace &&
1897 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
1898 <DropdownMenuItem asChild>
1899 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1900 <UsersRound />
1901 New team
1902 </Link>
1903 </DropdownMenuItem>
1904 ) : (
1905 <DropdownMenuItem disabled title="Only owners can create teams in this workspace.">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1906 <UsersRound />
1907 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'1908 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
1909 </DropdownMenuItem>
1910 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell1911 </DropdownMenuContent>
1912 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1913 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1914 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1915 </div>
1916 </header>
1917 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1918 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in1919 <InMain.Provider value={true}>{children}</InMain.Provider>
Merge branch 'worktree-agent-aca4790ea223aa235'1920 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1921 </div>
Search across all of g1t, Explore, and a command palette1922 <CommandPalette
1923 open={palette}
1924 onOpenChange={setPalette}
1925 commands={commands}
1926 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1927 />
Agents as a team: lifecycle, merge queue, billing and a new shell1928 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1929 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell1930 );
1931}

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