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

1,402 lines54,486 bytesCodeBlame

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

Agents as a team: lifecycle, merge queue, billing and a new shell1import {
Observability gets its own page; tabs never overflow2 Activity,
A flat project menu with Code first, views as tabs, and Stripe for every owner3 GanttChart,
4 KanbanSquare,
5 Package,
6 Sparkles,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request7 BarChart3,
Agents as a team: lifecycle, merge queue, billing and a new shell8 BookMarked,
9 BookOpen,
10 Check,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11 ChevronLeft,
12 ChevronRight,
Agents as a team: lifecycle, merge queue, billing and a new shell13 ChevronsUpDown,
A Vercel-like sidebar and a new mark14 Ellipsis,
Agents as a team: lifecycle, merge queue, billing and a new shell15 CircleDot,
16 Code2,
17 Compass,
18 CreditCard,
19 GitPullRequest,
20 History,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21 House,
Secrets and variables: one list, rows per environment, for workflows and deployments22 Fingerprint,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look23 Gauge,
Agents as a team: lifecycle, merge queue, billing and a new shell24 KeyRound,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25 LifeBuoy,
Projects: what a workspace builds and runs, first on every page26 Box,
27 LayoutGrid,
Agents as a team: lifecycle, merge queue, billing and a new shell28 ListTree,
29 Lock,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put30 LogIn,
Agents as a team: lifecycle, merge queue, billing and a new shell31 LogOut,
32 Menu,
33 Plus,
34 Search,
Integrations: your own model provider, alerts that open issues, tickets agents read35 Plug,
Agents as a team: lifecycle, merge queue, billing and a new shell36 Settings,
37 Users,
Webhooks: every event, to your own addresses, signed and retried38 Webhook,
Project settings move into the sidebar, grouped, with a heading per page39 Globe,
40 GitBranch,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs41 PlayCircle,
Pull requests say where they are live; the project menu shows what is coming42 Bot,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains43 Brain,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API44 Network,
Pull requests say where they are live; the project menu shows what is coming45 ShieldCheck,
Deployments: a preview for every pull request, production on g1t.page46 Rocket,
Agents as a team: lifecycle, merge queue, billing and a new shell47 X,
48} from "lucide-react";
49import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look50import { Form, Link, NavLink, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell51
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look52import type { Abilities, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell53
Search across all of g1t, Explore, and a command palette54import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look55import { LegalRow } from "./footer";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put56import { Logo, Mark } from "./logo";
Search across all of g1t, Explore, and a command palette57import { Avatar, notACredential } from "./ui";
Agents as a team: lifecycle, merge queue, billing and a new shell58import {
59 DropdownMenu,
60 DropdownMenuContent,
61 DropdownMenuItem,
62 DropdownMenuLabel,
63 DropdownMenuSeparator,
64 DropdownMenuTrigger,
65} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales66import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67import { 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 put68import { VISITOR_LINKS, projectPages } from "../lib/chrome";
69import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look70import { useSignUpCopy } from "../lib/registration";
Agents as a team: lifecycle, merge queue, billing and a new shell71
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put72/**
73 * What the sidebar needs, worked out by the root loader. For a visitor who
74 * is not signed in there is no workspace, no projects and no usage: only
75 * the project being looked at, if they can see it.
76 */
Agents as a team: lifecycle, merge queue, billing and a new shell77export type ShellData = {
78 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control79 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page80 /** Its projects, by name: `name` is the slug in their address. */
81 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 /** Repositories shared with them in workspaces they do not belong to. */
83 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page84 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell85 repo: {
86 namespace: string;
87 name: string;
88 member: boolean;
89 issues: number;
90 pulls: number;
91 } | null;
Billing accounts, terms and enterprises; g1t is no longer free92 /** Where the workspace stands against its usage limit, if billing is on. */
93 limit: {
94 exposureMicros: number;
95 ceilingMicros: number | null;
96 state: "ok" | "warning" | "stopped";
97 comped: boolean;
98 } | null;
Free while g1t is being built out; agents can check out their own forks99 /** Whether g1t charges nothing for now, while it is being built out. */
100 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request101 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy102 /** This month's usage: charged, or at cost while g1t is free. */
103 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look104 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
105 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Agents as a team: lifecycle, merge queue, billing and a new shell106};
107
108function SidebarLink({
109 to,
110 icon,
111 end,
112 count,
113 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell115 children,
116}: {
117 to: string;
118 icon: ReactNode;
119 end?: boolean;
120 count?: number;
Projects: what a workspace builds and runs, first on every page121 /** Other path prefixes under which this link is the current one. */
122 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123 /** It opens a list of its own: a chevron says so, always or on hover. */
124 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell125 children: ReactNode;
126}) {
127 const { pathname } = useLocation();
128 return (
129 <NavLink
130 to={to}
131 end={end}
132 prefetch="intent"
133 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page134 const current =
135 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark136 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 shell137 current
138 ? "bg-raised font-medium text-fg"
139 : isPending
140 ? "bg-raised/60 text-fg"
141 : "text-muted hover:bg-raised/60 hover:text-fg"
142 }`;
143 }}
144 >
145 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
146 <span className="min-w-0 grow truncate">{children}</span>
147 {count != null && count > 0 && (
148 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
149 {count}
150 </span>
151 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look152 {drill && (
153 <ChevronRight
154 size={14}
155 aria-hidden="true"
156 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
157 />
158 )}
Agents as a team: lifecycle, merge queue, billing and a new shell159 </NavLink>
160 );
161}
162
Pull requests say where they are live; the project menu shows what is coming163/**
164 * Something a project will have, shown where it will live so people can
165 * see where g1t is going. Not a link: nothing is behind it yet.
166 */
167function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
168 return (
169 <div
170 title={about}
171 aria-disabled="true"
172 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
173 >
174 <span className="shrink-0 opacity-70">{icon}</span>
175 <span className="grow truncate">{children}</span>
176 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
177 Soon
178 </span>
179 </div>
180 );
181}
182
The project menu opens by section, and the workspace has its own panel183/**
A flat project menu with Code first, views as tabs, and Stripe for every owner184 * A page that is coming, as a link to its roadmap page: faint, with Soon,
185 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel186 */
A flat project menu with Code first, views as tabs, and Stripe for every owner187function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel188 to,
189 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner190 icon,
191 about,
The project menu opens by section, and the workspace has its own panel192 children,
193}: {
194 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner195 also?: string[];
196 icon: ReactNode;
197 about: string;
The project menu opens by section, and the workspace has its own panel198 children: ReactNode;
199}) {
200 const { pathname } = useLocation();
201 return (
202 <NavLink
203 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner204 title={about}
The project menu opens by section, and the workspace has its own panel205 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner206 className={({ isActive }) => {
207 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
208 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
209 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 panel210 }`;
211 }}
212 >
A flat project menu with Code first, views as tabs, and Stripe for every owner213 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel214 <span className="min-w-0 grow truncate">{children}</span>
215 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
216 Soon
217 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales218 </NavLink>
The project menu opens by section, and the workspace has its own panel219 );
220}
221
A flat project menu with Code first, views as tabs, and Stripe for every owner222/** Icons for what spans projects. */
223const WORKSPACE_ICONS: Record<string, ReactNode> = {
224 board: <KanbanSquare size={15} />,
225 roadmap: <GanttChart size={15} />,
226 packages: <Package size={15} />,
227 fleet: <Bot size={15} />,
228};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales229
A flat project menu with Code first, views as tabs, and Stripe for every owner230/** The Soon pages of a section, as paths, so its link is current on them. */
231function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
232 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales233}
234
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look235function SidebarGroup({
236 title,
237 action,
238 className = "",
239 children,
240}: {
241 title: string;
242 action?: ReactNode;
243 className?: string;
244 children: ReactNode;
245}) {
Agents as a team: lifecycle, merge queue, billing and a new shell246 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look247 <section className={className}>
A Vercel-like sidebar and a new mark248 <div className="mb-1 flex h-6 items-center justify-between px-2">
249 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell250 {action}
251 </div>
252 <div className="space-y-px">{children}</div>
253 </section>
254 );
255}
256
Workspace names and icons, and a component kit for every control257/** What a workspace is called where people read it: its display name, else its slug. */
258function displayName(membership: Membership): string {
259 return membership.name?.trim() || membership.slug;
260}
261
Agents as a team: lifecycle, merge queue, billing and a new shell262function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control263 const workspace = shell.workspace;
264 const current = workspace?.slug;
265 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell266 return (
267 <DropdownMenu>
Workspace names and icons, and a component kit for every control268 {/* The name goes to the workspace; only the arrows switch it. The
269 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark270 <Link
271 to={current ? `/${current}` : "/workspaces/new"}
Workspace names and icons, and a component kit for every control272 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look273 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 mark274 >
Workspace names and icons, and a component kit for every control275 {workspace ? (
276 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell277 ) : (
Workspace names and icons, and a component kit for every control278 <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 shell279 )}
Workspace names and icons, and a component kit for every control280 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark281 </Link>
282 <DropdownMenuTrigger
283 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 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 mark285 >
286 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell287 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control288 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell289 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
290 {(user.workspaces ?? []).map((membership) => (
291 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control292 <Link to={`/${membership.slug}`} title={displayName(membership)}>
293 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
294 <span className="flex min-w-0 grow flex-col leading-tight">
295 <span className="truncate">{displayName(membership)}</span>
296 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
297 </span>
298 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
299 {membership.slug === current ? (
300 <Check className="shrink-0 text-accent" />
301 ) : (
302 <span className="size-4 shrink-0" aria-hidden="true" />
303 )}
Agents as a team: lifecycle, merge queue, billing and a new shell304 </Link>
305 </DropdownMenuItem>
306 ))}
307 <DropdownMenuSeparator />
308 <DropdownMenuItem asChild>
309 <Link to="/workspaces/new">
310 <Plus />
311 New workspace
312 </Link>
313 </DropdownMenuItem>
314 </DropdownMenuContent>
315 </DropdownMenu>
A Vercel-like sidebar and a new mark316 );
317}
318
319function AccountMenu({ user }: { user: User }) {
320 const submit = useSubmit();
321 return (
322 <DropdownMenu>
323 <DropdownMenuTrigger className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised data-[state=open]:bg-raised">
Workspace names and icons, and a component kit for every control324 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark325 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
326 <Ellipsis size={15} className="shrink-0 text-faint" />
327 </DropdownMenuTrigger>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look328 <DropdownMenuContent align="start" side="top" className="w-64">
A Vercel-like sidebar and a new mark329 <DropdownMenuLabel>
330 Signed in as <span className="font-mono font-medium text-fg">{user.username}</span>
331 </DropdownMenuLabel>
332 <DropdownMenuSeparator />
333 <DropdownMenuItem asChild>
334 <Link to="/settings">
335 <Settings />
336 Your settings
337 </Link>
338 </DropdownMenuItem>
339 <DropdownMenuItem asChild>
340 <a href="https://docs.g1t.sh/">
341 <BookOpen />
342 Documentation
343 </a>
344 </DropdownMenuItem>
345 <DropdownMenuSeparator />
346 {/* Submitted from here: the menu closes on select, and a button
347 that has left the page cannot submit a form. */}
348 <DropdownMenuItem onSelect={() => submit(null, { method: "post", action: "/logout" })}>
349 <LogOut />
350 Sign out
351 </DropdownMenuItem>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look352 {/* The site footer's row, slim: the app has no footer of its own.
353 Rendered only while the menu is open, so status is fetched then. */}
354 <DropdownMenuSeparator />
355 <div className="px-1.5 pt-1 pb-1.5">
356 <LegalRow user={user} compact />
357 </div>
A Vercel-like sidebar and a new mark358 </DropdownMenuContent>
359 </DropdownMenu>
360 );
361}
362
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look363/**
364 * A workspace's settings pages, which the sidebar drills into: who belongs,
365 * what it pays, and its record. What it builds and runs with (secrets,
366 * integrations, webhooks, guardrails) sits in the main list.
367 */
368const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|repositories|tokens|billing|audit)(\/|$)/;
369/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page370const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings371
Polish: phones, copy boxes, the plan page, the landing page, a real glide372/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look373 * The sidebar's lists are a stack, the way a phone pushes a screen: one
374 * drilled into slides in from the right over the full width, while the one
375 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide376 * ease-out, the fade quicker than the move so the two never blur together.
377 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide378 */
Sidebar: no settings flash on the way into a repository, and a better slide379const LAYER =
Sidebar: the panels really slide380 "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 look381/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide382const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings383
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look384/** One list in the stack: what it is, and its rows. */
385type Level = { key: string; node: ReactNode };
386
387/** True once the page has hydrated, so what the server drew never animates in. */
388function useHydrated(): boolean {
389 const [hydrated, setHydrated] = useState(false);
390 useEffect(() => setHydrated(true), []);
391 return hydrated;
392}
393
394/**
395 * Drilling in and out of the sidebar's lists. `trail` is the way from the
396 * main list to the one shown, worked out from the address, so a link
397 * straight to a settings page opens the sidebar already drilled in, and
398 * leaving it comes back out. Lists deeper than the one shown stay in
399 * place, off to the right, so going back slides them away rather than
400 * dropping them. Moving to a list at the same depth (a workspace's settings
401 * to a project) fades the new one in where it is.
402 */
403function Drill({ trail }: { trail: Level[] }) {
404 const depth = trail.length - 1;
405 const hydrated = useHydrated();
406 const stack = useRef<Level[]>(trail);
407 const before = stack.current;
408 const within = trail.every((level, index) => before[index]?.key === level.key);
409 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
410 const levels = stack.current;
411 // What was showing last time, to tell drilling in from moving across.
412 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
413 useEffect(() => {
414 shown.current = { depth, keys: levels.map((level) => level.key) };
415 });
A catalogue of model providers, and settings that feel like settings416 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look417 <div className="relative min-h-0 grow overflow-hidden">
418 {levels.map((level, index) => {
419 const current = index === depth;
420 const place =
421 index < depth
422 ? "pointer-events-none -translate-x-1/4 opacity-0"
423 : current
424 ? "translate-x-0 opacity-100"
425 : "pointer-events-none translate-x-full opacity-0";
426 // A list that was not there before arrives: from the right when
427 // drilling in, in place when moving across.
428 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
429 const enter = arriving
430 ? depth > shown.current.depth
431 ? "starting:translate-x-full starting:opacity-0"
432 : "starting:opacity-0"
433 : "";
434 return (
435 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
436 {level.node}
437 </div>
438 );
439 })}
440 </div>
441 );
442}
443
444/**
445 * The row at the top of a list drilled into: the list's name, which goes
446 * back out, and what it belongs to, faint at the end.
447 */
448function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
449 return (
450 <Link
451 to={to}
452 prefetch="intent"
453 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 settings454 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look455 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
456 <span className="min-w-0 shrink-0 truncate">{label}</span>
457 {context && (
458 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
459 )}
460 </Link>
461 );
462}
463
464/** A thin rule between groups of a list. */
465function Rule() {
466 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
467}
468
469/**
470 * The main list: where you go, the projects, what the workspace builds and
471 * runs with across them, then its usage, support and settings.
472 */
473function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
474 const ws = shell.workspace;
475 const active = shell.repo;
476 // The repository being looked at is listed even when it is someone else's.
477 const listed =
478 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
479 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
480 : shell.repos;
481 // A visitor browses: no workspace, no projects of their own.
482 if (!user) {
483 return (
484 <nav aria-label="g1t" className={PANEL}>
485 <div className="mt-3 space-y-px">
486 {VISITOR_LINKS.map((link) => (
487 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
488 {link.label}
489 </SidebarLink>
490 ))}
491 </div>
492 </nav>
493 );
494 }
495 return (
496 <nav aria-label="g1t" className={PANEL}>
497 <div className="mt-3 space-y-px">
498 <SidebarLink to="/" end icon={<House size={15} />}>
499 Mission control
500 </SidebarLink>
501 {ws && (
502 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutGrid size={15} />}>
503 Overview
504 </SidebarLink>
505 )}
506 <SidebarLink to="/explore" icon={<Compass size={15} />}>
507 Explore
508 </SidebarLink>
509 </div>
510
511 <Rule />
512 <SidebarGroup
513 title="Projects"
514 action={
515 ws && (
516 <Link
517 to={`/new?workspace=${ws.slug}`}
518 aria-label="New project"
519 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
520 >
521 <Plus size={13} />
522 </Link>
523 )
524 }
A catalogue of model providers, and settings that feel like settings525 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look526 {listed.length === 0 && <p className="px-2 py-1 text-xs text-faint">None yet.</p>}
527 {listed.map((repo) => (
528 <SidebarLink
529 key={`${repo.namespace}/${repo.name}`}
530 to={`/${repo.namespace}/${repo.name}`}
531 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
532 drill="hover"
533 >
534 {repo.namespace !== ws?.slug && <span className="font-mono text-faint">{repo.namespace}/</span>}
535 {"title" in repo && repo.title ? repo.title : repo.name}
536 </SidebarLink>
537 ))}
538 </SidebarGroup>
539 {(shell.shared ?? []).length > 0 && (
540 <SidebarGroup title="Shared with you" className="mt-3">
541 {(shell.shared ?? []).map((repo) => (
542 <SidebarLink
543 key={`${repo.namespace}/${repo.name}`}
544 to={`/${repo.namespace}/${repo.name}`}
545 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
546 drill="hover"
547 >
548 <span className="font-mono text-faint">{repo.namespace}/</span>
549 {repo.name}
550 </SidebarLink>
551 ))}
552 </SidebarGroup>
553 )}
554
555 {ws && (
556 <>
557 <Rule />
558 <div className="space-y-px">
559 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
560 Agent fleet
561 </SidebarLink>
562 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
563 Context
564 </SidebarLink>
565 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
566 Memory
567 </SidebarLink>
568 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
569 Security
570 </SidebarLink>
571 <SidebarLink to={`/${ws.slug}/-/guardrails`} icon={<Gauge size={15} />}>
572 Guardrails
573 </SidebarLink>
574 <SidebarLink to={`/${ws.slug}/-/secrets`} icon={<Lock size={15} />}>
575 Secrets and variables
576 </SidebarLink>
577 <SidebarLink to={`/${ws.slug}/-/integrations`} icon={<Plug size={15} />}>
578 Integrations
579 </SidebarLink>
580 <SidebarLink to={`/${ws.slug}/-/webhooks`} icon={<Webhook size={15} />}>
581 Webhooks
582 </SidebarLink>
583 {roadmapIn("Workspace").map((item) => (
584 <SidebarSoonLink
585 key={item.key}
586 to={`/${ws.slug}/-/soon/${item.key}`}
587 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
588 about={item.summary}
589 >
590 {item.title === "Board" ? "Boards" : item.title}
591 </SidebarSoonLink>
592 ))}
593 </div>
594
595 <Rule />
596 <div className="space-y-px">
597 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
598 Usage
599 </SidebarLink>
600 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
601 Support
602 </SidebarLink>
603 {/* Who belongs, what it pays and its record: a list of their own. */}
604 <SidebarLink
605 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/people`}
606 icon={<Settings size={15} />}
607 drill
608 >
609 Settings
610 </SidebarLink>
611 </div>
612 </>
613 )}
614 </nav>
615 );
616}
617
618/** A workspace's settings, drilled into from Settings in the main list. */
619function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
620 return (
621 <nav aria-label="Workspace settings" className={PANEL}>
622 <BackRow to={`/${slug}`} label="Settings" context={slug} />
623 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings624 {owner && (
625 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
626 General
627 </SidebarLink>
628 )}
629 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />}>
630 Members
631 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look632 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
633 Repositories
634 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments635 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
636 Access tokens
637 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look638 </div>
639 <Rule />
640 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings641 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments642 Billing and plans
A catalogue of model providers, and settings that feel like settings643 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API644 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
645 Audit log
646 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look647 </div>
A catalogue of model providers, and settings that feel like settings648 </nav>
649 );
650}
651
Sidebar: no settings flash on the way into a repository, and a better slide652/**
653 * What the repository menu needs. Known from the address before the page's
654 * data arrives, so the menu is right from the first frame; the counts fill
655 * in once it does.
656 */
657type MenuRepo = {
658 namespace: string;
659 name: string;
660 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look661 /** Whether they see its settings; from the repository's page once it loads. */
662 settings?: boolean;
663 /** What they may do there, once the repository's page loads. */
664 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide665 issues?: number;
666 pulls?: number;
667};
668
669function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
670 return (
671 a != null &&
672 b != null &&
673 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
674 a.name.toLowerCase() === b.name.toLowerCase()
675 );
676}
A repository has its own sidebar, as settings do677
678/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look679 * A project's own list, drilled into while you are in it: everything about
680 * the project, running and its code, and nothing else, with the way back
681 * out to all of them.
A repository has its own sidebar, as settings do682 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put683function RepoMenu({
684 repo,
685 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look686 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put687}: {
688 repo: MenuRepo;
689 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look690 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
691 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put692}) {
A repository has its own sidebar, as settings do693 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 put694 // 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 look695 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do696 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look697 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
698 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner699 <NavLink
A repository has its own sidebar, as settings do700 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner701 end
702 title="Overview"
703 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look704 `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 owner705 }
A repository has its own sidebar, as settings do706 >
707 <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 page708 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do709 </span>
710 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
711 <span className="text-faint">{repo.namespace}/</span>
712 <span className="font-semibold text-fg">{repo.name}</span>
713 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner714 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look715 {/* A project's pages, in the order people use them. A page with more
716 than one view shows them as tabs across its top. */}
717 <Rule />
718 <div className="space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner719 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
720 Code
721 </SidebarLink>
722 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
723 Issues
724 </SidebarLink>
725 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
726 Pull requests
727 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains728 <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 owner729 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains730 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner731 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
732 Workflows
Projects: what a workspace builds and runs, first on every page733 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look734 </div>
735 <Rule />
736 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put737 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner738 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
739 Deployments
740 </SidebarLink>
741 ) : null}
Observability gets its own page; tabs never overflow742 <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.">
743 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner744 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put745 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API746 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
747 Security
748 </SidebarLink>
749 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner750 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
751 Insights
752 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look753 </div>
754 {(repo.settings ?? shows.has("settings")) && (
755 <>
756 <Rule />
757 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner758 Settings
759 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look760 </>
761 )}
A repository has its own sidebar, as settings do762 </nav>
763 );
764}
765
Project settings move into the sidebar, grouped, with a heading per page766/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look767 * A project's settings, drilled into from its list: what is about running
768 * it, then its agents, then its code and who can reach it. The way back
769 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page770 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look771function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page772 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look773 // Each page shows to the roles that can use it, once the role is known.
774 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
775 const running = shows("deployments");
776 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page777 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look778 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
779 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
780 {shows("") && (
781 <div className="mt-2 space-y-px">
782 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
783 General
784 </SidebarLink>
785 </div>
786 )}
787 {running && (
788 <>
789 {shows("") && <Rule />}
790 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
791 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
792 Deployments
793 </SidebarLink>
794 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
795 Domains
796 </SidebarLink>
797 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
798 Dependencies
799 </SidebarLink>
800 </div>
801 </>
802 )}
803 {agents && (
804 <>
805 <Rule />
806 <div className="space-y-px">
807 {shows("agents") && (
808 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
809 Agents
810 </SidebarLink>
811 )}
812 {shows("guardrails") && (
813 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
814 Guardrails
815 </SidebarLink>
816 )}
817 </div>
818 </>
819 )}
820 <Rule />
821 <div className="space-y-px">
822 {shows("repository") && (
823 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
824 Repository
825 </SidebarLink>
826 )}
827 {shows("access") && (
828 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
829 Access
830 </SidebarLink>
831 )}
832 {shows("branches") && (
833 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
834 Branches and merging
835 </SidebarLink>
836 )}
837 {shows("secrets") && (
838 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
839 Secrets and variables
840 </SidebarLink>
841 )}
842 {shows("webhooks") && (
843 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
844 Webhooks
845 </SidebarLink>
846 )}
847 </div>
Project settings move into the sidebar, grouped, with a heading per page848 </nav>
849 );
850}
851
Account settings in the same shape as a workspace's852/** Your own settings, as the sidebar shows them on the settings page. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look853function AccountSettingsMenu() {
Account settings in the same shape as a workspace's854 const { hash } = useLocation();
855 const item = (id: string, icon: ReactNode, label: string) => (
856 <Link
857 to={`/settings#${id}`}
858 className={`group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
859 hash === `#${id}` ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"
860 }`}
861 >
862 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
863 {label}
864 </Link>
865 );
866 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look867 <nav aria-label="Your settings" className={PANEL}>
868 <BackRow to="/" label="Your settings" />
869 <div className="mt-2 space-y-px">
Secrets and variables: one list, rows per environment, for workflows and deployments870 {item("ssh-keys", <Fingerprint size={15} />, "SSH keys")}
871 {item("tokens", <KeyRound size={15} />, "Access tokens")}
Account settings in the same shape as a workspace's872 {item("applications", <Plug size={15} />, "Connected applications")}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look873 </div>
Account settings in the same shape as a workspace's874 </nav>
875 );
876}
877
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put878/**
879 * Signing in and signing up, in place of the account for a visitor. Signing
880 * in brings them back to the page they are on.
881 */
882function VisitorPanel() {
883 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look884 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put885 return (
886 <div className="space-y-2">
887 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
888 <div className="grid grid-cols-2 gap-2">
889 <Link
890 to={withNext("/login", pathname + search)}
891 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"
892 >
893 <LogIn size={14} />
894 Sign in
895 </Link>
896 <Link
897 to={withNext("/register", pathname + search)}
898 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
899 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look900 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put901 </Link>
902 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look903 <nav aria-label="About g1t" className="flex flex-wrap justify-center gap-x-2 gap-y-0.5 px-1 pt-1 text-[0.6875rem] text-faint">
904 <Link to="/status" className="hover:text-fg">Status</Link>
905 <Link to="/support" className="hover:text-fg">Support</Link>
906 <Link to="/policies" className="hover:text-fg">Policies</Link>
907 <Link to="/policies/privacy" className="hover:text-fg">Privacy</Link>
908 <Link to="/security" className="hover:text-fg">Security</Link>
909 </nav>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put910 </div>
911 );
912}
913
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look914function Sidebar({
915 user,
916 shell,
917 onFind,
918 onClose,
919}: {
920 user: User | null;
921 shell: ShellData;
922 onFind: () => void;
923 /** In the sheet on a small screen: closing it, at the end of the top row. */
924 onClose?: () => void;
925}) {
Agents as a team: lifecycle, merge queue, billing and a new shell926 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings927 const { pathname } = useLocation();
928 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look929 // Where the sidebar is drilled to follows the page being gone to, so it
930 // moves as the link is followed, not once the page arrives.
931 const target = going ?? pathname;
932 const inSettings = ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
933 const inAccount = target === "/settings";
A repository has its own sidebar, as settings do934 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look935 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do936 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look937 const reserved = new Set(["settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth", "policies", "security", "support", "status", "invite", ".well-known"]);
938 // An invitation to a repository is answered before its menu means anything.
939 const inRepo =
940 repoPath != null && !reserved.has(repoPath[1]) && repoPath[2] !== "-" && !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target);
941 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page942 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide943 // The repository the menu is for: the one loaded if it is the one being
944 // gone to, else what the address says, at once.
945 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 look946 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide947 ? sameRepo(active, targetRepo)
948 ? active
949 : {
950 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put951 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide952 }
953 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look954 // The viewer's role there, from the repository's page once it has loaded.
955 const page = useRouteLoaderData("routes/repo/layout") as
956 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
957 | undefined;
958 const menuRepo: MenuRepo | null =
959 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
960 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
961 : guessed;
962
963 // The way from the main list to the one shown.
964 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
965 if (user && inAccount) {
966 trail.push({ key: "account", node: <AccountSettingsMenu /> });
967 } else if (ws && inSettings) {
968 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
969 } else if (menuRepo) {
970 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
971 // Out of a project: to its workspace's projects when they are yours.
972 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
973 const back = !user
974 ? { to: "/explore", label: "Explore" }
975 : home
976 ? { to: `/${home.slug}`, label: "All projects" }
977 : { to: "/", label: "Mission control" };
978 trail.push({
979 key,
980 node: (
981 <RepoMenu
982 repo={menuRepo}
983 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
984 back={back}
985 />
986 ),
987 });
988 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
989 }
990
Agents as a team: lifecycle, merge queue, billing and a new shell991 return (
992 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark993 {/* The same height and rule as the top bar, so the two read as one line. */}
Workspace names and icons, and a component kit for every control994 <div className="flex h-16 shrink-0 items-center gap-1 border-b border-line pr-2 pl-2.5">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put995 {user ? (
996 <>
997 <Link to="/" aria-label="g1t home" className="shrink-0 rounded-md p-1.5 hover:bg-raised">
998 <Mark className="size-6" />
999 </Link>
1000 <span className="shrink-0 text-line-strong" aria-hidden="true">
1001 /
1002 </span>
1003 <WorkspaceSwitcher user={user} shell={shell} />
1004 </>
1005 ) : (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1006 <Link to="/" aria-label="g1t home" className="mr-auto flex rounded-md px-1.5 py-1.5 hover:bg-raised">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1007 <Logo />
1008 </Link>
1009 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1010 {onClose && (
1011 <button
1012 type="button"
1013 aria-label="Close menu"
1014 onClick={onClose}
1015 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1016 >
1017 <X size={16} />
1018 </button>
1019 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1020 </div>
A Vercel-like sidebar and a new mark1021 <div className="px-2 pt-3">
1022 <button
1023 type="button"
1024 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1025 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 mark1026 >
1027 <Search size={14} />
1028 <span className="grow text-left">Find…</span>
1029 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1030 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1031 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1032 <Drill trail={trail} />
1033 {/* The account, or signing in, as one row at the very bottom. */}
1034 <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 put1035 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1036 </div>
1037 </div>
1038 );
1039}
1040
1041/** Words for the sections a path can end in. */
1042const SECTIONS: Record<string, string> = {
1043 issues: "Issues",
1044 pulls: "Pull requests",
1045 queue: "Merge queue",
1046 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1047 plans: "Outcomes",
1048 actions: "Workflows",
1049 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1050 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1051 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1052 access: "Access",
1053 invitations: "Invitation",
1054 repositories: "Repositories",
1055 branches: "Branches and merging",
Project dependencies: addresses, preview stacks, Affects, and agents who know1056 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1057 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1058 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1059 settings: "Settings",
A catalogue of model providers, and settings that feel like settings1060 people: "Members",
A Vercel-like sidebar and a new mark1061 tokens: "Access tokens",
1062 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1063 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1064 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1065 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1066 domains: "Domains",
1067 guardrails: "Guardrails",
1068 audit: "Audit log",
A Vercel-like sidebar and a new mark1069 tree: "Code",
1070 blob: "Code",
1071};
1072
1073/** Where the page is, as a trail of links: workspace / repository / section. */
1074function Breadcrumbs({ pathname }: { pathname: string }) {
1075 const parts = pathname.split("/").filter(Boolean);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1076 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite"];
A Vercel-like sidebar and a new mark1077 if (parts.length === 0) return <span className="text-sm font-medium">Mission control</span>;
1078 if (reserved.includes(parts[0]!)) {
1079 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1080 settings: "Account",
A Vercel-like sidebar and a new mark1081 explore: "Explore",
Projects live: fixes from walking them end to end1082 new: "New project",
A Vercel-like sidebar and a new mark1083 search: "Search",
1084 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1085 policies: "Policies",
1086 security: "Security",
1087 support: "Support",
1088 status: "Status",
1089 invite: "Invite",
A Vercel-like sidebar and a new mark1090 };
1091 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1092 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1093 // A person's profile, by their handle.
1094 if (parts[0] === "u" && parts[1]) {
1095 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1096 }
A Vercel-like sidebar and a new mark1097 const [owner, second, third, fourth] = parts;
1098 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1099 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1100 const page = `/${owner}/-/${third}`;
1101 if (third && SETTINGS_PAGE.test(page)) {
1102 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1103 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1104 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1105 } else if (second) {
1106 const repo = `/${owner}/${second}`;
1107 trail.push({ label: second, to: repo, mono: true });
1108 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1109 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1110 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 deployments1111 else if (third && SECTIONS[third]) {
1112 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1113 // A settings page names which one: Settings / Secrets and variables.
1114 if (third === "settings" && fourth && SECTIONS[fourth]) {
1115 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
1116 }
1117 }
A Vercel-like sidebar and a new mark1118 }
1119 return (
1120 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1121 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1122 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1123 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1124 <Link
A Vercel-like sidebar and a new mark1125 to={crumb.to}
1126 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1127 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1128 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1129 >
A Vercel-like sidebar and a new mark1130 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1131 </Link>
A Vercel-like sidebar and a new mark1132 </span>
1133 ))}
1134 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1135 );
1136}
1137
Search across all of g1t, Explore, and a command palette1138type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1139
1140/** 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 look1141function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1142 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1143 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1144 { label: "Mission control", to: "/", icon: <House size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1145 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1146 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page1147 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1148 { label: "New workspace", to: "/workspaces/new", icon: <Plus size={15} /> },
1149 { label: "Your settings", to: "/settings", icon: <Settings size={15} /> },
1150 ];
1151 const repo = shell.repo;
1152 if (repo) {
1153 const base = `/${repo.namespace}/${repo.name}`;
1154 const name = `${repo.namespace}/${repo.name}`;
1155 commands.unshift(
Projects: what a workspace builds and runs, first on every page1156 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1157 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1158 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1159 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
1160 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1161 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1162 ...(repo.member
1163 ? [
1164 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1165 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1166 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1167 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1168 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1169 ]
1170 : []),
1171 );
1172 }
1173 for (const membership of user.workspaces ?? []) {
1174 commands.push(
Workspace names and icons, and a component kit for every control1175 { 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 request1176 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1177 { 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 settings1178 { 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 look1179 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1180 );
1181 }
1182 for (const listed of shell.repos) {
1183 commands.push({
Projects: what a workspace builds and runs, first on every page1184 label: listed.title ?? listed.name,
1185 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1186 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1187 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1188 });
1189 }
1190 return commands;
1191}
1192
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1193/** 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 look1194function 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 put1195 const commands: Command[] = [];
1196 const repo = shell.repo;
1197 if (repo) {
1198 const base = `/${repo.namespace}/${repo.name}`;
1199 const name = `${repo.namespace}/${repo.name}`;
1200 commands.push(
1201 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1202 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1203 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1204 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1205 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1206 );
1207 }
1208 commands.push(
1209 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1210 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1211 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1212 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1213 { 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 look1214 { 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 put1215 );
1216 return commands;
1217}
1218
Agents as a team: lifecycle, merge queue, billing and a new shell1219/**
1220 * A bar across the top of the page while the next one loads, as GitHub
1221 * has: it appears at once, creeps towards the end while waiting, then fills
1222 * and fades when the page arrives.
1223 */
1224export function Progress() {
1225 const navigation = useNavigation();
1226 const busy = navigation.state !== "idle";
1227 const [width, setWidth] = useState(0);
1228 const [visible, setVisible] = useState(false);
1229 useEffect(() => {
1230 if (busy) {
1231 setVisible(true);
1232 setWidth(12);
1233 // Slows as it goes, never quite arriving until the page does.
1234 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1235 return () => clearInterval(timer);
1236 }
1237 setWidth((now) => (now > 0 ? 100 : 0));
1238 const fade = setTimeout(() => setVisible(false), 250);
1239 const reset = setTimeout(() => setWidth(0), 550);
1240 return () => {
1241 clearTimeout(fade);
1242 clearTimeout(reset);
1243 };
1244 }, [busy]);
1245 return (
1246 <div
1247 aria-hidden="true"
1248 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1249 visible ? "opacity-100" : "opacity-0"
1250 }`}
1251 >
1252 <div
1253 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1254 style={{ width: `${width}%` }}
1255 />
1256 </div>
1257 );
1258}
1259
1260/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1261 * The app: a sidebar with the workspace, its repositories and the sections
1262 * of the one being looked at; a slim bar with search and the account; and
1263 * the page. A visitor who is not signed in gets the same frame, with
1264 * Explore and Search in place of the workspace, and signing in in place of
1265 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1266 */
1267export function AppShell({
1268 user,
1269 shell,
1270 banner,
1271 children,
1272}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1273 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1274 shell: ShellData;
1275 banner?: ReactNode;
1276 children: ReactNode;
1277}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1278 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1279 const [drawer, setDrawer] = useState(false);
1280 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1281 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1282 const signUpLabel = useSignUpCopy().primary;
1283 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1284
1285 // A new page closes the drawer on small screens.
1286 useEffect(() => setDrawer(false), [pathname]);
Search across all of g1t, Explore, and a command palette1287 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1288
1289 return (
1290 <div className="min-h-screen">
1291 <Progress />
1292 <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">
A Vercel-like sidebar and a new mark1293 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1294 </aside>
1295 {drawer && (
1296 <div className="fixed inset-0 z-50 lg:hidden">
1297 <button
1298 type="button"
1299 aria-label="Close menu"
1300 className="absolute inset-0 bg-black/60"
1301 onClick={() => setDrawer(false)}
1302 />
1303 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1304 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1305 </aside>
1306 </div>
1307 )}
1308
Polish: phones, copy boxes, the plan page, the landing page, a real glide1309 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals1310 <header className="sticky top-0 z-30 flex h-16 items-center gap-3 border-b border-line bg-bg/85 px-4 backdrop-blur sm:px-6">
Agents as a team: lifecycle, merge queue, billing and a new shell1311 <button
1312 type="button"
1313 aria-label="Open menu"
1314 onClick={() => setDrawer(true)}
1315 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1316 >
1317 <Menu size={18} />
1318 </button>
A Vercel-like sidebar and a new mark1319 <Breadcrumbs pathname={pathname} />
Search across all of g1t, Explore, and a command palette1320 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1321 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1322 <input
1323 name="q"
1324 {...notACredential()}
1325 placeholder="Search g1t"
1326 aria-label="Search g1t"
1327 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"
1328 />
1329 <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">
1330 ⌘K
1331 </kbd>
1332 </Form>
1333 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1334 <a
1335 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1336 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 mark1337 >
1338 Docs
1339 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1340 {!user ? (
1341 // The sidebar has these too, but on a phone it is folded away.
1342 <Link
1343 to={withNext("/login", here)}
1344 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"
1345 >
1346 Sign in
1347 </Link>
1348 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1349 <DropdownMenu>
1350 <DropdownMenuTrigger
1351 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1352 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 shell1353 >
A Vercel-like sidebar and a new mark1354 <Plus size={14} />
1355 New
Agents as a team: lifecycle, merge queue, billing and a new shell1356 </DropdownMenuTrigger>
1357 <DropdownMenuContent align="end">
1358 {shell.repo && (
1359 <DropdownMenuItem asChild>
1360 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1361 <CircleDot />
1362 New issue
1363 </Link>
1364 </DropdownMenuItem>
1365 )}
1366 {shell.repo?.member && (
1367 <DropdownMenuItem asChild>
1368 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1369 <ListTree />
1370 Plan work
1371 </Link>
1372 </DropdownMenuItem>
1373 )}
1374 <DropdownMenuItem asChild>
1375 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1376 <Box />
1377 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1378 </Link>
1379 </DropdownMenuItem>
1380 <DropdownMenuItem asChild>
1381 <Link to="/workspaces/new">
1382 <Users />
1383 New workspace
1384 </Link>
1385 </DropdownMenuItem>
1386 </DropdownMenuContent>
1387 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1388 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1389 </div>
1390 </header>
1391 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1392 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1393 </div>
Search across all of g1t, Explore, and a command palette1394 <CommandPalette
1395 open={palette}
1396 onOpenChange={setPalette}
1397 commands={commands}
1398 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1399 />
Agents as a team: lifecycle, merge queue, billing and a new shell1400 </div>
1401 );
1402}