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

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)1import { Activity, BarChart3, Building2, MessagesSquare, Bell, BookMarked, BookOpen, Bot, Box, Brain, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, CircleDot, GripVertical, CircleUserRound, Code2, Compass, CreditCard, Fingerprint, GanttChart, Gauge, GitBranch, GitPullRequest, Globe, History, House, Inbox, KanbanSquare, KeyRound, Layers, LayoutDashboard, LayoutGrid, LifeBuoy, ListTree, Lock, LogIn, LogOut, Mail, Menu, Network, Package, PlayCircle, Plug, Plus, Rocket, Search, ServerCog, Settings, ShieldCheck, Scale, Smile, Sparkles, Ticket, TrendingUp, UserRoundKey, Users, UsersRound, Webhook, X } from "lucide-react";
Agents as a team: lifecycle, merge queue, billing and a new shell2import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar3import { Link, NavLink, useFetcher, useLocation, useNavigation, useRouteLoaderData, useSubmit } from "react-router";
Agents as a team: lifecycle, merge queue, billing and a new shell4
One 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";
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)31import { HelpMenu, Rail } from "./rail";
32import { HomeSidebar } from "./home-sidebar";
33import { G1tMark } from "./orchestrator";
34import { AvatarSheetButton, MobileTabBar, isConversation, useVisualViewport } from "./mobile";
Chat and workspace agents: channels, DMs and named agents you talk to35import { useChatSidebar } from "./chat/actions";
36import { unreadTotals } from "../lib/chat";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look37import { SETTINGS_CAPABILITY, type ViewerAccess, seesSettings } from "../lib/access";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put38import { VISITOR_LINKS, projectPages } from "../lib/chrome";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas39import { ACCOUNT_SETTINGS, type AccountSettingsPage, FIRST_SETTINGS_PAGE, accountSettingsPage } from "../lib/account-settings";
40import { GithubMark } from "./github";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put41import { withNext } from "../lib/next";
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)42import { useInviteOnly, useSignUpCopy } from "../lib/registration";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas43import { STATUS_URL, statusTitle } from "../lib/status";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar44import { type ShortcutProject, movedPin, recentWith } from "../lib/pins";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas45import type { AccountMenuData } from "../routes/settings-menu-json";
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)46import { 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 managers47import { OwnPresenceDot, OwnPresenceItems, StatusDialog } from "./presence";
Agents as a team: lifecycle, merge queue, billing and a new shell48
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put49/**
50 * What the sidebar needs, worked out by the root loader. For a visitor who
51 * is not signed in there is no workspace, no projects and no usage: only
52 * the project being looked at, if they can see it.
53 */
Agents as a team: lifecycle, merge queue, billing and a new shell54export type ShellData = {
55 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control56 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar57 /** All its projects, by name, for the palette: `name` is the slug in their address. */
58 repos: ShortcutProject[];
59 /** The person's pinned projects in it, in their order (lib/pins.ts). */
60 pinned?: ShortcutProject[];
61 /** What they opened there last, latest first, leaving out the pinned. */
62 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63 /** Repositories shared with them in workspaces they do not belong to. */
64 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page65 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell66 repo: {
67 namespace: string;
68 name: string;
69 member: boolean;
70 issues: number;
71 pulls: number;
72 } | null;
Billing accounts, terms and enterprises; g1t is no longer free73 /** Where the workspace stands against its usage limit, if billing is on. */
74 limit: {
75 exposureMicros: number;
76 ceilingMicros: number | null;
77 state: "ok" | "warning" | "stopped";
78 comped: boolean;
79 } | null;
Free while g1t is being built out; agents can check out their own forks80 /** Whether g1t charges nothing for now, while it is being built out. */
81 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request82 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy83 /** This month's usage: charged, or at cost while g1t is free. */
84 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
86 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 all87 /** What is unread in their inbox, for the bell. Absent for a visitor. */
88 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)89 /**
90 * Chat, for the rail's badge and Home's sidebar: what is unread, and the
91 * starred and latest conversations. Null when chat did not answer in time.
92 */
93 chat?: { unread: number; mentions: number; starred?: ChatSidebarEntry[]; recent?: ChatSidebarEntry[] } | null;
94 /** The workspace's agents, g1t first, for Home's and Agents' sidebars; null when not known. */
95 agents?: ShellAgent[] | null;
Agents as a team: lifecycle, merge queue, billing and a new shell96};
97
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)98/** An agent as the shell lists it. */
99export type ShellAgent = Pick<
100 WorkspaceAgent,
101 "id" | "handle" | "display_name" | "avatar" | "avatar_seed" | "role" | "status" | "title" | "team" | "department" | "builtin"
102>;
103
Agents as a team: lifecycle, merge queue, billing and a new shell104function SidebarLink({
105 to,
106 icon,
107 end,
108 count,
109 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look110 drill,
Merge branch 'worktree-agent-ad7c6d88d93adc817'111 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell112 children,
113}: {
114 to: string;
115 icon: ReactNode;
116 end?: boolean;
117 count?: number;
Projects: what a workspace builds and runs, first on every page118 /** Other path prefixes under which this link is the current one. */
119 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look120 /** It opens a list of its own: a chevron says so, always or on hover. */
121 drill?: boolean | "hover";
Merge branch 'worktree-agent-ad7c6d88d93adc817'122 /** Whether it is the current row, when the list works that out itself. */
123 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell124 children: ReactNode;
125}) {
126 const { pathname } = useLocation();
127 return (
128 <NavLink
129 to={to}
130 end={end}
131 prefetch="intent"
132 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page133 const current =
Merge branch 'worktree-agent-ad7c6d88d93adc817'134 lit ??
135 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark136 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 shell137 current
138 ? "bg-raised font-medium text-fg"
139 : isPending
140 ? "bg-raised/60 text-fg"
141 : "text-muted hover:bg-raised/60 hover:text-fg"
142 }`;
143 }}
144 >
145 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
146 <span className="min-w-0 grow truncate">{children}</span>
147 {count != null && count > 0 && (
148 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
149 {count}
150 </span>
151 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look152 {drill && (
153 <ChevronRight
154 size={14}
155 aria-hidden="true"
156 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
157 />
158 )}
Agents as a team: lifecycle, merge queue, billing and a new shell159 </NavLink>
160 );
161}
162
Pull requests say where they are live; the project menu shows what is coming163/**
164 * Something a project will have, shown where it will live so people can
165 * see where g1t is going. Not a link: nothing is behind it yet.
166 */
167function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
168 return (
Merge branch 'worktree-agent-ae1299e92e4462012'169 <Hint label={about} side="right">
170 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
171 <span className="shrink-0 opacity-70">{icon}</span>
172 <span className="grow truncate">{children}</span>
173 <span className="sr-only">{about}</span>
174 <SoonPill />
175 </div>
176 </Hint>
Pull requests say where they are live; the project menu shows what is coming177 );
178}
179
The project menu opens by section, and the workspace has its own panel180/**
A flat project menu with Code first, views as tabs, and Stripe for every owner181 * A page that is coming, as a link to its roadmap page: faint, with Soon,
182 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel183 */
A flat project menu with Code first, views as tabs, and Stripe for every owner184function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel185 to,
186 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner187 icon,
188 about,
Merge branch 'worktree-agent-ad7c6d88d93adc817'189 current: lit,
The project menu opens by section, and the workspace has its own panel190 children,
191}: {
192 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner193 also?: string[];
194 icon: ReactNode;
195 about: string;
Merge branch 'worktree-agent-ad7c6d88d93adc817'196 /** Whether it is the current row, when the list works that out itself. */
197 current?: boolean;
The project menu opens by section, and the workspace has its own panel198 children: ReactNode;
199}) {
200 const { pathname } = useLocation();
201 return (
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero202 // The hint wraps a plain box: Radix's asChild merges className as a
203 // string, which would break NavLink's className function.
Merge branch 'worktree-agent-ae1299e92e4462012'204 <Hint label={about} side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero205 <div>
The project menu opens by section, and the workspace has its own panel206 <NavLink
207 to={to}
208 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner209 className={({ isActive }) => {
Merge branch 'worktree-agent-ad7c6d88d93adc817'210 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 owner211 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
212 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 panel213 }`;
214 }}
215 >
A flat project menu with Code first, views as tabs, and Stripe for every owner216 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel217 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit218 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales219 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero220 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'221 </Hint>
The project menu opens by section, and the workspace has its own panel222 );
223}
224
A flat project menu with Code first, views as tabs, and Stripe for every owner225/** Icons for what spans projects. */
226const WORKSPACE_ICONS: Record<string, ReactNode> = {
227 board: <KanbanSquare size={15} />,
228 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member229 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner230 packages: <Package size={15} />,
231 fleet: <Bot size={15} />,
232};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales233
A flat project menu with Code first, views as tabs, and Stripe for every owner234/** The Soon pages of a section, as paths, so its link is current on them. */
235function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
236 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales237}
238
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look239function SidebarGroup({
240 title,
241 action,
242 className = "",
243 children,
244}: {
245 title: string;
246 action?: ReactNode;
247 className?: string;
248 children: ReactNode;
249}) {
Agents as a team: lifecycle, merge queue, billing and a new shell250 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look251 <section className={className}>
A Vercel-like sidebar and a new mark252 <div className="mb-1 flex h-6 items-center justify-between px-2">
253 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell254 {action}
255 </div>
256 <div className="space-y-px">{children}</div>
257 </section>
258 );
259}
260
Workspace names and icons, and a component kit for every control261/** What a workspace is called where people read it: its display name, else its slug. */
262function displayName(membership: Membership): string {
263 return membership.name?.trim() || membership.slug;
264}
265
Agents as a team: lifecycle, merge queue, billing and a new shell266function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control267 const workspace = shell.workspace;
268 const current = workspace?.slug;
269 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell270 return (
271 <DropdownMenu>
Workspace names and icons, and a component kit for every control272 {/* The name goes to the workspace; only the arrows switch it. The
273 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'274 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark275 <Link
276 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents277 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look278 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 mark279 >
Workspace names and icons, and a component kit for every control280 {workspace ? (
281 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell282 ) : (
Workspace names and icons, and a component kit for every control283 <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 shell284 )}
Workspace names and icons, and a component kit for every control285 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark286 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'287 </Hint>
A Vercel-like sidebar and a new mark288 <DropdownMenuTrigger
289 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit290 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 mark291 >
292 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell293 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control294 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar295 {workspace && (
296 <>
297 {/* The workspace's own page: its projects, packages and people. */}
298 <DropdownMenuItem asChild>
299 <Link to={`/${workspace.slug}`}>
300 <LayoutGrid />
301 Workspace overview
302 </Link>
303 </DropdownMenuItem>
304 <DropdownMenuItem asChild>
305 <Link to={`/${workspace.slug}/-/projects`}>
306 <Box />
307 All projects
308 </Link>
309 </DropdownMenuItem>
310 <DropdownMenuSeparator />
311 </>
312 )}
Agents as a team: lifecycle, merge queue, billing and a new shell313 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
314 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'315 <Hint key={membership.slug} label={displayName(membership)} side="right">
316 <DropdownMenuItem asChild>
317 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control318 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
319 <span className="flex min-w-0 grow flex-col leading-tight">
320 <span className="truncate">{displayName(membership)}</span>
321 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
322 </span>
323 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
324 {membership.slug === current ? (
325 <Check className="shrink-0 text-accent" />
326 ) : (
327 <span className="size-4 shrink-0" aria-hidden="true" />
328 )}
Agents as a team: lifecycle, merge queue, billing and a new shell329 </Link>
330 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'331 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell332 ))}
333 <DropdownMenuSeparator />
334 <DropdownMenuItem asChild>
335 <Link to="/workspaces/new">
336 <Plus />
337 New workspace
338 </Link>
339 </DropdownMenuItem>
340 </DropdownMenuContent>
341 </DropdownMenu>
A Vercel-like sidebar and a new mark342 );
343}
344
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas345/**
346 * The fine print, as one quiet row at the foot of the account menu and the
347 * visitor's panel: each link a full-height target, left-aligned. In the
348 * menu each is a menu item, so the arrow keys reach it.
349 */
350const LEGAL_LINKS: [string, string][] = [
351 ["Policies", "/policies"],
352 ["Privacy", "/policies/privacy"],
353 ["Security", "/security"],
354];
355const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
356
357function MenuLegalRow() {
358 return (
359 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
360 {LEGAL_LINKS.map(([label, to], index) => (
361 <span key={to} className="flex items-center gap-0.5">
362 {index > 0 && (
363 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
364 ·
365 </span>
366 )}
367 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
368 <Link to={to}>{label}</Link>
369 </DropdownMenuItem>
370 </span>
371 ))}
372 </div>
373 );
374}
375
376/** Status's row: the live dot and two or three words, fetched when the menu opens. */
377function StatusSummary({ open }: { open: boolean }) {
378 const status = useSiteStatus(open);
379 const words = statusTitle(status);
380 return (
381 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
382 {/* The words say it; the dot's own words for screen readers would repeat them. */}
383 <span aria-hidden="true" className="flex">
384 <StatusDot state={status?.overall.state ?? null} />
385 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents386 {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 quotas387 </span>
388 );
389}
390
391/** A menu row: an icon, words, and whatever sits at its end. */
392const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
393
Chat and workspace agents: channels, DMs and named agents you talk to394function AccountMenu({ user, rail = false }: { user: User; rail?: boolean }) {
A Vercel-like sidebar and a new mark395 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas396 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 managers397 // Your status's dialog: outside the menu, so the menu closes behind it.
398 const [editing, setEditing] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents399 // Name, primary address and invites left: asked for once, as soon as the
400 // 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 quotas401 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents402 const prefetch = () => {
403 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
404 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas405 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents406 if (open) prefetch();
407 // eslint-disable-next-line react-hooks/exhaustive-deps
408 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas409 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents410 // Still on its way: shapes where the words will be, the same size.
411 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)412 // While anyone can sign up, listed once the data says there are invites to look back on.
413 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 quotas414 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark415 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 managers416 <>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas417 <DropdownMenu open={open} onOpenChange={setOpen}>
Chat and workspace agents: channels, DMs and named agents you talk to418 {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 managers419 // 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 to420 <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 managers421 aria-label={`Account menu for ${shownUsername(user)}`}
Chat and workspace agents: channels, DMs and named agents you talk to422 onPointerEnter={prefetch}
423 onFocus={prefetch}
424 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"
425 >
426 <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 managers427 <OwnPresenceDot ring="#0b0b0d" className="absolute -right-0.5 -bottom-0.5" />
Chat and workspace agents: channels, DMs and named agents you talk to428 </DropdownMenuTrigger>
429 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas430 <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 managers431 aria-label={`Account menu for ${shownUsername(user)}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents432 onPointerEnter={prefetch}
433 onFocus={prefetch}
Merge branch 'worktree-agent-ad7c6d88d93adc817'434 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 quotas435 >
Workspace names and icons, and a component kit for every control436 <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 managers437 <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 quotas438 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark439 </DropdownMenuTrigger>
Chat and workspace agents: channels, DMs and named agents you talk to440 )}
441 <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 quotas442 {/* Who is signed in, and a way to their profile. */}
443 <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 managers444 <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 quotas445 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents446 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
447 {loading ? (
448 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
449 ) : (
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers450 <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 incidents451 )}
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers452 <span className="truncate font-mono text-xs text-muted">@{shownUsername(user)}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents453 {loading ? (
454 <Skeleton className="mt-1 h-3 w-40" />
455 ) : (
456 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
457 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas458 </span>
A Vercel-like sidebar and a new mark459 </Link>
460 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas461 <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 managers462 {/* Your status, away, and pausing notifications. */}
463 <DropdownMenuGroup>
464 <OwnPresenceItems className="min-h-9 py-1.5 text-[0.8125rem]" onEdit={() => setEditing(true)} />
465 </DropdownMenuGroup>
466 <DropdownMenuSeparator className="my-1.5" />
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas467 <DropdownMenuGroup>
468 <DropdownMenuItem asChild className={MENU_ROW}>
469 <Link to={profile}>
470 <CircleUserRound />
471 Your profile
472 </Link>
473 </DropdownMenuItem>
474 <DropdownMenuItem asChild className={MENU_ROW}>
475 <Link to={FIRST_SETTINGS_PAGE}>
476 <Settings />
477 Your settings
478 </Link>
479 </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)480 {invitesListed && (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas481 <DropdownMenuItem asChild className={MENU_ROW}>
482 <Link to="/settings/invites">
483 <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)484 {ACCOUNT_SETTINGS.invites.title}
Fast pages, required checks on the branch, self-hosted runners, honest incidents485 {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 quotas486 {me?.invites_left != null && (
487 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
488 {me.invites_left} left
489 </span>
490 )}
491 </Link>
492 </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)493 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas494 </DropdownMenuGroup>
495 <DropdownMenuSeparator className="my-1.5" />
496 <DropdownMenuGroup>
497 <DropdownMenuItem asChild className={MENU_ROW}>
498 <a href="https://docs.g1t.sh/">
499 <BookOpen />
500 Documentation
501 </a>
502 </DropdownMenuItem>
503 <DropdownMenuItem asChild className={MENU_ROW}>
504 <Link to="/support">
505 <LifeBuoy />
506 Support
507 </Link>
508 </DropdownMenuItem>
509 <DropdownMenuItem asChild className={MENU_ROW}>
510 <a href={STATUS_URL}>
511 <Activity />
512 Status
513 <StatusSummary open={open} />
514 </a>
515 </DropdownMenuItem>
516 </DropdownMenuGroup>
517 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark518 {/* Submitted from here: the menu closes on select, and a button
519 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 quotas520 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark521 <LogOut />
522 Sign out
523 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas524 <DropdownMenuSeparator className="my-1.5" />
525 <MenuLegalRow />
A Vercel-like sidebar and a new mark526 </DropdownMenuContent>
527 </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 managers528 <StatusDialog open={editing} onOpenChange={setEditing} />
529 </>
A Vercel-like sidebar and a new mark530 );
531}
532
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look533/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar534 * A workspace's settings pages, which the sidebar drills into: how it is
535 * set up and connected (guardrails, secrets, runners, integrations,
536 * webhooks), what it pays, its repositories, tokens and record. The main
537 * list keeps the places work happens and who belongs; every member can
538 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look539 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar540const SETTINGS_PAGE =
Merge branch 'worktree-agent-ad7c6d88d93adc817'541 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look542/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page543const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings544
Polish: phones, copy boxes, the plan page, the landing page, a real glide545/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look546 * The sidebar's lists are a stack, the way a phone pushes a screen: one
547 * drilled into slides in from the right over the full width, while the one
548 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide549 * ease-out, the fade quicker than the move so the two never blur together.
550 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide551 */
Sidebar: no settings flash on the way into a repository, and a better slide552const LAYER =
Sidebar: the panels really slide553 "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 look554/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide555const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings556
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look557/** One list in the stack: what it is, and its rows. */
558type Level = { key: string; node: ReactNode };
559
560/** True once the page has hydrated, so what the server drew never animates in. */
561function useHydrated(): boolean {
562 const [hydrated, setHydrated] = useState(false);
563 useEffect(() => setHydrated(true), []);
564 return hydrated;
565}
566
567/**
568 * Drilling in and out of the sidebar's lists. `trail` is the way from the
569 * main list to the one shown, worked out from the address, so a link
570 * straight to a settings page opens the sidebar already drilled in, and
571 * leaving it comes back out. Lists deeper than the one shown stay in
572 * place, off to the right, so going back slides them away rather than
573 * dropping them. Moving to a list at the same depth (a workspace's settings
574 * to a project) fades the new one in where it is.
575 */
576function Drill({ trail }: { trail: Level[] }) {
577 const depth = trail.length - 1;
578 const hydrated = useHydrated();
579 const stack = useRef<Level[]>(trail);
580 const before = stack.current;
581 const within = trail.every((level, index) => before[index]?.key === level.key);
582 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
583 const levels = stack.current;
584 // What was showing last time, to tell drilling in from moving across.
585 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
586 useEffect(() => {
587 shown.current = { depth, keys: levels.map((level) => level.key) };
588 });
A catalogue of model providers, and settings that feel like settings589 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look590 <div className="relative min-h-0 grow overflow-hidden">
591 {levels.map((level, index) => {
592 const current = index === depth;
593 const place =
594 index < depth
595 ? "pointer-events-none -translate-x-1/4 opacity-0"
596 : current
597 ? "translate-x-0 opacity-100"
598 : "pointer-events-none translate-x-full opacity-0";
599 // A list that was not there before arrives: from the right when
600 // drilling in, in place when moving across.
601 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
602 const enter = arriving
603 ? depth > shown.current.depth
604 ? "starting:translate-x-full starting:opacity-0"
605 : "starting:opacity-0"
606 : "";
607 return (
608 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
609 {level.node}
610 </div>
611 );
612 })}
613 </div>
614 );
615}
616
617/**
618 * The row at the top of a list drilled into: the list's name, which goes
619 * back out, and what it belongs to, faint at the end.
620 */
621function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
622 return (
623 <Link
624 to={to}
625 prefetch="intent"
626 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 settings627 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look628 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
629 <span className="min-w-0 shrink-0 truncate">{label}</span>
630 {context && (
631 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
632 )}
633 </Link>
634 );
635}
636
637/** A thin rule between groups of a list. */
638function Rule() {
639 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
640}
641
Navigation: the workspace's own page with tabs, pins and recent in the sidebar642/** A quiet heading inside a group: Pinned, Recent. */
643function SidebarSubhead({ children }: { children: ReactNode }) {
644 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
645}
646
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look647/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar648 * The workspace's projects as the sidebar keeps them, however many there
649 * are: the person's pins in their order, what they opened last, and the
650 * way to all of them. Pins move by dragging, or with Alt and the arrow
651 * keys; the projects service keeps the order.
652 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'653function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar654 const reorder = useFetcher({ key: `pins:${slug}` });
655 const saved = shell.pinned ?? [];
656 // While a new order is on its way, it shows as made.
657 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
658 const pinned = asked
659 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
660 : saved;
661 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
662 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
663 const [dragging, setDragging] = useState<number | null>(null);
664 const move = (from: number, by: number) => {
665 const next = movedPin(pinned, from, by);
666 if (!next) return;
667 const form = new FormData();
668 form.set("intent", "reorder");
669 for (const project of next) form.append("slug", project.name);
670 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
671 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar672 // Each row pins or unpins in place: the pin shows on hover or focus, and
673 // stays shown on a pinned row's hover so it reads as "unpin".
674 const row = (project: ShortcutProject, isPinned: boolean) => (
675 <div className="group/row relative">
676 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
677 <span className="block truncate pr-6">{project.title ?? project.name}</span>
678 </SidebarLink>
679 <PinButton
680 workspace={slug}
681 slug={project.name}
682 name={project.title ?? project.name}
683 pinned={isPinned}
684 small
685 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"
686 />
687 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar688 );
689 return (
690 <SidebarGroup
691 title="Projects"
692 action={
693 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
694 <Plus size={13} />
695 </Link>
696 }
697 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar698 {/* Only there once something is pinned: each row below has its own pin. */}
699 {pinned.length > 0 && (
700 <>
701 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar702 <ul aria-label="Pinned projects" className="space-y-px">
703 {pinned.map((project, index) => (
704 <li
705 key={project.name}
706 draggable={pinned.length > 1}
707 onDragStart={(event) => {
708 setDragging(index);
709 event.dataTransfer.effectAllowed = "move";
710 }}
711 onDragOver={(event) => {
712 if (dragging != null) event.preventDefault();
713 }}
714 onDrop={(event) => {
715 event.preventDefault();
716 if (dragging != null && dragging !== index) move(dragging, index - dragging);
717 setDragging(null);
718 }}
719 onDragEnd={() => setDragging(null)}
720 onKeyDown={(event) => {
721 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
722 event.preventDefault();
723 move(index, event.key === "ArrowUp" ? -1 : 1);
724 }}
725 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
726 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar727 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar728 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'729 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
730 <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">
731 <GripVertical size={12} aria-hidden="true" />
732 </span>
733 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar734 )}
735 </li>
736 ))}
737 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar738 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar739 )}
740 {recent.length > 0 && (
741 <>
742 <SidebarSubhead>Recent</SidebarSubhead>
743 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar744 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar745 ))}
746 </>
747 )}
748 <div className="pt-1">
Merge branch 'worktree-agent-ad7c6d88d93adc817'749 <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 sidebar750 All projects
751 </SidebarLink>
752 </div>
753 </SidebarGroup>
754 );
755}
756
757/**
Merge branch 'worktree-agent-ad7c6d88d93adc817'758 * The main list, in two parts a rule apart. Above it, what is yours
759 * whichever workspace you are in: Mission control, your inbox, and what
760 * others have shared with you. Below it, under the workspace's name, the
761 * workspace: its overview, its projects, what it builds and runs with
762 * across them, then its people, usage, support and settings. One row is
763 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look764 */
765function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
766 const ws = shell.workspace;
Merge branch 'worktree-agent-ad7c6d88d93adc817'767 const { pathname } = useLocation();
768 const going = useNavigation().location?.pathname;
769 // The row lights as the link is followed, not once the page arrives.
770 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look771 // A visitor browses: no workspace, no projects of their own.
772 if (!user) {
773 return (
774 <nav aria-label="g1t" className={PANEL}>
775 <div className="mt-3 space-y-px">
776 {VISITOR_LINKS.map((link) => (
777 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
778 {link.label}
779 </SidebarLink>
780 ))}
781 </div>
782 </nav>
783 );
784 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'785 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look786 return (
787 <nav aria-label="g1t" className={PANEL}>
Merge branch 'worktree-agent-ad7c6d88d93adc817'788 {/* 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 look789 <div className="mt-3 space-y-px">
Merge branch 'worktree-agent-ad7c6d88d93adc817'790 <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 look791 Mission control
792 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'793 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
794 Inbox
795 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look796 </div>
Merge branch 'worktree-agent-ad7c6d88d93adc817'797 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look798 <SidebarGroup title="Shared with you" className="mt-3">
Merge branch 'worktree-agent-ad7c6d88d93adc817'799 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look800 <SidebarLink
801 key={`${repo.namespace}/${repo.name}`}
802 to={`/${repo.namespace}/${repo.name}`}
803 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
804 drill="hover"
805 >
806 <span className="font-mono text-faint">{repo.namespace}/</span>
807 {repo.name}
808 </SidebarLink>
809 ))}
810 </SidebarGroup>
811 )}
812
Merge branch 'worktree-agent-ad7c6d88d93adc817'813 <Rule />
814 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look815 <>
Merge branch 'worktree-agent-ad7c6d88d93adc817'816 {/* The workspace the switcher names: its own page first. */}
817 <SidebarGroup title={displayName(ws)}>
818 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
819 Overview
820 </SidebarLink>
821 </SidebarGroup>
822 <div className="mt-3">
823 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
824 </div>
825
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member826 <Rule />
Merge branch 'worktree-agent-ad7c6d88d93adc817'827 <div className="space-y-px">
828 <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 managers829 Agents
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look830 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'831 <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 look832 Context
833 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'834 <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 look835 Memory
836 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'837 <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 look838 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents839 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'840 <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 member841 Packages
842 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'843 {INSIGHTS && (
844 <SidebarSoonLink
845 to={`/${ws.slug}/-/insights`}
846 icon={<TrendingUp size={15} />}
847 about={INSIGHTS.summary}
848 current={here === "insights"}
849 >
850 {INSIGHTS.title}
851 </SidebarSoonLink>
852 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar853 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look854 <SidebarSoonLink
855 key={item.key}
856 to={`/${ws.slug}/-/soon/${item.key}`}
857 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
858 about={item.summary}
859 >
860 {item.title === "Board" ? "Boards" : item.title}
861 </SidebarSoonLink>
862 ))}
Merge branch 'worktree-agent-ad7c6d88d93adc817'863 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look864
865 <Rule />
866 <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 bar867 {/* Who belongs, for every member to see; owners invite and manage there. */}
Merge branch 'worktree-agent-ad7c6d88d93adc817'868 <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 sidebar869 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar870 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'871 <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 bar872 Teams
873 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'874 <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 look875 Usage
876 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'877 <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 look878 Support
879 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar880 {/* 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 look881 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member882 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 look883 icon={<Settings size={15} />}
884 drill
Merge branch 'worktree-agent-ad7c6d88d93adc817'885 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look886 >
887 Settings
888 </SidebarLink>
889 </div>
890 </>
Merge branch 'worktree-agent-ad7c6d88d93adc817'891 ) : (
892 <SidebarGroup title="Projects">
893 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
894 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look895 )}
896 </nav>
897 );
898}
899
Merge branch 'worktree-agent-ad7c6d88d93adc817'900/** Insights, coming: the workspace's own page says what it will be. */
901const INSIGHTS = roadmapItem("insights");
902
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look903/** A workspace's settings, drilled into from Settings in the main list. */
904function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
905 return (
906 <nav aria-label="Workspace settings" className={PANEL}>
907 <BackRow to={`/${slug}`} label="Settings" context={slug} />
908 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings909 {owner && (
910 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
911 General
912 </SidebarLink>
913 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look914 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
915 Repositories
916 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments917 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
918 Access tokens
919 </SidebarLink>
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules920 {owner && (
921 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
922 Personal access tokens
923 </SidebarLink>
924 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge925 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />}>
926 Rules
927 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look928 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar929 <SidebarGroup title="Agents and runs" className="mt-3">
930 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
931 Guardrails
932 </SidebarLink>
933 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
934 Secrets and variables
935 </SidebarLink>
Merge branch 'worktree-agent-a3abfcce648e87dca'936 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
937 Actions
938 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it939 {owner && (
940 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
941 Runners
942 </SidebarLink>
943 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar944 </SidebarGroup>
945 <SidebarGroup title="Connections" className="mt-3">
946 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
947 Integrations
948 </SidebarLink>
949 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
950 Webhooks
951 </SidebarLink>
952 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look953 <Rule />
954 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings955 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments956 Billing and plans
A catalogue of model providers, and settings that feel like settings957 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API958 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
959 Audit log
960 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look961 </div>
A catalogue of model providers, and settings that feel like settings962 </nav>
963 );
964}
965
Sidebar: no settings flash on the way into a repository, and a better slide966/**
967 * What the repository menu needs. Known from the address before the page's
968 * data arrives, so the menu is right from the first frame; the counts fill
969 * in once it does.
970 */
971type MenuRepo = {
972 namespace: string;
973 name: string;
974 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look975 /** Whether they see its settings; from the repository's page once it loads. */
976 settings?: boolean;
977 /** What they may do there, once the repository's page loads. */
978 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide979 issues?: number;
980 pulls?: number;
981};
982
983function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
984 return (
985 a != null &&
986 b != null &&
987 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
988 a.name.toLowerCase() === b.name.toLowerCase()
989 );
990}
A repository has its own sidebar, as settings do991
992/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look993 * A project's own list, drilled into while you are in it: everything about
994 * the project, running and its code, and nothing else, with the way back
995 * out to all of them.
A repository has its own sidebar, as settings do996 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put997function RepoMenu({
998 repo,
999 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1000 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1001}: {
1002 repo: MenuRepo;
1003 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1004 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
1005 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1006}) {
A repository has its own sidebar, as settings do1007 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 put1008 // 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 look1009 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do1010 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1011 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
1012 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'1013 <Hint label="Overview" side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1014 <div>
A flat project menu with Code first, views as tabs, and Stripe for every owner1015 <NavLink
A repository has its own sidebar, as settings do1016 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner1017 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents1018 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner1019 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1020 `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 owner1021 }
A repository has its own sidebar, as settings do1022 >
1023 <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 page1024 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do1025 </span>
1026 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
1027 <span className="text-faint">{repo.namespace}/</span>
1028 <span className="font-semibold text-fg">{repo.name}</span>
1029 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner1030 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1031 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'1032 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1033 {/* A project's pages, in the order people use them. A page with more
1034 than one view shows them as tabs across its top. */}
1035 <Rule />
1036 <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 request1037 <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 owner1038 Code
1039 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1040 <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 owner1041 Issues
1042 </SidebarLink>
1043 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
1044 Pull requests
1045 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1046 <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 owner1047 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1048 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner1049 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
1050 Workflows
Projects: what a workspace builds and runs, first on every page1051 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1052 </div>
1053 <Rule />
1054 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1055 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner1056 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
1057 Deployments
1058 </SidebarLink>
1059 ) : null}
Observability gets its own page; tabs never overflow1060 <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.">
1061 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner1062 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1063 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1064 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
1065 Security
1066 </SidebarLink>
1067 ) : null}
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971068 <SidebarLink
1069 to={`${base}/contributors`}
1070 also={[`${base}/activity`, `${base}/stargazers`, ...soonPaths(base, "Insights")]}
1071 icon={<BarChart3 size={15} />}
1072 >
A flat project menu with Code first, views as tabs, and Stripe for every owner1073 Insights
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971074 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1075 </div>
1076 {(repo.settings ?? shows.has("settings")) && (
1077 <>
1078 <Rule />
1079 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1080 Settings
1081 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1082 </>
1083 )}
A repository has its own sidebar, as settings do1084 </nav>
1085 );
1086}
1087
Project settings move into the sidebar, grouped, with a heading per page1088/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1089 * A project's settings, drilled into from its list: what is about running
1090 * it, then its agents, then its code and who can reach it. The way back
1091 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1092 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1093function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1094 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1095 // Each page shows to the roles that can use it, once the role is known.
1096 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1097 const running = shows("deployments");
1098 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1099 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1100 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1101 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1102 {shows("") && (
1103 <div className="mt-2 space-y-px">
1104 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1105 General
1106 </SidebarLink>
1107 </div>
1108 )}
1109 {running && (
1110 <>
1111 {shows("") && <Rule />}
1112 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1113 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1114 Deployments
1115 </SidebarLink>
1116 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1117 Domains
1118 </SidebarLink>
1119 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1120 Dependencies
1121 </SidebarLink>
1122 </div>
1123 </>
1124 )}
1125 {agents && (
1126 <>
1127 <Rule />
1128 <div className="space-y-px">
1129 {shows("agents") && (
1130 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1131 Agents
1132 </SidebarLink>
1133 )}
1134 {shows("guardrails") && (
1135 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1136 Guardrails
1137 </SidebarLink>
1138 )}
1139 </div>
1140 </>
1141 )}
1142 <Rule />
1143 <div className="space-y-px">
1144 {shows("repository") && (
1145 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1146 Repository
1147 </SidebarLink>
1148 )}
1149 {shows("access") && (
1150 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1151 Access
1152 </SidebarLink>
1153 )}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1154 {shows("keys") && (
1155 <SidebarLink to={`${base}/settings/keys`} icon={<KeyRound size={15} />}>
1156 Deploy keys
1157 </SidebarLink>
1158 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1159 {shows("branches") && (
1160 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1161 Branches and merging
1162 </SidebarLink>
1163 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1164 {shows("rules") && (
1165 <SidebarLink to={`${base}/settings/rules`} icon={<Scale size={15} />}>
1166 Rules
1167 </SidebarLink>
1168 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1169 {shows("secrets") && (
1170 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1171 Secrets and variables
1172 </SidebarLink>
1173 )}
Merge branch 'worktree-agent-a3abfcce648e87dca'1174 {shows("actions") && (
1175 <SidebarLink to={`${base}/settings/actions`} icon={<PlayCircle size={15} />}>
1176 Actions
1177 </SidebarLink>
1178 )}
1179 {shows("environments") && (
1180 <SidebarLink to={`${base}/settings/environments`} icon={<Layers size={15} />}>
1181 Environments
1182 </SidebarLink>
1183 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1184 {shows("runners") && (
1185 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1186 Runners
1187 </SidebarLink>
1188 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1189 {shows("webhooks") && (
1190 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1191 Webhooks
1192 </SidebarLink>
1193 )}
1194 </div>
Project settings move into the sidebar, grouped, with a heading per page1195 </nav>
1196 );
1197}
1198
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)1199/**
1200 * Whether the menus list Settings → Invites (invites to g1t): always while
1201 * sign-up takes an invite; once anyone can sign up, only for someone with
1202 * invites already made to look back on, which the account menu's data says.
1203 */
1204function useInvitesListed(): boolean {
1205 const inviteOnly = useInviteOnly();
1206 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
1207 useEffect(() => {
1208 if (!inviteOnly && details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
1209 // eslint-disable-next-line react-hooks/exhaustive-deps
1210 }, [inviteOnly]);
1211 return inviteOnly || details.data?.invites_page === true;
1212}
1213
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1214/** Your own settings, drilled into from Your settings: one page each. */
1215function 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)1216 const invites = useInvitesListed();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1217 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1218 <SidebarLink to={`/settings/${page}`} icon={icon}>
1219 {ACCOUNT_SETTINGS[page].title}
1220 </SidebarLink>
Account settings in the same shape as a workspace's1221 );
1222 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1223 <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 quotas1224 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1225 <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 quotas1226 {link("profile", <CircleUserRound size={15} />)}
1227 {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)1228 {invites && link("invites", <Ticket size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1229 </div>
1230 <Rule />
1231 <div className="space-y-px">
1232 {link("keys", <Fingerprint size={15} />)}
1233 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1234 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1235 <Rule />
1236 <div className="space-y-px">
1237 {link("github", <GithubMark className="size-[15px]" />)}
1238 {link("applications", <Plug size={15} />)}
1239 </div>
1240 <Rule />
1241 <div className="space-y-px">
Merge main (membership, two-factor, GitHub repo roles) into tokens1242 {link("two-factor", <ShieldCheck size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1243 {link("security-log", <History size={15} />)}
1244 </div>
Account settings in the same shape as a workspace's1245 </nav>
1246 );
1247}
1248
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1249/**
1250 * Signing in and signing up, in place of the account for a visitor. Signing
1251 * in brings them back to the page they are on.
1252 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1253/** Help, status and the fine print under a visitor's sign-in buttons. */
1254function VisitorLinks() {
1255 const status = useSiteStatus();
Merge branch 'worktree-agent-ad7c6d88d93adc817'1256 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 quotas1257 const dot = (
1258 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1259 ·
1260 </span>
1261 );
1262 return (
1263 // Two short rows, so a narrow sidebar never starts a line with a dot.
1264 <nav aria-label="About g1t" className="pt-0.5">
1265 <div className="flex items-center gap-0.5">
1266 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1267 <StatusDot state={status?.overall.state ?? null} />
1268 Status
1269 </a>
1270 {dot}
1271 <Link to="/support" className={quiet}>
1272 Support
1273 </Link>
1274 </div>
1275 <div className="flex items-center gap-0.5">
1276 {LEGAL_LINKS.map(([label, to], index) => (
1277 <span key={to} className="flex items-center gap-0.5">
1278 {index > 0 && dot}
1279 <Link to={to} className={quiet}>
1280 {label}
1281 </Link>
1282 </span>
1283 ))}
1284 </div>
1285 </nav>
1286 );
1287}
1288
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1289function VisitorPanel() {
1290 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1291 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1292 return (
1293 <div className="space-y-2">
1294 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1295 <div className="grid grid-cols-2 gap-2">
1296 <Link
1297 to={withNext("/login", pathname + search)}
1298 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"
1299 >
1300 <LogIn size={14} />
1301 Sign in
1302 </Link>
1303 <Link
1304 to={withNext("/register", pathname + search)}
1305 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1306 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1307 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1308 </Link>
1309 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1310 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1311 </div>
1312 );
1313}
1314
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1315function Sidebar({
1316 user,
1317 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1318 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1319 onFind,
1320 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)1321 rail = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1322}: {
1323 user: User | null;
1324 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1325 /** The page is a 404: the address names nothing the viewer can see. */
1326 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1327 onFind: () => void;
1328 /** In the sheet on a small screen: closing it, at the end of the top row. */
1329 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)1330 /**
1331 * Beside the rail, as Code's sidebar: code only, under a Code heading.
1332 * The workspace, its settings and the account are the rail's.
1333 */
1334 rail?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1335}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1336 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1337 const { pathname } = useLocation();
1338 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1339 // Where the sidebar is drilled to follows the page being gone to, so it
1340 // moves as the link is followed, not once the page arrives.
1341 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1342 // On a 404, the address is not a place: no list is drilled into from it,
1343 // except the project's own when the project is real and visible and only
1344 // something inside it is missing (a file, a commit, an issue).
1345 const lost = missing && going == null;
1346 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1347 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1348 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1349 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1350 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1351 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 look1352 // An invitation to a repository is answered before its menu means anything.
1353 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1354 repoPath != null &&
1355 !reserved.has(repoPath[1]) &&
1356 repoPath[2] !== "-" &&
1357 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1358 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1359 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1360 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1361 // The repository the menu is for: the one loaded if it is the one being
1362 // gone to, else what the address says, at once.
1363 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 look1364 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1365 ? sameRepo(active, targetRepo)
1366 ? active
1367 : {
1368 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1369 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1370 }
1371 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1372 // The viewer's role there, from the repository's page once it has loaded.
1373 const page = useRouteLoaderData("routes/repo/layout") as
1374 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1375 | undefined;
1376 const menuRepo: MenuRepo | null =
1377 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1378 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1379 : guessed;
1380
1381 // 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)1382 const trail: Level[] = [{ key: "main", node: rail && user && ws ? <CodeMenu shell={shell} slug={ws.slug} /> : <MainMenu user={user} shell={shell} /> }];
1383 if (rail && !menuRepo) {
1384 // Code's own list: nothing to drill into but a project.
1385 } else if (user && inAccount && !rail) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1386 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)1387 } else if (ws && inSettings && !rail) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1388 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1389 } else if (menuRepo) {
1390 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1391 // Out of a project: to its workspace's projects when they are yours.
1392 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1393 const back = !user
1394 ? { to: "/explore", label: "Explore" }
1395 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1396 ? { 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)1397 : { to: "/", label: "Home" };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1398 trail.push({
1399 key,
1400 node: (
1401 <RepoMenu
1402 repo={menuRepo}
1403 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1404 back={back}
1405 />
1406 ),
1407 });
1408 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1409 }
1410
Agents as a team: lifecycle, merge queue, billing and a new shell1411 return (
1412 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1413 {/* 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)1414 {rail ? (
1415 <ModeHeader title="Code" onClose={onClose} />
1416 ) : (
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1417 <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 put1418 {user ? (
1419 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1420 {/* The 1 alone beside the workspace: a square hover the height of the
1421 switcher, the mark as tall as the workspace avatar. */}
1422 <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 bar1423 <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 put1424 </Link>
1425 <span className="shrink-0 text-line-strong" aria-hidden="true">
1426 /
1427 </span>
1428 <WorkspaceSwitcher user={user} shell={shell} />
1429 </>
1430 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1431 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1432 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1433 </Link>
1434 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1435 {onClose && (
1436 <button
1437 type="button"
1438 aria-label="Close menu"
1439 onClick={onClose}
1440 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1441 >
1442 <X size={16} />
1443 </button>
1444 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1445 </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)1446 )}
A Vercel-like sidebar and a new mark1447 <div className="px-2 pt-3">
1448 <button
1449 type="button"
1450 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1451 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 mark1452 >
1453 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1454 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1455 <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 mark1456 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1457 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1458 <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)1459 {/* The account, or signing in, as one row at the very bottom; beside the rail, the rail has it. */}
1460 {!rail && (
1461 <div className="shrink-0 border-t border-line p-2">
1462 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
1463 </div>
1464 )}
1465 </div>
1466 );
1467}
1468
1469/** A mode's sidebar heading: the same height and rule as the top bar, with a way to close the phone's sheet. */
1470export function ModeHeader({ title, action, onClose }: { title: ReactNode; action?: ReactNode; onClose?: () => void }) {
1471 return (
1472 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2.5 pl-4">
1473 <h2 className="min-w-0 grow truncate text-[0.9375rem] font-semibold">{title}</h2>
1474 {action}
1475 {onClose && (
1476 <button
1477 type="button"
1478 aria-label="Close menu"
1479 onClick={onClose}
1480 className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1481 >
1482 <X size={16} />
1483 </button>
1484 )}
1485 </div>
1486 );
1487}
1488
1489/**
1490 * Code's sidebar (beside the rail): only code. Its Overview (Mission
1491 * control's code panels), the projects, what spans them, and what is
1492 * coming. The workspace's people, money and settings are Workspace's;
1493 * agents, context and memory are Agents'.
1494 */
1495function CodeMenu({ shell, slug }: { shell: ShellData; slug: string }) {
1496 const { pathname } = useLocation();
1497 const going = useNavigation().location?.pathname;
1498 const path = going ?? pathname;
1499 const at = (page: string) => path === `/${slug}/-/${page}` || path.startsWith(`/${slug}/-/${page}/`);
1500 const shared = shell.shared ?? [];
1501 return (
1502 <nav aria-label="Code" className={PANEL}>
1503 <div className="mt-3 space-y-px">
1504 <SidebarLink to={`/${slug}/-/overview`} icon={<LayoutDashboard size={15} />} current={at("overview")}>
1505 Overview
1506 </SidebarLink>
1507 </div>
1508 <div className="mt-3">
1509 <SidebarProjects slug={slug} shell={shell} current={at("projects")} />
1510 </div>
1511 <Rule />
1512 <div className="space-y-px">
1513 <SidebarLink to={`/${slug}/-/security`} icon={<ShieldCheck size={15} />} current={at("security") && !at("security/settings")}>
1514 Security
1515 </SidebarLink>
1516 <SidebarLink to={`/${slug}/-/packages`} icon={<Package size={15} />} current={at("packages")}>
1517 Packages
1518 </SidebarLink>
1519 {INSIGHTS && (
1520 <SidebarSoonLink to={`/${slug}/-/insights`} icon={<TrendingUp size={15} />} about={INSIGHTS.summary} current={at("insights")}>
1521 {INSIGHTS.title}
1522 </SidebarSoonLink>
1523 )}
1524 {roadmapIn("Workspace")
1525 .filter((item) => item.key !== "insights" && item.key !== "teams" && item.key !== "fleet")
1526 .map((item) => (
1527 <SidebarSoonLink key={item.key} to={`/${slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />} about={item.summary}>
1528 {item.title === "Board" ? "Boards" : item.title}
1529 </SidebarSoonLink>
1530 ))}
1531 </div>
1532 {shared.length > 0 && (
1533 <>
1534 <Rule />
1535 <SidebarGroup title="Shared with you">
1536 {shared.map((repo) => (
1537 <SidebarLink
1538 key={`${repo.namespace}/${repo.name}`}
1539 to={`/${repo.namespace}/${repo.name}`}
1540 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
1541 drill="hover"
1542 >
1543 <span className="font-mono text-faint">{repo.namespace}/</span>
1544 {repo.name}
1545 </SidebarLink>
1546 ))}
1547 </SidebarGroup>
1548 </>
1549 )}
1550 </nav>
1551 );
1552}
1553
1554/** Workspace pages that sit in its Settings list, drilled into from the Workspace sidebar. */
1555const WORKSPACE_SETTINGS = ["settings", "repositories", "tokens", "personal-access-tokens", "secrets", "actions", "runners", "webhooks", "emoji"];
1556
1557/**
1558 * The Workspace mode's sidebar: the workspace itself, for every member.
1559 * Its overview, people, money, connections, policies and record, then its
1560 * settings as a list of their own. Owner-only pages stay owner-only.
1561 */
1562export function WorkspaceSidebar({ slug, owner, onClose }: { slug: string; owner: boolean; onClose?: () => void }) {
1563 const { pathname } = useLocation();
1564 const going = useNavigation().location?.pathname;
1565 const path = going ?? pathname;
1566 const page = path.startsWith(`/${slug}/-/`) ? path.slice(`/${slug}/-/`.length) : "";
1567 const top = page.split("/")[0] ?? "";
1568 const at = (...pages: string[]) => pages.some((p) => page === p || page.startsWith(`${p}/`));
1569 const inSettings = WORKSPACE_SETTINGS.includes(top);
1570 const main = (
1571 <nav aria-label="Workspace" className={PANEL}>
1572 <div className="mt-3 space-y-px">
1573 <SidebarLink to={`/${slug}/-/workspace`} icon={<LayoutGrid size={15} />} current={at("workspace")}>
1574 Overview
1575 </SidebarLink>
1576 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />} current={at("people")}>
1577 People
1578 </SidebarLink>
1579 <SidebarLink to={`/${slug}/-/teams`} icon={<UsersRound size={15} />} current={at("teams")}>
1580 Teams
1581 </SidebarLink>
1582 </div>
1583 <SidebarGroup title="Usage and billing" className="mt-3">
1584 <SidebarLink to={`/${slug}/-/usage`} icon={<BarChart3 size={15} />} current={at("usage", "gateway")}>
1585 Usage
1586 </SidebarLink>
1587 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />} current={at("billing")}>
1588 Billing and plans
1589 </SidebarLink>
1590 </SidebarGroup>
1591 <SidebarGroup title="Connections" className="mt-3">
1592 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />} current={at("integrations")}>
1593 Integrations
1594 </SidebarLink>
1595 </SidebarGroup>
1596 <SidebarGroup title="Security policies" className="mt-3">
1597 <SidebarLink to={`/${slug}/-/security/settings`} icon={<ShieldCheck size={15} />} current={at("security/settings")}>
1598 Security settings
1599 </SidebarLink>
1600 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />} current={at("guardrails")}>
1601 Guardrails
1602 </SidebarLink>
1603 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />} current={at("rules")}>
1604 Rules
1605 </SidebarLink>
1606 </SidebarGroup>
1607 <Rule />
1608 <div className="space-y-px">
1609 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />} current={at("audit")}>
1610 Audit log
1611 </SidebarLink>
1612 <SidebarLink to={owner ? `/${slug}/-/settings` : `/${slug}/-/repositories`} icon={<Settings size={15} />} drill current={inSettings}>
1613 Settings
1614 </SidebarLink>
1615 </div>
1616 </nav>
1617 );
1618 const settings = (
1619 <nav aria-label="Workspace settings" className={PANEL}>
1620 <BackRow to={`/${slug}/-/workspace`} label="Settings" context={slug} />
1621 <div className="mt-2 space-y-px">
1622 {owner && (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1623 <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)1624 General
1625 </SidebarLink>
1626 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1627 <SidebarLink to={`/${slug}/-/settings/chat`} icon={<MessagesSquare size={15} />}>
1628 Chat
1629 </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)1630 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
1631 Repositories
1632 </SidebarLink>
1633 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
1634 Access tokens
1635 </SidebarLink>
1636 {owner && (
1637 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
1638 Personal access tokens
1639 </SidebarLink>
1640 )}
1641 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
1642 Webhooks
1643 </SidebarLink>
1644 <SidebarLink to={`/${slug}/-/emoji`} icon={<Smile size={15} />}>
1645 Emoji
1646 </SidebarLink>
1647 </div>
1648 <SidebarGroup title="Runs" className="mt-3">
1649 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
1650 Secrets and variables
1651 </SidebarLink>
1652 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
1653 Actions
1654 </SidebarLink>
1655 {owner && (
1656 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
1657 Runners
1658 </SidebarLink>
1659 )}
1660 </SidebarGroup>
1661 </nav>
1662 );
1663 const trail: Level[] = [{ key: "workspace", node: main }];
1664 if (inSettings) trail.push({ key: "workspace:settings", node: settings });
1665 return (
1666 <div className="flex h-full flex-col">
1667 <ModeHeader title="Workspace" onClose={onClose} />
1668 <Drill trail={trail} />
1669 </div>
1670 );
1671}
1672
1673/** Your own settings, beside the rail: under the account menu, not any workspace's. */
1674export function AccountSidebar({ username, onClose }: { username: string; onClose?: () => void }) {
1675 return (
1676 <div className="flex h-full flex-col">
1677 <ModeHeader title="Your account" onClose={onClose} />
1678 <div className="min-h-0 grow">
1679 <AccountSettingsMenu username={username} />
A Vercel-like sidebar and a new mark1680 </div>
1681 </div>
1682 );
1683}
1684
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)1685
A Vercel-like sidebar and a new mark1686/** Words for the sections a path can end in. */
1687const SECTIONS: Record<string, string> = {
1688 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1689 milestones: "Milestones",
1690 labels: "Labels",
A Vercel-like sidebar and a new mark1691 pulls: "Pull requests",
1692 queue: "Merge queue",
1693 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1694 plans: "Outcomes",
1695 actions: "Workflows",
1696 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1697 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1698 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1699 access: "Access",
1700 invitations: "Invitation",
1701 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1702 branches: "Branches",
1703 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1704 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1705 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1706 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1707 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1708 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1709 people: "People",
1710 projects: "Projects",
1711 teams: "Teams",
A Vercel-like sidebar and a new mark1712 tokens: "Access tokens",
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1713 "personal-access-tokens": "Personal access tokens",
A Vercel-like sidebar and a new mark1714 usage: "Usage",
Site: the AI Gateway page's breadcrumb reads "AI Gateway"1715 gateway: "AI Gateway",
Secrets and variables: one list, rows per environment, for workflows and deployments1716 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1717 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1718 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)1719 emoji: "Emoji",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1720 domains: "Domains",
1721 guardrails: "Guardrails",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1722 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1723 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1724 tree: "Files",
1725 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1726 agents: "Agents",
1727 context: "Context",
1728 memory: "Memory",
1729 security: "Security",
1730 packages: "Packages",
1731 runners: "Runners",
Merge branch 'worktree-agent-a3abfcce648e87dca'1732 environments: "Environments",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1733 workflows: "Workflows",
1734 observability: "Observability",
1735 insights: "Insights",
1736 sessions: "Sessions",
Chat and workspace agents: channels, DMs and named agents you talk to1737 chat: "Chat",
1738 docs: "Docs",
1739 home: "Home",
1740 "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)1741 overview: "Overview",
1742 workspace: "Workspace",
A Vercel-like sidebar and a new mark1743};
1744
Merge branch 'worktree-agent-a3abfcce648e87dca'1745/** Settings pages whose name differs from the section's of the same word. */
1746const SETTINGS_SECTIONS: Record<string, string> = { branches: "Branches and merging", actions: "Actions" };
1747
A Vercel-like sidebar and a new mark1748/** 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 quotas1749function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1750 // A 404 names nothing from the address, unless the project is real and
1751 // visible and only something inside it is missing.
1752 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1753 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1754 return <span className="text-sm font-medium">Not found</span>;
1755 }
A Vercel-like sidebar and a new mark1756 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1757 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)1758 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 quotas1759 // Your settings: Settings / Emails.
1760 if (parts[0] === "settings") {
1761 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1762 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 quotas1763 }
A Vercel-like sidebar and a new mark1764 if (reserved.includes(parts[0]!)) {
1765 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1766 settings: "Account",
A Vercel-like sidebar and a new mark1767 explore: "Explore",
Projects live: fixes from walking them end to end1768 new: "New project",
A Vercel-like sidebar and a new mark1769 search: "Search",
1770 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1771 policies: "Policies",
1772 security: "Security",
1773 support: "Support",
1774 status: "Status",
1775 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1776 inbox: "Inbox",
A Vercel-like sidebar and a new mark1777 };
1778 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1779 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1780 // A person's profile, by their handle.
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1781 if (parts[0] === "u" && parts[1]) return <ProfileCrumb username={parts[1]} />;
A Vercel-like sidebar and a new mark1782 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1783 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1784 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1785 const page = `/${owner}/-/${third}`;
1786 if (third && SETTINGS_PAGE.test(page)) {
1787 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1788 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1789 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1790 } else if (second) {
1791 const repo = `/${owner}/${second}`;
1792 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1793 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1794 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 mark1795 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 deployments1796 else if (third && SECTIONS[third]) {
1797 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1798 // A settings page names which one: Settings / Secrets and variables.
1799 if (third === "settings" && fourth && SECTIONS[fourth]) {
Merge branch 'worktree-agent-a3abfcce648e87dca'1800 trail.push({ label: SETTINGS_SECTIONS[fourth] ?? SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1801 }
1802 }
A Vercel-like sidebar and a new mark1803 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1804 return <Trail trail={trail} />;
1805}
1806
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1807/** A profile's place in the top bar: the person's avatar and @handle, from the page once it has loaded. */
1808function 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 managers1809 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 project1810 const profile = page?.profile?.username.toLowerCase() === username.toLowerCase() ? page.profile : null;
1811 return (
1812 <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">
1813 <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 managers1814 <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 project1815 </Link>
1816 );
1817}
1818
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1819/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1820type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1821
1822/**
1823 * The trail of links. On a phone there is room for one: the page itself,
1824 * with a way back to where it sits; the whole trail from a wider screen up.
1825 */
1826function 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)1827 // Up on a phone, unless up is the workspace itself: its pages are the tabs.
1828 const up = trail.length > 1 && !/^\/[^/]+$/.test(trail[trail.length - 2]!.to) ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1829 return (
1830 <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 audit1831 {up && (
1832 <Link
1833 to={up.to}
1834 prefetch="intent"
1835 aria-label={`Back to ${up.label}`}
1836 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"
1837 >
1838 <ChevronLeft size={16} />
1839 </Link>
1840 )}
1841 {trail.map((crumb, index) => {
1842 const last = index === trail.length - 1;
1843 return (
1844 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1845 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1846 <Link
1847 to={crumb.to}
1848 prefetch="intent"
1849 aria-current={last ? "page" : undefined}
1850 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1851 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1852 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1853 >
1854 {last && crumb.short ? (
1855 <>
1856 <span className="sm:hidden">{crumb.short}</span>
1857 <span className="hidden sm:inline">{crumb.label}</span>
1858 </>
1859 ) : (
1860 crumb.label
1861 )}
1862 </Link>
1863 </span>
1864 );
1865 })}
A Vercel-like sidebar and a new mark1866 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1867 );
1868}
1869
Search across all of g1t, Explore, and a command palette1870type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1871
1872/** 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 look1873function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1874 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1875 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)1876 { label: "Home", to: "/", icon: <House size={15} /> },
1877 ...(shell.workspace
1878 ? [
1879 { label: "Chat", to: `/${shell.workspace.slug}/-/chat`, icon: <MessagesSquare size={15} /> },
1880 { label: "Agents", to: `/${shell.workspace.slug}/-/agents`, icon: <Sparkles size={15} /> },
1881 ...(hasCodeAccess(shell.workspace) ? [{ label: "Code overview", to: `/${shell.workspace.slug}/-/overview`, icon: <Code2 size={15} /> }] : []),
1882 { label: "Workspace", hint: "People, billing, policies, settings", to: `/${shell.workspace.slug}/-/workspace`, icon: <Building2 size={15} /> },
1883 ]
1884 : []),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1885 ...(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-agent1886 ? [{ 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 step1887 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1888 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1889 { 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 all1890 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1891 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1892 { 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 quotas1893 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1894 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1895 .filter((page) => page !== "profile")
1896 .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 shell1897 ];
1898 const repo = shell.repo;
1899 if (repo) {
1900 const base = `/${repo.namespace}/${repo.name}`;
1901 const name = `${repo.namespace}/${repo.name}`;
1902 commands.unshift(
Projects: what a workspace builds and runs, first on every page1903 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1904 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1905 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1906 { 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 step1907 ...(repo.member
1908 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1909 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1910 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1911 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1912 ...(repo.member
1913 ? [
1914 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1915 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1916 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1917 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1918 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1919 ]
1920 : []),
1921 );
1922 }
1923 for (const membership of user.workspaces ?? []) {
1924 commands.push(
Workspace names and icons, and a component kit for every control1925 { 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 sidebar1926 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1927 { 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 request1928 { 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 tokens1929 { 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 page1930 { 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 settings1931 { 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 look1932 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1933 );
1934 }
1935 for (const listed of shell.repos) {
1936 commands.push({
Projects: what a workspace builds and runs, first on every page1937 label: listed.title ?? listed.name,
1938 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1939 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1940 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1941 });
1942 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1943 // "Put an agent on …": one per project, after the projects themselves.
1944 for (const listed of shell.repos) {
1945 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1946 commands.push({
1947 label: `Put an agent on ${listed.title ?? listed.name}`,
1948 hint: `${listed.namespace}/${listed.name}`,
1949 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1950 icon: <Sparkles size={15} />,
1951 });
1952 }
Agents as a team: lifecycle, merge queue, billing and a new shell1953 return commands;
1954}
1955
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1956/** 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 look1957function 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 put1958 const commands: Command[] = [];
1959 const repo = shell.repo;
1960 if (repo) {
1961 const base = `/${repo.namespace}/${repo.name}`;
1962 const name = `${repo.namespace}/${repo.name}`;
1963 commands.push(
1964 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1965 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1966 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1967 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1968 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1969 );
1970 }
1971 commands.push(
1972 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1973 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1974 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1975 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1976 { 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 look1977 { 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 put1978 );
1979 return commands;
1980}
1981
Agents as a team: lifecycle, merge queue, billing and a new shell1982/**
Merge branch 'worktree-agent-aca4790ea223aa235'1983 * The page while the next one loads: after a moment it dims, under the
1984 * bar below, so it is plainly on its way out rather than frozen, and the
1985 * next page fades up from there. Sending a form does not dim it: the
1986 * form's button says it is working. Spread on the element holding the page.
1987 */
1988export function useLeaving() {
1989 const navigation = useNavigation();
1990 const method = navigation.formMethod?.toUpperCase();
1991 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1992 return {
1993 "aria-busy": leaving || undefined,
1994 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1995 };
1996}
1997
1998/**
Agents as a team: lifecycle, merge queue, billing and a new shell1999 * A bar across the top of the page while the next one loads, as GitHub
2000 * has: it appears at once, creeps towards the end while waiting, then fills
2001 * and fades when the page arrives.
2002 */
2003export function Progress() {
2004 const navigation = useNavigation();
2005 const busy = navigation.state !== "idle";
2006 const [width, setWidth] = useState(0);
2007 const [visible, setVisible] = useState(false);
2008 useEffect(() => {
2009 if (busy) {
2010 setVisible(true);
2011 setWidth(12);
2012 // Slows as it goes, never quite arriving until the page does.
2013 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
2014 return () => clearInterval(timer);
2015 }
2016 setWidth((now) => (now > 0 ? 100 : 0));
2017 const fade = setTimeout(() => setVisible(false), 250);
2018 const reset = setTimeout(() => setWidth(0), 550);
2019 return () => {
2020 clearTimeout(fade);
2021 clearTimeout(reset);
2022 };
2023 }, [busy]);
2024 return (
2025 <div
2026 aria-hidden="true"
2027 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
2028 visible ? "opacity-100" : "opacity-0"
2029 }`}
2030 >
2031 <div
2032 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
2033 style={{ width: `${width}%` }}
2034 />
2035 </div>
2036 );
2037}
2038
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)2039/** The top bar's way to g1t: the direct message with the workspace's orchestrator, opened or made. */
2040function AskG1tButton({ slug }: { slug: string }) {
2041 return (
2042 <Hint label="Ask g1t">
2043 <Link
2044 to={`/${slug}/-/chat?agent=g1t`}
2045 aria-label="Ask g1t"
2046 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"
2047 >
2048 <G1tMark size={18} />
2049 <span className="hidden sm:inline">Ask g1t</span>
2050 </Link>
2051 </Hint>
2052 );
2053}
2054
Chat and workspace agents: channels, DMs and named agents you talk to2055/** The mode's sidebar beside the rail: a shade lighter than the page. */
2056const SIDEBAR_BOX = "h-full border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]";
2057
Agents as a team: lifecycle, merge queue, billing and a new shell2058/**
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2059 * Which sidebar sits beside the rail: each mode's own, or none (Docs, the
2060 * Inbox, which is a page of its own, and g1t's public pages, such as a
2061 * profile, which are no workspace's and carry their own left column).
2062 * Without a workspace (a visitor), the one sidebar there always was.
Chat and workspace agents: channels, DMs and named agents you talk to2063 */
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)2064function sidebarFor(mode: ModeKey | null): Panel | null {
Chat and workspace agents: channels, DMs and named agents you talk to2065 if (mode == null) return "code";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2066 if (mode === "docs" || 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)2067 return mode;
Chat and workspace agents: channels, DMs and named agents you talk to2068}
2069
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)2070/** The sidebars that sit beside the rail, one per mode that has one. */
2071type Panel = "home" | "chat" | "agents" | "code" | "workspace" | "account";
2072
Chat and workspace agents: channels, DMs and named agents you talk to2073/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2074 * The app: a sidebar with the workspace, its repositories and the sections
2075 * of the one being looked at; a slim bar with search and the account; and
2076 * the page. A visitor who is not signed in gets the same frame, with
2077 * Explore and Search in place of the workspace, and signing in in place of
2078 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell2079 */
2080export function AppShell({
2081 user,
2082 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2083 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell2084 banner,
2085 children,
2086}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2087 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell2088 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2089 /** The page is a 404 (see Sidebar). */
2090 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell2091 banner?: ReactNode;
2092 children: ReactNode;
2093}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2094 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell2095 const [drawer, setDrawer] = useState(false);
2096 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2097 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2098 const signUpLabel = useSignUpCopy().primary;
2099 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell2100
2101 // A new page closes the drawer on small screens.
2102 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2103 // Mission control's code, fetched while the browser is idle, so going
2104 // home never waits on it (routes/home.tsx).
2105 useEffect(() => {
2106 if (!user) return;
2107 const load = () => void import("./mission-control").catch(() => undefined);
2108 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
2109 if (idle) idle(load);
2110 else setTimeout(load, 1500);
2111 }, [user]);
Search across all of g1t, Explore, and a command palette2112 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'2113 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)2114 useVisualViewport();
Agents as a team: lifecycle, merge queue, billing and a new shell2115
Chat and workspace agents: channels, DMs and named agents you talk to2116 // The rail and the mode's sidebar (docs/WORKSPACE.md, "Shell"), for
2117 // someone in a workspace. A visitor, or someone with none, keeps the one
2118 // sidebar.
2119 const going = useNavigation().location?.pathname;
2120 const ws = user ? shell.workspace : null;
2121 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)2122 const panel = ws ? sidebarFor(mode) : "code";
2123 const code = ws ? hasCodeAccess(ws) : true;
Chat and workspace agents: channels, DMs and named agents you talk to2124 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)2125 // Notify: the feed socket's live counts (lib/notify-client.ts) once it has them; the page's until then.
2126 const live = useLiveBadges(ws?.slug);
2127 const chatUnread = chatSidebar
2128 ? unreadTotals(chatSidebar.entries)
2129 : live?.chat != null
2130 ? { unread: live.chat, mentions: live.mentions ?? 0 }
2131 : (shell.chat ?? { unread: 0, mentions: 0 });
2132 const inboxUnread = live?.inbox ?? shell.inbox?.unread ?? 0;
2133 const sidebarNode = (inSheet: boolean) => {
2134 const close = inSheet ? () => setDrawer(false) : undefined;
2135 const find = () => {
2136 if (inSheet) setDrawer(false);
2137 setPalette(true);
2138 };
2139 if (!ws || !user || panel === "code") {
2140 return <Sidebar user={user} shell={shell} missing={missing} onFind={find} onClose={close} rail={Boolean(ws && user)} />;
2141 }
2142 switch (panel) {
2143 case "home":
2144 return <HomeSidebar slug={ws.slug} shell={shell} code={code} onFind={find} header={<ModeHeader title="Home" onClose={close} />} />;
2145 case "chat":
2146 return <ChatSidebar slug={ws.slug} />;
2147 case "agents":
2148 return <AgentsSidebar slug={ws.slug} shellAgents={shell.agents ?? null} code={code} owner={ws.role === "owner"} />;
2149 case "workspace":
2150 return <WorkspaceSidebar slug={ws.slug} owner={ws.role === "owner"} onClose={close} />;
2151 case "account":
2152 return <AccountSidebar username={user.username} onClose={close} />;
2153 default:
2154 return null;
2155 }
2156 };
Chat and workspace agents: channels, DMs and named agents you talk to2157 const rail =
2158 user && ws ? (
2159 <Rail
2160 user={user}
2161 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)2162 unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }}
2163 help={<HelpMenu />}
Chat and workspace agents: channels, DMs and named agents you talk to2164 account={<AccountMenu user={user} rail />}
2165 />
2166 ) : null;
2167 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)2168 // On a phone (below 768px): the tab bar, and a conversation full screen.
2169 const conversation = isConversation(going ?? pathname);
2170 const tabs = Boolean(user && ws) && !conversation;
Chat and workspace agents: channels, DMs and named agents you talk to2171
Agents as a team: lifecycle, merge queue, billing and a new shell2172 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2173 // The phone's menu is a sheet: a dialog that holds focus, closes on
2174 // Escape or a tap outside, and gives focus back to the menu button.
2175 <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)2176 <div className="min-h-dvh">
Agents as a team: lifecycle, merge queue, billing and a new shell2177 <Progress />
Chat and workspace agents: channels, DMs and named agents you talk to2178 <aside className="fixed inset-y-0 left-0 z-40 hidden lg:flex">
2179 {rail}
2180 {panel && <div className={`${SIDEBAR_BOX} w-64`}>{sidebarNode(false)}</div>}
Agents as a team: lifecycle, merge queue, billing and a new shell2181 </aside>
Chat and workspace agents: channels, DMs and named agents you talk to2182 <SheetContent
2183 side="left"
2184 showClose={false}
2185 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)2186 // Focus would land on the workspace's avatar and open its hint over
2187 // the sidebar's heading; the sheet itself takes it instead.
2188 onOpenAutoFocus={(event) => {
2189 event.preventDefault();
2190 (event.currentTarget as HTMLElement | null)?.focus();
2191 }}
Chat and workspace agents: channels, DMs and named agents you talk to2192 className={`lg:hidden ${rail ? `flex flex-row ${panel ? "w-[21rem] max-w-[92vw] sm:max-w-[21rem]" : "w-20 sm:max-w-20"}` : "w-72 max-w-[85vw] sm:max-w-72"}`}
2193 >
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2194 <SheetTitle className="sr-only">Menu</SheetTitle>
Chat and workspace agents: channels, DMs and named agents you talk to2195 {rail}
2196 {panel && <div className={`min-w-0 grow ${rail ? SIDEBAR_BOX : ""}`}>{sidebarNode(true)}</div>}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2197 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell2198
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)2199 <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 to2200 <header
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 className={`sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-line bg-bg/85 pt-[env(safe-area-inset-top)] pr-[max(1rem,env(safe-area-inset-right))] pl-[max(1rem,env(safe-area-inset-left))] backdrop-blur sm:px-6 max-md:h-[calc(3.5rem+env(safe-area-inset-top))] ${mode === "chat" ? "lg:hidden" : ""} ${conversation ? "max-md:hidden" : ""}`}
Chat and workspace agents: channels, DMs and named agents you talk to2202 >
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)2203 {/* A phone: the workspace's avatar opens everything else (components/mobile.tsx). */}
2204 {user && ws && <AvatarSheetButton user={user} workspace={ws} />}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2205 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell2206 aria-label="Open menu"
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)2207 className={`rounded-md p-1.5 text-muted hover:bg-raised hover:text-fg lg:hidden ${user && ws ? "max-md:hidden" : ""}`}
Agents as a team: lifecycle, merge queue, billing and a new shell2208 >
2209 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2210 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2211 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2212 <div className="ml-auto flex items-center gap-1.5">
2213 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
2214 <button
2215 type="button"
2216 aria-label="Search or jump to"
2217 onClick={() => setPalette(true)}
2218 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
2219 >
2220 <Search size={16} />
2221 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2222 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
2223 <NavLink
2224 to="/explore"
2225 prefetch="intent"
2226 aria-label="Explore"
2227 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)2228 `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 sidebar2229 }
2230 >
2231 <Compass size={16} className="sm:hidden" />
2232 <span className="hidden sm:inline">Explore</span>
2233 </NavLink>
A Vercel-like sidebar and a new mark2234 <a
2235 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals2236 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 mark2237 >
2238 Docs
2239 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2240 {user && (
2241 <>
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)2242 {shell.workspace ? <AskG1tButton slug={shell.workspace.slug} /> : <AgentButton />}
2243 {/* On a phone the Inbox is a tab. */}
2244 <span className={shell.workspace ? "max-md:hidden" : undefined}>
2245 <InboxBell counts={shell.inbox ? { ...shell.inbox, unread: inboxUnread } : null} />
2246 </span>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2247 </>
2248 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2249 {!user ? (
2250 // The sidebar has these too, but on a phone it is folded away.
2251 <Link
2252 to={withNext("/login", here)}
2253 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"
2254 >
2255 Sign in
2256 </Link>
2257 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2258 <>
2259 {/* Room between what comes in (the inbox) and what you make. */}
2260 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell2261 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2262 <Tooltip>
2263 <TooltipTrigger asChild>
2264 <DropdownMenuTrigger
2265 aria-label="Create new"
2266 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"
2267 >
2268 <Plus size={16} />
2269 <ChevronDown size={13} className="text-muted" />
2270 </DropdownMenuTrigger>
2271 </TooltipTrigger>
2272 <TooltipContent>Create new…</TooltipContent>
2273 </Tooltip>
2274 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell2275 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2276 <>
2277 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
2278 <DropdownMenuItem asChild>
2279 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
2280 <CircleDot />
2281 New issue
2282 </Link>
2283 </DropdownMenuItem>
2284 {shell.repo.member && (
2285 <DropdownMenuItem asChild>
2286 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
2287 <ListTree />
2288 Plan work
2289 </Link>
2290 </DropdownMenuItem>
2291 )}
2292 <DropdownMenuSeparator />
2293 </>
Agents as a team: lifecycle, merge queue, billing and a new shell2294 )}
2295 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2296 <Link to="/?agent=new">
2297 <Sparkles />
2298 Put an agent on it
2299 </Link>
2300 </DropdownMenuItem>
2301 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2302 <Link to="/new">
Projects: what a workspace builds and runs, first on every page2303 <Box />
2304 New project
Agents as a team: lifecycle, merge queue, billing and a new shell2305 </Link>
2306 </DropdownMenuItem>
2307 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2308 <Link to="/new/github">
2309 <GitBranch />
2310 Import from GitHub
2311 </Link>
2312 </DropdownMenuItem>
2313 <DropdownMenuSeparator />
2314 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2315 <Link to="/workspaces/new">
2316 <Users />
2317 New workspace
2318 </Link>
2319 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'2320 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
2321 {shell.workspace &&
2322 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
2323 <DropdownMenuItem asChild>
2324 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
2325 <UsersRound />
2326 New team
2327 </Link>
2328 </DropdownMenuItem>
2329 ) : (
2330 <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 bar2331 <UsersRound />
2332 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'2333 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
2334 </DropdownMenuItem>
2335 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell2336 </DropdownMenuContent>
2337 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2338 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2339 )}
Agents as a team: lifecycle, merge queue, billing and a new shell2340 </div>
2341 </header>
2342 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2343 <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 in2344 <InMain.Provider value={true}>{children}</InMain.Provider>
Merge branch 'worktree-agent-aca4790ea223aa235'2345 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell2346 </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)2347 {user && ws && <MobileTabBar workspace={ws} unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }} />}
Search across all of g1t, Explore, and a command palette2348 <CommandPalette
2349 open={palette}
2350 onOpenChange={setPalette}
2351 commands={commands}
2352 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
2353 />
Agents as a team: lifecycle, merge queue, billing and a new shell2354 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2355 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell2356 );
2357}

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