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,143 lines45,741 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,
27 LogOut,
28 Menu,
29 Plus,
30 Search,
Integrations: your own model provider, alerts that open issues, tickets agents read31 Plug,
Agents as a team: lifecycle, merge queue, billing and a new shell32 Settings,
33 Users,
Webhooks: every event, to your own addresses, signed and retried34 Webhook,
Project settings move into the sidebar, grouped, with a heading per page35 Globe,
36 GitBranch,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs37 PlayCircle,
Pull requests say where they are live; the project menu shows what is coming38 Bot,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains39 Brain,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API40 Network,
Pull requests say where they are live; the project menu shows what is coming41 ShieldCheck,
Deployments: a preview for every pull request, production on g1t.page42 Rocket,
Agents as a team: lifecycle, merge queue, billing and a new shell43 X,
44} from "lucide-react";
45import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Search across all of g1t, Explore, and a command palette46import { Form, Link, NavLink, useLocation, useNavigation, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell47
Workspace names and icons, and a component kit for every control48import type { Membership, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell49import { MICROS_PER_DOLLAR } from "@g1t/contracts";
50
Search across all of g1t, Explore, and a command palette51import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
A Vercel-like sidebar and a new mark52import { Mark } from "./logo";
Search across all of g1t, Explore, and a command palette53import { Avatar, notACredential } from "./ui";
Agents as a team: lifecycle, merge queue, billing and a new shell54import {
55 DropdownMenu,
56 DropdownMenuContent,
57 DropdownMenuItem,
58 DropdownMenuLabel,
59 DropdownMenuSeparator,
60 DropdownMenuTrigger,
61} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales62import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Agents as a team: lifecycle, merge queue, billing and a new shell63
64/** What the sidebar needs, worked out by the root loader for a signed-in person. */
65export type ShellData = {
66 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control67 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page68 /** Its projects, by name: `name` is the slug in their address. */
69 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
70 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell71 repo: {
72 namespace: string;
73 name: string;
74 member: boolean;
75 issues: number;
76 pulls: number;
77 } | null;
Billing accounts, terms and enterprises; g1t is no longer free78 /** Where the workspace stands against its usage limit, if billing is on. */
79 limit: {
80 exposureMicros: number;
81 ceilingMicros: number | null;
82 state: "ok" | "warning" | "stopped";
83 comped: boolean;
84 } | null;
Free while g1t is being built out; agents can check out their own forks85 /** Whether g1t charges nothing for now, while it is being built out. */
86 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request87 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy88 /** This month's usage: charged, or at cost while g1t is free. */
89 monthUsageMicros: number | null;
Agents as a team: lifecycle, merge queue, billing and a new shell90};
91
92function SidebarLink({
93 to,
94 icon,
95 end,
96 count,
97 also,
98 children,
99}: {
100 to: string;
101 icon: ReactNode;
102 end?: boolean;
103 count?: number;
Projects: what a workspace builds and runs, first on every page104 /** Other path prefixes under which this link is the current one. */
105 also?: string | string[];
Agents as a team: lifecycle, merge queue, billing and a new shell106 children: ReactNode;
107}) {
108 const { pathname } = useLocation();
109 return (
110 <NavLink
111 to={to}
112 end={end}
113 prefetch="intent"
114 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page115 const current =
116 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark117 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 shell118 current
119 ? "bg-raised font-medium text-fg"
120 : isPending
121 ? "bg-raised/60 text-fg"
122 : "text-muted hover:bg-raised/60 hover:text-fg"
123 }`;
124 }}
125 >
126 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
127 <span className="min-w-0 grow truncate">{children}</span>
128 {count != null && count > 0 && (
129 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
130 {count}
131 </span>
132 )}
133 </NavLink>
134 );
135}
136
Pull requests say where they are live; the project menu shows what is coming137/**
138 * Something a project will have, shown where it will live so people can
139 * see where g1t is going. Not a link: nothing is behind it yet.
140 */
141function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
142 return (
143 <div
144 title={about}
145 aria-disabled="true"
146 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
147 >
148 <span className="shrink-0 opacity-70">{icon}</span>
149 <span className="grow truncate">{children}</span>
150 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
151 Soon
152 </span>
153 </div>
154 );
155}
156
The project menu opens by section, and the workspace has its own panel157/**
A flat project menu with Code first, views as tabs, and Stripe for every owner158 * A page that is coming, as a link to its roadmap page: faint, with Soon,
159 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel160 */
A flat project menu with Code first, views as tabs, and Stripe for every owner161function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel162 to,
163 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner164 icon,
165 about,
The project menu opens by section, and the workspace has its own panel166 children,
167}: {
168 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner169 also?: string[];
170 icon: ReactNode;
171 about: string;
The project menu opens by section, and the workspace has its own panel172 children: ReactNode;
173}) {
174 const { pathname } = useLocation();
175 return (
176 <NavLink
177 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner178 title={about}
The project menu opens by section, and the workspace has its own panel179 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner180 className={({ isActive }) => {
181 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
182 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
183 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 panel184 }`;
185 }}
186 >
A flat project menu with Code first, views as tabs, and Stripe for every owner187 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel188 <span className="min-w-0 grow truncate">{children}</span>
189 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
190 Soon
191 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales192 </NavLink>
The project menu opens by section, and the workspace has its own panel193 );
194}
195
A flat project menu with Code first, views as tabs, and Stripe for every owner196/** Icons for what spans projects. */
197const WORKSPACE_ICONS: Record<string, ReactNode> = {
198 board: <KanbanSquare size={15} />,
199 roadmap: <GanttChart size={15} />,
200 packages: <Package size={15} />,
201 fleet: <Bot size={15} />,
202};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales203
A flat project menu with Code first, views as tabs, and Stripe for every owner204/** The Soon pages of a section, as paths, so its link is current on them. */
205function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
206 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales207}
208
Agents as a team: lifecycle, merge queue, billing and a new shell209function SidebarGroup({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {
210 return (
A Vercel-like sidebar and a new mark211 <section className="mt-6">
212 <div className="mb-1 flex h-6 items-center justify-between px-2">
213 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell214 {action}
215 </div>
216 <div className="space-y-px">{children}</div>
217 </section>
218 );
219}
220
Workspace names and icons, and a component kit for every control221/** What a workspace is called where people read it: its display name, else its slug. */
222function displayName(membership: Membership): string {
223 return membership.name?.trim() || membership.slug;
224}
225
Agents as a team: lifecycle, merge queue, billing and a new shell226function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control227 const workspace = shell.workspace;
228 const current = workspace?.slug;
229 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell230 return (
231 <DropdownMenu>
Workspace names and icons, and a component kit for every control232 {/* The name goes to the workspace; only the arrows switch it. The
233 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark234 <Link
235 to={current ? `/${current}` : "/workspaces/new"}
Workspace names and icons, and a component kit for every control236 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
237 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 mark238 >
Workspace names and icons, and a component kit for every control239 {workspace ? (
240 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell241 ) : (
Workspace names and icons, and a component kit for every control242 <span className="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell243 )}
Workspace names and icons, and a component kit for every control244 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark245 </Link>
246 <DropdownMenuTrigger
247 aria-label="Switch workspace"
Workspace names and icons, and a component kit for every control248 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 mark249 >
250 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell251 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control252 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell253 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
254 {(user.workspaces ?? []).map((membership) => (
255 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control256 <Link to={`/${membership.slug}`} title={displayName(membership)}>
257 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
258 <span className="flex min-w-0 grow flex-col leading-tight">
259 <span className="truncate">{displayName(membership)}</span>
260 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
261 </span>
262 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
263 {membership.slug === current ? (
264 <Check className="shrink-0 text-accent" />
265 ) : (
266 <span className="size-4 shrink-0" aria-hidden="true" />
267 )}
Agents as a team: lifecycle, merge queue, billing and a new shell268 </Link>
269 </DropdownMenuItem>
270 ))}
271 <DropdownMenuSeparator />
272 <DropdownMenuItem asChild>
273 <Link to="/workspaces/new">
274 <Plus />
275 New workspace
276 </Link>
277 </DropdownMenuItem>
278 </DropdownMenuContent>
279 </DropdownMenu>
280 );
281}
282
Billing accounts, terms and enterprises; g1t is no longer free283/**
284 * This month's usage, and how close the workspace is to its usage limit,
285 * as Vercel shows a plan's usage.
286 */
A Vercel-like sidebar and a new mark287function UsageCard({ slug, shell }: { slug: string; shell: ShellData }) {
Usage while free is shown at cost; agents get rustfmt and clippy288 if (shell.monthUsageMicros == null) return null;
289 const spent = shell.monthUsageMicros;
Billing accounts, terms and enterprises; g1t is no longer free290 const limit = shell.limit;
291 const ceiling = limit?.ceilingMicros ?? null;
292 const share = limit && ceiling ? Math.min(1, limit.exposureMicros / Math.max(ceiling, 1)) : 0;
293 const tone = limit?.state === "stopped" ? "text-danger" : limit?.state === "warning" ? "text-warn" : "text-faint";
294 const bar = limit?.state === "stopped" ? "bg-danger" : limit?.state === "warning" ? "bg-warn" : "bg-accent";
A Vercel-like sidebar and a new mark295 return (
296 <Link
297 to={`/${slug}/-/usage`}
298 className="block rounded-lg bg-surface p-3 ring-1 ring-line transition-colors hover:ring-line-strong"
299 >
300 <span className="flex items-baseline justify-between text-xs">
301 <span className="font-medium text-fg">Usage</span>
302 <span className="text-faint">this month</span>
303 </span>
304 <span className="mt-2 flex items-baseline justify-between">
305 <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 forks306 {shell.free ? (
307 <span className="text-xs text-accent">Free for now</span>
Billing accounts, terms and enterprises; g1t is no longer free308 ) : limit?.comped ? (
309 <span className="text-xs text-accent">Comped</span>
Free while g1t is being built out; agents can check out their own forks310 ) : (
Billing accounts, terms and enterprises; g1t is no longer free311 ceiling != null && (
312 <span className={`text-xs ${tone}`}>
313 {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 forks314 </span>
315 )
A Vercel-like sidebar and a new mark316 )}
317 </span>
Billing accounts, terms and enterprises; g1t is no longer free318 {ceiling != null && !limit?.comped && (
319 <span className="mt-2 block h-1 overflow-hidden rounded-full bg-raised">
320 <span
321 className={`block h-full rounded-full ${bar}`}
322 style={{ width: `${Math.max(share * 100, share > 0 ? 3 : 0)}%` }}
323 />
324 </span>
325 )}
A Vercel-like sidebar and a new mark326 </Link>
327 );
328}
329
330function AccountMenu({ user }: { user: User }) {
331 const submit = useSubmit();
332 return (
333 <DropdownMenu>
334 <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 control335 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark336 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
337 <Ellipsis size={15} className="shrink-0 text-faint" />
338 </DropdownMenuTrigger>
339 <DropdownMenuContent align="start" side="top" className="w-56">
340 <DropdownMenuLabel>
341 Signed in as <span className="font-mono font-medium text-fg">{user.username}</span>
342 </DropdownMenuLabel>
343 <DropdownMenuSeparator />
344 <DropdownMenuItem asChild>
345 <Link to="/settings">
346 <Settings />
347 Your settings
348 </Link>
349 </DropdownMenuItem>
350 <DropdownMenuItem asChild>
351 <a href="https://docs.g1t.sh/">
352 <BookOpen />
353 Documentation
354 </a>
355 </DropdownMenuItem>
356 <DropdownMenuSeparator />
357 {/* Submitted from here: the menu closes on select, and a button
358 that has left the page cannot submit a form. */}
359 <DropdownMenuItem onSelect={() => submit(null, { method: "post", action: "/logout" })}>
360 <LogOut />
361 Sign out
362 </DropdownMenuItem>
363 </DropdownMenuContent>
364 </DropdownMenu>
365 );
366}
367
A catalogue of model providers, and settings that feel like settings368/** A workspace's settings pages, which the sidebar slides over to. */
Project settings move into the sidebar, grouped, with a heading per page369const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets|guardrails|audit)(\/|$)/;
370/** A project's settings pages, which the project's menu gives way to. */
371const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings372
Polish: phones, copy boxes, the plan page, the landing page, a real glide373/**
Sidebar: no settings flash on the way into a repository, and a better slide374 * The sidebar's menus are two layers, the way a phone pushes a screen: the
375 * one arriving slides in from the right over the full width, while the one
376 * leaving drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide377 * ease-out, the fade quicker than the move so the two never blur together.
378 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide379 */
Sidebar: no settings flash on the way into a repository, and a better slide380const LAYER =
Sidebar: the panels really slide381 "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 slide382/** One menu, filling its layer. */
383const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings384
385/** A workspace's settings, as the sidebar shows them in place of everything else. */
386function SettingsMenu({ slug, owner, open }: { slug: string; owner: boolean; open: boolean }) {
387 return (
388 <nav
389 aria-label="Workspace settings"
390 inert={!open}
Polish: phones, copy boxes, the plan page, the landing page, a real glide391 className={PANEL}
A catalogue of model providers, and settings that feel like settings392 >
393 <Link
394 to={`/${slug}`}
395 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"
396 >
397 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
398 <span className="truncate">{slug}</span>
399 </Link>
400 <SidebarGroup title="Settings">
401 {owner && (
402 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
403 General
404 </SidebarLink>
405 )}
406 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />}>
407 Members
408 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments409 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
410 Access tokens
411 </SidebarLink>
A catalogue of model providers, and settings that feel like settings412 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments413 Billing and plans
A catalogue of model providers, and settings that feel like settings414 </SidebarLink>
415 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
416 Integrations
Webhooks: every event, to your own addresses, signed and retried417 </SidebarLink>
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs418 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
419 Secrets and variables
420 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API421 <SidebarLink to={`/${slug}/-/guardrails`} icon={<ShieldCheck size={15} />}>
422 Guardrails
423 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments424 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
425 Webhooks
A catalogue of model providers, and settings that feel like settings426 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API427 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
428 Audit log
429 </SidebarLink>
A catalogue of model providers, and settings that feel like settings430 </SidebarGroup>
431 </nav>
432 );
433}
434
Sidebar: no settings flash on the way into a repository, and a better slide435/**
436 * What the repository menu needs. Known from the address before the page's
437 * data arrives, so the menu is right from the first frame; the counts fill
438 * in once it does.
439 */
440type MenuRepo = {
441 namespace: string;
442 name: string;
443 member: boolean;
444 issues?: number;
445 pulls?: number;
446};
447
448function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
449 return (
450 a != null &&
451 b != null &&
452 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
453 a.name.toLowerCase() === b.name.toLowerCase()
454 );
455}
A repository has its own sidebar, as settings do456
457/**
Projects: what a workspace builds and runs, first on every page458 * A project's own menu, which the sidebar slides to while you are in it,
459 * as it does for settings: everything about the project, running and its
460 * code, and nothing else, with the way back to everything.
A repository has its own sidebar, as settings do461 */
Sidebar: no settings flash on the way into a repository, and a better slide462function RepoMenu({ repo, isPrivate, open }: { repo: MenuRepo; isPrivate: boolean; open: boolean }) {
A repository has its own sidebar, as settings do463 const base = `/${repo.namespace}/${repo.name}`;
464 return (
465 <nav aria-label={`${repo.namespace}/${repo.name}`} inert={!open} className={PANEL}>
466 <Link
467 to="/"
468 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"
469 >
470 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
471 Mission control
472 </Link>
A flat project menu with Code first, views as tabs, and Stripe for every owner473 <NavLink
A repository has its own sidebar, as settings do474 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner475 end
476 title="Overview"
477 className={({ isActive }) =>
478 `mt-3 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
479 }
A repository has its own sidebar, as settings do480 >
481 <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 page482 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do483 </span>
484 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
485 <span className="text-faint">{repo.namespace}/</span>
486 <span className="font-semibold text-fg">{repo.name}</span>
487 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner488 </NavLink>
489 {/* A project's pages, flat and in the order people use them. A page
490 with more than one view shows them as tabs across its top. */}
Projects: what a workspace builds and runs, first on every page491 <div className="mt-3 space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner492 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
493 Code
494 </SidebarLink>
495 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
496 Issues
497 </SidebarLink>
498 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
499 Pull requests
500 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains501 <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 owner502 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains503 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner504 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
505 Workflows
Projects: what a workspace builds and runs, first on every page506 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner507 {repo.member ? (
508 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
509 Deployments
510 </SidebarLink>
511 ) : null}
Observability gets its own page; tabs never overflow512 <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.">
513 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner514 </SidebarSoonLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API515 {repo.member ? (
516 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
517 Security
518 </SidebarLink>
519 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner520 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
521 Insights
522 </SidebarSoonLink>
Projects: what a workspace builds and runs, first on every page523 {repo.member && (
A flat project menu with Code first, views as tabs, and Stripe for every owner524 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />}>
525 Settings
526 </SidebarLink>
Projects: what a workspace builds and runs, first on every page527 )}
528 </div>
A repository has its own sidebar, as settings do529 </nav>
530 );
531}
532
Project settings move into the sidebar, grouped, with a heading per page533/**
534 * A project's settings, as the sidebar shows them in place of the project's
535 * menu: what is about running it, then its agents, then its code and who
536 * can reach it. The way back leads to the project.
537 */
538function RepoSettingsMenu({ repo, open }: { repo: MenuRepo; open: boolean }) {
539 const base = `/${repo.namespace}/${repo.name}`;
540 return (
541 <nav aria-label={`${repo.namespace}/${repo.name} settings`} inert={!open} className={PANEL}>
542 <Link
543 to={base}
544 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"
545 >
546 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
547 <span className="min-w-0 truncate font-mono">
548 <span className="text-faint">{repo.namespace}/</span>
549 {repo.name}
550 </span>
551 </Link>
552 <SidebarGroup title="Settings">
553 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
554 General
555 </SidebarLink>
556 </SidebarGroup>
557 <SidebarGroup title="Running">
558 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
559 Deployments
560 </SidebarLink>
561 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
562 Domains
563 </SidebarLink>
564 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
565 Dependencies
566 </SidebarLink>
567 </SidebarGroup>
568 <SidebarGroup title="Agents">
569 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
570 Agents
571 </SidebarLink>
572 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
573 Guardrails
574 </SidebarLink>
575 </SidebarGroup>
576 <SidebarGroup title="Code and access">
577 <SidebarLink to={`${base}/settings/repository`} icon={<GitBranch size={15} />}>
578 Repository
579 </SidebarLink>
580 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
581 Secrets and variables
582 </SidebarLink>
583 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
584 Webhooks
585 </SidebarLink>
586 </SidebarGroup>
587 </nav>
588 );
589}
590
Account settings in the same shape as a workspace's591/** Your own settings, as the sidebar shows them on the settings page. */
592function AccountSettingsMenu({ open }: { open: boolean }) {
593 const { hash } = useLocation();
594 const item = (id: string, icon: ReactNode, label: string) => (
595 <Link
596 to={`/settings#${id}`}
597 className={`group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
598 hash === `#${id}` ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"
599 }`}
600 >
601 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
602 {label}
603 </Link>
604 );
605 return (
606 <nav
607 aria-label="Your settings"
608 inert={!open}
Polish: phones, copy boxes, the plan page, the landing page, a real glide609 className={PANEL}
Account settings in the same shape as a workspace's610 >
611 <Link
612 to="/"
613 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"
614 >
615 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
616 Mission control
617 </Link>
618 <SidebarGroup title="Account">
Secrets and variables: one list, rows per environment, for workflows and deployments619 {item("ssh-keys", <Fingerprint size={15} />, "SSH keys")}
620 {item("tokens", <KeyRound size={15} />, "Access tokens")}
Account settings in the same shape as a workspace's621 {item("applications", <Plug size={15} />, "Connected applications")}
622 </SidebarGroup>
623 </nav>
624 );
625}
626
A Vercel-like sidebar and a new mark627function Sidebar({ user, shell, onFind }: { user: User; shell: ShellData; onFind: () => void }) {
Agents as a team: lifecycle, merge queue, billing and a new shell628 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings629 const { pathname } = useLocation();
630 const going = useNavigation().location?.pathname;
631 // On a workspace's settings page, or on the way to one, its settings take
632 // the sidebar over.
633 const inSettings = ws != null && SETTINGS_PAGE.exec(going ?? pathname)?.[1]?.toLowerCase() === ws.slug;
Account settings in the same shape as a workspace's634 const inAccount = (going ?? pathname) === "/settings";
A repository has its own sidebar, as settings do635 const active = shell.repo;
636 // In a repository, or on the way into one, its own menu takes the sidebar.
637 const target = going ?? pathname;
638 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains639 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 do640 const inRepo = repoPath != null && !reserved.has(repoPath[1]) && repoPath[2] !== "-";
Project settings move into the sidebar, grouped, with a heading per page641 // A project's settings take its menu over in turn.
642 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
A repository has its own sidebar, as settings do643 const away = inSettings || inAccount || inRepo;
Sidebar: no settings flash on the way into a repository, and a better slide644 // The repository the menu is for: the one loaded if it is the one being
645 // gone to, else what the address says, at once.
646 const targetRepo = inRepo && repoPath ? { namespace: repoPath[1], name: repoPath[2] } : null;
647 const menuRepo: MenuRepo | null = targetRepo
648 ? sameRepo(active, targetRepo)
649 ? active
650 : {
651 ...targetRepo,
652 member: (user.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
653 }
654 : null;
A repository has its own sidebar, as settings do655 // What sits on the far side of the track. Kept while sliding back, so it
656 // does not vanish on the way out.
Project settings move into the sidebar, grouped, with a heading per page657 const side = useRef<"workspace" | "account" | "repo" | "repo-settings">("workspace");
Polish: phones, copy boxes, the plan page, the landing page, a real glide658 if (inAccount) side.current = "account";
659 else if (inSettings) side.current = "workspace";
Project settings move into the sidebar, grouped, with a heading per page660 else if (inRepoSettings) side.current = "repo-settings";
A repository has its own sidebar, as settings do661 else if (inRepo) side.current = "repo";
662 // The repository last shown, kept for the slide back.
Sidebar: no settings flash on the way into a repository, and a better slide663 const shown = useRef<MenuRepo | null>(menuRepo);
664 if (menuRepo) shown.current = menuRepo;
665 // Moving between two of the far-side menus (settings to a repository,
666 // one repository to another) crossfades in place instead of sliding.
Project settings move into the sidebar, grouped, with a heading per page667 const detailKey =
668 side.current === "repo" || side.current === "repo-settings"
669 ? `${side.current}:${shown.current?.namespace}/${shown.current?.name}`
670 : side.current;
Sidebar: no settings flash on the way into a repository, and a better slide671 const lastAway = useRef(away);
672 const lastKey = useRef(detailKey);
673 const swapped = lastAway.current && away && lastKey.current !== detailKey;
674 useEffect(() => {
675 lastAway.current = away;
676 lastKey.current = detailKey;
677 });
Agents as a team: lifecycle, merge queue, billing and a new shell678 // The repository being looked at is listed even when it is someone else's.
679 const listed =
680 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
681 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
682 : shell.repos;
683 return (
684 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark685 {/* 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 control686 <div className="flex h-16 shrink-0 items-center gap-1 border-b border-line pr-2 pl-2.5">
687 <Link to="/" aria-label="g1t home" className="shrink-0 rounded-md p-1.5 hover:bg-raised">
Two limits, real invoices, trust that grows by itself, sales signals688 <Mark className="size-6" />
Agents as a team: lifecycle, merge queue, billing and a new shell689 </Link>
A Vercel-like sidebar and a new mark690 <span className="shrink-0 text-line-strong" aria-hidden="true">
691 /
692 </span>
693 <WorkspaceSwitcher user={user} shell={shell} />
Agents as a team: lifecycle, merge queue, billing and a new shell694 </div>
A Vercel-like sidebar and a new mark695 <div className="px-2 pt-3">
696 <button
697 type="button"
698 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals699 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 mark700 >
701 <Search size={14} />
702 <span className="grow text-left">Find…</span>
703 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
704 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell705 </div>
Sidebar: no settings flash on the way into a repository, and a better slide706 <div className="relative min-h-0 grow overflow-hidden">
707 <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 glide708 <nav aria-label="g1t" inert={away} className={PANEL}>
Agents as a team: lifecycle, merge queue, billing and a new shell709 <div className="mt-3 space-y-px">
710 <SidebarLink to="/" end icon={<LayoutDashboard size={15} />}>
711 Mission control
712 </SidebarLink>
713 <SidebarLink to="/explore" icon={<Compass size={15} />}>
714 Explore
715 </SidebarLink>
716 </div>
717
718 <SidebarGroup
Projects: what a workspace builds and runs, first on every page719 title="Projects"
Agents as a team: lifecycle, merge queue, billing and a new shell720 action={
721 ws && (
722 <Link
723 to={`/new?workspace=${ws.slug}`}
Projects: what a workspace builds and runs, first on every page724 aria-label="New project"
Agents as a team: lifecycle, merge queue, billing and a new shell725 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
726 >
727 <Plus size={13} />
728 </Link>
729 )
730 }
731 >
732 {listed.length === 0 && (
733 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
734 )}
735 {listed.map((repo) => {
736 const base = `/${repo.namespace}/${repo.name}`;
737 return (
738 <div key={base}>
739 <SidebarLink
740 to={base}
Projects: what a workspace builds and runs, first on every page741 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
Agents as a team: lifecycle, merge queue, billing and a new shell742 >
Projects: what a workspace builds and runs, first on every page743 <span className="text-[0.8125rem]">
Agents as a team: lifecycle, merge queue, billing and a new shell744 {repo.namespace !== ws?.slug && (
Projects: what a workspace builds and runs, first on every page745 <span className="font-mono text-faint">{repo.namespace}/</span>
Agents as a team: lifecycle, merge queue, billing and a new shell746 )}
Projects: what a workspace builds and runs, first on every page747 {"title" in repo && repo.title ? repo.title : repo.name}
Agents as a team: lifecycle, merge queue, billing and a new shell748 </span>
749 </SidebarLink>
750 </div>
751 );
752 })}
753 </SidebarGroup>
A flat project menu with Code first, views as tabs, and Stripe for every owner754 {ws && (
755 <SidebarGroup title="Across projects">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains756 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
757 Agent fleet
758 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API759 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
760 Context
761 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains762 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
763 Memory
764 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API765 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
766 Security
767 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner768 {roadmapIn("Workspace").map((item) => (
769 <SidebarSoonLink
770 key={item.key}
771 to={`/${ws.slug}/-/soon/${item.key}`}
772 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
773 about={item.summary}
774 >
775 {item.title === "Board" ? "Boards" : item.title}
776 </SidebarSoonLink>
777 ))}
778 </SidebarGroup>
779 )}
Agents as a team: lifecycle, merge queue, billing and a new shell780 </nav>
Polish: phones, copy boxes, the plan page, the landing page, a real glide781 </div>
Sidebar: no settings flash on the way into a repository, and a better slide782 <div className={`${LAYER} ${away ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}>
783 <div key={detailKey} className={`h-full ${swapped ? "animate-[g1t-swap_220ms_ease-out]" : ""}`}>
Project settings move into the sidebar, grouped, with a heading per page784 {side.current === "repo-settings" && shown.current ? (
785 <RepoSettingsMenu repo={shown.current} open={inRepoSettings} />
786 ) : side.current === "repo" && shown.current ? (
Sidebar: no settings flash on the way into a repository, and a better slide787 <RepoMenu
788 repo={shown.current}
789 isPrivate={shell.repos.some((repo) => sameRepo(repo, shown.current) && repo.isPrivate)}
790 open={inRepo}
791 />
792 ) : side.current === "account" || !ws ? (
793 <AccountSettingsMenu open={inAccount} />
794 ) : (
795 <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />
796 )}
797 </div>
798 </div>
A catalogue of model providers, and settings that feel like settings799 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell800
The project menu opens by section, and the workspace has its own panel801 {/* The workspace's own things sit at the bottom, by its usage and the
802 account: a panel of their own, ruled off from whatever menu is above,
803 however long it grows. */}
804 <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 control805 {ws && (
806 <div className="space-y-px">
The project menu opens by section, and the workspace has its own panel807 <p className="flex items-center gap-2 px-2 pb-1 text-[0.6875rem] font-medium uppercase tracking-wider text-faint">
808 <span>Workspace</span>
809 <span className="min-w-0 truncate font-mono normal-case tracking-normal text-muted">{ws.slug}</span>
810 </p>
Workspace things at the bottom of the sidebar, and on mission control811 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
812 Usage
813 </SidebarLink>
814 {/* Everything else about the workspace lives in its settings, and only there. */}
815 <SidebarLink
816 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/people`}
817 icon={<Settings size={15} />}
818 >
819 Settings
820 </SidebarLink>
821 </div>
822 )}
A Vercel-like sidebar and a new mark823 {ws && <UsageCard slug={ws.slug} shell={shell} />}
824 <AccountMenu user={user} />
825 </div>
826 </div>
827 );
828}
829
830/** Words for the sections a path can end in. */
831const SECTIONS: Record<string, string> = {
832 issues: "Issues",
833 pulls: "Pull requests",
834 queue: "Merge queue",
835 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales836 plans: "Outcomes",
837 actions: "Workflows",
838 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page839 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page840 repository: "Repository",
Project dependencies: addresses, preview stacks, Affects, and agents who know841 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales842 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs843 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark844 settings: "Settings",
A catalogue of model providers, and settings that feel like settings845 people: "Members",
A Vercel-like sidebar and a new mark846 tokens: "Access tokens",
847 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments848 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings849 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried850 webhooks: "Webhooks",
A Vercel-like sidebar and a new mark851 tree: "Code",
852 blob: "Code",
853};
854
855/** Where the page is, as a trail of links: workspace / repository / section. */
856function Breadcrumbs({ pathname }: { pathname: string }) {
857 const parts = pathname.split("/").filter(Boolean);
858 const reserved = ["settings", "explore", "new", "search", "workspaces"];
859 if (parts.length === 0) return <span className="text-sm font-medium">Mission control</span>;
860 if (reserved.includes(parts[0]!)) {
861 const words: Record<string, string> = {
Account settings in the same shape as a workspace's862 settings: "Account",
A Vercel-like sidebar and a new mark863 explore: "Explore",
Projects live: fixes from walking them end to end864 new: "New project",
A Vercel-like sidebar and a new mark865 search: "Search",
866 workspaces: "New workspace",
867 };
868 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
869 }
870 const [owner, second, third, fourth] = parts;
871 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
872 if (second === "-") {
A catalogue of model providers, and settings that feel like settings873 const page = `/${owner}/-/${third}`;
874 if (third && SETTINGS_PAGE.test(page)) {
875 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
876 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
877 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark878 } else if (second) {
879 const repo = `/${owner}/${second}`;
880 trail.push({ label: second, to: repo, mono: true });
881 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
882 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
883 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 deployments884 else if (third && SECTIONS[third]) {
885 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
886 // A settings page names which one: Settings / Secrets and variables.
887 if (third === "settings" && fourth && SECTIONS[fourth]) {
888 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
889 }
890 }
A Vercel-like sidebar and a new mark891 }
892 return (
893 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
894 {trail.map((crumb, index) => (
895 <span key={crumb.to} className="flex min-w-0 items-center gap-1.5">
896 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell897 <Link
A Vercel-like sidebar and a new mark898 to={crumb.to}
899 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
900 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
901 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell902 >
A Vercel-like sidebar and a new mark903 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell904 </Link>
A Vercel-like sidebar and a new mark905 </span>
906 ))}
907 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell908 );
909}
910
Search across all of g1t, Explore, and a command palette911type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell912
913/** Everything the palette can jump to, from what the sidebar already knows. */
914function commandsFor(user: User, shell: ShellData): Command[] {
915 const commands: Command[] = [
916 { label: "Mission control", to: "/", icon: <LayoutDashboard size={15} /> },
917 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette918 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page919 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell920 { label: "New workspace", to: "/workspaces/new", icon: <Plus size={15} /> },
921 { label: "Your settings", to: "/settings", icon: <Settings size={15} /> },
922 ];
923 const repo = shell.repo;
924 if (repo) {
925 const base = `/${repo.namespace}/${repo.name}`;
926 const name = `${repo.namespace}/${repo.name}`;
927 commands.unshift(
Projects: what a workspace builds and runs, first on every page928 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
929 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell930 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
931 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
932 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
933 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
934 ...(repo.member
935 ? [
936 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page937 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
938 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
939 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
940 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell941 ]
942 : []),
943 );
944 }
945 for (const membership of user.workspaces ?? []) {
946 commands.push(
Workspace names and icons, and a component kit for every control947 { 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 request948 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page949 { 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 settings950 { label: "Access tokens", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/tokens`, icon: <KeyRound size={15} /> },
951 { 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 shell952 );
953 }
954 for (const listed of shell.repos) {
955 commands.push({
Projects: what a workspace builds and runs, first on every page956 label: listed.title ?? listed.name,
957 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell958 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page959 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell960 });
961 }
962 return commands;
963}
964
965/**
966 * A bar across the top of the page while the next one loads, as GitHub
967 * has: it appears at once, creeps towards the end while waiting, then fills
968 * and fades when the page arrives.
969 */
970export function Progress() {
971 const navigation = useNavigation();
972 const busy = navigation.state !== "idle";
973 const [width, setWidth] = useState(0);
974 const [visible, setVisible] = useState(false);
975 useEffect(() => {
976 if (busy) {
977 setVisible(true);
978 setWidth(12);
979 // Slows as it goes, never quite arriving until the page does.
980 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
981 return () => clearInterval(timer);
982 }
983 setWidth((now) => (now > 0 ? 100 : 0));
984 const fade = setTimeout(() => setVisible(false), 250);
985 const reset = setTimeout(() => setWidth(0), 550);
986 return () => {
987 clearTimeout(fade);
988 clearTimeout(reset);
989 };
990 }, [busy]);
991 return (
992 <div
993 aria-hidden="true"
994 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
995 visible ? "opacity-100" : "opacity-0"
996 }`}
997 >
998 <div
999 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1000 style={{ width: `${width}%` }}
1001 />
1002 </div>
1003 );
1004}
1005
1006/**
1007 * The signed-in app: a sidebar with the workspace, its repositories and the
1008 * sections of the one being looked at; a slim bar with search and the
1009 * account; and the page.
1010 */
1011export function AppShell({
1012 user,
1013 shell,
1014 banner,
1015 children,
1016}: {
1017 user: User;
1018 shell: ShellData;
1019 banner?: ReactNode;
1020 children: ReactNode;
1021}) {
1022 const submit = useSubmit();
1023 const { pathname } = useLocation();
1024 const [drawer, setDrawer] = useState(false);
1025 const [palette, setPalette] = useState(false);
1026 const commands = useMemo(() => commandsFor(user, shell), [user, shell]);
1027
1028 // A new page closes the drawer on small screens.
1029 useEffect(() => setDrawer(false), [pathname]);
Search across all of g1t, Explore, and a command palette1030 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell1031
1032 return (
1033 <div className="min-h-screen">
1034 <Progress />
1035 <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 mark1036 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1037 </aside>
1038 {drawer && (
1039 <div className="fixed inset-0 z-50 lg:hidden">
1040 <button
1041 type="button"
1042 aria-label="Close menu"
1043 className="absolute inset-0 bg-black/60"
1044 onClick={() => setDrawer(false)}
1045 />
1046 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
1047 <button
1048 type="button"
1049 aria-label="Close menu"
1050 onClick={() => setDrawer(false)}
1051 className="absolute top-3 right-3 rounded-md p-1 text-faint hover:bg-raised hover:text-fg"
1052 >
1053 <X size={16} />
1054 </button>
A Vercel-like sidebar and a new mark1055 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1056 </aside>
1057 </div>
1058 )}
1059
Polish: phones, copy boxes, the plan page, the landing page, a real glide1060 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals1061 <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 shell1062 <button
1063 type="button"
1064 aria-label="Open menu"
1065 onClick={() => setDrawer(true)}
1066 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1067 >
1068 <Menu size={18} />
1069 </button>
A Vercel-like sidebar and a new mark1070 <Breadcrumbs pathname={pathname} />
Search across all of g1t, Explore, and a command palette1071 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1072 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1073 <input
1074 name="q"
1075 {...notACredential()}
1076 placeholder="Search g1t"
1077 aria-label="Search g1t"
1078 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"
1079 />
1080 <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">
1081 ⌘K
1082 </kbd>
1083 </Form>
1084 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1085 <a
1086 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1087 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 mark1088 >
1089 Docs
1090 </a>
Agents as a team: lifecycle, merge queue, billing and a new shell1091 <DropdownMenu>
1092 <DropdownMenuTrigger
1093 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1094 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 shell1095 >
A Vercel-like sidebar and a new mark1096 <Plus size={14} />
1097 New
Agents as a team: lifecycle, merge queue, billing and a new shell1098 </DropdownMenuTrigger>
1099 <DropdownMenuContent align="end">
1100 {shell.repo && (
1101 <DropdownMenuItem asChild>
1102 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1103 <CircleDot />
1104 New issue
1105 </Link>
1106 </DropdownMenuItem>
1107 )}
1108 {shell.repo?.member && (
1109 <DropdownMenuItem asChild>
1110 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1111 <ListTree />
1112 Plan work
1113 </Link>
1114 </DropdownMenuItem>
1115 )}
1116 <DropdownMenuItem asChild>
1117 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1118 <Box />
1119 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1120 </Link>
1121 </DropdownMenuItem>
1122 <DropdownMenuItem asChild>
1123 <Link to="/workspaces/new">
1124 <Users />
1125 New workspace
1126 </Link>
1127 </DropdownMenuItem>
1128 </DropdownMenuContent>
1129 </DropdownMenu>
1130 </div>
1131 </header>
1132 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1133 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1134 </div>
Search across all of g1t, Explore, and a command palette1135 <CommandPalette
1136 open={palette}
1137 onOpenChange={setPalette}
1138 commands={commands}
1139 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1140 />
Agents as a team: lifecycle, merge queue, billing and a new shell1141 </div>
1142 );
1143}