Skip to content
1,815 linesCodeBlameRaw

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.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1import { Activity, BarChart3, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, KanbanSquare, KeyRound, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Sparkles, Ticket, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all5import type { Abilities, InboxCounts, Membership, Spike, User } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Search across all of g1t, Explore, and a command palette7import { CommandPalette, type PaletteCommand, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar8import { AgentButton, InboxBell } from "./inbox";
9import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas11import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height12import { Logo, Mark } from "./logo";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar13import { Avatar } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents14import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell15import {
16 DropdownMenu,
17 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas18 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell19 DropdownMenuItem,
20 DropdownMenuLabel,
21 DropdownMenuSeparator,
22 DropdownMenuTrigger,
23} from "./ui/dropdown-menu";
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales24import { type RoadmapItem, roadmapIn } from "../lib/roadmap";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25import { SETTINGS_CAPABILITY, type ViewerAccess, seesSettings } from "../lib/access";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put26import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas27import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
28import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put29import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas31import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar32import { type ShortcutProject, movedPin, recentWith } from "../lib/pins";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas33import type { AccountMenuData } from "../routes/settings-menu-json";
Agents as a team: lifecycle, merge queue, billing and a new shell34
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put35/**
36 * What the sidebar needs, worked out by the root loader. For a visitor who
37 * is not signed in there is no workspace, no projects and no usage: only
38 * the project being looked at, if they can see it.
39 */
Agents as a team: lifecycle, merge queue, billing and a new shell40export type ShellData = {
41 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control42 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar43 /** All its projects, by name, for the palette: `name` is the slug in their address. */
44 repos: ShortcutProject[];
45 /** The person's pinned projects in it, in their order (lib/pins.ts). */
46 pinned?: ShortcutProject[];
47 /** What they opened there last, latest first, leaving out the pinned. */
48 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look49 /** Repositories shared with them in workspaces they do not belong to. */
50 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page51 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell52 repo: {
53 namespace: string;
54 name: string;
55 member: boolean;
56 issues: number;
57 pulls: number;
58 } | null;
Billing accounts, terms and enterprises; g1t is no longer free59 /** Where the workspace stands against its usage limit, if billing is on. */
60 limit: {
61 exposureMicros: number;
62 ceilingMicros: number | null;
63 state: "ok" | "warning" | "stopped";
64 comped: boolean;
65 } | null;
Free while g1t is being built out; agents can check out their own forks66 /** Whether g1t charges nothing for now, while it is being built out. */
67 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request68 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy69 /** This month's usage: charged, or at cost while g1t is free. */
70 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look71 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
72 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all73 /** What is unread in their inbox, for the bell. Absent for a visitor. */
74 inbox?: InboxCounts | null;
Agents as a team: lifecycle, merge queue, billing and a new shell75};
76
77function SidebarLink({
78 to,
79 icon,
80 end,
81 count,
82 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look83 drill,
Agents as a team: lifecycle, merge queue, billing and a new shell84 children,
85}: {
86 to: string;
87 icon: ReactNode;
88 end?: boolean;
89 count?: number;
Projects: what a workspace builds and runs, first on every page90 /** Other path prefixes under which this link is the current one. */
91 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look92 /** It opens a list of its own: a chevron says so, always or on hover. */
93 drill?: boolean | "hover";
Agents as a team: lifecycle, merge queue, billing and a new shell94 children: ReactNode;
95}) {
96 const { pathname } = useLocation();
97 return (
98 <NavLink
99 to={to}
100 end={end}
101 prefetch="intent"
102 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page103 const current =
104 isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
A Vercel-like sidebar and a new mark105 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 shell106 current
107 ? "bg-raised font-medium text-fg"
108 : isPending
109 ? "bg-raised/60 text-fg"
110 : "text-muted hover:bg-raised/60 hover:text-fg"
111 }`;
112 }}
113 >
114 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
115 <span className="min-w-0 grow truncate">{children}</span>
116 {count != null && count > 0 && (
117 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
118 {count}
119 </span>
120 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look121 {drill && (
122 <ChevronRight
123 size={14}
124 aria-hidden="true"
125 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
126 />
127 )}
Agents as a team: lifecycle, merge queue, billing and a new shell128 </NavLink>
129 );
130}
131
Pull requests say where they are live; the project menu shows what is coming132/**
133 * Something a project will have, shown where it will live so people can
134 * see where g1t is going. Not a link: nothing is behind it yet.
135 */
136function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
137 return (
138 <div
139 title={about}
140 aria-disabled="true"
141 className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint"
142 >
143 <span className="shrink-0 opacity-70">{icon}</span>
144 <span className="grow truncate">{children}</span>
145 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
146 Soon
147 </span>
148 </div>
149 );
150}
151
The project menu opens by section, and the workspace has its own panel152/**
A flat project menu with Code first, views as tabs, and Stripe for every owner153 * A page that is coming, as a link to its roadmap page: faint, with Soon,
154 * and marked current on any of its section's Soon pages.
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 owner156function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel157 to,
158 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner159 icon,
160 about,
The project menu opens by section, and the workspace has its own panel161 children,
162}: {
163 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner164 also?: string[];
165 icon: ReactNode;
166 about: string;
The project menu opens by section, and the workspace has its own panel167 children: ReactNode;
168}) {
169 const { pathname } = useLocation();
170 return (
171 <NavLink
172 to={to}
A flat project menu with Code first, views as tabs, and Stripe for every owner173 title={about}
The project menu opens by section, and the workspace has its own panel174 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner175 className={({ isActive }) => {
176 const current = isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/"));
177 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
178 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 panel179 }`;
180 }}
181 >
A flat project menu with Code first, views as tabs, and Stripe for every owner182 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel183 <span className="min-w-0 grow truncate">{children}</span>
184 <span className="rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
185 Soon
186 </span>
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales187 </NavLink>
The project menu opens by section, and the workspace has its own panel188 );
189}
190
A flat project menu with Code first, views as tabs, and Stripe for every owner191/** Icons for what spans projects. */
192const WORKSPACE_ICONS: Record<string, ReactNode> = {
193 board: <KanbanSquare size={15} />,
194 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member195 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner196 packages: <Package size={15} />,
197 fleet: <Bot size={15} />,
198};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales199
A flat project menu with Code first, views as tabs, and Stripe for every owner200/** The Soon pages of a section, as paths, so its link is current on them. */
201function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
202 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales203}
204
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look205function SidebarGroup({
206 title,
207 action,
208 className = "",
209 children,
210}: {
211 title: string;
212 action?: ReactNode;
213 className?: string;
214 children: ReactNode;
215}) {
Agents as a team: lifecycle, merge queue, billing and a new shell216 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look217 <section className={className}>
A Vercel-like sidebar and a new mark218 <div className="mb-1 flex h-6 items-center justify-between px-2">
219 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell220 {action}
221 </div>
222 <div className="space-y-px">{children}</div>
223 </section>
224 );
225}
226
Workspace names and icons, and a component kit for every control227/** What a workspace is called where people read it: its display name, else its slug. */
228function displayName(membership: Membership): string {
229 return membership.name?.trim() || membership.slug;
230}
231
Agents as a team: lifecycle, merge queue, billing and a new shell232function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control233 const workspace = shell.workspace;
234 const current = workspace?.slug;
235 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell236 return (
237 <DropdownMenu>
Workspace names and icons, and a component kit for every control238 {/* The name goes to the workspace; only the arrows switch it. The
239 name gets all the room there is, and the whole of it on hover. */}
A Vercel-like sidebar and a new mark240 <Link
241 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents242 prefetch="intent"
Workspace names and icons, and a component kit for every control243 title={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look244 className="flex h-9 min-w-0 grow items-center gap-2 rounded-md px-2 transition-colors hover:bg-raised"
A Vercel-like sidebar and a new mark245 >
Workspace names and icons, and a component kit for every control246 {workspace ? (
247 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell248 ) : (
Workspace names and icons, and a component kit for every control249 <span className="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell250 )}
Workspace names and icons, and a component kit for every control251 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark252 </Link>
253 <DropdownMenuTrigger
254 aria-label="Switch workspace"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look255 className="flex h-9 w-7 shrink-0 items-center justify-center rounded-md text-faint outline-none transition-colors hover:bg-raised hover:text-fg data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark256 >
257 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell258 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control259 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar260 {workspace && (
261 <>
262 {/* The workspace's own page: its projects, packages and people. */}
263 <DropdownMenuItem asChild>
264 <Link to={`/${workspace.slug}`}>
265 <LayoutGrid />
266 Workspace overview
267 </Link>
268 </DropdownMenuItem>
269 <DropdownMenuItem asChild>
270 <Link to={`/${workspace.slug}/-/projects`}>
271 <Box />
272 All projects
273 </Link>
274 </DropdownMenuItem>
275 <DropdownMenuSeparator />
276 </>
277 )}
Agents as a team: lifecycle, merge queue, billing and a new shell278 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
279 {(user.workspaces ?? []).map((membership) => (
280 <DropdownMenuItem asChild key={membership.slug}>
Workspace names and icons, and a component kit for every control281 <Link to={`/${membership.slug}`} title={displayName(membership)}>
282 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
283 <span className="flex min-w-0 grow flex-col leading-tight">
284 <span className="truncate">{displayName(membership)}</span>
285 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
286 </span>
287 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
288 {membership.slug === current ? (
289 <Check className="shrink-0 text-accent" />
290 ) : (
291 <span className="size-4 shrink-0" aria-hidden="true" />
292 )}
Agents as a team: lifecycle, merge queue, billing and a new shell293 </Link>
294 </DropdownMenuItem>
295 ))}
296 <DropdownMenuSeparator />
297 <DropdownMenuItem asChild>
298 <Link to="/workspaces/new">
299 <Plus />
300 New workspace
301 </Link>
302 </DropdownMenuItem>
303 </DropdownMenuContent>
304 </DropdownMenu>
A Vercel-like sidebar and a new mark305 );
306}
307
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas308/**
309 * The fine print, as one quiet row at the foot of the account menu and the
310 * visitor's panel: each link a full-height target, left-aligned. In the
311 * menu each is a menu item, so the arrow keys reach it.
312 */
313const LEGAL_LINKS: [string, string][] = [
314 ["Policies", "/policies"],
315 ["Privacy", "/policies/privacy"],
316 ["Security", "/security"],
317];
318const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
319
320function MenuLegalRow() {
321 return (
322 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
323 {LEGAL_LINKS.map(([label, to], index) => (
324 <span key={to} className="flex items-center gap-0.5">
325 {index > 0 && (
326 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
327 ·
328 </span>
329 )}
330 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
331 <Link to={to}>{label}</Link>
332 </DropdownMenuItem>
333 </span>
334 ))}
335 </div>
336 );
337}
338
339/** Status's row: the live dot and two or three words, fetched when the menu opens. */
340function StatusSummary({ open }: { open: boolean }) {
341 const status = useSiteStatus(open);
342 const words = statusTitle(status);
343 return (
344 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
345 {/* The words say it; the dot's own words for screen readers would repeat them. */}
346 <span aria-hidden="true" className="flex">
347 <StatusDot state={status?.overall.state ?? null} />
348 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents349 {words ? <span className="truncate">{words}</span> : <Skeleton className="h-3 w-20" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas350 </span>
351 );
352}
353
354/** A menu row: an icon, words, and whatever sits at its end. */
355const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
356
A Vercel-like sidebar and a new mark357function AccountMenu({ user }: { user: User }) {
358 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas359 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents360 // Name, primary address and invites left: asked for once, as soon as the
361 // pointer or focus reaches the button, so they are there when it opens.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas362 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents363 const prefetch = () => {
364 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
365 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas366 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents367 if (open) prefetch();
368 // eslint-disable-next-line react-hooks/exhaustive-deps
369 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas370 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents371 // Still on its way: shapes where the words will be, the same size.
372 const loading = details.data === undefined && details.state !== "idle";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas373 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark374 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas375 <DropdownMenu open={open} onOpenChange={setOpen}>
376 <DropdownMenuTrigger
377 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents378 onPointerEnter={prefetch}
379 onFocus={prefetch}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas380 className="flex h-10 w-full items-center gap-2.5 rounded-md px-2 text-left outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-merged/60 data-[state=open]:bg-raised"
381 >
Workspace names and icons, and a component kit for every control382 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark383 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas384 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark385 </DropdownMenuTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas386 <DropdownMenuContent align="start" side="top" collisionPadding={8} className="w-[17.5rem] p-1.5">
387 {/* Who is signed in, and a way to their profile. */}
388 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
389 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
390 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents391 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
392 {loading ? (
393 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
394 ) : (
395 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
396 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas397 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents398 {loading ? (
399 <Skeleton className="mt-1 h-3 w-40" />
400 ) : (
401 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
402 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas403 </span>
A Vercel-like sidebar and a new mark404 </Link>
405 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas406 <DropdownMenuSeparator className="my-1.5" />
407 <DropdownMenuGroup>
408 <DropdownMenuItem asChild className={MENU_ROW}>
409 <Link to={profile}>
410 <CircleUserRound />
411 Your profile
412 </Link>
413 </DropdownMenuItem>
414 <DropdownMenuItem asChild className={MENU_ROW}>
415 <Link to={FIRST_SETTINGS_PAGE}>
416 <Settings />
417 Your settings
418 </Link>
419 </DropdownMenuItem>
420 <DropdownMenuItem asChild className={MENU_ROW}>
421 <Link to="/settings/invites">
422 <Ticket />
423 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents424 {loading && <Skeleton className="ml-auto h-4 w-12 rounded-full" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas425 {me?.invites_left != null && (
426 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
427 {me.invites_left} left
428 </span>
429 )}
430 </Link>
431 </DropdownMenuItem>
432 </DropdownMenuGroup>
433 <DropdownMenuSeparator className="my-1.5" />
434 <DropdownMenuGroup>
435 <DropdownMenuItem asChild className={MENU_ROW}>
436 <a href="https://docs.g1t.sh/">
437 <BookOpen />
438 Documentation
439 </a>
440 </DropdownMenuItem>
441 <DropdownMenuItem asChild className={MENU_ROW}>
442 <Link to="/support">
443 <LifeBuoy />
444 Support
445 </Link>
446 </DropdownMenuItem>
447 <DropdownMenuItem asChild className={MENU_ROW}>
448 <a href={STATUS_URL}>
449 <Activity />
450 Status
451 <StatusSummary open={open} />
452 </a>
453 </DropdownMenuItem>
454 </DropdownMenuGroup>
455 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark456 {/* Submitted from here: the menu closes on select, and a button
457 that has left the page cannot submit a form. */}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas458 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark459 <LogOut />
460 Sign out
461 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas462 <DropdownMenuSeparator className="my-1.5" />
463 <MenuLegalRow />
A Vercel-like sidebar and a new mark464 </DropdownMenuContent>
465 </DropdownMenu>
466 );
467}
468
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look469/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar470 * A workspace's settings pages, which the sidebar drills into: how it is
471 * set up and connected (guardrails, secrets, runners, integrations,
472 * webhooks), what it pays, its repositories, tokens and record. The main
473 * list keeps the places work happens and who belongs; every member can
474 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look475 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar476const SETTINGS_PAGE =
477 /^\/([^/]+)\/-\/(settings|repositories|tokens|guardrails|secrets|runners|integrations|webhooks|billing|audit)(\/|$)/;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look478/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page479const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings480
Polish: phones, copy boxes, the plan page, the landing page, a real glide481/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look482 * The sidebar's lists are a stack, the way a phone pushes a screen: one
483 * drilled into slides in from the right over the full width, while the one
484 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide485 * ease-out, the fade quicker than the move so the two never blur together.
486 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide487 */
Sidebar: no settings flash on the way into a repository, and a better slide488const LAYER =
Sidebar: the panels really slide489 "absolute inset-0 [transition:translate_380ms_cubic-bezier(0.32,0.72,0,1),opacity_220ms_ease-out] will-change-[translate,opacity] motion-reduce:transition-none";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look490/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide491const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings492
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look493/** One list in the stack: what it is, and its rows. */
494type Level = { key: string; node: ReactNode };
495
496/** True once the page has hydrated, so what the server drew never animates in. */
497function useHydrated(): boolean {
498 const [hydrated, setHydrated] = useState(false);
499 useEffect(() => setHydrated(true), []);
500 return hydrated;
501}
502
503/**
504 * Drilling in and out of the sidebar's lists. `trail` is the way from the
505 * main list to the one shown, worked out from the address, so a link
506 * straight to a settings page opens the sidebar already drilled in, and
507 * leaving it comes back out. Lists deeper than the one shown stay in
508 * place, off to the right, so going back slides them away rather than
509 * dropping them. Moving to a list at the same depth (a workspace's settings
510 * to a project) fades the new one in where it is.
511 */
512function Drill({ trail }: { trail: Level[] }) {
513 const depth = trail.length - 1;
514 const hydrated = useHydrated();
515 const stack = useRef<Level[]>(trail);
516 const before = stack.current;
517 const within = trail.every((level, index) => before[index]?.key === level.key);
518 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
519 const levels = stack.current;
520 // What was showing last time, to tell drilling in from moving across.
521 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
522 useEffect(() => {
523 shown.current = { depth, keys: levels.map((level) => level.key) };
524 });
A catalogue of model providers, and settings that feel like settings525 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look526 <div className="relative min-h-0 grow overflow-hidden">
527 {levels.map((level, index) => {
528 const current = index === depth;
529 const place =
530 index < depth
531 ? "pointer-events-none -translate-x-1/4 opacity-0"
532 : current
533 ? "translate-x-0 opacity-100"
534 : "pointer-events-none translate-x-full opacity-0";
535 // A list that was not there before arrives: from the right when
536 // drilling in, in place when moving across.
537 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
538 const enter = arriving
539 ? depth > shown.current.depth
540 ? "starting:translate-x-full starting:opacity-0"
541 : "starting:opacity-0"
542 : "";
543 return (
544 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
545 {level.node}
546 </div>
547 );
548 })}
549 </div>
550 );
551}
552
553/**
554 * The row at the top of a list drilled into: the list's name, which goes
555 * back out, and what it belongs to, faint at the end.
556 */
557function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
558 return (
559 <Link
560 to={to}
561 prefetch="intent"
562 className="group mt-3 flex h-8 items-center gap-1.5 rounded-md pr-2 pl-1 text-[0.8125rem] font-medium text-fg transition-colors hover:bg-raised/60"
A catalogue of model providers, and settings that feel like settings563 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look564 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
565 <span className="min-w-0 shrink-0 truncate">{label}</span>
566 {context && (
567 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
568 )}
569 </Link>
570 );
571}
572
573/** A thin rule between groups of a list. */
574function Rule() {
575 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
576}
577
Navigation: the workspace's own page with tabs, pins and recent in the sidebar578/** A quiet heading inside a group: Pinned, Recent. */
579function SidebarSubhead({ children }: { children: ReactNode }) {
580 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
581}
582
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look583/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar584 * The workspace's projects as the sidebar keeps them, however many there
585 * are: the person's pins in their order, what they opened last, and the
586 * way to all of them. Pins move by dragging, or with Alt and the arrow
587 * keys; the projects service keeps the order.
588 */
589function SidebarProjects({ slug, shell }: { slug: string; shell: ShellData }) {
590 const reorder = useFetcher({ key: `pins:${slug}` });
591 const saved = shell.pinned ?? [];
592 // While a new order is on its way, it shows as made.
593 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
594 const pinned = asked
595 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
596 : saved;
597 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
598 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
599 const [dragging, setDragging] = useState<number | null>(null);
600 const move = (from: number, by: number) => {
601 const next = movedPin(pinned, from, by);
602 if (!next) return;
603 const form = new FormData();
604 form.set("intent", "reorder");
605 for (const project of next) form.append("slug", project.name);
606 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
607 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar608 // Each row pins or unpins in place: the pin shows on hover or focus, and
609 // stays shown on a pinned row's hover so it reads as "unpin".
610 const row = (project: ShortcutProject, isPinned: boolean) => (
611 <div className="group/row relative">
612 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
613 <span className="block truncate pr-6">{project.title ?? project.name}</span>
614 </SidebarLink>
615 <PinButton
616 workspace={slug}
617 slug={project.name}
618 name={project.title ?? project.name}
619 pinned={isPinned}
620 small
621 className="absolute top-1/2 right-6 -translate-y-1/2 opacity-0 transition-opacity group-hover/row:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100"
622 />
623 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar624 );
625 return (
626 <SidebarGroup
627 title="Projects"
628 action={
629 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
630 <Plus size={13} />
631 </Link>
632 }
633 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar634 {/* Only there once something is pinned: each row below has its own pin. */}
635 {pinned.length > 0 && (
636 <>
637 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar638 <ul aria-label="Pinned projects" className="space-y-px">
639 {pinned.map((project, index) => (
640 <li
641 key={project.name}
642 draggable={pinned.length > 1}
643 onDragStart={(event) => {
644 setDragging(index);
645 event.dataTransfer.effectAllowed = "move";
646 }}
647 onDragOver={(event) => {
648 if (dragging != null) event.preventDefault();
649 }}
650 onDrop={(event) => {
651 event.preventDefault();
652 if (dragging != null && dragging !== index) move(dragging, index - dragging);
653 setDragging(null);
654 }}
655 onDragEnd={() => setDragging(null)}
656 onKeyDown={(event) => {
657 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
658 event.preventDefault();
659 move(index, event.key === "ArrowUp" ? -1 : 1);
660 }}
661 title={pinned.length > 1 ? "Drag, or Alt and an arrow key, to reorder" : undefined}
662 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
663 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar664 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar665 {pinned.length > 1 && (
666 <GripVertical
667 size={12}
668 aria-hidden="true"
669 className="pointer-events-none absolute top-1/2 -left-1.5 -translate-y-1/2 text-faint opacity-0 transition-opacity group-hover/pin:opacity-100"
670 />
671 )}
672 </li>
673 ))}
674 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar675 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar676 )}
677 {recent.length > 0 && (
678 <>
679 <SidebarSubhead>Recent</SidebarSubhead>
680 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar681 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar682 ))}
683 </>
684 )}
685 <div className="pt-1">
686 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length}>
687 All projects
688 </SidebarLink>
689 </div>
690 </SidebarGroup>
691 );
692}
693
694/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look695 * The main list: where you go, the projects, what the workspace builds and
696 * runs with across them, then its usage, support and settings.
697 */
698function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
699 const ws = shell.workspace;
700 // A visitor browses: no workspace, no projects of their own.
701 if (!user) {
702 return (
703 <nav aria-label="g1t" className={PANEL}>
704 <div className="mt-3 space-y-px">
705 {VISITOR_LINKS.map((link) => (
706 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
707 {link.label}
708 </SidebarLink>
709 ))}
710 </div>
711 </nav>
712 );
713 }
714 return (
715 <nav aria-label="g1t" className={PANEL}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar716 {/* Everything here is the workspace the switcher names: its home first.
717 The workspace's own page is the switcher's; Explore, all of g1t,
718 is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look719 <div className="mt-3 space-y-px">
720 <SidebarLink to="/" end icon={<House size={15} />}>
721 Mission control
722 </SidebarLink>
723 </div>
724
725 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar726 {ws ? (
727 <SidebarProjects slug={ws.slug} shell={shell} />
728 ) : (
729 <SidebarGroup title="Projects">
730 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
731 </SidebarGroup>
732 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look733 {(shell.shared ?? []).length > 0 && (
734 <SidebarGroup title="Shared with you" className="mt-3">
735 {(shell.shared ?? []).map((repo) => (
736 <SidebarLink
737 key={`${repo.namespace}/${repo.name}`}
738 to={`/${repo.namespace}/${repo.name}`}
739 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
740 drill="hover"
741 >
742 <span className="font-mono text-faint">{repo.namespace}/</span>
743 {repo.name}
744 </SidebarLink>
745 ))}
746 </SidebarGroup>
747 )}
748
749 {ws && (
750 <>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member751 <Rule />
Navigation: the workspace's own page with tabs, pins and recent in the sidebar752 <SidebarGroup title="Workspace">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look753 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />}>
754 Agent fleet
755 </SidebarLink>
756 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />}>
757 Context
758 </SidebarLink>
759 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />}>
760 Memory
761 </SidebarLink>
762 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />}>
763 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents764 </SidebarLink>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member765 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />}>
766 Packages
767 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar768 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look769 <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 ))}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar778 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look779
780 <Rule />
781 <div className="space-y-px">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar782 {/* Who belongs, for every member to see; owners invite and manage there. */}
783 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar784 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar785 </SidebarLink>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar786 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams}>
787 Teams
788 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look789 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />}>
790 Usage
791 </SidebarLink>
792 <SidebarLink to="/support" icon={<LifeBuoy size={15} />}>
793 Support
794 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar795 {/* How it is set up and connected, what it pays and its record: a list of their own. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look796 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member797 to={ws.role === "owner" ? `/${ws.slug}/-/settings` : `/${ws.slug}/-/repositories`}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look798 icon={<Settings size={15} />}
799 drill
800 >
801 Settings
802 </SidebarLink>
803 </div>
804 </>
805 )}
806 </nav>
807 );
808}
809
810/** A workspace's settings, drilled into from Settings in the main list. */
811function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
812 return (
813 <nav aria-label="Workspace settings" className={PANEL}>
814 <BackRow to={`/${slug}`} label="Settings" context={slug} />
815 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings816 {owner && (
817 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
818 General
819 </SidebarLink>
820 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look821 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
822 Repositories
823 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments824 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
825 Access tokens
826 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look827 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar828 <SidebarGroup title="Agents and runs" className="mt-3">
829 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
830 Guardrails
831 </SidebarLink>
832 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
833 Secrets and variables
834 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it835 {owner && (
836 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
837 Runners
838 </SidebarLink>
839 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar840 </SidebarGroup>
841 <SidebarGroup title="Connections" className="mt-3">
842 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
843 Integrations
844 </SidebarLink>
845 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
846 Webhooks
847 </SidebarLink>
848 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look849 <Rule />
850 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings851 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments852 Billing and plans
A catalogue of model providers, and settings that feel like settings853 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API854 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
855 Audit log
856 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look857 </div>
A catalogue of model providers, and settings that feel like settings858 </nav>
859 );
860}
861
Sidebar: no settings flash on the way into a repository, and a better slide862/**
863 * What the repository menu needs. Known from the address before the page's
864 * data arrives, so the menu is right from the first frame; the counts fill
865 * in once it does.
866 */
867type MenuRepo = {
868 namespace: string;
869 name: string;
870 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871 /** Whether they see its settings; from the repository's page once it loads. */
872 settings?: boolean;
873 /** What they may do there, once the repository's page loads. */
874 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide875 issues?: number;
876 pulls?: number;
877};
878
879function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
880 return (
881 a != null &&
882 b != null &&
883 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
884 a.name.toLowerCase() === b.name.toLowerCase()
885 );
886}
A repository has its own sidebar, as settings do887
888/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look889 * A project's own list, drilled into while you are in it: everything about
890 * the project, running and its code, and nothing else, with the way back
891 * out to all of them.
A repository has its own sidebar, as settings do892 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put893function RepoMenu({
894 repo,
895 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look896 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put897}: {
898 repo: MenuRepo;
899 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look900 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
901 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put902}) {
A repository has its own sidebar, as settings do903 const base = `/${repo.namespace}/${repo.name}`;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put904 // What a member sees, and what everyone who can see the project does.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look905 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do906 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look907 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
908 <BackRow to={back.to} label={back.label} />
A flat project menu with Code first, views as tabs, and Stripe for every owner909 <NavLink
A repository has its own sidebar, as settings do910 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner911 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents912 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner913 title="Overview"
914 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look915 `mt-2 flex items-center gap-2 rounded-md px-2 py-1.5 transition-colors ${isActive ? "bg-raised" : "hover:bg-raised/60"}`
A flat project menu with Code first, views as tabs, and Stripe for every owner916 }
A repository has its own sidebar, as settings do917 >
918 <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 page919 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do920 </span>
921 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
922 <span className="text-faint">{repo.namespace}/</span>
923 <span className="font-semibold text-fg">{repo.name}</span>
924 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner925 </NavLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look926 {/* A project's pages, in the order people use them. A page with more
927 than one view shows them as tabs across its top. */}
928 <Rule />
929 <div className="space-y-px">
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request930 <SidebarLink to={`${base}/code`} also={[`${base}/tree`, `${base}/blob`, `${base}/commits`, `${base}/commit`, `${base}/branches`, `${base}/tags`, `${base}/compare`, ...soonPaths(base, "Code")]} icon={<Code2 size={15} />}>
A flat project menu with Code first, views as tabs, and Stripe for every owner931 Code
932 </SidebarLink>
933 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
934 Issues
935 </SidebarLink>
936 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
937 Pull requests
938 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains939 <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 owner940 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains941 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner942 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
943 Workflows
Projects: what a workspace builds and runs, first on every page944 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look945 </div>
946 <Rule />
947 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put948 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner949 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
950 Deployments
951 </SidebarLink>
952 ) : null}
Observability gets its own page; tabs never overflow953 <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.">
954 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner955 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put956 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API957 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
958 Security
959 </SidebarLink>
960 ) : null}
A flat project menu with Code first, views as tabs, and Stripe for every owner961 <SidebarSoonLink to={`${base}/soon/delivery`} also={soonPaths(base, "Insights")} icon={<BarChart3 size={15} />} about="Delivery metrics, costs and the work agents do.">
962 Insights
963 </SidebarSoonLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look964 </div>
965 {(repo.settings ?? shows.has("settings")) && (
966 <>
967 <Rule />
968 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner969 Settings
970 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look971 </>
972 )}
A repository has its own sidebar, as settings do973 </nav>
974 );
975}
976
Project settings move into the sidebar, grouped, with a heading per page977/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look978 * A project's settings, drilled into from its list: what is about running
979 * it, then its agents, then its code and who can reach it. The way back
980 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page981 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look982function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page983 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look984 // Each page shows to the roles that can use it, once the role is known.
985 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
986 const running = shows("deployments");
987 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page988 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look989 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
990 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
991 {shows("") && (
992 <div className="mt-2 space-y-px">
993 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
994 General
995 </SidebarLink>
996 </div>
997 )}
998 {running && (
999 <>
1000 {shows("") && <Rule />}
1001 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1002 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1003 Deployments
1004 </SidebarLink>
1005 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1006 Domains
1007 </SidebarLink>
1008 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1009 Dependencies
1010 </SidebarLink>
1011 </div>
1012 </>
1013 )}
1014 {agents && (
1015 <>
1016 <Rule />
1017 <div className="space-y-px">
1018 {shows("agents") && (
1019 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1020 Agents
1021 </SidebarLink>
1022 )}
1023 {shows("guardrails") && (
1024 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1025 Guardrails
1026 </SidebarLink>
1027 )}
1028 </div>
1029 </>
1030 )}
1031 <Rule />
1032 <div className="space-y-px">
1033 {shows("repository") && (
1034 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1035 Repository
1036 </SidebarLink>
1037 )}
1038 {shows("access") && (
1039 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1040 Access
1041 </SidebarLink>
1042 )}
1043 {shows("branches") && (
1044 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1045 Branches and merging
1046 </SidebarLink>
1047 )}
1048 {shows("secrets") && (
1049 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1050 Secrets and variables
1051 </SidebarLink>
1052 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1053 {shows("runners") && (
1054 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1055 Runners
1056 </SidebarLink>
1057 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1058 {shows("webhooks") && (
1059 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1060 Webhooks
1061 </SidebarLink>
1062 )}
1063 </div>
Project settings move into the sidebar, grouped, with a heading per page1064 </nav>
1065 );
1066}
1067
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1068/** Your own settings, drilled into from Your settings: one page each. */
1069function AccountSettingsMenu({ username }: { username: string }) {
1070 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1071 <SidebarLink to={`/settings/${page}`} icon={icon}>
1072 {ACCOUNT_SETTINGS[page].title}
1073 </SidebarLink>
Account settings in the same shape as a workspace's1074 );
1075 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1076 <nav aria-label="Your settings" className={PANEL}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1077 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1078 <div className="mt-2 space-y-px">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1079 {link("profile", <CircleUserRound size={15} />)}
1080 {link("emails", <Mail size={15} />)}
1081 {link("invites", <Ticket size={15} />)}
1082 </div>
1083 <Rule />
1084 <div className="space-y-px">
1085 {link("keys", <Fingerprint size={15} />)}
1086 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1087 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1088 <Rule />
1089 <div className="space-y-px">
1090 {link("github", <GithubMark className="size-[15px]" />)}
1091 {link("applications", <Plug size={15} />)}
1092 </div>
1093 <Rule />
1094 <div className="space-y-px">
1095 {link("security-log", <History size={15} />)}
1096 </div>
Account settings in the same shape as a workspace's1097 </nav>
1098 );
1099}
1100
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1101/**
1102 * Signing in and signing up, in place of the account for a visitor. Signing
1103 * in brings them back to the page they are on.
1104 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1105/** Help, status and the fine print under a visitor's sign-in buttons. */
1106function VisitorLinks() {
1107 const status = useSiteStatus();
1108 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-merged/60`;
1109 const dot = (
1110 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1111 ·
1112 </span>
1113 );
1114 return (
1115 // Two short rows, so a narrow sidebar never starts a line with a dot.
1116 <nav aria-label="About g1t" className="pt-0.5">
1117 <div className="flex items-center gap-0.5">
1118 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1119 <StatusDot state={status?.overall.state ?? null} />
1120 Status
1121 </a>
1122 {dot}
1123 <Link to="/support" className={quiet}>
1124 Support
1125 </Link>
1126 </div>
1127 <div className="flex items-center gap-0.5">
1128 {LEGAL_LINKS.map(([label, to], index) => (
1129 <span key={to} className="flex items-center gap-0.5">
1130 {index > 0 && dot}
1131 <Link to={to} className={quiet}>
1132 {label}
1133 </Link>
1134 </span>
1135 ))}
1136 </div>
1137 </nav>
1138 );
1139}
1140
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1141function VisitorPanel() {
1142 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1143 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1144 return (
1145 <div className="space-y-2">
1146 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1147 <div className="grid grid-cols-2 gap-2">
1148 <Link
1149 to={withNext("/login", pathname + search)}
1150 className="flex h-9 items-center justify-center gap-1.5 rounded-md border border-line text-[0.8125rem] font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg"
1151 >
1152 <LogIn size={14} />
1153 Sign in
1154 </Link>
1155 <Link
1156 to={withNext("/register", pathname + search)}
1157 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1158 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1159 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1160 </Link>
1161 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1162 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1163 </div>
1164 );
1165}
1166
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1167function Sidebar({
1168 user,
1169 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1170 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1171 onFind,
1172 onClose,
1173}: {
1174 user: User | null;
1175 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1176 /** The page is a 404: the address names nothing the viewer can see. */
1177 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1178 onFind: () => void;
1179 /** In the sheet on a small screen: closing it, at the end of the top row. */
1180 onClose?: () => void;
1181}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1182 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1183 const { pathname } = useLocation();
1184 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1185 // Where the sidebar is drilled to follows the page being gone to, so it
1186 // moves as the link is followed, not once the page arrives.
1187 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1188 // On a 404, the address is not a place: no list is drilled into from it,
1189 // except the project's own when the project is real and visible and only
1190 // something inside it is missing (a file, a commit, an issue).
1191 const lost = missing && going == null;
1192 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1193 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1194 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1195 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1196 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1197 const reserved = new Set(["settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", "register", "verify", "forgot", "reset", "device", "oauth", "policies", "security", "support", "status", "invite", "inbox", ".well-known"]);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1198 // An invitation to a repository is answered before its menu means anything.
1199 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1200 repoPath != null &&
1201 !reserved.has(repoPath[1]) &&
1202 repoPath[2] !== "-" &&
1203 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1204 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1205 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1206 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1207 // The repository the menu is for: the one loaded if it is the one being
1208 // gone to, else what the address says, at once.
1209 const targetRepo = inRepo && repoPath ? { namespace: repoPath[1], name: repoPath[2] } : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1210 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1211 ? sameRepo(active, targetRepo)
1212 ? active
1213 : {
1214 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1215 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1216 }
1217 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1218 // The viewer's role there, from the repository's page once it has loaded.
1219 const page = useRouteLoaderData("routes/repo/layout") as
1220 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1221 | undefined;
1222 const menuRepo: MenuRepo | null =
1223 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1224 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1225 : guessed;
1226
1227 // The way from the main list to the one shown.
1228 const trail: Level[] = [{ key: "main", node: <MainMenu user={user} shell={shell} /> }];
1229 if (user && inAccount) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1230 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1231 } else if (ws && inSettings) {
1232 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1233 } else if (menuRepo) {
1234 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1235 // Out of a project: to its workspace's projects when they are yours.
1236 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1237 const back = !user
1238 ? { to: "/explore", label: "Explore" }
1239 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1240 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1241 : { to: "/", label: "Mission control" };
1242 trail.push({
1243 key,
1244 node: (
1245 <RepoMenu
1246 repo={menuRepo}
1247 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1248 back={back}
1249 />
1250 ),
1251 });
1252 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1253 }
1254
Agents as a team: lifecycle, merge queue, billing and a new shell1255 return (
1256 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1257 {/* The same height and rule as the top bar, so the two read as one line. */}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1258 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2 pl-2.5">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1259 {user ? (
1260 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1261 {/* The 1 alone beside the workspace: a square hover the height of the
1262 switcher, the mark as tall as the workspace avatar. */}
1263 <Link to="/" aria-label="g1t home" className="flex size-9 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-raised">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1264 <Mark tight className="h-4 w-auto" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1265 </Link>
1266 <span className="shrink-0 text-line-strong" aria-hidden="true">
1267 /
1268 </span>
1269 <WorkspaceSwitcher user={user} shell={shell} />
1270 </>
1271 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1272 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1273 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1274 </Link>
1275 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1276 {onClose && (
1277 <button
1278 type="button"
1279 aria-label="Close menu"
1280 onClick={onClose}
1281 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1282 >
1283 <X size={16} />
1284 </button>
1285 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1286 </div>
A Vercel-like sidebar and a new mark1287 <div className="px-2 pt-3">
1288 <button
1289 type="button"
1290 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1291 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 mark1292 >
1293 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1294 <span className="grow text-left">Search or jump to…</span>
A Vercel-like sidebar and a new mark1295 <kbd className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line">⌘K</kbd>
1296 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1297 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1298 <Drill trail={trail} />
1299 {/* The account, or signing in, as one row at the very bottom. */}
1300 <div className="shrink-0 border-t border-line p-2">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1301 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
A Vercel-like sidebar and a new mark1302 </div>
1303 </div>
1304 );
1305}
1306
1307/** Words for the sections a path can end in. */
1308const SECTIONS: Record<string, string> = {
1309 issues: "Issues",
1310 pulls: "Pull requests",
1311 queue: "Merge queue",
1312 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1313 plans: "Outcomes",
1314 actions: "Workflows",
1315 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1316 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1317 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1318 access: "Access",
1319 invitations: "Invitation",
1320 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1321 branches: "Branches",
1322 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1323 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1324 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1325 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1326 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1327 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1328 people: "People",
1329 projects: "Projects",
1330 teams: "Teams",
A Vercel-like sidebar and a new mark1331 tokens: "Access tokens",
1332 usage: "Usage",
Secrets and variables: one list, rows per environment, for workflows and deployments1333 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1334 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1335 webhooks: "Webhooks",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1336 domains: "Domains",
1337 guardrails: "Guardrails",
1338 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1339 tree: "Files",
1340 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1341 agents: "Agents",
1342 context: "Context",
1343 memory: "Memory",
1344 security: "Security",
1345 packages: "Packages",
1346 runners: "Runners",
1347 workflows: "Workflows",
1348 observability: "Observability",
1349 insights: "Insights",
1350 sessions: "Sessions",
A Vercel-like sidebar and a new mark1351};
1352
1353/** Where the page is, as a trail of links: workspace / repository / section. */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1354function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1355 // A 404 names nothing from the address, unless the project is real and
1356 // visible and only something inside it is missing.
1357 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1358 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1359 return <span className="text-sm font-medium">Not found</span>;
1360 }
A Vercel-like sidebar and a new mark1361 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1362 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
A Vercel-like sidebar and a new mark1363 if (parts.length === 0) return <span className="text-sm font-medium">Mission control</span>;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1364 // Your settings: Settings / Emails.
1365 if (parts[0] === "settings") {
1366 const page = accountSettingsPage(pathname);
1367 const trail = [{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])];
1368 return (
1369 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1370 {trail.map((crumb, index) => (
1371 <span key={crumb.to + index} className="flex min-w-0 items-center gap-1.5">
1372 {index > 0 && <span className="text-line-strong">/</span>}
1373 <Link
1374 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1375 prefetch="intent"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1376 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1377 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1378 }`}
1379 >
1380 {crumb.label}
1381 </Link>
1382 </span>
1383 ))}
1384 </nav>
1385 );
1386 }
A Vercel-like sidebar and a new mark1387 if (reserved.includes(parts[0]!)) {
1388 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1389 settings: "Account",
A Vercel-like sidebar and a new mark1390 explore: "Explore",
Projects live: fixes from walking them end to end1391 new: "New project",
A Vercel-like sidebar and a new mark1392 search: "Search",
1393 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1394 policies: "Policies",
1395 security: "Security",
1396 support: "Support",
1397 status: "Status",
1398 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1399 inbox: "Inbox",
A Vercel-like sidebar and a new mark1400 };
1401 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1402 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1403 // A person's profile, by their handle.
1404 if (parts[0] === "u" && parts[1]) {
1405 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1406 }
A Vercel-like sidebar and a new mark1407 const [owner, second, third, fourth] = parts;
1408 const trail: { label: string; to: string; mono?: boolean }[] = [{ label: owner!, to: `/${owner}`, mono: true }];
1409 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1410 const page = `/${owner}/-/${third}`;
1411 if (third && SETTINGS_PAGE.test(page)) {
1412 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1413 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1414 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1415 } else if (second) {
1416 const repo = `/${owner}/${second}`;
1417 trail.push({ label: second, to: repo, mono: true });
1418 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, to: `${repo}/pull/${fourth}` });
1419 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, to: `${repo}/issues/${fourth}` });
1420 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 deployments1421 else if (third && SECTIONS[third]) {
1422 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1423 // A settings page names which one: Settings / Secrets and variables.
1424 if (third === "settings" && fourth && SECTIONS[fourth]) {
Branches and Tags pages, each file's last commit, and the branch menu on files1425 trail.push({ label: fourth === "branches" ? "Branches and merging" : SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1426 }
1427 }
A Vercel-like sidebar and a new mark1428 }
1429 return (
1430 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
1431 {trail.map((crumb, index) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1432 <span key={index} className="flex min-w-0 items-center gap-1.5">
A Vercel-like sidebar and a new mark1433 {index > 0 && <span className="text-line-strong">/</span>}
Agents as a team: lifecycle, merge queue, billing and a new shell1434 <Link
A Vercel-like sidebar and a new mark1435 to={crumb.to}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1436 prefetch="intent"
A Vercel-like sidebar and a new mark1437 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1438 index === trail.length - 1 ? "font-medium text-fg" : "text-muted hover:text-fg"
1439 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell1440 >
A Vercel-like sidebar and a new mark1441 {crumb.label}
Agents as a team: lifecycle, merge queue, billing and a new shell1442 </Link>
A Vercel-like sidebar and a new mark1443 </span>
1444 ))}
1445 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1446 );
1447}
1448
Search across all of g1t, Explore, and a command palette1449type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1450
1451/** Everything the palette can jump to, from what the sidebar already knows. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1452function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1453 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1454 const commands: Command[] = [
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1455 { label: "Mission control", to: "/", icon: <House size={15} /> },
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1456 ...(shell.repos.length > 0
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1457 ? [{ label: "Put an agent on it", hint: "Open an issue and assign g1t", to: "/?agent=new", icon: <Sparkles size={15} /> }]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1458 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1459 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1460 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1461 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1462 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1463 { label: "New workspace", to: "/workspaces/new", icon: <Plus size={15} /> },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1464 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1465 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1466 .filter((page) => page !== "profile")
1467 .map((page) => ({ label: ACCOUNT_SETTINGS[page].title, hint: "Your settings", to: `/settings/${page}`, icon: <Settings size={15} /> })),
Agents as a team: lifecycle, merge queue, billing and a new shell1468 ];
1469 const repo = shell.repo;
1470 if (repo) {
1471 const base = `/${repo.namespace}/${repo.name}`;
1472 const name = `${repo.namespace}/${repo.name}`;
1473 commands.unshift(
Projects: what a workspace builds and runs, first on every page1474 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1475 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1476 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1477 { label: "New issue", hint: name, to: `${base}/issues/new`, icon: <Plus size={15} /> },
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1478 ...(repo.member
1479 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1480 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1481 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1482 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1483 ...(repo.member
1484 ? [
1485 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1486 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1487 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1488 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1489 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1490 ]
1491 : []),
1492 );
1493 }
1494 for (const membership of user.workspaces ?? []) {
1495 commands.push(
Workspace names and icons, and a component kit for every control1496 { label: displayName(membership), hint: `Workspace · ${membership.slug}`, to: `/${membership.slug}`, icon: <Avatar name={membership.slug} image={membership.avatar} size={15} square /> },
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1497 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1498 { label: "People", hint: membership.slug, to: `/${membership.slug}/-/people`, icon: <Users size={15} /> },
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request1499 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Projects: what a workspace builds and runs, first on every page1500 { 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 settings1501 { label: "Access tokens", hint: `${membership.slug} · Settings`, to: `/${membership.slug}/-/tokens`, icon: <KeyRound size={15} /> },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1502 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1503 );
1504 }
1505 for (const listed of shell.repos) {
1506 commands.push({
Projects: what a workspace builds and runs, first on every page1507 label: listed.title ?? listed.name,
1508 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1509 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1510 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1511 });
1512 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1513 // "Put an agent on …": one per project, after the projects themselves.
1514 for (const listed of shell.repos) {
1515 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1516 commands.push({
1517 label: `Put an agent on ${listed.title ?? listed.name}`,
1518 hint: `${listed.namespace}/${listed.name}`,
1519 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1520 icon: <Sparkles size={15} />,
1521 });
1522 }
Agents as a team: lifecycle, merge queue, billing and a new shell1523 return commands;
1524}
1525
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1526/** What the palette offers a visitor: browsing, the project they are in, and signing in. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1527function visitorCommands(shell: ShellData, here: string, signUpLabel: string): Command[] {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1528 const commands: Command[] = [];
1529 const repo = shell.repo;
1530 if (repo) {
1531 const base = `/${repo.namespace}/${repo.name}`;
1532 const name = `${repo.namespace}/${repo.name}`;
1533 commands.push(
1534 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1535 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1536 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1537 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1538 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1539 );
1540 }
1541 commands.push(
1542 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1543 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1544 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1545 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1546 { label: "Sign in", to: withNext("/login", here), icon: <LogIn size={15} /> },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1547 { label: signUpLabel, to: withNext("/register", here), icon: <Plus size={15} /> },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1548 );
1549 return commands;
1550}
1551
Agents as a team: lifecycle, merge queue, billing and a new shell1552/**
Merge branch 'worktree-agent-aca4790ea223aa235'1553 * The page while the next one loads: after a moment it dims, under the
1554 * bar below, so it is plainly on its way out rather than frozen, and the
1555 * next page fades up from there. Sending a form does not dim it: the
1556 * form's button says it is working. Spread on the element holding the page.
1557 */
1558export function useLeaving() {
1559 const navigation = useNavigation();
1560 const method = navigation.formMethod?.toUpperCase();
1561 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1562 return {
1563 "aria-busy": leaving || undefined,
1564 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1565 };
1566}
1567
1568/**
Agents as a team: lifecycle, merge queue, billing and a new shell1569 * A bar across the top of the page while the next one loads, as GitHub
1570 * has: it appears at once, creeps towards the end while waiting, then fills
1571 * and fades when the page arrives.
1572 */
1573export function Progress() {
1574 const navigation = useNavigation();
1575 const busy = navigation.state !== "idle";
1576 const [width, setWidth] = useState(0);
1577 const [visible, setVisible] = useState(false);
1578 useEffect(() => {
1579 if (busy) {
1580 setVisible(true);
1581 setWidth(12);
1582 // Slows as it goes, never quite arriving until the page does.
1583 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1584 return () => clearInterval(timer);
1585 }
1586 setWidth((now) => (now > 0 ? 100 : 0));
1587 const fade = setTimeout(() => setVisible(false), 250);
1588 const reset = setTimeout(() => setWidth(0), 550);
1589 return () => {
1590 clearTimeout(fade);
1591 clearTimeout(reset);
1592 };
1593 }, [busy]);
1594 return (
1595 <div
1596 aria-hidden="true"
1597 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1598 visible ? "opacity-100" : "opacity-0"
1599 }`}
1600 >
1601 <div
1602 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1603 style={{ width: `${width}%` }}
1604 />
1605 </div>
1606 );
1607}
1608
1609/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1610 * The app: a sidebar with the workspace, its repositories and the sections
1611 * of the one being looked at; a slim bar with search and the account; and
1612 * the page. A visitor who is not signed in gets the same frame, with
1613 * Explore and Search in place of the workspace, and signing in in place of
1614 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell1615 */
1616export function AppShell({
1617 user,
1618 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1619 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell1620 banner,
1621 children,
1622}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1623 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell1624 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1625 /** The page is a 404 (see Sidebar). */
1626 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell1627 banner?: ReactNode;
1628 children: ReactNode;
1629}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1630 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell1631 const [drawer, setDrawer] = useState(false);
1632 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1633 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1634 const signUpLabel = useSignUpCopy().primary;
1635 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell1636
1637 // A new page closes the drawer on small screens.
1638 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents1639 // Mission control's code, fetched while the browser is idle, so going
1640 // home never waits on it (routes/home.tsx).
1641 useEffect(() => {
1642 if (!user) return;
1643 const load = () => void import("./mission-control").catch(() => undefined);
1644 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
1645 if (idle) idle(load);
1646 else setTimeout(load, 1500);
1647 }, [user]);
Search across all of g1t, Explore, and a command palette1648 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'1649 const leaving = useLeaving();
Agents as a team: lifecycle, merge queue, billing and a new shell1650
1651 return (
1652 <div className="min-h-screen">
1653 <Progress />
1654 <aside className="fixed inset-y-0 left-0 z-40 hidden w-64 border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))] lg:block">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1655 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1656 </aside>
1657 {drawer && (
1658 <div className="fixed inset-0 z-50 lg:hidden">
1659 <button
1660 type="button"
1661 aria-label="Close menu"
1662 className="absolute inset-0 bg-black/60"
1663 onClick={() => setDrawer(false)}
1664 />
1665 <aside className="absolute inset-y-0 left-0 w-72 border-r border-line bg-surface">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1666 <Sidebar user={user} shell={shell} missing={missing} onFind={() => setPalette(true)} onClose={() => setDrawer(false)} />
Agents as a team: lifecycle, merge queue, billing and a new shell1667 </aside>
1668 </div>
1669 )}
1670
Polish: phones, copy boxes, the plan page, the landing page, a real glide1671 <div className="flex min-h-screen min-w-0 flex-col lg:pl-64">
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1672 <header className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-line bg-bg/85 px-4 backdrop-blur sm:px-6">
Agents as a team: lifecycle, merge queue, billing and a new shell1673 <button
1674 type="button"
1675 aria-label="Open menu"
1676 onClick={() => setDrawer(true)}
1677 className="rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden"
1678 >
1679 <Menu size={18} />
1680 </button>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1681 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1682 <div className="ml-auto flex items-center gap-1.5">
1683 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
1684 <button
1685 type="button"
1686 aria-label="Search or jump to"
1687 onClick={() => setPalette(true)}
1688 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
1689 >
1690 <Search size={16} />
1691 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1692 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
1693 <NavLink
1694 to="/explore"
1695 prefetch="intent"
1696 aria-label="Explore"
1697 className={({ isActive }) =>
1698 `flex h-9 items-center gap-1.5 rounded-md px-2 text-sm transition-colors hover:bg-raised hover:text-fg sm:px-2.5 ${isActive ? "text-fg" : "text-muted"}`
1699 }
1700 >
1701 <Compass size={16} className="sm:hidden" />
1702 <span className="hidden sm:inline">Explore</span>
1703 </NavLink>
A Vercel-like sidebar and a new mark1704 <a
1705 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals1706 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 mark1707 >
1708 Docs
1709 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1710 {user && (
1711 <>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar1712 <AgentButton />
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1713 <InboxBell counts={shell.inbox ?? null} />
1714 </>
1715 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1716 {!user ? (
1717 // The sidebar has these too, but on a phone it is folded away.
1718 <Link
1719 to={withNext("/login", here)}
1720 className="flex h-9 items-center rounded-md bg-fg px-3 text-sm font-medium text-bg transition-colors hover:bg-white lg:hidden"
1721 >
1722 Sign in
1723 </Link>
1724 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1725 <>
1726 {/* Room between what comes in (the inbox) and what you make. */}
1727 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell1728 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1729 <Tooltip>
1730 <TooltipTrigger asChild>
1731 <DropdownMenuTrigger
1732 aria-label="Create new"
1733 className="flex h-9 items-center gap-1 rounded-md border border-line px-2 text-fg/90 outline-none transition-colors hover:border-line-strong hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised"
1734 >
1735 <Plus size={16} />
1736 <ChevronDown size={13} className="text-muted" />
1737 </DropdownMenuTrigger>
1738 </TooltipTrigger>
1739 <TooltipContent>Create new…</TooltipContent>
1740 </Tooltip>
1741 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell1742 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1743 <>
1744 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
1745 <DropdownMenuItem asChild>
1746 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
1747 <CircleDot />
1748 New issue
1749 </Link>
1750 </DropdownMenuItem>
1751 {shell.repo.member && (
1752 <DropdownMenuItem asChild>
1753 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
1754 <ListTree />
1755 Plan work
1756 </Link>
1757 </DropdownMenuItem>
1758 )}
1759 <DropdownMenuSeparator />
1760 </>
Agents as a team: lifecycle, merge queue, billing and a new shell1761 )}
1762 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1763 <Link to="/?agent=new">
1764 <Sparkles />
1765 Put an agent on it
1766 </Link>
1767 </DropdownMenuItem>
1768 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1769 <Link to="/new">
Projects: what a workspace builds and runs, first on every page1770 <Box />
1771 New project
Agents as a team: lifecycle, merge queue, billing and a new shell1772 </Link>
1773 </DropdownMenuItem>
1774 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1775 <Link to="/new/github">
1776 <GitBranch />
1777 Import from GitHub
1778 </Link>
1779 </DropdownMenuItem>
1780 <DropdownMenuSeparator />
1781 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell1782 <Link to="/workspaces/new">
1783 <Users />
1784 New workspace
1785 </Link>
1786 </DropdownMenuItem>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1787 {/* A team in the workspace the sidebar is about. */}
1788 {shell.workspace && (
1789 <DropdownMenuItem asChild>
1790 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
1791 <UsersRound />
1792 New team
1793 </Link>
1794 </DropdownMenuItem>
1795 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1796 </DropdownMenuContent>
1797 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1798 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1799 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1800 </div>
1801 </header>
1802 {banner}
Merge branch 'worktree-agent-aca4790ea223aa235'1803 <main {...leaving} className={`min-w-0 grow ${leaving.className}`}>
1804 {children}
1805 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell1806 </div>
Search across all of g1t, Explore, and a command palette1807 <CommandPalette
1808 open={palette}
1809 onOpenChange={setPalette}
1810 commands={commands}
1811 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
1812 />
Agents as a team: lifecycle, merge queue, billing and a new shell1813 </div>
1814 );
1815}

This file's history is long; its oldest lines are credited to the oldest commit read.