Skip to content
2,312 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.

One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1import { Activity, BarChart3, Building2, MessagesSquare, 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, Inbox, KanbanSquare, KeyRound, Layers, LayoutDashboard, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Scale, Smile, Sparkles, Ticket, TrendingUp, UserRoundKey, 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
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell5import { type Abilities, type ChatSidebarEntry, type InboxCounts, type WorkspaceAgent, type Membership, type Spike, type User, hasCodeAccess, mayCreateTeams } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in7import { InMain } from "./landmark";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit8import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar9import { AgentButton, InboxBell } from "./inbox";
10import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar11import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Merge branch 'worktree-agent-ae1299e92e4462012'12import { Hint } from "./ui/hint";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas14import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height15import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit16import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents17import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell18import {
19 DropdownMenu,
20 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas21 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell22 DropdownMenuItem,
23 DropdownMenuLabel,
24 DropdownMenuSeparator,
25 DropdownMenuTrigger,
26} from "./ui/dropdown-menu";
Merge branch 'worktree-agent-ad7c6d88d93adc817'27import { type RoadmapItem, roadmapIn, roadmapItem } from "../lib/roadmap";
Chat and workspace agents: channels, DMs and named agents you talk to28import { type ModeKey, SETTINGS_PAGES, modeOf, sidebarCurrent } from "../lib/workspace-nav";
29import { AgentsSidebar } from "./agents-mode";
30import { ChatSidebar } from "./chat/sidebar";
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell31import { HelpMenu, Rail } from "./rail";
32import { HomeSidebar } from "./home-sidebar";
33import { G1tMark } from "./orchestrator";
34import { AvatarSheetButton, MobileTabBar, isConversation, useVisualViewport } from "./mobile";
Chat and workspace agents: channels, DMs and named agents you talk to35import { useChatSidebar } from "./chat/actions";
36import { unreadTotals } from "../lib/chat";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look37import { 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 put38import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas39import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
40import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put41import { withNext } from "../lib/next";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look42import { useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas43import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar44import { 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 quotas45import type { AccountMenuData } from "../routes/settings-menu-json";
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell46import { useLiveBadges } from "../lib/notify-client";
Agents as a team: lifecycle, merge queue, billing and a new shell47
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put48/**
49 * What the sidebar needs, worked out by the root loader. For a visitor who
50 * is not signed in there is no workspace, no projects and no usage: only
51 * the project being looked at, if they can see it.
52 */
Agents as a team: lifecycle, merge queue, billing and a new shell53export type ShellData = {
54 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control55 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar56 /** All its projects, by name, for the palette: `name` is the slug in their address. */
57 repos: ShortcutProject[];
58 /** The person's pinned projects in it, in their order (lib/pins.ts). */
59 pinned?: ShortcutProject[];
60 /** What they opened there last, latest first, leaving out the pinned. */
61 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look62 /** Repositories shared with them in workspaces they do not belong to. */
63 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page64 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell65 repo: {
66 namespace: string;
67 name: string;
68 member: boolean;
69 issues: number;
70 pulls: number;
71 } | null;
Billing accounts, terms and enterprises; g1t is no longer free72 /** Where the workspace stands against its usage limit, if billing is on. */
73 limit: {
74 exposureMicros: number;
75 ceilingMicros: number | null;
76 state: "ok" | "warning" | "stopped";
77 comped: boolean;
78 } | null;
Free while g1t is being built out; agents can check out their own forks79 /** Whether g1t charges nothing for now, while it is being built out. */
80 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request81 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy82 /** This month's usage: charged, or at cost while g1t is free. */
83 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
85 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 all86 /** What is unread in their inbox, for the bell. Absent for a visitor. */
87 inbox?: InboxCounts | null;
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell88 /**
89 * Chat, for the rail's badge and Home's sidebar: what is unread, and the
90 * starred and latest conversations. Null when chat did not answer in time.
91 */
92 chat?: { unread: number; mentions: number; starred?: ChatSidebarEntry[]; recent?: ChatSidebarEntry[] } | null;
93 /** The workspace's agents, g1t first, for Home's and Agents' sidebars; null when not known. */
94 agents?: ShellAgent[] | null;
Agents as a team: lifecycle, merge queue, billing and a new shell95};
96
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell97/** An agent as the shell lists it. */
98export type ShellAgent = Pick<
99 WorkspaceAgent,
100 "id" | "handle" | "display_name" | "avatar" | "avatar_seed" | "role" | "status" | "title" | "team" | "department" | "builtin"
101>;
102
Agents as a team: lifecycle, merge queue, billing and a new shell103function SidebarLink({
104 to,
105 icon,
106 end,
107 count,
108 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109 drill,
Merge branch 'worktree-agent-ad7c6d88d93adc817'110 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell111 children,
112}: {
113 to: string;
114 icon: ReactNode;
115 end?: boolean;
116 count?: number;
Projects: what a workspace builds and runs, first on every page117 /** Other path prefixes under which this link is the current one. */
118 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 /** It opens a list of its own: a chevron says so, always or on hover. */
120 drill?: boolean | "hover";
Merge branch 'worktree-agent-ad7c6d88d93adc817'121 /** Whether it is the current row, when the list works that out itself. */
122 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell123 children: ReactNode;
124}) {
125 const { pathname } = useLocation();
126 return (
127 <NavLink
128 to={to}
129 end={end}
130 prefetch="intent"
131 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page132 const current =
Merge branch 'worktree-agent-ad7c6d88d93adc817'133 lit ??
134 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark135 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 shell136 current
137 ? "bg-raised font-medium text-fg"
138 : isPending
139 ? "bg-raised/60 text-fg"
140 : "text-muted hover:bg-raised/60 hover:text-fg"
141 }`;
142 }}
143 >
144 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
145 <span className="min-w-0 grow truncate">{children}</span>
146 {count != null && count > 0 && (
147 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
148 {count}
149 </span>
150 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look151 {drill && (
152 <ChevronRight
153 size={14}
154 aria-hidden="true"
155 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
156 />
157 )}
Agents as a team: lifecycle, merge queue, billing and a new shell158 </NavLink>
159 );
160}
161
Pull requests say where they are live; the project menu shows what is coming162/**
163 * Something a project will have, shown where it will live so people can
164 * see where g1t is going. Not a link: nothing is behind it yet.
165 */
166function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
167 return (
Merge branch 'worktree-agent-ae1299e92e4462012'168 <Hint label={about} side="right">
169 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
170 <span className="shrink-0 opacity-70">{icon}</span>
171 <span className="grow truncate">{children}</span>
172 <span className="sr-only">{about}</span>
173 <SoonPill />
174 </div>
175 </Hint>
Pull requests say where they are live; the project menu shows what is coming176 );
177}
178
The project menu opens by section, and the workspace has its own panel179/**
A flat project menu with Code first, views as tabs, and Stripe for every owner180 * A page that is coming, as a link to its roadmap page: faint, with Soon,
181 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel182 */
A flat project menu with Code first, views as tabs, and Stripe for every owner183function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel184 to,
185 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner186 icon,
187 about,
Merge branch 'worktree-agent-ad7c6d88d93adc817'188 current: lit,
The project menu opens by section, and the workspace has its own panel189 children,
190}: {
191 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner192 also?: string[];
193 icon: ReactNode;
194 about: string;
Merge branch 'worktree-agent-ad7c6d88d93adc817'195 /** Whether it is the current row, when the list works that out itself. */
196 current?: boolean;
The project menu opens by section, and the workspace has its own panel197 children: ReactNode;
198}) {
199 const { pathname } = useLocation();
200 return (
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero201 // The hint wraps a plain box: Radix's asChild merges className as a
202 // string, which would break NavLink's className function.
Merge branch 'worktree-agent-ae1299e92e4462012'203 <Hint label={about} side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero204 <div>
The project menu opens by section, and the workspace has its own panel205 <NavLink
206 to={to}
207 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner208 className={({ isActive }) => {
Merge branch 'worktree-agent-ad7c6d88d93adc817'209 const current = lit ?? (isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A flat project menu with Code first, views as tabs, and Stripe for every owner210 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
211 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 panel212 }`;
213 }}
214 >
A flat project menu with Code first, views as tabs, and Stripe for every owner215 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel216 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit217 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales218 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero219 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'220 </Hint>
The project menu opens by section, and the workspace has its own panel221 );
222}
223
A flat project menu with Code first, views as tabs, and Stripe for every owner224/** Icons for what spans projects. */
225const WORKSPACE_ICONS: Record<string, ReactNode> = {
226 board: <KanbanSquare size={15} />,
227 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member228 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner229 packages: <Package size={15} />,
230 fleet: <Bot size={15} />,
231};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales232
A flat project menu with Code first, views as tabs, and Stripe for every owner233/** The Soon pages of a section, as paths, so its link is current on them. */
234function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
235 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales236}
237
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look238function SidebarGroup({
239 title,
240 action,
241 className = "",
242 children,
243}: {
244 title: string;
245 action?: ReactNode;
246 className?: string;
247 children: ReactNode;
248}) {
Agents as a team: lifecycle, merge queue, billing and a new shell249 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look250 <section className={className}>
A Vercel-like sidebar and a new mark251 <div className="mb-1 flex h-6 items-center justify-between px-2">
252 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell253 {action}
254 </div>
255 <div className="space-y-px">{children}</div>
256 </section>
257 );
258}
259
Workspace names and icons, and a component kit for every control260/** What a workspace is called where people read it: its display name, else its slug. */
261function displayName(membership: Membership): string {
262 return membership.name?.trim() || membership.slug;
263}
264
Agents as a team: lifecycle, merge queue, billing and a new shell265function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control266 const workspace = shell.workspace;
267 const current = workspace?.slug;
268 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell269 return (
270 <DropdownMenu>
Workspace names and icons, and a component kit for every control271 {/* The name goes to the workspace; only the arrows switch it. The
272 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'273 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark274 <Link
275 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents276 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look277 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 mark278 >
Workspace names and icons, and a component kit for every control279 {workspace ? (
280 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell281 ) : (
Workspace names and icons, and a component kit for every control282 <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 shell283 )}
Workspace names and icons, and a component kit for every control284 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark285 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'286 </Hint>
A Vercel-like sidebar and a new mark287 <DropdownMenuTrigger
288 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit289 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 focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark290 >
291 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell292 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control293 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar294 {workspace && (
295 <>
296 {/* The workspace's own page: its projects, packages and people. */}
297 <DropdownMenuItem asChild>
298 <Link to={`/${workspace.slug}`}>
299 <LayoutGrid />
300 Workspace overview
301 </Link>
302 </DropdownMenuItem>
303 <DropdownMenuItem asChild>
304 <Link to={`/${workspace.slug}/-/projects`}>
305 <Box />
306 All projects
307 </Link>
308 </DropdownMenuItem>
309 <DropdownMenuSeparator />
310 </>
311 )}
Agents as a team: lifecycle, merge queue, billing and a new shell312 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
313 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'314 <Hint key={membership.slug} label={displayName(membership)} side="right">
315 <DropdownMenuItem asChild>
316 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control317 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
318 <span className="flex min-w-0 grow flex-col leading-tight">
319 <span className="truncate">{displayName(membership)}</span>
320 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
321 </span>
322 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
323 {membership.slug === current ? (
324 <Check className="shrink-0 text-accent" />
325 ) : (
326 <span className="size-4 shrink-0" aria-hidden="true" />
327 )}
Agents as a team: lifecycle, merge queue, billing and a new shell328 </Link>
329 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'330 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell331 ))}
332 <DropdownMenuSeparator />
333 <DropdownMenuItem asChild>
334 <Link to="/workspaces/new">
335 <Plus />
336 New workspace
337 </Link>
338 </DropdownMenuItem>
339 </DropdownMenuContent>
340 </DropdownMenu>
A Vercel-like sidebar and a new mark341 );
342}
343
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas344/**
345 * The fine print, as one quiet row at the foot of the account menu and the
346 * visitor's panel: each link a full-height target, left-aligned. In the
347 * menu each is a menu item, so the arrow keys reach it.
348 */
349const LEGAL_LINKS: [string, string][] = [
350 ["Policies", "/policies"],
351 ["Privacy", "/policies/privacy"],
352 ["Security", "/security"],
353];
354const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
355
356function MenuLegalRow() {
357 return (
358 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
359 {LEGAL_LINKS.map(([label, to], index) => (
360 <span key={to} className="flex items-center gap-0.5">
361 {index > 0 && (
362 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
363 ·
364 </span>
365 )}
366 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
367 <Link to={to}>{label}</Link>
368 </DropdownMenuItem>
369 </span>
370 ))}
371 </div>
372 );
373}
374
375/** Status's row: the live dot and two or three words, fetched when the menu opens. */
376function StatusSummary({ open }: { open: boolean }) {
377 const status = useSiteStatus(open);
378 const words = statusTitle(status);
379 return (
380 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
381 {/* The words say it; the dot's own words for screen readers would repeat them. */}
382 <span aria-hidden="true" className="flex">
383 <StatusDot state={status?.overall.state ?? null} />
384 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents385 {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 quotas386 </span>
387 );
388}
389
390/** A menu row: an icon, words, and whatever sits at its end. */
391const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
392
Chat and workspace agents: channels, DMs and named agents you talk to393function AccountMenu({ user, rail = false }: { user: User; rail?: boolean }) {
A Vercel-like sidebar and a new mark394 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas395 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 // Name, primary address and invites left: asked for once, as soon as the
397 // 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 quotas398 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents399 const prefetch = () => {
400 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
401 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas402 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents403 if (open) prefetch();
404 // eslint-disable-next-line react-hooks/exhaustive-deps
405 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas406 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents407 // Still on its way: shapes where the words will be, the same size.
408 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 quotas409 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark410 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas411 <DropdownMenu open={open} onOpenChange={setOpen}>
Chat and workspace agents: channels, DMs and named agents you talk to412 {rail ? (
413 // On the rail: the avatar alone, with a dot that says you are here.
414 <DropdownMenuTrigger
415 aria-label={`Account menu for ${user.username}`}
416 onPointerEnter={prefetch}
417 onFocus={prefetch}
418 className="relative rounded-full outline-none transition-transform hover:scale-[1.04] focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:ring-2 data-[state=open]:ring-line-strong"
419 >
420 <Avatar name={user.username} image={user.avatar} size={34} />
421 <span aria-hidden="true" className="absolute -right-0.5 -bottom-0.5 size-3 rounded-full bg-success ring-[2.5px] ring-[#0b0b0d]" />
422 </DropdownMenuTrigger>
423 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas424 <DropdownMenuTrigger
425 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents426 onPointerEnter={prefetch}
427 onFocus={prefetch}
Merge branch 'worktree-agent-ad7c6d88d93adc817'428 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-accent/60 data-[state=open]:bg-raised"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas429 >
Workspace names and icons, and a component kit for every control430 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark431 <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 quotas432 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark433 </DropdownMenuTrigger>
Chat and workspace agents: channels, DMs and named agents you talk to434 )}
435 <DropdownMenuContent align={rail ? "end" : "start"} side={rail ? "right" : "top"} collisionPadding={8} className="w-[17.5rem] p-1.5">
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas436 {/* Who is signed in, and a way to their profile. */}
437 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
438 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
439 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents440 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
441 {loading ? (
442 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
443 ) : (
444 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
445 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas446 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents447 {loading ? (
448 <Skeleton className="mt-1 h-3 w-40" />
449 ) : (
450 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
451 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas452 </span>
A Vercel-like sidebar and a new mark453 </Link>
454 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas455 <DropdownMenuSeparator className="my-1.5" />
456 <DropdownMenuGroup>
457 <DropdownMenuItem asChild className={MENU_ROW}>
458 <Link to={profile}>
459 <CircleUserRound />
460 Your profile
461 </Link>
462 </DropdownMenuItem>
463 <DropdownMenuItem asChild className={MENU_ROW}>
464 <Link to={FIRST_SETTINGS_PAGE}>
465 <Settings />
466 Your settings
467 </Link>
468 </DropdownMenuItem>
469 <DropdownMenuItem asChild className={MENU_ROW}>
470 <Link to="/settings/invites">
471 <Ticket />
472 Invites
Fast pages, required checks on the branch, self-hosted runners, honest incidents473 {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 quotas474 {me?.invites_left != null && (
475 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
476 {me.invites_left} left
477 </span>
478 )}
479 </Link>
480 </DropdownMenuItem>
481 </DropdownMenuGroup>
482 <DropdownMenuSeparator className="my-1.5" />
483 <DropdownMenuGroup>
484 <DropdownMenuItem asChild className={MENU_ROW}>
485 <a href="https://docs.g1t.sh/">
486 <BookOpen />
487 Documentation
488 </a>
489 </DropdownMenuItem>
490 <DropdownMenuItem asChild className={MENU_ROW}>
491 <Link to="/support">
492 <LifeBuoy />
493 Support
494 </Link>
495 </DropdownMenuItem>
496 <DropdownMenuItem asChild className={MENU_ROW}>
497 <a href={STATUS_URL}>
498 <Activity />
499 Status
500 <StatusSummary open={open} />
501 </a>
502 </DropdownMenuItem>
503 </DropdownMenuGroup>
504 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark505 {/* Submitted from here: the menu closes on select, and a button
506 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 quotas507 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark508 <LogOut />
509 Sign out
510 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas511 <DropdownMenuSeparator className="my-1.5" />
512 <MenuLegalRow />
A Vercel-like sidebar and a new mark513 </DropdownMenuContent>
514 </DropdownMenu>
515 );
516}
517
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look518/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar519 * A workspace's settings pages, which the sidebar drills into: how it is
520 * set up and connected (guardrails, secrets, runners, integrations,
521 * webhooks), what it pays, its repositories, tokens and record. The main
522 * list keeps the places work happens and who belongs; every member can
523 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look524 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar525const SETTINGS_PAGE =
Merge branch 'worktree-agent-ad7c6d88d93adc817'526 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look527/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page528const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings529
Polish: phones, copy boxes, the plan page, the landing page, a real glide530/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look531 * The sidebar's lists are a stack, the way a phone pushes a screen: one
532 * drilled into slides in from the right over the full width, while the one
533 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide534 * ease-out, the fade quicker than the move so the two never blur together.
535 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide536 */
Sidebar: no settings flash on the way into a repository, and a better slide537const LAYER =
Sidebar: the panels really slide538 "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 look539/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide540const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings541
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look542/** One list in the stack: what it is, and its rows. */
543type Level = { key: string; node: ReactNode };
544
545/** True once the page has hydrated, so what the server drew never animates in. */
546function useHydrated(): boolean {
547 const [hydrated, setHydrated] = useState(false);
548 useEffect(() => setHydrated(true), []);
549 return hydrated;
550}
551
552/**
553 * Drilling in and out of the sidebar's lists. `trail` is the way from the
554 * main list to the one shown, worked out from the address, so a link
555 * straight to a settings page opens the sidebar already drilled in, and
556 * leaving it comes back out. Lists deeper than the one shown stay in
557 * place, off to the right, so going back slides them away rather than
558 * dropping them. Moving to a list at the same depth (a workspace's settings
559 * to a project) fades the new one in where it is.
560 */
561function Drill({ trail }: { trail: Level[] }) {
562 const depth = trail.length - 1;
563 const hydrated = useHydrated();
564 const stack = useRef<Level[]>(trail);
565 const before = stack.current;
566 const within = trail.every((level, index) => before[index]?.key === level.key);
567 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
568 const levels = stack.current;
569 // What was showing last time, to tell drilling in from moving across.
570 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
571 useEffect(() => {
572 shown.current = { depth, keys: levels.map((level) => level.key) };
573 });
A catalogue of model providers, and settings that feel like settings574 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look575 <div className="relative min-h-0 grow overflow-hidden">
576 {levels.map((level, index) => {
577 const current = index === depth;
578 const place =
579 index < depth
580 ? "pointer-events-none -translate-x-1/4 opacity-0"
581 : current
582 ? "translate-x-0 opacity-100"
583 : "pointer-events-none translate-x-full opacity-0";
584 // A list that was not there before arrives: from the right when
585 // drilling in, in place when moving across.
586 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
587 const enter = arriving
588 ? depth > shown.current.depth
589 ? "starting:translate-x-full starting:opacity-0"
590 : "starting:opacity-0"
591 : "";
592 return (
593 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
594 {level.node}
595 </div>
596 );
597 })}
598 </div>
599 );
600}
601
602/**
603 * The row at the top of a list drilled into: the list's name, which goes
604 * back out, and what it belongs to, faint at the end.
605 */
606function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
607 return (
608 <Link
609 to={to}
610 prefetch="intent"
611 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 settings612 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look613 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
614 <span className="min-w-0 shrink-0 truncate">{label}</span>
615 {context && (
616 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
617 )}
618 </Link>
619 );
620}
621
622/** A thin rule between groups of a list. */
623function Rule() {
624 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
625}
626
Navigation: the workspace's own page with tabs, pins and recent in the sidebar627/** A quiet heading inside a group: Pinned, Recent. */
628function SidebarSubhead({ children }: { children: ReactNode }) {
629 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
630}
631
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look632/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar633 * The workspace's projects as the sidebar keeps them, however many there
634 * are: the person's pins in their order, what they opened last, and the
635 * way to all of them. Pins move by dragging, or with Alt and the arrow
636 * keys; the projects service keeps the order.
637 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'638function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar639 const reorder = useFetcher({ key: `pins:${slug}` });
640 const saved = shell.pinned ?? [];
641 // While a new order is on its way, it shows as made.
642 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
643 const pinned = asked
644 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
645 : saved;
646 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
647 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
648 const [dragging, setDragging] = useState<number | null>(null);
649 const move = (from: number, by: number) => {
650 const next = movedPin(pinned, from, by);
651 if (!next) return;
652 const form = new FormData();
653 form.set("intent", "reorder");
654 for (const project of next) form.append("slug", project.name);
655 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
656 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar657 // Each row pins or unpins in place: the pin shows on hover or focus, and
658 // stays shown on a pinned row's hover so it reads as "unpin".
659 const row = (project: ShortcutProject, isPinned: boolean) => (
660 <div className="group/row relative">
661 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
662 <span className="block truncate pr-6">{project.title ?? project.name}</span>
663 </SidebarLink>
664 <PinButton
665 workspace={slug}
666 slug={project.name}
667 name={project.title ?? project.name}
668 pinned={isPinned}
669 small
670 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"
671 />
672 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar673 );
674 return (
675 <SidebarGroup
676 title="Projects"
677 action={
678 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
679 <Plus size={13} />
680 </Link>
681 }
682 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar683 {/* Only there once something is pinned: each row below has its own pin. */}
684 {pinned.length > 0 && (
685 <>
686 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar687 <ul aria-label="Pinned projects" className="space-y-px">
688 {pinned.map((project, index) => (
689 <li
690 key={project.name}
691 draggable={pinned.length > 1}
692 onDragStart={(event) => {
693 setDragging(index);
694 event.dataTransfer.effectAllowed = "move";
695 }}
696 onDragOver={(event) => {
697 if (dragging != null) event.preventDefault();
698 }}
699 onDrop={(event) => {
700 event.preventDefault();
701 if (dragging != null && dragging !== index) move(dragging, index - dragging);
702 setDragging(null);
703 }}
704 onDragEnd={() => setDragging(null)}
705 onKeyDown={(event) => {
706 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
707 event.preventDefault();
708 move(index, event.key === "ArrowUp" ? -1 : 1);
709 }}
710 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
711 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar712 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar713 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'714 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
715 <span className="absolute top-1/2 -left-1.5 -translate-y-1/2 cursor-grab text-faint opacity-0 transition-opacity group-hover/pin:opacity-100">
716 <GripVertical size={12} aria-hidden="true" />
717 </span>
718 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar719 )}
720 </li>
721 ))}
722 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar723 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar724 )}
725 {recent.length > 0 && (
726 <>
727 <SidebarSubhead>Recent</SidebarSubhead>
728 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar729 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar730 ))}
731 </>
732 )}
733 <div className="pt-1">
Merge branch 'worktree-agent-ad7c6d88d93adc817'734 <SidebarLink to={`/${slug}/-/projects`} icon={<LayoutGrid size={15} />} count={shell.repos.length} current={current}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar735 All projects
736 </SidebarLink>
737 </div>
738 </SidebarGroup>
739 );
740}
741
742/**
Merge branch 'worktree-agent-ad7c6d88d93adc817'743 * The main list, in two parts a rule apart. Above it, what is yours
744 * whichever workspace you are in: Mission control, your inbox, and what
745 * others have shared with you. Below it, under the workspace's name, the
746 * workspace: its overview, its projects, what it builds and runs with
747 * across them, then its people, usage, support and settings. One row is
748 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look749 */
750function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
751 const ws = shell.workspace;
Merge branch 'worktree-agent-ad7c6d88d93adc817'752 const { pathname } = useLocation();
753 const going = useNavigation().location?.pathname;
754 // The row lights as the link is followed, not once the page arrives.
755 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look756 // A visitor browses: no workspace, no projects of their own.
757 if (!user) {
758 return (
759 <nav aria-label="g1t" className={PANEL}>
760 <div className="mt-3 space-y-px">
761 {VISITOR_LINKS.map((link) => (
762 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
763 {link.label}
764 </SidebarLink>
765 ))}
766 </div>
767 </nav>
768 );
769 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'770 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look771 return (
772 <nav aria-label="g1t" className={PANEL}>
Merge branch 'worktree-agent-ad7c6d88d93adc817'773 {/* Yours, in every workspace. Explore, all of g1t, is in the top bar. */}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look774 <div className="mt-3 space-y-px">
Merge branch 'worktree-agent-ad7c6d88d93adc817'775 <SidebarLink to="/" end icon={<House size={15} />} current={here === "mission"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look776 Mission control
777 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'778 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
779 Inbox
780 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look781 </div>
Merge branch 'worktree-agent-ad7c6d88d93adc817'782 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look783 <SidebarGroup title="Shared with you" className="mt-3">
Merge branch 'worktree-agent-ad7c6d88d93adc817'784 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 <SidebarLink
786 key={`${repo.namespace}/${repo.name}`}
787 to={`/${repo.namespace}/${repo.name}`}
788 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
789 drill="hover"
790 >
791 <span className="font-mono text-faint">{repo.namespace}/</span>
792 {repo.name}
793 </SidebarLink>
794 ))}
795 </SidebarGroup>
796 )}
797
Merge branch 'worktree-agent-ad7c6d88d93adc817'798 <Rule />
799 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look800 <>
Merge branch 'worktree-agent-ad7c6d88d93adc817'801 {/* The workspace the switcher names: its own page first. */}
802 <SidebarGroup title={displayName(ws)}>
803 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
804 Overview
805 </SidebarLink>
806 </SidebarGroup>
807 <div className="mt-3">
808 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
809 </div>
810
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member811 <Rule />
Merge branch 'worktree-agent-ad7c6d88d93adc817'812 <div className="space-y-px">
813 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />} current={here === "agents"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look814 Agent fleet
815 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'816 <SidebarLink to={`/${ws.slug}/-/context`} icon={<Network size={15} />} current={here === "context"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look817 Context
818 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'819 <SidebarLink to={`/${ws.slug}/-/memory`} icon={<Brain size={15} />} current={here === "memory"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look820 Memory
821 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'822 <SidebarLink to={`/${ws.slug}/-/security`} icon={<ShieldCheck size={15} />} current={here === "security"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look823 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents824 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'825 <SidebarLink to={`/${ws.slug}/-/packages`} icon={<Package size={15} />} current={here === "packages"}>
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member826 Packages
827 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'828 {INSIGHTS && (
829 <SidebarSoonLink
830 to={`/${ws.slug}/-/insights`}
831 icon={<TrendingUp size={15} />}
832 about={INSIGHTS.summary}
833 current={here === "insights"}
834 >
835 {INSIGHTS.title}
836 </SidebarSoonLink>
837 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar838 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look839 <SidebarSoonLink
840 key={item.key}
841 to={`/${ws.slug}/-/soon/${item.key}`}
842 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
843 about={item.summary}
844 >
845 {item.title === "Board" ? "Boards" : item.title}
846 </SidebarSoonLink>
847 ))}
Merge branch 'worktree-agent-ad7c6d88d93adc817'848 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look849
850 <Rule />
851 <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 bar852 {/* Who belongs, for every member to see; owners invite and manage there. */}
Merge branch 'worktree-agent-ad7c6d88d93adc817'853 <SidebarLink to={`/${ws.slug}/-/people`} icon={<Users size={15} />} current={here === "people"}>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar854 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar855 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'856 <SidebarLink to={`/${ws.slug}/-/teams`} icon={WORKSPACE_ICONS.teams} current={here === "teams"}>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar857 Teams
858 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'859 <SidebarLink to={`/${ws.slug}/-/usage`} icon={<BarChart3 size={15} />} current={here === "usage"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look860 Usage
861 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'862 <SidebarLink to="/support" icon={<LifeBuoy size={15} />} current={here === "support"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look863 Support
864 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar865 {/* 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 look866 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member867 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 look868 icon={<Settings size={15} />}
869 drill
Merge branch 'worktree-agent-ad7c6d88d93adc817'870 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871 >
872 Settings
873 </SidebarLink>
874 </div>
875 </>
Merge branch 'worktree-agent-ad7c6d88d93adc817'876 ) : (
877 <SidebarGroup title="Projects">
878 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
879 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look880 )}
881 </nav>
882 );
883}
884
Merge branch 'worktree-agent-ad7c6d88d93adc817'885/** Insights, coming: the workspace's own page says what it will be. */
886const INSIGHTS = roadmapItem("insights");
887
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look888/** A workspace's settings, drilled into from Settings in the main list. */
889function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
890 return (
891 <nav aria-label="Workspace settings" className={PANEL}>
892 <BackRow to={`/${slug}`} label="Settings" context={slug} />
893 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings894 {owner && (
895 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
896 General
897 </SidebarLink>
898 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look899 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
900 Repositories
901 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments902 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
903 Access tokens
904 </SidebarLink>
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules905 {owner && (
906 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
907 Personal access tokens
908 </SidebarLink>
909 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge910 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />}>
911 Rules
912 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look913 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar914 <SidebarGroup title="Agents and runs" className="mt-3">
915 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
916 Guardrails
917 </SidebarLink>
918 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
919 Secrets and variables
920 </SidebarLink>
Merge branch 'worktree-agent-a3abfcce648e87dca'921 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
922 Actions
923 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it924 {owner && (
925 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
926 Runners
927 </SidebarLink>
928 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar929 </SidebarGroup>
930 <SidebarGroup title="Connections" className="mt-3">
931 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
932 Integrations
933 </SidebarLink>
934 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
935 Webhooks
936 </SidebarLink>
937 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look938 <Rule />
939 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings940 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments941 Billing and plans
A catalogue of model providers, and settings that feel like settings942 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API943 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
944 Audit log
945 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look946 </div>
A catalogue of model providers, and settings that feel like settings947 </nav>
948 );
949}
950
Sidebar: no settings flash on the way into a repository, and a better slide951/**
952 * What the repository menu needs. Known from the address before the page's
953 * data arrives, so the menu is right from the first frame; the counts fill
954 * in once it does.
955 */
956type MenuRepo = {
957 namespace: string;
958 name: string;
959 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look960 /** Whether they see its settings; from the repository's page once it loads. */
961 settings?: boolean;
962 /** What they may do there, once the repository's page loads. */
963 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide964 issues?: number;
965 pulls?: number;
966};
967
968function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
969 return (
970 a != null &&
971 b != null &&
972 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
973 a.name.toLowerCase() === b.name.toLowerCase()
974 );
975}
A repository has its own sidebar, as settings do976
977/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look978 * A project's own list, drilled into while you are in it: everything about
979 * the project, running and its code, and nothing else, with the way back
980 * out to all of them.
A repository has its own sidebar, as settings do981 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put982function RepoMenu({
983 repo,
984 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look985 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put986}: {
987 repo: MenuRepo;
988 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look989 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
990 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put991}) {
A repository has its own sidebar, as settings do992 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 put993 // 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 look994 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do995 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look996 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
997 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'998 <Hint label="Overview" side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero999 <div>
A flat project menu with Code first, views as tabs, and Stripe for every owner1000 <NavLink
A repository has its own sidebar, as settings do1001 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner1002 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents1003 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner1004 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1005 `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 owner1006 }
A repository has its own sidebar, as settings do1007 >
1008 <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 page1009 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do1010 </span>
1011 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
1012 <span className="text-faint">{repo.namespace}/</span>
1013 <span className="font-semibold text-fg">{repo.name}</span>
1014 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner1015 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1016 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'1017 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1018 {/* A project's pages, in the order people use them. A page with more
1019 than one view shows them as tabs across its top. */}
1020 <Rule />
1021 <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 request1022 <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 owner1023 Code
1024 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1025 <SidebarLink to={`${base}/issues`} also={[`${base}/plans`, `${base}/milestones`, `${base}/labels`, ...soonPaths(base, "Issues")]} icon={<CircleDot size={15} />} count={repo.issues}>
A flat project menu with Code first, views as tabs, and Stripe for every owner1026 Issues
1027 </SidebarLink>
1028 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
1029 Pull requests
1030 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1031 <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 owner1032 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1033 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner1034 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
1035 Workflows
Projects: what a workspace builds and runs, first on every page1036 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1037 </div>
1038 <Rule />
1039 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1040 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner1041 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
1042 Deployments
1043 </SidebarLink>
1044 ) : null}
Observability gets its own page; tabs never overflow1045 <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.">
1046 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner1047 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1048 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1049 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
1050 Security
1051 </SidebarLink>
1052 ) : null}
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971053 <SidebarLink
1054 to={`${base}/contributors`}
1055 also={[`${base}/activity`, `${base}/stargazers`, ...soonPaths(base, "Insights")]}
1056 icon={<BarChart3 size={15} />}
1057 >
A flat project menu with Code first, views as tabs, and Stripe for every owner1058 Insights
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971059 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1060 </div>
1061 {(repo.settings ?? shows.has("settings")) && (
1062 <>
1063 <Rule />
1064 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1065 Settings
1066 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1067 </>
1068 )}
A repository has its own sidebar, as settings do1069 </nav>
1070 );
1071}
1072
Project settings move into the sidebar, grouped, with a heading per page1073/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1074 * A project's settings, drilled into from its list: what is about running
1075 * it, then its agents, then its code and who can reach it. The way back
1076 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1077 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1078function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1079 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1080 // Each page shows to the roles that can use it, once the role is known.
1081 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1082 const running = shows("deployments");
1083 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1084 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1085 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1086 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1087 {shows("") && (
1088 <div className="mt-2 space-y-px">
1089 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1090 General
1091 </SidebarLink>
1092 </div>
1093 )}
1094 {running && (
1095 <>
1096 {shows("") && <Rule />}
1097 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1098 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1099 Deployments
1100 </SidebarLink>
1101 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1102 Domains
1103 </SidebarLink>
1104 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1105 Dependencies
1106 </SidebarLink>
1107 </div>
1108 </>
1109 )}
1110 {agents && (
1111 <>
1112 <Rule />
1113 <div className="space-y-px">
1114 {shows("agents") && (
1115 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1116 Agents
1117 </SidebarLink>
1118 )}
1119 {shows("guardrails") && (
1120 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1121 Guardrails
1122 </SidebarLink>
1123 )}
1124 </div>
1125 </>
1126 )}
1127 <Rule />
1128 <div className="space-y-px">
1129 {shows("repository") && (
1130 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1131 Repository
1132 </SidebarLink>
1133 )}
1134 {shows("access") && (
1135 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1136 Access
1137 </SidebarLink>
1138 )}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1139 {shows("keys") && (
1140 <SidebarLink to={`${base}/settings/keys`} icon={<KeyRound size={15} />}>
1141 Deploy keys
1142 </SidebarLink>
1143 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1144 {shows("branches") && (
1145 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1146 Branches and merging
1147 </SidebarLink>
1148 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1149 {shows("rules") && (
1150 <SidebarLink to={`${base}/settings/rules`} icon={<Scale size={15} />}>
1151 Rules
1152 </SidebarLink>
1153 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1154 {shows("secrets") && (
1155 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1156 Secrets and variables
1157 </SidebarLink>
1158 )}
Merge branch 'worktree-agent-a3abfcce648e87dca'1159 {shows("actions") && (
1160 <SidebarLink to={`${base}/settings/actions`} icon={<PlayCircle size={15} />}>
1161 Actions
1162 </SidebarLink>
1163 )}
1164 {shows("environments") && (
1165 <SidebarLink to={`${base}/settings/environments`} icon={<Layers size={15} />}>
1166 Environments
1167 </SidebarLink>
1168 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1169 {shows("runners") && (
1170 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1171 Runners
1172 </SidebarLink>
1173 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1174 {shows("webhooks") && (
1175 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1176 Webhooks
1177 </SidebarLink>
1178 )}
1179 </div>
Project settings move into the sidebar, grouped, with a heading per page1180 </nav>
1181 );
1182}
1183
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1184/** Your own settings, drilled into from Your settings: one page each. */
1185function AccountSettingsMenu({ username }: { username: string }) {
1186 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1187 <SidebarLink to={`/settings/${page}`} icon={icon}>
1188 {ACCOUNT_SETTINGS[page].title}
1189 </SidebarLink>
Account settings in the same shape as a workspace's1190 );
1191 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1192 <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 quotas1193 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1194 <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 quotas1195 {link("profile", <CircleUserRound size={15} />)}
1196 {link("emails", <Mail size={15} />)}
1197 {link("invites", <Ticket size={15} />)}
1198 </div>
1199 <Rule />
1200 <div className="space-y-px">
1201 {link("keys", <Fingerprint size={15} />)}
1202 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1203 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1204 <Rule />
1205 <div className="space-y-px">
1206 {link("github", <GithubMark className="size-[15px]" />)}
1207 {link("applications", <Plug size={15} />)}
1208 </div>
1209 <Rule />
1210 <div className="space-y-px">
Merge main (membership, two-factor, GitHub repo roles) into tokens1211 {link("two-factor", <ShieldCheck size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1212 {link("security-log", <History size={15} />)}
1213 </div>
Account settings in the same shape as a workspace's1214 </nav>
1215 );
1216}
1217
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1218/**
1219 * Signing in and signing up, in place of the account for a visitor. Signing
1220 * in brings them back to the page they are on.
1221 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1222/** Help, status and the fine print under a visitor's sign-in buttons. */
1223function VisitorLinks() {
1224 const status = useSiteStatus();
Merge branch 'worktree-agent-ad7c6d88d93adc817'1225 const quiet = `${LEGAL_LINK} hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/60`;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1226 const dot = (
1227 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1228 ·
1229 </span>
1230 );
1231 return (
1232 // Two short rows, so a narrow sidebar never starts a line with a dot.
1233 <nav aria-label="About g1t" className="pt-0.5">
1234 <div className="flex items-center gap-0.5">
1235 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1236 <StatusDot state={status?.overall.state ?? null} />
1237 Status
1238 </a>
1239 {dot}
1240 <Link to="/support" className={quiet}>
1241 Support
1242 </Link>
1243 </div>
1244 <div className="flex items-center gap-0.5">
1245 {LEGAL_LINKS.map(([label, to], index) => (
1246 <span key={to} className="flex items-center gap-0.5">
1247 {index > 0 && dot}
1248 <Link to={to} className={quiet}>
1249 {label}
1250 </Link>
1251 </span>
1252 ))}
1253 </div>
1254 </nav>
1255 );
1256}
1257
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1258function VisitorPanel() {
1259 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1260 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1261 return (
1262 <div className="space-y-2">
1263 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1264 <div className="grid grid-cols-2 gap-2">
1265 <Link
1266 to={withNext("/login", pathname + search)}
1267 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"
1268 >
1269 <LogIn size={14} />
1270 Sign in
1271 </Link>
1272 <Link
1273 to={withNext("/register", pathname + search)}
1274 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1275 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1276 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1277 </Link>
1278 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1279 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1280 </div>
1281 );
1282}
1283
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1284function Sidebar({
1285 user,
1286 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1287 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1288 onFind,
1289 onClose,
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1290 rail = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1291}: {
1292 user: User | null;
1293 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1294 /** The page is a 404: the address names nothing the viewer can see. */
1295 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1296 onFind: () => void;
1297 /** In the sheet on a small screen: closing it, at the end of the top row. */
1298 onClose?: () => void;
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1299 /**
1300 * Beside the rail, as Code's sidebar: code only, under a Code heading.
1301 * The workspace, its settings and the account are the rail's.
1302 */
1303 rail?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1304}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1305 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1306 const { pathname } = useLocation();
1307 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1308 // Where the sidebar is drilled to follows the page being gone to, so it
1309 // moves as the link is followed, not once the page arrives.
1310 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1311 // On a 404, the address is not a place: no list is drilled into from it,
1312 // except the project's own when the project is real and visible and only
1313 // something inside it is missing (a file, a commit, an issue).
1314 const lost = missing && going == null;
1315 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1316 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1317 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1318 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1319 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1320 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 look1321 // An invitation to a repository is answered before its menu means anything.
1322 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1323 repoPath != null &&
1324 !reserved.has(repoPath[1]) &&
1325 repoPath[2] !== "-" &&
1326 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1327 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1328 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1329 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1330 // The repository the menu is for: the one loaded if it is the one being
1331 // gone to, else what the address says, at once.
1332 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 look1333 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1334 ? sameRepo(active, targetRepo)
1335 ? active
1336 : {
1337 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1338 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1339 }
1340 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1341 // The viewer's role there, from the repository's page once it has loaded.
1342 const page = useRouteLoaderData("routes/repo/layout") as
1343 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1344 | undefined;
1345 const menuRepo: MenuRepo | null =
1346 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1347 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1348 : guessed;
1349
1350 // The way from the main list to the one shown.
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1351 const trail: Level[] = [{ key: "main", node: rail && user && ws ? <CodeMenu shell={shell} slug={ws.slug} /> : <MainMenu user={user} shell={shell} /> }];
1352 if (rail && !menuRepo) {
1353 // Code's own list: nothing to drill into but a project.
1354 } else if (user && inAccount && !rail) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1355 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1356 } else if (ws && inSettings && !rail) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1357 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1358 } else if (menuRepo) {
1359 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1360 // Out of a project: to its workspace's projects when they are yours.
1361 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1362 const back = !user
1363 ? { to: "/explore", label: "Explore" }
1364 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1365 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1366 : { to: "/", label: "Home" };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1367 trail.push({
1368 key,
1369 node: (
1370 <RepoMenu
1371 repo={menuRepo}
1372 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1373 back={back}
1374 />
1375 ),
1376 });
1377 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1378 }
1379
Agents as a team: lifecycle, merge queue, billing and a new shell1380 return (
1381 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1382 {/* The same height and rule as the top bar, so the two read as one line. */}
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1383 {rail ? (
1384 <ModeHeader title="Code" onClose={onClose} />
1385 ) : (
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1386 <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 put1387 {user ? (
1388 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1389 {/* The 1 alone beside the workspace: a square hover the height of the
1390 switcher, the mark as tall as the workspace avatar. */}
1391 <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 bar1392 <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 put1393 </Link>
1394 <span className="shrink-0 text-line-strong" aria-hidden="true">
1395 /
1396 </span>
1397 <WorkspaceSwitcher user={user} shell={shell} />
1398 </>
1399 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1400 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1401 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1402 </Link>
1403 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1404 {onClose && (
1405 <button
1406 type="button"
1407 aria-label="Close menu"
1408 onClick={onClose}
1409 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1410 >
1411 <X size={16} />
1412 </button>
1413 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1414 </div>
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1415 )}
A Vercel-like sidebar and a new mark1416 <div className="px-2 pt-3">
1417 <button
1418 type="button"
1419 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1420 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 mark1421 >
1422 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1423 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1424 <PaletteKey className="rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" />
A Vercel-like sidebar and a new mark1425 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1426 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1427 <Drill trail={trail} />
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1428 {/* The account, or signing in, as one row at the very bottom; beside the rail, the rail has it. */}
1429 {!rail && (
1430 <div className="shrink-0 border-t border-line p-2">
1431 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
1432 </div>
1433 )}
1434 </div>
1435 );
1436}
1437
1438/** A mode's sidebar heading: the same height and rule as the top bar, with a way to close the phone's sheet. */
1439export function ModeHeader({ title, action, onClose }: { title: ReactNode; action?: ReactNode; onClose?: () => void }) {
1440 return (
1441 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2.5 pl-4">
1442 <h2 className="min-w-0 grow truncate text-[0.9375rem] font-semibold">{title}</h2>
1443 {action}
1444 {onClose && (
1445 <button
1446 type="button"
1447 aria-label="Close menu"
1448 onClick={onClose}
1449 className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1450 >
1451 <X size={16} />
1452 </button>
1453 )}
1454 </div>
1455 );
1456}
1457
1458/**
1459 * Code's sidebar (beside the rail): only code. Its Overview (Mission
1460 * control's code panels), the projects, what spans them, and what is
1461 * coming. The workspace's people, money and settings are Workspace's;
1462 * agents, context and memory are Agents'.
1463 */
1464function CodeMenu({ shell, slug }: { shell: ShellData; slug: string }) {
1465 const { pathname } = useLocation();
1466 const going = useNavigation().location?.pathname;
1467 const path = going ?? pathname;
1468 const at = (page: string) => path === `/${slug}/-/${page}` || path.startsWith(`/${slug}/-/${page}/`);
1469 const shared = shell.shared ?? [];
1470 return (
1471 <nav aria-label="Code" className={PANEL}>
1472 <div className="mt-3 space-y-px">
1473 <SidebarLink to={`/${slug}/-/overview`} icon={<LayoutDashboard size={15} />} current={at("overview")}>
1474 Overview
1475 </SidebarLink>
1476 </div>
1477 <div className="mt-3">
1478 <SidebarProjects slug={slug} shell={shell} current={at("projects")} />
1479 </div>
1480 <Rule />
1481 <div className="space-y-px">
1482 <SidebarLink to={`/${slug}/-/security`} icon={<ShieldCheck size={15} />} current={at("security") && !at("security/settings")}>
1483 Security
1484 </SidebarLink>
1485 <SidebarLink to={`/${slug}/-/packages`} icon={<Package size={15} />} current={at("packages")}>
1486 Packages
1487 </SidebarLink>
1488 {INSIGHTS && (
1489 <SidebarSoonLink to={`/${slug}/-/insights`} icon={<TrendingUp size={15} />} about={INSIGHTS.summary} current={at("insights")}>
1490 {INSIGHTS.title}
1491 </SidebarSoonLink>
1492 )}
1493 {roadmapIn("Workspace")
1494 .filter((item) => item.key !== "insights" && item.key !== "teams" && item.key !== "fleet")
1495 .map((item) => (
1496 <SidebarSoonLink key={item.key} to={`/${slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />} about={item.summary}>
1497 {item.title === "Board" ? "Boards" : item.title}
1498 </SidebarSoonLink>
1499 ))}
1500 </div>
1501 {shared.length > 0 && (
1502 <>
1503 <Rule />
1504 <SidebarGroup title="Shared with you">
1505 {shared.map((repo) => (
1506 <SidebarLink
1507 key={`${repo.namespace}/${repo.name}`}
1508 to={`/${repo.namespace}/${repo.name}`}
1509 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
1510 drill="hover"
1511 >
1512 <span className="font-mono text-faint">{repo.namespace}/</span>
1513 {repo.name}
1514 </SidebarLink>
1515 ))}
1516 </SidebarGroup>
1517 </>
1518 )}
1519 </nav>
1520 );
1521}
1522
1523/** Workspace pages that sit in its Settings list, drilled into from the Workspace sidebar. */
1524const WORKSPACE_SETTINGS = ["settings", "repositories", "tokens", "personal-access-tokens", "secrets", "actions", "runners", "webhooks", "emoji"];
1525
1526/**
1527 * The Workspace mode's sidebar: the workspace itself, for every member.
1528 * Its overview, people, money, connections, policies and record, then its
1529 * settings as a list of their own. Owner-only pages stay owner-only.
1530 */
1531export function WorkspaceSidebar({ slug, owner, onClose }: { slug: string; owner: boolean; onClose?: () => void }) {
1532 const { pathname } = useLocation();
1533 const going = useNavigation().location?.pathname;
1534 const path = going ?? pathname;
1535 const page = path.startsWith(`/${slug}/-/`) ? path.slice(`/${slug}/-/`.length) : "";
1536 const top = page.split("/")[0] ?? "";
1537 const at = (...pages: string[]) => pages.some((p) => page === p || page.startsWith(`${p}/`));
1538 const inSettings = WORKSPACE_SETTINGS.includes(top);
1539 const main = (
1540 <nav aria-label="Workspace" className={PANEL}>
1541 <div className="mt-3 space-y-px">
1542 <SidebarLink to={`/${slug}/-/workspace`} icon={<LayoutGrid size={15} />} current={at("workspace")}>
1543 Overview
1544 </SidebarLink>
1545 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />} current={at("people")}>
1546 People
1547 </SidebarLink>
1548 <SidebarLink to={`/${slug}/-/teams`} icon={<UsersRound size={15} />} current={at("teams")}>
1549 Teams
1550 </SidebarLink>
1551 </div>
1552 <SidebarGroup title="Usage and billing" className="mt-3">
1553 <SidebarLink to={`/${slug}/-/usage`} icon={<BarChart3 size={15} />} current={at("usage", "gateway")}>
1554 Usage
1555 </SidebarLink>
1556 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />} current={at("billing")}>
1557 Billing and plans
1558 </SidebarLink>
1559 </SidebarGroup>
1560 <SidebarGroup title="Connections" className="mt-3">
1561 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />} current={at("integrations")}>
1562 Integrations
1563 </SidebarLink>
1564 </SidebarGroup>
1565 <SidebarGroup title="Security policies" className="mt-3">
1566 <SidebarLink to={`/${slug}/-/security/settings`} icon={<ShieldCheck size={15} />} current={at("security/settings")}>
1567 Security settings
1568 </SidebarLink>
1569 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />} current={at("guardrails")}>
1570 Guardrails
1571 </SidebarLink>
1572 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />} current={at("rules")}>
1573 Rules
1574 </SidebarLink>
1575 </SidebarGroup>
1576 <Rule />
1577 <div className="space-y-px">
1578 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />} current={at("audit")}>
1579 Audit log
1580 </SidebarLink>
1581 <SidebarLink to={owner ? `/${slug}/-/settings` : `/${slug}/-/repositories`} icon={<Settings size={15} />} drill current={inSettings}>
1582 Settings
1583 </SidebarLink>
1584 </div>
1585 </nav>
1586 );
1587 const settings = (
1588 <nav aria-label="Workspace settings" className={PANEL}>
1589 <BackRow to={`/${slug}/-/workspace`} label="Settings" context={slug} />
1590 <div className="mt-2 space-y-px">
1591 {owner && (
1592 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
1593 General
1594 </SidebarLink>
1595 )}
1596 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
1597 Repositories
1598 </SidebarLink>
1599 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
1600 Access tokens
1601 </SidebarLink>
1602 {owner && (
1603 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
1604 Personal access tokens
1605 </SidebarLink>
1606 )}
1607 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
1608 Webhooks
1609 </SidebarLink>
1610 <SidebarLink to={`/${slug}/-/emoji`} icon={<Smile size={15} />}>
1611 Emoji
1612 </SidebarLink>
1613 </div>
1614 <SidebarGroup title="Runs" className="mt-3">
1615 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
1616 Secrets and variables
1617 </SidebarLink>
1618 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
1619 Actions
1620 </SidebarLink>
1621 {owner && (
1622 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
1623 Runners
1624 </SidebarLink>
1625 )}
1626 </SidebarGroup>
1627 </nav>
1628 );
1629 const trail: Level[] = [{ key: "workspace", node: main }];
1630 if (inSettings) trail.push({ key: "workspace:settings", node: settings });
1631 return (
1632 <div className="flex h-full flex-col">
1633 <ModeHeader title="Workspace" onClose={onClose} />
1634 <Drill trail={trail} />
1635 </div>
1636 );
1637}
1638
1639/** Your own settings, beside the rail: under the account menu, not any workspace's. */
1640export function AccountSidebar({ username, onClose }: { username: string; onClose?: () => void }) {
1641 return (
1642 <div className="flex h-full flex-col">
1643 <ModeHeader title="Your account" onClose={onClose} />
1644 <div className="min-h-0 grow">
1645 <AccountSettingsMenu username={username} />
A Vercel-like sidebar and a new mark1646 </div>
1647 </div>
1648 );
1649}
1650
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1651
A Vercel-like sidebar and a new mark1652/** Words for the sections a path can end in. */
1653const SECTIONS: Record<string, string> = {
1654 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1655 milestones: "Milestones",
1656 labels: "Labels",
A Vercel-like sidebar and a new mark1657 pulls: "Pull requests",
1658 queue: "Merge queue",
1659 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1660 plans: "Outcomes",
1661 actions: "Workflows",
1662 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1663 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1664 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1665 access: "Access",
1666 invitations: "Invitation",
1667 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1668 branches: "Branches",
1669 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1670 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1671 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1672 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1673 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1674 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1675 people: "People",
1676 projects: "Projects",
1677 teams: "Teams",
A Vercel-like sidebar and a new mark1678 tokens: "Access tokens",
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1679 "personal-access-tokens": "Personal access tokens",
A Vercel-like sidebar and a new mark1680 usage: "Usage",
Site: the AI Gateway page's breadcrumb reads "AI Gateway"1681 gateway: "AI Gateway",
Secrets and variables: one list, rows per environment, for workflows and deployments1682 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1683 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1684 webhooks: "Webhooks",
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1685 emoji: "Emoji",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1686 domains: "Domains",
1687 guardrails: "Guardrails",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1688 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1689 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1690 tree: "Files",
1691 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1692 agents: "Agents",
1693 context: "Context",
1694 memory: "Memory",
1695 security: "Security",
1696 packages: "Packages",
1697 runners: "Runners",
Merge branch 'worktree-agent-a3abfcce648e87dca'1698 environments: "Environments",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1699 workflows: "Workflows",
1700 observability: "Observability",
1701 insights: "Insights",
1702 sessions: "Sessions",
Chat and workspace agents: channels, DMs and named agents you talk to1703 chat: "Chat",
1704 docs: "Docs",
1705 home: "Home",
1706 "code-access": "Code access",
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1707 overview: "Overview",
1708 workspace: "Workspace",
A Vercel-like sidebar and a new mark1709};
1710
Merge branch 'worktree-agent-a3abfcce648e87dca'1711/** Settings pages whose name differs from the section's of the same word. */
1712const SETTINGS_SECTIONS: Record<string, string> = { branches: "Branches and merging", actions: "Actions" };
1713
A Vercel-like sidebar and a new mark1714/** 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 quotas1715function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1716 // A 404 names nothing from the address, unless the project is real and
1717 // visible and only something inside it is missing.
1718 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1719 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1720 return <span className="text-sm font-medium">Not found</span>;
1721 }
A Vercel-like sidebar and a new mark1722 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1723 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1724 if (parts.length === 0) return <span className="text-sm font-medium">Home</span>;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1725 // Your settings: Settings / Emails.
1726 if (parts[0] === "settings") {
1727 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1728 return <Trail trail={[{ label: "Settings", to: FIRST_SETTINGS_PAGE }, ...(page ? [{ label: ACCOUNT_SETTINGS[page].title, to: `/settings/${page}` }] : [])]} />;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1729 }
A Vercel-like sidebar and a new mark1730 if (reserved.includes(parts[0]!)) {
1731 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1732 settings: "Account",
A Vercel-like sidebar and a new mark1733 explore: "Explore",
Projects live: fixes from walking them end to end1734 new: "New project",
A Vercel-like sidebar and a new mark1735 search: "Search",
1736 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1737 policies: "Policies",
1738 security: "Security",
1739 support: "Support",
1740 status: "Status",
1741 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1742 inbox: "Inbox",
A Vercel-like sidebar and a new mark1743 };
1744 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1745 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1746 // A person's profile, by their handle.
1747 if (parts[0] === "u" && parts[1]) {
1748 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1749 }
A Vercel-like sidebar and a new mark1750 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1751 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1752 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1753 const page = `/${owner}/-/${third}`;
1754 if (third && SETTINGS_PAGE.test(page)) {
1755 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1756 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1757 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1758 } else if (second) {
1759 const repo = `/${owner}/${second}`;
1760 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1761 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1762 else if (third === "issues" && fourth && fourth !== "new") trail.push({ label: `Issue #${fourth}`, short: `#${fourth}`, to: `${repo}/issues/${fourth}` });
A Vercel-like sidebar and a new mark1763 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 deployments1764 else if (third && SECTIONS[third]) {
1765 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1766 // A settings page names which one: Settings / Secrets and variables.
1767 if (third === "settings" && fourth && SECTIONS[fourth]) {
Merge branch 'worktree-agent-a3abfcce648e87dca'1768 trail.push({ label: SETTINGS_SECTIONS[fourth] ?? SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1769 }
1770 }
A Vercel-like sidebar and a new mark1771 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1772 return <Trail trail={trail} />;
1773}
1774
1775/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1776type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1777
1778/**
1779 * The trail of links. On a phone there is room for one: the page itself,
1780 * with a way back to where it sits; the whole trail from a wider screen up.
1781 */
1782function Trail({ trail }: { trail: Crumb[] }) {
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1783 // Up on a phone, unless up is the workspace itself: its pages are the tabs.
1784 const up = trail.length > 1 && !/^\/[^/]+$/.test(trail[trail.length - 2]!.to) ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1785 return (
1786 <nav aria-label="Where you are" className="flex min-w-0 items-center gap-1.5 text-sm">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1787 {up && (
1788 <Link
1789 to={up.to}
1790 prefetch="intent"
1791 aria-label={`Back to ${up.label}`}
1792 className="-ml-1 flex size-8 shrink-0 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg sm:hidden"
1793 >
1794 <ChevronLeft size={16} />
1795 </Link>
1796 )}
1797 {trail.map((crumb, index) => {
1798 const last = index === trail.length - 1;
1799 return (
1800 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1801 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1802 <Link
1803 to={crumb.to}
1804 prefetch="intent"
1805 aria-current={last ? "page" : undefined}
1806 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1807 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1808 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1809 >
1810 {last && crumb.short ? (
1811 <>
1812 <span className="sm:hidden">{crumb.short}</span>
1813 <span className="hidden sm:inline">{crumb.label}</span>
1814 </>
1815 ) : (
1816 crumb.label
1817 )}
1818 </Link>
1819 </span>
1820 );
1821 })}
A Vercel-like sidebar and a new mark1822 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1823 );
1824}
1825
Search across all of g1t, Explore, and a command palette1826type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1827
1828/** 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 look1829function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1830 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1831 const commands: Command[] = [
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1832 { label: "Home", to: "/", icon: <House size={15} /> },
1833 ...(shell.workspace
1834 ? [
1835 { label: "Chat", to: `/${shell.workspace.slug}/-/chat`, icon: <MessagesSquare size={15} /> },
1836 { label: "Agents", to: `/${shell.workspace.slug}/-/agents`, icon: <Sparkles size={15} /> },
1837 ...(hasCodeAccess(shell.workspace) ? [{ label: "Code overview", to: `/${shell.workspace.slug}/-/overview`, icon: <Code2 size={15} /> }] : []),
1838 { label: "Workspace", hint: "People, billing, policies, settings", to: `/${shell.workspace.slug}/-/workspace`, icon: <Building2 size={15} /> },
1839 ]
1840 : []),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1841 ...(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-agent1842 ? [{ 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 step1843 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1844 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1845 { 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 all1846 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1847 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1848 { 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 quotas1849 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1850 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1851 .filter((page) => page !== "profile")
1852 .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 shell1853 ];
1854 const repo = shell.repo;
1855 if (repo) {
1856 const base = `/${repo.namespace}/${repo.name}`;
1857 const name = `${repo.namespace}/${repo.name}`;
1858 commands.unshift(
Projects: what a workspace builds and runs, first on every page1859 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1860 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1861 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1862 { 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 step1863 ...(repo.member
1864 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1865 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1866 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1867 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1868 ...(repo.member
1869 ? [
1870 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1871 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1872 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1873 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1874 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1875 ]
1876 : []),
1877 );
1878 }
1879 for (const membership of user.workspaces ?? []) {
1880 commands.push(
Workspace names and icons, and a component kit for every control1881 { 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 sidebar1882 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1883 { 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 request1884 { label: "Usage", hint: membership.slug, to: `/${membership.slug}/-/usage`, icon: <BarChart3 size={15} /> },
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1885 { label: "AI Gateway", hint: `${membership.slug} · Usage`, to: `/${membership.slug}/-/gateway`, icon: <Network size={15} /> },
Projects: what a workspace builds and runs, first on every page1886 { 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 settings1887 { 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 look1888 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1889 );
1890 }
1891 for (const listed of shell.repos) {
1892 commands.push({
Projects: what a workspace builds and runs, first on every page1893 label: listed.title ?? listed.name,
1894 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1895 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1896 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1897 });
1898 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1899 // "Put an agent on …": one per project, after the projects themselves.
1900 for (const listed of shell.repos) {
1901 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1902 commands.push({
1903 label: `Put an agent on ${listed.title ?? listed.name}`,
1904 hint: `${listed.namespace}/${listed.name}`,
1905 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1906 icon: <Sparkles size={15} />,
1907 });
1908 }
Agents as a team: lifecycle, merge queue, billing and a new shell1909 return commands;
1910}
1911
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1912/** 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 look1913function 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 put1914 const commands: Command[] = [];
1915 const repo = shell.repo;
1916 if (repo) {
1917 const base = `/${repo.namespace}/${repo.name}`;
1918 const name = `${repo.namespace}/${repo.name}`;
1919 commands.push(
1920 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1921 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1922 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1923 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1924 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1925 );
1926 }
1927 commands.push(
1928 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1929 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1930 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1931 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1932 { 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 look1933 { 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 put1934 );
1935 return commands;
1936}
1937
Agents as a team: lifecycle, merge queue, billing and a new shell1938/**
Merge branch 'worktree-agent-aca4790ea223aa235'1939 * The page while the next one loads: after a moment it dims, under the
1940 * bar below, so it is plainly on its way out rather than frozen, and the
1941 * next page fades up from there. Sending a form does not dim it: the
1942 * form's button says it is working. Spread on the element holding the page.
1943 */
1944export function useLeaving() {
1945 const navigation = useNavigation();
1946 const method = navigation.formMethod?.toUpperCase();
1947 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1948 return {
1949 "aria-busy": leaving || undefined,
1950 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1951 };
1952}
1953
1954/**
Agents as a team: lifecycle, merge queue, billing and a new shell1955 * A bar across the top of the page while the next one loads, as GitHub
1956 * has: it appears at once, creeps towards the end while waiting, then fills
1957 * and fades when the page arrives.
1958 */
1959export function Progress() {
1960 const navigation = useNavigation();
1961 const busy = navigation.state !== "idle";
1962 const [width, setWidth] = useState(0);
1963 const [visible, setVisible] = useState(false);
1964 useEffect(() => {
1965 if (busy) {
1966 setVisible(true);
1967 setWidth(12);
1968 // Slows as it goes, never quite arriving until the page does.
1969 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1970 return () => clearInterval(timer);
1971 }
1972 setWidth((now) => (now > 0 ? 100 : 0));
1973 const fade = setTimeout(() => setVisible(false), 250);
1974 const reset = setTimeout(() => setWidth(0), 550);
1975 return () => {
1976 clearTimeout(fade);
1977 clearTimeout(reset);
1978 };
1979 }, [busy]);
1980 return (
1981 <div
1982 aria-hidden="true"
1983 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
1984 visible ? "opacity-100" : "opacity-0"
1985 }`}
1986 >
1987 <div
1988 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
1989 style={{ width: `${width}%` }}
1990 />
1991 </div>
1992 );
1993}
1994
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell1995/** The top bar's way to g1t: the direct message with the workspace's orchestrator, opened or made. */
1996function AskG1tButton({ slug }: { slug: string }) {
1997 return (
1998 <Hint label="Ask g1t">
1999 <Link
2000 to={`/${slug}/-/chat?agent=g1t`}
2001 aria-label="Ask g1t"
2002 className="flex h-9 items-center gap-1.5 rounded-md border border-line px-2 text-sm text-fg/90 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg sm:px-2.5"
2003 >
2004 <G1tMark size={18} />
2005 <span className="hidden sm:inline">Ask g1t</span>
2006 </Link>
2007 </Hint>
2008 );
2009}
2010
Chat and workspace agents: channels, DMs and named agents you talk to2011/** The mode's sidebar beside the rail: a shade lighter than the page. */
2012const SIDEBAR_BOX = "h-full border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]";
2013
Agents as a team: lifecycle, merge queue, billing and a new shell2014/**
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2015 * Which sidebar sits beside the rail: each mode's own, or none (Docs, and
2016 * the Inbox, which is a page of its own). Without a workspace (a visitor),
2017 * the one sidebar there always was.
Chat and workspace agents: channels, DMs and named agents you talk to2018 */
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2019function sidebarFor(mode: ModeKey | null): Panel | null {
Chat and workspace agents: channels, DMs and named agents you talk to2020 if (mode == null) return "code";
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2021 if (mode === "docs" || mode === "inbox") return null;
2022 return mode;
Chat and workspace agents: channels, DMs and named agents you talk to2023}
2024
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2025/** The sidebars that sit beside the rail, one per mode that has one. */
2026type Panel = "home" | "chat" | "agents" | "code" | "workspace" | "account";
2027
Chat and workspace agents: channels, DMs and named agents you talk to2028/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2029 * The app: a sidebar with the workspace, its repositories and the sections
2030 * of the one being looked at; a slim bar with search and the account; and
2031 * the page. A visitor who is not signed in gets the same frame, with
2032 * Explore and Search in place of the workspace, and signing in in place of
2033 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell2034 */
2035export function AppShell({
2036 user,
2037 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2038 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell2039 banner,
2040 children,
2041}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2042 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell2043 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2044 /** The page is a 404 (see Sidebar). */
2045 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell2046 banner?: ReactNode;
2047 children: ReactNode;
2048}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2049 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell2050 const [drawer, setDrawer] = useState(false);
2051 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2052 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2053 const signUpLabel = useSignUpCopy().primary;
2054 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell2055
2056 // A new page closes the drawer on small screens.
2057 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2058 // Mission control's code, fetched while the browser is idle, so going
2059 // home never waits on it (routes/home.tsx).
2060 useEffect(() => {
2061 if (!user) return;
2062 const load = () => void import("./mission-control").catch(() => undefined);
2063 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
2064 if (idle) idle(load);
2065 else setTimeout(load, 1500);
2066 }, [user]);
Search across all of g1t, Explore, and a command palette2067 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'2068 const leaving = useLeaving();
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2069 useVisualViewport();
Agents as a team: lifecycle, merge queue, billing and a new shell2070
Chat and workspace agents: channels, DMs and named agents you talk to2071 // The rail and the mode's sidebar (docs/WORKSPACE.md, "Shell"), for
2072 // someone in a workspace. A visitor, or someone with none, keeps the one
2073 // sidebar.
2074 const going = useNavigation().location?.pathname;
2075 const ws = user ? shell.workspace : null;
2076 const mode: ModeKey | null = ws ? modeOf(going ?? pathname, ws.slug) : null;
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2077 const panel = ws ? sidebarFor(mode) : "code";
2078 const code = ws ? hasCodeAccess(ws) : true;
Chat and workspace agents: channels, DMs and named agents you talk to2079 const { sidebar: chatSidebar } = useChatSidebar();
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2080 // Notify: the feed socket's live counts (lib/notify-client.ts) once it has them; the page's until then.
2081 const live = useLiveBadges(ws?.slug);
2082 const chatUnread = chatSidebar
2083 ? unreadTotals(chatSidebar.entries)
2084 : live?.chat != null
2085 ? { unread: live.chat, mentions: live.mentions ?? 0 }
2086 : (shell.chat ?? { unread: 0, mentions: 0 });
2087 const inboxUnread = live?.inbox ?? shell.inbox?.unread ?? 0;
2088 const sidebarNode = (inSheet: boolean) => {
2089 const close = inSheet ? () => setDrawer(false) : undefined;
2090 const find = () => {
2091 if (inSheet) setDrawer(false);
2092 setPalette(true);
2093 };
2094 if (!ws || !user || panel === "code") {
2095 return <Sidebar user={user} shell={shell} missing={missing} onFind={find} onClose={close} rail={Boolean(ws && user)} />;
2096 }
2097 switch (panel) {
2098 case "home":
2099 return <HomeSidebar slug={ws.slug} shell={shell} code={code} onFind={find} header={<ModeHeader title="Home" onClose={close} />} />;
2100 case "chat":
2101 return <ChatSidebar slug={ws.slug} />;
2102 case "agents":
2103 return <AgentsSidebar slug={ws.slug} shellAgents={shell.agents ?? null} code={code} owner={ws.role === "owner"} />;
2104 case "workspace":
2105 return <WorkspaceSidebar slug={ws.slug} owner={ws.role === "owner"} onClose={close} />;
2106 case "account":
2107 return <AccountSidebar username={user.username} onClose={close} />;
2108 default:
2109 return null;
2110 }
2111 };
Chat and workspace agents: channels, DMs and named agents you talk to2112 const rail =
2113 user && ws ? (
2114 <Rail
2115 user={user}
2116 workspace={ws}
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2117 unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }}
2118 help={<HelpMenu />}
Chat and workspace agents: channels, DMs and named agents you talk to2119 account={<AccountMenu user={user} rail />}
2120 />
2121 ) : null;
2122 const pad = rail ? (panel ? "lg:pl-[21rem]" : "lg:pl-20") : "lg:pl-64";
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2123 // On a phone (below 768px): the tab bar, and a conversation full screen.
2124 const conversation = isConversation(going ?? pathname);
2125 const tabs = Boolean(user && ws) && !conversation;
Chat and workspace agents: channels, DMs and named agents you talk to2126
Agents as a team: lifecycle, merge queue, billing and a new shell2127 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2128 // The phone's menu is a sheet: a dialog that holds focus, closes on
2129 // Escape or a tap outside, and gives focus back to the menu button.
2130 <Sheet open={drawer} onOpenChange={setDrawer}>
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2131 <div className="min-h-dvh">
Agents as a team: lifecycle, merge queue, billing and a new shell2132 <Progress />
Chat and workspace agents: channels, DMs and named agents you talk to2133 <aside className="fixed inset-y-0 left-0 z-40 hidden lg:flex">
2134 {rail}
2135 {panel && <div className={`${SIDEBAR_BOX} w-64`}>{sidebarNode(false)}</div>}
Agents as a team: lifecycle, merge queue, billing and a new shell2136 </aside>
Chat and workspace agents: channels, DMs and named agents you talk to2137 <SheetContent
2138 side="left"
2139 showClose={false}
2140 aria-describedby={undefined}
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2141 // Focus would land on the workspace's avatar and open its hint over
2142 // the sidebar's heading; the sheet itself takes it instead.
2143 onOpenAutoFocus={(event) => {
2144 event.preventDefault();
2145 (event.currentTarget as HTMLElement | null)?.focus();
2146 }}
Chat and workspace agents: channels, DMs and named agents you talk to2147 className={`lg:hidden ${rail ? `flex flex-row ${panel ? "w-[21rem] max-w-[92vw] sm:max-w-[21rem]" : "w-20 sm:max-w-20"}` : "w-72 max-w-[85vw] sm:max-w-72"}`}
2148 >
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2149 <SheetTitle className="sr-only">Menu</SheetTitle>
Chat and workspace agents: channels, DMs and named agents you talk to2150 {rail}
2151 {panel && <div className={`min-w-0 grow ${rail ? SIDEBAR_BOX : ""}`}>{sidebarNode(true)}</div>}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2152 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell2153
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2154 <div className={`flex min-h-dvh min-w-0 flex-col ${pad} ${tabs ? "pb-(--tabbar-h)" : ""}`}>
Chat and workspace agents: channels, DMs and named agents you talk to2155 <header
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2156 className={`sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-line bg-bg/85 pt-[env(safe-area-inset-top)] pr-[max(1rem,env(safe-area-inset-right))] pl-[max(1rem,env(safe-area-inset-left))] backdrop-blur sm:px-6 max-md:h-[calc(3.5rem+env(safe-area-inset-top))] ${mode === "chat" ? "lg:hidden" : ""} ${conversation ? "max-md:hidden" : ""}`}
Chat and workspace agents: channels, DMs and named agents you talk to2157 >
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2158 {/* A phone: the workspace's avatar opens everything else (components/mobile.tsx). */}
2159 {user && ws && <AvatarSheetButton user={user} workspace={ws} />}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2160 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell2161 aria-label="Open menu"
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2162 className={`rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden ${user && ws ? "max-md:hidden" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell2163 >
2164 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2165 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2166 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2167 <div className="ml-auto flex items-center gap-1.5">
2168 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
2169 <button
2170 type="button"
2171 aria-label="Search or jump to"
2172 onClick={() => setPalette(true)}
2173 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
2174 >
2175 <Search size={16} />
2176 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2177 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
2178 <NavLink
2179 to="/explore"
2180 prefetch="intent"
2181 aria-label="Explore"
2182 className={({ isActive }) =>
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2183 `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 ${user ? "max-md:hidden" : ""} ${isActive ? "text-fg" : "text-muted"}`
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2184 }
2185 >
2186 <Compass size={16} className="sm:hidden" />
2187 <span className="hidden sm:inline">Explore</span>
2188 </NavLink>
A Vercel-like sidebar and a new mark2189 <a
2190 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals2191 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 mark2192 >
2193 Docs
2194 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2195 {user && (
2196 <>
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2197 {shell.workspace ? <AskG1tButton slug={shell.workspace.slug} /> : <AgentButton />}
2198 {/* On a phone the Inbox is a tab. */}
2199 <span className={shell.workspace ? "max-md:hidden" : undefined}>
2200 <InboxBell counts={shell.inbox ? { ...shell.inbox, unread: inboxUnread } : null} />
2201 </span>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2202 </>
2203 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2204 {!user ? (
2205 // The sidebar has these too, but on a phone it is folded away.
2206 <Link
2207 to={withNext("/login", here)}
2208 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"
2209 >
2210 Sign in
2211 </Link>
2212 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2213 <>
2214 {/* Room between what comes in (the inbox) and what you make. */}
2215 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell2216 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2217 <Tooltip>
2218 <TooltipTrigger asChild>
2219 <DropdownMenuTrigger
2220 aria-label="Create new"
2221 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"
2222 >
2223 <Plus size={16} />
2224 <ChevronDown size={13} className="text-muted" />
2225 </DropdownMenuTrigger>
2226 </TooltipTrigger>
2227 <TooltipContent>Create new…</TooltipContent>
2228 </Tooltip>
2229 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell2230 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2231 <>
2232 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
2233 <DropdownMenuItem asChild>
2234 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
2235 <CircleDot />
2236 New issue
2237 </Link>
2238 </DropdownMenuItem>
2239 {shell.repo.member && (
2240 <DropdownMenuItem asChild>
2241 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
2242 <ListTree />
2243 Plan work
2244 </Link>
2245 </DropdownMenuItem>
2246 )}
2247 <DropdownMenuSeparator />
2248 </>
Agents as a team: lifecycle, merge queue, billing and a new shell2249 )}
2250 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2251 <Link to="/?agent=new">
2252 <Sparkles />
2253 Put an agent on it
2254 </Link>
2255 </DropdownMenuItem>
2256 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2257 <Link to="/new">
Projects: what a workspace builds and runs, first on every page2258 <Box />
2259 New project
Agents as a team: lifecycle, merge queue, billing and a new shell2260 </Link>
2261 </DropdownMenuItem>
2262 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2263 <Link to="/new/github">
2264 <GitBranch />
2265 Import from GitHub
2266 </Link>
2267 </DropdownMenuItem>
2268 <DropdownMenuSeparator />
2269 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2270 <Link to="/workspaces/new">
2271 <Users />
2272 New workspace
2273 </Link>
2274 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'2275 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
2276 {shell.workspace &&
2277 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
2278 <DropdownMenuItem asChild>
2279 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
2280 <UsersRound />
2281 New team
2282 </Link>
2283 </DropdownMenuItem>
2284 ) : (
2285 <DropdownMenuItem disabled title="Only owners can create teams in this workspace.">
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2286 <UsersRound />
2287 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'2288 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
2289 </DropdownMenuItem>
2290 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell2291 </DropdownMenuContent>
2292 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2293 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2294 )}
Agents as a team: lifecycle, merge queue, billing and a new shell2295 </div>
2296 </header>
2297 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2298 <main id="content" tabIndex={-1} {...leaving} className={`min-w-0 grow outline-none ${leaving.className}`}>
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in2299 <InMain.Provider value={true}>{children}</InMain.Provider>
Merge branch 'worktree-agent-aca4790ea223aa235'2300 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell2301 </div>
One platform: g1t the orchestrator, agents hired into roles, live notifications, reactions, and a phone-first shell2302 {user && ws && <MobileTabBar workspace={ws} unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }} />}
Search across all of g1t, Explore, and a command palette2303 <CommandPalette
2304 open={palette}
2305 onOpenChange={setPalette}
2306 commands={commands}
2307 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
2308 />
Agents as a team: lifecycle, merge queue, billing and a new shell2309 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2310 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell2311 );
2312}

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