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

Integrations are a directory of connectors, for a workspace and for each person1import { Activity, BarChart3, Building2, MessagesSquare, Bell, BookMarked, BookOpen, Blocks, 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 kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers5import { type Abilities, type ChatSidebarEntry, type InboxCounts, type WorkspaceAgent, type Membership, type Spike, type User, hasCodeAccess, mayCreateTeams, shownUsername } 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";
Docs: a workspace knowledge base people and agents write together31import { DocsSidebar } from "./docs/sidebar";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)32import { HelpMenu, Rail } from "./rail";
33import { HomeSidebar } from "./home-sidebar";
34import { G1tMark } from "./orchestrator";
35import { AvatarSheetButton, MobileTabBar, isConversation, useVisualViewport } from "./mobile";
Chat and workspace agents: channels, DMs and named agents you talk to36import { useChatSidebar } from "./chat/actions";
37import { unreadTotals } from "../lib/chat";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38import { 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 put39import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas40import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
41import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put42import { withNext } from "../lib/next";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)43import { useInviteOnly, useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas44import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar45import { 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 quotas46import type { AccountMenuData } from "../routes/settings-menu-json";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)47import { useLiveBadges } from "../lib/notify-client";
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers48import { OwnPresenceDot, OwnPresenceItems, StatusDialog } from "./presence";
Agents as a team: lifecycle, merge queue, billing and a new shell49
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put50/**
51 * What the sidebar needs, worked out by the root loader. For a visitor who
52 * is not signed in there is no workspace, no projects and no usage: only
53 * the project being looked at, if they can see it.
54 */
Agents as a team: lifecycle, merge queue, billing and a new shell55export type ShellData = {
56 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control57 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar58 /** All its projects, by name, for the palette: `name` is the slug in their address. */
59 repos: ShortcutProject[];
60 /** The person's pinned projects in it, in their order (lib/pins.ts). */
61 pinned?: ShortcutProject[];
62 /** What they opened there last, latest first, leaving out the pinned. */
63 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look64 /** Repositories shared with them in workspaces they do not belong to. */
65 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page66 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell67 repo: {
68 namespace: string;
69 name: string;
70 member: boolean;
71 issues: number;
72 pulls: number;
73 } | null;
Billing accounts, terms and enterprises; g1t is no longer free74 /** Where the workspace stands against its usage limit, if billing is on. */
75 limit: {
76 exposureMicros: number;
77 ceilingMicros: number | null;
78 state: "ok" | "warning" | "stopped";
79 comped: boolean;
80 } | null;
Free while g1t is being built out; agents can check out their own forks81 /** Whether g1t charges nothing for now, while it is being built out. */
82 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request83 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy84 /** This month's usage: charged, or at cost while g1t is free. */
85 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look86 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
87 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 all88 /** What is unread in their inbox, for the bell. Absent for a visitor. */
89 inbox?: InboxCounts | null;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)90 /**
91 * Chat, for the rail's badge and Home's sidebar: what is unread, and the
92 * starred and latest conversations. Null when chat did not answer in time.
93 */
94 chat?: { unread: number; mentions: number; starred?: ChatSidebarEntry[]; recent?: ChatSidebarEntry[] } | null;
95 /** The workspace's agents, g1t first, for Home's and Agents' sidebars; null when not known. */
96 agents?: ShellAgent[] | null;
Agents as a team: lifecycle, merge queue, billing and a new shell97};
98
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)99/** An agent as the shell lists it. */
100export type ShellAgent = Pick<
101 WorkspaceAgent,
102 "id" | "handle" | "display_name" | "avatar" | "avatar_seed" | "role" | "status" | "title" | "team" | "department" | "builtin"
103>;
104
Agents as a team: lifecycle, merge queue, billing and a new shell105function SidebarLink({
106 to,
107 icon,
108 end,
109 count,
110 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look111 drill,
Merge branch 'worktree-agent-ad7c6d88d93adc817'112 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell113 children,
114}: {
115 to: string;
116 icon: ReactNode;
117 end?: boolean;
118 count?: number;
Projects: what a workspace builds and runs, first on every page119 /** Other path prefixes under which this link is the current one. */
120 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look121 /** It opens a list of its own: a chevron says so, always or on hover. */
122 drill?: boolean | "hover";
Merge branch 'worktree-agent-ad7c6d88d93adc817'123 /** Whether it is the current row, when the list works that out itself. */
124 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell125 children: ReactNode;
126}) {
127 const { pathname } = useLocation();
128 return (
129 <NavLink
130 to={to}
131 end={end}
132 prefetch="intent"
133 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page134 const current =
Merge branch 'worktree-agent-ad7c6d88d93adc817'135 lit ??
136 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark137 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 shell138 current
139 ? "bg-raised font-medium text-fg"
140 : isPending
141 ? "bg-raised/60 text-fg"
142 : "text-muted hover:bg-raised/60 hover:text-fg"
143 }`;
144 }}
145 >
146 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
147 <span className="min-w-0 grow truncate">{children}</span>
148 {count != null && count > 0 && (
149 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
150 {count}
151 </span>
152 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look153 {drill && (
154 <ChevronRight
155 size={14}
156 aria-hidden="true"
157 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
158 />
159 )}
Agents as a team: lifecycle, merge queue, billing and a new shell160 </NavLink>
161 );
162}
163
Pull requests say where they are live; the project menu shows what is coming164/**
165 * Something a project will have, shown where it will live so people can
166 * see where g1t is going. Not a link: nothing is behind it yet.
167 */
168function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
169 return (
Merge branch 'worktree-agent-ae1299e92e4462012'170 <Hint label={about} side="right">
171 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
172 <span className="shrink-0 opacity-70">{icon}</span>
173 <span className="grow truncate">{children}</span>
174 <span className="sr-only">{about}</span>
175 <SoonPill />
176 </div>
177 </Hint>
Pull requests say where they are live; the project menu shows what is coming178 );
179}
180
The project menu opens by section, and the workspace has its own panel181/**
A flat project menu with Code first, views as tabs, and Stripe for every owner182 * A page that is coming, as a link to its roadmap page: faint, with Soon,
183 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel184 */
A flat project menu with Code first, views as tabs, and Stripe for every owner185function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel186 to,
187 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner188 icon,
189 about,
Merge branch 'worktree-agent-ad7c6d88d93adc817'190 current: lit,
The project menu opens by section, and the workspace has its own panel191 children,
192}: {
193 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner194 also?: string[];
195 icon: ReactNode;
196 about: string;
Merge branch 'worktree-agent-ad7c6d88d93adc817'197 /** Whether it is the current row, when the list works that out itself. */
198 current?: boolean;
The project menu opens by section, and the workspace has its own panel199 children: ReactNode;
200}) {
201 const { pathname } = useLocation();
202 return (
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero203 // The hint wraps a plain box: Radix's asChild merges className as a
204 // string, which would break NavLink's className function.
Merge branch 'worktree-agent-ae1299e92e4462012'205 <Hint label={about} side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero206 <div>
The project menu opens by section, and the workspace has its own panel207 <NavLink
208 to={to}
209 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner210 className={({ isActive }) => {
Merge branch 'worktree-agent-ad7c6d88d93adc817'211 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 owner212 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
213 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 panel214 }`;
215 }}
216 >
A flat project menu with Code first, views as tabs, and Stripe for every owner217 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel218 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit219 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales220 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero221 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'222 </Hint>
The project menu opens by section, and the workspace has its own panel223 );
224}
225
A flat project menu with Code first, views as tabs, and Stripe for every owner226/** Icons for what spans projects. */
227const WORKSPACE_ICONS: Record<string, ReactNode> = {
228 board: <KanbanSquare size={15} />,
229 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member230 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner231 packages: <Package size={15} />,
232 fleet: <Bot size={15} />,
233};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales234
A flat project menu with Code first, views as tabs, and Stripe for every owner235/** The Soon pages of a section, as paths, so its link is current on them. */
236function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
237 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales238}
239
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look240function SidebarGroup({
241 title,
242 action,
243 className = "",
244 children,
245}: {
246 title: string;
247 action?: ReactNode;
248 className?: string;
249 children: ReactNode;
250}) {
Agents as a team: lifecycle, merge queue, billing and a new shell251 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look252 <section className={className}>
A Vercel-like sidebar and a new mark253 <div className="mb-1 flex h-6 items-center justify-between px-2">
254 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell255 {action}
256 </div>
257 <div className="space-y-px">{children}</div>
258 </section>
259 );
260}
261
Workspace names and icons, and a component kit for every control262/** What a workspace is called where people read it: its display name, else its slug. */
263function displayName(membership: Membership): string {
264 return membership.name?.trim() || membership.slug;
265}
266
Agents as a team: lifecycle, merge queue, billing and a new shell267function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control268 const workspace = shell.workspace;
269 const current = workspace?.slug;
270 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell271 return (
272 <DropdownMenu>
Workspace names and icons, and a component kit for every control273 {/* The name goes to the workspace; only the arrows switch it. The
274 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'275 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark276 <Link
277 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents278 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look279 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 mark280 >
Workspace names and icons, and a component kit for every control281 {workspace ? (
282 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
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="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell285 )}
Workspace names and icons, and a component kit for every control286 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark287 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'288 </Hint>
A Vercel-like sidebar and a new mark289 <DropdownMenuTrigger
290 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit291 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 mark292 >
293 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell294 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control295 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar296 {workspace && (
297 <>
298 {/* The workspace's own page: its projects, packages and people. */}
299 <DropdownMenuItem asChild>
300 <Link to={`/${workspace.slug}`}>
301 <LayoutGrid />
302 Workspace overview
303 </Link>
304 </DropdownMenuItem>
305 <DropdownMenuItem asChild>
306 <Link to={`/${workspace.slug}/-/projects`}>
307 <Box />
308 All projects
309 </Link>
310 </DropdownMenuItem>
311 <DropdownMenuSeparator />
312 </>
313 )}
Agents as a team: lifecycle, merge queue, billing and a new shell314 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
315 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'316 <Hint key={membership.slug} label={displayName(membership)} side="right">
317 <DropdownMenuItem asChild>
318 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control319 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
320 <span className="flex min-w-0 grow flex-col leading-tight">
321 <span className="truncate">{displayName(membership)}</span>
322 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
323 </span>
324 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
325 {membership.slug === current ? (
326 <Check className="shrink-0 text-accent" />
327 ) : (
328 <span className="size-4 shrink-0" aria-hidden="true" />
329 )}
Agents as a team: lifecycle, merge queue, billing and a new shell330 </Link>
331 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'332 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell333 ))}
334 <DropdownMenuSeparator />
335 <DropdownMenuItem asChild>
336 <Link to="/workspaces/new">
337 <Plus />
338 New workspace
339 </Link>
340 </DropdownMenuItem>
341 </DropdownMenuContent>
342 </DropdownMenu>
A Vercel-like sidebar and a new mark343 );
344}
345
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas346/**
347 * The fine print, as one quiet row at the foot of the account menu and the
348 * visitor's panel: each link a full-height target, left-aligned. In the
349 * menu each is a menu item, so the arrow keys reach it.
350 */
351const LEGAL_LINKS: [string, string][] = [
352 ["Policies", "/policies"],
353 ["Privacy", "/policies/privacy"],
354 ["Security", "/security"],
355];
356const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
357
358function MenuLegalRow() {
359 return (
360 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
361 {LEGAL_LINKS.map(([label, to], index) => (
362 <span key={to} className="flex items-center gap-0.5">
363 {index > 0 && (
364 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
365 ·
366 </span>
367 )}
368 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
369 <Link to={to}>{label}</Link>
370 </DropdownMenuItem>
371 </span>
372 ))}
373 </div>
374 );
375}
376
377/** Status's row: the live dot and two or three words, fetched when the menu opens. */
378function StatusSummary({ open }: { open: boolean }) {
379 const status = useSiteStatus(open);
380 const words = statusTitle(status);
381 return (
382 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
383 {/* The words say it; the dot's own words for screen readers would repeat them. */}
384 <span aria-hidden="true" className="flex">
385 <StatusDot state={status?.overall.state ?? null} />
386 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents387 {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 quotas388 </span>
389 );
390}
391
392/** A menu row: an icon, words, and whatever sits at its end. */
393const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
394
Chat and workspace agents: channels, DMs and named agents you talk to395function AccountMenu({ user, rail = false }: { user: User; rail?: boolean }) {
A Vercel-like sidebar and a new mark396 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas397 const [open, setOpen] = useState(false);
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers398 // Your status's dialog: outside the menu, so the menu closes behind it.
399 const [editing, setEditing] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents400 // Name, primary address and invites left: asked for once, as soon as the
401 // 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 quotas402 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents403 const prefetch = () => {
404 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
405 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas406 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents407 if (open) prefetch();
408 // eslint-disable-next-line react-hooks/exhaustive-deps
409 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas410 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents411 // Still on its way: shapes where the words will be, the same size.
412 const loading = details.data === undefined && details.state !== "idle";
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)413 // While anyone can sign up, listed once the data says there are invites to look back on.
414 const invitesListed = useInviteOnly() || me?.invites_page === true;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas415 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark416 return (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers417 <>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas418 <DropdownMenu open={open} onOpenChange={setOpen}>
Chat and workspace agents: channels, DMs and named agents you talk to419 {rail ? (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers420 // On the rail: the avatar alone, with a dot that says how others see you (components/presence.tsx).
Chat and workspace agents: channels, DMs and named agents you talk to421 <DropdownMenuTrigger
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers422 aria-label={`Account menu for ${shownUsername(user)}`}
Chat and workspace agents: channels, DMs and named agents you talk to423 onPointerEnter={prefetch}
424 onFocus={prefetch}
425 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"
426 >
427 <Avatar name={user.username} image={user.avatar} size={34} />
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers428 <OwnPresenceDot ring="#0b0b0d" className="absolute -right-0.5 -bottom-0.5" />
Chat and workspace agents: channels, DMs and named agents you talk to429 </DropdownMenuTrigger>
430 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas431 <DropdownMenuTrigger
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers432 aria-label={`Account menu for ${shownUsername(user)}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents433 onPointerEnter={prefetch}
434 onFocus={prefetch}
Merge branch 'worktree-agent-ad7c6d88d93adc817'435 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 quotas436 >
Workspace names and icons, and a component kit for every control437 <Avatar name={user.username} image={user.avatar} size={22} />
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers438 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{shownUsername(user)}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas439 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark440 </DropdownMenuTrigger>
Chat and workspace agents: channels, DMs and named agents you talk to441 )}
442 <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 quotas443 {/* Who is signed in, and a way to their profile. */}
444 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers445 <Link to={profile} aria-label={`${me?.name ?? shownUsername(user)} (@${shownUsername(user)}), your profile`}>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas446 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents447 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
448 {loading ? (
449 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
450 ) : (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers451 <span className="truncate text-sm font-medium text-fg">{me?.name ?? shownUsername(user)}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents452 )}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers453 <span className="truncate font-mono text-xs text-muted">@{shownUsername(user)}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents454 {loading ? (
455 <Skeleton className="mt-1 h-3 w-40" />
456 ) : (
457 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
458 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas459 </span>
A Vercel-like sidebar and a new mark460 </Link>
461 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas462 <DropdownMenuSeparator className="my-1.5" />
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers463 {/* Your status, away, and pausing notifications. */}
464 <DropdownMenuGroup>
465 <OwnPresenceItems className="min-h-9 py-1.5 text-[0.8125rem]" onEdit={() => setEditing(true)} />
466 </DropdownMenuGroup>
467 <DropdownMenuSeparator className="my-1.5" />
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas468 <DropdownMenuGroup>
469 <DropdownMenuItem asChild className={MENU_ROW}>
470 <Link to={profile}>
471 <CircleUserRound />
472 Your profile
473 </Link>
474 </DropdownMenuItem>
475 <DropdownMenuItem asChild className={MENU_ROW}>
476 <Link to={FIRST_SETTINGS_PAGE}>
477 <Settings />
478 Your settings
479 </Link>
480 </DropdownMenuItem>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)481 {invitesListed && (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas482 <DropdownMenuItem asChild className={MENU_ROW}>
483 <Link to="/settings/invites">
484 <Ticket />
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)485 {ACCOUNT_SETTINGS.invites.title}
Fast pages, required checks on the branch, self-hosted runners, honest incidents486 {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 quotas487 {me?.invites_left != null && (
488 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
489 {me.invites_left} left
490 </span>
491 )}
492 </Link>
493 </DropdownMenuItem>
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)494 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas495 </DropdownMenuGroup>
496 <DropdownMenuSeparator className="my-1.5" />
497 <DropdownMenuGroup>
498 <DropdownMenuItem asChild className={MENU_ROW}>
499 <a href="https://docs.g1t.sh/">
500 <BookOpen />
501 Documentation
502 </a>
503 </DropdownMenuItem>
504 <DropdownMenuItem asChild className={MENU_ROW}>
505 <Link to="/support">
506 <LifeBuoy />
507 Support
508 </Link>
509 </DropdownMenuItem>
510 <DropdownMenuItem asChild className={MENU_ROW}>
511 <a href={STATUS_URL}>
512 <Activity />
513 Status
514 <StatusSummary open={open} />
515 </a>
516 </DropdownMenuItem>
517 </DropdownMenuGroup>
518 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark519 {/* Submitted from here: the menu closes on select, and a button
520 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 quotas521 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark522 <LogOut />
523 Sign out
524 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas525 <DropdownMenuSeparator className="my-1.5" />
526 <MenuLegalRow />
A Vercel-like sidebar and a new mark527 </DropdownMenuContent>
528 </DropdownMenu>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers529 <StatusDialog open={editing} onOpenChange={setEditing} />
530 </>
A Vercel-like sidebar and a new mark531 );
532}
533
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look534/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar535 * A workspace's settings pages, which the sidebar drills into: how it is
536 * set up and connected (guardrails, secrets, runners, integrations,
537 * webhooks), what it pays, its repositories, tokens and record. The main
538 * list keeps the places work happens and who belongs; every member can
539 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look540 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar541const SETTINGS_PAGE =
Merge branch 'worktree-agent-ad7c6d88d93adc817'542 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look543/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page544const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings545
Polish: phones, copy boxes, the plan page, the landing page, a real glide546/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look547 * The sidebar's lists are a stack, the way a phone pushes a screen: one
548 * drilled into slides in from the right over the full width, while the one
549 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide550 * ease-out, the fade quicker than the move so the two never blur together.
551 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide552 */
Sidebar: no settings flash on the way into a repository, and a better slide553const LAYER =
Sidebar: the panels really slide554 "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 look555/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide556const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings557
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look558/** One list in the stack: what it is, and its rows. */
559type Level = { key: string; node: ReactNode };
560
561/** True once the page has hydrated, so what the server drew never animates in. */
562function useHydrated(): boolean {
563 const [hydrated, setHydrated] = useState(false);
564 useEffect(() => setHydrated(true), []);
565 return hydrated;
566}
567
568/**
569 * Drilling in and out of the sidebar's lists. `trail` is the way from the
570 * main list to the one shown, worked out from the address, so a link
571 * straight to a settings page opens the sidebar already drilled in, and
572 * leaving it comes back out. Lists deeper than the one shown stay in
573 * place, off to the right, so going back slides them away rather than
574 * dropping them. Moving to a list at the same depth (a workspace's settings
575 * to a project) fades the new one in where it is.
576 */
577function Drill({ trail }: { trail: Level[] }) {
578 const depth = trail.length - 1;
579 const hydrated = useHydrated();
580 const stack = useRef<Level[]>(trail);
581 const before = stack.current;
582 const within = trail.every((level, index) => before[index]?.key === level.key);
583 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
584 const levels = stack.current;
585 // What was showing last time, to tell drilling in from moving across.
586 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
587 useEffect(() => {
588 shown.current = { depth, keys: levels.map((level) => level.key) };
589 });
A catalogue of model providers, and settings that feel like settings590 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look591 <div className="relative min-h-0 grow overflow-hidden">
592 {levels.map((level, index) => {
593 const current = index === depth;
594 const place =
595 index < depth
596 ? "pointer-events-none -translate-x-1/4 opacity-0"
597 : current
598 ? "translate-x-0 opacity-100"
599 : "pointer-events-none translate-x-full opacity-0";
600 // A list that was not there before arrives: from the right when
601 // drilling in, in place when moving across.
602 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
603 const enter = arriving
604 ? depth > shown.current.depth
605 ? "starting:translate-x-full starting:opacity-0"
606 : "starting:opacity-0"
607 : "";
608 return (
609 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
610 {level.node}
611 </div>
612 );
613 })}
614 </div>
615 );
616}
617
618/**
619 * The row at the top of a list drilled into: the list's name, which goes
620 * back out, and what it belongs to, faint at the end.
621 */
622function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
623 return (
624 <Link
625 to={to}
626 prefetch="intent"
627 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 settings628 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look629 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
630 <span className="min-w-0 shrink-0 truncate">{label}</span>
631 {context && (
632 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
633 )}
634 </Link>
635 );
636}
637
638/** A thin rule between groups of a list. */
639function Rule() {
640 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
641}
642
Navigation: the workspace's own page with tabs, pins and recent in the sidebar643/** A quiet heading inside a group: Pinned, Recent. */
644function SidebarSubhead({ children }: { children: ReactNode }) {
645 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
646}
647
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look648/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar649 * The workspace's projects as the sidebar keeps them, however many there
650 * are: the person's pins in their order, what they opened last, and the
651 * way to all of them. Pins move by dragging, or with Alt and the arrow
652 * keys; the projects service keeps the order.
653 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'654function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar655 const reorder = useFetcher({ key: `pins:${slug}` });
656 const saved = shell.pinned ?? [];
657 // While a new order is on its way, it shows as made.
658 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
659 const pinned = asked
660 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
661 : saved;
662 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
663 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
664 const [dragging, setDragging] = useState<number | null>(null);
665 const move = (from: number, by: number) => {
666 const next = movedPin(pinned, from, by);
667 if (!next) return;
668 const form = new FormData();
669 form.set("intent", "reorder");
670 for (const project of next) form.append("slug", project.name);
671 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
672 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar673 // Each row pins or unpins in place: the pin shows on hover or focus, and
674 // stays shown on a pinned row's hover so it reads as "unpin".
675 const row = (project: ShortcutProject, isPinned: boolean) => (
676 <div className="group/row relative">
677 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
678 <span className="block truncate pr-6">{project.title ?? project.name}</span>
679 </SidebarLink>
680 <PinButton
681 workspace={slug}
682 slug={project.name}
683 name={project.title ?? project.name}
684 pinned={isPinned}
685 small
686 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"
687 />
688 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar689 );
690 return (
691 <SidebarGroup
692 title="Projects"
693 action={
694 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
695 <Plus size={13} />
696 </Link>
697 }
698 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar699 {/* Only there once something is pinned: each row below has its own pin. */}
700 {pinned.length > 0 && (
701 <>
702 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar703 <ul aria-label="Pinned projects" className="space-y-px">
704 {pinned.map((project, index) => (
705 <li
706 key={project.name}
707 draggable={pinned.length > 1}
708 onDragStart={(event) => {
709 setDragging(index);
710 event.dataTransfer.effectAllowed = "move";
711 }}
712 onDragOver={(event) => {
713 if (dragging != null) event.preventDefault();
714 }}
715 onDrop={(event) => {
716 event.preventDefault();
717 if (dragging != null && dragging !== index) move(dragging, index - dragging);
718 setDragging(null);
719 }}
720 onDragEnd={() => setDragging(null)}
721 onKeyDown={(event) => {
722 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
723 event.preventDefault();
724 move(index, event.key === "ArrowUp" ? -1 : 1);
725 }}
726 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
727 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar728 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar729 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'730 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
731 <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">
732 <GripVertical size={12} aria-hidden="true" />
733 </span>
734 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar735 )}
736 </li>
737 ))}
738 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar739 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar740 )}
741 {recent.length > 0 && (
742 <>
743 <SidebarSubhead>Recent</SidebarSubhead>
744 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar745 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar746 ))}
747 </>
748 )}
749 <div className="pt-1">
Merge branch 'worktree-agent-ad7c6d88d93adc817'750 <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 sidebar751 All projects
752 </SidebarLink>
753 </div>
754 </SidebarGroup>
755 );
756}
757
758/**
Merge branch 'worktree-agent-ad7c6d88d93adc817'759 * The main list, in two parts a rule apart. Above it, what is yours
760 * whichever workspace you are in: Mission control, your inbox, and what
761 * others have shared with you. Below it, under the workspace's name, the
762 * workspace: its overview, its projects, what it builds and runs with
763 * across them, then its people, usage, support and settings. One row is
764 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look765 */
766function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
767 const ws = shell.workspace;
Merge branch 'worktree-agent-ad7c6d88d93adc817'768 const { pathname } = useLocation();
769 const going = useNavigation().location?.pathname;
770 // The row lights as the link is followed, not once the page arrives.
771 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look772 // A visitor browses: no workspace, no projects of their own.
773 if (!user) {
774 return (
775 <nav aria-label="g1t" className={PANEL}>
776 <div className="mt-3 space-y-px">
777 {VISITOR_LINKS.map((link) => (
778 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
779 {link.label}
780 </SidebarLink>
781 ))}
782 </div>
783 </nav>
784 );
785 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'786 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 return (
788 <nav aria-label="g1t" className={PANEL}>
Merge branch 'worktree-agent-ad7c6d88d93adc817'789 {/* 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 look790 <div className="mt-3 space-y-px">
Merge branch 'worktree-agent-ad7c6d88d93adc817'791 <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 look792 Mission control
793 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'794 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
795 Inbox
796 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look797 </div>
Merge branch 'worktree-agent-ad7c6d88d93adc817'798 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look799 <SidebarGroup title="Shared with you" className="mt-3">
Merge branch 'worktree-agent-ad7c6d88d93adc817'800 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look801 <SidebarLink
802 key={`${repo.namespace}/${repo.name}`}
803 to={`/${repo.namespace}/${repo.name}`}
804 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
805 drill="hover"
806 >
807 <span className="font-mono text-faint">{repo.namespace}/</span>
808 {repo.name}
809 </SidebarLink>
810 ))}
811 </SidebarGroup>
812 )}
813
Merge branch 'worktree-agent-ad7c6d88d93adc817'814 <Rule />
815 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look816 <>
Merge branch 'worktree-agent-ad7c6d88d93adc817'817 {/* The workspace the switcher names: its own page first. */}
818 <SidebarGroup title={displayName(ws)}>
819 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
820 Overview
821 </SidebarLink>
822 </SidebarGroup>
823 <div className="mt-3">
824 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
825 </div>
826
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member827 <Rule />
Merge branch 'worktree-agent-ad7c6d88d93adc817'828 <div className="space-y-px">
829 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />} current={here === "agents"}>
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers830 Agents
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look831 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'832 <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 look833 Context
834 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'835 <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 look836 Memory
837 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'838 <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 look839 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents840 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'841 <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 member842 Packages
843 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'844 {INSIGHTS && (
845 <SidebarSoonLink
846 to={`/${ws.slug}/-/insights`}
847 icon={<TrendingUp size={15} />}
848 about={INSIGHTS.summary}
849 current={here === "insights"}
850 >
851 {INSIGHTS.title}
852 </SidebarSoonLink>
853 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar854 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look855 <SidebarSoonLink
856 key={item.key}
857 to={`/${ws.slug}/-/soon/${item.key}`}
858 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
859 about={item.summary}
860 >
861 {item.title === "Board" ? "Boards" : item.title}
862 </SidebarSoonLink>
863 ))}
Merge branch 'worktree-agent-ad7c6d88d93adc817'864 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look865
866 <Rule />
867 <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 bar868 {/* Who belongs, for every member to see; owners invite and manage there. */}
Merge branch 'worktree-agent-ad7c6d88d93adc817'869 <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 sidebar870 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar871 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'872 <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 bar873 Teams
874 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'875 <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 look876 Usage
877 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'878 <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 look879 Support
880 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar881 {/* 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 look882 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member883 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 look884 icon={<Settings size={15} />}
885 drill
Merge branch 'worktree-agent-ad7c6d88d93adc817'886 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look887 >
888 Settings
889 </SidebarLink>
890 </div>
891 </>
Merge branch 'worktree-agent-ad7c6d88d93adc817'892 ) : (
893 <SidebarGroup title="Projects">
894 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
895 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look896 )}
897 </nav>
898 );
899}
900
Merge branch 'worktree-agent-ad7c6d88d93adc817'901/** Insights, coming: the workspace's own page says what it will be. */
902const INSIGHTS = roadmapItem("insights");
903
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look904/** A workspace's settings, drilled into from Settings in the main list. */
905function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
906 return (
907 <nav aria-label="Workspace settings" className={PANEL}>
908 <BackRow to={`/${slug}`} label="Settings" context={slug} />
909 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings910 {owner && (
911 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
912 General
913 </SidebarLink>
914 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look915 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
916 Repositories
917 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments918 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
919 Access tokens
920 </SidebarLink>
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules921 {owner && (
922 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
923 Personal access tokens
924 </SidebarLink>
925 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge926 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />}>
927 Rules
928 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look929 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar930 <SidebarGroup title="Agents and runs" className="mt-3">
931 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
932 Guardrails
933 </SidebarLink>
934 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
935 Secrets and variables
936 </SidebarLink>
Merge branch 'worktree-agent-a3abfcce648e87dca'937 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
938 Actions
939 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it940 {owner && (
941 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
942 Runners
943 </SidebarLink>
944 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar945 </SidebarGroup>
946 <SidebarGroup title="Connections" className="mt-3">
947 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
948 Integrations
949 </SidebarLink>
950 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
951 Webhooks
952 </SidebarLink>
953 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look954 <Rule />
955 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings956 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments957 Billing and plans
A catalogue of model providers, and settings that feel like settings958 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API959 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
960 Audit log
961 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look962 </div>
A catalogue of model providers, and settings that feel like settings963 </nav>
964 );
965}
966
Sidebar: no settings flash on the way into a repository, and a better slide967/**
968 * What the repository menu needs. Known from the address before the page's
969 * data arrives, so the menu is right from the first frame; the counts fill
970 * in once it does.
971 */
972type MenuRepo = {
973 namespace: string;
974 name: string;
975 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look976 /** Whether they see its settings; from the repository's page once it loads. */
977 settings?: boolean;
978 /** What they may do there, once the repository's page loads. */
979 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide980 issues?: number;
981 pulls?: number;
982};
983
984function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
985 return (
986 a != null &&
987 b != null &&
988 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
989 a.name.toLowerCase() === b.name.toLowerCase()
990 );
991}
A repository has its own sidebar, as settings do992
993/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look994 * A project's own list, drilled into while you are in it: everything about
995 * the project, running and its code, and nothing else, with the way back
996 * out to all of them.
A repository has its own sidebar, as settings do997 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put998function RepoMenu({
999 repo,
1000 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1001 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1002}: {
1003 repo: MenuRepo;
1004 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1005 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
1006 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1007}) {
A repository has its own sidebar, as settings do1008 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 put1009 // 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 look1010 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do1011 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1012 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
1013 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'1014 <Hint label="Overview" side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1015 <div>
A flat project menu with Code first, views as tabs, and Stripe for every owner1016 <NavLink
A repository has its own sidebar, as settings do1017 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner1018 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents1019 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner1020 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1021 `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 owner1022 }
A repository has its own sidebar, as settings do1023 >
1024 <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 page1025 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do1026 </span>
1027 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
1028 <span className="text-faint">{repo.namespace}/</span>
1029 <span className="font-semibold text-fg">{repo.name}</span>
1030 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner1031 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1032 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'1033 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1034 {/* A project's pages, in the order people use them. A page with more
1035 than one view shows them as tabs across its top. */}
1036 <Rule />
1037 <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 request1038 <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 owner1039 Code
1040 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1041 <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 owner1042 Issues
1043 </SidebarLink>
1044 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
1045 Pull requests
1046 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1047 <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 owner1048 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1049 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner1050 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
1051 Workflows
Projects: what a workspace builds and runs, first on every page1052 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1053 </div>
1054 <Rule />
1055 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1056 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner1057 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
1058 Deployments
1059 </SidebarLink>
1060 ) : null}
Observability gets its own page; tabs never overflow1061 <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.">
1062 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner1063 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1064 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1065 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
1066 Security
1067 </SidebarLink>
1068 ) : null}
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971069 <SidebarLink
1070 to={`${base}/contributors`}
1071 also={[`${base}/activity`, `${base}/stargazers`, ...soonPaths(base, "Insights")]}
1072 icon={<BarChart3 size={15} />}
1073 >
A flat project menu with Code first, views as tabs, and Stripe for every owner1074 Insights
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971075 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1076 </div>
1077 {(repo.settings ?? shows.has("settings")) && (
1078 <>
1079 <Rule />
1080 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1081 Settings
1082 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1083 </>
1084 )}
A repository has its own sidebar, as settings do1085 </nav>
1086 );
1087}
1088
Project settings move into the sidebar, grouped, with a heading per page1089/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1090 * A project's settings, drilled into from its list: what is about running
1091 * it, then its agents, then its code and who can reach it. The way back
1092 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1093 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1094function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1095 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1096 // Each page shows to the roles that can use it, once the role is known.
1097 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1098 const running = shows("deployments");
1099 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1100 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1101 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1102 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1103 {shows("") && (
1104 <div className="mt-2 space-y-px">
1105 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1106 General
1107 </SidebarLink>
1108 </div>
1109 )}
1110 {running && (
1111 <>
1112 {shows("") && <Rule />}
1113 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1114 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1115 Deployments
1116 </SidebarLink>
1117 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1118 Domains
1119 </SidebarLink>
1120 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1121 Dependencies
1122 </SidebarLink>
1123 </div>
1124 </>
1125 )}
1126 {agents && (
1127 <>
1128 <Rule />
1129 <div className="space-y-px">
1130 {shows("agents") && (
1131 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1132 Agents
1133 </SidebarLink>
1134 )}
1135 {shows("guardrails") && (
1136 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1137 Guardrails
1138 </SidebarLink>
1139 )}
1140 </div>
1141 </>
1142 )}
1143 <Rule />
1144 <div className="space-y-px">
1145 {shows("repository") && (
1146 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1147 Repository
1148 </SidebarLink>
1149 )}
1150 {shows("access") && (
1151 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1152 Access
1153 </SidebarLink>
1154 )}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1155 {shows("keys") && (
1156 <SidebarLink to={`${base}/settings/keys`} icon={<KeyRound size={15} />}>
1157 Deploy keys
1158 </SidebarLink>
1159 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1160 {shows("branches") && (
1161 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1162 Branches and merging
1163 </SidebarLink>
1164 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1165 {shows("rules") && (
1166 <SidebarLink to={`${base}/settings/rules`} icon={<Scale size={15} />}>
1167 Rules
1168 </SidebarLink>
1169 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1170 {shows("secrets") && (
1171 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1172 Secrets and variables
1173 </SidebarLink>
1174 )}
Merge branch 'worktree-agent-a3abfcce648e87dca'1175 {shows("actions") && (
1176 <SidebarLink to={`${base}/settings/actions`} icon={<PlayCircle size={15} />}>
1177 Actions
1178 </SidebarLink>
1179 )}
1180 {shows("environments") && (
1181 <SidebarLink to={`${base}/settings/environments`} icon={<Layers size={15} />}>
1182 Environments
1183 </SidebarLink>
1184 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1185 {shows("runners") && (
1186 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1187 Runners
1188 </SidebarLink>
1189 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1190 {shows("webhooks") && (
1191 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1192 Webhooks
1193 </SidebarLink>
1194 )}
1195 </div>
Project settings move into the sidebar, grouped, with a heading per page1196 </nav>
1197 );
1198}
1199
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)1200/**
1201 * Whether the menus list Settings → Invites (invites to g1t): always while
1202 * sign-up takes an invite; once anyone can sign up, only for someone with
1203 * invites already made to look back on, which the account menu's data says.
1204 */
1205function useInvitesListed(): boolean {
1206 const inviteOnly = useInviteOnly();
1207 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
1208 useEffect(() => {
1209 if (!inviteOnly && details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
1210 // eslint-disable-next-line react-hooks/exhaustive-deps
1211 }, [inviteOnly]);
1212 return inviteOnly || details.data?.invites_page === true;
1213}
1214
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1215/** Your own settings, drilled into from Your settings: one page each. */
1216function AccountSettingsMenu({ username }: { username: string }) {
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)1217 const invites = useInvitesListed();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1218 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1219 <SidebarLink to={`/settings/${page}`} icon={icon}>
1220 {ACCOUNT_SETTINGS[page].title}
1221 </SidebarLink>
Account settings in the same shape as a workspace's1222 );
1223 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1224 <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 quotas1225 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1226 <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 quotas1227 {link("profile", <CircleUserRound size={15} />)}
1228 {link("emails", <Mail size={15} />)}
Merge two kinds of invite, kept apart: an invite to g1t (Settings, invite-only only, no workspace unless asked) and an invitation to a workspace (its People page)1229 {invites && link("invites", <Ticket size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1230 </div>
1231 <Rule />
1232 <div className="space-y-px">
1233 {link("keys", <Fingerprint size={15} />)}
1234 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1235 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1236 <Rule />
1237 <div className="space-y-px">
Integrations are a directory of connectors, for a workspace and for each person1238 {link("integrations", <Blocks size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1239 {link("github", <GithubMark className="size-[15px]" />)}
1240 {link("applications", <Plug size={15} />)}
1241 </div>
1242 <Rule />
1243 <div className="space-y-px">
Merge main (membership, two-factor, GitHub repo roles) into tokens1244 {link("two-factor", <ShieldCheck size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1245 {link("security-log", <History size={15} />)}
1246 </div>
Account settings in the same shape as a workspace's1247 </nav>
1248 );
1249}
1250
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1251/**
1252 * Signing in and signing up, in place of the account for a visitor. Signing
1253 * in brings them back to the page they are on.
1254 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1255/** Help, status and the fine print under a visitor's sign-in buttons. */
1256function VisitorLinks() {
1257 const status = useSiteStatus();
Merge branch 'worktree-agent-ad7c6d88d93adc817'1258 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 quotas1259 const dot = (
1260 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1261 ·
1262 </span>
1263 );
1264 return (
1265 // Two short rows, so a narrow sidebar never starts a line with a dot.
1266 <nav aria-label="About g1t" className="pt-0.5">
1267 <div className="flex items-center gap-0.5">
1268 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1269 <StatusDot state={status?.overall.state ?? null} />
1270 Status
1271 </a>
1272 {dot}
1273 <Link to="/support" className={quiet}>
1274 Support
1275 </Link>
1276 </div>
1277 <div className="flex items-center gap-0.5">
1278 {LEGAL_LINKS.map(([label, to], index) => (
1279 <span key={to} className="flex items-center gap-0.5">
1280 {index > 0 && dot}
1281 <Link to={to} className={quiet}>
1282 {label}
1283 </Link>
1284 </span>
1285 ))}
1286 </div>
1287 </nav>
1288 );
1289}
1290
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1291function VisitorPanel() {
1292 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1293 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1294 return (
1295 <div className="space-y-2">
1296 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1297 <div className="grid grid-cols-2 gap-2">
1298 <Link
1299 to={withNext("/login", pathname + search)}
1300 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"
1301 >
1302 <LogIn size={14} />
1303 Sign in
1304 </Link>
1305 <Link
1306 to={withNext("/register", pathname + search)}
1307 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1308 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1309 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1310 </Link>
1311 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1312 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1313 </div>
1314 );
1315}
1316
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1317function Sidebar({
1318 user,
1319 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1320 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1321 onFind,
1322 onClose,
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1323 rail = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1324}: {
1325 user: User | null;
1326 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1327 /** The page is a 404: the address names nothing the viewer can see. */
1328 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1329 onFind: () => void;
1330 /** In the sheet on a small screen: closing it, at the end of the top row. */
1331 onClose?: () => void;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1332 /**
1333 * Beside the rail, as Code's sidebar: code only, under a Code heading.
1334 * The workspace, its settings and the account are the rail's.
1335 */
1336 rail?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1337}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1338 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1339 const { pathname } = useLocation();
1340 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1341 // Where the sidebar is drilled to follows the page being gone to, so it
1342 // moves as the link is followed, not once the page arrives.
1343 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1344 // On a 404, the address is not a place: no list is drilled into from it,
1345 // except the project's own when the project is real and visible and only
1346 // something inside it is missing (a file, a commit, an issue).
1347 const lost = missing && going == null;
1348 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1349 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1350 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1351 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1352 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1353 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 look1354 // An invitation to a repository is answered before its menu means anything.
1355 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1356 repoPath != null &&
1357 !reserved.has(repoPath[1]) &&
1358 repoPath[2] !== "-" &&
1359 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1360 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1361 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1362 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1363 // The repository the menu is for: the one loaded if it is the one being
1364 // gone to, else what the address says, at once.
1365 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 look1366 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1367 ? sameRepo(active, targetRepo)
1368 ? active
1369 : {
1370 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1371 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1372 }
1373 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1374 // The viewer's role there, from the repository's page once it has loaded.
1375 const page = useRouteLoaderData("routes/repo/layout") as
1376 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1377 | undefined;
1378 const menuRepo: MenuRepo | null =
1379 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1380 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1381 : guessed;
1382
1383 // The way from the main list to the one shown.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1384 const trail: Level[] = [{ key: "main", node: rail && user && ws ? <CodeMenu shell={shell} slug={ws.slug} /> : <MainMenu user={user} shell={shell} /> }];
1385 if (rail && !menuRepo) {
1386 // Code's own list: nothing to drill into but a project.
1387 } else if (user && inAccount && !rail) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1388 trail.push({ key: "account", node: <AccountSettingsMenu username={user.username} /> });
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1389 } else if (ws && inSettings && !rail) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1390 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1391 } else if (menuRepo) {
1392 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1393 // Out of a project: to its workspace's projects when they are yours.
1394 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1395 const back = !user
1396 ? { to: "/explore", label: "Explore" }
1397 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1398 ? { to: `/${home.slug}/-/projects`, label: "All projects" }
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1399 : { to: "/", label: "Home" };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1400 trail.push({
1401 key,
1402 node: (
1403 <RepoMenu
1404 repo={menuRepo}
1405 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1406 back={back}
1407 />
1408 ),
1409 });
1410 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1411 }
1412
Agents as a team: lifecycle, merge queue, billing and a new shell1413 return (
1414 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1415 {/* The same height and rule as the top bar, so the two read as one line. */}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1416 {rail ? (
1417 <ModeHeader title="Code" onClose={onClose} />
1418 ) : (
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1419 <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 put1420 {user ? (
1421 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1422 {/* The 1 alone beside the workspace: a square hover the height of the
1423 switcher, the mark as tall as the workspace avatar. */}
1424 <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 bar1425 <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 put1426 </Link>
1427 <span className="shrink-0 text-line-strong" aria-hidden="true">
1428 /
1429 </span>
1430 <WorkspaceSwitcher user={user} shell={shell} />
1431 </>
1432 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1433 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1434 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1435 </Link>
1436 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1437 {onClose && (
1438 <button
1439 type="button"
1440 aria-label="Close menu"
1441 onClick={onClose}
1442 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1443 >
1444 <X size={16} />
1445 </button>
1446 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1447 </div>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1448 )}
A Vercel-like sidebar and a new mark1449 <div className="px-2 pt-3">
1450 <button
1451 type="button"
1452 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1453 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 mark1454 >
1455 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1456 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1457 <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 mark1458 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1459 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1460 <Drill trail={trail} />
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1461 {/* The account, or signing in, as one row at the very bottom; beside the rail, the rail has it. */}
1462 {!rail && (
1463 <div className="shrink-0 border-t border-line p-2">
1464 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
1465 </div>
1466 )}
1467 </div>
1468 );
1469}
1470
1471/** A mode's sidebar heading: the same height and rule as the top bar, with a way to close the phone's sheet. */
1472export function ModeHeader({ title, action, onClose }: { title: ReactNode; action?: ReactNode; onClose?: () => void }) {
1473 return (
1474 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2.5 pl-4">
1475 <h2 className="min-w-0 grow truncate text-[0.9375rem] font-semibold">{title}</h2>
1476 {action}
1477 {onClose && (
1478 <button
1479 type="button"
1480 aria-label="Close menu"
1481 onClick={onClose}
1482 className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1483 >
1484 <X size={16} />
1485 </button>
1486 )}
1487 </div>
1488 );
1489}
1490
1491/**
1492 * Code's sidebar (beside the rail): only code. Its Overview (Mission
1493 * control's code panels), the projects, what spans them, and what is
1494 * coming. The workspace's people, money and settings are Workspace's;
1495 * agents, context and memory are Agents'.
1496 */
1497function CodeMenu({ shell, slug }: { shell: ShellData; slug: string }) {
1498 const { pathname } = useLocation();
1499 const going = useNavigation().location?.pathname;
1500 const path = going ?? pathname;
1501 const at = (page: string) => path === `/${slug}/-/${page}` || path.startsWith(`/${slug}/-/${page}/`);
1502 const shared = shell.shared ?? [];
1503 return (
1504 <nav aria-label="Code" className={PANEL}>
1505 <div className="mt-3 space-y-px">
1506 <SidebarLink to={`/${slug}/-/overview`} icon={<LayoutDashboard size={15} />} current={at("overview")}>
1507 Overview
1508 </SidebarLink>
1509 </div>
1510 <div className="mt-3">
1511 <SidebarProjects slug={slug} shell={shell} current={at("projects")} />
1512 </div>
1513 <Rule />
1514 <div className="space-y-px">
1515 <SidebarLink to={`/${slug}/-/security`} icon={<ShieldCheck size={15} />} current={at("security") && !at("security/settings")}>
1516 Security
1517 </SidebarLink>
1518 <SidebarLink to={`/${slug}/-/packages`} icon={<Package size={15} />} current={at("packages")}>
1519 Packages
1520 </SidebarLink>
1521 {INSIGHTS && (
1522 <SidebarSoonLink to={`/${slug}/-/insights`} icon={<TrendingUp size={15} />} about={INSIGHTS.summary} current={at("insights")}>
1523 {INSIGHTS.title}
1524 </SidebarSoonLink>
1525 )}
1526 {roadmapIn("Workspace")
1527 .filter((item) => item.key !== "insights" && item.key !== "teams" && item.key !== "fleet")
1528 .map((item) => (
1529 <SidebarSoonLink key={item.key} to={`/${slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />} about={item.summary}>
1530 {item.title === "Board" ? "Boards" : item.title}
1531 </SidebarSoonLink>
1532 ))}
1533 </div>
1534 {shared.length > 0 && (
1535 <>
1536 <Rule />
1537 <SidebarGroup title="Shared with you">
1538 {shared.map((repo) => (
1539 <SidebarLink
1540 key={`${repo.namespace}/${repo.name}`}
1541 to={`/${repo.namespace}/${repo.name}`}
1542 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
1543 drill="hover"
1544 >
1545 <span className="font-mono text-faint">{repo.namespace}/</span>
1546 {repo.name}
1547 </SidebarLink>
1548 ))}
1549 </SidebarGroup>
1550 </>
1551 )}
1552 </nav>
1553 );
1554}
1555
1556/** Workspace pages that sit in its Settings list, drilled into from the Workspace sidebar. */
1557const WORKSPACE_SETTINGS = ["settings", "repositories", "tokens", "personal-access-tokens", "secrets", "actions", "runners", "webhooks", "emoji"];
1558
1559/**
1560 * The Workspace mode's sidebar: the workspace itself, for every member.
1561 * Its overview, people, money, connections, policies and record, then its
1562 * settings as a list of their own. Owner-only pages stay owner-only.
1563 */
1564export function WorkspaceSidebar({ slug, owner, onClose }: { slug: string; owner: boolean; onClose?: () => void }) {
1565 const { pathname } = useLocation();
1566 const going = useNavigation().location?.pathname;
1567 const path = going ?? pathname;
1568 const page = path.startsWith(`/${slug}/-/`) ? path.slice(`/${slug}/-/`.length) : "";
1569 const top = page.split("/")[0] ?? "";
1570 const at = (...pages: string[]) => pages.some((p) => page === p || page.startsWith(`${p}/`));
1571 const inSettings = WORKSPACE_SETTINGS.includes(top);
1572 const main = (
1573 <nav aria-label="Workspace" className={PANEL}>
1574 <div className="mt-3 space-y-px">
1575 <SidebarLink to={`/${slug}/-/workspace`} icon={<LayoutGrid size={15} />} current={at("workspace")}>
1576 Overview
1577 </SidebarLink>
1578 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />} current={at("people")}>
1579 People
1580 </SidebarLink>
1581 <SidebarLink to={`/${slug}/-/teams`} icon={<UsersRound size={15} />} current={at("teams")}>
1582 Teams
1583 </SidebarLink>
1584 </div>
1585 <SidebarGroup title="Usage and billing" className="mt-3">
1586 <SidebarLink to={`/${slug}/-/usage`} icon={<BarChart3 size={15} />} current={at("usage", "gateway")}>
1587 Usage
1588 </SidebarLink>
1589 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />} current={at("billing")}>
1590 Billing and plans
1591 </SidebarLink>
1592 </SidebarGroup>
1593 <SidebarGroup title="Connections" className="mt-3">
1594 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />} current={at("integrations")}>
1595 Integrations
1596 </SidebarLink>
1597 </SidebarGroup>
1598 <SidebarGroup title="Security policies" className="mt-3">
1599 <SidebarLink to={`/${slug}/-/security/settings`} icon={<ShieldCheck size={15} />} current={at("security/settings")}>
1600 Security settings
1601 </SidebarLink>
1602 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />} current={at("guardrails")}>
1603 Guardrails
1604 </SidebarLink>
1605 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />} current={at("rules")}>
1606 Rules
1607 </SidebarLink>
1608 </SidebarGroup>
1609 <Rule />
1610 <div className="space-y-px">
1611 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />} current={at("audit")}>
1612 Audit log
1613 </SidebarLink>
1614 <SidebarLink to={owner ? `/${slug}/-/settings` : `/${slug}/-/repositories`} icon={<Settings size={15} />} drill current={inSettings}>
1615 Settings
1616 </SidebarLink>
1617 </div>
1618 </nav>
1619 );
1620 const settings = (
1621 <nav aria-label="Workspace settings" className={PANEL}>
1622 <BackRow to={`/${slug}/-/workspace`} label="Settings" context={slug} />
1623 <div className="mt-2 space-y-px">
1624 {owner && (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1625 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />} end>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1626 General
1627 </SidebarLink>
1628 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1629 <SidebarLink to={`/${slug}/-/settings/chat`} icon={<MessagesSquare size={15} />}>
1630 Chat
1631 </SidebarLink>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1632 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
1633 Repositories
1634 </SidebarLink>
1635 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
1636 Access tokens
1637 </SidebarLink>
1638 {owner && (
1639 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
1640 Personal access tokens
1641 </SidebarLink>
1642 )}
1643 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
1644 Webhooks
1645 </SidebarLink>
1646 <SidebarLink to={`/${slug}/-/emoji`} icon={<Smile size={15} />}>
1647 Emoji
1648 </SidebarLink>
1649 </div>
1650 <SidebarGroup title="Runs" className="mt-3">
1651 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
1652 Secrets and variables
1653 </SidebarLink>
1654 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
1655 Actions
1656 </SidebarLink>
1657 {owner && (
1658 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
1659 Runners
1660 </SidebarLink>
1661 )}
1662 </SidebarGroup>
1663 </nav>
1664 );
1665 const trail: Level[] = [{ key: "workspace", node: main }];
1666 if (inSettings) trail.push({ key: "workspace:settings", node: settings });
1667 return (
1668 <div className="flex h-full flex-col">
1669 <ModeHeader title="Workspace" onClose={onClose} />
1670 <Drill trail={trail} />
1671 </div>
1672 );
1673}
1674
1675/** Your own settings, beside the rail: under the account menu, not any workspace's. */
1676export function AccountSidebar({ username, onClose }: { username: string; onClose?: () => void }) {
1677 return (
1678 <div className="flex h-full flex-col">
1679 <ModeHeader title="Your account" onClose={onClose} />
1680 <div className="min-h-0 grow">
1681 <AccountSettingsMenu username={username} />
A Vercel-like sidebar and a new mark1682 </div>
1683 </div>
1684 );
1685}
1686
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1687
A Vercel-like sidebar and a new mark1688/** Words for the sections a path can end in. */
1689const SECTIONS: Record<string, string> = {
1690 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1691 milestones: "Milestones",
1692 labels: "Labels",
A Vercel-like sidebar and a new mark1693 pulls: "Pull requests",
1694 queue: "Merge queue",
1695 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1696 plans: "Outcomes",
1697 actions: "Workflows",
1698 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1699 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1700 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1701 access: "Access",
1702 invitations: "Invitation",
1703 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1704 branches: "Branches",
1705 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1706 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1707 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1708 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1709 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1710 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1711 people: "People",
1712 projects: "Projects",
1713 teams: "Teams",
A Vercel-like sidebar and a new mark1714 tokens: "Access tokens",
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1715 "personal-access-tokens": "Personal access tokens",
A Vercel-like sidebar and a new mark1716 usage: "Usage",
Site: the AI Gateway page's breadcrumb reads "AI Gateway"1717 gateway: "AI Gateway",
Secrets and variables: one list, rows per environment, for workflows and deployments1718 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1719 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1720 webhooks: "Webhooks",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1721 emoji: "Emoji",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1722 domains: "Domains",
1723 guardrails: "Guardrails",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1724 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1725 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1726 tree: "Files",
1727 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1728 agents: "Agents",
1729 context: "Context",
1730 memory: "Memory",
1731 security: "Security",
1732 packages: "Packages",
1733 runners: "Runners",
Merge branch 'worktree-agent-a3abfcce648e87dca'1734 environments: "Environments",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1735 workflows: "Workflows",
1736 observability: "Observability",
1737 insights: "Insights",
1738 sessions: "Sessions",
Chat and workspace agents: channels, DMs and named agents you talk to1739 chat: "Chat",
1740 docs: "Docs",
1741 home: "Home",
1742 "code-access": "Code access",
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1743 overview: "Overview",
1744 workspace: "Workspace",
A Vercel-like sidebar and a new mark1745};
1746
Merge branch 'worktree-agent-a3abfcce648e87dca'1747/** Settings pages whose name differs from the section's of the same word. */
1748const SETTINGS_SECTIONS: Record<string, string> = { branches: "Branches and merging", actions: "Actions" };
1749
A Vercel-like sidebar and a new mark1750/** 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 quotas1751function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1752 // A 404 names nothing from the address, unless the project is real and
1753 // visible and only something inside it is missing.
1754 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1755 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1756 return <span className="text-sm font-medium">Not found</span>;
1757 }
A Vercel-like sidebar and a new mark1758 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1759 const reserved = ["settings", "explore", "new", "search", "workspaces", "policies", "security", "support", "status", "invite", "inbox"];
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1760 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 quotas1761 // Your settings: Settings / Emails.
1762 if (parts[0] === "settings") {
1763 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1764 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 quotas1765 }
A Vercel-like sidebar and a new mark1766 if (reserved.includes(parts[0]!)) {
1767 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1768 settings: "Account",
A Vercel-like sidebar and a new mark1769 explore: "Explore",
Projects live: fixes from walking them end to end1770 new: "New project",
A Vercel-like sidebar and a new mark1771 search: "Search",
1772 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1773 policies: "Policies",
1774 security: "Security",
1775 support: "Support",
1776 status: "Status",
1777 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1778 inbox: "Inbox",
A Vercel-like sidebar and a new mark1779 };
1780 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1781 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1782 // A person's profile, by their handle.
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1783 if (parts[0] === "u" && parts[1]) return <ProfileCrumb username={parts[1]} />;
A Vercel-like sidebar and a new mark1784 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1785 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1786 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1787 const page = `/${owner}/-/${third}`;
1788 if (third && SETTINGS_PAGE.test(page)) {
1789 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1790 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1791 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1792 } else if (second) {
1793 const repo = `/${owner}/${second}`;
1794 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1795 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1796 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 mark1797 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 deployments1798 else if (third && SECTIONS[third]) {
1799 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1800 // A settings page names which one: Settings / Secrets and variables.
1801 if (third === "settings" && fourth && SECTIONS[fourth]) {
Merge branch 'worktree-agent-a3abfcce648e87dca'1802 trail.push({ label: SETTINGS_SECTIONS[fourth] ?? SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1803 }
1804 }
A Vercel-like sidebar and a new mark1805 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1806 return <Trail trail={trail} />;
1807}
1808
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1809/** A profile's place in the top bar: the person's avatar and @handle, from the page once it has loaded. */
1810function ProfileCrumb({ username }: { username: string }) {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1811 const page = useRouteLoaderData("routes/user") as { profile?: { username: string; displayUsername?: string | null; avatar: string | null } } | undefined;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1812 const profile = page?.profile?.username.toLowerCase() === username.toLowerCase() ? page.profile : null;
1813 return (
1814 <Link to={`/u/${profile?.username ?? username}`} aria-current="page" className="flex min-w-0 items-center gap-2 rounded px-1 py-0.5 transition-colors hover:bg-raised">
1815 <Avatar name={profile?.username ?? username} image={profile?.avatar ?? null} size={20} />
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1816 <span className="truncate font-mono text-[0.8125rem] font-medium">@{profile ? shownUsername(profile) : username}</span>
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1817 </Link>
1818 );
1819}
1820
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1821/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1822type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1823
1824/**
1825 * The trail of links. On a phone there is room for one: the page itself,
1826 * with a way back to where it sits; the whole trail from a wider screen up.
1827 */
1828function Trail({ trail }: { trail: Crumb[] }) {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1829 // Up on a phone, unless up is the workspace itself: its pages are the tabs.
1830 const up = trail.length > 1 && !/^\/[^/]+$/.test(trail[trail.length - 2]!.to) ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1831 return (
1832 <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 audit1833 {up && (
1834 <Link
1835 to={up.to}
1836 prefetch="intent"
1837 aria-label={`Back to ${up.label}`}
1838 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"
1839 >
1840 <ChevronLeft size={16} />
1841 </Link>
1842 )}
1843 {trail.map((crumb, index) => {
1844 const last = index === trail.length - 1;
1845 return (
1846 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1847 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1848 <Link
1849 to={crumb.to}
1850 prefetch="intent"
1851 aria-current={last ? "page" : undefined}
1852 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1853 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1854 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1855 >
1856 {last && crumb.short ? (
1857 <>
1858 <span className="sm:hidden">{crumb.short}</span>
1859 <span className="hidden sm:inline">{crumb.label}</span>
1860 </>
1861 ) : (
1862 crumb.label
1863 )}
1864 </Link>
1865 </span>
1866 );
1867 })}
A Vercel-like sidebar and a new mark1868 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1869 );
1870}
1871
Search across all of g1t, Explore, and a command palette1872type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1873
1874/** 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 look1875function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1876 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1877 const commands: Command[] = [
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1878 { label: "Home", to: "/", icon: <House size={15} /> },
1879 ...(shell.workspace
1880 ? [
1881 { label: "Chat", to: `/${shell.workspace.slug}/-/chat`, icon: <MessagesSquare size={15} /> },
1882 { label: "Agents", to: `/${shell.workspace.slug}/-/agents`, icon: <Sparkles size={15} /> },
1883 ...(hasCodeAccess(shell.workspace) ? [{ label: "Code overview", to: `/${shell.workspace.slug}/-/overview`, icon: <Code2 size={15} /> }] : []),
1884 { label: "Workspace", hint: "People, billing, policies, settings", to: `/${shell.workspace.slug}/-/workspace`, icon: <Building2 size={15} /> },
1885 ]
1886 : []),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1887 ...(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-agent1888 ? [{ 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 step1889 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1890 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1891 { 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 all1892 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1893 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1894 { 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 quotas1895 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1896 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1897 .filter((page) => page !== "profile")
1898 .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 shell1899 ];
1900 const repo = shell.repo;
1901 if (repo) {
1902 const base = `/${repo.namespace}/${repo.name}`;
1903 const name = `${repo.namespace}/${repo.name}`;
1904 commands.unshift(
Projects: what a workspace builds and runs, first on every page1905 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1906 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1907 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1908 { 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 step1909 ...(repo.member
1910 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1911 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1912 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1913 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1914 ...(repo.member
1915 ? [
1916 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1917 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1918 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1919 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1920 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1921 ]
1922 : []),
1923 );
1924 }
1925 for (const membership of user.workspaces ?? []) {
1926 commands.push(
Workspace names and icons, and a component kit for every control1927 { 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 sidebar1928 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1929 { 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 request1930 { 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 tokens1931 { 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 page1932 { 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 settings1933 { 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 look1934 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1935 );
1936 }
1937 for (const listed of shell.repos) {
1938 commands.push({
Projects: what a workspace builds and runs, first on every page1939 label: listed.title ?? listed.name,
1940 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1941 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1942 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1943 });
1944 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1945 // "Put an agent on …": one per project, after the projects themselves.
1946 for (const listed of shell.repos) {
1947 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1948 commands.push({
1949 label: `Put an agent on ${listed.title ?? listed.name}`,
1950 hint: `${listed.namespace}/${listed.name}`,
1951 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1952 icon: <Sparkles size={15} />,
1953 });
1954 }
Agents as a team: lifecycle, merge queue, billing and a new shell1955 return commands;
1956}
1957
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1958/** 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 look1959function 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 put1960 const commands: Command[] = [];
1961 const repo = shell.repo;
1962 if (repo) {
1963 const base = `/${repo.namespace}/${repo.name}`;
1964 const name = `${repo.namespace}/${repo.name}`;
1965 commands.push(
1966 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1967 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1968 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1969 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1970 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1971 );
1972 }
1973 commands.push(
1974 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1975 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1976 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1977 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1978 { 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 look1979 { 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 put1980 );
1981 return commands;
1982}
1983
Agents as a team: lifecycle, merge queue, billing and a new shell1984/**
Merge branch 'worktree-agent-aca4790ea223aa235'1985 * The page while the next one loads: after a moment it dims, under the
1986 * bar below, so it is plainly on its way out rather than frozen, and the
1987 * next page fades up from there. Sending a form does not dim it: the
1988 * form's button says it is working. Spread on the element holding the page.
1989 */
1990export function useLeaving() {
1991 const navigation = useNavigation();
1992 const method = navigation.formMethod?.toUpperCase();
1993 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1994 return {
1995 "aria-busy": leaving || undefined,
1996 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1997 };
1998}
1999
2000/**
Agents as a team: lifecycle, merge queue, billing and a new shell2001 * A bar across the top of the page while the next one loads, as GitHub
2002 * has: it appears at once, creeps towards the end while waiting, then fills
2003 * and fades when the page arrives.
2004 */
2005export function Progress() {
2006 const navigation = useNavigation();
2007 const busy = navigation.state !== "idle";
2008 const [width, setWidth] = useState(0);
2009 const [visible, setVisible] = useState(false);
2010 useEffect(() => {
2011 if (busy) {
2012 setVisible(true);
2013 setWidth(12);
2014 // Slows as it goes, never quite arriving until the page does.
2015 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
2016 return () => clearInterval(timer);
2017 }
2018 setWidth((now) => (now > 0 ? 100 : 0));
2019 const fade = setTimeout(() => setVisible(false), 250);
2020 const reset = setTimeout(() => setWidth(0), 550);
2021 return () => {
2022 clearTimeout(fade);
2023 clearTimeout(reset);
2024 };
2025 }, [busy]);
2026 return (
2027 <div
2028 aria-hidden="true"
2029 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
2030 visible ? "opacity-100" : "opacity-0"
2031 }`}
2032 >
2033 <div
2034 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
2035 style={{ width: `${width}%` }}
2036 />
2037 </div>
2038 );
2039}
2040
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2041/** The top bar's way to g1t: the direct message with the workspace's orchestrator, opened or made. */
2042function AskG1tButton({ slug }: { slug: string }) {
2043 return (
2044 <Hint label="Ask g1t">
2045 <Link
2046 to={`/${slug}/-/chat?agent=g1t`}
2047 aria-label="Ask g1t"
2048 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"
2049 >
2050 <G1tMark size={18} />
2051 <span className="hidden sm:inline">Ask g1t</span>
2052 </Link>
2053 </Hint>
2054 );
2055}
2056
Chat and workspace agents: channels, DMs and named agents you talk to2057/** The mode's sidebar beside the rail: a shade lighter than the page. */
2058const SIDEBAR_BOX = "h-full border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]";
2059
Agents as a team: lifecycle, merge queue, billing and a new shell2060/**
Docs: a workspace knowledge base people and agents write together2061 * Which sidebar sits beside the rail: each mode's own, or none (the Inbox,
2062 * which is a page of its own, and g1t's public pages, such as a profile,
2063 * which are no workspace's and carry their own left column). Without a
2064 * workspace (a visitor), the one sidebar there always was.
Chat and workspace agents: channels, DMs and named agents you talk to2065 */
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2066function sidebarFor(mode: ModeKey | null): Panel | null {
Chat and workspace agents: channels, DMs and named agents you talk to2067 if (mode == null) return "code";
Docs: a workspace knowledge base people and agents write together2068 if (mode === "inbox" || mode === "site") return null;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2069 return mode;
Chat and workspace agents: channels, DMs and named agents you talk to2070}
2071
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2072/** The sidebars that sit beside the rail, one per mode that has one. */
Docs: a workspace knowledge base people and agents write together2073type Panel = "home" | "chat" | "docs" | "agents" | "code" | "workspace" | "account";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2074
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2075/** Each sidebar's name, for the phone's button that opens it. */
2076const MODE_MENU: Record<Panel, string> = {
2077 home: "Home",
2078 chat: "Chat",
2079 docs: "Docs",
2080 agents: "Agents",
2081 code: "Code",
2082 workspace: "Workspace",
2083 account: "Account",
2084};
2085
Chat and workspace agents: channels, DMs and named agents you talk to2086/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2087 * The app: a sidebar with the workspace, its repositories and the sections
2088 * of the one being looked at; a slim bar with search and the account; and
2089 * the page. A visitor who is not signed in gets the same frame, with
2090 * Explore and Search in place of the workspace, and signing in in place of
2091 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell2092 */
2093export function AppShell({
2094 user,
2095 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2096 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell2097 banner,
2098 children,
2099}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2100 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell2101 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2102 /** The page is a 404 (see Sidebar). */
2103 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell2104 banner?: ReactNode;
2105 children: ReactNode;
2106}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2107 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell2108 const [drawer, setDrawer] = useState(false);
2109 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2110 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2111 const signUpLabel = useSignUpCopy().primary;
2112 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell2113
2114 // A new page closes the drawer on small screens.
2115 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2116 // Mission control's code, fetched while the browser is idle, so going
2117 // home never waits on it (routes/home.tsx).
2118 useEffect(() => {
2119 if (!user) return;
2120 const load = () => void import("./mission-control").catch(() => undefined);
2121 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
2122 if (idle) idle(load);
2123 else setTimeout(load, 1500);
2124 }, [user]);
Search across all of g1t, Explore, and a command palette2125 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'2126 const leaving = useLeaving();
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2127 useVisualViewport();
Agents as a team: lifecycle, merge queue, billing and a new shell2128
Chat and workspace agents: channels, DMs and named agents you talk to2129 // The rail and the mode's sidebar (docs/WORKSPACE.md, "Shell"), for
2130 // someone in a workspace. A visitor, or someone with none, keeps the one
2131 // sidebar.
2132 const going = useNavigation().location?.pathname;
2133 const ws = user ? shell.workspace : null;
2134 const mode: ModeKey | null = ws ? modeOf(going ?? pathname, ws.slug) : null;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2135 const panel = ws ? sidebarFor(mode) : "code";
2136 const code = ws ? hasCodeAccess(ws) : true;
Chat and workspace agents: channels, DMs and named agents you talk to2137 const { sidebar: chatSidebar } = useChatSidebar();
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2138 // Notify: the feed socket's live counts (lib/notify-client.ts) once it has them; the page's until then.
2139 const live = useLiveBadges(ws?.slug);
2140 const chatUnread = chatSidebar
2141 ? unreadTotals(chatSidebar.entries)
2142 : live?.chat != null
2143 ? { unread: live.chat, mentions: live.mentions ?? 0 }
2144 : (shell.chat ?? { unread: 0, mentions: 0 });
2145 const inboxUnread = live?.inbox ?? shell.inbox?.unread ?? 0;
2146 const sidebarNode = (inSheet: boolean) => {
2147 const close = inSheet ? () => setDrawer(false) : undefined;
2148 const find = () => {
2149 if (inSheet) setDrawer(false);
2150 setPalette(true);
2151 };
2152 if (!ws || !user || panel === "code") {
2153 return <Sidebar user={user} shell={shell} missing={missing} onFind={find} onClose={close} rail={Boolean(ws && user)} />;
2154 }
2155 switch (panel) {
2156 case "home":
2157 return <HomeSidebar slug={ws.slug} shell={shell} code={code} onFind={find} header={<ModeHeader title="Home" onClose={close} />} />;
2158 case "chat":
2159 return <ChatSidebar slug={ws.slug} />;
Docs: a workspace knowledge base people and agents write together2160 case "docs":
2161 return <DocsSidebar slug={ws.slug} onClose={close} />;
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2162 case "agents":
2163 return <AgentsSidebar slug={ws.slug} shellAgents={shell.agents ?? null} code={code} owner={ws.role === "owner"} />;
2164 case "workspace":
2165 return <WorkspaceSidebar slug={ws.slug} owner={ws.role === "owner"} onClose={close} />;
2166 case "account":
2167 return <AccountSidebar username={user.username} onClose={close} />;
2168 default:
2169 return null;
2170 }
2171 };
Chat and workspace agents: channels, DMs and named agents you talk to2172 const rail =
2173 user && ws ? (
2174 <Rail
2175 user={user}
2176 workspace={ws}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2177 unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }}
2178 help={<HelpMenu />}
Chat and workspace agents: channels, DMs and named agents you talk to2179 account={<AccountMenu user={user} rail />}
2180 />
2181 ) : null;
2182 const pad = rail ? (panel ? "lg:pl-[21rem]" : "lg:pl-20") : "lg:pl-64";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2183 // On a phone (below 768px): the tab bar, and a conversation full screen.
2184 const conversation = isConversation(going ?? pathname);
2185 const tabs = Boolean(user && ws) && !conversation;
Chat and workspace agents: channels, DMs and named agents you talk to2186
Agents as a team: lifecycle, merge queue, billing and a new shell2187 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2188 // The phone's menu is a sheet: a dialog that holds focus, closes on
2189 // Escape or a tap outside, and gives focus back to the menu button.
2190 <Sheet open={drawer} onOpenChange={setDrawer}>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2191 <div className="min-h-dvh">
Agents as a team: lifecycle, merge queue, billing and a new shell2192 <Progress />
Chat and workspace agents: channels, DMs and named agents you talk to2193 <aside className="fixed inset-y-0 left-0 z-40 hidden lg:flex">
2194 {rail}
2195 {panel && <div className={`${SIDEBAR_BOX} w-64`}>{sidebarNode(false)}</div>}
Agents as a team: lifecycle, merge queue, billing and a new shell2196 </aside>
Chat and workspace agents: channels, DMs and named agents you talk to2197 <SheetContent
2198 side="left"
2199 showClose={false}
2200 aria-describedby={undefined}
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2201 // Focus would land on the workspace's avatar and open its hint over
2202 // the sidebar's heading; the sheet itself takes it instead.
2203 onOpenAutoFocus={(event) => {
2204 event.preventDefault();
2205 (event.currentTarget as HTMLElement | null)?.focus();
2206 }}
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2207 // A phone has the tab bar for the modes: the sheet is the mode's
2208 // sidebar alone, a little wider, its rows tall enough for a thumb.
2209 className={`lg:hidden ${rail ? `flex flex-row ${panel ? "w-[21rem] max-w-[92vw] sm:max-w-[21rem] max-md:w-[min(20rem,86vw)]" : "w-20 sm:max-w-20"}` : "w-72 max-w-[85vw] sm:max-w-72"}`}
Chat and workspace agents: channels, DMs and named agents you talk to2210 >
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2211 <SheetTitle className="sr-only">{ws && panel ? `${MODE_MENU[panel]} menu` : "Menu"}</SheetTitle>
2212 {rail && <div className={`flex h-full shrink-0 ${panel ? "max-md:hidden" : ""}`}>{rail}</div>}
2213 {panel && <div className={`min-w-0 grow ${rail ? SIDEBAR_BOX : ""} max-md:[&_nav_a]:min-h-10`}>{sidebarNode(true)}</div>}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2214 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell2215
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2216 <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 to2217 <header
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2218 className={`sticky top-0 z-30 flex h-14 items-center gap-3 max-md:gap-1.5 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 to2219 >
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2220 {/* A phone: the workspace's avatar opens everything else (components/mobile.tsx). */}
2221 {user && ws && <AvatarSheetButton user={user} workspace={ws} />}
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2222 {/* The mode's own menu (its sidebar); on a phone, beside the avatar, wherever the mode has one. */}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2223 <SheetTrigger
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2224 aria-label={ws && panel ? `Open the ${MODE_MENU[panel]} menu` : "Open menu"}
2225 className={`flex shrink-0 items-center justify-center rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg max-md:-ml-1 max-md:size-10 lg:hidden ${user && ws && !panel ? "max-md:hidden" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell2226 >
2227 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2228 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2229 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2230 <div className="ml-auto flex items-center gap-1.5">
2231 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
2232 <button
2233 type="button"
2234 aria-label="Search or jump to"
2235 onClick={() => setPalette(true)}
2236 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
2237 >
2238 <Search size={16} />
2239 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2240 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
2241 <NavLink
2242 to="/explore"
2243 prefetch="intent"
2244 aria-label="Explore"
2245 className={({ isActive }) =>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2246 `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 sidebar2247 }
2248 >
2249 <Compass size={16} className="sm:hidden" />
2250 <span className="hidden sm:inline">Explore</span>
2251 </NavLink>
A Vercel-like sidebar and a new mark2252 <a
2253 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals2254 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 mark2255 >
2256 Docs
2257 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2258 {user && (
2259 <>
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2260 {shell.workspace ? <AskG1tButton slug={shell.workspace.slug} /> : <AgentButton />}
2261 {/* On a phone the Inbox is a tab. */}
2262 <span className={shell.workspace ? "max-md:hidden" : undefined}>
2263 <InboxBell counts={shell.inbox ? { ...shell.inbox, unread: inboxUnread } : null} />
2264 </span>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2265 </>
2266 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2267 {!user ? (
2268 // The sidebar has these too, but on a phone it is folded away.
2269 <Link
2270 to={withNext("/login", here)}
2271 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"
2272 >
2273 Sign in
2274 </Link>
2275 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2276 <>
2277 {/* Room between what comes in (the inbox) and what you make. */}
2278 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell2279 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2280 <Tooltip>
2281 <TooltipTrigger asChild>
2282 <DropdownMenuTrigger
2283 aria-label="Create new"
2284 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"
2285 >
2286 <Plus size={16} />
2287 <ChevronDown size={13} className="text-muted" />
2288 </DropdownMenuTrigger>
2289 </TooltipTrigger>
2290 <TooltipContent>Create new…</TooltipContent>
2291 </Tooltip>
2292 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell2293 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2294 <>
2295 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
2296 <DropdownMenuItem asChild>
2297 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
2298 <CircleDot />
2299 New issue
2300 </Link>
2301 </DropdownMenuItem>
2302 {shell.repo.member && (
2303 <DropdownMenuItem asChild>
2304 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
2305 <ListTree />
2306 Plan work
2307 </Link>
2308 </DropdownMenuItem>
2309 )}
2310 <DropdownMenuSeparator />
2311 </>
Agents as a team: lifecycle, merge queue, billing and a new shell2312 )}
2313 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2314 <Link to="/?agent=new">
2315 <Sparkles />
2316 Put an agent on it
2317 </Link>
2318 </DropdownMenuItem>
2319 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2320 <Link to="/new">
Projects: what a workspace builds and runs, first on every page2321 <Box />
2322 New project
Agents as a team: lifecycle, merge queue, billing and a new shell2323 </Link>
2324 </DropdownMenuItem>
2325 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2326 <Link to="/new/github">
2327 <GitBranch />
2328 Import from GitHub
2329 </Link>
2330 </DropdownMenuItem>
2331 <DropdownMenuSeparator />
2332 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2333 <Link to="/workspaces/new">
2334 <Users />
2335 New workspace
2336 </Link>
2337 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'2338 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
2339 {shell.workspace &&
2340 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
2341 <DropdownMenuItem asChild>
2342 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
2343 <UsersRound />
2344 New team
2345 </Link>
2346 </DropdownMenuItem>
2347 ) : (
2348 <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 bar2349 <UsersRound />
2350 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'2351 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
2352 </DropdownMenuItem>
2353 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell2354 </DropdownMenuContent>
2355 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2356 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2357 )}
Agents as a team: lifecycle, merge queue, billing and a new shell2358 </div>
2359 </header>
2360 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2361 <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 in2362 <InMain.Provider value={true}>{children}</InMain.Provider>
Merge branch 'worktree-agent-aca4790ea223aa235'2363 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell2364 </div>
Phones reach every page: each mode's menu from the top bar or by tapping the tab you're on, Docs first in the avatar sheet, a project's pages as a swipeable strip; dialogs as bottom sheets above the keyboard, safe areas, menus kept on screen, 40px targets, touch-visible controls, save bars above the tab bar2365 {user && ws && (
2366 <MobileTabBar
2367 workspace={ws}
2368 unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }}
2369 onReselect={panel ? () => setDrawer(true) : undefined}
2370 />
2371 )}
Search across all of g1t, Explore, and a command palette2372 <CommandPalette
2373 open={palette}
2374 onOpenChange={setPalette}
2375 commands={commands}
2376 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
2377 />
Agents as a team: lifecycle, merge queue, billing and a new shell2378 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2379 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell2380 );
2381}

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