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

1,590 lines62,321 bytesCodeBlame

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

Agents as a team: lifecycle, merge queue, billing and a new shell1import {
Observability gets its own page; tabs never overflow2 Activity,
A flat project menu with Code first, views as tabs, and Stripe for every owner3 GanttChart,
4 KanbanSquare,
5 Package,
6 Sparkles,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request7 BarChart3,
Agents as a team: lifecycle, merge queue, billing and a new shell8 BookMarked,
9 BookOpen,
10 Check,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11 ChevronLeft,
12 ChevronRight,
Agents as a team: lifecycle, merge queue, billing and a new shell13 ChevronsUpDown,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas14 CircleUserRound,
15 Mail,
16 Ticket,
Agents as a team: lifecycle, merge queue, billing and a new shell17 CircleDot,
18 Code2,
19 Compass,
20 CreditCard,
21 GitPullRequest,
22 History,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23 House,
Secrets and variables: one list, rows per environment, for workflows and deployments24 Fingerprint,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25 Gauge,
Agents as a team: lifecycle, merge queue, billing and a new shell26 KeyRound,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27 LifeBuoy,
Projects: what a workspace builds and runs, first on every page28 Box,
29 LayoutGrid,
Agents as a team: lifecycle, merge queue, billing and a new shell30 ListTree,
31 Lock,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put32 LogIn,
Agents as a team: lifecycle, merge queue, billing and a new shell33 LogOut,
34 Menu,
35 Plus,
36 Search,
Integrations: your own model provider, alerts that open issues, tickets agents read37 Plug,
Agents as a team: lifecycle, merge queue, billing and a new shell38 Settings,
39 Users,
Webhooks: every event, to your own addresses, signed and retried40 Webhook,
Project settings move into the sidebar, grouped, with a heading per page41 Globe,
42 GitBranch,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs43 PlayCircle,
Pull requests say where they are live; the project menu shows what is coming44 Bot,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains45 Brain,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API46 Network,
Pull requests say where they are live; the project menu shows what is coming47 ShieldCheck,
Deployments: a preview for every pull request, production on g1t.page48 Rocket,
Agents as a team: lifecycle, merge queue, billing and a new shell49 X,
50} from "lucide-react";
51import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas52import { Form, Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell53
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54import type { Abilities, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell55
Search across all of g1t, Explore, and a command palette56import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas57import { StatusDot, useSiteStatus } from "./footer";
58import { Logo } from "./logo";
Search across all of g1t, Explore, and a command palette59import { Avatar, notACredential } from "./ui";
Agents as a team: lifecycle, merge queue, billing and a new shell60import {
61 DropdownMenu,
62 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas63 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell64 DropdownMenuItem,
65 DropdownMenuLabel,
66 DropdownMenuSeparator,
67 DropdownMenuTrigger,
68} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales69import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look70import { 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 put71import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas72import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
73import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put74import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas76import { STATUS_URL, statusTitle } from "../lib/status";
77import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell78
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put79/**
80 * What the sidebar needs, worked out by the root loader. For a visitor who
81 * is not signed in there is no workspace, no projects and no usage: only
82 * the project being looked at, if they can see it.
83 */
Agents as a team: lifecycle, merge queue, billing and a new shell84export type ShellData = {
85 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control86 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page87 /** Its projects, by name: `name` is the slug in their address. */
88 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look89 /** Repositories shared with them in workspaces they do not belong to. */
90 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page91 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell92 repo: {
93 namespace: string;
94 name: string;
95 member: boolean;
96 issues: number;
97 pulls: number;
98 } | null;
Billing accounts, terms and enterprises; g1t is no longer free99 /** Where the workspace stands against its usage limit, if billing is on. */
100 limit: {
101 exposureMicros: number;
102 ceilingMicros: number | null;
103 state: "ok" | "warning" | "stopped";
104 comped: boolean;
105 } | null;
Free while g1t is being built out; agents can check out their own forks106 /** Whether g1t charges nothing for now, while it is being built out. */
107 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request108 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy109 /** This month's usage: charged, or at cost while g1t is free. */
110 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look111 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
112 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Agents as a team: lifecycle, merge queue, billing and a new shell113};
114
115function SidebarLink({
116 to,
117 icon,
118 end,
119 count,
120 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look121 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell122 children,
123}: {
124 to: string;
125 icon: ReactNode;
126 end?: boolean;
127 count?: number;
Projects: what a workspace builds and runs, first on every page128 /** Other path prefixes under which this link is the current one. */
129 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look130 /** It opens a list of its own: a chevron says so, always or on hover. */
131 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell132 children: ReactNode;
133}) {
134 const { pathname } = useLocation();
135 return (
136 <NavLink
137 to={to}
138 end={end}
139 prefetch="intent"
140 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page141 const current =
142 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark143 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 shell144 current
145 ? "bg-raised font-medium text-fg"
146 : isPending
147 ? "bg-raised/60 text-fg"
148 : "text-muted hover:bg-raised/60 hover:text-fg"
149 }`;
150 }}
151 >
152 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
153 <span className="min-w-0 grow truncate">{children}</span>
154 {count != null && count > 0 && (
155 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
156 {count}
157 </span>
158 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look159 {drill && (
160 <ChevronRight
161 size={14}
162 aria-hidden="true"
163 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
164 />
165 )}
Agents as a team: lifecycle, merge queue, billing and a new shell166 </NavLink>
167 );
168}
169
Pull requests say where they are live; the project menu shows what is coming170/**
171 * Something a project will have, shown where it will live so people can
172 * see where g1t is going. Not a link: nothing is behind it yet.
173 */
174function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
175 return (
176 <div
177 title={about}
178 aria-disabled="true"
179 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
180 >
181 <span className="shrink-0 opacity-70">{icon}</span>
182 <span className="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>
186 </div>
187 );
188}
189
The project menu opens by section, and the workspace has its own panel190/**
A flat project menu with Code first, views as tabs, and Stripe for every owner191 * A page that is coming, as a link to its roadmap page: faint, with Soon,
192 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel193 */
A flat project menu with Code first, views as tabs, and Stripe for every owner194function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel195 to,
196 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner197 icon,
198 about,
The project menu opens by section, and the workspace has its own panel199 children,
200}: {
201 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner202 also?: string[];
203 icon: ReactNode;
204 about: string;
The project menu opens by section, and the workspace has its own panel205 children: ReactNode;
206}) {
207 const { pathname } = useLocation();
208 return (
209 <NavLink
210 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner211 title={about}
The project menu opens by section, and the workspace has its own panel212 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner213 className={({ isActive }) => {
214 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
215 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
216 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 panel217 }`;
218 }}
219 >
A flat project menu with Code first, views as tabs, and Stripe for every owner220 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel221 <span className="min-w-0 grow truncate">{children}</span>
222 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
223 Soon
224 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales225 </NavLink>
The project menu opens by section, and the workspace has its own panel226 );
227}
228
A flat project menu with Code first, views as tabs, and Stripe for every owner229/** Icons for what spans projects. */
230const WORKSPACE_ICONS: Record<string, ReactNode> = {
231 board: <KanbanSquare size={15} />,
232 roadmap: <GanttChart size={15} />,
233 packages: <Package size={15} />,
234 fleet: <Bot size={15} />,
235};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales236
A flat project menu with Code first, views as tabs, and Stripe for every owner237/** The Soon pages of a section, as paths, so its link is current on them. */
238function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
239 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales240}
241
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242function SidebarGroup({
243 title,
244 action,
245 className = "",
246 children,
247}: {
248 title: string;
249 action?: ReactNode;
250 className?: string;
251 children: ReactNode;
252}) {
Agents as a team: lifecycle, merge queue, billing and a new shell253 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look254 <section className={className}>
A Vercel-like sidebar and a new mark255 <div className="mb-1 flex h-6 items-center justify-between px-2">
256 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell257 {action}
258 </div>
259 <div className="space-y-px">{children}</div>
260 </section>
261 );
262}
263
Workspace names and icons, and a component kit for every control264/** What a workspace is called where people read it: its display name, else its slug. */
265function displayName(membership: Membership): string {
266 return membership.name?.trim() || membership.slug;
267}
268
Agents as a team: lifecycle, merge queue, billing and a new shell269function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control270 const workspace = shell.workspace;
271 const current = workspace?.slug;
272 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell273 return (
274 <DropdownMenu>
Workspace names and icons, and a component kit for every control275 {/* The name goes to the workspace; only the arrows switch it. The
276 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark277 <Link
278 to={current ? `/${current}` : "/workspaces/new"}
Workspace names and icons, and a component kit for every control279 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look280 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 mark281 >
Workspace names and icons, and a component kit for every control282 {workspace ? (
283 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell284 ) : (
Workspace names and icons, and a component kit for every control285 <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 shell286 )}
Workspace names and icons, and a component kit for every control287 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark288 </Link>
289 <DropdownMenuTrigger
290 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look291 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 mark292 >
293 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell294 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control295 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell296 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
297 {(user.workspaces ?? []).map((membership) => (
298 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control299 <Link to={`/${membership.slug}`} title={displayName(membership)}>
300 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
301 <span className="flex min-w-0 grow flex-col leading-tight">
302 <span className="truncate">{displayName(membership)}</span>
303 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
304 </span>
305 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
306 {membership.slug === current ? (
307 <Check className="shrink-0 text-accent" />
308 ) : (
309 <span className="size-4 shrink-0" aria-hidden="true" />
310 )}
Agents as a team: lifecycle, merge queue, billing and a new shell311 </Link>
312 </DropdownMenuItem>
313 ))}
314 <DropdownMenuSeparator />
315 <DropdownMenuItem asChild>
316 <Link to="/workspaces/new">
317 <Plus />
318 New workspace
319 </Link>
320 </DropdownMenuItem>
321 </DropdownMenuContent>
322 </DropdownMenu>
A Vercel-like sidebar and a new mark323 );
324}
325
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas326/**
327 * The fine print, as one quiet row at the foot of the account menu and the
328 * visitor's panel: each link a full-height target, left-aligned. In the
329 * menu each is a menu item, so the arrow keys reach it.
330 */
331const LEGAL_LINKS: [string, string][] = [
332 ["Policies", "/policies"],
333 ["Privacy", "/policies/privacy"],
334 ["Security", "/security"],
335];
336const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
337
338function MenuLegalRow() {
339 return (
340 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
341 {LEGAL_LINKS.map(([label, to], index) => (
342 <span key={to} className="flex items-center gap-0.5">
343 {index > 0 && (
344 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
345 ·
346 </span>
347 )}
348 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
349 <Link to={to}>{label}</Link>
350 </DropdownMenuItem>
351 </span>
352 ))}
353 </div>
354 );
355}
356
357/** Status's row: the live dot and two or three words, fetched when the menu opens. */
358function StatusSummary({ open }: { open: boolean }) {
359 const status = useSiteStatus(open);
360 const words = statusTitle(status);
361 return (
362 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
363 {/* The words say it; the dot's own words for screen readers would repeat them. */}
364 <span aria-hidden="true" className="flex">
365 <StatusDot state={status?.overall.state ?? null} />
366 </span>
367 <span className="truncate">{words ?? "Checking…"}</span>
368 </span>
369 );
370}
371
372/** A menu row: an icon, words, and whatever sits at its end. */
373const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
374
A Vercel-like sidebar and a new mark375function AccountMenu({ user }: { user: User }) {
376 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas377 const [open, setOpen] = useState(false);
378 // Name, primary address and invites left: asked for once, when first opened.
379 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
380 useEffect(() => {
381 if (open && details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
382 }, [open, details]);
383 const me = details.data ?? null;
384 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark385 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas386 <DropdownMenu open={open} onOpenChange={setOpen}>
387 <DropdownMenuTrigger
388 aria-label={`Account menu for ${user.username}`}
389 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-merged/60 data-[state=open]:bg-raised"
390 >
Workspace names and icons, and a component kit for every control391 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark392 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas393 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark394 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas395 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
396 {/* Who is signed in, and a way to their profile. */}
397 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
398 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
399 <Avatar name={user.username} image={user.avatar} size={36} />
400 <span className="flex min-w-0 flex-col leading-tight">
401 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
402 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
403 {me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>}
404 </span>
A Vercel-like sidebar and a new mark405 </Link>
406 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas407 <DropdownMenuSeparator className="my-1.5" />
408 <DropdownMenuGroup>
409 <DropdownMenuItem asChild className={MENU_ROW}>
410 <Link to={profile}>
411 <CircleUserRound />
412 Your profile
413 </Link>
414 </DropdownMenuItem>
415 <DropdownMenuItem asChild className={MENU_ROW}>
416 <Link to={FIRST_SETTINGS_PAGE}>
417 <Settings />
418 Your settings
419 </Link>
420 </DropdownMenuItem>
421 <DropdownMenuItem asChild className={MENU_ROW}>
422 <Link to="/settings/invites">
423 <Ticket />
424 Invites
425 {me?.invites_left != null && (
426 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
427 {me.invites_left} left
428 </span>
429 )}
430 </Link>
431 </DropdownMenuItem>
432 </DropdownMenuGroup>
433 <DropdownMenuSeparator className="my-1.5" />
434 <DropdownMenuGroup>
435 <DropdownMenuItem asChild className={MENU_ROW}>
436 <a href="https://docs.g1t.sh/">
437 <BookOpen />
438 Documentation
439 </a>
440 </DropdownMenuItem>
441 <DropdownMenuItem asChild className={MENU_ROW}>
442 <Link to="/support">
443 <LifeBuoy />
444 Support
445 </Link>
446 </DropdownMenuItem>
447 <DropdownMenuItem asChild className={MENU_ROW}>
448 <a href={STATUS_URL}>
449 <Activity />
450 Status
451 <StatusSummary open={open} />
452 </a>
453 </DropdownMenuItem>
454 </DropdownMenuGroup>
455 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark456 {/* Submitted from here: the menu closes on select, and a button
457 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 quotas458 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark459 <LogOut />
460 Sign out
461 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas462 <DropdownMenuSeparator className="my-1.5" />
463 <MenuLegalRow />
A Vercel-like sidebar and a new mark464 </DropdownMenuContent>
465 </DropdownMenu>
466 );
467}
468
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look469/**
470 * A workspace's settings pages, which the sidebar drills into: who belongs,
471 * what it pays, and its record. What it builds and runs with (secrets,
472 * integrations, webhooks, guardrails) sits in the main list.
473 */
474const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|repositories|tokens|billing|audit)(\/|$)/;
475/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page476const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings477
Polish: phones, copy boxes, the plan page, the landing page, a real glide478/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look479 * The sidebar's lists are a stack, the way a phone pushes a screen: one
480 * drilled into slides in from the right over the full width, while the one
481 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide482 * ease-out, the fade quicker than the move so the two never blur together.
483 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide484 */
Sidebar: no settings flash on the way into a repository, and a better slide485const LAYER =
Sidebar: the panels really slide486 "absolute inset-0 [transition:translate_380ms_cubic-bezier(0.32,0.72,0,1),opacity_220ms_ease-out] will-change-[translate,opacity] motion-reduce:transition-none";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look487/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide488const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings489
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look490/** One list in the stack: what it is, and its rows. */
491type Level = { key: string; node: ReactNode };
492
493/** True once the page has hydrated, so what the server drew never animates in. */
494function useHydrated(): boolean {
495 const [hydrated, setHydrated] = useState(false);
496 useEffect(() => setHydrated(true), []);
497 return hydrated;
498}
499
500/**
501 * Drilling in and out of the sidebar's lists. `trail` is the way from the
502 * main list to the one shown, worked out from the address, so a link
503 * straight to a settings page opens the sidebar already drilled in, and
504 * leaving it comes back out. Lists deeper than the one shown stay in
505 * place, off to the right, so going back slides them away rather than
506 * dropping them. Moving to a list at the same depth (a workspace's settings
507 * to a project) fades the new one in where it is.
508 */
509function Drill({ trail }: { trail: Level[] }) {
510 const depth = trail.length - 1;
511 const hydrated = useHydrated();
512 const stack = useRef<Level[]>(trail);
513 const before = stack.current;
514 const within = trail.every((level, index) => before[index]?.key === level.key);
515 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
516 const levels = stack.current;
517 // What was showing last time, to tell drilling in from moving across.
518 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
519 useEffect(() => {
520 shown.current = { depth, keys: levels.map((level) => level.key) };
521 });
A catalogue of model providers, and settings that feel like settings522 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look523 <div className="relative min-h-0 grow overflow-hidden">
524 {levels.map((level, index) => {
525 const current = index === depth;
526 const place =
527 index < depth
528 ? "pointer-events-none -translate-x-1/4 opacity-0"
529 : current
530 ? "translate-x-0 opacity-100"
531 : "pointer-events-none translate-x-full opacity-0";
532 // A list that was not there before arrives: from the right when
533 // drilling in, in place when moving across.
534 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
535 const enter = arriving
536 ? depth > shown.current.depth
537 ? "starting:translate-x-full starting:opacity-0"
538 : "starting:opacity-0"
539 : "";
540 return (
541 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
542 {level.node}
543 </div>
544 );
545 })}
546 </div>
547 );
548}
549
550/**
551 * The row at the top of a list drilled into: the list's name, which goes
552 * back out, and what it belongs to, faint at the end.
553 */
554function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
555 return (
556 <Link
557 to={to}
558 prefetch="intent"
559 className="group mt-3 flex h-8 items-center gap-1.5 rounded-md pr-2 pl-1 text-[0.8125rem] font-medium text-fg transition-colors hover:bg-raised/60"
A catalogue of model providers, and settings that feel like settings560 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look561 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
562 <span className="min-w-0 shrink-0 truncate">{label}</span>
563 {context && (
564 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
565 )}
566 </Link>
567 );
568}
569
570/** A thin rule between groups of a list. */
571function Rule() {
572 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
573}
574
575/**
576 * The main list: where you go, the projects, what the workspace builds and
577 * runs with across them, then its usage, support and settings.
578 */
579function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
580 const ws = shell.workspace;
581 const active = shell.repo;
582 // The repository being looked at is listed even when it is someone else's.
583 const listed =
584 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
585 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
586 : shell.repos;
587 // A visitor browses: no workspace, no projects of their own.
588 if (!user) {
589 return (
590 <nav aria-label="g1t" className={PANEL}>
591 <div className="mt-3 space-y-px">
592 {VISITOR_LINKS.map((link) => (
593 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
594 {link.label}
595 </SidebarLink>
596 ))}
597 </div>
598 </nav>
599 );
600 }
601 return (
602 <nav aria-label="g1t" className={PANEL}>
603 <div className="mt-3 space-y-px">
604 <SidebarLink to="/" end icon={<House size={15} />}>
605 Mission control
606 </SidebarLink>
607 {ws && (
608 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutGrid size={15} />}>
609 Overview
610 </SidebarLink>
611 )}
612 <SidebarLink to="/explore" icon={<Compass size={15} />}>
613 Explore
614 </SidebarLink>
615 </div>
616
617 <Rule />
618 <SidebarGroup
619 title="Projects"
620 action={
621 ws && (
622 <Link
623 to={`/new?workspace=${ws.slug}`}
624 aria-label="New project"
625 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
626 >
627 <Plus size={13} />
628 </Link>
629 )
630 }
A catalogue of model providers, and settings that feel like settings631 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look632 {listed.length === 0 && <p className="px-2 py-1 text-xs text-faint">None yet.</p>}
633 {listed.map((repo) => (
634 <SidebarLink
635 key={`${repo.namespace}/${repo.name}`}
636 to={`/${repo.namespace}/${repo.name}`}
637 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
638 drill="hover"
639 >
640 {repo.namespace !== ws?.slug && <span className="font-mono text-faint">{repo.namespace}/</span>}
641 {"title" in repo && repo.title ? repo.title : repo.name}
642 </SidebarLink>
643 ))}
644 </SidebarGroup>
645 {(shell.shared ?? []).length > 0 && (
646 <SidebarGroup title="Shared with you" className="mt-3">
647 {(shell.shared ?? []).map((repo) => (
648 <SidebarLink
649 key={`${repo.namespace}/${repo.name}`}
650 to={`/${repo.namespace}/${repo.name}`}
651 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
652 drill="hover"
653 >
654 <span className="font-mono text-faint">{repo.namespace}/</span>
655 {repo.name}
656 </SidebarLink>
657 ))}
658 </SidebarGroup>
659 )}
660
661 {ws && (
662 <>
663 <Rule />
664 <div className="space-y-px">
665 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
666 Agent fleet
667 </SidebarLink>
668 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
669 Context
670 </SidebarLink>
671 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
672 Memory
673 </SidebarLink>
674 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
675 Security
676 </SidebarLink>
677 <SidebarLink to={`/${ws.slug}/-/guardrails`} icon={<Gauge size={15} />}>
678 Guardrails
679 </SidebarLink>
680 <SidebarLink to={`/${ws.slug}/-/secrets`} icon={<Lock size={15} />}>
681 Secrets and variables
682 </SidebarLink>
683 <SidebarLink to={`/${ws.slug}/-/integrations`} icon={<Plug size={15} />}>
684 Integrations
685 </SidebarLink>
686 <SidebarLink to={`/${ws.slug}/-/webhooks`} icon={<Webhook size={15} />}>
687 Webhooks
688 </SidebarLink>
689 {roadmapIn("Workspace").map((item) => (
690 <SidebarSoonLink
691 key={item.key}
692 to={`/${ws.slug}/-/soon/${item.key}`}
693 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
694 about={item.summary}
695 >
696 {item.title === "Board" ? "Boards" : item.title}
697 </SidebarSoonLink>
698 ))}
699 </div>
700
701 <Rule />
702 <div className="space-y-px">
703 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
704 Usage
705 </SidebarLink>
706 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
707 Support
708 </SidebarLink>
709 {/* Who belongs, what it pays and its record: a list of their own. */}
710 <SidebarLink
711 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/people`}
712 icon={<Settings size={15} />}
713 drill
714 >
715 Settings
716 </SidebarLink>
717 </div>
718 </>
719 )}
720 </nav>
721 );
722}
723
724/** A workspace's settings, drilled into from Settings in the main list. */
725function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
726 return (
727 <nav aria-label="Workspace settings" className={PANEL}>
728 <BackRow to={`/${slug}`} label="Settings" context={slug} />
729 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings730 {owner && (
731 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
732 General
733 </SidebarLink>
734 )}
735 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />}>
736 Members
737 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look738 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
739 Repositories
740 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments741 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
742 Access tokens
743 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look744 </div>
745 <Rule />
746 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings747 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments748 Billing and plans
A catalogue of model providers, and settings that feel like settings749 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API750 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
751 Audit log
752 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look753 </div>
A catalogue of model providers, and settings that feel like settings754 </nav>
755 );
756}
757
Sidebar: no settings flash on the way into a repository, and a better slide758/**
759 * What the repository menu needs. Known from the address before the page's
760 * data arrives, so the menu is right from the first frame; the counts fill
761 * in once it does.
762 */
763type MenuRepo = {
764 namespace: string;
765 name: string;
766 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look767 /** Whether they see its settings; from the repository's page once it loads. */
768 settings?: boolean;
769 /** What they may do there, once the repository's page loads. */
770 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide771 issues?: number;
772 pulls?: number;
773};
774
775function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
776 return (
777 a != null &&
778 b != null &&
779 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
780 a.name.toLowerCase() === b.name.toLowerCase()
781 );
782}
A repository has its own sidebar, as settings do783
784/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 * A project's own list, drilled into while you are in it: everything about
786 * the project, running and its code, and nothing else, with the way back
787 * out to all of them.
A repository has its own sidebar, as settings do788 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put789function RepoMenu({
790 repo,
791 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look792 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put793}: {
794 repo: MenuRepo;
795 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look796 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
797 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put798}) {
A repository has its own sidebar, as settings do799 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 put800 // 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 look801 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do802 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look803 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
804 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner805 <NavLink
A repository has its own sidebar, as settings do806 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner807 end
808 title="Overview"
809 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look810 `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 owner811 }
A repository has its own sidebar, as settings do812 >
813 <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 page814 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do815 </span>
816 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
817 <span className="text-faint">{repo.namespace}/</span>
818 <span className="font-semibold text-fg">{repo.name}</span>
819 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner820 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look821 {/* A project's pages, in the order people use them. A page with more
822 than one view shows them as tabs across its top. */}
823 <Rule />
824 <div className="space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner825 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
826 Code
827 </SidebarLink>
828 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
829 Issues
830 </SidebarLink>
831 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
832 Pull requests
833 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains834 <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 owner835 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains836 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner837 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
838 Workflows
Projects: what a workspace builds and runs, first on every page839 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look840 </div>
841 <Rule />
842 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put843 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner844 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
845 Deployments
846 </SidebarLink>
847 ) : null}
Observability gets its own page; tabs never overflow848 <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.">
849 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner850 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put851 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API852 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
853 Security
854 </SidebarLink>
855 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner856 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
857 Insights
858 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look859 </div>
860 {(repo.settings ?? shows.has("settings")) && (
861 <>
862 <Rule />
863 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner864 Settings
865 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look866 </>
867 )}
A repository has its own sidebar, as settings do868 </nav>
869 );
870}
871
Project settings move into the sidebar, grouped, with a heading per page872/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look873 * A project's settings, drilled into from its list: what is about running
874 * it, then its agents, then its code and who can reach it. The way back
875 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page876 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look877function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page878 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look879 // Each page shows to the roles that can use it, once the role is known.
880 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
881 const running = shows("deployments");
882 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page883 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look884 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
885 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
886 {shows("") && (
887 <div className="mt-2 space-y-px">
888 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
889 General
890 </SidebarLink>
891 </div>
892 )}
893 {running && (
894 <>
895 {shows("") && <Rule />}
896 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
897 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
898 Deployments
899 </SidebarLink>
900 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
901 Domains
902 </SidebarLink>
903 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
904 Dependencies
905 </SidebarLink>
906 </div>
907 </>
908 )}
909 {agents && (
910 <>
911 <Rule />
912 <div className="space-y-px">
913 {shows("agents") && (
914 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
915 Agents
916 </SidebarLink>
917 )}
918 {shows("guardrails") && (
919 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
920 Guardrails
921 </SidebarLink>
922 )}
923 </div>
924 </>
925 )}
926 <Rule />
927 <div className="space-y-px">
928 {shows("repository") && (
929 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
930 Repository
931 </SidebarLink>
932 )}
933 {shows("access") && (
934 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
935 Access
936 </SidebarLink>
937 )}
938 {shows("branches") && (
939 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
940 Branches and merging
941 </SidebarLink>
942 )}
943 {shows("secrets") && (
944 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
945 Secrets and variables
946 </SidebarLink>
947 )}
948 {shows("webhooks") && (
949 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
950 Webhooks
951 </SidebarLink>
952 )}
953 </div>
Project settings move into the sidebar, grouped, with a heading per page954 </nav>
955 );
956}
957
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas958/** Your own settings, drilled into from Your settings: one page each. */
959function AccountSettingsMenu({ username }: { username: string }) {
960 const link = (page: AccountSettingsPage, icon: ReactNode) => (
961 <SidebarLink to={`/settings/${page}`} icon={icon}>
962 {ACCOUNT_SETTINGS[page].title}
963 </SidebarLink>
Account settings in the same shape as a workspace's964 );
965 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look966 <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 quotas967 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look968 <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 quotas969 {link("profile", <CircleUserRound size={15} />)}
970 {link("emails", <Mail size={15} />)}
971 {link("invites", <Ticket size={15} />)}
972 </div>
973 <Rule />
974 <div className="space-y-px">
975 {link("keys", <Fingerprint size={15} />)}
976 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look977 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas978 <Rule />
979 <div className="space-y-px">
980 {link("github", <GithubMark className="size-[15px]" />)}
981 {link("applications", <Plug size={15} />)}
982 </div>
983 <Rule />
984 <div className="space-y-px">
985 {link("security-log", <History size={15} />)}
986 </div>
Account settings in the same shape as a workspace's987 </nav>
988 );
989}
990
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put991/**
992 * Signing in and signing up, in place of the account for a visitor. Signing
993 * in brings them back to the page they are on.
994 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas995/** Help, status and the fine print under a visitor's sign-in buttons. */
996function VisitorLinks() {
997 const status = useSiteStatus();
998 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
999 const dot = (
1000 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1001 ·
1002 </span>
1003 );
1004 return (
1005 // Two short rows, so a narrow sidebar never starts a line with a dot.
1006 <nav aria-label="About g1t" className="pt-0.5">
1007 <div className="flex items-center gap-0.5">
1008 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1009 <StatusDot state={status?.overall.state ?? null} />
1010 Status
1011 </a>
1012 {dot}
1013 <Link to="/support" className={quiet}>
1014 Support
1015 </Link>
1016 </div>
1017 <div className="flex items-center gap-0.5">
1018 {LEGAL_LINKS.map(([label, to], index) => (
1019 <span key={to} className="flex items-center gap-0.5">
1020 {index > 0 && dot}
1021 <Link to={to} className={quiet}>
1022 {label}
1023 </Link>
1024 </span>
1025 ))}
1026 </div>
1027 </nav>
1028 );
1029}
1030
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1031function VisitorPanel() {
1032 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1033 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1034 return (
1035 <div className="space-y-2">
1036 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1037 <div className="grid grid-cols-2 gap-2">
1038 <Link
1039 to={withNext("/login", pathname + search)}
1040 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"
1041 >
1042 <LogIn size={14} />
1043 Sign in
1044 </Link>
1045 <Link
1046 to={withNext("/register", pathname + search)}
1047 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1048 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1049 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1050 </Link>
1051 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1052 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1053 </div>
1054 );
1055}
1056
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1057function Sidebar({
1058 user,
1059 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1060 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1061 onFind,
1062 onClose,
1063}: {
1064 user: User | null;
1065 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1066 /** The page is a 404: the address names nothing the viewer can see. */
1067 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1068 onFind: () => void;
1069 /** In the sheet on a small screen: closing it, at the end of the top row. */
1070 onClose?: () => void;
1071}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1072 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1073 const { pathname } = useLocation();
1074 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1075 // Where the sidebar is drilled to follows the page being gone to, so it
1076 // moves as the link is followed, not once the page arrives.
1077 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1078 // On a 404, the address is not a place: no list is drilled into from it,
1079 // except the project's own when the project is real and visible and only
1080 // something inside it is missing (a file, a commit, an issue).
1081 const lost = missing && going == null;
1082 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1083 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1084 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1085 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1086 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1087 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", ".well-known"]);
1088 // An invitation to a repository is answered before its menu means anything.
1089 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1090 repoPath != null &&
1091 !reserved.has(repoPath[1]) &&
1092 repoPath[2] !== "-" &&
1093 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1094 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1095 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1096 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1097 // The repository the menu is for: the one loaded if it is the one being
1098 // gone to, else what the address says, at once.
1099 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 look1100 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1101 ? sameRepo(active, targetRepo)
1102 ? active
1103 : {
1104 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1105 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1106 }
1107 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1108 // The viewer's role there, from the repository's page once it has loaded.
1109 const page = useRouteLoaderData("routes/repo/layout") as
1110 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1111 | undefined;
1112 const menuRepo: MenuRepo | null =
1113 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1114 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1115 : guessed;
1116
1117 // The way from the main list to the one shown.
1118 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1119 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1120 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1121 } else if (ws && inSettings) {
1122 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1123 } else if (menuRepo) {
1124 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1125 // Out of a project: to its workspace's projects when they are yours.
1126 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1127 const back = !user
1128 ? { to: "/explore", label: "Explore" }
1129 : home
1130 ? { to: `/${home.slug}`, label: "All projects" }
1131 : { to: "/", label: "Mission control" };
1132 trail.push({
1133 key,
1134 node: (
1135 <RepoMenu
1136 repo={menuRepo}
1137 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1138 back={back}
1139 />
1140 ),
1141 });
1142 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1143 }
1144
Agents as a team: lifecycle, merge queue, billing and a new shell1145 return (
1146 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1147 {/* The same height and rule as the top bar, so the two read as one line. */}
Workspace names and icons, and a component kit for every control1148 <div className="flex h-16 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 put1149 {user ? (
1150 <>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1151 <Link to="/" aria-label="g1t home" className="flex h-9 shrink-0 items-center rounded-md px-2 transition-colors hover:bg-raised">
1152 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1153 </Link>
1154 <span className="shrink-0 text-line-strong" aria-hidden="true">
1155 /
1156 </span>
1157 <WorkspaceSwitcher user={user} shell={shell} />
1158 </>
1159 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1160 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1161 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1162 </Link>
1163 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1164 {onClose && (
1165 <button
1166 type="button"
1167 aria-label="Close menu"
1168 onClick={onClose}
1169 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1170 >
1171 <X size={16} />
1172 </button>
1173 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1174 </div>
A Vercel-like sidebar and a new mark1175 <div className="px-2 pt-3">
1176 <button
1177 type="button"
1178 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1179 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 mark1180 >
1181 <Search size={14} />
1182 <span className="grow text-left">Find…</span>
1183 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1184 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1185 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1186 <Drill trail={trail} />
1187 {/* The account, or signing in, as one row at the very bottom. */}
1188 <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 put1189 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1190 </div>
1191 </div>
1192 );
1193}
1194
1195/** Words for the sections a path can end in. */
1196const SECTIONS: Record<string, string> = {
1197 issues: "Issues",
1198 pulls: "Pull requests",
1199 queue: "Merge queue",
1200 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1201 plans: "Outcomes",
1202 actions: "Workflows",
1203 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1204 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1205 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1206 access: "Access",
1207 invitations: "Invitation",
1208 repositories: "Repositories",
1209 branches: "Branches and merging",
Project dependencies: addresses, preview stacks, Affects, and agents who know1210 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1211 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1212 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1213 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1214 people: "Members",
A Vercel-like sidebar and a new mark1215 tokens: "Access tokens",
1216 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1217 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1218 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1219 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1220 domains: "Domains",
1221 guardrails: "Guardrails",
1222 audit: "Audit log",
A Vercel-like sidebar and a new mark1223 tree: "Code",
1224 blob: "Code",
1225};
1226
1227/** 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 quotas1228function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1229 // A 404 names nothing from the address, unless the project is real and
1230 // visible and only something inside it is missing.
1231 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1232 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1233 return <span className="text-sm font-medium">Not found</span>;
1234 }
A Vercel-like sidebar and a new mark1235 const parts = pathname.split("/").filter(Boolean);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1236 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite"];
A Vercel-like sidebar and a new mark1237 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 quotas1238 // Your settings: Settings / Emails.
1239 if (parts[0] === "settings") {
1240 const page = accountSettingsPage(pathname);
1241 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1242 return (
1243 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1244 {trail.map((crumb, index) => (
1245 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1246 {index > 0 && <span className="text-line-strong">/</span>}
1247 <Link
1248 to={crumb.to}
1249 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1250 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1251 }`}
1252 >
1253 {crumb.label}
1254 </Link>
1255 </span>
1256 ))}
1257 </nav>
1258 );
1259 }
A Vercel-like sidebar and a new mark1260 if (reserved.includes(parts[0]!)) {
1261 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1262 settings: "Account",
A Vercel-like sidebar and a new mark1263 explore: "Explore",
Projects live: fixes from walking them end to end1264 new: "New project",
A Vercel-like sidebar and a new mark1265 search: "Search",
1266 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1267 policies: "Policies",
1268 security: "Security",
1269 support: "Support",
1270 status: "Status",
1271 invite: "Invite",
A Vercel-like sidebar and a new mark1272 };
1273 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1274 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1275 // A person's profile, by their handle.
1276 if (parts[0] === "u" && parts[1]) {
1277 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1278 }
A Vercel-like sidebar and a new mark1279 const [owner, second, third, fourth] = parts;
1280 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1281 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1282 const page = `/${owner}/-/${third}`;
1283 if (third && SETTINGS_PAGE.test(page)) {
1284 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1285 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1286 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1287 } else if (second) {
1288 const repo = `/${owner}/${second}`;
1289 trail.push({ label: second, to: repo, mono: true });
1290 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1291 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1292 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 deployments1293 else if (third && SECTIONS[third]) {
1294 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1295 // A settings page names which one: Settings / Secrets and variables.
1296 if (third === "settings" && fourth && SECTIONS[fourth]) {
1297 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
1298 }
1299 }
A Vercel-like sidebar and a new mark1300 }
1301 return (
1302 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1303 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1304 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1305 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1306 <Link
A Vercel-like sidebar and a new mark1307 to={crumb.to}
1308 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1309 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1310 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1311 >
A Vercel-like sidebar and a new mark1312 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1313 </Link>
A Vercel-like sidebar and a new mark1314 </span>
1315 ))}
1316 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1317 );
1318}
1319
Search across all of g1t, Explore, and a command palette1320type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1321
1322/** 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 look1323function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1324 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1325 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1326 { label: "Mission control", to: "/", icon: <House size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1327 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1328 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page1329 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1330 { 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 quotas1331 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1332 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1333 .filter((page) => page !== "profile")
1334 .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 shell1335 ];
1336 const repo = shell.repo;
1337 if (repo) {
1338 const base = `/${repo.namespace}/${repo.name}`;
1339 const name = `${repo.namespace}/${repo.name}`;
1340 commands.unshift(
Projects: what a workspace builds and runs, first on every page1341 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1342 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1343 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1344 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
1345 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1346 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1347 ...(repo.member
1348 ? [
1349 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1350 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1351 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1352 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1353 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1354 ]
1355 : []),
1356 );
1357 }
1358 for (const membership of user.workspaces ?? []) {
1359 commands.push(
Workspace names and icons, and a component kit for every control1360 { label: displayName(membership), hint: `Workspace · ${membership.slug}`, to: `/${membership.slug}`, icon: <Avatar name={membership.slug} image={membership.avatar} size={15} square /> },
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1361 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1362 { 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 settings1363 { 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 look1364 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1365 );
1366 }
1367 for (const listed of shell.repos) {
1368 commands.push({
Projects: what a workspace builds and runs, first on every page1369 label: listed.title ?? listed.name,
1370 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1371 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1372 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1373 });
1374 }
1375 return commands;
1376}
1377
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1378/** 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 look1379function 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 put1380 const commands: Command[] = [];
1381 const repo = shell.repo;
1382 if (repo) {
1383 const base = `/${repo.namespace}/${repo.name}`;
1384 const name = `${repo.namespace}/${repo.name}`;
1385 commands.push(
1386 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1387 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1388 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1389 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1390 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1391 );
1392 }
1393 commands.push(
1394 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1395 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1396 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1397 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1398 { 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 look1399 { 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 put1400 );
1401 return commands;
1402}
1403
Agents as a team: lifecycle, merge queue, billing and a new shell1404/**
1405 * A bar across the top of the page while the next one loads, as GitHub
1406 * has: it appears at once, creeps towards the end while waiting, then fills
1407 * and fades when the page arrives.
1408 */
1409export function Progress() {
1410 const navigation = useNavigation();
1411 const busy = navigation.state !== "idle";
1412 const [width, setWidth] = useState(0);
1413 const [visible, setVisible] = useState(false);
1414 useEffect(() => {
1415 if (busy) {
1416 setVisible(true);
1417 setWidth(12);
1418 // Slows as it goes, never quite arriving until the page does.
1419 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1420 return () => clearInterval(timer);
1421 }
1422 setWidth((now) => (now > 0 ? 100 : 0));
1423 const fade = setTimeout(() => setVisible(false), 250);
1424 const reset = setTimeout(() => setWidth(0), 550);
1425 return () => {
1426 clearTimeout(fade);
1427 clearTimeout(reset);
1428 };
1429 }, [busy]);
1430 return (
1431 <div
1432 aria-hidden="true"
1433 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1434 visible ? "opacity-100" : "opacity-0"
1435 }`}
1436 >
1437 <div
1438 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1439 style={{ width: `${width}%` }}
1440 />
1441 </div>
1442 );
1443}
1444
1445/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1446 * The app: a sidebar with the workspace, its repositories and the sections
1447 * of the one being looked at; a slim bar with search and the account; and
1448 * the page. A visitor who is not signed in gets the same frame, with
1449 * Explore and Search in place of the workspace, and signing in in place of
1450 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1451 */
1452export function AppShell({
1453 user,
1454 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1455 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1456 banner,
1457 children,
1458}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1459 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1460 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1461 /** The page is a 404 (see Sidebar). */
1462 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1463 banner?: ReactNode;
1464 children: ReactNode;
1465}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1466 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1467 const [drawer, setDrawer] = useState(false);
1468 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1469 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1470 const signUpLabel = useSignUpCopy().primary;
1471 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1472
1473 // A new page closes the drawer on small screens.
1474 useEffect(() => setDrawer(false), [pathname]);
Search across all of g1t, Explore, and a command palette1475 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1476
1477 return (
1478 <div className="min-h-screen">
1479 <Progress />
1480 <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 quotas1481 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1482 </aside>
1483 {drawer && (
1484 <div className="fixed inset-0 z-50 lg:hidden">
1485 <button
1486 type="button"
1487 aria-label="Close menu"
1488 className="absolute inset-0 bg-black/60"
1489 onClick={() => setDrawer(false)}
1490 />
1491 <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 quotas1492 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1493 </aside>
1494 </div>
1495 )}
1496
Polish: phones, copy boxes, the plan page, the landing page, a real glide1497 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals1498 <header className="sticky top-0 z-30 flex h-16 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 shell1499 <button
1500 type="button"
1501 aria-label="Open menu"
1502 onClick={() => setDrawer(true)}
1503 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1504 >
1505 <Menu size={18} />
1506 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1507 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1508 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1509 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1510 <input
1511 name="q"
1512 {...notACredential()}
1513 placeholder="Search g1t"
1514 aria-label="Search g1t"
1515 className="h-9 w-full rounded-md border border-line bg-surface pr-12 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
1516 />
1517 <kbd className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">
1518 ⌘K
1519 </kbd>
1520 </Form>
1521 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1522 <a
1523 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1524 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 mark1525 >
1526 Docs
1527 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1528 {!user ? (
1529 // The sidebar has these too, but on a phone it is folded away.
1530 <Link
1531 to={withNext("/login", here)}
1532 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"
1533 >
1534 Sign in
1535 </Link>
1536 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1537 <DropdownMenu>
1538 <DropdownMenuTrigger
1539 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1540 className="flex h-9 items-center gap-1.5 rounded-md bg-fg px-3 text-sm font-medium text-bg outline-none transition-colors hover:bg-white"
Agents as a team: lifecycle, merge queue, billing and a new shell1541 >
A Vercel-like sidebar and a new mark1542 <Plus size={14} />
1543 New
Agents as a team: lifecycle, merge queue, billing and a new shell1544 </DropdownMenuTrigger>
1545 <DropdownMenuContent align="end">
1546 {shell.repo && (
1547 <DropdownMenuItem asChild>
1548 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1549 <CircleDot />
1550 New issue
1551 </Link>
1552 </DropdownMenuItem>
1553 )}
1554 {shell.repo?.member && (
1555 <DropdownMenuItem asChild>
1556 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1557 <ListTree />
1558 Plan work
1559 </Link>
1560 </DropdownMenuItem>
1561 )}
1562 <DropdownMenuItem asChild>
1563 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1564 <Box />
1565 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1566 </Link>
1567 </DropdownMenuItem>
1568 <DropdownMenuItem asChild>
1569 <Link to="/workspaces/new">
1570 <Users />
1571 New workspace
1572 </Link>
1573 </DropdownMenuItem>
1574 </DropdownMenuContent>
1575 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1576 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1577 </div>
1578 </header>
1579 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1580 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1581 </div>
Search across all of g1t, Explore, and a command palette1582 <CommandPalette
1583 open={palette}
1584 onOpenChange={setPalette}
1585 commands={commands}
1586 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1587 />
Agents as a team: lifecycle, merge queue, billing and a new shell1588 </div>
1589 );
1590}