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

1,649 lines64,960 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,
Fast pages, required checks on the branch, self-hosted runners, honest incidents3 ServerCog,
A flat project menu with Code first, views as tabs, and Stripe for every owner4 GanttChart,
5 KanbanSquare,
6 Package,
7 Sparkles,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request8 BarChart3,
Agents as a team: lifecycle, merge queue, billing and a new shell9 BookMarked,
10 BookOpen,
11 Check,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12 ChevronLeft,
13 ChevronRight,
Agents as a team: lifecycle, merge queue, billing and a new shell14 ChevronsUpDown,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas15 CircleUserRound,
16 Mail,
17 Ticket,
Agents as a team: lifecycle, merge queue, billing and a new shell18 CircleDot,
19 Code2,
20 Compass,
21 CreditCard,
22 GitPullRequest,
23 History,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look24 House,
Secrets and variables: one list, rows per environment, for workflows and deployments25 Fingerprint,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26 Gauge,
Agents as a team: lifecycle, merge queue, billing and a new shell27 KeyRound,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28 LifeBuoy,
Projects: what a workspace builds and runs, first on every page29 Box,
30 LayoutGrid,
Agents as a team: lifecycle, merge queue, billing and a new shell31 ListTree,
32 Lock,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put33 LogIn,
Agents as a team: lifecycle, merge queue, billing and a new shell34 LogOut,
35 Menu,
36 Plus,
37 Search,
Integrations: your own model provider, alerts that open issues, tickets agents read38 Plug,
Agents as a team: lifecycle, merge queue, billing and a new shell39 Settings,
40 Users,
Webhooks: every event, to your own addresses, signed and retried41 Webhook,
Project settings move into the sidebar, grouped, with a heading per page42 Globe,
43 GitBranch,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs44 PlayCircle,
Pull requests say where they are live; the project menu shows what is coming45 Bot,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains46 Brain,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API47 Network,
Pull requests say where they are live; the project menu shows what is coming48 ShieldCheck,
Deployments: a preview for every pull request, production on g1t.page49 Rocket,
Agents as a team: lifecycle, merge queue, billing and a new shell50 X,
51} from "lucide-react";
52import { 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 quotas53import { Form, Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell54
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look55import type { Abilities, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell56
Search across all of g1t, Explore, and a command palette57import { 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 quotas58import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height59import { Logo, Mark } from "./logo";
Search across all of g1t, Explore, and a command palette60import { Avatar, notACredential } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents61import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell62import {
63 DropdownMenu,
64 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas65 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell66 DropdownMenuItem,
67 DropdownMenuLabel,
68 DropdownMenuSeparator,
69 DropdownMenuTrigger,
70} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales71import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72import { 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 put73import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas74import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
75import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put76import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look77import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas78import { STATUS_URL, statusTitle } from "../lib/status";
79import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell80
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put81/**
82 * What the sidebar needs, worked out by the root loader. For a visitor who
83 * is not signed in there is no workspace, no projects and no usage: only
84 * the project being looked at, if they can see it.
85 */
Agents as a team: lifecycle, merge queue, billing and a new shell86export type ShellData = {
87 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control88 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page89 /** Its projects, by name: `name` is the slug in their address. */
90 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91 /** Repositories shared with them in workspaces they do not belong to. */
92 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page93 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell94 repo: {
95 namespace: string;
96 name: string;
97 member: boolean;
98 issues: number;
99 pulls: number;
100 } | null;
Billing accounts, terms and enterprises; g1t is no longer free101 /** Where the workspace stands against its usage limit, if billing is on. */
102 limit: {
103 exposureMicros: number;
104 ceilingMicros: number | null;
105 state: "ok" | "warning" | "stopped";
106 comped: boolean;
107 } | null;
Free while g1t is being built out; agents can check out their own forks108 /** Whether g1t charges nothing for now, while it is being built out. */
109 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request110 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy111 /** This month's usage: charged, or at cost while g1t is free. */
112 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look113 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
114 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Agents as a team: lifecycle, merge queue, billing and a new shell115};
116
117function SidebarLink({
118 to,
119 icon,
120 end,
121 count,
122 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell124 children,
125}: {
126 to: string;
127 icon: ReactNode;
128 end?: boolean;
129 count?: number;
Projects: what a workspace builds and runs, first on every page130 /** Other path prefixes under which this link is the current one. */
131 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look132 /** It opens a list of its own: a chevron says so, always or on hover. */
133 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell134 children: ReactNode;
135}) {
136 const { pathname } = useLocation();
137 return (
138 <NavLink
139 to={to}
140 end={end}
141 prefetch="intent"
142 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page143 const current =
144 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark145 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 shell146 current
147 ? "bg-raised font-medium text-fg"
148 : isPending
149 ? "bg-raised/60 text-fg"
150 : "text-muted hover:bg-raised/60 hover:text-fg"
151 }`;
152 }}
153 >
154 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
155 <span className="min-w-0 grow truncate">{children}</span>
156 {count != null && count > 0 && (
157 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
158 {count}
159 </span>
160 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 {drill && (
162 <ChevronRight
163 size={14}
164 aria-hidden="true"
165 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
166 />
167 )}
Agents as a team: lifecycle, merge queue, billing and a new shell168 </NavLink>
169 );
170}
171
Pull requests say where they are live; the project menu shows what is coming172/**
173 * Something a project will have, shown where it will live so people can
174 * see where g1t is going. Not a link: nothing is behind it yet.
175 */
176function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
177 return (
178 <div
179 title={about}
180 aria-disabled="true"
181 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
182 >
183 <span className="shrink-0 opacity-70">{icon}</span>
184 <span className="grow truncate">{children}</span>
185 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
186 Soon
187 </span>
188 </div>
189 );
190}
191
The project menu opens by section, and the workspace has its own panel192/**
A flat project menu with Code first, views as tabs, and Stripe for every owner193 * A page that is coming, as a link to its roadmap page: faint, with Soon,
194 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel195 */
A flat project menu with Code first, views as tabs, and Stripe for every owner196function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel197 to,
198 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner199 icon,
200 about,
The project menu opens by section, and the workspace has its own panel201 children,
202}: {
203 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner204 also?: string[];
205 icon: ReactNode;
206 about: string;
The project menu opens by section, and the workspace has its own panel207 children: ReactNode;
208}) {
209 const { pathname } = useLocation();
210 return (
211 <NavLink
212 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner213 title={about}
The project menu opens by section, and the workspace has its own panel214 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner215 className={({ isActive }) => {
216 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
217 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
218 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 panel219 }`;
220 }}
221 >
A flat project menu with Code first, views as tabs, and Stripe for every owner222 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel223 <span className="min-w-0 grow truncate">{children}</span>
224 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
225 Soon
226 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales227 </NavLink>
The project menu opens by section, and the workspace has its own panel228 );
229}
230
A flat project menu with Code first, views as tabs, and Stripe for every owner231/** Icons for what spans projects. */
232const WORKSPACE_ICONS: Record<string, ReactNode> = {
233 board: <KanbanSquare size={15} />,
234 roadmap: <GanttChart size={15} />,
235 packages: <Package size={15} />,
236 fleet: <Bot size={15} />,
237};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales238
A flat project menu with Code first, views as tabs, and Stripe for every owner239/** The Soon pages of a section, as paths, so its link is current on them. */
240function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
241 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales242}
243
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look244function SidebarGroup({
245 title,
246 action,
247 className = "",
248 children,
249}: {
250 title: string;
251 action?: ReactNode;
252 className?: string;
253 children: ReactNode;
254}) {
Agents as a team: lifecycle, merge queue, billing and a new shell255 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look256 <section className={className}>
A Vercel-like sidebar and a new mark257 <div className="mb-1 flex h-6 items-center justify-between px-2">
258 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell259 {action}
260 </div>
261 <div className="space-y-px">{children}</div>
262 </section>
263 );
264}
265
Workspace names and icons, and a component kit for every control266/** What a workspace is called where people read it: its display name, else its slug. */
267function displayName(membership: Membership): string {
268 return membership.name?.trim() || membership.slug;
269}
270
Agents as a team: lifecycle, merge queue, billing and a new shell271function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control272 const workspace = shell.workspace;
273 const current = workspace?.slug;
274 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell275 return (
276 <DropdownMenu>
Workspace names and icons, and a component kit for every control277 {/* The name goes to the workspace; only the arrows switch it. The
278 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark279 <Link
280 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents281 prefetch="intent"
Workspace names and icons, and a component kit for every control282 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look283 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 mark284 >
Workspace names and icons, and a component kit for every control285 {workspace ? (
286 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell287 ) : (
Workspace names and icons, and a component kit for every control288 <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 shell289 )}
Workspace names and icons, and a component kit for every control290 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark291 </Link>
292 <DropdownMenuTrigger
293 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look294 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 mark295 >
296 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell297 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control298 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell299 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
300 {(user.workspaces ?? []).map((membership) => (
301 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control302 <Link to={`/${membership.slug}`} title={displayName(membership)}>
303 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
304 <span className="flex min-w-0 grow flex-col leading-tight">
305 <span className="truncate">{displayName(membership)}</span>
306 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
307 </span>
308 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
309 {membership.slug === current ? (
310 <Check className="shrink-0 text-accent" />
311 ) : (
312 <span className="size-4 shrink-0" aria-hidden="true" />
313 )}
Agents as a team: lifecycle, merge queue, billing and a new shell314 </Link>
315 </DropdownMenuItem>
316 ))}
317 <DropdownMenuSeparator />
318 <DropdownMenuItem asChild>
319 <Link to="/workspaces/new">
320 <Plus />
321 New workspace
322 </Link>
323 </DropdownMenuItem>
324 </DropdownMenuContent>
325 </DropdownMenu>
A Vercel-like sidebar and a new mark326 );
327}
328
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas329/**
330 * The fine print, as one quiet row at the foot of the account menu and the
331 * visitor's panel: each link a full-height target, left-aligned. In the
332 * menu each is a menu item, so the arrow keys reach it.
333 */
334const LEGAL_LINKS: [string, string][] = [
335 ["Policies", "/policies"],
336 ["Privacy", "/policies/privacy"],
337 ["Security", "/security"],
338];
339const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
340
341function MenuLegalRow() {
342 return (
343 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
344 {LEGAL_LINKS.map(([label, to], index) => (
345 <span key={to} className="flex items-center gap-0.5">
346 {index > 0 && (
347 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
348 ·
349 </span>
350 )}
351 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
352 <Link to={to}>{label}</Link>
353 </DropdownMenuItem>
354 </span>
355 ))}
356 </div>
357 );
358}
359
360/** Status's row: the live dot and two or three words, fetched when the menu opens. */
361function StatusSummary({ open }: { open: boolean }) {
362 const status = useSiteStatus(open);
363 const words = statusTitle(status);
364 return (
365 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
366 {/* The words say it; the dot's own words for screen readers would repeat them. */}
367 <span aria-hidden="true" className="flex">
368 <StatusDot state={status?.overall.state ?? null} />
369 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents370 {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 quotas371 </span>
372 );
373}
374
375/** A menu row: an icon, words, and whatever sits at its end. */
376const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
377
A Vercel-like sidebar and a new mark378function AccountMenu({ user }: { user: User }) {
379 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas380 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents381 // Name, primary address and invites left: asked for once, as soon as the
382 // 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 quotas383 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents384 const prefetch = () => {
385 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
386 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas387 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents388 if (open) prefetch();
389 // eslint-disable-next-line react-hooks/exhaustive-deps
390 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas391 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents392 // Still on its way: shapes where the words will be, the same size.
393 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 quotas394 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark395 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas396 <DropdownMenu open={open} onOpenChange={setOpen}>
397 <DropdownMenuTrigger
398 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents399 onPointerEnter={prefetch}
400 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas401 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"
402 >
Workspace names and icons, and a component kit for every control403 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark404 <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 quotas405 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark406 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas407 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
408 {/* Who is signed in, and a way to their profile. */}
409 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
410 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
411 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents412 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
413 {loading ? (
414 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
415 ) : (
416 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
417 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas418 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents419 {loading ? (
420 <Skeleton className="mt-1 h-3 w-40" />
421 ) : (
422 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
423 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas424 </span>
A Vercel-like sidebar and a new mark425 </Link>
426 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas427 <DropdownMenuSeparator className="my-1.5" />
428 <DropdownMenuGroup>
429 <DropdownMenuItem asChild className={MENU_ROW}>
430 <Link to={profile}>
431 <CircleUserRound />
432 Your profile
433 </Link>
434 </DropdownMenuItem>
435 <DropdownMenuItem asChild className={MENU_ROW}>
436 <Link to={FIRST_SETTINGS_PAGE}>
437 <Settings />
438 Your settings
439 </Link>
440 </DropdownMenuItem>
441 <DropdownMenuItem asChild className={MENU_ROW}>
442 <Link to="/settings/invites">
443 <Ticket />
444 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents445 {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 quotas446 {me?.invites_left != null && (
447 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
448 {me.invites_left} left
449 </span>
450 )}
451 </Link>
452 </DropdownMenuItem>
453 </DropdownMenuGroup>
454 <DropdownMenuSeparator className="my-1.5" />
455 <DropdownMenuGroup>
456 <DropdownMenuItem asChild className={MENU_ROW}>
457 <a href="https://docs.g1t.sh/">
458 <BookOpen />
459 Documentation
460 </a>
461 </DropdownMenuItem>
462 <DropdownMenuItem asChild className={MENU_ROW}>
463 <Link to="/support">
464 <LifeBuoy />
465 Support
466 </Link>
467 </DropdownMenuItem>
468 <DropdownMenuItem asChild className={MENU_ROW}>
469 <a href={STATUS_URL}>
470 <Activity />
471 Status
472 <StatusSummary open={open} />
473 </a>
474 </DropdownMenuItem>
475 </DropdownMenuGroup>
476 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark477 {/* Submitted from here: the menu closes on select, and a button
478 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 quotas479 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark480 <LogOut />
481 Sign out
482 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas483 <DropdownMenuSeparator className="my-1.5" />
484 <MenuLegalRow />
A Vercel-like sidebar and a new mark485 </DropdownMenuContent>
486 </DropdownMenu>
487 );
488}
489
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look490/**
491 * A workspace's settings pages, which the sidebar drills into: who belongs,
492 * what it pays, and its record. What it builds and runs with (secrets,
493 * integrations, webhooks, guardrails) sits in the main list.
494 */
495const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|repositories|tokens|billing|audit)(\/|$)/;
496/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page497const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings498
Polish: phones, copy boxes, the plan page, the landing page, a real glide499/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look500 * The sidebar's lists are a stack, the way a phone pushes a screen: one
501 * drilled into slides in from the right over the full width, while the one
502 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide503 * ease-out, the fade quicker than the move so the two never blur together.
504 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide505 */
Sidebar: no settings flash on the way into a repository, and a better slide506const LAYER =
Sidebar: the panels really slide507 "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 look508/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide509const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings510
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look511/** One list in the stack: what it is, and its rows. */
512type Level = { key: string; node: ReactNode };
513
514/** True once the page has hydrated, so what the server drew never animates in. */
515function useHydrated(): boolean {
516 const [hydrated, setHydrated] = useState(false);
517 useEffect(() => setHydrated(true), []);
518 return hydrated;
519}
520
521/**
522 * Drilling in and out of the sidebar's lists. `trail` is the way from the
523 * main list to the one shown, worked out from the address, so a link
524 * straight to a settings page opens the sidebar already drilled in, and
525 * leaving it comes back out. Lists deeper than the one shown stay in
526 * place, off to the right, so going back slides them away rather than
527 * dropping them. Moving to a list at the same depth (a workspace's settings
528 * to a project) fades the new one in where it is.
529 */
530function Drill({ trail }: { trail: Level[] }) {
531 const depth = trail.length - 1;
532 const hydrated = useHydrated();
533 const stack = useRef<Level[]>(trail);
534 const before = stack.current;
535 const within = trail.every((level, index) => before[index]?.key === level.key);
536 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
537 const levels = stack.current;
538 // What was showing last time, to tell drilling in from moving across.
539 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
540 useEffect(() => {
541 shown.current = { depth, keys: levels.map((level) => level.key) };
542 });
A catalogue of model providers, and settings that feel like settings543 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look544 <div className="relative min-h-0 grow overflow-hidden">
545 {levels.map((level, index) => {
546 const current = index === depth;
547 const place =
548 index < depth
549 ? "pointer-events-none -translate-x-1/4 opacity-0"
550 : current
551 ? "translate-x-0 opacity-100"
552 : "pointer-events-none translate-x-full opacity-0";
553 // A list that was not there before arrives: from the right when
554 // drilling in, in place when moving across.
555 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
556 const enter = arriving
557 ? depth > shown.current.depth
558 ? "starting:translate-x-full starting:opacity-0"
559 : "starting:opacity-0"
560 : "";
561 return (
562 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
563 {level.node}
564 </div>
565 );
566 })}
567 </div>
568 );
569}
570
571/**
572 * The row at the top of a list drilled into: the list's name, which goes
573 * back out, and what it belongs to, faint at the end.
574 */
575function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
576 return (
577 <Link
578 to={to}
579 prefetch="intent"
580 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 settings581 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look582 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
583 <span className="min-w-0 shrink-0 truncate">{label}</span>
584 {context && (
585 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
586 )}
587 </Link>
588 );
589}
590
591/** A thin rule between groups of a list. */
592function Rule() {
593 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
594}
595
596/**
597 * The main list: where you go, the projects, what the workspace builds and
598 * runs with across them, then its usage, support and settings.
599 */
600function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
601 const ws = shell.workspace;
602 const active = shell.repo;
603 // The repository being looked at is listed even when it is someone else's.
604 const listed =
605 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
606 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
607 : shell.repos;
608 // A visitor browses: no workspace, no projects of their own.
609 if (!user) {
610 return (
611 <nav aria-label="g1t" className={PANEL}>
612 <div className="mt-3 space-y-px">
613 {VISITOR_LINKS.map((link) => (
614 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
615 {link.label}
616 </SidebarLink>
617 ))}
618 </div>
619 </nav>
620 );
621 }
622 return (
623 <nav aria-label="g1t" className={PANEL}>
624 <div className="mt-3 space-y-px">
625 <SidebarLink to="/" end icon={<House size={15} />}>
626 Mission control
627 </SidebarLink>
628 {ws && (
629 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutGrid size={15} />}>
630 Overview
631 </SidebarLink>
632 )}
633 <SidebarLink to="/explore" icon={<Compass size={15} />}>
634 Explore
635 </SidebarLink>
636 </div>
637
638 <Rule />
639 <SidebarGroup
640 title="Projects"
641 action={
642 ws && (
643 <Link
644 to={`/new?workspace=${ws.slug}`}
645 aria-label="New project"
646 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
647 >
648 <Plus size={13} />
649 </Link>
650 )
651 }
A catalogue of model providers, and settings that feel like settings652 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look653 {listed.length === 0 && <p className="px-2 py-1 text-xs text-faint">None yet.</p>}
654 {listed.map((repo) => (
655 <SidebarLink
656 key={`${repo.namespace}/${repo.name}`}
657 to={`/${repo.namespace}/${repo.name}`}
658 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
659 drill="hover"
660 >
661 {repo.namespace !== ws?.slug && <span className="font-mono text-faint">{repo.namespace}/</span>}
662 {"title" in repo && repo.title ? repo.title : repo.name}
663 </SidebarLink>
664 ))}
665 </SidebarGroup>
666 {(shell.shared ?? []).length > 0 && (
667 <SidebarGroup title="Shared with you" className="mt-3">
668 {(shell.shared ?? []).map((repo) => (
669 <SidebarLink
670 key={`${repo.namespace}/${repo.name}`}
671 to={`/${repo.namespace}/${repo.name}`}
672 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
673 drill="hover"
674 >
675 <span className="font-mono text-faint">{repo.namespace}/</span>
676 {repo.name}
677 </SidebarLink>
678 ))}
679 </SidebarGroup>
680 )}
681
682 {ws && (
683 <>
684 <Rule />
685 <div className="space-y-px">
686 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
687 Agent fleet
688 </SidebarLink>
689 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
690 Context
691 </SidebarLink>
692 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
693 Memory
694 </SidebarLink>
695 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
696 Security
697 </SidebarLink>
698 <SidebarLink to={`/${ws.slug}/-/guardrails`} icon={<Gauge size={15} />}>
699 Guardrails
700 </SidebarLink>
701 <SidebarLink to={`/${ws.slug}/-/secrets`} icon={<Lock size={15} />}>
702 Secrets and variables
703 </SidebarLink>
Fast pages, required checks on the branch, self-hosted runners, honest incidents704 <SidebarLink to={`/${ws.slug}/-/runners`} icon={<ServerCog size={15} />}>
705 Runners
706 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look707 <SidebarLink to={`/${ws.slug}/-/integrations`} icon={<Plug size={15} />}>
708 Integrations
709 </SidebarLink>
710 <SidebarLink to={`/${ws.slug}/-/webhooks`} icon={<Webhook size={15} />}>
711 Webhooks
712 </SidebarLink>
713 {roadmapIn("Workspace").map((item) => (
714 <SidebarSoonLink
715 key={item.key}
716 to={`/${ws.slug}/-/soon/${item.key}`}
717 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
718 about={item.summary}
719 >
720 {item.title === "Board" ? "Boards" : item.title}
721 </SidebarSoonLink>
722 ))}
723 </div>
724
725 <Rule />
726 <div className="space-y-px">
727 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
728 Usage
729 </SidebarLink>
730 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
731 Support
732 </SidebarLink>
733 {/* Who belongs, what it pays and its record: a list of their own. */}
734 <SidebarLink
735 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/people`}
736 icon={<Settings size={15} />}
737 drill
738 >
739 Settings
740 </SidebarLink>
741 </div>
742 </>
743 )}
744 </nav>
745 );
746}
747
748/** A workspace's settings, drilled into from Settings in the main list. */
749function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
750 return (
751 <nav aria-label="Workspace settings" className={PANEL}>
752 <BackRow to={`/${slug}`} label="Settings" context={slug} />
753 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings754 {owner && (
755 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
756 General
757 </SidebarLink>
758 )}
759 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />}>
760 Members
761 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look762 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
763 Repositories
764 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments765 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
766 Access tokens
767 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look768 </div>
769 <Rule />
770 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings771 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments772 Billing and plans
A catalogue of model providers, and settings that feel like settings773 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API774 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
775 Audit log
776 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look777 </div>
A catalogue of model providers, and settings that feel like settings778 </nav>
779 );
780}
781
Sidebar: no settings flash on the way into a repository, and a better slide782/**
783 * What the repository menu needs. Known from the address before the page's
784 * data arrives, so the menu is right from the first frame; the counts fill
785 * in once it does.
786 */
787type MenuRepo = {
788 namespace: string;
789 name: string;
790 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look791 /** Whether they see its settings; from the repository's page once it loads. */
792 settings?: boolean;
793 /** What they may do there, once the repository's page loads. */
794 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide795 issues?: number;
796 pulls?: number;
797};
798
799function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
800 return (
801 a != null &&
802 b != null &&
803 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
804 a.name.toLowerCase() === b.name.toLowerCase()
805 );
806}
A repository has its own sidebar, as settings do807
808/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look809 * A project's own list, drilled into while you are in it: everything about
810 * the project, running and its code, and nothing else, with the way back
811 * out to all of them.
A repository has its own sidebar, as settings do812 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put813function RepoMenu({
814 repo,
815 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look816 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put817}: {
818 repo: MenuRepo;
819 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look820 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
821 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put822}) {
A repository has its own sidebar, as settings do823 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 put824 // 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 look825 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do826 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look827 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
828 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner829 <NavLink
A repository has its own sidebar, as settings do830 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner831 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents832 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner833 title="Overview"
834 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look835 `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 owner836 }
A repository has its own sidebar, as settings do837 >
838 <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 page839 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do840 </span>
841 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
842 <span className="text-faint">{repo.namespace}/</span>
843 <span className="font-semibold text-fg">{repo.name}</span>
844 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner845 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look846 {/* A project's pages, in the order people use them. A page with more
847 than one view shows them as tabs across its top. */}
848 <Rule />
849 <div className="space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner850 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
851 Code
852 </SidebarLink>
853 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
854 Issues
855 </SidebarLink>
856 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
857 Pull requests
858 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains859 <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 owner860 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains861 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner862 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
863 Workflows
Projects: what a workspace builds and runs, first on every page864 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look865 </div>
866 <Rule />
867 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put868 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner869 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
870 Deployments
871 </SidebarLink>
872 ) : null}
Observability gets its own page; tabs never overflow873 <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.">
874 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner875 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put876 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API877 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
878 Security
879 </SidebarLink>
880 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner881 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
882 Insights
883 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look884 </div>
885 {(repo.settings ?? shows.has("settings")) && (
886 <>
887 <Rule />
888 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner889 Settings
890 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look891 </>
892 )}
A repository has its own sidebar, as settings do893 </nav>
894 );
895}
896
Project settings move into the sidebar, grouped, with a heading per page897/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look898 * A project's settings, drilled into from its list: what is about running
899 * it, then its agents, then its code and who can reach it. The way back
900 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page901 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look902function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page903 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look904 // Each page shows to the roles that can use it, once the role is known.
905 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
906 const running = shows("deployments");
907 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page908 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look909 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
910 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
911 {shows("") && (
912 <div className="mt-2 space-y-px">
913 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
914 General
915 </SidebarLink>
916 </div>
917 )}
918 {running && (
919 <>
920 {shows("") && <Rule />}
921 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
922 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
923 Deployments
924 </SidebarLink>
925 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
926 Domains
927 </SidebarLink>
928 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
929 Dependencies
930 </SidebarLink>
931 </div>
932 </>
933 )}
934 {agents && (
935 <>
936 <Rule />
937 <div className="space-y-px">
938 {shows("agents") && (
939 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
940 Agents
941 </SidebarLink>
942 )}
943 {shows("guardrails") && (
944 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
945 Guardrails
946 </SidebarLink>
947 )}
948 </div>
949 </>
950 )}
951 <Rule />
952 <div className="space-y-px">
953 {shows("repository") && (
954 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
955 Repository
956 </SidebarLink>
957 )}
958 {shows("access") && (
959 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
960 Access
961 </SidebarLink>
962 )}
963 {shows("branches") && (
964 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
965 Branches and merging
966 </SidebarLink>
967 )}
968 {shows("secrets") && (
969 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
970 Secrets and variables
971 </SidebarLink>
972 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents973 {shows("runners") && (
974 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
975 Runners
976 </SidebarLink>
977 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look978 {shows("webhooks") && (
979 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
980 Webhooks
981 </SidebarLink>
982 )}
983 </div>
Project settings move into the sidebar, grouped, with a heading per page984 </nav>
985 );
986}
987
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas988/** Your own settings, drilled into from Your settings: one page each. */
989function AccountSettingsMenu({ username }: { username: string }) {
990 const link = (page: AccountSettingsPage, icon: ReactNode) => (
991 <SidebarLink to={`/settings/${page}`} icon={icon}>
992 {ACCOUNT_SETTINGS[page].title}
993 </SidebarLink>
Account settings in the same shape as a workspace's994 );
995 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look996 <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 quotas997 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look998 <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 quotas999 {link("profile", <CircleUserRound size={15} />)}
1000 {link("emails", <Mail size={15} />)}
1001 {link("invites", <Ticket size={15} />)}
1002 </div>
1003 <Rule />
1004 <div className="space-y-px">
1005 {link("keys", <Fingerprint size={15} />)}
1006 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1007 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1008 <Rule />
1009 <div className="space-y-px">
1010 {link("github", <GithubMark className="size-[15px]" />)}
1011 {link("applications", <Plug size={15} />)}
1012 </div>
1013 <Rule />
1014 <div className="space-y-px">
1015 {link("security-log", <History size={15} />)}
1016 </div>
Account settings in the same shape as a workspace's1017 </nav>
1018 );
1019}
1020
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1021/**
1022 * Signing in and signing up, in place of the account for a visitor. Signing
1023 * in brings them back to the page they are on.
1024 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1025/** Help, status and the fine print under a visitor's sign-in buttons. */
1026function VisitorLinks() {
1027 const status = useSiteStatus();
1028 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1029 const dot = (
1030 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1031 ·
1032 </span>
1033 );
1034 return (
1035 // Two short rows, so a narrow sidebar never starts a line with a dot.
1036 <nav aria-label="About g1t" className="pt-0.5">
1037 <div className="flex items-center gap-0.5">
1038 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1039 <StatusDot state={status?.overall.state ?? null} />
1040 Status
1041 </a>
1042 {dot}
1043 <Link to="/support" className={quiet}>
1044 Support
1045 </Link>
1046 </div>
1047 <div className="flex items-center gap-0.5">
1048 {LEGAL_LINKS.map(([label, to], index) => (
1049 <span key={to} className="flex items-center gap-0.5">
1050 {index > 0 && dot}
1051 <Link to={to} className={quiet}>
1052 {label}
1053 </Link>
1054 </span>
1055 ))}
1056 </div>
1057 </nav>
1058 );
1059}
1060
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1061function VisitorPanel() {
1062 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1063 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1064 return (
1065 <div className="space-y-2">
1066 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1067 <div className="grid grid-cols-2 gap-2">
1068 <Link
1069 to={withNext("/login", pathname + search)}
1070 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"
1071 >
1072 <LogIn size={14} />
1073 Sign in
1074 </Link>
1075 <Link
1076 to={withNext("/register", pathname + search)}
1077 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1078 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1079 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1080 </Link>
1081 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1082 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1083 </div>
1084 );
1085}
1086
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1087function Sidebar({
1088 user,
1089 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1090 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1091 onFind,
1092 onClose,
1093}: {
1094 user: User | null;
1095 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1096 /** The page is a 404: the address names nothing the viewer can see. */
1097 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1098 onFind: () => void;
1099 /** In the sheet on a small screen: closing it, at the end of the top row. */
1100 onClose?: () => void;
1101}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1102 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1103 const { pathname } = useLocation();
1104 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1105 // Where the sidebar is drilled to follows the page being gone to, so it
1106 // moves as the link is followed, not once the page arrives.
1107 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1108 // On a 404, the address is not a place: no list is drilled into from it,
1109 // except the project's own when the project is real and visible and only
1110 // something inside it is missing (a file, a commit, an issue).
1111 const lost = missing && going == null;
1112 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1113 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1114 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1115 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1116 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1117 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"]);
1118 // An invitation to a repository is answered before its menu means anything.
1119 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1120 repoPath != null &&
1121 !reserved.has(repoPath[1]) &&
1122 repoPath[2] !== "-" &&
1123 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1124 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1125 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1126 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1127 // The repository the menu is for: the one loaded if it is the one being
1128 // gone to, else what the address says, at once.
1129 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 look1130 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1131 ? sameRepo(active, targetRepo)
1132 ? active
1133 : {
1134 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1135 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1136 }
1137 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1138 // The viewer's role there, from the repository's page once it has loaded.
1139 const page = useRouteLoaderData("routes/repo/layout") as
1140 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1141 | undefined;
1142 const menuRepo: MenuRepo | null =
1143 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1144 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1145 : guessed;
1146
1147 // The way from the main list to the one shown.
1148 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1149 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1150 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1151 } else if (ws && inSettings) {
1152 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1153 } else if (menuRepo) {
1154 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1155 // Out of a project: to its workspace's projects when they are yours.
1156 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1157 const back = !user
1158 ? { to: "/explore", label: "Explore" }
1159 : home
1160 ? { to: `/${home.slug}`, label: "All projects" }
1161 : { to: "/", label: "Mission control" };
1162 trail.push({
1163 key,
1164 node: (
1165 <RepoMenu
1166 repo={menuRepo}
1167 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1168 back={back}
1169 />
1170 ),
1171 });
1172 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1173 }
1174
Agents as a team: lifecycle, merge queue, billing and a new shell1175 return (
1176 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1177 {/* 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 control1178 <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 put1179 {user ? (
1180 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1181 {/* The 1 alone beside the workspace: a square hover the height of the
1182 switcher, the mark as tall as the workspace avatar. */}
1183 <Link to="/" aria-label="g1t home" className="flex size-9 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-raised">
1184 <Mark tight className="h-5 w-auto" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1185 </Link>
1186 <span className="shrink-0 text-line-strong" aria-hidden="true">
1187 /
1188 </span>
1189 <WorkspaceSwitcher user={user} shell={shell} />
1190 </>
1191 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1192 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1193 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1194 </Link>
1195 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1196 {onClose && (
1197 <button
1198 type="button"
1199 aria-label="Close menu"
1200 onClick={onClose}
1201 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1202 >
1203 <X size={16} />
1204 </button>
1205 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1206 </div>
A Vercel-like sidebar and a new mark1207 <div className="px-2 pt-3">
1208 <button
1209 type="button"
1210 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1211 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 mark1212 >
1213 <Search size={14} />
1214 <span className="grow text-left">Find…</span>
1215 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1216 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1217 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1218 <Drill trail={trail} />
1219 {/* The account, or signing in, as one row at the very bottom. */}
1220 <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 put1221 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1222 </div>
1223 </div>
1224 );
1225}
1226
1227/** Words for the sections a path can end in. */
1228const SECTIONS: Record<string, string> = {
1229 issues: "Issues",
1230 pulls: "Pull requests",
1231 queue: "Merge queue",
1232 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1233 plans: "Outcomes",
1234 actions: "Workflows",
1235 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1236 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1237 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1238 access: "Access",
1239 invitations: "Invitation",
1240 repositories: "Repositories",
1241 branches: "Branches and merging",
Project dependencies: addresses, preview stacks, Affects, and agents who know1242 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1243 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1244 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1245 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1246 people: "Members",
A Vercel-like sidebar and a new mark1247 tokens: "Access tokens",
1248 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1249 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1250 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1251 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1252 domains: "Domains",
1253 guardrails: "Guardrails",
1254 audit: "Audit log",
A Vercel-like sidebar and a new mark1255 tree: "Code",
1256 blob: "Code",
1257};
1258
1259/** 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 quotas1260function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1261 // A 404 names nothing from the address, unless the project is real and
1262 // visible and only something inside it is missing.
1263 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1264 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1265 return <span className="text-sm font-medium">Not found</span>;
1266 }
A Vercel-like sidebar and a new mark1267 const parts = pathname.split("/").filter(Boolean);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1268 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite"];
A Vercel-like sidebar and a new mark1269 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 quotas1270 // Your settings: Settings / Emails.
1271 if (parts[0] === "settings") {
1272 const page = accountSettingsPage(pathname);
1273 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1274 return (
1275 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1276 {trail.map((crumb, index) => (
1277 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1278 {index > 0 && <span className="text-line-strong">/</span>}
1279 <Link
1280 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1281 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1282 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1283 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1284 }`}
1285 >
1286 {crumb.label}
1287 </Link>
1288 </span>
1289 ))}
1290 </nav>
1291 );
1292 }
A Vercel-like sidebar and a new mark1293 if (reserved.includes(parts[0]!)) {
1294 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1295 settings: "Account",
A Vercel-like sidebar and a new mark1296 explore: "Explore",
Projects live: fixes from walking them end to end1297 new: "New project",
A Vercel-like sidebar and a new mark1298 search: "Search",
1299 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1300 policies: "Policies",
1301 security: "Security",
1302 support: "Support",
1303 status: "Status",
1304 invite: "Invite",
A Vercel-like sidebar and a new mark1305 };
1306 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1307 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1308 // A person's profile, by their handle.
1309 if (parts[0] === "u" && parts[1]) {
1310 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1311 }
A Vercel-like sidebar and a new mark1312 const [owner, second, third, fourth] = parts;
1313 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1314 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1315 const page = `/${owner}/-/${third}`;
1316 if (third && SETTINGS_PAGE.test(page)) {
1317 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1318 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1319 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1320 } else if (second) {
1321 const repo = `/${owner}/${second}`;
1322 trail.push({ label: second, to: repo, mono: true });
1323 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1324 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1325 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 deployments1326 else if (third && SECTIONS[third]) {
1327 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1328 // A settings page names which one: Settings / Secrets and variables.
1329 if (third === "settings" && fourth && SECTIONS[fourth]) {
1330 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
1331 }
1332 }
A Vercel-like sidebar and a new mark1333 }
1334 return (
1335 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1336 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1337 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1338 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1339 <Link
A Vercel-like sidebar and a new mark1340 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1341 prefetch="intent"
A Vercel-like sidebar and a new mark1342 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1343 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1344 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1345 >
A Vercel-like sidebar and a new mark1346 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1347 </Link>
A Vercel-like sidebar and a new mark1348 </span>
1349 ))}
1350 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1351 );
1352}
1353
Search across all of g1t, Explore, and a command palette1354type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1355
1356/** 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 look1357function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1358 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1359 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1360 { 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 step1361 ...(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-agent1362 ? [{ 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 step1363 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1364 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1365 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page1366 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1367 { 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 quotas1368 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1369 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1370 .filter((page) => page !== "profile")
1371 .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 shell1372 ];
1373 const repo = shell.repo;
1374 if (repo) {
1375 const base = `/${repo.namespace}/${repo.name}`;
1376 const name = `${repo.namespace}/${repo.name}`;
1377 commands.unshift(
Projects: what a workspace builds and runs, first on every page1378 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1379 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1380 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1381 { 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 step1382 ...(repo.member
1383 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1384 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1385 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1386 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1387 ...(repo.member
1388 ? [
1389 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1390 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1391 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1392 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1393 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1394 ]
1395 : []),
1396 );
1397 }
1398 for (const membership of user.workspaces ?? []) {
1399 commands.push(
Workspace names and icons, and a component kit for every control1400 { 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 request1401 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1402 { 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 settings1403 { 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 look1404 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1405 );
1406 }
1407 for (const listed of shell.repos) {
1408 commands.push({
Projects: what a workspace builds and runs, first on every page1409 label: listed.title ?? listed.name,
1410 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1411 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1412 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1413 });
1414 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1415 // "Put an agent on …": one per project, after the projects themselves.
1416 for (const listed of shell.repos) {
1417 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1418 commands.push({
1419 label: `Put an agent on ${listed.title ?? listed.name}`,
1420 hint: `${listed.namespace}/${listed.name}`,
1421 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1422 icon: <Sparkles size={15} />,
1423 });
1424 }
Agents as a team: lifecycle, merge queue, billing and a new shell1425 return commands;
1426}
1427
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1428/** 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 look1429function 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 put1430 const commands: Command[] = [];
1431 const repo = shell.repo;
1432 if (repo) {
1433 const base = `/${repo.namespace}/${repo.name}`;
1434 const name = `${repo.namespace}/${repo.name}`;
1435 commands.push(
1436 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1437 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1438 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1439 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1440 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1441 );
1442 }
1443 commands.push(
1444 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1445 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1446 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1447 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1448 { 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 look1449 { 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 put1450 );
1451 return commands;
1452}
1453
Agents as a team: lifecycle, merge queue, billing and a new shell1454/**
1455 * A bar across the top of the page while the next one loads, as GitHub
1456 * has: it appears at once, creeps towards the end while waiting, then fills
1457 * and fades when the page arrives.
1458 */
1459export function Progress() {
1460 const navigation = useNavigation();
1461 const busy = navigation.state !== "idle";
1462 const [width, setWidth] = useState(0);
1463 const [visible, setVisible] = useState(false);
1464 useEffect(() => {
1465 if (busy) {
1466 setVisible(true);
1467 setWidth(12);
1468 // Slows as it goes, never quite arriving until the page does.
1469 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1470 return () => clearInterval(timer);
1471 }
1472 setWidth((now) => (now > 0 ? 100 : 0));
1473 const fade = setTimeout(() => setVisible(false), 250);
1474 const reset = setTimeout(() => setWidth(0), 550);
1475 return () => {
1476 clearTimeout(fade);
1477 clearTimeout(reset);
1478 };
1479 }, [busy]);
1480 return (
1481 <div
1482 aria-hidden="true"
1483 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1484 visible ? "opacity-100" : "opacity-0"
1485 }`}
1486 >
1487 <div
1488 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1489 style={{ width: `${width}%` }}
1490 />
1491 </div>
1492 );
1493}
1494
1495/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1496 * The app: a sidebar with the workspace, its repositories and the sections
1497 * of the one being looked at; a slim bar with search and the account; and
1498 * the page. A visitor who is not signed in gets the same frame, with
1499 * Explore and Search in place of the workspace, and signing in in place of
1500 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1501 */
1502export function AppShell({
1503 user,
1504 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1505 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1506 banner,
1507 children,
1508}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1509 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1510 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1511 /** The page is a 404 (see Sidebar). */
1512 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1513 banner?: ReactNode;
1514 children: ReactNode;
1515}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1516 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1517 const [drawer, setDrawer] = useState(false);
1518 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1519 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1520 const signUpLabel = useSignUpCopy().primary;
1521 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1522
1523 // A new page closes the drawer on small screens.
1524 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1525 // Mission control's code, fetched while the browser is idle, so going
1526 // home never waits on it (routes/home.tsx).
1527 useEffect(() => {
1528 if (!user) return;
1529 const load = () => void import("./mission-control").catch(() => undefined);
1530 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1531 if (idle) idle(load);
1532 else setTimeout(load, 1500);
1533 }, [user]);
Search across all of g1t, Explore, and a command palette1534 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1535
1536 return (
1537 <div className="min-h-screen">
1538 <Progress />
1539 <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 quotas1540 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1541 </aside>
1542 {drawer && (
1543 <div className="fixed inset-0 z-50 lg:hidden">
1544 <button
1545 type="button"
1546 aria-label="Close menu"
1547 className="absolute inset-0 bg-black/60"
1548 onClick={() => setDrawer(false)}
1549 />
1550 <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 quotas1551 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1552 </aside>
1553 </div>
1554 )}
1555
Polish: phones, copy boxes, the plan page, the landing page, a real glide1556 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals1557 <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 shell1558 <button
1559 type="button"
1560 aria-label="Open menu"
1561 onClick={() => setDrawer(true)}
1562 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1563 >
1564 <Menu size={18} />
1565 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1566 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1567 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1568 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1569 <input
1570 name="q"
1571 {...notACredential()}
1572 placeholder="Search g1t"
1573 aria-label="Search g1t"
1574 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"
1575 />
1576 <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">
1577 ⌘K
1578 </kbd>
1579 </Form>
1580 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1581 <a
1582 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1583 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 mark1584 >
1585 Docs
1586 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1587 {!user ? (
1588 // The sidebar has these too, but on a phone it is folded away.
1589 <Link
1590 to={withNext("/login", here)}
1591 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"
1592 >
1593 Sign in
1594 </Link>
1595 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1596 <DropdownMenu>
1597 <DropdownMenuTrigger
1598 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1599 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 shell1600 >
A Vercel-like sidebar and a new mark1601 <Plus size={14} />
1602 New
Agents as a team: lifecycle, merge queue, billing and a new shell1603 </DropdownMenuTrigger>
1604 <DropdownMenuContent align="end">
1605 {shell.repo && (
1606 <DropdownMenuItem asChild>
1607 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1608 <CircleDot />
1609 New issue
1610 </Link>
1611 </DropdownMenuItem>
1612 )}
1613 {shell.repo?.member && (
1614 <DropdownMenuItem asChild>
1615 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1616 <ListTree />
1617 Plan work
1618 </Link>
1619 </DropdownMenuItem>
1620 )}
1621 <DropdownMenuItem asChild>
1622 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1623 <Box />
1624 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1625 </Link>
1626 </DropdownMenuItem>
1627 <DropdownMenuItem asChild>
1628 <Link to="/workspaces/new">
1629 <Users />
1630 New workspace
1631 </Link>
1632 </DropdownMenuItem>
1633 </DropdownMenuContent>
1634 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1635 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1636 </div>
1637 </header>
1638 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1639 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1640 </div>
Search across all of g1t, Explore, and a command palette1641 <CommandPalette
1642 open={palette}
1643 onOpenChange={setPalette}
1644 commands={commands}
1645 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1646 />
Agents as a team: lifecycle, merge queue, billing and a new shell1647 </div>
1648 );
1649}