Skip to content
2,346 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
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)5import { type Abilities, type ChatSidebarEntry, type InboxCounts, type WorkspaceAgent, type Membership, type Spike, type User, hasCodeAccess, mayCreateTeams } from "@g1t/contracts";
Agents as a team: lifecycle, merge queue, billing and a new shell6
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in7import { InMain } from "./landmark";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit8import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./command-palette";
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar9import { AgentButton, InboxBell } from "./inbox";
10import { PinButton } from "./pin-button";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar11import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
Merge branch 'worktree-agent-ae1299e92e4462012'12import { Hint } from "./ui/hint";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "./ui/sheet";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas14import { StatusDot, useSiteStatus } from "./footer";
The 1 beside the workspace switcher again, in a square hover the switcher's height15import { Logo, Mark } from "./logo";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit16import { Avatar, SoonPill } from "./ui";
Fast pages, required checks on the branch, self-hosted runners, honest incidents17import { Skeleton } from "./ui/skeleton";
Agents as a team: lifecycle, merge queue, billing and a new shell18import {
19 DropdownMenu,
20 DropdownMenuContent,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas21 DropdownMenuGroup,
Agents as a team: lifecycle, merge queue, billing and a new shell22 DropdownMenuItem,
23 DropdownMenuLabel,
24 DropdownMenuSeparator,
25 DropdownMenuTrigger,
26} from "./ui/dropdown-menu";
Merge branch 'worktree-agent-ad7c6d88d93adc817'27import { type RoadmapItem, roadmapIn, roadmapItem } from "../lib/roadmap";
Chat and workspace agents: channels, DMs and named agents you talk to28import { type ModeKey, SETTINGS_PAGES, modeOf, sidebarCurrent } from "../lib/workspace-nav";
29import { AgentsSidebar } from "./agents-mode";
30import { ChatSidebar } from "./chat/sidebar";
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";
Agents as a team: lifecycle, merge queue, billing and a new shell47
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put48/**
49 * What the sidebar needs, worked out by the root loader. For a visitor who
50 * is not signed in there is no workspace, no projects and no usage: only
51 * the project being looked at, if they can see it.
52 */
Agents as a team: lifecycle, merge queue, billing and a new shell53export type ShellData = {
54 /** The workspace the sidebar is about: the one being looked at, or their first. */
Workspace names and icons, and a component kit for every control55 workspace: Membership | null;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar56 /** All its projects, by name, for the palette: `name` is the slug in their address. */
57 repos: ShortcutProject[];
58 /** The person's pinned projects in it, in their order (lib/pins.ts). */
59 pinned?: ShortcutProject[];
60 /** What they opened there last, latest first, leaving out the pinned. */
61 recent?: ShortcutProject[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look62 /** Repositories shared with them in workspaces they do not belong to. */
63 shared?: { namespace: string; name: string; isPrivate: boolean }[];
Projects: what a workspace builds and runs, first on every page64 /** The project being looked at, if any, whoever owns it. */
Agents as a team: lifecycle, merge queue, billing and a new shell65 repo: {
66 namespace: string;
67 name: string;
68 member: boolean;
69 issues: number;
70 pulls: number;
71 } | null;
Billing accounts, terms and enterprises; g1t is no longer free72 /** Where the workspace stands against its usage limit, if billing is on. */
73 limit: {
74 exposureMicros: number;
75 ceilingMicros: number | null;
76 state: "ok" | "warning" | "stopped";
77 comped: boolean;
78 } | null;
Free while g1t is being built out; agents can check out their own forks79 /** Whether g1t charges nothing for now, while it is being built out. */
80 free?: boolean;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request81 /** What its agents have cost since the start of the month. */
Usage while free is shown at cost; agents get rustfmt and clippy82 /** This month's usage: charged, or at cost while g1t is free. */
83 monthUsageMicros: number | null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 /** Whether new compute is paused (a spend spike or a hold), and whether the viewer can answer it. */
85 compute?: { paused: string | null; spike: Spike | null; owner: boolean } | null;
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all86 /** What is unread in their inbox, for the bell. Absent for a visitor. */
87 inbox?: InboxCounts | null;
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)88 /**
89 * Chat, for the rail's badge and Home's sidebar: what is unread, and the
90 * starred and latest conversations. Null when chat did not answer in time.
91 */
92 chat?: { unread: number; mentions: number; starred?: ChatSidebarEntry[]; recent?: ChatSidebarEntry[] } | null;
93 /** The workspace's agents, g1t first, for Home's and Agents' sidebars; null when not known. */
94 agents?: ShellAgent[] | null;
Agents as a team: lifecycle, merge queue, billing and a new shell95};
96
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)97/** An agent as the shell lists it. */
98export type ShellAgent = Pick<
99 WorkspaceAgent,
100 "id" | "handle" | "display_name" | "avatar" | "avatar_seed" | "role" | "status" | "title" | "team" | "department" | "builtin"
101>;
102
Agents as a team: lifecycle, merge queue, billing and a new shell103function SidebarLink({
104 to,
105 icon,
106 end,
107 count,
108 also,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109 drill,
Merge branch 'worktree-agent-ad7c6d88d93adc817'110 current: lit,
Agents as a team: lifecycle, merge queue, billing and a new shell111 children,
112}: {
113 to: string;
114 icon: ReactNode;
115 end?: boolean;
116 count?: number;
Projects: what a workspace builds and runs, first on every page117 /** Other path prefixes under which this link is the current one. */
118 also?: string | string[];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look119 /** It opens a list of its own: a chevron says so, always or on hover. */
120 drill?: boolean | "hover";
Merge branch 'worktree-agent-ad7c6d88d93adc817'121 /** Whether it is the current row, when the list works that out itself. */
122 current?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell123 children: ReactNode;
124}) {
125 const { pathname } = useLocation();
126 return (
127 <NavLink
128 to={to}
129 end={end}
130 prefetch="intent"
131 className={({ isActive, isPending }) => {
Projects: what a workspace builds and runs, first on every page132 const current =
Merge branch 'worktree-agent-ad7c6d88d93adc817'133 lit ??
134 (isActive || [also ?? []].flat().some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A Vercel-like sidebar and a new mark135 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
Agents as a team: lifecycle, merge queue, billing and a new shell136 current
137 ? "bg-raised font-medium text-fg"
138 : isPending
139 ? "bg-raised/60 text-fg"
140 : "text-muted hover:bg-raised/60 hover:text-fg"
141 }`;
142 }}
143 >
144 <span className="shrink-0 text-faint group-hover:text-muted">{icon}</span>
145 <span className="min-w-0 grow truncate">{children}</span>
146 {count != null && count > 0 && (
147 <span className="rounded bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
148 {count}
149 </span>
150 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look151 {drill && (
152 <ChevronRight
153 size={14}
154 aria-hidden="true"
155 className={`-mr-0.5 shrink-0 text-faint transition-opacity ${drill === "hover" ? "opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100" : ""}`}
156 />
157 )}
Agents as a team: lifecycle, merge queue, billing and a new shell158 </NavLink>
159 );
160}
161
Pull requests say where they are live; the project menu shows what is coming162/**
163 * Something a project will have, shown where it will live so people can
164 * see where g1t is going. Not a link: nothing is behind it yet.
165 */
166function SidebarSoon({ icon, children, about }: { icon: ReactNode; children: ReactNode; about: string }) {
167 return (
Merge branch 'worktree-agent-ae1299e92e4462012'168 <Hint label={about} side="right">
169 <div aria-disabled="true" className="flex h-8 cursor-default items-center gap-2.5 rounded-md px-2 text-[0.8125rem] text-faint">
170 <span className="shrink-0 opacity-70">{icon}</span>
171 <span className="grow truncate">{children}</span>
172 <span className="sr-only">{about}</span>
173 <SoonPill />
174 </div>
175 </Hint>
Pull requests say where they are live; the project menu shows what is coming176 );
177}
178
The project menu opens by section, and the workspace has its own panel179/**
A flat project menu with Code first, views as tabs, and Stripe for every owner180 * A page that is coming, as a link to its roadmap page: faint, with Soon,
181 * and marked current on any of its section's Soon pages.
The project menu opens by section, and the workspace has its own panel182 */
A flat project menu with Code first, views as tabs, and Stripe for every owner183function SidebarSoonLink({
The project menu opens by section, and the workspace has its own panel184 to,
185 also,
A flat project menu with Code first, views as tabs, and Stripe for every owner186 icon,
187 about,
Merge branch 'worktree-agent-ad7c6d88d93adc817'188 current: lit,
The project menu opens by section, and the workspace has its own panel189 children,
190}: {
191 to: string;
A flat project menu with Code first, views as tabs, and Stripe for every owner192 also?: string[];
193 icon: ReactNode;
194 about: string;
Merge branch 'worktree-agent-ad7c6d88d93adc817'195 /** Whether it is the current row, when the list works that out itself. */
196 current?: boolean;
The project menu opens by section, and the workspace has its own panel197 children: ReactNode;
198}) {
199 const { pathname } = useLocation();
200 return (
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero201 // The hint wraps a plain box: Radix's asChild merges className as a
202 // string, which would break NavLink's className function.
Merge branch 'worktree-agent-ae1299e92e4462012'203 <Hint label={about} side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero204 <div>
The project menu opens by section, and the workspace has its own panel205 <NavLink
206 to={to}
207 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner208 className={({ isActive }) => {
Merge branch 'worktree-agent-ad7c6d88d93adc817'209 const current = lit ?? (isActive || (also ?? []).some((prefix) => pathname === prefix || pathname.startsWith(prefix + "/")));
A flat project menu with Code first, views as tabs, and Stripe for every owner210 return `group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
211 current ? "bg-raised font-medium text-fg" : "text-faint hover:bg-raised/60 hover:text-muted"
The project menu opens by section, and the workspace has its own panel212 }`;
213 }}
214 >
A flat project menu with Code first, views as tabs, and Stripe for every owner215 <span className="shrink-0 opacity-80">{icon}</span>
The project menu opens by section, and the workspace has its own panel216 <span className="min-w-0 grow truncate">{children}</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit217 <SoonPill />
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales218 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero219 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'220 </Hint>
The project menu opens by section, and the workspace has its own panel221 );
222}
223
A flat project menu with Code first, views as tabs, and Stripe for every owner224/** Icons for what spans projects. */
225const WORKSPACE_ICONS: Record<string, ReactNode> = {
226 board: <KanbanSquare size={15} />,
227 roadmap: <GanttChart size={15} />,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member228 teams: <UsersRound size={15} />,
A flat project menu with Code first, views as tabs, and Stripe for every owner229 packages: <Package size={15} />,
230 fleet: <Bot size={15} />,
231};
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales232
A flat project menu with Code first, views as tabs, and Stripe for every owner233/** The Soon pages of a section, as paths, so its link is current on them. */
234function soonPaths(base: string, section: RoadmapItem["section"]): string[] {
235 return roadmapIn(section).map((item) => `${base}/soon/${item.key}`);
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales236}
237
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look238function SidebarGroup({
239 title,
240 action,
241 className = "",
242 children,
243}: {
244 title: string;
245 action?: ReactNode;
246 className?: string;
247 children: ReactNode;
248}) {
Agents as a team: lifecycle, merge queue, billing and a new shell249 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look250 <section className={className}>
A Vercel-like sidebar and a new mark251 <div className="mb-1 flex h-6 items-center justify-between px-2">
252 <h2 className="text-xs font-medium text-faint">{title}</h2>
Agents as a team: lifecycle, merge queue, billing and a new shell253 {action}
254 </div>
255 <div className="space-y-px">{children}</div>
256 </section>
257 );
258}
259
Workspace names and icons, and a component kit for every control260/** What a workspace is called where people read it: its display name, else its slug. */
261function displayName(membership: Membership): string {
262 return membership.name?.trim() || membership.slug;
263}
264
Agents as a team: lifecycle, merge queue, billing and a new shell265function WorkspaceSwitcher({ user, shell }: { user: User; shell: ShellData }) {
Workspace names and icons, and a component kit for every control266 const workspace = shell.workspace;
267 const current = workspace?.slug;
268 const label = workspace ? displayName(workspace) : "Choose a workspace";
Agents as a team: lifecycle, merge queue, billing and a new shell269 return (
270 <DropdownMenu>
Workspace names and icons, and a component kit for every control271 {/* The name goes to the workspace; only the arrows switch it. The
272 name gets all the room there is, and the whole of it on hover. */}
Merge branch 'worktree-agent-ae1299e92e4462012'273 <Hint label={workspace ? `${label} · g1t.sh/${workspace.slug}` : undefined}>
A Vercel-like sidebar and a new mark274 <Link
275 to={current ? `/${current}` : "/workspaces/new"}
Fast pages, required checks on the branch, self-hosted runners, honest incidents276 prefetch="intent"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look277 className="flex h-9 min-w-0 grow items-center gap-2 rounded-md px-2 transition-colors hover:bg-raised"
A Vercel-like sidebar and a new mark278 >
Workspace names and icons, and a component kit for every control279 {workspace ? (
280 <Avatar name={workspace.slug} image={workspace.avatar} size={20} square />
Agents as a team: lifecycle, merge queue, billing and a new shell281 ) : (
Workspace names and icons, and a component kit for every control282 <span className="size-5 shrink-0 rounded-md border border-dashed border-line-strong" />
Agents as a team: lifecycle, merge queue, billing and a new shell283 )}
Workspace names and icons, and a component kit for every control284 <span className="min-w-0 truncate text-[0.8125rem] font-medium">{label}</span>
A Vercel-like sidebar and a new mark285 </Link>
Merge branch 'worktree-agent-ae1299e92e4462012'286 </Hint>
A Vercel-like sidebar and a new mark287 <DropdownMenuTrigger
288 aria-label="Switch workspace"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit289 className="flex h-9 w-7 shrink-0 items-center justify-center rounded-md text-faint outline-none transition-colors hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg"
A Vercel-like sidebar and a new mark290 >
291 <ChevronsUpDown size={14} />
Agents as a team: lifecycle, merge queue, billing and a new shell292 </DropdownMenuTrigger>
Workspace names and icons, and a component kit for every control293 <DropdownMenuContent align="start" className="w-64">
Navigation: the workspace's own page with tabs, pins and recent in the sidebar294 {workspace && (
295 <>
296 {/* The workspace's own page: its projects, packages and people. */}
297 <DropdownMenuItem asChild>
298 <Link to={`/${workspace.slug}`}>
299 <LayoutGrid />
300 Workspace overview
301 </Link>
302 </DropdownMenuItem>
303 <DropdownMenuItem asChild>
304 <Link to={`/${workspace.slug}/-/projects`}>
305 <Box />
306 All projects
307 </Link>
308 </DropdownMenuItem>
309 <DropdownMenuSeparator />
310 </>
311 )}
Agents as a team: lifecycle, merge queue, billing and a new shell312 <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
313 {(user.workspaces ?? []).map((membership) => (
Merge branch 'worktree-agent-ae1299e92e4462012'314 <Hint key={membership.slug} label={displayName(membership)} side="right">
315 <DropdownMenuItem asChild>
316 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control317 <Avatar name={membership.slug} image={membership.avatar} size={24} square />
318 <span className="flex min-w-0 grow flex-col leading-tight">
319 <span className="truncate">{displayName(membership)}</span>
320 <span className="truncate font-mono text-[0.6875rem] text-faint">{membership.slug}</span>
321 </span>
322 <span className="shrink-0 text-[0.6875rem] text-faint capitalize">{membership.role}</span>
323 {membership.slug === current ? (
324 <Check className="shrink-0 text-accent" />
325 ) : (
326 <span className="size-4 shrink-0" aria-hidden="true" />
327 )}
Agents as a team: lifecycle, merge queue, billing and a new shell328 </Link>
329 </DropdownMenuItem>
Merge branch 'worktree-agent-ae1299e92e4462012'330 </Hint>
Agents as a team: lifecycle, merge queue, billing and a new shell331 ))}
332 <DropdownMenuSeparator />
333 <DropdownMenuItem asChild>
334 <Link to="/workspaces/new">
335 <Plus />
336 New workspace
337 </Link>
338 </DropdownMenuItem>
339 </DropdownMenuContent>
340 </DropdownMenu>
A Vercel-like sidebar and a new mark341 );
342}
343
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas344/**
345 * The fine print, as one quiet row at the foot of the account menu and the
346 * visitor's panel: each link a full-height target, left-aligned. In the
347 * menu each is a menu item, so the arrow keys reach it.
348 */
349const LEGAL_LINKS: [string, string][] = [
350 ["Policies", "/policies"],
351 ["Privacy", "/policies/privacy"],
352 ["Security", "/security"],
353];
354const LEGAL_LINK = "flex h-7 items-center rounded px-1.5 text-xs text-faint outline-none transition-colors";
355
356function MenuLegalRow() {
357 return (
358 <div role="group" aria-label="Legal" className="flex items-center gap-0.5 px-0.5 pt-0.5">
359 {LEGAL_LINKS.map(([label, to], index) => (
360 <span key={to} className="flex items-center gap-0.5">
361 {index > 0 && (
362 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
363 ·
364 </span>
365 )}
366 <DropdownMenuItem asChild className={`${LEGAL_LINK} py-0 data-highlighted:bg-line data-highlighted:text-fg`}>
367 <Link to={to}>{label}</Link>
368 </DropdownMenuItem>
369 </span>
370 ))}
371 </div>
372 );
373}
374
375/** Status's row: the live dot and two or three words, fetched when the menu opens. */
376function StatusSummary({ open }: { open: boolean }) {
377 const status = useSiteStatus(open);
378 const words = statusTitle(status);
379 return (
380 <span className="ml-auto flex min-w-0 items-center gap-1.5 pl-2 text-xs text-faint">
381 {/* The words say it; the dot's own words for screen readers would repeat them. */}
382 <span aria-hidden="true" className="flex">
383 <StatusDot state={status?.overall.state ?? null} />
384 </span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents385 {words ? <span className="truncate">{words}</span> : <Skeleton className="h-3 w-20" />}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas386 </span>
387 );
388}
389
390/** A menu row: an icon, words, and whatever sits at its end. */
391const MENU_ROW = "h-9 gap-2.5 px-2.5 text-[0.8125rem]";
392
Chat and workspace agents: channels, DMs and named agents you talk to393function AccountMenu({ user, rail = false }: { user: User; rail?: boolean }) {
A Vercel-like sidebar and a new mark394 const submit = useSubmit();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas395 const [open, setOpen] = useState(false);
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 // Name, primary address and invites left: asked for once, as soon as the
397 // pointer or focus reaches the button, so they are there when it opens.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas398 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
Fast pages, required checks on the branch, self-hosted runners, honest incidents399 const prefetch = () => {
400 if (details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
401 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas402 useEffect(() => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents403 if (open) prefetch();
404 // eslint-disable-next-line react-hooks/exhaustive-deps
405 }, [open]);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas406 const me = details.data ?? null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents407 // Still on its way: shapes where the words will be, the same size.
408 const loading = details.data === undefined && details.state !== "idle";
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)409 // While anyone can sign up, listed once the data says there are invites to look back on.
410 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 quotas411 const profile = `/u/${user.username}`;
A Vercel-like sidebar and a new mark412 return (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas413 <DropdownMenu open={open} onOpenChange={setOpen}>
Chat and workspace agents: channels, DMs and named agents you talk to414 {rail ? (
415 // On the rail: the avatar alone, with a dot that says you are here.
416 <DropdownMenuTrigger
417 aria-label={`Account menu for ${user.username}`}
418 onPointerEnter={prefetch}
419 onFocus={prefetch}
420 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"
421 >
422 <Avatar name={user.username} image={user.avatar} size={34} />
423 <span aria-hidden="true" className="absolute -right-0.5 -bottom-0.5 size-3 rounded-full bg-success ring-[2.5px] ring-[#0b0b0d]" />
424 </DropdownMenuTrigger>
425 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas426 <DropdownMenuTrigger
427 aria-label={`Account menu for ${user.username}`}
Fast pages, required checks on the branch, self-hosted runners, honest incidents428 onPointerEnter={prefetch}
429 onFocus={prefetch}
Merge branch 'worktree-agent-ad7c6d88d93adc817'430 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 quotas431 >
Workspace names and icons, and a component kit for every control432 <Avatar name={user.username} image={user.avatar} size={22} />
A Vercel-like sidebar and a new mark433 <span className="min-w-0 grow truncate text-[0.8125rem] font-medium">{user.username}</span>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas434 <ChevronsUpDown size={14} className="shrink-0 text-faint" />
A Vercel-like sidebar and a new mark435 </DropdownMenuTrigger>
Chat and workspace agents: channels, DMs and named agents you talk to436 )}
437 <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 quotas438 {/* Who is signed in, and a way to their profile. */}
439 <DropdownMenuItem asChild className="gap-3 px-2 py-2">
440 <Link to={profile} aria-label={`${me?.name ?? user.username} (@${user.username}), your profile`}>
441 <Avatar name={user.username} image={user.avatar} size={36} />
Fast pages, required checks on the branch, self-hosted runners, honest incidents442 <span className="flex min-w-0 grow flex-col leading-tight" aria-busy={loading}>
443 {loading ? (
444 <Skeleton className="my-[0.1875rem] h-3.5 w-28" />
445 ) : (
446 <span className="truncate text-sm font-medium text-fg">{me?.name ?? user.username}</span>
447 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas448 <span className="truncate font-mono text-xs text-muted">@{user.username}</span>
Fast pages, required checks on the branch, self-hosted runners, honest incidents449 {loading ? (
450 <Skeleton className="mt-1 h-3 w-40" />
451 ) : (
452 me?.email && <span className="mt-0.5 truncate text-xs text-faint">{me.email}</span>
453 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas454 </span>
A Vercel-like sidebar and a new mark455 </Link>
456 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas457 <DropdownMenuSeparator className="my-1.5" />
458 <DropdownMenuGroup>
459 <DropdownMenuItem asChild className={MENU_ROW}>
460 <Link to={profile}>
461 <CircleUserRound />
462 Your profile
463 </Link>
464 </DropdownMenuItem>
465 <DropdownMenuItem asChild className={MENU_ROW}>
466 <Link to={FIRST_SETTINGS_PAGE}>
467 <Settings />
468 Your settings
469 </Link>
470 </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)471 {invitesListed && (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas472 <DropdownMenuItem asChild className={MENU_ROW}>
473 <Link to="/settings/invites">
474 <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)475 {ACCOUNT_SETTINGS.invites.title}
Fast pages, required checks on the branch, self-hosted runners, honest incidents476 {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 quotas477 {me?.invites_left != null && (
478 <span className="ml-auto rounded-full bg-line px-1.5 text-[0.6875rem] tabular-nums text-muted">
479 {me.invites_left} left
480 </span>
481 )}
482 </Link>
483 </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)484 )}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas485 </DropdownMenuGroup>
486 <DropdownMenuSeparator className="my-1.5" />
487 <DropdownMenuGroup>
488 <DropdownMenuItem asChild className={MENU_ROW}>
489 <a href="https://docs.g1t.sh/">
490 <BookOpen />
491 Documentation
492 </a>
493 </DropdownMenuItem>
494 <DropdownMenuItem asChild className={MENU_ROW}>
495 <Link to="/support">
496 <LifeBuoy />
497 Support
498 </Link>
499 </DropdownMenuItem>
500 <DropdownMenuItem asChild className={MENU_ROW}>
501 <a href={STATUS_URL}>
502 <Activity />
503 Status
504 <StatusSummary open={open} />
505 </a>
506 </DropdownMenuItem>
507 </DropdownMenuGroup>
508 <DropdownMenuSeparator className="my-1.5" />
A Vercel-like sidebar and a new mark509 {/* Submitted from here: the menu closes on select, and a button
510 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 quotas511 <DropdownMenuItem className={MENU_ROW} onSelect={() => submit(null, { method: "post", action: "/logout" })}>
A Vercel-like sidebar and a new mark512 <LogOut />
513 Sign out
514 </DropdownMenuItem>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas515 <DropdownMenuSeparator className="my-1.5" />
516 <MenuLegalRow />
A Vercel-like sidebar and a new mark517 </DropdownMenuContent>
518 </DropdownMenu>
519 );
520}
521
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look522/**
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar523 * A workspace's settings pages, which the sidebar drills into: how it is
524 * set up and connected (guardrails, secrets, runners, integrations,
525 * webhooks), what it pays, its repositories, tokens and record. The main
526 * list keeps the places work happens and who belongs; every member can
527 * still open these.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look528 */
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar529const SETTINGS_PAGE =
Merge branch 'worktree-agent-ad7c6d88d93adc817'530 new RegExp(`^/([^/]+)/-/(${SETTINGS_PAGES.join("|")})(/|$)`);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look531/** A project's settings pages, which the project's menu drills into. */
Project settings move into the sidebar, grouped, with a heading per page532const REPO_SETTINGS_PAGE = /^\/([^/]+)\/([^/-][^/]*)\/settings(\/|$)/;
A catalogue of model providers, and settings that feel like settings533
Polish: phones, copy boxes, the plan page, the landing page, a real glide534/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look535 * The sidebar's lists are a stack, the way a phone pushes a screen: one
536 * drilled into slides in from the right over the full width, while the one
537 * it covers drifts a quarter of the way left and fades, both on one long
Sidebar: the panels really slide538 * ease-out, the fade quicker than the move so the two never blur together.
539 * Going back reverses it.
Polish: phones, copy boxes, the plan page, the landing page, a real glide540 */
Sidebar: no settings flash on the way into a repository, and a better slide541const LAYER =
Sidebar: the panels really slide542 "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 look543/** One list, filling its layer. */
Sidebar: no settings flash on the way into a repository, and a better slide544const PANEL = "h-full overflow-y-auto px-2 pb-4";
A catalogue of model providers, and settings that feel like settings545
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look546/** One list in the stack: what it is, and its rows. */
547type Level = { key: string; node: ReactNode };
548
549/** True once the page has hydrated, so what the server drew never animates in. */
550function useHydrated(): boolean {
551 const [hydrated, setHydrated] = useState(false);
552 useEffect(() => setHydrated(true), []);
553 return hydrated;
554}
555
556/**
557 * Drilling in and out of the sidebar's lists. `trail` is the way from the
558 * main list to the one shown, worked out from the address, so a link
559 * straight to a settings page opens the sidebar already drilled in, and
560 * leaving it comes back out. Lists deeper than the one shown stay in
561 * place, off to the right, so going back slides them away rather than
562 * dropping them. Moving to a list at the same depth (a workspace's settings
563 * to a project) fades the new one in where it is.
564 */
565function Drill({ trail }: { trail: Level[] }) {
566 const depth = trail.length - 1;
567 const hydrated = useHydrated();
568 const stack = useRef<Level[]>(trail);
569 const before = stack.current;
570 const within = trail.every((level, index) => before[index]?.key === level.key);
571 stack.current = within ? [...trail, ...before.slice(trail.length)] : trail;
572 const levels = stack.current;
573 // What was showing last time, to tell drilling in from moving across.
574 const shown = useRef({ depth, keys: levels.map((level) => level.key) });
575 useEffect(() => {
576 shown.current = { depth, keys: levels.map((level) => level.key) };
577 });
A catalogue of model providers, and settings that feel like settings578 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look579 <div className="relative min-h-0 grow overflow-hidden">
580 {levels.map((level, index) => {
581 const current = index === depth;
582 const place =
583 index < depth
584 ? "pointer-events-none -translate-x-1/4 opacity-0"
585 : current
586 ? "translate-x-0 opacity-100"
587 : "pointer-events-none translate-x-full opacity-0";
588 // A list that was not there before arrives: from the right when
589 // drilling in, in place when moving across.
590 const arriving = hydrated && current && !shown.current.keys.includes(level.key);
591 const enter = arriving
592 ? depth > shown.current.depth
593 ? "starting:translate-x-full starting:opacity-0"
594 : "starting:opacity-0"
595 : "";
596 return (
597 <div key={level.key} className={`${LAYER} ${place} ${enter}`} inert={!current} aria-hidden={!current || undefined}>
598 {level.node}
599 </div>
600 );
601 })}
602 </div>
603 );
604}
605
606/**
607 * The row at the top of a list drilled into: the list's name, which goes
608 * back out, and what it belongs to, faint at the end.
609 */
610function BackRow({ to, label, context }: { to: string; label: ReactNode; context?: ReactNode }) {
611 return (
612 <Link
613 to={to}
614 prefetch="intent"
615 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 settings616 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look617 <ChevronLeft size={16} className="shrink-0 text-faint transition-transform group-hover:-translate-x-0.5 group-hover:text-muted" />
618 <span className="min-w-0 shrink-0 truncate">{label}</span>
619 {context && (
620 <span className="ml-auto min-w-0 truncate pl-2 font-mono text-[0.6875rem] font-normal text-faint">{context}</span>
621 )}
622 </Link>
623 );
624}
625
626/** A thin rule between groups of a list. */
627function Rule() {
628 return <div role="separator" className="mx-2 my-2.5 h-px bg-line" />;
629}
630
Navigation: the workspace's own page with tabs, pins and recent in the sidebar631/** A quiet heading inside a group: Pinned, Recent. */
632function SidebarSubhead({ children }: { children: ReactNode }) {
633 return <h3 className="px-2 pt-1.5 pb-0.5 text-[0.6875rem] font-medium tracking-wide text-faint">{children}</h3>;
634}
635
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look636/**
Navigation: the workspace's own page with tabs, pins and recent in the sidebar637 * The workspace's projects as the sidebar keeps them, however many there
638 * are: the person's pins in their order, what they opened last, and the
639 * way to all of them. Pins move by dragging, or with Alt and the arrow
640 * keys; the projects service keeps the order.
641 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'642function SidebarProjects({ slug, shell, current }: { slug: string; shell: ShellData; current: boolean }) {
Navigation: the workspace's own page with tabs, pins and recent in the sidebar643 const reorder = useFetcher({ key: `pins:${slug}` });
644 const saved = shell.pinned ?? [];
645 // While a new order is on its way, it shows as made.
646 const asked = reorder.formData?.get("intent") === "reorder" ? reorder.formData.getAll("slug").map(String) : null;
647 const pinned = asked
648 ? asked.map((name) => saved.find((project) => project.name === name)).filter((project): project is ShortcutProject => project != null)
649 : saved;
650 const active = shell.repo ? { namespace: shell.repo.namespace, name: shell.repo.name, isPrivate: false } : null;
651 const recent = recentWith(shell.recent ?? [], pinned, active, slug);
652 const [dragging, setDragging] = useState<number | null>(null);
653 const move = (from: number, by: number) => {
654 const next = movedPin(pinned, from, by);
655 if (!next) return;
656 const form = new FormData();
657 form.set("intent", "reorder");
658 for (const project of next) form.append("slug", project.name);
659 reorder.submit(form, { method: "post", action: `/${slug}/-/pins` });
660 };
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar661 // Each row pins or unpins in place: the pin shows on hover or focus, and
662 // stays shown on a pinned row's hover so it reads as "unpin".
663 const row = (project: ShortcutProject, isPinned: boolean) => (
664 <div className="group/row relative">
665 <SidebarLink to={`/${project.namespace}/${project.name}`} icon={project.isPrivate ? <Lock size={15} /> : <Box size={15} />} drill="hover">
666 <span className="block truncate pr-6">{project.title ?? project.name}</span>
667 </SidebarLink>
668 <PinButton
669 workspace={slug}
670 slug={project.name}
671 name={project.title ?? project.name}
672 pinned={isPinned}
673 small
674 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"
675 />
676 </div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar677 );
678 return (
679 <SidebarGroup
680 title="Projects"
681 action={
682 <Link to={`/new?workspace=${slug}`} aria-label="New project" className="rounded p-0.5 text-faint hover:bg-raised hover:text-fg">
683 <Plus size={13} />
684 </Link>
685 }
686 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar687 {/* Only there once something is pinned: each row below has its own pin. */}
688 {pinned.length > 0 && (
689 <>
690 <SidebarSubhead>Pinned</SidebarSubhead>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar691 <ul aria-label="Pinned projects" className="space-y-px">
692 {pinned.map((project, index) => (
693 <li
694 key={project.name}
695 draggable={pinned.length > 1}
696 onDragStart={(event) => {
697 setDragging(index);
698 event.dataTransfer.effectAllowed = "move";
699 }}
700 onDragOver={(event) => {
701 if (dragging != null) event.preventDefault();
702 }}
703 onDrop={(event) => {
704 event.preventDefault();
705 if (dragging != null && dragging !== index) move(dragging, index - dragging);
706 setDragging(null);
707 }}
708 onDragEnd={() => setDragging(null)}
709 onKeyDown={(event) => {
710 if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
711 event.preventDefault();
712 move(index, event.key === "ArrowUp" ? -1 : 1);
713 }}
714 className={`group/pin relative ${dragging === index ? "opacity-50" : ""}`}
715 >
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar716 {row(project, true)}
Navigation: the workspace's own page with tabs, pins and recent in the sidebar717 {pinned.length > 1 && (
Merge branch 'worktree-agent-ae1299e92e4462012'718 <Hint label="Drag, or Alt and an arrow key, to reorder" side="right">
719 <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">
720 <GripVertical size={12} aria-hidden="true" />
721 </span>
722 </Hint>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar723 )}
724 </li>
725 ))}
726 </ul>
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar727 </>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar728 )}
729 {recent.length > 0 && (
730 <>
731 <SidebarSubhead>Recent</SidebarSubhead>
732 {recent.map((project) => (
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar733 <div key={`${project.namespace}/${project.name}`}>{row(project, false)}</div>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar734 ))}
735 </>
736 )}
737 <div className="pt-1">
Merge branch 'worktree-agent-ad7c6d88d93adc817'738 <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 sidebar739 All projects
740 </SidebarLink>
741 </div>
742 </SidebarGroup>
743 );
744}
745
746/**
Merge branch 'worktree-agent-ad7c6d88d93adc817'747 * The main list, in two parts a rule apart. Above it, what is yours
748 * whichever workspace you are in: Mission control, your inbox, and what
749 * others have shared with you. Below it, under the workspace's name, the
750 * workspace: its overview, its projects, what it builds and runs with
751 * across them, then its people, usage, support and settings. One row is
752 * lit wherever you are (lib/workspace-nav.ts).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look753 */
754function MainMenu({ user, shell }: { user: User | null; shell: ShellData }) {
755 const ws = shell.workspace;
Merge branch 'worktree-agent-ad7c6d88d93adc817'756 const { pathname } = useLocation();
757 const going = useNavigation().location?.pathname;
758 // The row lights as the link is followed, not once the page arrives.
759 const here = sidebarCurrent(going ?? pathname, ws?.slug ?? null);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look760 // A visitor browses: no workspace, no projects of their own.
761 if (!user) {
762 return (
763 <nav aria-label="g1t" className={PANEL}>
764 <div className="mt-3 space-y-px">
765 {VISITOR_LINKS.map((link) => (
766 <SidebarLink key={link.to} to={link.to} icon={link.to === "/search" ? <Search size={15} /> : <Compass size={15} />}>
767 {link.label}
768 </SidebarLink>
769 ))}
770 </div>
771 </nav>
772 );
773 }
Merge branch 'worktree-agent-ad7c6d88d93adc817'774 const shared = shell.shared ?? [];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look775 return (
776 <nav aria-label="g1t" className={PANEL}>
Merge branch 'worktree-agent-ad7c6d88d93adc817'777 {/* 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 look778 <div className="mt-3 space-y-px">
Merge branch 'worktree-agent-ad7c6d88d93adc817'779 <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 look780 Mission control
781 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'782 <SidebarLink to="/inbox" icon={<Inbox size={15} />} count={shell.inbox?.unread ?? undefined} current={here === "inbox"}>
783 Inbox
784 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look785 </div>
Merge branch 'worktree-agent-ad7c6d88d93adc817'786 {shared.length > 0 && (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look787 <SidebarGroup title="Shared with you" className="mt-3">
Merge branch 'worktree-agent-ad7c6d88d93adc817'788 {shared.map((repo) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look789 <SidebarLink
790 key={`${repo.namespace}/${repo.name}`}
791 to={`/${repo.namespace}/${repo.name}`}
792 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
793 drill="hover"
794 >
795 <span className="font-mono text-faint">{repo.namespace}/</span>
796 {repo.name}
797 </SidebarLink>
798 ))}
799 </SidebarGroup>
800 )}
801
Merge branch 'worktree-agent-ad7c6d88d93adc817'802 <Rule />
803 {ws ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look804 <>
Merge branch 'worktree-agent-ad7c6d88d93adc817'805 {/* The workspace the switcher names: its own page first. */}
806 <SidebarGroup title={displayName(ws)}>
807 <SidebarLink to={`/${ws.slug}`} end icon={<LayoutDashboard size={15} />} current={here === "overview"}>
808 Overview
809 </SidebarLink>
810 </SidebarGroup>
811 <div className="mt-3">
812 <SidebarProjects slug={ws.slug} shell={shell} current={here === "projects"} />
813 </div>
814
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member815 <Rule />
Merge branch 'worktree-agent-ad7c6d88d93adc817'816 <div className="space-y-px">
817 <SidebarLink to={`/${ws.slug}/-/agents`} icon={<Bot size={15} />} current={here === "agents"}>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look818 Agent fleet
819 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'820 <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 look821 Context
822 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'823 <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 look824 Memory
825 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'826 <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 look827 Security
Fast pages, required checks on the branch, self-hosted runners, honest incidents828 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'829 <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 member830 Packages
831 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'832 {INSIGHTS && (
833 <SidebarSoonLink
834 to={`/${ws.slug}/-/insights`}
835 icon={<TrendingUp size={15} />}
836 about={INSIGHTS.summary}
837 current={here === "insights"}
838 >
839 {INSIGHTS.title}
840 </SidebarSoonLink>
841 )}
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar842 {roadmapIn("Workspace").filter((item) => item.key !== "insights").map((item) => (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look843 <SidebarSoonLink
844 key={item.key}
845 to={`/${ws.slug}/-/soon/${item.key}`}
846 icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />}
847 about={item.summary}
848 >
849 {item.title === "Board" ? "Boards" : item.title}
850 </SidebarSoonLink>
851 ))}
Merge branch 'worktree-agent-ad7c6d88d93adc817'852 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look853
854 <Rule />
855 <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 bar856 {/* Who belongs, for every member to see; owners invite and manage there. */}
Merge branch 'worktree-agent-ad7c6d88d93adc817'857 <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 sidebar858 People
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar859 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'860 <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 bar861 Teams
862 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'863 <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 look864 Usage
865 </SidebarLink>
Merge branch 'worktree-agent-ad7c6d88d93adc817'866 <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 look867 Support
868 </SidebarLink>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar869 {/* 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 look870 <SidebarLink
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member871 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 look872 icon={<Settings size={15} />}
873 drill
Merge branch 'worktree-agent-ad7c6d88d93adc817'874 current={here === "settings"}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look875 >
876 Settings
877 </SidebarLink>
878 </div>
879 </>
Merge branch 'worktree-agent-ad7c6d88d93adc817'880 ) : (
881 <SidebarGroup title="Projects">
882 <p className="px-2 py-1 text-xs text-faint">None yet.</p>
883 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look884 )}
885 </nav>
886 );
887}
888
Merge branch 'worktree-agent-ad7c6d88d93adc817'889/** Insights, coming: the workspace's own page says what it will be. */
890const INSIGHTS = roadmapItem("insights");
891
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look892/** A workspace's settings, drilled into from Settings in the main list. */
893function SettingsMenu({ slug, owner }: { slug: string; owner: boolean }) {
894 return (
895 <nav aria-label="Workspace settings" className={PANEL}>
896 <BackRow to={`/${slug}`} label="Settings" context={slug} />
897 <div className="mt-2 space-y-px">
A catalogue of model providers, and settings that feel like settings898 {owner && (
899 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
900 General
901 </SidebarLink>
902 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look903 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
904 Repositories
905 </SidebarLink>
Secrets and variables: one list, rows per environment, for workflows and deployments906 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
907 Access tokens
908 </SidebarLink>
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules909 {owner && (
910 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
911 Personal access tokens
912 </SidebarLink>
913 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge914 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />}>
915 Rules
916 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look917 </div>
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar918 <SidebarGroup title="Agents and runs" className="mt-3">
919 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />}>
920 Guardrails
921 </SidebarLink>
922 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
923 Secrets and variables
924 </SidebarLink>
Merge branch 'worktree-agent-a3abfcce648e87dca'925 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
926 Actions
927 </SidebarLink>
Runners are owner-only: the page, its actions, the Settings entry and the links to it928 {owner && (
929 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
930 Runners
931 </SidebarLink>
932 )}
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar933 </SidebarGroup>
934 <SidebarGroup title="Connections" className="mt-3">
935 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />}>
936 Integrations
937 </SidebarLink>
938 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
939 Webhooks
940 </SidebarLink>
941 </SidebarGroup>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look942 <Rule />
943 <div className="space-y-px">
A catalogue of model providers, and settings that feel like settings944 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />}>
Secrets and variables: one list, rows per environment, for workflows and deployments945 Billing and plans
A catalogue of model providers, and settings that feel like settings946 </SidebarLink>
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API947 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />}>
948 Audit log
949 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look950 </div>
A catalogue of model providers, and settings that feel like settings951 </nav>
952 );
953}
954
Sidebar: no settings flash on the way into a repository, and a better slide955/**
956 * What the repository menu needs. Known from the address before the page's
957 * data arrives, so the menu is right from the first frame; the counts fill
958 * in once it does.
959 */
960type MenuRepo = {
961 namespace: string;
962 name: string;
963 member: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look964 /** Whether they see its settings; from the repository's page once it loads. */
965 settings?: boolean;
966 /** What they may do there, once the repository's page loads. */
967 can?: Abilities;
Sidebar: no settings flash on the way into a repository, and a better slide968 issues?: number;
969 pulls?: number;
970};
971
972function sameRepo(a: { namespace: string; name: string } | null, b: { namespace: string; name: string } | null) {
973 return (
974 a != null &&
975 b != null &&
976 a.namespace.toLowerCase() === b.namespace.toLowerCase() &&
977 a.name.toLowerCase() === b.name.toLowerCase()
978 );
979}
A repository has its own sidebar, as settings do980
981/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look982 * A project's own list, drilled into while you are in it: everything about
983 * the project, running and its code, and nothing else, with the way back
984 * out to all of them.
A repository has its own sidebar, as settings do985 */
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put986function RepoMenu({
987 repo,
988 isPrivate,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look989 back,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put990}: {
991 repo: MenuRepo;
992 isPrivate: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look993 /** Where the way back leads: the workspace's projects, mission control, or Explore. */
994 back: { to: string; label: string };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put995}) {
A repository has its own sidebar, as settings do996 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 put997 // 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 look998 const shows = new Set(projectPages(repo.member, repo.can));
A repository has its own sidebar, as settings do999 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1000 <nav aria-label={`${repo.namespace}/${repo.name}`} className={PANEL}>
1001 <BackRow to={back.to} label={back.label} />
Merge branch 'worktree-agent-ae1299e92e4462012'1002 <Hint label="Overview" side="right">
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1003 <div>
A flat project menu with Code first, views as tabs, and Stripe for every owner1004 <NavLink
A repository has its own sidebar, as settings do1005 to={base}
A flat project menu with Code first, views as tabs, and Stripe for every owner1006 end
Fast pages, required checks on the branch, self-hosted runners, honest incidents1007 prefetch="intent"
A flat project menu with Code first, views as tabs, and Stripe for every owner1008 className={({ isActive }) =>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1009 `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 owner1010 }
A repository has its own sidebar, as settings do1011 >
1012 <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 page1013 {isPrivate ? <Lock size={13} /> : <Box size={13} />}
A repository has its own sidebar, as settings do1014 </span>
1015 <span className="min-w-0 truncate font-mono text-[0.8125rem]">
1016 <span className="text-faint">{repo.namespace}/</span>
1017 <span className="font-semibold text-fg">{repo.name}</span>
1018 </span>
A flat project menu with Code first, views as tabs, and Stripe for every owner1019 </NavLink>
Sidebar: soon rows and the project row keep their styles inside a hint; tiny amounts read as zero1020 </div>
Merge branch 'worktree-agent-ae1299e92e4462012'1021 </Hint>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1022 {/* A project's pages, in the order people use them. A page with more
1023 than one view shows them as tabs across its top. */}
1024 <Rule />
1025 <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 request1026 <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 owner1027 Code
1028 </SidebarLink>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1029 <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 owner1030 Issues
1031 </SidebarLink>
1032 <SidebarLink to={`${base}/pulls`} also={[`${base}/pull`, `${base}/queue`]} icon={<GitPullRequest size={15} />} count={repo.pulls}>
1033 Pull requests
1034 </SidebarLink>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1035 <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 owner1036 Agents
Agents and memory, checks and conflicts, profiles, slug renames, custom domains1037 </SidebarLink>
A flat project menu with Code first, views as tabs, and Stripe for every owner1038 <SidebarLink to={`${base}/actions`} icon={<PlayCircle size={15} />}>
1039 Workflows
Projects: what a workspace builds and runs, first on every page1040 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1041 </div>
1042 <Rule />
1043 <div className="space-y-px">
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1044 {shows.has("deployments") ? (
A flat project menu with Code first, views as tabs, and Stripe for every owner1045 <SidebarLink to={`${base}/deployments`} also={soonPaths(base, "Deployments")} icon={<Rocket size={15} />}>
1046 Deployments
1047 </SidebarLink>
1048 ) : null}
Observability gets its own page; tabs never overflow1049 <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.">
1050 Observability
A flat project menu with Code first, views as tabs, and Stripe for every owner1051 </SidebarSoonLink>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1052 {shows.has("security") ? (
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1053 <SidebarLink to={`${base}/security`} also={soonPaths(base, "Security")} icon={<ShieldCheck size={15} />}>
1054 Security
1055 </SidebarLink>
1056 ) : null}
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971057 <SidebarLink
1058 to={`${base}/contributors`}
1059 also={[`${base}/activity`, `${base}/stargazers`, ...soonPaths(base, "Insights")]}
1060 icon={<BarChart3 size={15} />}
1061 >
A flat project menu with Code first, views as tabs, and Stripe for every owner1062 Insights
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971063 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1064 </div>
1065 {(repo.settings ?? shows.has("settings")) && (
1066 <>
1067 <Rule />
1068 <SidebarLink to={`${base}/settings`} icon={<Settings size={15} />} drill>
A flat project menu with Code first, views as tabs, and Stripe for every owner1069 Settings
1070 </SidebarLink>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1071 </>
1072 )}
A repository has its own sidebar, as settings do1073 </nav>
1074 );
1075}
1076
Project settings move into the sidebar, grouped, with a heading per page1077/**
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1078 * A project's settings, drilled into from its list: what is about running
1079 * it, then its agents, then its code and who can reach it. The way back
1080 * leads to the project.
Project settings move into the sidebar, grouped, with a heading per page1081 */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1082function RepoSettingsMenu({ repo }: { repo: MenuRepo }) {
Project settings move into the sidebar, grouped, with a heading per page1083 const base = `/${repo.namespace}/${repo.name}`;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1084 // Each page shows to the roles that can use it, once the role is known.
1085 const shows = (page: string) => !repo.can || repo.can[SETTINGS_CAPABILITY[page] ?? "administer"];
1086 const running = shows("deployments");
1087 const agents = shows("agents") || shows("guardrails");
Project settings move into the sidebar, grouped, with a heading per page1088 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1089 <nav aria-label={`${repo.namespace}/${repo.name} settings`} className={PANEL}>
1090 <BackRow to={base} label="Settings" context={`${repo.namespace}/${repo.name}`} />
1091 {shows("") && (
1092 <div className="mt-2 space-y-px">
1093 <SidebarLink to={`${base}/settings`} end icon={<Settings size={15} />}>
1094 General
1095 </SidebarLink>
1096 </div>
1097 )}
1098 {running && (
1099 <>
1100 {shows("") && <Rule />}
1101 <div className={shows("") ? "space-y-px" : "mt-2 space-y-px"}>
1102 <SidebarLink to={`${base}/settings/deployments`} icon={<Rocket size={15} />}>
1103 Deployments
1104 </SidebarLink>
1105 <SidebarLink to={`${base}/settings/domains`} icon={<Globe size={15} />}>
1106 Domains
1107 </SidebarLink>
1108 <SidebarLink to={`${base}/settings/dependencies`} icon={<Network size={15} />}>
1109 Dependencies
1110 </SidebarLink>
1111 </div>
1112 </>
1113 )}
1114 {agents && (
1115 <>
1116 <Rule />
1117 <div className="space-y-px">
1118 {shows("agents") && (
1119 <SidebarLink to={`${base}/settings/agents`} icon={<Bot size={15} />}>
1120 Agents
1121 </SidebarLink>
1122 )}
1123 {shows("guardrails") && (
1124 <SidebarLink to={`${base}/settings/guardrails`} icon={<ShieldCheck size={15} />}>
1125 Guardrails
1126 </SidebarLink>
1127 )}
1128 </div>
1129 </>
1130 )}
1131 <Rule />
1132 <div className="space-y-px">
1133 {shows("repository") && (
1134 <SidebarLink to={`${base}/settings/repository`} icon={<BookMarked size={15} />}>
1135 Repository
1136 </SidebarLink>
1137 )}
1138 {shows("access") && (
1139 <SidebarLink to={`${base}/settings/access`} icon={<Users size={15} />}>
1140 Access
1141 </SidebarLink>
1142 )}
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1143 {shows("keys") && (
1144 <SidebarLink to={`${base}/settings/keys`} icon={<KeyRound size={15} />}>
1145 Deploy keys
1146 </SidebarLink>
1147 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1148 {shows("branches") && (
1149 <SidebarLink to={`${base}/settings/branches`} icon={<GitBranch size={15} />}>
1150 Branches and merging
1151 </SidebarLink>
1152 )}
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1153 {shows("rules") && (
1154 <SidebarLink to={`${base}/settings/rules`} icon={<Scale size={15} />}>
1155 Rules
1156 </SidebarLink>
1157 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1158 {shows("secrets") && (
1159 <SidebarLink to={`${base}/settings/secrets`} icon={<Lock size={15} />}>
1160 Secrets and variables
1161 </SidebarLink>
1162 )}
Merge branch 'worktree-agent-a3abfcce648e87dca'1163 {shows("actions") && (
1164 <SidebarLink to={`${base}/settings/actions`} icon={<PlayCircle size={15} />}>
1165 Actions
1166 </SidebarLink>
1167 )}
1168 {shows("environments") && (
1169 <SidebarLink to={`${base}/settings/environments`} icon={<Layers size={15} />}>
1170 Environments
1171 </SidebarLink>
1172 )}
Fast pages, required checks on the branch, self-hosted runners, honest incidents1173 {shows("runners") && (
1174 <SidebarLink to={`${base}/settings/runners`} icon={<ServerCog size={15} />}>
1175 Runners
1176 </SidebarLink>
1177 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1178 {shows("webhooks") && (
1179 <SidebarLink to={`${base}/settings/webhooks`} icon={<Webhook size={15} />}>
1180 Webhooks
1181 </SidebarLink>
1182 )}
1183 </div>
Project settings move into the sidebar, grouped, with a heading per page1184 </nav>
1185 );
1186}
1187
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)1188/**
1189 * Whether the menus list Settings → Invites (invites to g1t): always while
1190 * sign-up takes an invite; once anyone can sign up, only for someone with
1191 * invites already made to look back on, which the account menu's data says.
1192 */
1193function useInvitesListed(): boolean {
1194 const inviteOnly = useInviteOnly();
1195 const details = useFetcher<AccountMenuData | null>({ key: "account-menu" });
1196 useEffect(() => {
1197 if (!inviteOnly && details.state === "idle" && details.data === undefined) details.load("/settings/menu.json");
1198 // eslint-disable-next-line react-hooks/exhaustive-deps
1199 }, [inviteOnly]);
1200 return inviteOnly || details.data?.invites_page === true;
1201}
1202
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1203/** Your own settings, drilled into from Your settings: one page each. */
1204function 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)1205 const invites = useInvitesListed();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1206 const link = (page: AccountSettingsPage, icon: ReactNode) => (
1207 <SidebarLink to={`/settings/${page}`} icon={icon}>
1208 {ACCOUNT_SETTINGS[page].title}
1209 </SidebarLink>
Account settings in the same shape as a workspace's1210 );
1211 return (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1212 <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 quotas1213 <BackRow to="/" label="Your settings" context={username} />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1214 <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 quotas1215 {link("profile", <CircleUserRound size={15} />)}
1216 {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)1217 {invites && link("invites", <Ticket size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1218 </div>
1219 <Rule />
1220 <div className="space-y-px">
1221 {link("keys", <Fingerprint size={15} />)}
1222 {link("tokens", <KeyRound size={15} />)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1223 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1224 <Rule />
1225 <div className="space-y-px">
1226 {link("github", <GithubMark className="size-[15px]" />)}
1227 {link("applications", <Plug size={15} />)}
1228 </div>
1229 <Rule />
1230 <div className="space-y-px">
Merge main (membership, two-factor, GitHub repo roles) into tokens1231 {link("two-factor", <ShieldCheck size={15} />)}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1232 {link("security-log", <History size={15} />)}
1233 </div>
Account settings in the same shape as a workspace's1234 </nav>
1235 );
1236}
1237
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1238/**
1239 * Signing in and signing up, in place of the account for a visitor. Signing
1240 * in brings them back to the page they are on.
1241 */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1242/** Help, status and the fine print under a visitor's sign-in buttons. */
1243function VisitorLinks() {
1244 const status = useSiteStatus();
Merge branch 'worktree-agent-ad7c6d88d93adc817'1245 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 quotas1246 const dot = (
1247 <span aria-hidden="true" className="text-[0.625rem] text-line-strong">
1248 ·
1249 </span>
1250 );
1251 return (
1252 // Two short rows, so a narrow sidebar never starts a line with a dot.
1253 <nav aria-label="About g1t" className="pt-0.5">
1254 <div className="flex items-center gap-0.5">
1255 <a href={STATUS_URL} className={`${quiet} gap-1.5`}>
1256 <StatusDot state={status?.overall.state ?? null} />
1257 Status
1258 </a>
1259 {dot}
1260 <Link to="/support" className={quiet}>
1261 Support
1262 </Link>
1263 </div>
1264 <div className="flex items-center gap-0.5">
1265 {LEGAL_LINKS.map(([label, to], index) => (
1266 <span key={to} className="flex items-center gap-0.5">
1267 {index > 0 && dot}
1268 <Link to={to} className={quiet}>
1269 {label}
1270 </Link>
1271 </span>
1272 ))}
1273 </div>
1274 </nav>
1275 );
1276}
1277
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1278function VisitorPanel() {
1279 const { pathname, search } = useLocation();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1280 const signUp = useSignUpCopy();
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1281 return (
1282 <div className="space-y-2">
1283 <p className="px-1 text-xs text-muted">Sign in to open issues, review pull requests and run agents.</p>
1284 <div className="grid grid-cols-2 gap-2">
1285 <Link
1286 to={withNext("/login", pathname + search)}
1287 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"
1288 >
1289 <LogIn size={14} />
1290 Sign in
1291 </Link>
1292 <Link
1293 to={withNext("/register", pathname + search)}
1294 className="flex h-9 items-center justify-center rounded-md bg-fg text-[0.8125rem] font-medium text-bg transition-colors hover:bg-white"
1295 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1296 {signUp.primary}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1297 </Link>
1298 </div>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1299 <VisitorLinks />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1300 </div>
1301 );
1302}
1303
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1304function Sidebar({
1305 user,
1306 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1307 missing = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1308 onFind,
1309 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)1310 rail = false,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1311}: {
1312 user: User | null;
1313 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1314 /** The page is a 404: the address names nothing the viewer can see. */
1315 missing?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1316 onFind: () => void;
1317 /** In the sheet on a small screen: closing it, at the end of the top row. */
1318 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)1319 /**
1320 * Beside the rail, as Code's sidebar: code only, under a Code heading.
1321 * The workspace, its settings and the account are the rail's.
1322 */
1323 rail?: boolean;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1324}) {
Agents as a team: lifecycle, merge queue, billing and a new shell1325 const ws = shell.workspace;
A catalogue of model providers, and settings that feel like settings1326 const { pathname } = useLocation();
1327 const going = useNavigation().location?.pathname;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1328 // Where the sidebar is drilled to follows the page being gone to, so it
1329 // moves as the link is followed, not once the page arrives.
1330 const target = going ?? pathname;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1331 // On a 404, the address is not a place: no list is drilled into from it,
1332 // except the project's own when the project is real and visible and only
1333 // something inside it is missing (a file, a commit, an issue).
1334 const lost = missing && going == null;
1335 const inSettings = !lost && ws != null && SETTINGS_PAGE.exec(target)?.[1]?.toLowerCase() === ws.slug;
1336 const inAccount = !lost && target === "/settings" || target.startsWith("/settings/");
A repository has its own sidebar, as settings do1337 const active = shell.repo;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1338 // In a repository, or on the way into one, its own list.
A repository has its own sidebar, as settings do1339 const repoPath = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(target);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1340 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 look1341 // An invitation to a repository is answered before its menu means anything.
1342 const inRepo =
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1343 repoPath != null &&
1344 !reserved.has(repoPath[1]) &&
1345 repoPath[2] !== "-" &&
1346 !/^\/[^/]+\/[^/]+\/invitations\/?$/.test(target) &&
1347 (!lost || sameRepo(active, { namespace: repoPath[1]!, name: repoPath[2]! }));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1348 // A project's settings, one level further in.
Project settings move into the sidebar, grouped, with a heading per page1349 const inRepoSettings = inRepo && REPO_SETTINGS_PAGE.test(target);
Sidebar: no settings flash on the way into a repository, and a better slide1350 // The repository the menu is for: the one loaded if it is the one being
1351 // gone to, else what the address says, at once.
1352 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 look1353 const guessed: MenuRepo | null = targetRepo
Sidebar: no settings flash on the way into a repository, and a better slide1354 ? sameRepo(active, targetRepo)
1355 ? active
1356 : {
1357 ...targetRepo,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1358 member: (user?.workspaces ?? []).some((m) => m.slug === targetRepo.namespace.toLowerCase()),
Sidebar: no settings flash on the way into a repository, and a better slide1359 }
1360 : null;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1361 // The viewer's role there, from the repository's page once it has loaded.
1362 const page = useRouteLoaderData("routes/repo/layout") as
1363 | { repo?: { namespace: string; name: string }; access?: ViewerAccess }
1364 | undefined;
1365 const menuRepo: MenuRepo | null =
1366 guessed && page?.access && page.repo && sameRepo(page.repo, guessed)
1367 ? { ...guessed, member: page.access.insider, settings: seesSettings(page.access), can: page.access.can }
1368 : guessed;
1369
1370 // 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)1371 const trail: Level[] = [{ key: "main", node: rail && user && ws ? <CodeMenu shell={shell} slug={ws.slug} /> : <MainMenu user={user} shell={shell} /> }];
1372 if (rail && !menuRepo) {
1373 // Code's own list: nothing to drill into but a project.
1374 } else if (user && inAccount && !rail) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1375 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)1376 } else if (ws && inSettings && !rail) {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1377 trail.push({ key: `settings:${ws.slug}`, node: <SettingsMenu slug={ws.slug} owner={ws.role === "owner"} /> });
1378 } else if (menuRepo) {
1379 const key = `repo:${menuRepo.namespace}/${menuRepo.name}`.toLowerCase();
1380 // Out of a project: to its workspace's projects when they are yours.
1381 const home = (user?.workspaces ?? []).find((m) => m.slug === menuRepo.namespace.toLowerCase());
1382 const back = !user
1383 ? { to: "/explore", label: "Explore" }
1384 : home
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1385 ? { 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)1386 : { to: "/", label: "Home" };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1387 trail.push({
1388 key,
1389 node: (
1390 <RepoMenu
1391 repo={menuRepo}
1392 isPrivate={[...shell.repos, ...(shell.shared ?? [])].some((repo) => sameRepo(repo, menuRepo) && repo.isPrivate)}
1393 back={back}
1394 />
1395 ),
1396 });
1397 if (inRepoSettings) trail.push({ key: `${key}:settings`, node: <RepoSettingsMenu repo={menuRepo} /> });
1398 }
1399
Agents as a team: lifecycle, merge queue, billing and a new shell1400 return (
1401 <div className="flex h-full flex-col">
A Vercel-like sidebar and a new mark1402 {/* 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)1403 {rail ? (
1404 <ModeHeader title="Code" onClose={onClose} />
1405 ) : (
Mission control counts people's own changes as theirs; a shorter sidebar with setup under Settings, and a 56px top bar1406 <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 put1407 {user ? (
1408 <>
The 1 beside the workspace switcher again, in a square hover the switcher's height1409 {/* The 1 alone beside the workspace: a square hover the height of the
1410 switcher, the mark as tall as the workspace avatar. */}
1411 <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 bar1412 <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 put1413 </Link>
1414 <span className="shrink-0 text-line-strong" aria-hidden="true">
1415 /
1416 </span>
1417 <WorkspaceSwitcher user={user} shell={shell} />
1418 </>
1419 ) : (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1420 <Link to="/" aria-label="g1t home" className="mr-auto flex h-9 items-center rounded-md px-2 transition-colors hover:bg-raised">
1421 <Logo className="text-[1.25rem]" />
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1422 </Link>
1423 )}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1424 {onClose && (
1425 <button
1426 type="button"
1427 aria-label="Close menu"
1428 onClick={onClose}
1429 className="flex size-9 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1430 >
1431 <X size={16} />
1432 </button>
1433 )}
Agents as a team: lifecycle, merge queue, billing and a new shell1434 </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)1435 )}
A Vercel-like sidebar and a new mark1436 <div className="px-2 pt-3">
1437 <button
1438 type="button"
1439 onClick={onFind}
Two limits, real invoices, trust that grows by itself, sales signals1440 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 mark1441 >
1442 <Search size={14} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1443 <span className="grow text-left">Search or jump to…</span>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1444 <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 mark1445 </button>
Agents as a team: lifecycle, merge queue, billing and a new shell1446 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1447 <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)1448 {/* The account, or signing in, as one row at the very bottom; beside the rail, the rail has it. */}
1449 {!rail && (
1450 <div className="shrink-0 border-t border-line p-2">
1451 {user ? <AccountMenu user={user} /> : <VisitorPanel />}
1452 </div>
1453 )}
1454 </div>
1455 );
1456}
1457
1458/** A mode's sidebar heading: the same height and rule as the top bar, with a way to close the phone's sheet. */
1459export function ModeHeader({ title, action, onClose }: { title: ReactNode; action?: ReactNode; onClose?: () => void }) {
1460 return (
1461 <div className="flex h-14 shrink-0 items-center gap-1 border-b border-line pr-2.5 pl-4">
1462 <h2 className="min-w-0 grow truncate text-[0.9375rem] font-semibold">{title}</h2>
1463 {action}
1464 {onClose && (
1465 <button
1466 type="button"
1467 aria-label="Close menu"
1468 onClick={onClose}
1469 className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
1470 >
1471 <X size={16} />
1472 </button>
1473 )}
1474 </div>
1475 );
1476}
1477
1478/**
1479 * Code's sidebar (beside the rail): only code. Its Overview (Mission
1480 * control's code panels), the projects, what spans them, and what is
1481 * coming. The workspace's people, money and settings are Workspace's;
1482 * agents, context and memory are Agents'.
1483 */
1484function CodeMenu({ shell, slug }: { shell: ShellData; slug: string }) {
1485 const { pathname } = useLocation();
1486 const going = useNavigation().location?.pathname;
1487 const path = going ?? pathname;
1488 const at = (page: string) => path === `/${slug}/-/${page}` || path.startsWith(`/${slug}/-/${page}/`);
1489 const shared = shell.shared ?? [];
1490 return (
1491 <nav aria-label="Code" className={PANEL}>
1492 <div className="mt-3 space-y-px">
1493 <SidebarLink to={`/${slug}/-/overview`} icon={<LayoutDashboard size={15} />} current={at("overview")}>
1494 Overview
1495 </SidebarLink>
1496 </div>
1497 <div className="mt-3">
1498 <SidebarProjects slug={slug} shell={shell} current={at("projects")} />
1499 </div>
1500 <Rule />
1501 <div className="space-y-px">
1502 <SidebarLink to={`/${slug}/-/security`} icon={<ShieldCheck size={15} />} current={at("security") && !at("security/settings")}>
1503 Security
1504 </SidebarLink>
1505 <SidebarLink to={`/${slug}/-/packages`} icon={<Package size={15} />} current={at("packages")}>
1506 Packages
1507 </SidebarLink>
1508 {INSIGHTS && (
1509 <SidebarSoonLink to={`/${slug}/-/insights`} icon={<TrendingUp size={15} />} about={INSIGHTS.summary} current={at("insights")}>
1510 {INSIGHTS.title}
1511 </SidebarSoonLink>
1512 )}
1513 {roadmapIn("Workspace")
1514 .filter((item) => item.key !== "insights" && item.key !== "teams" && item.key !== "fleet")
1515 .map((item) => (
1516 <SidebarSoonLink key={item.key} to={`/${slug}/-/soon/${item.key}`} icon={WORKSPACE_ICONS[item.key] ?? <Sparkles size={15} />} about={item.summary}>
1517 {item.title === "Board" ? "Boards" : item.title}
1518 </SidebarSoonLink>
1519 ))}
1520 </div>
1521 {shared.length > 0 && (
1522 <>
1523 <Rule />
1524 <SidebarGroup title="Shared with you">
1525 {shared.map((repo) => (
1526 <SidebarLink
1527 key={`${repo.namespace}/${repo.name}`}
1528 to={`/${repo.namespace}/${repo.name}`}
1529 icon={repo.isPrivate ? <Lock size={15} /> : <Box size={15} />}
1530 drill="hover"
1531 >
1532 <span className="font-mono text-faint">{repo.namespace}/</span>
1533 {repo.name}
1534 </SidebarLink>
1535 ))}
1536 </SidebarGroup>
1537 </>
1538 )}
1539 </nav>
1540 );
1541}
1542
1543/** Workspace pages that sit in its Settings list, drilled into from the Workspace sidebar. */
1544const WORKSPACE_SETTINGS = ["settings", "repositories", "tokens", "personal-access-tokens", "secrets", "actions", "runners", "webhooks", "emoji"];
1545
1546/**
1547 * The Workspace mode's sidebar: the workspace itself, for every member.
1548 * Its overview, people, money, connections, policies and record, then its
1549 * settings as a list of their own. Owner-only pages stay owner-only.
1550 */
1551export function WorkspaceSidebar({ slug, owner, onClose }: { slug: string; owner: boolean; onClose?: () => void }) {
1552 const { pathname } = useLocation();
1553 const going = useNavigation().location?.pathname;
1554 const path = going ?? pathname;
1555 const page = path.startsWith(`/${slug}/-/`) ? path.slice(`/${slug}/-/`.length) : "";
1556 const top = page.split("/")[0] ?? "";
1557 const at = (...pages: string[]) => pages.some((p) => page === p || page.startsWith(`${p}/`));
1558 const inSettings = WORKSPACE_SETTINGS.includes(top);
1559 const main = (
1560 <nav aria-label="Workspace" className={PANEL}>
1561 <div className="mt-3 space-y-px">
1562 <SidebarLink to={`/${slug}/-/workspace`} icon={<LayoutGrid size={15} />} current={at("workspace")}>
1563 Overview
1564 </SidebarLink>
1565 <SidebarLink to={`/${slug}/-/people`} icon={<Users size={15} />} current={at("people")}>
1566 People
1567 </SidebarLink>
1568 <SidebarLink to={`/${slug}/-/teams`} icon={<UsersRound size={15} />} current={at("teams")}>
1569 Teams
1570 </SidebarLink>
1571 </div>
1572 <SidebarGroup title="Usage and billing" className="mt-3">
1573 <SidebarLink to={`/${slug}/-/usage`} icon={<BarChart3 size={15} />} current={at("usage", "gateway")}>
1574 Usage
1575 </SidebarLink>
1576 <SidebarLink to={`/${slug}/-/billing`} icon={<CreditCard size={15} />} current={at("billing")}>
1577 Billing and plans
1578 </SidebarLink>
1579 </SidebarGroup>
1580 <SidebarGroup title="Connections" className="mt-3">
1581 <SidebarLink to={`/${slug}/-/integrations`} icon={<Plug size={15} />} current={at("integrations")}>
1582 Integrations
1583 </SidebarLink>
1584 </SidebarGroup>
1585 <SidebarGroup title="Security policies" className="mt-3">
1586 <SidebarLink to={`/${slug}/-/security/settings`} icon={<ShieldCheck size={15} />} current={at("security/settings")}>
1587 Security settings
1588 </SidebarLink>
1589 <SidebarLink to={`/${slug}/-/guardrails`} icon={<Gauge size={15} />} current={at("guardrails")}>
1590 Guardrails
1591 </SidebarLink>
1592 <SidebarLink to={`/${slug}/-/rules`} icon={<Scale size={15} />} current={at("rules")}>
1593 Rules
1594 </SidebarLink>
1595 </SidebarGroup>
1596 <Rule />
1597 <div className="space-y-px">
1598 <SidebarLink to={`/${slug}/-/audit`} icon={<History size={15} />} current={at("audit")}>
1599 Audit log
1600 </SidebarLink>
1601 <SidebarLink to={owner ? `/${slug}/-/settings` : `/${slug}/-/repositories`} icon={<Settings size={15} />} drill current={inSettings}>
1602 Settings
1603 </SidebarLink>
1604 </div>
1605 </nav>
1606 );
1607 const settings = (
1608 <nav aria-label="Workspace settings" className={PANEL}>
1609 <BackRow to={`/${slug}/-/workspace`} label="Settings" context={slug} />
1610 <div className="mt-2 space-y-px">
1611 {owner && (
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1612 <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)1613 General
1614 </SidebarLink>
1615 )}
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1616 <SidebarLink to={`/${slug}/-/settings/chat`} icon={<MessagesSquare size={15} />}>
1617 Chat
1618 </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)1619 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
1620 Repositories
1621 </SidebarLink>
1622 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
1623 Access tokens
1624 </SidebarLink>
1625 {owner && (
1626 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
1627 Personal access tokens
1628 </SidebarLink>
1629 )}
1630 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
1631 Webhooks
1632 </SidebarLink>
1633 <SidebarLink to={`/${slug}/-/emoji`} icon={<Smile size={15} />}>
1634 Emoji
1635 </SidebarLink>
1636 </div>
1637 <SidebarGroup title="Runs" className="mt-3">
1638 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
1639 Secrets and variables
1640 </SidebarLink>
1641 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
1642 Actions
1643 </SidebarLink>
1644 {owner && (
1645 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
1646 Runners
1647 </SidebarLink>
1648 )}
1649 </SidebarGroup>
1650 </nav>
1651 );
1652 const trail: Level[] = [{ key: "workspace", node: main }];
1653 if (inSettings) trail.push({ key: "workspace:settings", node: settings });
1654 return (
1655 <div className="flex h-full flex-col">
1656 <ModeHeader title="Workspace" onClose={onClose} />
1657 <Drill trail={trail} />
1658 </div>
1659 );
1660}
1661
1662/** Your own settings, beside the rail: under the account menu, not any workspace's. */
1663export function AccountSidebar({ username, onClose }: { username: string; onClose?: () => void }) {
1664 return (
1665 <div className="flex h-full flex-col">
1666 <ModeHeader title="Your account" onClose={onClose} />
1667 <div className="min-h-0 grow">
1668 <AccountSettingsMenu username={username} />
A Vercel-like sidebar and a new mark1669 </div>
1670 </div>
1671 );
1672}
1673
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)1674
A Vercel-like sidebar and a new mark1675/** Words for the sections a path can end in. */
1676const SECTIONS: Record<string, string> = {
1677 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1678 milestones: "Milestones",
1679 labels: "Labels",
A Vercel-like sidebar and a new mark1680 pulls: "Pull requests",
1681 queue: "Merge queue",
1682 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1683 plans: "Outcomes",
1684 actions: "Workflows",
1685 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1686 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1687 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1688 access: "Access",
1689 invitations: "Invitation",
1690 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1691 branches: "Branches",
1692 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1693 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1694 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1695 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1696 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1697 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1698 people: "People",
1699 projects: "Projects",
1700 teams: "Teams",
A Vercel-like sidebar and a new mark1701 tokens: "Access tokens",
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1702 "personal-access-tokens": "Personal access tokens",
A Vercel-like sidebar and a new mark1703 usage: "Usage",
Site: the AI Gateway page's breadcrumb reads "AI Gateway"1704 gateway: "AI Gateway",
Secrets and variables: one list, rows per environment, for workflows and deployments1705 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1706 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1707 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)1708 emoji: "Emoji",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1709 domains: "Domains",
1710 guardrails: "Guardrails",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1711 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1712 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1713 tree: "Files",
1714 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1715 agents: "Agents",
1716 context: "Context",
1717 memory: "Memory",
1718 security: "Security",
1719 packages: "Packages",
1720 runners: "Runners",
Merge branch 'worktree-agent-a3abfcce648e87dca'1721 environments: "Environments",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1722 workflows: "Workflows",
1723 observability: "Observability",
1724 insights: "Insights",
1725 sessions: "Sessions",
Chat and workspace agents: channels, DMs and named agents you talk to1726 chat: "Chat",
1727 docs: "Docs",
1728 home: "Home",
1729 "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)1730 overview: "Overview",
1731 workspace: "Workspace",
A Vercel-like sidebar and a new mark1732};
1733
Merge branch 'worktree-agent-a3abfcce648e87dca'1734/** Settings pages whose name differs from the section's of the same word. */
1735const SETTINGS_SECTIONS: Record<string, string> = { branches: "Branches and merging", actions: "Actions" };
1736
A Vercel-like sidebar and a new mark1737/** 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 quotas1738function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1739 // A 404 names nothing from the address, unless the project is real and
1740 // visible and only something inside it is missing.
1741 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1742 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1743 return <span className="text-sm font-medium">Not found</span>;
1744 }
A Vercel-like sidebar and a new mark1745 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1746 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)1747 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 quotas1748 // Your settings: Settings / Emails.
1749 if (parts[0] === "settings") {
1750 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1751 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 quotas1752 }
A Vercel-like sidebar and a new mark1753 if (reserved.includes(parts[0]!)) {
1754 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1755 settings: "Account",
A Vercel-like sidebar and a new mark1756 explore: "Explore",
Projects live: fixes from walking them end to end1757 new: "New project",
A Vercel-like sidebar and a new mark1758 search: "Search",
1759 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1760 policies: "Policies",
1761 security: "Security",
1762 support: "Support",
1763 status: "Status",
1764 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1765 inbox: "Inbox",
A Vercel-like sidebar and a new mark1766 };
1767 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1768 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1769 // A person's profile, by their handle.
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1770 if (parts[0] === "u" && parts[1]) return <ProfileCrumb username={parts[1]} />;
A Vercel-like sidebar and a new mark1771 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1772 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1773 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1774 const page = `/${owner}/-/${third}`;
1775 if (third && SETTINGS_PAGE.test(page)) {
1776 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1777 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1778 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1779 } else if (second) {
1780 const repo = `/${owner}/${second}`;
1781 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1782 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1783 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 mark1784 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 deployments1785 else if (third && SECTIONS[third]) {
1786 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1787 // A settings page names which one: Settings / Secrets and variables.
1788 if (third === "settings" && fourth && SECTIONS[fourth]) {
Merge branch 'worktree-agent-a3abfcce648e87dca'1789 trail.push({ label: SETTINGS_SECTIONS[fourth] ?? SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1790 }
1791 }
A Vercel-like sidebar and a new mark1792 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1793 return <Trail trail={trail} />;
1794}
1795
Chat controls, public profiles, shadcn selects, and no Docs tab in a project1796/** A profile's place in the top bar: the person's avatar and @handle, from the page once it has loaded. */
1797function ProfileCrumb({ username }: { username: string }) {
1798 const page = useRouteLoaderData("routes/user") as { profile?: { username: string; avatar: string | null } } | undefined;
1799 const profile = page?.profile?.username.toLowerCase() === username.toLowerCase() ? page.profile : null;
1800 return (
1801 <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">
1802 <Avatar name={profile?.username ?? username} image={profile?.avatar ?? null} size={20} />
1803 <span className="truncate font-mono text-[0.8125rem] font-medium">@{profile?.username ?? username}</span>
1804 </Link>
1805 );
1806}
1807
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1808/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1809type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1810
1811/**
1812 * The trail of links. On a phone there is room for one: the page itself,
1813 * with a way back to where it sits; the whole trail from a wider screen up.
1814 */
1815function 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)1816 // Up on a phone, unless up is the workspace itself: its pages are the tabs.
1817 const up = trail.length > 1 && !/^\/[^/]+$/.test(trail[trail.length - 2]!.to) ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1818 return (
1819 <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 audit1820 {up && (
1821 <Link
1822 to={up.to}
1823 prefetch="intent"
1824 aria-label={`Back to ${up.label}`}
1825 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"
1826 >
1827 <ChevronLeft size={16} />
1828 </Link>
1829 )}
1830 {trail.map((crumb, index) => {
1831 const last = index === trail.length - 1;
1832 return (
1833 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1834 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1835 <Link
1836 to={crumb.to}
1837 prefetch="intent"
1838 aria-current={last ? "page" : undefined}
1839 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1840 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1841 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1842 >
1843 {last && crumb.short ? (
1844 <>
1845 <span className="sm:hidden">{crumb.short}</span>
1846 <span className="hidden sm:inline">{crumb.label}</span>
1847 </>
1848 ) : (
1849 crumb.label
1850 )}
1851 </Link>
1852 </span>
1853 );
1854 })}
A Vercel-like sidebar and a new mark1855 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1856 );
1857}
1858
Search across all of g1t, Explore, and a command palette1859type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1860
1861/** 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 look1862function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1863 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1864 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)1865 { label: "Home", to: "/", icon: <House size={15} /> },
1866 ...(shell.workspace
1867 ? [
1868 { label: "Chat", to: `/${shell.workspace.slug}/-/chat`, icon: <MessagesSquare size={15} /> },
1869 { label: "Agents", to: `/${shell.workspace.slug}/-/agents`, icon: <Sparkles size={15} /> },
1870 ...(hasCodeAccess(shell.workspace) ? [{ label: "Code overview", to: `/${shell.workspace.slug}/-/overview`, icon: <Code2 size={15} /> }] : []),
1871 { label: "Workspace", hint: "People, billing, policies, settings", to: `/${shell.workspace.slug}/-/workspace`, icon: <Building2 size={15} /> },
1872 ]
1873 : []),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1874 ...(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-agent1875 ? [{ 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 step1876 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1877 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1878 { 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 all1879 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1880 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1881 { 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 quotas1882 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1883 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1884 .filter((page) => page !== "profile")
1885 .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 shell1886 ];
1887 const repo = shell.repo;
1888 if (repo) {
1889 const base = `/${repo.namespace}/${repo.name}`;
1890 const name = `${repo.namespace}/${repo.name}`;
1891 commands.unshift(
Projects: what a workspace builds and runs, first on every page1892 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1893 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1894 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1895 { 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 step1896 ...(repo.member
1897 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1898 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1899 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1900 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1901 ...(repo.member
1902 ? [
1903 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1904 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1905 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1906 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1907 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1908 ]
1909 : []),
1910 );
1911 }
1912 for (const membership of user.workspaces ?? []) {
1913 commands.push(
Workspace names and icons, and a component kit for every control1914 { 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 sidebar1915 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1916 { 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 request1917 { 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 tokens1918 { 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 page1919 { 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 settings1920 { 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 look1921 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1922 );
1923 }
1924 for (const listed of shell.repos) {
1925 commands.push({
Projects: what a workspace builds and runs, first on every page1926 label: listed.title ?? listed.name,
1927 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1928 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1929 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1930 });
1931 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1932 // "Put an agent on …": one per project, after the projects themselves.
1933 for (const listed of shell.repos) {
1934 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1935 commands.push({
1936 label: `Put an agent on ${listed.title ?? listed.name}`,
1937 hint: `${listed.namespace}/${listed.name}`,
1938 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1939 icon: <Sparkles size={15} />,
1940 });
1941 }
Agents as a team: lifecycle, merge queue, billing and a new shell1942 return commands;
1943}
1944
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1945/** 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 look1946function 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 put1947 const commands: Command[] = [];
1948 const repo = shell.repo;
1949 if (repo) {
1950 const base = `/${repo.namespace}/${repo.name}`;
1951 const name = `${repo.namespace}/${repo.name}`;
1952 commands.push(
1953 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1954 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1955 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1956 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1957 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1958 );
1959 }
1960 commands.push(
1961 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1962 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1963 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1964 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1965 { 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 look1966 { 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 put1967 );
1968 return commands;
1969}
1970
Agents as a team: lifecycle, merge queue, billing and a new shell1971/**
Merge branch 'worktree-agent-aca4790ea223aa235'1972 * The page while the next one loads: after a moment it dims, under the
1973 * bar below, so it is plainly on its way out rather than frozen, and the
1974 * next page fades up from there. Sending a form does not dim it: the
1975 * form's button says it is working. Spread on the element holding the page.
1976 */
1977export function useLeaving() {
1978 const navigation = useNavigation();
1979 const method = navigation.formMethod?.toUpperCase();
1980 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1981 return {
1982 "aria-busy": leaving || undefined,
1983 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1984 };
1985}
1986
1987/**
Agents as a team: lifecycle, merge queue, billing and a new shell1988 * A bar across the top of the page while the next one loads, as GitHub
1989 * has: it appears at once, creeps towards the end while waiting, then fills
1990 * and fades when the page arrives.
1991 */
1992export function Progress() {
1993 const navigation = useNavigation();
1994 const busy = navigation.state !== "idle";
1995 const [width, setWidth] = useState(0);
1996 const [visible, setVisible] = useState(false);
1997 useEffect(() => {
1998 if (busy) {
1999 setVisible(true);
2000 setWidth(12);
2001 // Slows as it goes, never quite arriving until the page does.
2002 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
2003 return () => clearInterval(timer);
2004 }
2005 setWidth((now) => (now > 0 ? 100 : 0));
2006 const fade = setTimeout(() => setVisible(false), 250);
2007 const reset = setTimeout(() => setWidth(0), 550);
2008 return () => {
2009 clearTimeout(fade);
2010 clearTimeout(reset);
2011 };
2012 }, [busy]);
2013 return (
2014 <div
2015 aria-hidden="true"
2016 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
2017 visible ? "opacity-100" : "opacity-0"
2018 }`}
2019 >
2020 <div
2021 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
2022 style={{ width: `${width}%` }}
2023 />
2024 </div>
2025 );
2026}
2027
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)2028/** The top bar's way to g1t: the direct message with the workspace's orchestrator, opened or made. */
2029function AskG1tButton({ slug }: { slug: string }) {
2030 return (
2031 <Hint label="Ask g1t">
2032 <Link
2033 to={`/${slug}/-/chat?agent=g1t`}
2034 aria-label="Ask g1t"
2035 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"
2036 >
2037 <G1tMark size={18} />
2038 <span className="hidden sm:inline">Ask g1t</span>
2039 </Link>
2040 </Hint>
2041 );
2042}
2043
Chat and workspace agents: channels, DMs and named agents you talk to2044/** The mode's sidebar beside the rail: a shade lighter than the page. */
2045const SIDEBAR_BOX = "h-full border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]";
2046
Agents as a team: lifecycle, merge queue, billing and a new shell2047/**
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2048 * Which sidebar sits beside the rail: each mode's own, or none (Docs, the
2049 * Inbox, which is a page of its own, and g1t's public pages, such as a
2050 * profile, which are no workspace's and carry their own left column).
2051 * Without a workspace (a visitor), the one sidebar there always was.
Chat and workspace agents: channels, DMs and named agents you talk to2052 */
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)2053function sidebarFor(mode: ModeKey | null): Panel | null {
Chat and workspace agents: channels, DMs and named agents you talk to2054 if (mode == null) return "code";
Chat controls, public profiles, shadcn selects, and no Docs tab in a project2055 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)2056 return mode;
Chat and workspace agents: channels, DMs and named agents you talk to2057}
2058
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)2059/** The sidebars that sit beside the rail, one per mode that has one. */
2060type Panel = "home" | "chat" | "agents" | "code" | "workspace" | "account";
2061
Chat and workspace agents: channels, DMs and named agents you talk to2062/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2063 * The app: a sidebar with the workspace, its repositories and the sections
2064 * of the one being looked at; a slim bar with search and the account; and
2065 * the page. A visitor who is not signed in gets the same frame, with
2066 * Explore and Search in place of the workspace, and signing in in place of
2067 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell2068 */
2069export function AppShell({
2070 user,
2071 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2072 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell2073 banner,
2074 children,
2075}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2076 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell2077 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2078 /** The page is a 404 (see Sidebar). */
2079 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell2080 banner?: ReactNode;
2081 children: ReactNode;
2082}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2083 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell2084 const [drawer, setDrawer] = useState(false);
2085 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2086 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2087 const signUpLabel = useSignUpCopy().primary;
2088 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell2089
2090 // A new page closes the drawer on small screens.
2091 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2092 // Mission control's code, fetched while the browser is idle, so going
2093 // home never waits on it (routes/home.tsx).
2094 useEffect(() => {
2095 if (!user) return;
2096 const load = () => void import("./mission-control").catch(() => undefined);
2097 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
2098 if (idle) idle(load);
2099 else setTimeout(load, 1500);
2100 }, [user]);
Search across all of g1t, Explore, and a command palette2101 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'2102 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)2103 useVisualViewport();
Agents as a team: lifecycle, merge queue, billing and a new shell2104
Chat and workspace agents: channels, DMs and named agents you talk to2105 // The rail and the mode's sidebar (docs/WORKSPACE.md, "Shell"), for
2106 // someone in a workspace. A visitor, or someone with none, keeps the one
2107 // sidebar.
2108 const going = useNavigation().location?.pathname;
2109 const ws = user ? shell.workspace : null;
2110 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)2111 const panel = ws ? sidebarFor(mode) : "code";
2112 const code = ws ? hasCodeAccess(ws) : true;
Chat and workspace agents: channels, DMs and named agents you talk to2113 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)2114 // Notify: the feed socket's live counts (lib/notify-client.ts) once it has them; the page's until then.
2115 const live = useLiveBadges(ws?.slug);
2116 const chatUnread = chatSidebar
2117 ? unreadTotals(chatSidebar.entries)
2118 : live?.chat != null
2119 ? { unread: live.chat, mentions: live.mentions ?? 0 }
2120 : (shell.chat ?? { unread: 0, mentions: 0 });
2121 const inboxUnread = live?.inbox ?? shell.inbox?.unread ?? 0;
2122 const sidebarNode = (inSheet: boolean) => {
2123 const close = inSheet ? () => setDrawer(false) : undefined;
2124 const find = () => {
2125 if (inSheet) setDrawer(false);
2126 setPalette(true);
2127 };
2128 if (!ws || !user || panel === "code") {
2129 return <Sidebar user={user} shell={shell} missing={missing} onFind={find} onClose={close} rail={Boolean(ws && user)} />;
2130 }
2131 switch (panel) {
2132 case "home":
2133 return <HomeSidebar slug={ws.slug} shell={shell} code={code} onFind={find} header={<ModeHeader title="Home" onClose={close} />} />;
2134 case "chat":
2135 return <ChatSidebar slug={ws.slug} />;
2136 case "agents":
2137 return <AgentsSidebar slug={ws.slug} shellAgents={shell.agents ?? null} code={code} owner={ws.role === "owner"} />;
2138 case "workspace":
2139 return <WorkspaceSidebar slug={ws.slug} owner={ws.role === "owner"} onClose={close} />;
2140 case "account":
2141 return <AccountSidebar username={user.username} onClose={close} />;
2142 default:
2143 return null;
2144 }
2145 };
Chat and workspace agents: channels, DMs and named agents you talk to2146 const rail =
2147 user && ws ? (
2148 <Rail
2149 user={user}
2150 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)2151 unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }}
2152 help={<HelpMenu />}
Chat and workspace agents: channels, DMs and named agents you talk to2153 account={<AccountMenu user={user} rail />}
2154 />
2155 ) : null;
2156 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)2157 // On a phone (below 768px): the tab bar, and a conversation full screen.
2158 const conversation = isConversation(going ?? pathname);
2159 const tabs = Boolean(user && ws) && !conversation;
Chat and workspace agents: channels, DMs and named agents you talk to2160
Agents as a team: lifecycle, merge queue, billing and a new shell2161 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2162 // The phone's menu is a sheet: a dialog that holds focus, closes on
2163 // Escape or a tap outside, and gives focus back to the menu button.
2164 <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)2165 <div className="min-h-dvh">
Agents as a team: lifecycle, merge queue, billing and a new shell2166 <Progress />
Chat and workspace agents: channels, DMs and named agents you talk to2167 <aside className="fixed inset-y-0 left-0 z-40 hidden lg:flex">
2168 {rail}
2169 {panel && <div className={`${SIDEBAR_BOX} w-64`}>{sidebarNode(false)}</div>}
Agents as a team: lifecycle, merge queue, billing and a new shell2170 </aside>
Chat and workspace agents: channels, DMs and named agents you talk to2171 <SheetContent
2172 side="left"
2173 showClose={false}
2174 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)2175 // Focus would land on the workspace's avatar and open its hint over
2176 // the sidebar's heading; the sheet itself takes it instead.
2177 onOpenAutoFocus={(event) => {
2178 event.preventDefault();
2179 (event.currentTarget as HTMLElement | null)?.focus();
2180 }}
Chat and workspace agents: channels, DMs and named agents you talk to2181 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"}`}
2182 >
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2183 <SheetTitle className="sr-only">Menu</SheetTitle>
Chat and workspace agents: channels, DMs and named agents you talk to2184 {rail}
2185 {panel && <div className={`min-w-0 grow ${rail ? SIDEBAR_BOX : ""}`}>{sidebarNode(true)}</div>}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2186 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell2187
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)2188 <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 to2189 <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)2190 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 to2191 >
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)2192 {/* A phone: the workspace's avatar opens everything else (components/mobile.tsx). */}
2193 {user && ws && <AvatarSheetButton user={user} workspace={ws} />}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2194 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell2195 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)2196 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 shell2197 >
2198 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2199 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2200 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2201 <div className="ml-auto flex items-center gap-1.5">
2202 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
2203 <button
2204 type="button"
2205 aria-label="Search or jump to"
2206 onClick={() => setPalette(true)}
2207 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
2208 >
2209 <Search size={16} />
2210 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2211 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
2212 <NavLink
2213 to="/explore"
2214 prefetch="intent"
2215 aria-label="Explore"
2216 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)2217 `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 sidebar2218 }
2219 >
2220 <Compass size={16} className="sm:hidden" />
2221 <span className="hidden sm:inline">Explore</span>
2222 </NavLink>
A Vercel-like sidebar and a new mark2223 <a
2224 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals2225 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 mark2226 >
2227 Docs
2228 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2229 {user && (
2230 <>
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)2231 {shell.workspace ? <AskG1tButton slug={shell.workspace.slug} /> : <AgentButton />}
2232 {/* On a phone the Inbox is a tab. */}
2233 <span className={shell.workspace ? "max-md:hidden" : undefined}>
2234 <InboxBell counts={shell.inbox ? { ...shell.inbox, unread: inboxUnread } : null} />
2235 </span>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2236 </>
2237 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2238 {!user ? (
2239 // The sidebar has these too, but on a phone it is folded away.
2240 <Link
2241 to={withNext("/login", here)}
2242 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"
2243 >
2244 Sign in
2245 </Link>
2246 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2247 <>
2248 {/* Room between what comes in (the inbox) and what you make. */}
2249 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell2250 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2251 <Tooltip>
2252 <TooltipTrigger asChild>
2253 <DropdownMenuTrigger
2254 aria-label="Create new"
2255 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"
2256 >
2257 <Plus size={16} />
2258 <ChevronDown size={13} className="text-muted" />
2259 </DropdownMenuTrigger>
2260 </TooltipTrigger>
2261 <TooltipContent>Create new…</TooltipContent>
2262 </Tooltip>
2263 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell2264 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2265 <>
2266 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
2267 <DropdownMenuItem asChild>
2268 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
2269 <CircleDot />
2270 New issue
2271 </Link>
2272 </DropdownMenuItem>
2273 {shell.repo.member && (
2274 <DropdownMenuItem asChild>
2275 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
2276 <ListTree />
2277 Plan work
2278 </Link>
2279 </DropdownMenuItem>
2280 )}
2281 <DropdownMenuSeparator />
2282 </>
Agents as a team: lifecycle, merge queue, billing and a new shell2283 )}
2284 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2285 <Link to="/?agent=new">
2286 <Sparkles />
2287 Put an agent on it
2288 </Link>
2289 </DropdownMenuItem>
2290 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2291 <Link to="/new">
Projects: what a workspace builds and runs, first on every page2292 <Box />
2293 New project
Agents as a team: lifecycle, merge queue, billing and a new shell2294 </Link>
2295 </DropdownMenuItem>
2296 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2297 <Link to="/new/github">
2298 <GitBranch />
2299 Import from GitHub
2300 </Link>
2301 </DropdownMenuItem>
2302 <DropdownMenuSeparator />
2303 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2304 <Link to="/workspaces/new">
2305 <Users />
2306 New workspace
2307 </Link>
2308 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'2309 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
2310 {shell.workspace &&
2311 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
2312 <DropdownMenuItem asChild>
2313 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
2314 <UsersRound />
2315 New team
2316 </Link>
2317 </DropdownMenuItem>
2318 ) : (
2319 <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 bar2320 <UsersRound />
2321 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'2322 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
2323 </DropdownMenuItem>
2324 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell2325 </DropdownMenuContent>
2326 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2327 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2328 )}
Agents as a team: lifecycle, merge queue, billing and a new shell2329 </div>
2330 </header>
2331 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2332 <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 in2333 <InMain.Provider value={true}>{children}</InMain.Provider>
Merge branch 'worktree-agent-aca4790ea223aa235'2334 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell2335 </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)2336 {user && ws && <MobileTabBar workspace={ws} unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }} />}
Search across all of g1t, Explore, and a command palette2337 <CommandPalette
2338 open={palette}
2339 onOpenChange={setPalette}
2340 commands={commands}
2341 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
2342 />
Agents as a team: lifecycle, merge queue, billing and a new shell2343 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2344 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell2345 );
2346}

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