flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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

1,256 lines50,354 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 catalogue of model providers, and settings that feel like settings3 ArrowLeft,
A flat project menu with Code first, views as tabs, and Stripe for every owner4 GanttChart,
5 KanbanSquare,
6 Package,
7 Sparkles,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request8 BarChart3,
Agents as a team: lifecycle, merge queue, billing and a new shell9 BookMarked,
10 BookOpen,
11 Check,
12 ChevronsUpDown,
A Vercel-like sidebar and a new mark13 Ellipsis,
Agents as a team: lifecycle, merge queue, billing and a new shell14 CircleDot,
15 Code2,
16 Compass,
17 CreditCard,
18 GitPullRequest,
19 History,
Secrets and variables: one list, rows per environment, for workflows and deployments20 Fingerprint,
Agents as a team: lifecycle, merge queue, billing and a new shell21 KeyRound,
Projects: what a workspace builds and runs, first on every page22 Box,
Agents as a team: lifecycle, merge queue, billing and a new shell23 LayoutDashboard,
Projects: what a workspace builds and runs, first on every page24 LayoutGrid,
Agents as a team: lifecycle, merge queue, billing and a new shell25 ListTree,
26 Lock,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put27 LogIn,
Agents as a team: lifecycle, merge queue, billing and a new shell28 LogOut,
29 Menu,
30 Plus,
31 Search,
Integrations: your own model provider, alerts that open issues, tickets agents read32 Plug,
Agents as a team: lifecycle, merge queue, billing and a new shell33 Settings,
34 Users,
Webhooks: every event, to your own addresses, signed and retried35 Webhook,
Project settings move into the sidebar, grouped, with a heading per page36 Globe,
37 GitBranch,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs38 PlayCircle,
Pull requests say where they are live; the project menu shows what is coming39 Bot,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains40 Brain,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API41 Network,
Pull requests say where they are live; the project menu shows what is coming42 ShieldCheck,
Deployments: a preview for every pull request, production on g1t.page43 Rocket,
Agents as a team: lifecycle, merge queue, billing and a new shell44 X,
45} from "lucide-react";
46import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Search across all of g1t, Explore, and a command palette47import { Form, Link, NavLink, useLocation, useNavigation, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell48
Workspace names and icons, and a component kit for every control49import type { Membership, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell50import { MICROS_PER_DOLLAR } from "@g1t/contracts";
51
Search across all of g1t, Explore, and a command palette52import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put53import { Logo, Mark } from "./logo";
Search across all of g1t, Explore, and a command palette54import { Avatar, notACredential } from "./ui";
Agents as a team: lifecycle, merge queue, billing and a new shell55import {
56 DropdownMenu,
57 DropdownMenuContent,
58 DropdownMenuItem,
59 DropdownMenuLabel,
60 DropdownMenuSeparator,
61 DropdownMenuTrigger,
62} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales63import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put64import { VISITOR_LINKS, projectPages } from "../lib/chrome";
65import { withNext } from "../lib/next";
Agents as a team: lifecycle, merge queue, billing and a new shell66
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put67/**
68 * What the sidebar needs, worked out by the root loader. For a visitor who
69 * is not signed in there is no workspace, no projects and no usage: only
70 * the project being looked at, if they can see it.
71 */
Agents as a team: lifecycle, merge queue, billing and a new shell72export type ShellData = {
73 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control74 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page75 /** Its projects, by name: `name` is the slug in their address. */
76 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
77 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell78 repo: {
79 namespace: string;
80 name: string;
81 member: boolean;
82 issues: number;
83 pulls: number;
84 } | null;
Billing accounts, terms and enterprises; g1t is no longer free85 /** Where the workspace stands against its usage limit, if billing is on. */
86 limit: {
87 exposureMicros: number;
88 ceilingMicros: number | null;
89 state: "ok" | "warning" | "stopped";
90 comped: boolean;
91 } | null;
Free while g1t is being built out; agents can check out their own forks92 /** Whether g1t charges nothing for now, while it is being built out. */
93 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request94 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy95 /** This month's usage: charged, or at cost while g1t is free. */
96 monthUsageMicros: number | null;
Agents as a team: lifecycle, merge queue, billing and a new shell97};
98
99function SidebarLink({
100 to,
101 icon,
102 end,
103 count,
104 also,
105 children,
106}: {
107 to: string;
108 icon: ReactNode;
109 end?: boolean;
110 count?: number;
Projects: what a workspace builds and runs, first on every page111 /** Other path prefixes under which this link is the current one. */
112 also?: string | string[];
Agents as a team: lifecycle, merge queue, billing and a new shell113 children: ReactNode;
114}) {
115 const { pathname } = useLocation();
116 return (
117 <NavLink
118 to={to}
119 end={end}
120 prefetch="intent"
121 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page122 const current =
123 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark124 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 shell125 current
126 ? "bg-raised font-medium text-fg"
127 : isPending
128 ? "bg-raised/60 text-fg"
129 : "text-muted hover:bg-raised/60 hover:text-fg"
130 }`;
131 }}
132 >
133 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
134 <span className="min-w-0 grow truncate">{children}</span>
135 {count != null && count > 0 && (
136 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
137 {count}
138 </span>
139 )}
140 </NavLink>
141 );
142}
143
Pull requests say where they are live; the project menu shows what is coming144/**
145 * Something a project will have, shown where it will live so people can
146 * see where g1t is going. Not a link: nothing is behind it yet.
147 */
148function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
149 return (
150 <div
151 title={about}
152 aria-disabled="true"
153 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
154 >
155 <span className="shrink-0 opacity-70">{icon}</span>
156 <span className="grow truncate">{children}</span>
157 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
158 Soon
159 </span>
160 </div>
161 );
162}
163
The project menu opens by section, and the workspace has its own panel164/**
A flat project menu with Code first, views as tabs, and Stripe for every owner165 * A page that is coming, as a link to its roadmap page: faint, with Soon,
166 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel167 */
A flat project menu with Code first, views as tabs, and Stripe for every owner168function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel169 to,
170 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner171 icon,
172 about,
The project menu opens by section, and the workspace has its own panel173 children,
174}: {
175 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner176 also?: string[];
177 icon: ReactNode;
178 about: string;
The project menu opens by section, and the workspace has its own panel179 children: ReactNode;
180}) {
181 const { pathname } = useLocation();
182 return (
183 <NavLink
184 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner185 title={about}
The project menu opens by section, and the workspace has its own panel186 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner187 className={({ isActive }) => {
188 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
189 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
190 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 panel191 }`;
192 }}
193 >
A flat project menu with Code first, views as tabs, and Stripe for every owner194 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel195 <span className="min-w-0 grow truncate">{children}</span>
196 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
197 Soon
198 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales199 </NavLink>
The project menu opens by section, and the workspace has its own panel200 );
201}
202
A flat project menu with Code first, views as tabs, and Stripe for every owner203/** Icons for what spans projects. */
204const WORKSPACE_ICONS: Record<string, ReactNode> = {
205 board: <KanbanSquare size={15} />,
206 roadmap: <GanttChart size={15} />,
207 packages: <Package size={15} />,
208 fleet: <Bot size={15} />,
209};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales210
A flat project menu with Code first, views as tabs, and Stripe for every owner211/** The Soon pages of a section, as paths, so its link is current on them. */
212function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
213 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales214}
215
Agents as a team: lifecycle, merge queue, billing and a new shell216function SidebarGroup({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {
217 return (
A Vercel-like sidebar and a new mark218 <section className="mt-6">
219 <div className="mb-1 flex h-6 items-center justify-between px-2">
220 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell221 {action}
222 </div>
223 <div className="space-y-px">{children}</div>
224 </section>
225 );
226}
227
Workspace names and icons, and a component kit for every control228/** What a workspace is called where people read it: its display name, else its slug. */
229function displayName(membership: Membership): string {
230 return membership.name?.trim() || membership.slug;
231}
232
Agents as a team: lifecycle, merge queue, billing and a new shell233function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control234 const workspace = shell.workspace;
235 const current = workspace?.slug;
236 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell237 return (
238 <DropdownMenu>
Workspace names and icons, and a component kit for every control239 {/* The name goes to the workspace; only the arrows switch it. The
240 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark241 <Link
242 to={current ? `/${current}` : "/workspaces/new"}
Workspace names and icons, and a component kit for every control243 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
244 className="flex h-8 min-w-0 grow items-center gap-1.5 rounded-md px-1 transition-colors hover:bg-raised"
A Vercel-like sidebar and a new mark245 >
Workspace names and icons, and a component kit for every control246 {workspace ? (
247 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell248 ) : (
Workspace names and icons, and a component kit for every control249 <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 shell250 )}
Workspace names and icons, and a component kit for every control251 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark252 </Link>
253 <DropdownMenuTrigger
254 aria-label="Switch workspace"
Workspace names and icons, and a component kit for every control255 className="flex h-8 w-5 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 mark256 >
257 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell258 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control259 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell260 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
261 {(user.workspaces ?? []).map((membership) => (
262 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control263 <Link to={`/${membership.slug}`} title={displayName(membership)}>
264 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
265 <span className="flex min-w-0 grow flex-col leading-tight">
266 <span className="truncate">{displayName(membership)}</span>
267 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
268 </span>
269 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
270 {membership.slug === current ? (
271 <Check className="shrink-0 text-accent" />
272 ) : (
273 <span className="size-4 shrink-0" aria-hidden="true" />
274 )}
Agents as a team: lifecycle, merge queue, billing and a new shell275 </Link>
276 </DropdownMenuItem>
277 ))}
278 <DropdownMenuSeparator />
279 <DropdownMenuItem asChild>
280 <Link to="/workspaces/new">
281 <Plus />
282 New workspace
283 </Link>
284 </DropdownMenuItem>
285 </DropdownMenuContent>
286 </DropdownMenu>
287 );
288}
289
Billing accounts, terms and enterprises; g1t is no longer free290/**
291 * This month's usage, and how close the workspace is to its usage limit,
292 * as Vercel shows a plan's usage.
293 */
A Vercel-like sidebar and a new mark294function UsageCard({ slug, shell }: { slug: string; shell: ShellData }) {
Usage while free is shown at cost; agents get rustfmt and clippy295 if (shell.monthUsageMicros == null) return null;
296 const spent = shell.monthUsageMicros;
Billing accounts, terms and enterprises; g1t is no longer free297 const limit = shell.limit;
298 const ceiling = limit?.ceilingMicros ?? null;
299 const share = limit && ceiling ? Math.min(1, limit.exposureMicros / Math.max(ceiling, 1)) : 0;
300 const tone = limit?.state === "stopped" ? "text-danger" : limit?.state === "warning" ? "text-warn" : "text-faint";
301 const bar = limit?.state === "stopped" ? "bg-danger" : limit?.state === "warning" ? "bg-warn" : "bg-accent";
A Vercel-like sidebar and a new mark302 return (
303 <Link
304 to={`/${slug}/-/usage`}
305 className="block rounded-lg bg-surface p-3 ring-1 ring-line transition-colors hover:ring-line-strong"
306 >
307 <span className="flex items-baseline justify-between text-xs">
308 <span className="font-medium text-fg">Usage</span>
309 <span className="text-faint">this month</span>
310 </span>
311 <span className="mt-2 flex items-baseline justify-between">
312 <span className="font-mono text-sm tabular-nums">${(spent / MICROS_PER_DOLLAR).toFixed(2)}</span>
Free while g1t is being built out; agents can check out their own forks313 {shell.free ? (
314 <span className="text-xs text-accent">Free for now</span>
Billing accounts, terms and enterprises; g1t is no longer free315 ) : limit?.comped ? (
316 <span className="text-xs text-accent">Comped</span>
Free while g1t is being built out; agents can check out their own forks317 ) : (
Billing accounts, terms and enterprises; g1t is no longer free318 ceiling != null && (
319 <span className={`text-xs ${tone}`}>
320 {limit?.state === "stopped" ? "Limit reached" : `$${(ceiling / MICROS_PER_DOLLAR).toFixed(2)} limit`}
Free while g1t is being built out; agents can check out their own forks321 </span>
322 )
A Vercel-like sidebar and a new mark323 )}
324 </span>
Billing accounts, terms and enterprises; g1t is no longer free325 {ceiling != null && !limit?.comped && (
326 <span className="mt-2 block h-1 overflow-hidden rounded-full bg-raised">
327 <span
328 className={`block h-full rounded-full ${bar}`}
329 style={{ width: `${Math.max(share * 100, share > 0 ? 3 : 0)}%` }}
330 />
331 </span>
332 )}
A Vercel-like sidebar and a new mark333 </Link>
334 );
335}
336
337function AccountMenu({ user }: { user: User }) {
338 const submit = useSubmit();
339 return (
340 <DropdownMenu>
341 <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 control342 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark343 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
344 <Ellipsis size={15} className="shrink-0 text-faint" />
345 </DropdownMenuTrigger>
346 <DropdownMenuContent align="start" side="top" className="w-56">
347 <DropdownMenuLabel>
348 Signed in as <span className="font-mono font-medium text-fg">{user.username}</span>
349 </DropdownMenuLabel>
350 <DropdownMenuSeparator />
351 <DropdownMenuItem asChild>
352 <Link to="/settings">
353 <Settings />
354 Your settings
355 </Link>
356 </DropdownMenuItem>
357 <DropdownMenuItem asChild>
358 <a href="https://docs.g1t.sh/">
359 <BookOpen />
360 Documentation
361 </a>
362 </DropdownMenuItem>
363 <DropdownMenuSeparator />
364 {/* Submitted from here: the menu closes on select, and a button
365 that has left the page cannot submit a form. */}
366 <DropdownMenuItem onSelect={() => submit(null, { method: "post", action: "/logout" })}>
367 <LogOut />
368 Sign out
369 </DropdownMenuItem>
370 </DropdownMenuContent>
371 </DropdownMenu>
372 );
373}
374
A catalogue of model providers, and settings that feel like settings375/** A workspace's settings pages, which the sidebar slides over to. */
Project settings move into the sidebar, grouped, with a heading per page376const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets|guardrails|audit)(\/|$)/;
377/** A project's settings pages, which the project's menu gives way to. */
378const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings379
Polish: phones, copy boxes, the plan page, the landing page, a real glide380/**
Sidebar: no settings flash on the way into a repository, and a better slide381 * The sidebar's menus are two layers, the way a phone pushes a screen: the
382 * one arriving slides in from the right over the full width, while the one
383 * leaving drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide384 * ease-out, the fade quicker than the move so the two never blur together.
385 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide386 */
Sidebar: no settings flash on the way into a repository, and a better slide387const LAYER =
Sidebar: the panels really slide388 "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";
Sidebar: no settings flash on the way into a repository, and a better slide389/** One menu, filling its layer. */
390const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings391
392/** A workspace's settings, as the sidebar shows them in place of everything else. */
393function SettingsMenu({ slug, owner, open }: { slug: string; owner: boolean; open: boolean }) {
394 return (
395 <nav
396 aria-label="Workspace settings"
397 inert={!open}
Polish: phones, copy boxes, the plan page, the landing page, a real glide398 className={PANEL}
A catalogue of model providers, and settings that feel like settings399 >
400 <Link
401 to={`/${slug}`}
402 className="group mt-3 flex h-8 items-center gap-2 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-raised/60 hover:text-fg"
403 >
404 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
405 <span className="truncate">{slug}</span>
406 </Link>
407 <SidebarGroup title="Settings">
408 {owner && (
409 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
410 General
411 </SidebarLink>
412 )}
413 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />}>
414 Members
415 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments416 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
417 Access tokens
418 </SidebarLink>
A catalogue of model providers, and settings that feel like settings419 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments420 Billing and plans
A catalogue of model providers, and settings that feel like settings421 </SidebarLink>
422 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
423 Integrations
Webhooks: every event, to your own addresses, signed and retried424 </SidebarLink>
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs425 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
426 Secrets and variables
427 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API428 <SidebarLink to={`/${slug}/-/guardrails`} icon={<ShieldCheck size={15} />}>
429 Guardrails
430 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments431 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
432 Webhooks
A catalogue of model providers, and settings that feel like settings433 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API434 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
435 Audit log
436 </SidebarLink>
A catalogue of model providers, and settings that feel like settings437 </SidebarGroup>
438 </nav>
439 );
440}
441
Sidebar: no settings flash on the way into a repository, and a better slide442/**
443 * What the repository menu needs. Known from the address before the page's
444 * data arrives, so the menu is right from the first frame; the counts fill
445 * in once it does.
446 */
447type MenuRepo = {
448 namespace: string;
449 name: string;
450 member: boolean;
451 issues?: number;
452 pulls?: number;
453};
454
455function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
456 return (
457 a != null &&
458 b != null &&
459 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
460 a.name.toLowerCase() === b.name.toLowerCase()
461 );
462}
A repository has its own sidebar, as settings do463
464/**
Projects: what a workspace builds and runs, first on every page465 * A project's own menu, which the sidebar slides to while you are in it,
466 * as it does for settings: everything about the project, running and its
467 * code, and nothing else, with the way back to everything.
A repository has its own sidebar, as settings do468 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put469function RepoMenu({
470 repo,
471 isPrivate,
472 open,
473 visitor = false,
474}: {
475 repo: MenuRepo;
476 isPrivate: boolean;
477 open: boolean;
478 /** Not signed in: the way back is Explore, not mission control. */
479 visitor?: boolean;
480}) {
A repository has its own sidebar, as settings do481 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 put482 // What a member sees, and what everyone who can see the project does.
483 const shows = new Set(projectPages(repo.member));
A repository has its own sidebar, as settings do484 return (
485 <nav aria-label={`${repo.namespace}/${repo.name}`} inert={!open} className={PANEL}>
486 <Link
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put487 to={visitor ? "/explore" : "/"}
A repository has its own sidebar, as settings do488 className="group mt-3 flex h-8 items-center gap-2 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-raised/60 hover:text-fg"
489 >
490 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put491 {visitor ? "Explore" : "Mission control"}
A repository has its own sidebar, as settings do492 </Link>
A flat project menu with Code first, views as tabs, and Stripe for every owner493 <NavLink
A repository has its own sidebar, as settings do494 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner495 end
496 title="Overview"
497 className={({ isActive }) =>
498 `mt-3 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
499 }
A repository has its own sidebar, as settings do500 >
501 <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 page502 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do503 </span>
504 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
505 <span className="text-faint">{repo.namespace}/</span>
506 <span className="font-semibold text-fg">{repo.name}</span>
507 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner508 </NavLink>
509 {/* A project's pages, flat and in the order people use them. A page
510 with more than one view shows them as tabs across its top. */}
Projects: what a workspace builds and runs, first on every page511 <div className="mt-3 space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner512 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
513 Code
514 </SidebarLink>
515 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
516 Issues
517 </SidebarLink>
518 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
519 Pull requests
520 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains521 <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 owner522 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains523 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner524 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
525 Workflows
Projects: what a workspace builds and runs, first on every page526 </SidebarLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put527 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner528 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
529 Deployments
530 </SidebarLink>
531 ) : null}
Observability gets its own page; tabs never overflow532 <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.">
533 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner534 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put535 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API536 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
537 Security
538 </SidebarLink>
539 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner540 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
541 Insights
542 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put543 {shows.has("settings") && (
A flat project menu with Code first, views as tabs, and Stripe for every owner544 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />}>
545 Settings
546 </SidebarLink>
Projects: what a workspace builds and runs, first on every page547 )}
548 </div>
A repository has its own sidebar, as settings do549 </nav>
550 );
551}
552
Project settings move into the sidebar, grouped, with a heading per page553/**
554 * A project's settings, as the sidebar shows them in place of the project's
555 * menu: what is about running it, then its agents, then its code and who
556 * can reach it. The way back leads to the project.
557 */
558function RepoSettingsMenu({ repo, open }: { repo: MenuRepo; open: boolean }) {
559 const base = `/${repo.namespace}/${repo.name}`;
560 return (
561 <nav aria-label={`${repo.namespace}/${repo.name} settings`} inert={!open} className={PANEL}>
562 <Link
563 to={base}
564 className="group mt-3 flex h-8 items-center gap-2 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-raised/60 hover:text-fg"
565 >
566 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
567 <span className="min-w-0 truncate font-mono">
568 <span className="text-faint">{repo.namespace}/</span>
569 {repo.name}
570 </span>
571 </Link>
572 <SidebarGroup title="Settings">
573 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
574 General
575 </SidebarLink>
576 </SidebarGroup>
577 <SidebarGroup title="Running">
578 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
579 Deployments
580 </SidebarLink>
581 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
582 Domains
583 </SidebarLink>
584 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
585 Dependencies
586 </SidebarLink>
587 </SidebarGroup>
588 <SidebarGroup title="Agents">
589 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
590 Agents
591 </SidebarLink>
592 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
593 Guardrails
594 </SidebarLink>
595 </SidebarGroup>
596 <SidebarGroup title="Code and access">
597 <SidebarLink to={`${base}/settings/repository`} icon={<GitBranch size={15} />}>
598 Repository
599 </SidebarLink>
600 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
601 Secrets and variables
602 </SidebarLink>
603 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
604 Webhooks
605 </SidebarLink>
606 </SidebarGroup>
607 </nav>
608 );
609}
610
Account settings in the same shape as a workspace's611/** Your own settings, as the sidebar shows them on the settings page. */
612function AccountSettingsMenu({ open }: { open: boolean }) {
613 const { hash } = useLocation();
614 const item = (id: string, icon: ReactNode, label: string) => (
615 <Link
616 to={`/settings#${id}`}
617 className={`group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
618 hash === `#${id}` ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"
619 }`}
620 >
621 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
622 {label}
623 </Link>
624 );
625 return (
626 <nav
627 aria-label="Your settings"
628 inert={!open}
Polish: phones, copy boxes, the plan page, the landing page, a real glide629 className={PANEL}
Account settings in the same shape as a workspace's630 >
631 <Link
632 to="/"
633 className="group mt-3 flex h-8 items-center gap-2 rounded-md px-2 text-[0.8125rem] text-muted transition-colors hover:bg-raised/60 hover:text-fg"
634 >
635 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
636 Mission control
637 </Link>
638 <SidebarGroup title="Account">
Secrets and variables: one list, rows per environment, for workflows and deployments639 {item("ssh-keys", <Fingerprint size={15} />, "SSH keys")}
640 {item("tokens", <KeyRound size={15} />, "Access tokens")}
Account settings in the same shape as a workspace's641 {item("applications", <Plug size={15} />, "Connected applications")}
642 </SidebarGroup>
643 </nav>
644 );
645}
646
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put647/**
648 * Signing in and signing up, in place of the account for a visitor. Signing
649 * in brings them back to the page they are on.
650 */
651function VisitorPanel() {
652 const { pathname, search } = useLocation();
653 return (
654 <div className="space-y-2">
655 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
656 <div className="grid grid-cols-2 gap-2">
657 <Link
658 to={withNext("/login", pathname + search)}
659 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"
660 >
661 <LogIn size={14} />
662 Sign in
663 </Link>
664 <Link
665 to={withNext("/register", pathname + search)}
666 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
667 >
668 Sign up
669 </Link>
670 </div>
671 </div>
672 );
673}
674
675function Sidebar({ user, shell, onFind }: { user: User | null; shell: ShellData; onFind: () => void }) {
Agents as a team: lifecycle, merge queue, billing and a new shell676 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings677 const { pathname } = useLocation();
678 const going = useNavigation().location?.pathname;
679 // On a workspace's settings page, or on the way to one, its settings take
680 // the sidebar over.
681 const inSettings = ws != null && SETTINGS_PAGE.exec(going ?? pathname)?.[1]?.toLowerCase() === ws.slug;
Account settings in the same shape as a workspace's682 const inAccount = (going ?? pathname) === "/settings";
A repository has its own sidebar, as settings do683 const active = shell.repo;
684 // In a repository, or on the way into one, its own menu takes the sidebar.
685 const target = going ?? pathname;
686 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains687 const reserved = new Set(["settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth"]);
A repository has its own sidebar, as settings do688 const inRepo = repoPath != null && !reserved.has(repoPath[1]) && repoPath[2] !== "-";
Project settings move into the sidebar, grouped, with a heading per page689 // A project's settings take its menu over in turn.
690 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
A repository has its own sidebar, as settings do691 const away = inSettings || inAccount || inRepo;
Sidebar: no settings flash on the way into a repository, and a better slide692 // The repository the menu is for: the one loaded if it is the one being
693 // gone to, else what the address says, at once.
694 const targetRepo = inRepo && repoPath ? { namespace: repoPath[1], name: repoPath[2] } : null;
695 const menuRepo: MenuRepo | null = targetRepo
696 ? sameRepo(active, targetRepo)
697 ? active
698 : {
699 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put700 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide701 }
702 : null;
A repository has its own sidebar, as settings do703 // What sits on the far side of the track. Kept while sliding back, so it
704 // does not vanish on the way out.
Project settings move into the sidebar, grouped, with a heading per page705 const side = useRef<"workspace" | "account" | "repo" | "repo-settings">("workspace");
Polish: phones, copy boxes, the plan page, the landing page, a real glide706 if (inAccount) side.current = "account";
707 else if (inSettings) side.current = "workspace";
Project settings move into the sidebar, grouped, with a heading per page708 else if (inRepoSettings) side.current = "repo-settings";
A repository has its own sidebar, as settings do709 else if (inRepo) side.current = "repo";
710 // The repository last shown, kept for the slide back.
Sidebar: no settings flash on the way into a repository, and a better slide711 const shown = useRef<MenuRepo | null>(menuRepo);
712 if (menuRepo) shown.current = menuRepo;
713 // Moving between two of the far-side menus (settings to a repository,
714 // one repository to another) crossfades in place instead of sliding.
Project settings move into the sidebar, grouped, with a heading per page715 const detailKey =
716 side.current === "repo" || side.current === "repo-settings"
717 ? `${side.current}:${shown.current?.namespace}/${shown.current?.name}`
718 : side.current;
Sidebar: no settings flash on the way into a repository, and a better slide719 const lastAway = useRef(away);
720 const lastKey = useRef(detailKey);
721 const swapped = lastAway.current && away && lastKey.current !== detailKey;
722 useEffect(() => {
723 lastAway.current = away;
724 lastKey.current = detailKey;
725 });
Agents as a team: lifecycle, merge queue, billing and a new shell726 // The repository being looked at is listed even when it is someone else's.
727 const listed =
728 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
729 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
730 : shell.repos;
731 return (
732 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark733 {/* 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 control734 <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 put735 {user ? (
736 <>
737 <Link to="/" aria-label="g1t home" className="shrink-0 rounded-md p-1.5 hover:bg-raised">
738 <Mark className="size-6" />
739 </Link>
740 <span className="shrink-0 text-line-strong" aria-hidden="true">
741 /
742 </span>
743 <WorkspaceSwitcher user={user} shell={shell} />
744 </>
745 ) : (
746 <Link to="/" aria-label="g1t home" className="rounded-md px-1.5 py-1 hover:bg-raised">
747 <Logo />
748 </Link>
749 )}
Agents as a team: lifecycle, merge queue, billing and a new shell750 </div>
A Vercel-like sidebar and a new mark751 <div className="px-2 pt-3">
752 <button
753 type="button"
754 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals755 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 mark756 >
757 <Search size={14} />
758 <span className="grow text-left">Find…</span>
759 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
760 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell761 </div>
Sidebar: no settings flash on the way into a repository, and a better slide762 <div className="relative min-h-0 grow overflow-hidden">
763 <div className={`${LAYER} ${away ? "pointer-events-none -translate-x-1/4 opacity-0" : "translate-x-0 opacity-100"}`}>
Polish: phones, copy boxes, the plan page, the landing page, a real glide764 <nav aria-label="g1t" inert={away} className={PANEL}>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put765 {/* A visitor browses: no workspace, no projects of their own. */}
766 {!user ? (
767 <div className="mt-3 space-y-px">
768 {VISITOR_LINKS.map((link) => (
769 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
770 {link.label}
771 </SidebarLink>
772 ))}
773 </div>
774 ) : (
775 <>
Agents as a team: lifecycle, merge queue, billing and a new shell776 <div className="mt-3 space-y-px">
777 <SidebarLink to="/" end icon={<LayoutDashboard size={15} />}>
778 Mission control
779 </SidebarLink>
780 <SidebarLink to="/explore" icon={<Compass size={15} />}>
781 Explore
782 </SidebarLink>
783 </div>
784
785 <SidebarGroup
Projects: what a workspace builds and runs, first on every page786 title="Projects"
Agents as a team: lifecycle, merge queue, billing and a new shell787 action={
788 ws && (
789 <Link
790 to={`/new?workspace=${ws.slug}`}
Projects: what a workspace builds and runs, first on every page791 aria-label="New project"
Agents as a team: lifecycle, merge queue, billing and a new shell792 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
793 >
794 <Plus size={13} />
795 </Link>
796 )
797 }
798 >
799 {listed.length === 0 && (
800 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
801 )}
802 {listed.map((repo) => {
803 const base = `/${repo.namespace}/${repo.name}`;
804 return (
805 <div key={base}>
806 <SidebarLink
807 to={base}
Projects: what a workspace builds and runs, first on every page808 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
Agents as a team: lifecycle, merge queue, billing and a new shell809 >
Projects: what a workspace builds and runs, first on every page810 <span className="text-[0.8125rem]">
Agents as a team: lifecycle, merge queue, billing and a new shell811 {repo.namespace !== ws?.slug && (
Projects: what a workspace builds and runs, first on every page812 <span className="font-mono text-faint">{repo.namespace}/</span>
Agents as a team: lifecycle, merge queue, billing and a new shell813 )}
Projects: what a workspace builds and runs, first on every page814 {"title" in repo && repo.title ? repo.title : repo.name}
Agents as a team: lifecycle, merge queue, billing and a new shell815 </span>
816 </SidebarLink>
817 </div>
818 );
819 })}
820 </SidebarGroup>
A flat project menu with Code first, views as tabs, and Stripe for every owner821 {ws && (
822 <SidebarGroup title="Across projects">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains823 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
824 Agent fleet
825 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API826 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
827 Context
828 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains829 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
830 Memory
831 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API832 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
833 Security
834 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner835 {roadmapIn("Workspace").map((item) => (
836 <SidebarSoonLink
837 key={item.key}
838 to={`/${ws.slug}/-/soon/${item.key}`}
839 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
840 about={item.summary}
841 >
842 {item.title === "Board" ? "Boards" : item.title}
843 </SidebarSoonLink>
844 ))}
845 </SidebarGroup>
846 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put847 </>
848 )}
Agents as a team: lifecycle, merge queue, billing and a new shell849 </nav>
Polish: phones, copy boxes, the plan page, the landing page, a real glide850 </div>
Sidebar: no settings flash on the way into a repository, and a better slide851 <div className={`${LAYER} ${away ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}>
852 <div key={detailKey} className={`h-full ${swapped ? "animate-[g1t-swap_220ms_ease-out]" : ""}`}>
Project settings move into the sidebar, grouped, with a heading per page853 {side.current === "repo-settings" && shown.current ? (
854 <RepoSettingsMenu repo={shown.current} open={inRepoSettings} />
855 ) : side.current === "repo" && shown.current ? (
Sidebar: no settings flash on the way into a repository, and a better slide856 <RepoMenu
857 repo={shown.current}
858 isPrivate={shell.repos.some((repo) => sameRepo(repo, shown.current) && repo.isPrivate)}
859 open={inRepo}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put860 visitor={!user}
Sidebar: no settings flash on the way into a repository, and a better slide861 />
862 ) : side.current === "account" || !ws ? (
863 <AccountSettingsMenu open={inAccount} />
864 ) : (
865 <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />
866 )}
867 </div>
868 </div>
A catalogue of model providers, and settings that feel like settings869 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell870
The project menu opens by section, and the workspace has its own panel871 {/* The workspace's own things sit at the bottom, by its usage and the
872 account: a panel of their own, ruled off from whatever menu is above,
873 however long it grows. */}
874 <div className="shrink-0 space-y-2 border-t border-line bg-surface/50 p-2 pt-3">
Workspace things at the bottom of the sidebar, and on mission control875 {ws && (
876 <div className="space-y-px">
The project menu opens by section, and the workspace has its own panel877 <p className="flex items-center gap-2 px-2 pb-1 text-[0.6875rem] font-medium uppercase tracking-wider text-faint">
878 <span>Workspace</span>
879 <span className="min-w-0 truncate font-mono normal-case tracking-normal text-muted">{ws.slug}</span>
880 </p>
Workspace things at the bottom of the sidebar, and on mission control881 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
882 Usage
883 </SidebarLink>
884 {/* Everything else about the workspace lives in its settings, and only there. */}
885 <SidebarLink
886 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/people`}
887 icon={<Settings size={15} />}
888 >
889 Settings
890 </SidebarLink>
891 </div>
892 )}
A Vercel-like sidebar and a new mark893 {ws && <UsageCard slug={ws.slug} shell={shell} />}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put894 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark895 </div>
896 </div>
897 );
898}
899
900/** Words for the sections a path can end in. */
901const SECTIONS: Record<string, string> = {
902 issues: "Issues",
903 pulls: "Pull requests",
904 queue: "Merge queue",
905 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales906 plans: "Outcomes",
907 actions: "Workflows",
908 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page909 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page910 repository: "Repository",
Project dependencies: addresses, preview stacks, Affects, and agents who know911 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales912 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs913 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark914 settings: "Settings",
A catalogue of model providers, and settings that feel like settings915 people: "Members",
A Vercel-like sidebar and a new mark916 tokens: "Access tokens",
917 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments918 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings919 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried920 webhooks: "Webhooks",
A Vercel-like sidebar and a new mark921 tree: "Code",
922 blob: "Code",
923};
924
925/** Where the page is, as a trail of links: workspace / repository / section. */
926function Breadcrumbs({ pathname }: { pathname: string }) {
927 const parts = pathname.split("/").filter(Boolean);
928 const reserved = ["settings", "explore", "new", "search", "workspaces"];
929 if (parts.length === 0) return <span className="text-sm font-medium">Mission control</span>;
930 if (reserved.includes(parts[0]!)) {
931 const words: Record<string, string> = {
Account settings in the same shape as a workspace's932 settings: "Account",
A Vercel-like sidebar and a new mark933 explore: "Explore",
Projects live: fixes from walking them end to end934 new: "New project",
A Vercel-like sidebar and a new mark935 search: "Search",
936 workspaces: "New workspace",
937 };
938 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
939 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put940 // A person's profile, by their handle.
941 if (parts[0] === "u" && parts[1]) {
942 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
943 }
A Vercel-like sidebar and a new mark944 const [owner, second, third, fourth] = parts;
945 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
946 if (second === "-") {
A catalogue of model providers, and settings that feel like settings947 const page = `/${owner}/-/${third}`;
948 if (third && SETTINGS_PAGE.test(page)) {
949 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
950 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
951 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark952 } else if (second) {
953 const repo = `/${owner}/${second}`;
954 trail.push({ label: second, to: repo, mono: true });
955 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
956 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
957 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 deployments958 else if (third && SECTIONS[third]) {
959 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
960 // A settings page names which one: Settings / Secrets and variables.
961 if (third === "settings" && fourth && SECTIONS[fourth]) {
962 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
963 }
964 }
A Vercel-like sidebar and a new mark965 }
966 return (
967 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
968 {trail.map((crumb, index) => (
969 <span key={crumb.to} className="flex min-w-0 items-center gap-1.5">
970 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell971 <Link
A Vercel-like sidebar and a new mark972 to={crumb.to}
973 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
974 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
975 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell976 >
A Vercel-like sidebar and a new mark977 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell978 </Link>
A Vercel-like sidebar and a new mark979 </span>
980 ))}
981 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell982 );
983}
984
Search across all of g1t, Explore, and a command palette985type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell986
987/** Everything the palette can jump to, from what the sidebar already knows. */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put988function commandsFor(user: User | null, shell: ShellData, here: string): Command[] {
989 if (!user) return visitorCommands(shell, here);
Agents as a team: lifecycle, merge queue, billing and a new shell990 const commands: Command[] = [
991 { label: "Mission control", to: "/", icon: <LayoutDashboard size={15} /> },
992 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette993 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page994 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell995 { label: "New workspace", to: "/workspaces/new", icon: <Plus size={15} /> },
996 { label: "Your settings", to: "/settings", icon: <Settings size={15} /> },
997 ];
998 const repo = shell.repo;
999 if (repo) {
1000 const base = `/${repo.namespace}/${repo.name}`;
1001 const name = `${repo.namespace}/${repo.name}`;
1002 commands.unshift(
Projects: what a workspace builds and runs, first on every page1003 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1004 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1005 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1006 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
1007 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1008 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1009 ...(repo.member
1010 ? [
1011 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1012 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1013 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1014 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1015 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1016 ]
1017 : []),
1018 );
1019 }
1020 for (const membership of user.workspaces ?? []) {
1021 commands.push(
Workspace names and icons, and a component kit for every control1022 { 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 request1023 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1024 { 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 settings1025 { label: "Access tokens", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/tokens`, icon: <KeyRound size={15} /> },
1026 { label: "Integrations", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1027 );
1028 }
1029 for (const listed of shell.repos) {
1030 commands.push({
Projects: what a workspace builds and runs, first on every page1031 label: listed.title ?? listed.name,
1032 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1033 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1034 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1035 });
1036 }
1037 return commands;
1038}
1039
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1040/** What the palette offers a visitor: browsing, the project they are in, and signing in. */
1041function visitorCommands(shell: ShellData, here: string): Command[] {
1042 const commands: Command[] = [];
1043 const repo = shell.repo;
1044 if (repo) {
1045 const base = `/${repo.namespace}/${repo.name}`;
1046 const name = `${repo.namespace}/${repo.name}`;
1047 commands.push(
1048 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1049 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1050 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1051 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1052 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1053 );
1054 }
1055 commands.push(
1056 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1057 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1058 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1059 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1060 { label: "Sign in", to: withNext("/login", here), icon: <LogIn size={15} /> },
1061 { label: "Sign up", to: withNext("/register", here), icon: <Plus size={15} /> },
1062 );
1063 return commands;
1064}
1065
Agents as a team: lifecycle, merge queue, billing and a new shell1066/**
1067 * A bar across the top of the page while the next one loads, as GitHub
1068 * has: it appears at once, creeps towards the end while waiting, then fills
1069 * and fades when the page arrives.
1070 */
1071export function Progress() {
1072 const navigation = useNavigation();
1073 const busy = navigation.state !== "idle";
1074 const [width, setWidth] = useState(0);
1075 const [visible, setVisible] = useState(false);
1076 useEffect(() => {
1077 if (busy) {
1078 setVisible(true);
1079 setWidth(12);
1080 // Slows as it goes, never quite arriving until the page does.
1081 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1082 return () => clearInterval(timer);
1083 }
1084 setWidth((now) => (now > 0 ? 100 : 0));
1085 const fade = setTimeout(() => setVisible(false), 250);
1086 const reset = setTimeout(() => setWidth(0), 550);
1087 return () => {
1088 clearTimeout(fade);
1089 clearTimeout(reset);
1090 };
1091 }, [busy]);
1092 return (
1093 <div
1094 aria-hidden="true"
1095 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1096 visible ? "opacity-100" : "opacity-0"
1097 }`}
1098 >
1099 <div
1100 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1101 style={{ width: `${width}%` }}
1102 />
1103 </div>
1104 );
1105}
1106
1107/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1108 * The app: a sidebar with the workspace, its repositories and the sections
1109 * of the one being looked at; a slim bar with search and the account; and
1110 * the page. A visitor who is not signed in gets the same frame, with
1111 * Explore and Search in place of the workspace, and signing in in place of
1112 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1113 */
1114export function AppShell({
1115 user,
1116 shell,
1117 banner,
1118 children,
1119}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1120 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1121 shell: ShellData;
1122 banner?: ReactNode;
1123 children: ReactNode;
1124}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1125 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1126 const [drawer, setDrawer] = useState(false);
1127 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1128 const here = pathname + search;
1129 const commands = useMemo(() => commandsFor(user, shell, here), [user, shell, here]);
Agents as a team: lifecycle, merge queue, billing and a new shell1130
1131 // A new page closes the drawer on small screens.
1132 useEffect(() => setDrawer(false), [pathname]);
Search across all of g1t, Explore, and a command palette1133 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1134
1135 return (
1136 <div className="min-h-screen">
1137 <Progress />
1138 <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 mark1139 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1140 </aside>
1141 {drawer && (
1142 <div className="fixed inset-0 z-50 lg:hidden">
1143 <button
1144 type="button"
1145 aria-label="Close menu"
1146 className="absolute inset-0 bg-black/60"
1147 onClick={() => setDrawer(false)}
1148 />
1149 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
1150 <button
1151 type="button"
1152 aria-label="Close menu"
1153 onClick={() => setDrawer(false)}
1154 className="absolute top-3 right-3 rounded-md p-1 text-faint hover:bg-raised hover:text-fg"
1155 >
1156 <X size={16} />
1157 </button>
A Vercel-like sidebar and a new mark1158 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1159 </aside>
1160 </div>
1161 )}
1162
Polish: phones, copy boxes, the plan page, the landing page, a real glide1163 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals1164 <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 shell1165 <button
1166 type="button"
1167 aria-label="Open menu"
1168 onClick={() => setDrawer(true)}
1169 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1170 >
1171 <Menu size={18} />
1172 </button>
A Vercel-like sidebar and a new mark1173 <Breadcrumbs pathname={pathname} />
Search across all of g1t, Explore, and a command palette1174 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1175 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1176 <input
1177 name="q"
1178 {...notACredential()}
1179 placeholder="Search g1t"
1180 aria-label="Search g1t"
1181 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"
1182 />
1183 <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">
1184 ⌘K
1185 </kbd>
1186 </Form>
1187 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1188 <a
1189 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1190 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 mark1191 >
1192 Docs
1193 </a>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1194 {!user ? (
1195 // The sidebar has these too, but on a phone it is folded away.
1196 <Link
1197 to={withNext("/login", here)}
1198 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"
1199 >
1200 Sign in
1201 </Link>
1202 ) : (
Agents as a team: lifecycle, merge queue, billing and a new shell1203 <DropdownMenu>
1204 <DropdownMenuTrigger
1205 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1206 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 shell1207 >
A Vercel-like sidebar and a new mark1208 <Plus size={14} />
1209 New
Agents as a team: lifecycle, merge queue, billing and a new shell1210 </DropdownMenuTrigger>
1211 <DropdownMenuContent align="end">
1212 {shell.repo && (
1213 <DropdownMenuItem asChild>
1214 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1215 <CircleDot />
1216 New issue
1217 </Link>
1218 </DropdownMenuItem>
1219 )}
1220 {shell.repo?.member && (
1221 <DropdownMenuItem asChild>
1222 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1223 <ListTree />
1224 Plan work
1225 </Link>
1226 </DropdownMenuItem>
1227 )}
1228 <DropdownMenuItem asChild>
1229 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1230 <Box />
1231 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1232 </Link>
1233 </DropdownMenuItem>
1234 <DropdownMenuItem asChild>
1235 <Link to="/workspaces/new">
1236 <Users />
1237 New workspace
1238 </Link>
1239 </DropdownMenuItem>
1240 </DropdownMenuContent>
1241 </DropdownMenu>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1242 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1243 </div>
1244 </header>
1245 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1246 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1247 </div>
Search across all of g1t, Explore, and a command palette1248 <CommandPalette
1249 open={palette}
1250 onOpenChange={setPalette}
1251 commands={commands}
1252 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1253 />
Agents as a team: lifecycle, merge queue, billing and a new shell1254 </div>
1255 );
1256}