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

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, KanbanSquare, KeyRound, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all5import type { Abilities, InboxCounts, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Search across all of g1t, Explore, and a command palette7import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all8import { AskAi, InboxBell } from "./inbox";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar9import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas10import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height11import { Logo, Mark } from "./logo";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar12import { Avatar } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents13import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell14import {
15 DropdownMenu,
16 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas17 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell18 DropdownMenuItem,
19 DropdownMenuLabel,
20 DropdownMenuSeparator,
21 DropdownMenuTrigger,
22} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales23import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24import { 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 put25import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas26import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
27import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put28import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas30import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar31import { 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 quotas32import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell33
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put34/**
35 * What the sidebar needs, worked out by the root loader. For a visitor who
36 * is not signed in there is no workspace, no projects and no usage: only
37 * the project being looked at, if they can see it.
38 */
Agents as a team: lifecycle, merge queue, billing and a new shell39export type ShellData = {
40 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control41 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar42 /** All its projects, by name, for the palette: `name` is the slug in their address. */
43 repos: ShortcutProject[];
44 /** The person's pinned projects in it, in their order (lib/pins.ts). */
45 pinned?: ShortcutProject[];
46 /** What they opened there last, latest first, leaving out the pinned. */
47 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look48 /** Repositories shared with them in workspaces they do not belong to. */
49 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page50 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell51 repo: {
52 namespace: string;
53 name: string;
54 member: boolean;
55 issues: number;
56 pulls: number;
57 } | null;
Billing accounts, terms and enterprises; g1t is no longer free58 /** Where the workspace stands against its usage limit, if billing is on. */
59 limit: {
60 exposureMicros: number;
61 ceilingMicros: number | null;
62 state: "ok" | "warning" | "stopped";
63 comped: boolean;
64 } | null;
Free while g1t is being built out; agents can check out their own forks65 /** Whether g1t charges nothing for now, while it is being built out. */
66 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request67 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy68 /** This month's usage: charged, or at cost while g1t is free. */
69 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look70 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
71 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 all72 /** What is unread in their inbox, for the bell. Absent for a visitor. */
73 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell74};
75
76function SidebarLink({
77 to,
78 icon,
79 end,
80 count,
81 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell83 children,
84}: {
85 to: string;
86 icon: ReactNode;
87 end?: boolean;
88 count?: number;
Projects: what a workspace builds and runs, first on every page89 /** Other path prefixes under which this link is the current one. */
90 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91 /** It opens a list of its own: a chevron says so, always or on hover. */
92 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell93 children: ReactNode;
94}) {
95 const { pathname } = useLocation();
96 return (
97 <NavLink
98 to={to}
99 end={end}
100 prefetch="intent"
101 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page102 const current =
103 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark104 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 shell105 current
106 ? "bg-raised font-medium text-fg"
107 : isPending
108 ? "bg-raised/60 text-fg"
109 : "text-muted hover:bg-raised/60 hover:text-fg"
110 }`;
111 }}
112 >
113 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
114 <span className="min-w-0 grow truncate">{children}</span>
115 {count != null && count > 0 && (
116 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
117 {count}
118 </span>
119 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look120 {drill && (
121 <ChevronRight
122 size={14}
123 aria-hidden="true"
124 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
125 />
126 )}
Agents as a team: lifecycle, merge queue, billing and a new shell127 </NavLink>
128 );
129}
130
Pull requests say where they are live; the project menu shows what is coming131/**
132 * Something a project will have, shown where it will live so people can
133 * see where g1t is going. Not a link: nothing is behind it yet.
134 */
135function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
136 return (
137 <div
138 title={about}
139 aria-disabled="true"
140 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
141 >
142 <span className="shrink-0 opacity-70">{icon}</span>
143 <span className="grow truncate">{children}</span>
144 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
145 Soon
146 </span>
147 </div>
148 );
149}
150
The project menu opens by section, and the workspace has its own panel151/**
A flat project menu with Code first, views as tabs, and Stripe for every owner152 * A page that is coming, as a link to its roadmap page: faint, with Soon,
153 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel154 */
A flat project menu with Code first, views as tabs, and Stripe for every owner155function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel156 to,
157 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner158 icon,
159 about,
The project menu opens by section, and the workspace has its own panel160 children,
161}: {
162 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner163 also?: string[];
164 icon: ReactNode;
165 about: string;
The project menu opens by section, and the workspace has its own panel166 children: ReactNode;
167}) {
168 const { pathname } = useLocation();
169 return (
170 <NavLink
171 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner172 title={about}
The project menu opens by section, and the workspace has its own panel173 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner174 className={({ isActive }) => {
175 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
176 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
177 current ? "bg-raised font-medium text-fg" : "text-faint hover:bg-raised/60 hover:text-muted"
The project menu opens by section, and the workspace has its own panel178 }`;
179 }}
180 >
A flat project menu with Code first, views as tabs, and Stripe for every owner181 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel182 <span className="min-w-0 grow truncate">{children}</span>
183 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
184 Soon
185 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales186 </NavLink>
The project menu opens by section, and the workspace has its own panel187 );
188}
189
A flat project menu with Code first, views as tabs, and Stripe for every owner190/** Icons for what spans projects. */
191const WORKSPACE_ICONS: Record<string, ReactNode> = {
192 board: <KanbanSquare size={15} />,
193 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member194 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner195 packages: <Package size={15} />,
196 fleet: <Bot size={15} />,
197};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales198
A flat project menu with Code first, views as tabs, and Stripe for every owner199/** The Soon pages of a section, as paths, so its link is current on them. */
200function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
201 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales202}
203
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204function SidebarGroup({
205 title,
206 action,
207 className = "",
208 children,
209}: {
210 title: string;
211 action?: ReactNode;
212 className?: string;
213 children: ReactNode;
214}) {
Agents as a team: lifecycle, merge queue, billing and a new shell215 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look216 <section className={className}>
A Vercel-like sidebar and a new mark217 <div className="mb-1 flex h-6 items-center justify-between px-2">
218 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell219 {action}
220 </div>
221 <div className="space-y-px">{children}</div>
222 </section>
223 );
224}
225
Workspace names and icons, and a component kit for every control226/** What a workspace is called where people read it: its display name, else its slug. */
227function displayName(membership: Membership): string {
228 return membership.name?.trim() || membership.slug;
229}
230
Agents as a team: lifecycle, merge queue, billing and a new shell231function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control232 const workspace = shell.workspace;
233 const current = workspace?.slug;
234 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell235 return (
236 <DropdownMenu>
Workspace names and icons, and a component kit for every control237 {/* The name goes to the workspace; only the arrows switch it. The
238 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark239 <Link
240 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents241 prefetch="intent"
Workspace names and icons, and a component kit for every control242 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look243 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 mark244 >
Workspace names and icons, and a component kit for every control245 {workspace ? (
246 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell247 ) : (
Workspace names and icons, and a component kit for every control248 <span className="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell249 )}
Workspace names and icons, and a component kit for every control250 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark251 </Link>
252 <DropdownMenuTrigger
253 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look254 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 data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark255 >
256 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell257 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control258 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar259 {workspace && (
260 <>
261 {/* The workspace's own page: its projects, packages and people. */}
262 <DropdownMenuItem asChild>
263 <Link to={`/${workspace.slug}`}>
264 <LayoutGrid />
265 Workspace overview
266 </Link>
267 </DropdownMenuItem>
268 <DropdownMenuItem asChild>
269 <Link to={`/${workspace.slug}/-/projects`}>
270 <Box />
271 All projects
272 </Link>
273 </DropdownMenuItem>
274 <DropdownMenuSeparator />
275 </>
276 )}
Agents as a team: lifecycle, merge queue, billing and a new shell277 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
278 {(user.workspaces ?? []).map((membership) => (
279 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control280 <Link to={`/${membership.slug}`} title={displayName(membership)}>
281 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
282 <span className="flex min-w-0 grow flex-col leading-tight">
283 <span className="truncate">{displayName(membership)}</span>
284 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
285 </span>
286 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
287 {membership.slug === current ? (
288 <Check className="shrink-0 text-accent" />
289 ) : (
290 <span className="size-4 shrink-0" aria-hidden="true" />
291 )}
Agents as a team: lifecycle, merge queue, billing and a new shell292 </Link>
293 </DropdownMenuItem>
294 ))}
295 <DropdownMenuSeparator />
296 <DropdownMenuItem asChild>
297 <Link to="/workspaces/new">
298 <Plus />
299 New workspace
300 </Link>
301 </DropdownMenuItem>
302 </DropdownMenuContent>
303 </DropdownMenu>
A Vercel-like sidebar and a new mark304 );
305}
306
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas307/**
308 * The fine print, as one quiet row at the foot of the account menu and the
309 * visitor's panel: each link a full-height target, left-aligned. In the
310 * menu each is a menu item, so the arrow keys reach it.
311 */
312const LEGAL_LINKS: [string, string][] = [
313 ["Policies", "/policies"],
314 ["Privacy", "/policies/privacy"],
315 ["Security", "/security"],
316];
317const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
318
319function MenuLegalRow() {
320 return (
321 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
322 {LEGAL_LINKS.map(([label, to], index) => (
323 <span key={to} className="flex items-center gap-0.5">
324 {index > 0 && (
325 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
326 ·
327 </span>
328 )}
329 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
330 <Link to={to}>{label}</Link>
331 </DropdownMenuItem>
332 </span>
333 ))}
334 </div>
335 );
336}
337
338/** Status's row: the live dot and two or three words, fetched when the menu opens. */
339function StatusSummary({ open }: { open: boolean }) {
340 const status = useSiteStatus(open);
341 const words = statusTitle(status);
342 return (
343 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
344 {/* The words say it; the dot's own words for screen readers would repeat them. */}
345 <span aria-hidden="true" className="flex">
346 <StatusDot state={status?.overall.state ?? null} />
347 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents348 {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 quotas349 </span>
350 );
351}
352
353/** A menu row: an icon, words, and whatever sits at its end. */
354const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
355
A Vercel-like sidebar and a new mark356function AccountMenu({ user }: { user: User }) {
357 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas358 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents359 // Name, primary address and invites left: asked for once, as soon as the
360 // 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 quotas361 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents362 const prefetch = () => {
363 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
364 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas365 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents366 if (open) prefetch();
367 // eslint-disable-next-line react-hooks/exhaustive-deps
368 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas369 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents370 // Still on its way: shapes where the words will be, the same size.
371 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 quotas372 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark373 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas374 <DropdownMenu open={open} onOpenChange={setOpen}>
375 <DropdownMenuTrigger
376 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents377 onPointerEnter={prefetch}
378 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas379 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-merged/60 data-[state=open]:bg-raised"
380 >
Workspace names and icons, and a component kit for every control381 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark382 <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 quotas383 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark384 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas385 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
386 {/* Who is signed in, and a way to their profile. */}
387 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
388 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
389 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents390 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
391 {loading ? (
392 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
393 ) : (
394 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
395 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas396 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents397 {loading ? (
398 <Skeleton className="mt-1 h-3 w-40" />
399 ) : (
400 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
401 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas402 </span>
A Vercel-like sidebar and a new mark403 </Link>
404 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas405 <DropdownMenuSeparator className="my-1.5" />
406 <DropdownMenuGroup>
407 <DropdownMenuItem asChild className={MENU_ROW}>
408 <Link to={profile}>
409 <CircleUserRound />
410 Your profile
411 </Link>
412 </DropdownMenuItem>
413 <DropdownMenuItem asChild className={MENU_ROW}>
414 <Link to={FIRST_SETTINGS_PAGE}>
415 <Settings />
416 Your settings
417 </Link>
418 </DropdownMenuItem>
419 <DropdownMenuItem asChild className={MENU_ROW}>
420 <Link to="/settings/invites">
421 <Ticket />
422 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents423 {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 quotas424 {me?.invites_left != null && (
425 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
426 {me.invites_left} left
427 </span>
428 )}
429 </Link>
430 </DropdownMenuItem>
431 </DropdownMenuGroup>
432 <DropdownMenuSeparator className="my-1.5" />
433 <DropdownMenuGroup>
434 <DropdownMenuItem asChild className={MENU_ROW}>
435 <a href="https://docs.g1t.sh/">
436 <BookOpen />
437 Documentation
438 </a>
439 </DropdownMenuItem>
440 <DropdownMenuItem asChild className={MENU_ROW}>
441 <Link to="/support">
442 <LifeBuoy />
443 Support
444 </Link>
445 </DropdownMenuItem>
446 <DropdownMenuItem asChild className={MENU_ROW}>
447 <a href={STATUS_URL}>
448 <Activity />
449 Status
450 <StatusSummary open={open} />
451 </a>
452 </DropdownMenuItem>
453 </DropdownMenuGroup>
454 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark455 {/* Submitted from here: the menu closes on select, and a button
456 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 quotas457 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark458 <LogOut />
459 Sign out
460 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas461 <DropdownMenuSeparator className="my-1.5" />
462 <MenuLegalRow />
A Vercel-like sidebar and a new mark463 </DropdownMenuContent>
464 </DropdownMenu>
465 );
466}
467
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look468/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar469 * A workspace's settings pages, which the sidebar drills into: how it is
470 * set up and connected (guardrails, secrets, runners, integrations,
471 * webhooks), what it pays, its repositories, tokens and record. The main
472 * list keeps the places work happens and who belongs; every member can
473 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look474 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar475const SETTINGS_PAGE =
476 /^\/([^/]+)\/-\/(settings|repositories|tokens|guardrails|secrets|runners|integrations|webhooks|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look477/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page478const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings479
Polish: phones, copy boxes, the plan page, the landing page, a real glide480/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look481 * The sidebar's lists are a stack, the way a phone pushes a screen: one
482 * drilled into slides in from the right over the full width, while the one
483 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide484 * ease-out, the fade quicker than the move so the two never blur together.
485 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide486 */
Sidebar: no settings flash on the way into a repository, and a better slide487const LAYER =
Sidebar: the panels really slide488 "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 look489/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide490const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings491
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look492/** One list in the stack: what it is, and its rows. */
493type Level = { key: string; node: ReactNode };
494
495/** True once the page has hydrated, so what the server drew never animates in. */
496function useHydrated(): boolean {
497 const [hydrated, setHydrated] = useState(false);
498 useEffect(() => setHydrated(true), []);
499 return hydrated;
500}
501
502/**
503 * Drilling in and out of the sidebar's lists. `trail` is the way from the
504 * main list to the one shown, worked out from the address, so a link
505 * straight to a settings page opens the sidebar already drilled in, and
506 * leaving it comes back out. Lists deeper than the one shown stay in
507 * place, off to the right, so going back slides them away rather than
508 * dropping them. Moving to a list at the same depth (a workspace's settings
509 * to a project) fades the new one in where it is.
510 */
511function Drill({ trail }: { trail: Level[] }) {
512 const depth = trail.length - 1;
513 const hydrated = useHydrated();
514 const stack = useRef<Level[]>(trail);
515 const before = stack.current;
516 const within = trail.every((level, index) => before[index]?.key === level.key);
517 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
518 const levels = stack.current;
519 // What was showing last time, to tell drilling in from moving across.
520 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
521 useEffect(() => {
522 shown.current = { depth, keys: levels.map((level) => level.key) };
523 });
A catalogue of model providers, and settings that feel like settings524 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look525 <div className="relative min-h-0 grow overflow-hidden">
526 {levels.map((level, index) => {
527 const current = index === depth;
528 const place =
529 index < depth
530 ? "pointer-events-none -translate-x-1/4 opacity-0"
531 : current
532 ? "translate-x-0 opacity-100"
533 : "pointer-events-none translate-x-full opacity-0";
534 // A list that was not there before arrives: from the right when
535 // drilling in, in place when moving across.
536 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
537 const enter = arriving
538 ? depth > shown.current.depth
539 ? "starting:translate-x-full starting:opacity-0"
540 : "starting:opacity-0"
541 : "";
542 return (
543 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
544 {level.node}
545 </div>
546 );
547 })}
548 </div>
549 );
550}
551
552/**
553 * The row at the top of a list drilled into: the list's name, which goes
554 * back out, and what it belongs to, faint at the end.
555 */
556function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
557 return (
558 <Link
559 to={to}
560 prefetch="intent"
561 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 settings562 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look563 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
564 <span className="min-w-0 shrink-0 truncate">{label}</span>
565 {context && (
566 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
567 )}
568 </Link>
569 );
570}
571
572/** A thin rule between groups of a list. */
573function Rule() {
574 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
575}
576
Navigation: the workspace's own page with tabs, pins and recent in the sidebar577/** A quiet heading inside a group: Pinned, Recent. */
578function SidebarSubhead({ children }: { children: ReactNode }) {
579 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
580}
581
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look582/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar583 * The workspace's projects as the sidebar keeps them, however many there
584 * are: the person's pins in their order, what they opened last, and the
585 * way to all of them. Pins move by dragging, or with Alt and the arrow
586 * keys; the projects service keeps the order.
587 */
588function SidebarProjects({ slug, shell }: { slug: string; shell: ShellData }) {
589 const reorder = useFetcher({ key: `pins:${slug}` });
590 const saved = shell.pinned ?? [];
591 // While a new order is on its way, it shows as made.
592 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
593 const pinned = asked
594 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
595 : saved;
596 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
597 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
598 const [dragging, setDragging] = useState<number | null>(null);
599 const move = (from: number, by: number) => {
600 const next = movedPin(pinned, from, by);
601 if (!next) return;
602 const form = new FormData();
603 form.set("intent", "reorder");
604 for (const project of next) form.append("slug", project.name);
605 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
606 };
607 const row = (project: ShortcutProject) => (
608 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
609 {project.title ?? project.name}
610 </SidebarLink>
611 );
612 return (
613 <SidebarGroup
614 title="Projects"
615 action={
616 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
617 <Plus size={13} />
618 </Link>
619 }
620 >
621 <SidebarSubhead>Pinned</SidebarSubhead>
622 {pinned.length === 0 ? (
623 <p className="px-2 pb-1 text-xs text-faint">Pin projects to keep them here.</p>
624 ) : (
625 <ul aria-label="Pinned projects" className="space-y-px">
626 {pinned.map((project, index) => (
627 <li
628 key={project.name}
629 draggable={pinned.length > 1}
630 onDragStart={(event) => {
631 setDragging(index);
632 event.dataTransfer.effectAllowed = "move";
633 }}
634 onDragOver={(event) => {
635 if (dragging != null) event.preventDefault();
636 }}
637 onDrop={(event) => {
638 event.preventDefault();
639 if (dragging != null && dragging !== index) move(dragging, index - dragging);
640 setDragging(null);
641 }}
642 onDragEnd={() => setDragging(null)}
643 onKeyDown={(event) => {
644 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
645 event.preventDefault();
646 move(index, event.key === "ArrowUp" ? -1 : 1);
647 }}
648 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
649 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
650 >
651 {row(project)}
652 {pinned.length > 1 && (
653 <GripVertical
654 size={12}
655 aria-hidden="true"
656 className="pointer-events-none absolute top-1/2 -left-1.5 -translate-y-1/2 text-faint opacity-0 transition-opacity group-hover/pin:opacity-100"
657 />
658 )}
659 </li>
660 ))}
661 </ul>
662 )}
663 {recent.length > 0 && (
664 <>
665 <SidebarSubhead>Recent</SidebarSubhead>
666 {recent.map((project) => (
667 <div key={`${project.namespace}/${project.name}`}>{row(project)}</div>
668 ))}
669 </>
670 )}
671 <div className="pt-1">
672 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length}>
673 All projects
674 </SidebarLink>
675 </div>
676 </SidebarGroup>
677 );
678}
679
680/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look681 * The main list: where you go, the projects, what the workspace builds and
682 * runs with across them, then its usage, support and settings.
683 */
684function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
685 const ws = shell.workspace;
686 // A visitor browses: no workspace, no projects of their own.
687 if (!user) {
688 return (
689 <nav aria-label="g1t" className={PANEL}>
690 <div className="mt-3 space-y-px">
691 {VISITOR_LINKS.map((link) => (
692 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
693 {link.label}
694 </SidebarLink>
695 ))}
696 </div>
697 </nav>
698 );
699 }
700 return (
701 <nav aria-label="g1t" className={PANEL}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar702 {/* Everything here is the workspace the switcher names: its home first.
703 The workspace's own page is the switcher's; Explore, all of g1t,
704 is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look705 <div className="mt-3 space-y-px">
706 <SidebarLink to="/" end icon={<House size={15} />}>
707 Mission control
708 </SidebarLink>
709 </div>
710
711 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar712 {ws ? (
713 <SidebarProjects slug={ws.slug} shell={shell} />
714 ) : (
715 <SidebarGroup title="Projects">
716 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
717 </SidebarGroup>
718 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look719 {(shell.shared ?? []).length > 0 && (
720 <SidebarGroup title="Shared with you" className="mt-3">
721 {(shell.shared ?? []).map((repo) => (
722 <SidebarLink
723 key={`${repo.namespace}/${repo.name}`}
724 to={`/${repo.namespace}/${repo.name}`}
725 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
726 drill="hover"
727 >
728 <span className="font-mono text-faint">{repo.namespace}/</span>
729 {repo.name}
730 </SidebarLink>
731 ))}
732 </SidebarGroup>
733 )}
734
735 {ws && (
736 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member737 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar738 <SidebarGroup title="Workspace">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look739 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
740 Agent fleet
741 </SidebarLink>
742 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
743 Context
744 </SidebarLink>
745 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
746 Memory
747 </SidebarLink>
748 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
749 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents750 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member751 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
752 Packages
753 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar754 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look755 <SidebarSoonLink
756 key={item.key}
757 to={`/${ws.slug}/-/soon/${item.key}`}
758 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
759 about={item.summary}
760 >
761 {item.title === "Board" ? "Boards" : item.title}
762 </SidebarSoonLink>
763 ))}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar764 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look765
766 <Rule />
767 <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 bar768 {/* Who belongs, for every member to see; owners invite and manage there. */}
769 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar770 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar771 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar772 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams}>
773 Teams
774 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look775 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
776 Usage
777 </SidebarLink>
778 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
779 Support
780 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar781 {/* 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 look782 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member783 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 look784 icon={<Settings size={15} />}
785 drill
786 >
787 Settings
788 </SidebarLink>
789 </div>
790 </>
791 )}
792 </nav>
793 );
794}
795
796/** A workspace's settings, drilled into from Settings in the main list. */
797function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
798 return (
799 <nav aria-label="Workspace settings" className={PANEL}>
800 <BackRow to={`/${slug}`} label="Settings" context={slug} />
801 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings802 {owner && (
803 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
804 General
805 </SidebarLink>
806 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look807 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
808 Repositories
809 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments810 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
811 Access tokens
812 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look813 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar814 <SidebarGroup title="Agents and runs" className="mt-3">
815 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
816 Guardrails
817 </SidebarLink>
818 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
819 Secrets and variables
820 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it821 {owner && (
822 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
823 Runners
824 </SidebarLink>
825 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar826 </SidebarGroup>
827 <SidebarGroup title="Connections" className="mt-3">
828 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
829 Integrations
830 </SidebarLink>
831 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
832 Webhooks
833 </SidebarLink>
834 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look835 <Rule />
836 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings837 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments838 Billing and plans
A catalogue of model providers, and settings that feel like settings839 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API840 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
841 Audit log
842 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look843 </div>
A catalogue of model providers, and settings that feel like settings844 </nav>
845 );
846}
847
Sidebar: no settings flash on the way into a repository, and a better slide848/**
849 * What the repository menu needs. Known from the address before the page's
850 * data arrives, so the menu is right from the first frame; the counts fill
851 * in once it does.
852 */
853type MenuRepo = {
854 namespace: string;
855 name: string;
856 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look857 /** Whether they see its settings; from the repository's page once it loads. */
858 settings?: boolean;
859 /** What they may do there, once the repository's page loads. */
860 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide861 issues?: number;
862 pulls?: number;
863};
864
865function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
866 return (
867 a != null &&
868 b != null &&
869 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
870 a.name.toLowerCase() === b.name.toLowerCase()
871 );
872}
A repository has its own sidebar, as settings do873
874/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look875 * A project's own list, drilled into while you are in it: everything about
876 * the project, running and its code, and nothing else, with the way back
877 * out to all of them.
A repository has its own sidebar, as settings do878 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put879function RepoMenu({
880 repo,
881 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look882 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put883}: {
884 repo: MenuRepo;
885 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look886 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
887 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put888}) {
A repository has its own sidebar, as settings do889 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 put890 // 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 look891 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do892 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look893 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
894 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner895 <NavLink
A repository has its own sidebar, as settings do896 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner897 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents898 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner899 title="Overview"
900 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look901 `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 owner902 }
A repository has its own sidebar, as settings do903 >
904 <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 page905 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do906 </span>
907 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
908 <span className="text-faint">{repo.namespace}/</span>
909 <span className="font-semibold text-fg">{repo.name}</span>
910 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner911 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look912 {/* A project's pages, in the order people use them. A page with more
913 than one view shows them as tabs across its top. */}
914 <Rule />
915 <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 request916 <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 owner917 Code
918 </SidebarLink>
919 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
920 Issues
921 </SidebarLink>
922 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
923 Pull requests
924 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains925 <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 owner926 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains927 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner928 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
929 Workflows
Projects: what a workspace builds and runs, first on every page930 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look931 </div>
932 <Rule />
933 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put934 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner935 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
936 Deployments
937 </SidebarLink>
938 ) : null}
Observability gets its own page; tabs never overflow939 <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.">
940 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner941 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put942 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API943 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
944 Security
945 </SidebarLink>
946 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner947 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
948 Insights
949 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look950 </div>
951 {(repo.settings ?? shows.has("settings")) && (
952 <>
953 <Rule />
954 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner955 Settings
956 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look957 </>
958 )}
A repository has its own sidebar, as settings do959 </nav>
960 );
961}
962
Project settings move into the sidebar, grouped, with a heading per page963/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look964 * A project's settings, drilled into from its list: what is about running
965 * it, then its agents, then its code and who can reach it. The way back
966 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page967 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look968function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page969 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look970 // Each page shows to the roles that can use it, once the role is known.
971 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
972 const running = shows("deployments");
973 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page974 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look975 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
976 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
977 {shows("") && (
978 <div className="mt-2 space-y-px">
979 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
980 General
981 </SidebarLink>
982 </div>
983 )}
984 {running && (
985 <>
986 {shows("") && <Rule />}
987 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
988 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
989 Deployments
990 </SidebarLink>
991 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
992 Domains
993 </SidebarLink>
994 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
995 Dependencies
996 </SidebarLink>
997 </div>
998 </>
999 )}
1000 {agents && (
1001 <>
1002 <Rule />
1003 <div className="space-y-px">
1004 {shows("agents") && (
1005 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1006 Agents
1007 </SidebarLink>
1008 )}
1009 {shows("guardrails") && (
1010 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1011 Guardrails
1012 </SidebarLink>
1013 )}
1014 </div>
1015 </>
1016 )}
1017 <Rule />
1018 <div className="space-y-px">
1019 {shows("repository") && (
1020 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1021 Repository
1022 </SidebarLink>
1023 )}
1024 {shows("access") && (
1025 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1026 Access
1027 </SidebarLink>
1028 )}
1029 {shows("branches") && (
1030 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1031 Branches and merging
1032 </SidebarLink>
1033 )}
1034 {shows("secrets") && (
1035 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1036 Secrets and variables
1037 </SidebarLink>
1038 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1039 {shows("runners") && (
1040 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1041 Runners
1042 </SidebarLink>
1043 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1044 {shows("webhooks") && (
1045 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1046 Webhooks
1047 </SidebarLink>
1048 )}
1049 </div>
Project settings move into the sidebar, grouped, with a heading per page1050 </nav>
1051 );
1052}
1053
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1054/** Your own settings, drilled into from Your settings: one page each. */
1055function AccountSettingsMenu({ username }: { username: string }) {
1056 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1057 <SidebarLink to={`/settings/${page}`} icon={icon}>
1058 {ACCOUNT_SETTINGS[page].title}
1059 </SidebarLink>
Account settings in the same shape as a workspace's1060 );
1061 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1062 <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 quotas1063 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1064 <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 quotas1065 {link("profile", <CircleUserRound size={15} />)}
1066 {link("emails", <Mail size={15} />)}
1067 {link("invites", <Ticket size={15} />)}
1068 </div>
1069 <Rule />
1070 <div className="space-y-px">
1071 {link("keys", <Fingerprint size={15} />)}
1072 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1073 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1074 <Rule />
1075 <div className="space-y-px">
1076 {link("github", <GithubMark className="size-[15px]" />)}
1077 {link("applications", <Plug size={15} />)}
1078 </div>
1079 <Rule />
1080 <div className="space-y-px">
1081 {link("security-log", <History size={15} />)}
1082 </div>
Account settings in the same shape as a workspace's1083 </nav>
1084 );
1085}
1086
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1087/**
1088 * Signing in and signing up, in place of the account for a visitor. Signing
1089 * in brings them back to the page they are on.
1090 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1091/** Help, status and the fine print under a visitor's sign-in buttons. */
1092function VisitorLinks() {
1093 const status = useSiteStatus();
1094 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1095 const dot = (
1096 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1097 ·
1098 </span>
1099 );
1100 return (
1101 // Two short rows, so a narrow sidebar never starts a line with a dot.
1102 <nav aria-label="About g1t" className="pt-0.5">
1103 <div className="flex items-center gap-0.5">
1104 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1105 <StatusDot state={status?.overall.state ?? null} />
1106 Status
1107 </a>
1108 {dot}
1109 <Link to="/support" className={quiet}>
1110 Support
1111 </Link>
1112 </div>
1113 <div className="flex items-center gap-0.5">
1114 {LEGAL_LINKS.map(([label, to], index) => (
1115 <span key={to} className="flex items-center gap-0.5">
1116 {index > 0 && dot}
1117 <Link to={to} className={quiet}>
1118 {label}
1119 </Link>
1120 </span>
1121 ))}
1122 </div>
1123 </nav>
1124 );
1125}
1126
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1127function VisitorPanel() {
1128 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1129 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1130 return (
1131 <div className="space-y-2">
1132 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1133 <div className="grid grid-cols-2 gap-2">
1134 <Link
1135 to={withNext("/login", pathname + search)}
1136 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"
1137 >
1138 <LogIn size={14} />
1139 Sign in
1140 </Link>
1141 <Link
1142 to={withNext("/register", pathname + search)}
1143 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1144 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1145 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1146 </Link>
1147 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1148 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1149 </div>
1150 );
1151}
1152
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1153function Sidebar({
1154 user,
1155 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1156 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1157 onFind,
1158 onClose,
1159}: {
1160 user: User | null;
1161 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1162 /** The page is a 404: the address names nothing the viewer can see. */
1163 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1164 onFind: () => void;
1165 /** In the sheet on a small screen: closing it, at the end of the top row. */
1166 onClose?: () => void;
1167}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1168 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1169 const { pathname } = useLocation();
1170 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1171 // Where the sidebar is drilled to follows the page being gone to, so it
1172 // moves as the link is followed, not once the page arrives.
1173 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1174 // On a 404, the address is not a place: no list is drilled into from it,
1175 // except the project's own when the project is real and visible and only
1176 // something inside it is missing (a file, a commit, an issue).
1177 const lost = missing && going == null;
1178 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1179 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1180 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1181 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1182 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1183 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 look1184 // An invitation to a repository is answered before its menu means anything.
1185 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1186 repoPath != null &&
1187 !reserved.has(repoPath[1]) &&
1188 repoPath[2] !== "-" &&
1189 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1190 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1191 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1192 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1193 // The repository the menu is for: the one loaded if it is the one being
1194 // gone to, else what the address says, at once.
1195 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 look1196 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1197 ? sameRepo(active, targetRepo)
1198 ? active
1199 : {
1200 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1201 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1202 }
1203 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1204 // The viewer's role there, from the repository's page once it has loaded.
1205 const page = useRouteLoaderData("routes/repo/layout") as
1206 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1207 | undefined;
1208 const menuRepo: MenuRepo | null =
1209 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1210 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1211 : guessed;
1212
1213 // The way from the main list to the one shown.
1214 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1215 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1216 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1217 } else if (ws && inSettings) {
1218 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1219 } else if (menuRepo) {
1220 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1221 // Out of a project: to its workspace's projects when they are yours.
1222 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1223 const back = !user
1224 ? { to: "/explore", label: "Explore" }
1225 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1226 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1227 : { to: "/", label: "Mission control" };
1228 trail.push({
1229 key,
1230 node: (
1231 <RepoMenu
1232 repo={menuRepo}
1233 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1234 back={back}
1235 />
1236 ),
1237 });
1238 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1239 }
1240
Agents as a team: lifecycle, merge queue, billing and a new shell1241 return (
1242 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1243 {/* 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 bar1244 <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 put1245 {user ? (
1246 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1247 {/* The 1 alone beside the workspace: a square hover the height of the
1248 switcher, the mark as tall as the workspace avatar. */}
1249 <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 bar1250 <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 put1251 </Link>
1252 <span className="shrink-0 text-line-strong" aria-hidden="true">
1253 /
1254 </span>
1255 <WorkspaceSwitcher user={user} shell={shell} />
1256 </>
1257 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1258 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1259 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1260 </Link>
1261 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1262 {onClose && (
1263 <button
1264 type="button"
1265 aria-label="Close menu"
1266 onClick={onClose}
1267 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1268 >
1269 <X size={16} />
1270 </button>
1271 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1272 </div>
A Vercel-like sidebar and a new mark1273 <div className="px-2 pt-3">
1274 <button
1275 type="button"
1276 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1277 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 mark1278 >
1279 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1280 <span className="grow text-left">Search or jump to…</span>
A Vercel-like sidebar and a new mark1281 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1282 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1283 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1284 <Drill trail={trail} />
1285 {/* The account, or signing in, as one row at the very bottom. */}
1286 <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 put1287 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1288 </div>
1289 </div>
1290 );
1291}
1292
1293/** Words for the sections a path can end in. */
1294const SECTIONS: Record<string, string> = {
1295 issues: "Issues",
1296 pulls: "Pull requests",
1297 queue: "Merge queue",
1298 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1299 plans: "Outcomes",
1300 actions: "Workflows",
1301 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1302 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1303 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1304 access: "Access",
1305 invitations: "Invitation",
1306 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1307 branches: "Branches",
1308 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1309 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1310 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1311 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1312 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1313 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1314 people: "People",
1315 projects: "Projects",
1316 teams: "Teams",
A Vercel-like sidebar and a new mark1317 tokens: "Access tokens",
1318 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1319 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1320 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1321 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1322 domains: "Domains",
1323 guardrails: "Guardrails",
1324 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1325 tree: "Files",
1326 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1327 agents: "Agents",
1328 context: "Context",
1329 memory: "Memory",
1330 security: "Security",
1331 packages: "Packages",
1332 runners: "Runners",
1333 workflows: "Workflows",
1334 observability: "Observability",
1335 insights: "Insights",
1336 sessions: "Sessions",
A Vercel-like sidebar and a new mark1337};
1338
1339/** 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 quotas1340function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1341 // A 404 names nothing from the address, unless the project is real and
1342 // visible and only something inside it is missing.
1343 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1344 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1345 return <span className="text-sm font-medium">Not found</span>;
1346 }
A Vercel-like sidebar and a new mark1347 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1348 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1349 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 quotas1350 // Your settings: Settings / Emails.
1351 if (parts[0] === "settings") {
1352 const page = accountSettingsPage(pathname);
1353 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1354 return (
1355 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1356 {trail.map((crumb, index) => (
1357 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1358 {index > 0 && <span className="text-line-strong">/</span>}
1359 <Link
1360 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1361 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1362 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1363 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1364 }`}
1365 >
1366 {crumb.label}
1367 </Link>
1368 </span>
1369 ))}
1370 </nav>
1371 );
1372 }
A Vercel-like sidebar and a new mark1373 if (reserved.includes(parts[0]!)) {
1374 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1375 settings: "Account",
A Vercel-like sidebar and a new mark1376 explore: "Explore",
Projects live: fixes from walking them end to end1377 new: "New project",
A Vercel-like sidebar and a new mark1378 search: "Search",
1379 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1380 policies: "Policies",
1381 security: "Security",
1382 support: "Support",
1383 status: "Status",
1384 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1385 inbox: "Inbox",
A Vercel-like sidebar and a new mark1386 };
1387 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1388 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1389 // A person's profile, by their handle.
1390 if (parts[0] === "u" && parts[1]) {
1391 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1392 }
A Vercel-like sidebar and a new mark1393 const [owner, second, third, fourth] = parts;
1394 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1395 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1396 const page = `/${owner}/-/${third}`;
1397 if (third && SETTINGS_PAGE.test(page)) {
1398 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1399 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1400 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1401 } else if (second) {
1402 const repo = `/${owner}/${second}`;
1403 trail.push({ label: second, to: repo, mono: true });
1404 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1405 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1406 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 deployments1407 else if (third && SECTIONS[third]) {
1408 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1409 // A settings page names which one: Settings / Secrets and variables.
1410 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1411 trail.push({ label: fourth === "branches" ? "Branches and merging" : SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1412 }
1413 }
A Vercel-like sidebar and a new mark1414 }
1415 return (
1416 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1417 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1418 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1419 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1420 <Link
A Vercel-like sidebar and a new mark1421 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1422 prefetch="intent"
A Vercel-like sidebar and a new mark1423 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1424 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1425 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1426 >
A Vercel-like sidebar and a new mark1427 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1428 </Link>
A Vercel-like sidebar and a new mark1429 </span>
1430 ))}
1431 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1432 );
1433}
1434
Search across all of g1t, Explore, and a command palette1435type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1436
1437/** 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 look1438function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1439 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1440 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1441 { 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 step1442 ...(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-agent1443 ? [{ 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 step1444 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1445 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1446 { 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 all1447 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1448 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1449 { 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 quotas1450 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1451 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1452 .filter((page) => page !== "profile")
1453 .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 shell1454 ];
1455 const repo = shell.repo;
1456 if (repo) {
1457 const base = `/${repo.namespace}/${repo.name}`;
1458 const name = `${repo.namespace}/${repo.name}`;
1459 commands.unshift(
Projects: what a workspace builds and runs, first on every page1460 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1461 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1462 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1463 { 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 step1464 ...(repo.member
1465 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1466 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1467 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1468 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1469 ...(repo.member
1470 ? [
1471 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1472 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1473 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1474 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1475 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1476 ]
1477 : []),
1478 );
1479 }
1480 for (const membership of user.workspaces ?? []) {
1481 commands.push(
Workspace names and icons, and a component kit for every control1482 { 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 sidebar1483 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1484 { 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 request1485 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1486 { 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 settings1487 { 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 look1488 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1489 );
1490 }
1491 for (const listed of shell.repos) {
1492 commands.push({
Projects: what a workspace builds and runs, first on every page1493 label: listed.title ?? listed.name,
1494 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1495 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1496 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1497 });
1498 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1499 // "Put an agent on …": one per project, after the projects themselves.
1500 for (const listed of shell.repos) {
1501 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1502 commands.push({
1503 label: `Put an agent on ${listed.title ?? listed.name}`,
1504 hint: `${listed.namespace}/${listed.name}`,
1505 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1506 icon: <Sparkles size={15} />,
1507 });
1508 }
Agents as a team: lifecycle, merge queue, billing and a new shell1509 return commands;
1510}
1511
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1512/** 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 look1513function 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 put1514 const commands: Command[] = [];
1515 const repo = shell.repo;
1516 if (repo) {
1517 const base = `/${repo.namespace}/${repo.name}`;
1518 const name = `${repo.namespace}/${repo.name}`;
1519 commands.push(
1520 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1521 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1522 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1523 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1524 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1525 );
1526 }
1527 commands.push(
1528 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1529 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1530 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1531 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1532 { 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 look1533 { 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 put1534 );
1535 return commands;
1536}
1537
Agents as a team: lifecycle, merge queue, billing and a new shell1538/**
Merge branch 'worktree-agent-aca4790ea223aa235'1539 * The page while the next one loads: after a moment it dims, under the
1540 * bar below, so it is plainly on its way out rather than frozen, and the
1541 * next page fades up from there. Sending a form does not dim it: the
1542 * form's button says it is working. Spread on the element holding the page.
1543 */
1544export function useLeaving() {
1545 const navigation = useNavigation();
1546 const method = navigation.formMethod?.toUpperCase();
1547 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1548 return {
1549 "aria-busy": leaving || undefined,
1550 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1551 };
1552}
1553
1554/**
Agents as a team: lifecycle, merge queue, billing and a new shell1555 * A bar across the top of the page while the next one loads, as GitHub
1556 * has: it appears at once, creeps towards the end while waiting, then fills
1557 * and fades when the page arrives.
1558 */
1559export function Progress() {
1560 const navigation = useNavigation();
1561 const busy = navigation.state !== "idle";
1562 const [width, setWidth] = useState(0);
1563 const [visible, setVisible] = useState(false);
1564 useEffect(() => {
1565 if (busy) {
1566 setVisible(true);
1567 setWidth(12);
1568 // Slows as it goes, never quite arriving until the page does.
1569 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1570 return () => clearInterval(timer);
1571 }
1572 setWidth((now) => (now > 0 ? 100 : 0));
1573 const fade = setTimeout(() => setVisible(false), 250);
1574 const reset = setTimeout(() => setWidth(0), 550);
1575 return () => {
1576 clearTimeout(fade);
1577 clearTimeout(reset);
1578 };
1579 }, [busy]);
1580 return (
1581 <div
1582 aria-hidden="true"
1583 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1584 visible ? "opacity-100" : "opacity-0"
1585 }`}
1586 >
1587 <div
1588 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1589 style={{ width: `${width}%` }}
1590 />
1591 </div>
1592 );
1593}
1594
1595/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1596 * The app: a sidebar with the workspace, its repositories and the sections
1597 * of the one being looked at; a slim bar with search and the account; and
1598 * the page. A visitor who is not signed in gets the same frame, with
1599 * Explore and Search in place of the workspace, and signing in in place of
1600 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1601 */
1602export function AppShell({
1603 user,
1604 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1605 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1606 banner,
1607 children,
1608}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1609 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1610 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1611 /** The page is a 404 (see Sidebar). */
1612 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1613 banner?: ReactNode;
1614 children: ReactNode;
1615}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1616 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1617 const [drawer, setDrawer] = useState(false);
1618 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1619 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1620 const signUpLabel = useSignUpCopy().primary;
1621 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1622
1623 // A new page closes the drawer on small screens.
1624 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1625 // Mission control's code, fetched while the browser is idle, so going
1626 // home never waits on it (routes/home.tsx).
1627 useEffect(() => {
1628 if (!user) return;
1629 const load = () => void import("./mission-control").catch(() => undefined);
1630 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1631 if (idle) idle(load);
1632 else setTimeout(load, 1500);
1633 }, [user]);
Search across all of g1t, Explore, and a command palette1634 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1635 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1636
1637 return (
1638 <div className="min-h-screen">
1639 <Progress />
1640 <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 quotas1641 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1642 </aside>
1643 {drawer && (
1644 <div className="fixed inset-0 z-50 lg:hidden">
1645 <button
1646 type="button"
1647 aria-label="Close menu"
1648 className="absolute inset-0 bg-black/60"
1649 onClick={() => setDrawer(false)}
1650 />
1651 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1652 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1653 </aside>
1654 </div>
1655 )}
1656
Polish: phones, copy boxes, the plan page, the landing page, a real glide1657 <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 bar1658 <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">
Agents as a team: lifecycle, merge queue, billing and a new shell1659 <button
1660 type="button"
1661 aria-label="Open menu"
1662 onClick={() => setDrawer(true)}
1663 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1664 >
1665 <Menu size={18} />
1666 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1667 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1668 <div className="ml-auto flex items-center gap-1.5">
1669 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1670 <button
1671 type="button"
1672 aria-label="Search or jump to"
1673 onClick={() => setPalette(true)}
1674 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1675 >
1676 <Search size={16} />
1677 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1678 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1679 <NavLink
1680 to="/explore"
1681 prefetch="intent"
1682 aria-label="Explore"
1683 className={({ isActive }) =>
1684 `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"}`
1685 }
1686 >
1687 <Compass size={16} className="sm:hidden" />
1688 <span className="hidden sm:inline">Explore</span>
1689 </NavLink>
A Vercel-like sidebar and a new mark1690 <a
1691 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1692 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 mark1693 >
1694 Docs
1695 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1696 {user && (
1697 <>
1698 <AskAi />
1699 <InboxBell counts={shell.inbox ?? null} />
1700 </>
1701 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1702 {!user ? (
1703 // The sidebar has these too, but on a phone it is folded away.
1704 <Link
1705 to={withNext("/login", here)}
1706 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"
1707 >
1708 Sign in
1709 </Link>
1710 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1711 <>
1712 {/* Room between what comes in (the inbox) and what you make. */}
1713 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1714 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1715 <Tooltip>
1716 <TooltipTrigger asChild>
1717 <DropdownMenuTrigger
1718 aria-label="Create new"
1719 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"
1720 >
1721 <Plus size={16} />
1722 <ChevronDown size={13} className="text-muted" />
1723 </DropdownMenuTrigger>
1724 </TooltipTrigger>
1725 <TooltipContent>Create new…</TooltipContent>
1726 </Tooltip>
1727 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1728 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1729 <>
1730 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1731 <DropdownMenuItem asChild>
1732 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1733 <CircleDot />
1734 New issue
1735 </Link>
1736 </DropdownMenuItem>
1737 {shell.repo.member && (
1738 <DropdownMenuItem asChild>
1739 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1740 <ListTree />
1741 Plan work
1742 </Link>
1743 </DropdownMenuItem>
1744 )}
1745 <DropdownMenuSeparator />
1746 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1747 )}
1748 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1749 <Link to="/?agent=new">
1750 <Sparkles />
1751 Put an agent on it
1752 </Link>
1753 </DropdownMenuItem>
1754 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1755 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1756 <Box />
1757 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1758 </Link>
1759 </DropdownMenuItem>
1760 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1761 <Link to="/new/github">
1762 <GitBranch />
1763 Import from GitHub
1764 </Link>
1765 </DropdownMenuItem>
1766 <DropdownMenuSeparator />
1767 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1768 <Link to="/workspaces/new">
1769 <Users />
1770 New workspace
1771 </Link>
1772 </DropdownMenuItem>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1773 {/* A team in the workspace the sidebar is about. */}
1774 {shell.workspace && (
1775 <DropdownMenuItem asChild>
1776 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1777 <UsersRound />
1778 New team
1779 </Link>
1780 </DropdownMenuItem>
1781 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1782 </DropdownMenuContent>
1783 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1784 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1785 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1786 </div>
1787 </header>
1788 {banner}
Merge branch 'worktree-agent-aca4790ea223aa235'1789 <main {...leaving} className={`min-w-0 grow ${leaving.className}`}>
1790 {children}
1791 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1792 </div>
Search across all of g1t, Explore, and a command palette1793 <CommandPalette
1794 open={palette}
1795 onOpenChange={setPalette}
1796 commands={commands}
1797 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1798 />
Agents as a team: lifecycle, merge queue, billing and a new shell1799 </div>
1800 );
1801}

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