Skip to content

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

1,643 lines66,654 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.

Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, KanbanSquare, KeyRound, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas3import { Form, Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all5import type { Abilities, InboxCounts, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Search across all of g1t, Explore, and a command palette7import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all8import { AskAi, InboxBell } from "./inbox";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas9import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height10import { Logo, Mark } from "./logo";
Search across all of g1t, Explore, and a command palette11import { Avatar, notACredential } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents12import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell13import {
14 DropdownMenu,
15 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas16 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell17 DropdownMenuItem,
18 DropdownMenuLabel,
19 DropdownMenuSeparator,
20 DropdownMenuTrigger,
21} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales22import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23import { 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 put24import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas25import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
26import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put27import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look28import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas29import { STATUS_URL, statusTitle } from "../lib/status";
30import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell31
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put32/**
33 * What the sidebar needs, worked out by the root loader. For a visitor who
34 * is not signed in there is no workspace, no projects and no usage: only
35 * the project being looked at, if they can see it.
36 */
Agents as a team: lifecycle, merge queue, billing and a new shell37export type ShellData = {
38 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control39 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page40 /** Its projects, by name: `name` is the slug in their address. */
41 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42 /** Repositories shared with them in workspaces they do not belong to. */
43 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page44 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell45 repo: {
46 namespace: string;
47 name: string;
48 member: boolean;
49 issues: number;
50 pulls: number;
51 } | null;
Billing accounts, terms and enterprises; g1t is no longer free52 /** Where the workspace stands against its usage limit, if billing is on. */
53 limit: {
54 exposureMicros: number;
55 ceilingMicros: number | null;
56 state: "ok" | "warning" | "stopped";
57 comped: boolean;
58 } | null;
Free while g1t is being built out; agents can check out their own forks59 /** Whether g1t charges nothing for now, while it is being built out. */
60 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request61 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy62 /** This month's usage: charged, or at cost while g1t is free. */
63 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look64 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
65 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all66 /** What is unread in their inbox, for the bell. Absent for a visitor. */
67 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell68};
69
70function SidebarLink({
71 to,
72 icon,
73 end,
74 count,
75 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell77 children,
78}: {
79 to: string;
80 icon: ReactNode;
81 end?: boolean;
82 count?: number;
Projects: what a workspace builds and runs, first on every page83 /** Other path prefixes under which this link is the current one. */
84 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 /** It opens a list of its own: a chevron says so, always or on hover. */
86 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell87 children: ReactNode;
88}) {
89 const { pathname } = useLocation();
90 return (
91 <NavLink
92 to={to}
93 end={end}
94 prefetch="intent"
95 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page96 const current =
97 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark98 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 shell99 current
100 ? "bg-raised font-medium text-fg"
101 : isPending
102 ? "bg-raised/60 text-fg"
103 : "text-muted hover:bg-raised/60 hover:text-fg"
104 }`;
105 }}
106 >
107 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
108 <span className="min-w-0 grow truncate">{children}</span>
109 {count != null && count > 0 && (
110 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
111 {count}
112 </span>
113 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 {drill && (
115 <ChevronRight
116 size={14}
117 aria-hidden="true"
118 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
119 />
120 )}
Agents as a team: lifecycle, merge queue, billing and a new shell121 </NavLink>
122 );
123}
124
Pull requests say where they are live; the project menu shows what is coming125/**
126 * Something a project will have, shown where it will live so people can
127 * see where g1t is going. Not a link: nothing is behind it yet.
128 */
129function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
130 return (
131 <div
132 title={about}
133 aria-disabled="true"
134 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
135 >
136 <span className="shrink-0 opacity-70">{icon}</span>
137 <span className="grow truncate">{children}</span>
138 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
139 Soon
140 </span>
141 </div>
142 );
143}
144
The project menu opens by section, and the workspace has its own panel145/**
A flat project menu with Code first, views as tabs, and Stripe for every owner146 * A page that is coming, as a link to its roadmap page: faint, with Soon,
147 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel148 */
A flat project menu with Code first, views as tabs, and Stripe for every owner149function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel150 to,
151 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner152 icon,
153 about,
The project menu opens by section, and the workspace has its own panel154 children,
155}: {
156 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner157 also?: string[];
158 icon: ReactNode;
159 about: string;
The project menu opens by section, and the workspace has its own panel160 children: ReactNode;
161}) {
162 const { pathname } = useLocation();
163 return (
164 <NavLink
165 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner166 title={about}
The project menu opens by section, and the workspace has its own panel167 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner168 className={({ isActive }) => {
169 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
170 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
171 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 panel172 }`;
173 }}
174 >
A flat project menu with Code first, views as tabs, and Stripe for every owner175 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel176 <span className="min-w-0 grow truncate">{children}</span>
177 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
178 Soon
179 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales180 </NavLink>
The project menu opens by section, and the workspace has its own panel181 );
182}
183
A flat project menu with Code first, views as tabs, and Stripe for every owner184/** Icons for what spans projects. */
185const WORKSPACE_ICONS: Record<string, ReactNode> = {
186 board: <KanbanSquare size={15} />,
187 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member188 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner189 packages: <Package size={15} />,
190 fleet: <Bot size={15} />,
191};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales192
A flat project menu with Code first, views as tabs, and Stripe for every owner193/** The Soon pages of a section, as paths, so its link is current on them. */
194function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
195 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales196}
197
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look198function SidebarGroup({
199 title,
200 action,
201 className = "",
202 children,
203}: {
204 title: string;
205 action?: ReactNode;
206 className?: string;
207 children: ReactNode;
208}) {
Agents as a team: lifecycle, merge queue, billing and a new shell209 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look210 <section className={className}>
A Vercel-like sidebar and a new mark211 <div className="mb-1 flex h-6 items-center justify-between px-2">
212 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell213 {action}
214 </div>
215 <div className="space-y-px">{children}</div>
216 </section>
217 );
218}
219
Workspace names and icons, and a component kit for every control220/** What a workspace is called where people read it: its display name, else its slug. */
221function displayName(membership: Membership): string {
222 return membership.name?.trim() || membership.slug;
223}
224
Agents as a team: lifecycle, merge queue, billing and a new shell225function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control226 const workspace = shell.workspace;
227 const current = workspace?.slug;
228 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell229 return (
230 <DropdownMenu>
Workspace names and icons, and a component kit for every control231 {/* The name goes to the workspace; only the arrows switch it. The
232 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark233 <Link
234 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents235 prefetch="intent"
Workspace names and icons, and a component kit for every control236 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 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 mark238 >
Workspace names and icons, and a component kit for every control239 {workspace ? (
240 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell241 ) : (
Workspace names and icons, and a component kit for every control242 <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 shell243 )}
Workspace names and icons, and a component kit for every control244 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark245 </Link>
246 <DropdownMenuTrigger
247 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look248 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 mark249 >
250 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell251 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control252 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell253 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
254 {(user.workspaces ?? []).map((membership) => (
255 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control256 <Link to={`/${membership.slug}`} title={displayName(membership)}>
257 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
258 <span className="flex min-w-0 grow flex-col leading-tight">
259 <span className="truncate">{displayName(membership)}</span>
260 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
261 </span>
262 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
263 {membership.slug === current ? (
264 <Check className="shrink-0 text-accent" />
265 ) : (
266 <span className="size-4 shrink-0" aria-hidden="true" />
267 )}
Agents as a team: lifecycle, merge queue, billing and a new shell268 </Link>
269 </DropdownMenuItem>
270 ))}
271 <DropdownMenuSeparator />
272 <DropdownMenuItem asChild>
273 <Link to="/workspaces/new">
274 <Plus />
275 New workspace
276 </Link>
277 </DropdownMenuItem>
278 </DropdownMenuContent>
279 </DropdownMenu>
A Vercel-like sidebar and a new mark280 );
281}
282
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas283/**
284 * The fine print, as one quiet row at the foot of the account menu and the
285 * visitor's panel: each link a full-height target, left-aligned. In the
286 * menu each is a menu item, so the arrow keys reach it.
287 */
288const LEGAL_LINKS: [string, string][] = [
289 ["Policies", "/policies"],
290 ["Privacy", "/policies/privacy"],
291 ["Security", "/security"],
292];
293const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
294
295function MenuLegalRow() {
296 return (
297 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
298 {LEGAL_LINKS.map(([label, to], index) => (
299 <span key={to} className="flex items-center gap-0.5">
300 {index > 0 && (
301 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
302 ·
303 </span>
304 )}
305 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
306 <Link to={to}>{label}</Link>
307 </DropdownMenuItem>
308 </span>
309 ))}
310 </div>
311 );
312}
313
314/** Status's row: the live dot and two or three words, fetched when the menu opens. */
315function StatusSummary({ open }: { open: boolean }) {
316 const status = useSiteStatus(open);
317 const words = statusTitle(status);
318 return (
319 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
320 {/* The words say it; the dot's own words for screen readers would repeat them. */}
321 <span aria-hidden="true" className="flex">
322 <StatusDot state={status?.overall.state ?? null} />
323 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents324 {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 quotas325 </span>
326 );
327}
328
329/** A menu row: an icon, words, and whatever sits at its end. */
330const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
331
A Vercel-like sidebar and a new mark332function AccountMenu({ user }: { user: User }) {
333 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas334 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents335 // Name, primary address and invites left: asked for once, as soon as the
336 // 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 quotas337 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents338 const prefetch = () => {
339 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
340 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas341 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents342 if (open) prefetch();
343 // eslint-disable-next-line react-hooks/exhaustive-deps
344 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas345 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents346 // Still on its way: shapes where the words will be, the same size.
347 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 quotas348 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark349 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas350 <DropdownMenu open={open} onOpenChange={setOpen}>
351 <DropdownMenuTrigger
352 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents353 onPointerEnter={prefetch}
354 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas355 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"
356 >
Workspace names and icons, and a component kit for every control357 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark358 <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 quotas359 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark360 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas361 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
362 {/* Who is signed in, and a way to their profile. */}
363 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
364 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
365 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents366 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
367 {loading ? (
368 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
369 ) : (
370 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
371 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas372 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents373 {loading ? (
374 <Skeleton className="mt-1 h-3 w-40" />
375 ) : (
376 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
377 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas378 </span>
A Vercel-like sidebar and a new mark379 </Link>
380 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas381 <DropdownMenuSeparator className="my-1.5" />
382 <DropdownMenuGroup>
383 <DropdownMenuItem asChild className={MENU_ROW}>
384 <Link to={profile}>
385 <CircleUserRound />
386 Your profile
387 </Link>
388 </DropdownMenuItem>
389 <DropdownMenuItem asChild className={MENU_ROW}>
390 <Link to={FIRST_SETTINGS_PAGE}>
391 <Settings />
392 Your settings
393 </Link>
394 </DropdownMenuItem>
395 <DropdownMenuItem asChild className={MENU_ROW}>
396 <Link to="/settings/invites">
397 <Ticket />
398 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents399 {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 quotas400 {me?.invites_left != null && (
401 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
402 {me.invites_left} left
403 </span>
404 )}
405 </Link>
406 </DropdownMenuItem>
407 </DropdownMenuGroup>
408 <DropdownMenuSeparator className="my-1.5" />
409 <DropdownMenuGroup>
410 <DropdownMenuItem asChild className={MENU_ROW}>
411 <a href="https://docs.g1t.sh/">
412 <BookOpen />
413 Documentation
414 </a>
415 </DropdownMenuItem>
416 <DropdownMenuItem asChild className={MENU_ROW}>
417 <Link to="/support">
418 <LifeBuoy />
419 Support
420 </Link>
421 </DropdownMenuItem>
422 <DropdownMenuItem asChild className={MENU_ROW}>
423 <a href={STATUS_URL}>
424 <Activity />
425 Status
426 <StatusSummary open={open} />
427 </a>
428 </DropdownMenuItem>
429 </DropdownMenuGroup>
430 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark431 {/* Submitted from here: the menu closes on select, and a button
432 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 quotas433 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark434 <LogOut />
435 Sign out
436 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas437 <DropdownMenuSeparator className="my-1.5" />
438 <MenuLegalRow />
A Vercel-like sidebar and a new mark439 </DropdownMenuContent>
440 </DropdownMenu>
441 );
442}
443
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look444/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar445 * A workspace's settings pages, which the sidebar drills into: how it is
446 * set up and connected (guardrails, secrets, runners, integrations,
447 * webhooks), what it pays, its repositories, tokens and record. The main
448 * list keeps the places work happens and who belongs; every member can
449 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look450 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar451const SETTINGS_PAGE =
452 /^\/([^/]+)\/-\/(settings|repositories|tokens|guardrails|secrets|runners|integrations|webhooks|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look453/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page454const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings455
Polish: phones, copy boxes, the plan page, the landing page, a real glide456/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look457 * The sidebar's lists are a stack, the way a phone pushes a screen: one
458 * drilled into slides in from the right over the full width, while the one
459 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide460 * ease-out, the fade quicker than the move so the two never blur together.
461 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide462 */
Sidebar: no settings flash on the way into a repository, and a better slide463const LAYER =
Sidebar: the panels really slide464 "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 look465/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide466const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings467
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look468/** One list in the stack: what it is, and its rows. */
469type Level = { key: string; node: ReactNode };
470
471/** True once the page has hydrated, so what the server drew never animates in. */
472function useHydrated(): boolean {
473 const [hydrated, setHydrated] = useState(false);
474 useEffect(() => setHydrated(true), []);
475 return hydrated;
476}
477
478/**
479 * Drilling in and out of the sidebar's lists. `trail` is the way from the
480 * main list to the one shown, worked out from the address, so a link
481 * straight to a settings page opens the sidebar already drilled in, and
482 * leaving it comes back out. Lists deeper than the one shown stay in
483 * place, off to the right, so going back slides them away rather than
484 * dropping them. Moving to a list at the same depth (a workspace's settings
485 * to a project) fades the new one in where it is.
486 */
487function Drill({ trail }: { trail: Level[] }) {
488 const depth = trail.length - 1;
489 const hydrated = useHydrated();
490 const stack = useRef<Level[]>(trail);
491 const before = stack.current;
492 const within = trail.every((level, index) => before[index]?.key === level.key);
493 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
494 const levels = stack.current;
495 // What was showing last time, to tell drilling in from moving across.
496 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
497 useEffect(() => {
498 shown.current = { depth, keys: levels.map((level) => level.key) };
499 });
A catalogue of model providers, and settings that feel like settings500 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look501 <div className="relative min-h-0 grow overflow-hidden">
502 {levels.map((level, index) => {
503 const current = index === depth;
504 const place =
505 index < depth
506 ? "pointer-events-none -translate-x-1/4 opacity-0"
507 : current
508 ? "translate-x-0 opacity-100"
509 : "pointer-events-none translate-x-full opacity-0";
510 // A list that was not there before arrives: from the right when
511 // drilling in, in place when moving across.
512 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
513 const enter = arriving
514 ? depth > shown.current.depth
515 ? "starting:translate-x-full starting:opacity-0"
516 : "starting:opacity-0"
517 : "";
518 return (
519 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
520 {level.node}
521 </div>
522 );
523 })}
524 </div>
525 );
526}
527
528/**
529 * The row at the top of a list drilled into: the list's name, which goes
530 * back out, and what it belongs to, faint at the end.
531 */
532function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
533 return (
534 <Link
535 to={to}
536 prefetch="intent"
537 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 settings538 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look539 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
540 <span className="min-w-0 shrink-0 truncate">{label}</span>
541 {context && (
542 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
543 )}
544 </Link>
545 );
546}
547
548/** A thin rule between groups of a list. */
549function Rule() {
550 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
551}
552
553/**
554 * The main list: where you go, the projects, what the workspace builds and
555 * runs with across them, then its usage, support and settings.
556 */
557function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
558 const ws = shell.workspace;
559 const active = shell.repo;
560 // The repository being looked at is listed even when it is someone else's.
561 const listed =
562 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
563 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
564 : shell.repos;
565 // A visitor browses: no workspace, no projects of their own.
566 if (!user) {
567 return (
568 <nav aria-label="g1t" className={PANEL}>
569 <div className="mt-3 space-y-px">
570 {VISITOR_LINKS.map((link) => (
571 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
572 {link.label}
573 </SidebarLink>
574 ))}
575 </div>
576 </nav>
577 );
578 }
579 return (
580 <nav aria-label="g1t" className={PANEL}>
581 <div className="mt-3 space-y-px">
582 <SidebarLink to="/" end icon={<House size={15} />}>
583 Mission control
584 </SidebarLink>
585 {ws && (
586 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutGrid size={15} />}>
587 Overview
588 </SidebarLink>
589 )}
590 <SidebarLink to="/explore" icon={<Compass size={15} />}>
591 Explore
592 </SidebarLink>
593 </div>
594
595 <Rule />
596 <SidebarGroup
597 title="Projects"
598 action={
599 ws && (
600 <Link
601 to={`/new?workspace=${ws.slug}`}
602 aria-label="New project"
603 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
604 >
605 <Plus size={13} />
606 </Link>
607 )
608 }
A catalogue of model providers, and settings that feel like settings609 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look610 {listed.length === 0 && <p className="px-2 py-1 text-xs text-faint">None yet.</p>}
611 {listed.map((repo) => (
612 <SidebarLink
613 key={`${repo.namespace}/${repo.name}`}
614 to={`/${repo.namespace}/${repo.name}`}
615 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
616 drill="hover"
617 >
618 {repo.namespace !== ws?.slug && <span className="font-mono text-faint">{repo.namespace}/</span>}
619 {"title" in repo && repo.title ? repo.title : repo.name}
620 </SidebarLink>
621 ))}
622 </SidebarGroup>
623 {(shell.shared ?? []).length > 0 && (
624 <SidebarGroup title="Shared with you" className="mt-3">
625 {(shell.shared ?? []).map((repo) => (
626 <SidebarLink
627 key={`${repo.namespace}/${repo.name}`}
628 to={`/${repo.namespace}/${repo.name}`}
629 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
630 drill="hover"
631 >
632 <span className="font-mono text-faint">{repo.namespace}/</span>
633 {repo.name}
634 </SidebarLink>
635 ))}
636 </SidebarGroup>
637 )}
638
639 {ws && (
640 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member641 <Rule />
642 <div className="space-y-px">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look643 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
644 Agent fleet
645 </SidebarLink>
646 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
647 Context
648 </SidebarLink>
649 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
650 Memory
651 </SidebarLink>
652 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
653 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents654 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member655 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
656 Packages
657 </SidebarLink>
658 {roadmapIn("Workspace").filter((item) => item.key !== "teams").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look659 <SidebarSoonLink
660 key={item.key}
661 to={`/${ws.slug}/-/soon/${item.key}`}
662 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
663 about={item.summary}
664 >
665 {item.title === "Board" ? "Boards" : item.title}
666 </SidebarSoonLink>
667 ))}
668 </div>
669
670 <Rule />
671 <div className="space-y-px">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar672 {/* Who belongs, for every member to see; owners invite and manage there. */}
673 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
674 Members
675 </SidebarLink>
676 {roadmapIn("Workspace")
677 .filter((item) => item.key === "teams")
678 .map((item) => (
679 <SidebarSoonLink key={item.key} to={`/${ws.slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key]} about={item.summary}>
680 {item.title}
681 </SidebarSoonLink>
682 ))}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look683 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
684 Usage
685 </SidebarLink>
686 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
687 Support
688 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar689 {/* How it is set up and connected, what it pays and its record: a list of their own. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look690 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member691 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/repositories`}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look692 icon={<Settings size={15} />}
693 drill
694 >
695 Settings
696 </SidebarLink>
697 </div>
698 </>
699 )}
700 </nav>
701 );
702}
703
704/** A workspace's settings, drilled into from Settings in the main list. */
705function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
706 return (
707 <nav aria-label="Workspace settings" className={PANEL}>
708 <BackRow to={`/${slug}`} label="Settings" context={slug} />
709 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings710 {owner && (
711 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
712 General
713 </SidebarLink>
714 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look715 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
716 Repositories
717 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments718 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
719 Access tokens
720 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look721 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar722 <SidebarGroup title="Agents and runs" className="mt-3">
723 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
724 Guardrails
725 </SidebarLink>
726 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
727 Secrets and variables
728 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it729 {owner && (
730 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
731 Runners
732 </SidebarLink>
733 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar734 </SidebarGroup>
735 <SidebarGroup title="Connections" className="mt-3">
736 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
737 Integrations
738 </SidebarLink>
739 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
740 Webhooks
741 </SidebarLink>
742 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look743 <Rule />
744 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings745 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments746 Billing and plans
A catalogue of model providers, and settings that feel like settings747 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API748 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
749 Audit log
750 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look751 </div>
A catalogue of model providers, and settings that feel like settings752 </nav>
753 );
754}
755
Sidebar: no settings flash on the way into a repository, and a better slide756/**
757 * What the repository menu needs. Known from the address before the page's
758 * data arrives, so the menu is right from the first frame; the counts fill
759 * in once it does.
760 */
761type MenuRepo = {
762 namespace: string;
763 name: string;
764 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look765 /** Whether they see its settings; from the repository's page once it loads. */
766 settings?: boolean;
767 /** What they may do there, once the repository's page loads. */
768 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide769 issues?: number;
770 pulls?: number;
771};
772
773function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
774 return (
775 a != null &&
776 b != null &&
777 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
778 a.name.toLowerCase() === b.name.toLowerCase()
779 );
780}
A repository has its own sidebar, as settings do781
782/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look783 * A project's own list, drilled into while you are in it: everything about
784 * the project, running and its code, and nothing else, with the way back
785 * out to all of them.
A repository has its own sidebar, as settings do786 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put787function RepoMenu({
788 repo,
789 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look790 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put791}: {
792 repo: MenuRepo;
793 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look794 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
795 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put796}) {
A repository has its own sidebar, as settings do797 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 put798 // 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 look799 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do800 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look801 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
802 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner803 <NavLink
A repository has its own sidebar, as settings do804 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner805 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents806 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner807 title="Overview"
808 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look809 `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 owner810 }
A repository has its own sidebar, as settings do811 >
812 <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 page813 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do814 </span>
815 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
816 <span className="text-faint">{repo.namespace}/</span>
817 <span className="font-semibold text-fg">{repo.name}</span>
818 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner819 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look820 {/* A project's pages, in the order people use them. A page with more
821 than one view shows them as tabs across its top. */}
822 <Rule />
823 <div className="space-y-px">
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request824 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, `${base}/branches`, `${base}/tags`, `${base}/compare`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
A flat project menu with Code first, views as tabs, and Stripe for every owner825 Code
826 </SidebarLink>
827 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
828 Issues
829 </SidebarLink>
830 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
831 Pull requests
832 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains833 <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 owner834 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains835 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner836 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
837 Workflows
Projects: what a workspace builds and runs, first on every page838 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look839 </div>
840 <Rule />
841 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put842 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner843 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
844 Deployments
845 </SidebarLink>
846 ) : null}
Observability gets its own page; tabs never overflow847 <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.">
848 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner849 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put850 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API851 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
852 Security
853 </SidebarLink>
854 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner855 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
856 Insights
857 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look858 </div>
859 {(repo.settings ?? shows.has("settings")) && (
860 <>
861 <Rule />
862 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner863 Settings
864 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look865 </>
866 )}
A repository has its own sidebar, as settings do867 </nav>
868 );
869}
870
Project settings move into the sidebar, grouped, with a heading per page871/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look872 * A project's settings, drilled into from its list: what is about running
873 * it, then its agents, then its code and who can reach it. The way back
874 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page875 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look876function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page877 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look878 // Each page shows to the roles that can use it, once the role is known.
879 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
880 const running = shows("deployments");
881 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page882 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look883 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
884 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
885 {shows("") && (
886 <div className="mt-2 space-y-px">
887 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
888 General
889 </SidebarLink>
890 </div>
891 )}
892 {running && (
893 <>
894 {shows("") && <Rule />}
895 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
896 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
897 Deployments
898 </SidebarLink>
899 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
900 Domains
901 </SidebarLink>
902 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
903 Dependencies
904 </SidebarLink>
905 </div>
906 </>
907 )}
908 {agents && (
909 <>
910 <Rule />
911 <div className="space-y-px">
912 {shows("agents") && (
913 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
914 Agents
915 </SidebarLink>
916 )}
917 {shows("guardrails") && (
918 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
919 Guardrails
920 </SidebarLink>
921 )}
922 </div>
923 </>
924 )}
925 <Rule />
926 <div className="space-y-px">
927 {shows("repository") && (
928 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
929 Repository
930 </SidebarLink>
931 )}
932 {shows("access") && (
933 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
934 Access
935 </SidebarLink>
936 )}
937 {shows("branches") && (
938 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
939 Branches and merging
940 </SidebarLink>
941 )}
942 {shows("secrets") && (
943 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
944 Secrets and variables
945 </SidebarLink>
946 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents947 {shows("runners") && (
948 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
949 Runners
950 </SidebarLink>
951 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look952 {shows("webhooks") && (
953 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
954 Webhooks
955 </SidebarLink>
956 )}
957 </div>
Project settings move into the sidebar, grouped, with a heading per page958 </nav>
959 );
960}
961
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas962/** Your own settings, drilled into from Your settings: one page each. */
963function AccountSettingsMenu({ username }: { username: string }) {
964 const link = (page: AccountSettingsPage, icon: ReactNode) => (
965 <SidebarLink to={`/settings/${page}`} icon={icon}>
966 {ACCOUNT_SETTINGS[page].title}
967 </SidebarLink>
Account settings in the same shape as a workspace's968 );
969 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look970 <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 quotas971 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look972 <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 quotas973 {link("profile", <CircleUserRound size={15} />)}
974 {link("emails", <Mail size={15} />)}
975 {link("invites", <Ticket size={15} />)}
976 </div>
977 <Rule />
978 <div className="space-y-px">
979 {link("keys", <Fingerprint size={15} />)}
980 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look981 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas982 <Rule />
983 <div className="space-y-px">
984 {link("github", <GithubMark className="size-[15px]" />)}
985 {link("applications", <Plug size={15} />)}
986 </div>
987 <Rule />
988 <div className="space-y-px">
989 {link("security-log", <History size={15} />)}
990 </div>
Account settings in the same shape as a workspace's991 </nav>
992 );
993}
994
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put995/**
996 * Signing in and signing up, in place of the account for a visitor. Signing
997 * in brings them back to the page they are on.
998 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas999/** Help, status and the fine print under a visitor's sign-in buttons. */
1000function VisitorLinks() {
1001 const status = useSiteStatus();
1002 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1003 const dot = (
1004 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1005 ·
1006 </span>
1007 );
1008 return (
1009 // Two short rows, so a narrow sidebar never starts a line with a dot.
1010 <nav aria-label="About g1t" className="pt-0.5">
1011 <div className="flex items-center gap-0.5">
1012 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1013 <StatusDot state={status?.overall.state ?? null} />
1014 Status
1015 </a>
1016 {dot}
1017 <Link to="/support" className={quiet}>
1018 Support
1019 </Link>
1020 </div>
1021 <div className="flex items-center gap-0.5">
1022 {LEGAL_LINKS.map(([label, to], index) => (
1023 <span key={to} className="flex items-center gap-0.5">
1024 {index > 0 && dot}
1025 <Link to={to} className={quiet}>
1026 {label}
1027 </Link>
1028 </span>
1029 ))}
1030 </div>
1031 </nav>
1032 );
1033}
1034
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1035function VisitorPanel() {
1036 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1037 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1038 return (
1039 <div className="space-y-2">
1040 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1041 <div className="grid grid-cols-2 gap-2">
1042 <Link
1043 to={withNext("/login", pathname + search)}
1044 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"
1045 >
1046 <LogIn size={14} />
1047 Sign in
1048 </Link>
1049 <Link
1050 to={withNext("/register", pathname + search)}
1051 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1052 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1053 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1054 </Link>
1055 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1056 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1057 </div>
1058 );
1059}
1060
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1061function Sidebar({
1062 user,
1063 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1064 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1065 onFind,
1066 onClose,
1067}: {
1068 user: User | null;
1069 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1070 /** The page is a 404: the address names nothing the viewer can see. */
1071 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1072 onFind: () => void;
1073 /** In the sheet on a small screen: closing it, at the end of the top row. */
1074 onClose?: () => void;
1075}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1076 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1077 const { pathname } = useLocation();
1078 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1079 // Where the sidebar is drilled to follows the page being gone to, so it
1080 // moves as the link is followed, not once the page arrives.
1081 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1082 // On a 404, the address is not a place: no list is drilled into from it,
1083 // except the project's own when the project is real and visible and only
1084 // something inside it is missing (a file, a commit, an issue).
1085 const lost = missing && going == null;
1086 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1087 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1088 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1089 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1090 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1091 const reserved = new Set(["settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth", "policies", "security", "support", "status", "invite", "inbox", ".well-known"]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1092 // An invitation to a repository is answered before its menu means anything.
1093 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1094 repoPath != null &&
1095 !reserved.has(repoPath[1]) &&
1096 repoPath[2] !== "-" &&
1097 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1098 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1099 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1100 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1101 // The repository the menu is for: the one loaded if it is the one being
1102 // gone to, else what the address says, at once.
1103 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 look1104 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1105 ? sameRepo(active, targetRepo)
1106 ? active
1107 : {
1108 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1109 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1110 }
1111 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1112 // The viewer's role there, from the repository's page once it has loaded.
1113 const page = useRouteLoaderData("routes/repo/layout") as
1114 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1115 | undefined;
1116 const menuRepo: MenuRepo | null =
1117 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1118 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1119 : guessed;
1120
1121 // The way from the main list to the one shown.
1122 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1123 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1124 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1125 } else if (ws && inSettings) {
1126 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1127 } else if (menuRepo) {
1128 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1129 // Out of a project: to its workspace's projects when they are yours.
1130 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1131 const back = !user
1132 ? { to: "/explore", label: "Explore" }
1133 : home
1134 ? { to: `/${home.slug}`, label: "All projects" }
1135 : { to: "/", label: "Mission control" };
1136 trail.push({
1137 key,
1138 node: (
1139 <RepoMenu
1140 repo={menuRepo}
1141 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1142 back={back}
1143 />
1144 ),
1145 });
1146 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1147 }
1148
Agents as a team: lifecycle, merge queue, billing and a new shell1149 return (
1150 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1151 {/* The same height and rule as the top bar, so the two read as one line. */}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1152 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2 pl-2.5">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1153 {user ? (
1154 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1155 {/* The 1 alone beside the workspace: a square hover the height of the
1156 switcher, the mark as tall as the workspace avatar. */}
1157 <Link to="/" aria-label="g1t home" className="flex size-9 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-raised">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1158 <Mark tight className="h-4 w-auto" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1159 </Link>
1160 <span className="shrink-0 text-line-strong" aria-hidden="true">
1161 /
1162 </span>
1163 <WorkspaceSwitcher user={user} shell={shell} />
1164 </>
1165 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1166 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1167 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1168 </Link>
1169 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1170 {onClose && (
1171 <button
1172 type="button"
1173 aria-label="Close menu"
1174 onClick={onClose}
1175 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1176 >
1177 <X size={16} />
1178 </button>
1179 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1180 </div>
A Vercel-like sidebar and a new mark1181 <div className="px-2 pt-3">
1182 <button
1183 type="button"
1184 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1185 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 mark1186 >
1187 <Search size={14} />
1188 <span className="grow text-left">Find…</span>
1189 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1190 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1191 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1192 <Drill trail={trail} />
1193 {/* The account, or signing in, as one row at the very bottom. */}
1194 <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 put1195 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1196 </div>
1197 </div>
1198 );
1199}
1200
1201/** Words for the sections a path can end in. */
1202const SECTIONS: Record<string, string> = {
1203 issues: "Issues",
1204 pulls: "Pull requests",
1205 queue: "Merge queue",
1206 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1207 plans: "Outcomes",
1208 actions: "Workflows",
1209 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1210 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1211 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1212 access: "Access",
1213 invitations: "Invitation",
1214 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1215 branches: "Branches",
1216 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1217 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1218 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1219 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1220 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1221 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1222 people: "Members",
A Vercel-like sidebar and a new mark1223 tokens: "Access tokens",
1224 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1225 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1226 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1227 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1228 domains: "Domains",
1229 guardrails: "Guardrails",
1230 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1231 tree: "Files",
1232 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1233 agents: "Agents",
1234 context: "Context",
1235 memory: "Memory",
1236 security: "Security",
1237 packages: "Packages",
1238 runners: "Runners",
1239 workflows: "Workflows",
1240 observability: "Observability",
1241 insights: "Insights",
1242 sessions: "Sessions",
A Vercel-like sidebar and a new mark1243};
1244
1245/** 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 quotas1246function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1247 // A 404 names nothing from the address, unless the project is real and
1248 // visible and only something inside it is missing.
1249 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1250 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1251 return <span className="text-sm font-medium">Not found</span>;
1252 }
A Vercel-like sidebar and a new mark1253 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1254 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1255 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 quotas1256 // Your settings: Settings / Emails.
1257 if (parts[0] === "settings") {
1258 const page = accountSettingsPage(pathname);
1259 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1260 return (
1261 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1262 {trail.map((crumb, index) => (
1263 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1264 {index > 0 && <span className="text-line-strong">/</span>}
1265 <Link
1266 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1267 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1268 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1269 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1270 }`}
1271 >
1272 {crumb.label}
1273 </Link>
1274 </span>
1275 ))}
1276 </nav>
1277 );
1278 }
A Vercel-like sidebar and a new mark1279 if (reserved.includes(parts[0]!)) {
1280 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1281 settings: "Account",
A Vercel-like sidebar and a new mark1282 explore: "Explore",
Projects live: fixes from walking them end to end1283 new: "New project",
A Vercel-like sidebar and a new mark1284 search: "Search",
1285 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1286 policies: "Policies",
1287 security: "Security",
1288 support: "Support",
1289 status: "Status",
1290 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1291 inbox: "Inbox",
A Vercel-like sidebar and a new mark1292 };
1293 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1294 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1295 // A person's profile, by their handle.
1296 if (parts[0] === "u" && parts[1]) {
1297 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1298 }
A Vercel-like sidebar and a new mark1299 const [owner, second, third, fourth] = parts;
1300 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1301 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1302 const page = `/${owner}/-/${third}`;
1303 if (third && SETTINGS_PAGE.test(page)) {
1304 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1305 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1306 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1307 } else if (second) {
1308 const repo = `/${owner}/${second}`;
1309 trail.push({ label: second, to: repo, mono: true });
1310 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1311 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1312 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 deployments1313 else if (third && SECTIONS[third]) {
1314 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1315 // A settings page names which one: Settings / Secrets and variables.
1316 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1317 trail.push({ label: fourth === "branches" ? "Branches and merging" : SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1318 }
1319 }
A Vercel-like sidebar and a new mark1320 }
1321 return (
1322 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1323 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1324 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1325 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1326 <Link
A Vercel-like sidebar and a new mark1327 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1328 prefetch="intent"
A Vercel-like sidebar and a new mark1329 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1330 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1331 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1332 >
A Vercel-like sidebar and a new mark1333 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1334 </Link>
A Vercel-like sidebar and a new mark1335 </span>
1336 ))}
1337 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1338 );
1339}
1340
Search across all of g1t, Explore, and a command palette1341type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1342
1343/** 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 look1344function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1345 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1346 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1347 { 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 step1348 ...(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-agent1349 ? [{ 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 step1350 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1351 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1352 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1353 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1354 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1355 { 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 quotas1356 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1357 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1358 .filter((page) => page !== "profile")
1359 .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 shell1360 ];
1361 const repo = shell.repo;
1362 if (repo) {
1363 const base = `/${repo.namespace}/${repo.name}`;
1364 const name = `${repo.namespace}/${repo.name}`;
1365 commands.unshift(
Projects: what a workspace builds and runs, first on every page1366 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1367 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1368 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1369 { 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 step1370 ...(repo.member
1371 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1372 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1373 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1374 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1375 ...(repo.member
1376 ? [
1377 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1378 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1379 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1380 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1381 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1382 ]
1383 : []),
1384 );
1385 }
1386 for (const membership of user.workspaces ?? []) {
1387 commands.push(
Workspace names and icons, and a component kit for every control1388 { 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 request1389 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1390 { 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 settings1391 { 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 look1392 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1393 );
1394 }
1395 for (const listed of shell.repos) {
1396 commands.push({
Projects: what a workspace builds and runs, first on every page1397 label: listed.title ?? listed.name,
1398 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1399 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1400 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1401 });
1402 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1403 // "Put an agent on …": one per project, after the projects themselves.
1404 for (const listed of shell.repos) {
1405 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1406 commands.push({
1407 label: `Put an agent on ${listed.title ?? listed.name}`,
1408 hint: `${listed.namespace}/${listed.name}`,
1409 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1410 icon: <Sparkles size={15} />,
1411 });
1412 }
Agents as a team: lifecycle, merge queue, billing and a new shell1413 return commands;
1414}
1415
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1416/** 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 look1417function 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 put1418 const commands: Command[] = [];
1419 const repo = shell.repo;
1420 if (repo) {
1421 const base = `/${repo.namespace}/${repo.name}`;
1422 const name = `${repo.namespace}/${repo.name}`;
1423 commands.push(
1424 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1425 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1426 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1427 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1428 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1429 );
1430 }
1431 commands.push(
1432 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1433 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1434 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1435 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1436 { 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 look1437 { 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 put1438 );
1439 return commands;
1440}
1441
Agents as a team: lifecycle, merge queue, billing and a new shell1442/**
1443 * A bar across the top of the page while the next one loads, as GitHub
1444 * has: it appears at once, creeps towards the end while waiting, then fills
1445 * and fades when the page arrives.
1446 */
1447export function Progress() {
1448 const navigation = useNavigation();
1449 const busy = navigation.state !== "idle";
1450 const [width, setWidth] = useState(0);
1451 const [visible, setVisible] = useState(false);
1452 useEffect(() => {
1453 if (busy) {
1454 setVisible(true);
1455 setWidth(12);
1456 // Slows as it goes, never quite arriving until the page does.
1457 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1458 return () => clearInterval(timer);
1459 }
1460 setWidth((now) => (now > 0 ? 100 : 0));
1461 const fade = setTimeout(() => setVisible(false), 250);
1462 const reset = setTimeout(() => setWidth(0), 550);
1463 return () => {
1464 clearTimeout(fade);
1465 clearTimeout(reset);
1466 };
1467 }, [busy]);
1468 return (
1469 <div
1470 aria-hidden="true"
1471 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1472 visible ? "opacity-100" : "opacity-0"
1473 }`}
1474 >
1475 <div
1476 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1477 style={{ width: `${width}%` }}
1478 />
1479 </div>
1480 );
1481}
1482
1483/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1484 * The app: a sidebar with the workspace, its repositories and the sections
1485 * of the one being looked at; a slim bar with search and the account; and
1486 * the page. A visitor who is not signed in gets the same frame, with
1487 * Explore and Search in place of the workspace, and signing in in place of
1488 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1489 */
1490export function AppShell({
1491 user,
1492 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1493 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1494 banner,
1495 children,
1496}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1497 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1498 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1499 /** The page is a 404 (see Sidebar). */
1500 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1501 banner?: ReactNode;
1502 children: ReactNode;
1503}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1504 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1505 const [drawer, setDrawer] = useState(false);
1506 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1507 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1508 const signUpLabel = useSignUpCopy().primary;
1509 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1510
1511 // A new page closes the drawer on small screens.
1512 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1513 // Mission control's code, fetched while the browser is idle, so going
1514 // home never waits on it (routes/home.tsx).
1515 useEffect(() => {
1516 if (!user) return;
1517 const load = () => void import("./mission-control").catch(() => undefined);
1518 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1519 if (idle) idle(load);
1520 else setTimeout(load, 1500);
1521 }, [user]);
Search across all of g1t, Explore, and a command palette1522 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1523
1524 return (
1525 <div className="min-h-screen">
1526 <Progress />
1527 <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 quotas1528 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1529 </aside>
1530 {drawer && (
1531 <div className="fixed inset-0 z-50 lg:hidden">
1532 <button
1533 type="button"
1534 aria-label="Close menu"
1535 className="absolute inset-0 bg-black/60"
1536 onClick={() => setDrawer(false)}
1537 />
1538 <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 quotas1539 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1540 </aside>
1541 </div>
1542 )}
1543
Polish: phones, copy boxes, the plan page, the landing page, a real glide1544 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1545 <header className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-line bg-bg/85 px-4 backdrop-blur sm:px-6">
Agents as a team: lifecycle, merge queue, billing and a new shell1546 <button
1547 type="button"
1548 aria-label="Open menu"
1549 onClick={() => setDrawer(true)}
1550 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1551 >
1552 <Menu size={18} />
1553 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1554 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Search across all of g1t, Explore, and a command palette1555 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1556 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1557 <input
1558 name="q"
1559 {...notACredential()}
1560 placeholder="Search g1t"
1561 aria-label="Search g1t"
1562 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"
1563 />
1564 <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">
1565 ⌘K
1566 </kbd>
1567 </Form>
1568 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1569 <a
1570 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1571 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 mark1572 >
1573 Docs
1574 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1575 {user && (
1576 <>
1577 <AskAi />
1578 <InboxBell counts={shell.inbox ?? null} />
1579 </>
1580 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1581 {!user ? (
1582 // The sidebar has these too, but on a phone it is folded away.
1583 <Link
1584 to={withNext("/login", here)}
1585 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"
1586 >
1587 Sign in
1588 </Link>
1589 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1590 <DropdownMenu>
1591 <DropdownMenuTrigger
1592 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1593 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 shell1594 >
A Vercel-like sidebar and a new mark1595 <Plus size={14} />
1596 New
Agents as a team: lifecycle, merge queue, billing and a new shell1597 </DropdownMenuTrigger>
1598 <DropdownMenuContent align="end">
1599 {shell.repo && (
1600 <DropdownMenuItem asChild>
1601 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1602 <CircleDot />
1603 New issue
1604 </Link>
1605 </DropdownMenuItem>
1606 )}
1607 {shell.repo?.member && (
1608 <DropdownMenuItem asChild>
1609 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1610 <ListTree />
1611 Plan work
1612 </Link>
1613 </DropdownMenuItem>
1614 )}
1615 <DropdownMenuItem asChild>
1616 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1617 <Box />
1618 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1619 </Link>
1620 </DropdownMenuItem>
1621 <DropdownMenuItem asChild>
1622 <Link to="/workspaces/new">
1623 <Users />
1624 New workspace
1625 </Link>
1626 </DropdownMenuItem>
1627 </DropdownMenuContent>
1628 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1629 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1630 </div>
1631 </header>
1632 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1633 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1634 </div>
Search across all of g1t, Explore, and a command palette1635 <CommandPalette
1636 open={palette}
1637 onOpenChange={setPalette}
1638 commands={commands}
1639 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1640 />
Agents as a team: lifecycle, merge queue, billing and a new shell1641 </div>
1642 );
1643}