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,073 lines42,803 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,
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs35 PlayCircle,
Pull requests say where they are live; the project menu shows what is coming36 Bot,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains37 Brain,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API38 Network,
Pull requests say where they are live; the project menu shows what is coming39 ShieldCheck,
Deployments: a preview for every pull request, production on g1t.page40 Rocket,
Agents as a team: lifecycle, merge queue, billing and a new shell41 X,
42} from "lucide-react";
43import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Search across all of g1t, Explore, and a command palette44import { Form, Link, NavLink, useLocation, useNavigation, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell45
Workspace names and icons, and a component kit for every control46import type { Membership, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell47import { MICROS_PER_DOLLAR } from "@g1t/contracts";
48
Search across all of g1t, Explore, and a command palette49import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
A Vercel-like sidebar and a new mark50import { Mark } from "./logo";
Search across all of g1t, Explore, and a command palette51import { Avatar, notACredential } from "./ui";
Agents as a team: lifecycle, merge queue, billing and a new shell52import {
53 DropdownMenu,
54 DropdownMenuContent,
55 DropdownMenuItem,
56 DropdownMenuLabel,
57 DropdownMenuSeparator,
58 DropdownMenuTrigger,
59} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales60import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Agents as a team: lifecycle, merge queue, billing and a new shell61
62/** What the sidebar needs, worked out by the root loader for a signed-in person. */
63export type ShellData = {
64 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control65 workspace: Membership | null;
Projects: what a workspace builds and runs, first on every page66 /** Its projects, by name: `name` is the slug in their address. */
67 repos: { namespace: string; name: string; title?: string; isPrivate: boolean }[];
68 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell69 repo: {
70 namespace: string;
71 name: string;
72 member: boolean;
73 issues: number;
74 pulls: number;
75 } | null;
Billing accounts, terms and enterprises; g1t is no longer free76 /** Where the workspace stands against its usage limit, if billing is on. */
77 limit: {
78 exposureMicros: number;
79 ceilingMicros: number | null;
80 state: "ok" | "warning" | "stopped";
81 comped: boolean;
82 } | null;
Free while g1t is being built out; agents can check out their own forks83 /** Whether g1t charges nothing for now, while it is being built out. */
84 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request85 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy86 /** This month's usage: charged, or at cost while g1t is free. */
87 monthUsageMicros: number | null;
Agents as a team: lifecycle, merge queue, billing and a new shell88};
89
90function SidebarLink({
91 to,
92 icon,
93 end,
94 count,
95 also,
96 children,
97}: {
98 to: string;
99 icon: ReactNode;
100 end?: boolean;
101 count?: number;
Projects: what a workspace builds and runs, first on every page102 /** Other path prefixes under which this link is the current one. */
103 also?: string | string[];
Agents as a team: lifecycle, merge queue, billing and a new shell104 children: ReactNode;
105}) {
106 const { pathname } = useLocation();
107 return (
108 <NavLink
109 to={to}
110 end={end}
111 prefetch="intent"
112 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page113 const current =
114 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark115 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 shell116 current
117 ? "bg-raised font-medium text-fg"
118 : isPending
119 ? "bg-raised/60 text-fg"
120 : "text-muted hover:bg-raised/60 hover:text-fg"
121 }`;
122 }}
123 >
124 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
125 <span className="min-w-0 grow truncate">{children}</span>
126 {count != null && count > 0 && (
127 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
128 {count}
129 </span>
130 )}
131 </NavLink>
132 );
133}
134
Pull requests say where they are live; the project menu shows what is coming135/**
136 * Something a project will have, shown where it will live so people can
137 * see where g1t is going. Not a link: nothing is behind it yet.
138 */
139function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
140 return (
141 <div
142 title={about}
143 aria-disabled="true"
144 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
145 >
146 <span className="shrink-0 opacity-70">{icon}</span>
147 <span className="grow truncate">{children}</span>
148 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
149 Soon
150 </span>
151 </div>
152 );
153}
154
The project menu opens by section, and the workspace has its own panel155/**
A flat project menu with Code first, views as tabs, and Stripe for every owner156 * A page that is coming, as a link to its roadmap page: faint, with Soon,
157 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel158 */
A flat project menu with Code first, views as tabs, and Stripe for every owner159function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel160 to,
161 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner162 icon,
163 about,
The project menu opens by section, and the workspace has its own panel164 children,
165}: {
166 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner167 also?: string[];
168 icon: ReactNode;
169 about: string;
The project menu opens by section, and the workspace has its own panel170 children: ReactNode;
171}) {
172 const { pathname } = useLocation();
173 return (
174 <NavLink
175 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner176 title={about}
The project menu opens by section, and the workspace has its own panel177 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner178 className={({ isActive }) => {
179 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
180 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
181 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 panel182 }`;
183 }}
184 >
A flat project menu with Code first, views as tabs, and Stripe for every owner185 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel186 <span className="min-w-0 grow truncate">{children}</span>
187 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
188 Soon
189 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales190 </NavLink>
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/** Icons for what spans projects. */
195const WORKSPACE_ICONS: Record<string, ReactNode> = {
196 board: <KanbanSquare size={15} />,
197 roadmap: <GanttChart size={15} />,
198 packages: <Package size={15} />,
199 fleet: <Bot size={15} />,
200};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales201
A flat project menu with Code first, views as tabs, and Stripe for every owner202/** The Soon pages of a section, as paths, so its link is current on them. */
203function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
204 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales205}
206
Agents as a team: lifecycle, merge queue, billing and a new shell207function SidebarGroup({ title, action, children }: { title: string; action?: ReactNode; children: ReactNode }) {
208 return (
A Vercel-like sidebar and a new mark209 <section className="mt-6">
210 <div className="mb-1 flex h-6 items-center justify-between px-2">
211 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell212 {action}
213 </div>
214 <div className="space-y-px">{children}</div>
215 </section>
216 );
217}
218
Workspace names and icons, and a component kit for every control219/** What a workspace is called where people read it: its display name, else its slug. */
220function displayName(membership: Membership): string {
221 return membership.name?.trim() || membership.slug;
222}
223
Agents as a team: lifecycle, merge queue, billing and a new shell224function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control225 const workspace = shell.workspace;
226 const current = workspace?.slug;
227 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell228 return (
229 <DropdownMenu>
Workspace names and icons, and a component kit for every control230 {/* The name goes to the workspace; only the arrows switch it. The
231 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark232 <Link
233 to={current ? `/${current}` : "/workspaces/new"}
Workspace names and icons, and a component kit for every control234 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
235 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 mark236 >
Workspace names and icons, and a component kit for every control237 {workspace ? (
238 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell239 ) : (
Workspace names and icons, and a component kit for every control240 <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 shell241 )}
Workspace names and icons, and a component kit for every control242 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark243 </Link>
244 <DropdownMenuTrigger
245 aria-label="Switch workspace"
Workspace names and icons, and a component kit for every control246 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 mark247 >
248 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell249 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control250 <DropdownMenuContent align="start" className="w-64">
Agents as a team: lifecycle, merge queue, billing and a new shell251 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
252 {(user.workspaces ?? []).map((membership) => (
253 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control254 <Link to={`/${membership.slug}`} title={displayName(membership)}>
255 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
256 <span className="flex min-w-0 grow flex-col leading-tight">
257 <span className="truncate">{displayName(membership)}</span>
258 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
259 </span>
260 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
261 {membership.slug === current ? (
262 <Check className="shrink-0 text-accent" />
263 ) : (
264 <span className="size-4 shrink-0" aria-hidden="true" />
265 )}
Agents as a team: lifecycle, merge queue, billing and a new shell266 </Link>
267 </DropdownMenuItem>
268 ))}
269 <DropdownMenuSeparator />
270 <DropdownMenuItem asChild>
271 <Link to="/workspaces/new">
272 <Plus />
273 New workspace
274 </Link>
275 </DropdownMenuItem>
276 </DropdownMenuContent>
277 </DropdownMenu>
278 );
279}
280
Billing accounts, terms and enterprises; g1t is no longer free281/**
282 * This month's usage, and how close the workspace is to its usage limit,
283 * as Vercel shows a plan's usage.
284 */
A Vercel-like sidebar and a new mark285function UsageCard({ slug, shell }: { slug: string; shell: ShellData }) {
Usage while free is shown at cost; agents get rustfmt and clippy286 if (shell.monthUsageMicros == null) return null;
287 const spent = shell.monthUsageMicros;
Billing accounts, terms and enterprises; g1t is no longer free288 const limit = shell.limit;
289 const ceiling = limit?.ceilingMicros ?? null;
290 const share = limit && ceiling ? Math.min(1, limit.exposureMicros / Math.max(ceiling, 1)) : 0;
291 const tone = limit?.state === "stopped" ? "text-danger" : limit?.state === "warning" ? "text-warn" : "text-faint";
292 const bar = limit?.state === "stopped" ? "bg-danger" : limit?.state === "warning" ? "bg-warn" : "bg-accent";
A Vercel-like sidebar and a new mark293 return (
294 <Link
295 to={`/${slug}/-/usage`}
296 className="block rounded-lg bg-surface p-3 ring-1 ring-line transition-colors hover:ring-line-strong"
297 >
298 <span className="flex items-baseline justify-between text-xs">
299 <span className="font-medium text-fg">Usage</span>
300 <span className="text-faint">this month</span>
301 </span>
302 <span className="mt-2 flex items-baseline justify-between">
303 <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 forks304 {shell.free ? (
305 <span className="text-xs text-accent">Free for now</span>
Billing accounts, terms and enterprises; g1t is no longer free306 ) : limit?.comped ? (
307 <span className="text-xs text-accent">Comped</span>
Free while g1t is being built out; agents can check out their own forks308 ) : (
Billing accounts, terms and enterprises; g1t is no longer free309 ceiling != null && (
310 <span className={`text-xs ${tone}`}>
311 {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 forks312 </span>
313 )
A Vercel-like sidebar and a new mark314 )}
315 </span>
Billing accounts, terms and enterprises; g1t is no longer free316 {ceiling != null && !limit?.comped && (
317 <span className="mt-2 block h-1 overflow-hidden rounded-full bg-raised">
318 <span
319 className={`block h-full rounded-full ${bar}`}
320 style={{ width: `${Math.max(share * 100, share > 0 ? 3 : 0)}%` }}
321 />
322 </span>
323 )}
A Vercel-like sidebar and a new mark324 </Link>
325 );
326}
327
328function AccountMenu({ user }: { user: User }) {
329 const submit = useSubmit();
330 return (
331 <DropdownMenu>
332 <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 control333 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark334 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
335 <Ellipsis size={15} className="shrink-0 text-faint" />
336 </DropdownMenuTrigger>
337 <DropdownMenuContent align="start" side="top" className="w-56">
338 <DropdownMenuLabel>
339 Signed in as <span className="font-mono font-medium text-fg">{user.username}</span>
340 </DropdownMenuLabel>
341 <DropdownMenuSeparator />
342 <DropdownMenuItem asChild>
343 <Link to="/settings">
344 <Settings />
345 Your settings
346 </Link>
347 </DropdownMenuItem>
348 <DropdownMenuItem asChild>
349 <a href="https://docs.g1t.sh/">
350 <BookOpen />
351 Documentation
352 </a>
353 </DropdownMenuItem>
354 <DropdownMenuSeparator />
355 {/* Submitted from here: the menu closes on select, and a button
356 that has left the page cannot submit a form. */}
357 <DropdownMenuItem onSelect={() => submit(null, { method: "post", action: "/logout" })}>
358 <LogOut />
359 Sign out
360 </DropdownMenuItem>
361 </DropdownMenuContent>
362 </DropdownMenu>
363 );
364}
365
A catalogue of model providers, and settings that feel like settings366/** A workspace's settings pages, which the sidebar slides over to. */
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API367const SETTINGS_PAGE = /^\/([^/]+)\/-\/(settings|people|tokens|billing|integrations|webhooks|secrets|audit)(\/|$)/;
A catalogue of model providers, and settings that feel like settings368
Polish: phones, copy boxes, the plan page, the landing page, a real glide369/**
Sidebar: no settings flash on the way into a repository, and a better slide370 * The sidebar's menus are two layers, the way a phone pushes a screen: the
371 * one arriving slides in from the right over the full width, while the one
372 * leaving drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide373 * ease-out, the fade quicker than the move so the two never blur together.
374 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide375 */
Sidebar: no settings flash on the way into a repository, and a better slide376const LAYER =
Sidebar: the panels really slide377 "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 slide378/** One menu, filling its layer. */
379const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings380
381/** A workspace's settings, as the sidebar shows them in place of everything else. */
382function SettingsMenu({ slug, owner, open }: { slug: string; owner: boolean; open: boolean }) {
383 return (
384 <nav
385 aria-label="Workspace settings"
386 inert={!open}
Polish: phones, copy boxes, the plan page, the landing page, a real glide387 className={PANEL}
A catalogue of model providers, and settings that feel like settings388 >
389 <Link
390 to={`/${slug}`}
391 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"
392 >
393 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
394 <span className="truncate">{slug}</span>
395 </Link>
396 <SidebarGroup title="Settings">
397 {owner && (
398 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
399 General
400 </SidebarLink>
401 )}
402 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />}>
403 Members
404 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments405 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
406 Access tokens
407 </SidebarLink>
A catalogue of model providers, and settings that feel like settings408 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments409 Billing and plans
A catalogue of model providers, and settings that feel like settings410 </SidebarLink>
411 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
412 Integrations
Webhooks: every event, to your own addresses, signed and retried413 </SidebarLink>
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs414 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
415 Secrets and variables
416 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API417 <SidebarLink to={`/${slug}/-/guardrails`} icon={<ShieldCheck size={15} />}>
418 Guardrails
419 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments420 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
421 Webhooks
A catalogue of model providers, and settings that feel like settings422 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API423 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
424 Audit log
425 </SidebarLink>
A catalogue of model providers, and settings that feel like settings426 </SidebarGroup>
427 </nav>
428 );
429}
430
Sidebar: no settings flash on the way into a repository, and a better slide431/**
432 * What the repository menu needs. Known from the address before the page's
433 * data arrives, so the menu is right from the first frame; the counts fill
434 * in once it does.
435 */
436type MenuRepo = {
437 namespace: string;
438 name: string;
439 member: boolean;
440 issues?: number;
441 pulls?: number;
442};
443
444function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
445 return (
446 a != null &&
447 b != null &&
448 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
449 a.name.toLowerCase() === b.name.toLowerCase()
450 );
451}
A repository has its own sidebar, as settings do452
453/**
Projects: what a workspace builds and runs, first on every page454 * A project's own menu, which the sidebar slides to while you are in it,
455 * as it does for settings: everything about the project, running and its
456 * code, and nothing else, with the way back to everything.
A repository has its own sidebar, as settings do457 */
Sidebar: no settings flash on the way into a repository, and a better slide458function RepoMenu({ repo, isPrivate, open }: { repo: MenuRepo; isPrivate: boolean; open: boolean }) {
A repository has its own sidebar, as settings do459 const base = `/${repo.namespace}/${repo.name}`;
460 return (
461 <nav aria-label={`${repo.namespace}/${repo.name}`} inert={!open} className={PANEL}>
462 <Link
463 to="/"
464 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"
465 >
466 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
467 Mission control
468 </Link>
A flat project menu with Code first, views as tabs, and Stripe for every owner469 <NavLink
A repository has its own sidebar, as settings do470 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner471 end
472 title="Overview"
473 className={({ isActive }) =>
474 `mt-3 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
475 }
A repository has its own sidebar, as settings do476 >
477 <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 page478 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do479 </span>
480 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
481 <span className="text-faint">{repo.namespace}/</span>
482 <span className="font-semibold text-fg">{repo.name}</span>
483 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner484 </NavLink>
485 {/* A project's pages, flat and in the order people use them. A page
486 with more than one view shows them as tabs across its top. */}
Projects: what a workspace builds and runs, first on every page487 <div className="mt-3 space-y-px">
A flat project menu with Code first, views as tabs, and Stripe for every owner488 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
489 Code
490 </SidebarLink>
491 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
492 Issues
493 </SidebarLink>
494 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
495 Pull requests
496 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains497 <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 owner498 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains499 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner500 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
501 Workflows
Projects: what a workspace builds and runs, first on every page502 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner503 {repo.member ? (
504 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
505 Deployments
506 </SidebarLink>
507 ) : null}
Observability gets its own page; tabs never overflow508 <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.">
509 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner510 </SidebarSoonLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API511 {repo.member ? (
512 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
513 Security
514 </SidebarLink>
515 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner516 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
517 Insights
518 </SidebarSoonLink>
Projects: what a workspace builds and runs, first on every page519 {repo.member && (
A flat project menu with Code first, views as tabs, and Stripe for every owner520 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />}>
521 Settings
522 </SidebarLink>
Projects: what a workspace builds and runs, first on every page523 )}
524 </div>
A repository has its own sidebar, as settings do525 </nav>
526 );
527}
528
Account settings in the same shape as a workspace's529/** Your own settings, as the sidebar shows them on the settings page. */
530function AccountSettingsMenu({ open }: { open: boolean }) {
531 const { hash } = useLocation();
532 const item = (id: string, icon: ReactNode, label: string) => (
533 <Link
534 to={`/settings#${id}`}
535 className={`group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
536 hash === `#${id}` ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"
537 }`}
538 >
539 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
540 {label}
541 </Link>
542 );
543 return (
544 <nav
545 aria-label="Your settings"
546 inert={!open}
Polish: phones, copy boxes, the plan page, the landing page, a real glide547 className={PANEL}
Account settings in the same shape as a workspace's548 >
549 <Link
550 to="/"
551 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"
552 >
553 <ArrowLeft size={15} className="text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
554 Mission control
555 </Link>
556 <SidebarGroup title="Account">
Secrets and variables: one list, rows per environment, for workflows and deployments557 {item("ssh-keys", <Fingerprint size={15} />, "SSH keys")}
558 {item("tokens", <KeyRound size={15} />, "Access tokens")}
Account settings in the same shape as a workspace's559 {item("applications", <Plug size={15} />, "Connected applications")}
560 </SidebarGroup>
561 </nav>
562 );
563}
564
A Vercel-like sidebar and a new mark565function Sidebar({ user, shell, onFind }: { user: User; shell: ShellData; onFind: () => void }) {
Agents as a team: lifecycle, merge queue, billing and a new shell566 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings567 const { pathname } = useLocation();
568 const going = useNavigation().location?.pathname;
569 // On a workspace's settings page, or on the way to one, its settings take
570 // the sidebar over.
571 const inSettings = ws != null && SETTINGS_PAGE.exec(going ?? pathname)?.[1]?.toLowerCase() === ws.slug;
Account settings in the same shape as a workspace's572 const inAccount = (going ?? pathname) === "/settings";
A repository has its own sidebar, as settings do573 const active = shell.repo;
574 // In a repository, or on the way into one, its own menu takes the sidebar.
575 const target = going ?? pathname;
576 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Agents and memory, checks and conflicts, profiles, slug renames, custom domains577 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 do578 const inRepo = repoPath != null && !reserved.has(repoPath[1]) && repoPath[2] !== "-";
579 const away = inSettings || inAccount || inRepo;
Sidebar: no settings flash on the way into a repository, and a better slide580 // The repository the menu is for: the one loaded if it is the one being
581 // gone to, else what the address says, at once.
582 const targetRepo = inRepo && repoPath ? { namespace: repoPath[1], name: repoPath[2] } : null;
583 const menuRepo: MenuRepo | null = targetRepo
584 ? sameRepo(active, targetRepo)
585 ? active
586 : {
587 ...targetRepo,
588 member: (user.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
589 }
590 : null;
A repository has its own sidebar, as settings do591 // What sits on the far side of the track. Kept while sliding back, so it
592 // does not vanish on the way out.
593 const side = useRef<"workspace" | "account" | "repo">("workspace");
Polish: phones, copy boxes, the plan page, the landing page, a real glide594 if (inAccount) side.current = "account";
595 else if (inSettings) side.current = "workspace";
A repository has its own sidebar, as settings do596 else if (inRepo) side.current = "repo";
597 // The repository last shown, kept for the slide back.
Sidebar: no settings flash on the way into a repository, and a better slide598 const shown = useRef<MenuRepo | null>(menuRepo);
599 if (menuRepo) shown.current = menuRepo;
600 // Moving between two of the far-side menus (settings to a repository,
601 // one repository to another) crossfades in place instead of sliding.
602 const detailKey = side.current === "repo" ? `repo:${shown.current?.namespace}/${shown.current?.name}` : side.current;
603 const lastAway = useRef(away);
604 const lastKey = useRef(detailKey);
605 const swapped = lastAway.current && away && lastKey.current !== detailKey;
606 useEffect(() => {
607 lastAway.current = away;
608 lastKey.current = detailKey;
609 });
Agents as a team: lifecycle, merge queue, billing and a new shell610 // The repository being looked at is listed even when it is someone else's.
611 const listed =
612 active && !shell.repos.some((repo) => repo.namespace === active.namespace && repo.name === active.name)
613 ? [{ namespace: active.namespace, name: active.name, isPrivate: false }, ...shell.repos]
614 : shell.repos;
615 return (
616 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark617 {/* 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 control618 <div className="flex h-16 shrink-0 items-center gap-1 border-b border-line pr-2 pl-2.5">
619 <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 signals620 <Mark className="size-6" />
Agents as a team: lifecycle, merge queue, billing and a new shell621 </Link>
A Vercel-like sidebar and a new mark622 <span className="shrink-0 text-line-strong" aria-hidden="true">
623 /
624 </span>
625 <WorkspaceSwitcher user={user} shell={shell} />
Agents as a team: lifecycle, merge queue, billing and a new shell626 </div>
A Vercel-like sidebar and a new mark627 <div className="px-2 pt-3">
628 <button
629 type="button"
630 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals631 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 mark632 >
633 <Search size={14} />
634 <span className="grow text-left">Find…</span>
635 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
636 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell637 </div>
Sidebar: no settings flash on the way into a repository, and a better slide638 <div className="relative min-h-0 grow overflow-hidden">
639 <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 glide640 <nav aria-label="g1t" inert={away} className={PANEL}>
Agents as a team: lifecycle, merge queue, billing and a new shell641 <div className="mt-3 space-y-px">
642 <SidebarLink to="/" end icon={<LayoutDashboard size={15} />}>
643 Mission control
644 </SidebarLink>
645 <SidebarLink to="/explore" icon={<Compass size={15} />}>
646 Explore
647 </SidebarLink>
648 </div>
649
650 <SidebarGroup
Projects: what a workspace builds and runs, first on every page651 title="Projects"
Agents as a team: lifecycle, merge queue, billing and a new shell652 action={
653 ws && (
654 <Link
655 to={`/new?workspace=${ws.slug}`}
Projects: what a workspace builds and runs, first on every page656 aria-label="New project"
Agents as a team: lifecycle, merge queue, billing and a new shell657 className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg"
658 >
659 <Plus size={13} />
660 </Link>
661 )
662 }
663 >
664 {listed.length === 0 && (
665 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
666 )}
667 {listed.map((repo) => {
668 const base = `/${repo.namespace}/${repo.name}`;
669 return (
670 <div key={base}>
671 <SidebarLink
672 to={base}
Projects: what a workspace builds and runs, first on every page673 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
Agents as a team: lifecycle, merge queue, billing and a new shell674 >
Projects: what a workspace builds and runs, first on every page675 <span className="text-[0.8125rem]">
Agents as a team: lifecycle, merge queue, billing and a new shell676 {repo.namespace !== ws?.slug && (
Projects: what a workspace builds and runs, first on every page677 <span className="font-mono text-faint">{repo.namespace}/</span>
Agents as a team: lifecycle, merge queue, billing and a new shell678 )}
Projects: what a workspace builds and runs, first on every page679 {"title" in repo && repo.title ? repo.title : repo.name}
Agents as a team: lifecycle, merge queue, billing and a new shell680 </span>
681 </SidebarLink>
682 </div>
683 );
684 })}
685 </SidebarGroup>
A flat project menu with Code first, views as tabs, and Stripe for every owner686 {ws && (
687 <SidebarGroup title="Across projects">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains688 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
689 Agent fleet
690 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API691 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
692 Context
693 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains694 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
695 Memory
696 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API697 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
698 Security
699 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner700 {roadmapIn("Workspace").map((item) => (
701 <SidebarSoonLink
702 key={item.key}
703 to={`/${ws.slug}/-/soon/${item.key}`}
704 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
705 about={item.summary}
706 >
707 {item.title === "Board" ? "Boards" : item.title}
708 </SidebarSoonLink>
709 ))}
710 </SidebarGroup>
711 )}
Agents as a team: lifecycle, merge queue, billing and a new shell712 </nav>
Polish: phones, copy boxes, the plan page, the landing page, a real glide713 </div>
Sidebar: no settings flash on the way into a repository, and a better slide714 <div className={`${LAYER} ${away ? "translate-x-0 opacity-100" : "pointer-events-none translate-x-full opacity-0"}`}>
715 <div key={detailKey} className={`h-full ${swapped ? "animate-[g1t-swap_220ms_ease-out]" : ""}`}>
716 {side.current === "repo" && shown.current ? (
717 <RepoMenu
718 repo={shown.current}
719 isPrivate={shell.repos.some((repo) => sameRepo(repo, shown.current) && repo.isPrivate)}
720 open={inRepo}
721 />
722 ) : side.current === "account" || !ws ? (
723 <AccountSettingsMenu open={inAccount} />
724 ) : (
725 <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} open={inSettings} />
726 )}
727 </div>
728 </div>
A catalogue of model providers, and settings that feel like settings729 </div>
Agents as a team: lifecycle, merge queue, billing and a new shell730
The project menu opens by section, and the workspace has its own panel731 {/* The workspace's own things sit at the bottom, by its usage and the
732 account: a panel of their own, ruled off from whatever menu is above,
733 however long it grows. */}
734 <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 control735 {ws && (
736 <div className="space-y-px">
The project menu opens by section, and the workspace has its own panel737 <p className="flex items-center gap-2 px-2 pb-1 text-[0.6875rem] font-medium uppercase tracking-wider text-faint">
738 <span>Workspace</span>
739 <span className="min-w-0 truncate font-mono normal-case tracking-normal text-muted">{ws.slug}</span>
740 </p>
Workspace things at the bottom of the sidebar, and on mission control741 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
742 Usage
743 </SidebarLink>
744 {/* Everything else about the workspace lives in its settings, and only there. */}
745 <SidebarLink
746 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/people`}
747 icon={<Settings size={15} />}
748 >
749 Settings
750 </SidebarLink>
751 </div>
752 )}
A Vercel-like sidebar and a new mark753 {ws && <UsageCard slug={ws.slug} shell={shell} />}
754 <AccountMenu user={user} />
755 </div>
756 </div>
757 );
758}
759
760/** Words for the sections a path can end in. */
761const SECTIONS: Record<string, string> = {
762 issues: "Issues",
763 pulls: "Pull requests",
764 queue: "Merge queue",
765 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales766 plans: "Outcomes",
767 actions: "Workflows",
768 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page769 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page770 repository: "Repository",
Project dependencies: addresses, preview stacks, Affects, and agents who know771 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales772 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs773 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark774 settings: "Settings",
A catalogue of model providers, and settings that feel like settings775 people: "Members",
A Vercel-like sidebar and a new mark776 tokens: "Access tokens",
777 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments778 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings779 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried780 webhooks: "Webhooks",
A Vercel-like sidebar and a new mark781 tree: "Code",
782 blob: "Code",
783};
784
785/** Where the page is, as a trail of links: workspace / repository / section. */
786function Breadcrumbs({ pathname }: { pathname: string }) {
787 const parts = pathname.split("/").filter(Boolean);
788 const reserved = ["settings", "explore", "new", "search", "workspaces"];
789 if (parts.length === 0) return <span className="text-sm font-medium">Mission control</span>;
790 if (reserved.includes(parts[0]!)) {
791 const words: Record<string, string> = {
Account settings in the same shape as a workspace's792 settings: "Account",
A Vercel-like sidebar and a new mark793 explore: "Explore",
Projects live: fixes from walking them end to end794 new: "New project",
A Vercel-like sidebar and a new mark795 search: "Search",
796 workspaces: "New workspace",
797 };
798 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
799 }
800 const [owner, second, third, fourth] = parts;
801 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
802 if (second === "-") {
A catalogue of model providers, and settings that feel like settings803 const page = `/${owner}/-/${third}`;
804 if (third && SETTINGS_PAGE.test(page)) {
805 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
806 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
807 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark808 } else if (second) {
809 const repo = `/${owner}/${second}`;
810 trail.push({ label: second, to: repo, mono: true });
811 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
812 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
813 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 deployments814 else if (third && SECTIONS[third]) {
815 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
816 // A settings page names which one: Settings / Secrets and variables.
817 if (third === "settings" && fourth && SECTIONS[fourth]) {
818 trail.push({ label: SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
819 }
820 }
A Vercel-like sidebar and a new mark821 }
822 return (
823 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
824 {trail.map((crumb, index) => (
825 <span key={crumb.to} className="flex min-w-0 items-center gap-1.5">
826 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell827 <Link
A Vercel-like sidebar and a new mark828 to={crumb.to}
829 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
830 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
831 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell832 >
A Vercel-like sidebar and a new mark833 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell834 </Link>
A Vercel-like sidebar and a new mark835 </span>
836 ))}
837 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell838 );
839}
840
Search across all of g1t, Explore, and a command palette841type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell842
843/** Everything the palette can jump to, from what the sidebar already knows. */
844function commandsFor(user: User, shell: ShellData): Command[] {
845 const commands: Command[] = [
846 { label: "Mission control", to: "/", icon: <LayoutDashboard size={15} /> },
847 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette848 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Projects: what a workspace builds and runs, first on every page849 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell850 { label: "New workspace", to: "/workspaces/new", icon: <Plus size={15} /> },
851 { label: "Your settings", to: "/settings", icon: <Settings size={15} /> },
852 ];
853 const repo = shell.repo;
854 if (repo) {
855 const base = `/${repo.namespace}/${repo.name}`;
856 const name = `${repo.namespace}/${repo.name}`;
857 commands.unshift(
Projects: what a workspace builds and runs, first on every page858 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
859 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell860 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
861 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
862 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
863 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
864 ...(repo.member
865 ? [
866 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page867 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
868 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
869 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
870 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell871 ]
872 : []),
873 );
874 }
875 for (const membership of user.workspaces ?? []) {
876 commands.push(
Workspace names and icons, and a component kit for every control877 { 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 request878 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page879 { 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 settings880 { label: "Access tokens", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/tokens`, icon: <KeyRound size={15} /> },
881 { 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 shell882 );
883 }
884 for (const listed of shell.repos) {
885 commands.push({
Projects: what a workspace builds and runs, first on every page886 label: listed.title ?? listed.name,
887 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell888 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page889 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell890 });
891 }
892 return commands;
893}
894
895/**
896 * A bar across the top of the page while the next one loads, as GitHub
897 * has: it appears at once, creeps towards the end while waiting, then fills
898 * and fades when the page arrives.
899 */
900export function Progress() {
901 const navigation = useNavigation();
902 const busy = navigation.state !== "idle";
903 const [width, setWidth] = useState(0);
904 const [visible, setVisible] = useState(false);
905 useEffect(() => {
906 if (busy) {
907 setVisible(true);
908 setWidth(12);
909 // Slows as it goes, never quite arriving until the page does.
910 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
911 return () => clearInterval(timer);
912 }
913 setWidth((now) => (now > 0 ? 100 : 0));
914 const fade = setTimeout(() => setVisible(false), 250);
915 const reset = setTimeout(() => setWidth(0), 550);
916 return () => {
917 clearTimeout(fade);
918 clearTimeout(reset);
919 };
920 }, [busy]);
921 return (
922 <div
923 aria-hidden="true"
924 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
925 visible ? "opacity-100" : "opacity-0"
926 }`}
927 >
928 <div
929 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
930 style={{ width: `${width}%` }}
931 />
932 </div>
933 );
934}
935
936/**
937 * The signed-in app: a sidebar with the workspace, its repositories and the
938 * sections of the one being looked at; a slim bar with search and the
939 * account; and the page.
940 */
941export function AppShell({
942 user,
943 shell,
944 banner,
945 children,
946}: {
947 user: User;
948 shell: ShellData;
949 banner?: ReactNode;
950 children: ReactNode;
951}) {
952 const submit = useSubmit();
953 const { pathname } = useLocation();
954 const [drawer, setDrawer] = useState(false);
955 const [palette, setPalette] = useState(false);
956 const commands = useMemo(() => commandsFor(user, shell), [user, shell]);
957
958 // A new page closes the drawer on small screens.
959 useEffect(() => setDrawer(false), [pathname]);
Search across all of g1t, Explore, and a command palette960 usePaletteShortcut(() => setPalette((open) => !open));
Agents as a team: lifecycle, merge queue, billing and a new shell961
962 return (
963 <div className="min-h-screen">
964 <Progress />
965 <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 mark966 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell967 </aside>
968 {drawer && (
969 <div className="fixed inset-0 z-50 lg:hidden">
970 <button
971 type="button"
972 aria-label="Close menu"
973 className="absolute inset-0 bg-black/60"
974 onClick={() => setDrawer(false)}
975 />
976 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
977 <button
978 type="button"
979 aria-label="Close menu"
980 onClick={() => setDrawer(false)}
981 className="absolute top-3 right-3 rounded-md p-1 text-faint hover:bg-raised hover:text-fg"
982 >
983 <X size={16} />
984 </button>
A Vercel-like sidebar and a new mark985 <Sidebar user={user} shell={shell} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell986 </aside>
987 </div>
988 )}
989
Polish: phones, copy boxes, the plan page, the landing page, a real glide990 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Two limits, real invoices, trust that grows by itself, sales signals991 <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 shell992 <button
993 type="button"
994 aria-label="Open menu"
995 onClick={() => setDrawer(true)}
996 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
997 >
998 <Menu size={18} />
999 </button>
A Vercel-like sidebar and a new mark1000 <Breadcrumbs pathname={pathname} />
Search across all of g1t, Explore, and a command palette1001 <Form action="/search" role="search" className="relative ml-auto hidden w-full max-w-64 md:block">
1002 <Search size={14} className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" />
1003 <input
1004 name="q"
1005 {...notACredential()}
1006 placeholder="Search g1t"
1007 aria-label="Search g1t"
1008 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"
1009 />
1010 <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">
1011 ⌘K
1012 </kbd>
1013 </Form>
1014 <div className="ml-auto flex items-center gap-1.5 md:ml-0">
A Vercel-like sidebar and a new mark1015 <a
1016 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1017 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 mark1018 >
1019 Docs
1020 </a>
Agents as a team: lifecycle, merge queue, billing and a new shell1021 <DropdownMenu>
1022 <DropdownMenuTrigger
1023 aria-label="Create"
Two limits, real invoices, trust that grows by itself, sales signals1024 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 shell1025 >
A Vercel-like sidebar and a new mark1026 <Plus size={14} />
1027 New
Agents as a team: lifecycle, merge queue, billing and a new shell1028 </DropdownMenuTrigger>
1029 <DropdownMenuContent align="end">
1030 {shell.repo && (
1031 <DropdownMenuItem asChild>
1032 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1033 <CircleDot />
1034 New issue
1035 </Link>
1036 </DropdownMenuItem>
1037 )}
1038 {shell.repo?.member && (
1039 <DropdownMenuItem asChild>
1040 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1041 <ListTree />
1042 Plan work
1043 </Link>
1044 </DropdownMenuItem>
1045 )}
1046 <DropdownMenuItem asChild>
1047 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1048 <Box />
1049 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1050 </Link>
1051 </DropdownMenuItem>
1052 <DropdownMenuItem asChild>
1053 <Link to="/workspaces/new">
1054 <Users />
1055 New workspace
1056 </Link>
1057 </DropdownMenuItem>
1058 </DropdownMenuContent>
1059 </DropdownMenu>
1060 </div>
1061 </header>
1062 {banner}
Polish: phones, copy boxes, the plan page, the landing page, a real glide1063 <main className="min-w-0 grow">{children}</main>
Agents as a team: lifecycle, merge queue, billing and a new shell1064 </div>
Search across all of g1t, Explore, and a command palette1065 <CommandPalette
1066 open={palette}
1067 onOpenChange={setPalette}
1068 commands={commands}
1069 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1070 />
Agents as a team: lifecycle, merge queue, billing and a new shell1071 </div>
1072 );
1073}