Skip to content
2,332 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 && (
1612 <SidebarLink to={`/${slug}/-/settings`} icon={<Settings size={15} />}>
1613 General
1614 </SidebarLink>
1615 )}
1616 <SidebarLink to={`/${slug}/-/repositories`} icon={<BookMarked size={15} />}>
1617 Repositories
1618 </SidebarLink>
1619 <SidebarLink to={`/${slug}/-/tokens`} icon={<KeyRound size={15} />}>
1620 Access tokens
1621 </SidebarLink>
1622 {owner && (
1623 <SidebarLink to={`/${slug}/-/personal-access-tokens`} icon={<UserRoundKey size={15} />}>
1624 Personal access tokens
1625 </SidebarLink>
1626 )}
1627 <SidebarLink to={`/${slug}/-/webhooks`} icon={<Webhook size={15} />}>
1628 Webhooks
1629 </SidebarLink>
1630 <SidebarLink to={`/${slug}/-/emoji`} icon={<Smile size={15} />}>
1631 Emoji
1632 </SidebarLink>
1633 </div>
1634 <SidebarGroup title="Runs" className="mt-3">
1635 <SidebarLink to={`/${slug}/-/secrets`} icon={<Lock size={15} />}>
1636 Secrets and variables
1637 </SidebarLink>
1638 <SidebarLink to={`/${slug}/-/actions`} icon={<PlayCircle size={15} />}>
1639 Actions
1640 </SidebarLink>
1641 {owner && (
1642 <SidebarLink to={`/${slug}/-/runners`} icon={<ServerCog size={15} />}>
1643 Runners
1644 </SidebarLink>
1645 )}
1646 </SidebarGroup>
1647 </nav>
1648 );
1649 const trail: Level[] = [{ key: "workspace", node: main }];
1650 if (inSettings) trail.push({ key: "workspace:settings", node: settings });
1651 return (
1652 <div className="flex h-full flex-col">
1653 <ModeHeader title="Workspace" onClose={onClose} />
1654 <Drill trail={trail} />
1655 </div>
1656 );
1657}
1658
1659/** Your own settings, beside the rail: under the account menu, not any workspace's. */
1660export function AccountSidebar({ username, onClose }: { username: string; onClose?: () => void }) {
1661 return (
1662 <div className="flex h-full flex-col">
1663 <ModeHeader title="Your account" onClose={onClose} />
1664 <div className="min-h-0 grow">
1665 <AccountSettingsMenu username={username} />
A Vercel-like sidebar and a new mark1666 </div>
1667 </div>
1668 );
1669}
1670
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)1671
A Vercel-like sidebar and a new mark1672/** Words for the sections a path can end in. */
1673const SECTIONS: Record<string, string> = {
1674 issues: "Issues",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1675 milestones: "Milestones",
1676 labels: "Labels",
A Vercel-like sidebar and a new mark1677 pulls: "Pull requests",
1678 queue: "Merge queue",
1679 commits: "Commits",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1680 plans: "Outcomes",
1681 actions: "Workflows",
1682 soon: "Soon",
Deployments: a preview for every pull request, production on g1t.page1683 deployments: "Deployments",
Projects: what a workspace builds and runs, first on every page1684 repository: "Repository",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1685 access: "Access",
1686 invitations: "Invitation",
1687 repositories: "Repositories",
Branches and Tags pages, each file's last commit, and the branch menu on files1688 branches: "Branches",
1689 tags: "Tags",
Compare: any two branches, the commits one has that the other does not, and the diff, with Open a pull request1690 compare: "Compare",
Project dependencies: addresses, preview stacks, Affects, and agents who know1691 dependencies: "Dependencies",
A menu shaped by how g1t works, Soon pages for every promise, and sudo for sales1692 code: "Files",
GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs1693 secrets: "Secrets and variables",
A Vercel-like sidebar and a new mark1694 settings: "Settings",
Navigation: the workspace's own page with tabs, pins and recent in the sidebar1695 people: "People",
1696 projects: "Projects",
1697 teams: "Teams",
A Vercel-like sidebar and a new mark1698 tokens: "Access tokens",
Settings: fine-grained tokens, workspace token Admin, workspace personal access token rules1699 "personal-access-tokens": "Personal access tokens",
A Vercel-like sidebar and a new mark1700 usage: "Usage",
Site: the AI Gateway page's breadcrumb reads "AI Gateway"1701 gateway: "AI Gateway",
Secrets and variables: one list, rows per environment, for workflows and deployments1702 billing: "Billing and plans",
A catalogue of model providers, and settings that feel like settings1703 integrations: "Integrations",
Webhooks: every event, to your own addresses, signed and retried1704 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)1705 emoji: "Emoji",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1706 domains: "Domains",
1707 guardrails: "Guardrails",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1708 rules: "Rules",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1709 audit: "Audit log",
Files pages say Files in the trail and keep the description in About only1710 tree: "Files",
1711 blob: "Files",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1712 agents: "Agents",
1713 context: "Context",
1714 memory: "Memory",
1715 security: "Security",
1716 packages: "Packages",
1717 runners: "Runners",
Merge branch 'worktree-agent-a3abfcce648e87dca'1718 environments: "Environments",
Security matches the workspace's other pages and leaves out deleted repositories; every section has a name in the trail1719 workflows: "Workflows",
1720 observability: "Observability",
1721 insights: "Insights",
1722 sessions: "Sessions",
Chat and workspace agents: channels, DMs and named agents you talk to1723 chat: "Chat",
1724 docs: "Docs",
1725 home: "Home",
1726 "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)1727 overview: "Overview",
1728 workspace: "Workspace",
A Vercel-like sidebar and a new mark1729};
1730
Merge branch 'worktree-agent-a3abfcce648e87dca'1731/** Settings pages whose name differs from the section's of the same word. */
1732const SETTINGS_SECTIONS: Record<string, string> = { branches: "Branches and merging", actions: "Actions" };
1733
A Vercel-like sidebar and a new mark1734/** 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 quotas1735function Breadcrumbs({ pathname, missing, repo }: { pathname: string; missing?: boolean; repo?: ShellData["repo"] }) {
1736 // A 404 names nothing from the address, unless the project is real and
1737 // visible and only something inside it is missing.
1738 const visibleRepo = /^\/([^/]+)\/([^/-][^/]*)(\/|$)/.exec(pathname);
1739 if (missing && !(visibleRepo && sameRepo(repo ?? null, { namespace: visibleRepo[1]!, name: visibleRepo[2]! }))) {
1740 return <span className="text-sm font-medium">Not found</span>;
1741 }
A Vercel-like sidebar and a new mark1742 const parts = pathname.split("/").filter(Boolean);
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1743 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)1744 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 quotas1745 // Your settings: Settings / Emails.
1746 if (parts[0] === "settings") {
1747 const page = accountSettingsPage(pathname);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1748 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 quotas1749 }
A Vercel-like sidebar and a new mark1750 if (reserved.includes(parts[0]!)) {
1751 const words: Record<string, string> = {
Account settings in the same shape as a workspace's1752 settings: "Account",
A Vercel-like sidebar and a new mark1753 explore: "Explore",
Projects live: fixes from walking them end to end1754 new: "New project",
A Vercel-like sidebar and a new mark1755 search: "Search",
1756 workspaces: "New workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1757 policies: "Policies",
1758 security: "Security",
1759 support: "Support",
1760 status: "Status",
1761 invite: "Invite",
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all1762 inbox: "Inbox",
A Vercel-like sidebar and a new mark1763 };
1764 return <span className="text-sm font-medium">{words[parts[0]!]}</span>;
1765 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1766 // A person's profile, by their handle.
1767 if (parts[0] === "u" && parts[1]) {
1768 return <span className="truncate font-mono text-[0.8125rem] font-medium">@{parts[1]}</span>;
1769 }
A Vercel-like sidebar and a new mark1770 const [owner, second, third, fourth] = parts;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1771 const trail: Crumb[] = [{ label: owner!, to: `/${owner}`, mono: true }];
A Vercel-like sidebar and a new mark1772 if (second === "-") {
A catalogue of model providers, and settings that feel like settings1773 const page = `/${owner}/-/${third}`;
1774 if (third && SETTINGS_PAGE.test(page)) {
1775 trail.push({ label: "Settings", to: `/${owner}/-/settings` });
1776 trail.push({ label: third === "settings" ? "General" : (SECTIONS[third] ?? third), to: page });
1777 } else if (third) trail.push({ label: SECTIONS[third] ?? third, to: page });
A Vercel-like sidebar and a new mark1778 } else if (second) {
1779 const repo = `/${owner}/${second}`;
1780 trail.push({ label: second, to: repo, mono: true });
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1781 if (third === "pull" && fourth) trail.push({ label: `Pull request #${fourth}`, short: `#${fourth}`, to: `${repo}/pull/${fourth}` });
1782 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 mark1783 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 deployments1784 else if (third && SECTIONS[third]) {
1785 trail.push({ label: SECTIONS[third]!, to: `${repo}/${third}` });
1786 // A settings page names which one: Settings / Secrets and variables.
1787 if (third === "settings" && fourth && SECTIONS[fourth]) {
Merge branch 'worktree-agent-a3abfcce648e87dca'1788 trail.push({ label: SETTINGS_SECTIONS[fourth] ?? SECTIONS[fourth]!, to: `${repo}/settings/${fourth}` });
Secrets and variables: one list, rows per environment, for workflows and deployments1789 }
1790 }
A Vercel-like sidebar and a new mark1791 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1792 return <Trail trail={trail} />;
1793}
1794
1795/** One link of the trail; `short` is what a phone shows when it is the page itself. */
1796type Crumb = { label: string; to: string; mono?: boolean; short?: string };
1797
1798/**
1799 * The trail of links. On a phone there is room for one: the page itself,
1800 * with a way back to where it sits; the whole trail from a wider screen up.
1801 */
1802function 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)1803 // Up on a phone, unless up is the workspace itself: its pages are the tabs.
1804 const up = trail.length > 1 && !/^\/[^/]+$/.test(trail[trail.length - 2]!.to) ? trail[trail.length - 2]! : null;
A Vercel-like sidebar and a new mark1805 return (
1806 <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 audit1807 {up && (
1808 <Link
1809 to={up.to}
1810 prefetch="intent"
1811 aria-label={`Back to ${up.label}`}
1812 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"
1813 >
1814 <ChevronLeft size={16} />
1815 </Link>
1816 )}
1817 {trail.map((crumb, index) => {
1818 const last = index === trail.length - 1;
1819 return (
1820 <span key={index} className={`min-w-0 items-center gap-1.5 ${last ? "flex" : "hidden sm:flex"}`}>
1821 {index > 0 && <span className="hidden text-line-strong sm:inline">/</span>}
1822 <Link
1823 to={crumb.to}
1824 prefetch="intent"
1825 aria-current={last ? "page" : undefined}
1826 className={`truncate rounded px-1 py-0.5 transition-colors hover:bg-raised ${
1827 last ? "font-medium text-fg" : "text-muted hover:text-fg"
1828 } ${crumb.mono ? "font-mono text-[0.8125rem]" : ""}`}
1829 >
1830 {last && crumb.short ? (
1831 <>
1832 <span className="sm:hidden">{crumb.short}</span>
1833 <span className="hidden sm:inline">{crumb.label}</span>
1834 </>
1835 ) : (
1836 crumb.label
1837 )}
1838 </Link>
1839 </span>
1840 );
1841 })}
A Vercel-like sidebar and a new mark1842 </nav>
Agents as a team: lifecycle, merge queue, billing and a new shell1843 );
1844}
1845
Search across all of g1t, Explore, and a command palette1846type Command = PaletteCommand;
Agents as a team: lifecycle, merge queue, billing and a new shell1847
1848/** 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 look1849function commandsFor(user: User | null, shell: ShellData, here: string, signUpLabel = "Sign up"): Command[] {
1850 if (!user) return visitorCommands(shell, here, signUpLabel);
Agents as a team: lifecycle, merge queue, billing and a new shell1851 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)1852 { label: "Home", to: "/", icon: <House size={15} /> },
1853 ...(shell.workspace
1854 ? [
1855 { label: "Chat", to: `/${shell.workspace.slug}/-/chat`, icon: <MessagesSquare size={15} /> },
1856 { label: "Agents", to: `/${shell.workspace.slug}/-/agents`, icon: <Sparkles size={15} /> },
1857 ...(hasCodeAccess(shell.workspace) ? [{ label: "Code overview", to: `/${shell.workspace.slug}/-/overview`, icon: <Code2 size={15} /> }] : []),
1858 { label: "Workspace", hint: "People, billing, policies, settings", to: `/${shell.workspace.slug}/-/workspace`, icon: <Building2 size={15} /> },
1859 ]
1860 : []),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1861 ...(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-agent1862 ? [{ 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 step1863 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1864 { label: "Explore repositories", to: "/explore", icon: <Compass size={15} /> },
Search across all of g1t, Explore, and a command palette1865 { 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 all1866 { label: "Inbox", hint: "What needs you", to: "/inbox", icon: <Bell size={15} /> },
Projects: what a workspace builds and runs, first on every page1867 { label: "New project", to: "/new", icon: <Plus size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1868 { 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 quotas1869 { label: "Your settings", to: FIRST_SETTINGS_PAGE, icon: <Settings size={15} /> },
1870 ...(Object.keys(ACCOUNT_SETTINGS) as AccountSettingsPage[])
1871 .filter((page) => page !== "profile")
1872 .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 shell1873 ];
1874 const repo = shell.repo;
1875 if (repo) {
1876 const base = `/${repo.namespace}/${repo.name}`;
1877 const name = `${repo.namespace}/${repo.name}`;
1878 commands.unshift(
Projects: what a workspace builds and runs, first on every page1879 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1880 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1881 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1882 { 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 step1883 ...(repo.member
1884 ? [{ label: `Put an agent on ${repo.name}`, hint: name, to: `${base}/issues/new?agent=1`, icon: <Sparkles size={15} /> }]
1885 : []),
Agents as a team: lifecycle, merge queue, billing and a new shell1886 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1887 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1888 ...(repo.member
1889 ? [
1890 { label: "Plan work", hint: name, to: `${base}/plans`, icon: <ListTree size={15} /> },
Projects: what a workspace builds and runs, first on every page1891 { label: "Deployments", hint: name, to: `${base}/deployments`, icon: <Rocket size={15} /> },
1892 { label: "Secrets and variables", hint: name, to: `${base}/settings/secrets`, icon: <Lock size={15} /> },
1893 { label: "Project settings", hint: name, to: `${base}/settings`, icon: <Settings size={15} /> },
1894 { label: "Repository settings", hint: name, to: `${base}/settings/repository`, icon: <Settings size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1895 ]
1896 : []),
1897 );
1898 }
1899 for (const membership of user.workspaces ?? []) {
1900 commands.push(
Workspace names and icons, and a component kit for every control1901 { 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 sidebar1902 { label: "All projects", hint: membership.slug, to: `/${membership.slug}/-/projects`, icon: <LayoutGrid size={15} /> },
1903 { 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 request1904 { 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 tokens1905 { 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 page1906 { 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 settings1907 { 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 look1908 { label: "Integrations", hint: membership.slug, to: `/${membership.slug}/-/integrations`, icon: <Plug size={15} /> },
Agents as a team: lifecycle, merge queue, billing and a new shell1909 );
1910 }
1911 for (const listed of shell.repos) {
1912 commands.push({
Projects: what a workspace builds and runs, first on every page1913 label: listed.title ?? listed.name,
1914 hint: `${listed.namespace} · Project`,
Agents as a team: lifecycle, merge queue, billing and a new shell1915 to: `/${listed.namespace}/${listed.name}`,
Projects: what a workspace builds and runs, first on every page1916 icon: <Box size={15} />,
Agents as a team: lifecycle, merge queue, billing and a new shell1917 });
1918 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1919 // "Put an agent on …": one per project, after the projects themselves.
1920 for (const listed of shell.repos) {
1921 if (repo && listed.namespace === repo.namespace && listed.name === repo.name) continue;
1922 commands.push({
1923 label: `Put an agent on ${listed.title ?? listed.name}`,
1924 hint: `${listed.namespace}/${listed.name}`,
1925 to: `/${listed.namespace}/${listed.name}/issues/new?agent=1`,
1926 icon: <Sparkles size={15} />,
1927 });
1928 }
Agents as a team: lifecycle, merge queue, billing and a new shell1929 return commands;
1930}
1931
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1932/** 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 look1933function 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 put1934 const commands: Command[] = [];
1935 const repo = shell.repo;
1936 if (repo) {
1937 const base = `/${repo.namespace}/${repo.name}`;
1938 const name = `${repo.namespace}/${repo.name}`;
1939 commands.push(
1940 { label: "Overview", hint: name, to: base, icon: <LayoutGrid size={15} /> },
1941 { label: "Code", hint: name, to: `${base}/code`, icon: <Code2 size={15} /> },
1942 { label: "Issues", hint: name, to: `${base}/issues`, icon: <CircleDot size={15} /> },
1943 { label: "Pull requests", hint: name, to: `${base}/pulls`, icon: <GitPullRequest size={15} /> },
1944 { label: "Commits", hint: name, to: `${base}/commits`, icon: <History size={15} /> },
1945 );
1946 }
1947 commands.push(
1948 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
1949 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
1950 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
1951 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
1952 { 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 look1953 { 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 put1954 );
1955 return commands;
1956}
1957
Agents as a team: lifecycle, merge queue, billing and a new shell1958/**
Merge branch 'worktree-agent-aca4790ea223aa235'1959 * The page while the next one loads: after a moment it dims, under the
1960 * bar below, so it is plainly on its way out rather than frozen, and the
1961 * next page fades up from there. Sending a form does not dim it: the
1962 * form's button says it is working. Spread on the element holding the page.
1963 */
1964export function useLeaving() {
1965 const navigation = useNavigation();
1966 const method = navigation.formMethod?.toUpperCase();
1967 const leaving = navigation.state === "loading" && (method == null || method === "GET");
1968 return {
1969 "aria-busy": leaving || undefined,
1970 className: `transition-opacity motion-reduce:transition-none ${leaving ? "opacity-60 delay-200 duration-300" : "duration-150"}`,
1971 };
1972}
1973
1974/**
Agents as a team: lifecycle, merge queue, billing and a new shell1975 * A bar across the top of the page while the next one loads, as GitHub
1976 * has: it appears at once, creeps towards the end while waiting, then fills
1977 * and fades when the page arrives.
1978 */
1979export function Progress() {
1980 const navigation = useNavigation();
1981 const busy = navigation.state !== "idle";
1982 const [width, setWidth] = useState(0);
1983 const [visible, setVisible] = useState(false);
1984 useEffect(() => {
1985 if (busy) {
1986 setVisible(true);
1987 setWidth(12);
1988 // Slows as it goes, never quite arriving until the page does.
1989 const timer = setInterval(() => setWidth((now) => now + (90 - now) * 0.12), 200);
1990 return () => clearInterval(timer);
1991 }
1992 setWidth((now) => (now > 0 ? 100 : 0));
1993 const fade = setTimeout(() => setVisible(false), 250);
1994 const reset = setTimeout(() => setWidth(0), 550);
1995 return () => {
1996 clearTimeout(fade);
1997 clearTimeout(reset);
1998 };
1999 }, [busy]);
2000 return (
2001 <div
2002 aria-hidden="true"
2003 className={`pointer-events-none fixed inset-x-0 top-0 z-[60] h-[3px] transition-opacity duration-300 ${
2004 visible ? "opacity-100" : "opacity-0"
2005 }`}
2006 >
2007 <div
2008 className="h-full bg-accent shadow-[0_0_10px_var(--color-accent)] transition-[width] duration-200 ease-out"
2009 style={{ width: `${width}%` }}
2010 />
2011 </div>
2012 );
2013}
2014
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)2015/** The top bar's way to g1t: the direct message with the workspace's orchestrator, opened or made. */
2016function AskG1tButton({ slug }: { slug: string }) {
2017 return (
2018 <Hint label="Ask g1t">
2019 <Link
2020 to={`/${slug}/-/chat?agent=g1t`}
2021 aria-label="Ask g1t"
2022 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"
2023 >
2024 <G1tMark size={18} />
2025 <span className="hidden sm:inline">Ask g1t</span>
2026 </Link>
2027 </Hint>
2028 );
2029}
2030
Chat and workspace agents: channels, DMs and named agents you talk to2031/** The mode's sidebar beside the rail: a shade lighter than the page. */
2032const SIDEBAR_BOX = "h-full border-r border-line bg-[color-mix(in_srgb,var(--color-surface)_70%,var(--color-bg))]";
2033
Agents as a team: lifecycle, merge queue, billing and a new shell2034/**
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)2035 * Which sidebar sits beside the rail: each mode's own, or none (Docs, and
2036 * the Inbox, which is a page of its own). Without a workspace (a visitor),
2037 * the one sidebar there always was.
Chat and workspace agents: channels, DMs and named agents you talk to2038 */
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)2039function sidebarFor(mode: ModeKey | null): Panel | null {
Chat and workspace agents: channels, DMs and named agents you talk to2040 if (mode == null) return "code";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)2041 if (mode === "docs" || mode === "inbox") return null;
2042 return mode;
Chat and workspace agents: channels, DMs and named agents you talk to2043}
2044
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)2045/** The sidebars that sit beside the rail, one per mode that has one. */
2046type Panel = "home" | "chat" | "agents" | "code" | "workspace" | "account";
2047
Chat and workspace agents: channels, DMs and named agents you talk to2048/**
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2049 * The app: a sidebar with the workspace, its repositories and the sections
2050 * of the one being looked at; a slim bar with search and the account; and
2051 * the page. A visitor who is not signed in gets the same frame, with
2052 * Explore and Search in place of the workspace, and signing in in place of
2053 * the account.
Agents as a team: lifecycle, merge queue, billing and a new shell2054 */
2055export function AppShell({
2056 user,
2057 shell,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2058 missing = false,
Agents as a team: lifecycle, merge queue, billing and a new shell2059 banner,
2060 children,
2061}: {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2062 user: User | null;
Agents as a team: lifecycle, merge queue, billing and a new shell2063 shell: ShellData;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2064 /** The page is a 404 (see Sidebar). */
2065 missing?: boolean;
Agents as a team: lifecycle, merge queue, billing and a new shell2066 banner?: ReactNode;
2067 children: ReactNode;
2068}) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2069 const { pathname, search } = useLocation();
Agents as a team: lifecycle, merge queue, billing and a new shell2070 const [drawer, setDrawer] = useState(false);
2071 const [palette, setPalette] = useState(false);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2072 const here = pathname + search;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look2073 const signUpLabel = useSignUpCopy().primary;
2074 const commands = useMemo(() => commandsFor(user, shell, here, signUpLabel), [user, shell, here, signUpLabel]);
Agents as a team: lifecycle, merge queue, billing and a new shell2075
2076 // A new page closes the drawer on small screens.
2077 useEffect(() => setDrawer(false), [pathname]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents2078 // Mission control's code, fetched while the browser is idle, so going
2079 // home never waits on it (routes/home.tsx).
2080 useEffect(() => {
2081 if (!user) return;
2082 const load = () => void import("./mission-control").catch(() => undefined);
2083 const idle = (window as { requestIdleCallback?: (callback: () => void) => number }).requestIdleCallback;
2084 if (idle) idle(load);
2085 else setTimeout(load, 1500);
2086 }, [user]);
Search across all of g1t, Explore, and a command palette2087 usePaletteShortcut(() => setPalette((open) => !open));
Merge branch 'worktree-agent-aca4790ea223aa235'2088 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)2089 useVisualViewport();
Agents as a team: lifecycle, merge queue, billing and a new shell2090
Chat and workspace agents: channels, DMs and named agents you talk to2091 // The rail and the mode's sidebar (docs/WORKSPACE.md, "Shell"), for
2092 // someone in a workspace. A visitor, or someone with none, keeps the one
2093 // sidebar.
2094 const going = useNavigation().location?.pathname;
2095 const ws = user ? shell.workspace : null;
2096 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)2097 const panel = ws ? sidebarFor(mode) : "code";
2098 const code = ws ? hasCodeAccess(ws) : true;
Chat and workspace agents: channels, DMs and named agents you talk to2099 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)2100 // Notify: the feed socket's live counts (lib/notify-client.ts) once it has them; the page's until then.
2101 const live = useLiveBadges(ws?.slug);
2102 const chatUnread = chatSidebar
2103 ? unreadTotals(chatSidebar.entries)
2104 : live?.chat != null
2105 ? { unread: live.chat, mentions: live.mentions ?? 0 }
2106 : (shell.chat ?? { unread: 0, mentions: 0 });
2107 const inboxUnread = live?.inbox ?? shell.inbox?.unread ?? 0;
2108 const sidebarNode = (inSheet: boolean) => {
2109 const close = inSheet ? () => setDrawer(false) : undefined;
2110 const find = () => {
2111 if (inSheet) setDrawer(false);
2112 setPalette(true);
2113 };
2114 if (!ws || !user || panel === "code") {
2115 return <Sidebar user={user} shell={shell} missing={missing} onFind={find} onClose={close} rail={Boolean(ws && user)} />;
2116 }
2117 switch (panel) {
2118 case "home":
2119 return <HomeSidebar slug={ws.slug} shell={shell} code={code} onFind={find} header={<ModeHeader title="Home" onClose={close} />} />;
2120 case "chat":
2121 return <ChatSidebar slug={ws.slug} />;
2122 case "agents":
2123 return <AgentsSidebar slug={ws.slug} shellAgents={shell.agents ?? null} code={code} owner={ws.role === "owner"} />;
2124 case "workspace":
2125 return <WorkspaceSidebar slug={ws.slug} owner={ws.role === "owner"} onClose={close} />;
2126 case "account":
2127 return <AccountSidebar username={user.username} onClose={close} />;
2128 default:
2129 return null;
2130 }
2131 };
Chat and workspace agents: channels, DMs and named agents you talk to2132 const rail =
2133 user && ws ? (
2134 <Rail
2135 user={user}
2136 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)2137 unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }}
2138 help={<HelpMenu />}
Chat and workspace agents: channels, DMs and named agents you talk to2139 account={<AccountMenu user={user} rail />}
2140 />
2141 ) : null;
2142 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)2143 // On a phone (below 768px): the tab bar, and a conversation full screen.
2144 const conversation = isConversation(going ?? pathname);
2145 const tabs = Boolean(user && ws) && !conversation;
Chat and workspace agents: channels, DMs and named agents you talk to2146
Agents as a team: lifecycle, merge queue, billing and a new shell2147 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2148 // The phone's menu is a sheet: a dialog that holds focus, closes on
2149 // Escape or a tap outside, and gives focus back to the menu button.
2150 <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)2151 <div className="min-h-dvh">
Agents as a team: lifecycle, merge queue, billing and a new shell2152 <Progress />
Chat and workspace agents: channels, DMs and named agents you talk to2153 <aside className="fixed inset-y-0 left-0 z-40 hidden lg:flex">
2154 {rail}
2155 {panel && <div className={`${SIDEBAR_BOX} w-64`}>{sidebarNode(false)}</div>}
Agents as a team: lifecycle, merge queue, billing and a new shell2156 </aside>
Chat and workspace agents: channels, DMs and named agents you talk to2157 <SheetContent
2158 side="left"
2159 showClose={false}
2160 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)2161 // Focus would land on the workspace's avatar and open its hint over
2162 // the sidebar's heading; the sheet itself takes it instead.
2163 onOpenAutoFocus={(event) => {
2164 event.preventDefault();
2165 (event.currentTarget as HTMLElement | null)?.focus();
2166 }}
Chat and workspace agents: channels, DMs and named agents you talk to2167 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"}`}
2168 >
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2169 <SheetTitle className="sr-only">Menu</SheetTitle>
Chat and workspace agents: channels, DMs and named agents you talk to2170 {rail}
2171 {panel && <div className={`min-w-0 grow ${rail ? SIDEBAR_BOX : ""}`}>{sidebarNode(true)}</div>}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2172 </SheetContent>
Agents as a team: lifecycle, merge queue, billing and a new shell2173
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)2174 <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 to2175 <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)2176 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 to2177 >
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)2178 {/* A phone: the workspace's avatar opens everything else (components/mobile.tsx). */}
2179 {user && ws && <AvatarSheetButton user={user} workspace={ws} />}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2180 <SheetTrigger
Agents as a team: lifecycle, merge queue, billing and a new shell2181 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)2182 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 shell2183 >
2184 <Menu size={18} />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2185 </SheetTrigger>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas2186 <Breadcrumbs pathname={pathname} missing={missing} repo={shell.repo} />
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2187 <div className="ml-auto flex items-center gap-1.5">
2188 {/* Search lives in the sidebar ("Search or jump to"); with the sidebar folded away, this opens the same palette. */}
2189 <button
2190 type="button"
2191 aria-label="Search or jump to"
2192 onClick={() => setPalette(true)}
2193 className="flex size-9 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg lg:hidden"
2194 >
2195 <Search size={16} />
2196 </button>
Navigation: the workspace's own page with tabs, pins and recent in the sidebar2197 {/* All of g1t's public projects: not any one workspace's, so here, not in the sidebar. */}
2198 <NavLink
2199 to="/explore"
2200 prefetch="intent"
2201 aria-label="Explore"
2202 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)2203 `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 sidebar2204 }
2205 >
2206 <Compass size={16} className="sm:hidden" />
2207 <span className="hidden sm:inline">Explore</span>
2208 </NavLink>
A Vercel-like sidebar and a new mark2209 <a
2210 href="https://docs.g1t.sh/"
Two limits, real invoices, trust that grows by itself, sales signals2211 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 mark2212 >
2213 Docs
2214 </a>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2215 {user && (
2216 <>
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 {shell.workspace ? <AskG1tButton slug={shell.workspace.slug} /> : <AgentButton />}
2218 {/* On a phone the Inbox is a tab. */}
2219 <span className={shell.workspace ? "max-md:hidden" : undefined}>
2220 <InboxBell counts={shell.inbox ? { ...shell.inbox, unread: inboxUnread } : null} />
2221 </span>
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all2222 </>
2223 )}
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2224 {!user ? (
2225 // The sidebar has these too, but on a phone it is folded away.
2226 <Link
2227 to={withNext("/login", here)}
2228 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"
2229 >
2230 Sign in
2231 </Link>
2232 ) : (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2233 <>
2234 {/* Room between what comes in (the inbox) and what you make. */}
2235 <span aria-hidden="true" className="mx-1 h-5 w-px bg-line" />
Agents as a team: lifecycle, merge queue, billing and a new shell2236 <DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2237 <Tooltip>
2238 <TooltipTrigger asChild>
2239 <DropdownMenuTrigger
2240 aria-label="Create new"
2241 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"
2242 >
2243 <Plus size={16} />
2244 <ChevronDown size={13} className="text-muted" />
2245 </DropdownMenuTrigger>
2246 </TooltipTrigger>
2247 <TooltipContent>Create new…</TooltipContent>
2248 </Tooltip>
2249 <DropdownMenuContent align="end" className="min-w-52">
Agents as a team: lifecycle, merge queue, billing and a new shell2250 {shell.repo && (
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2251 <>
2252 <DropdownMenuLabel className="truncate">In {shell.repo.name}</DropdownMenuLabel>
2253 <DropdownMenuItem asChild>
2254 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/issues/new`}>
2255 <CircleDot />
2256 New issue
2257 </Link>
2258 </DropdownMenuItem>
2259 {shell.repo.member && (
2260 <DropdownMenuItem asChild>
2261 <Link to={`/${shell.repo.namespace}/${shell.repo.name}/plans`}>
2262 <ListTree />
2263 Plan work
2264 </Link>
2265 </DropdownMenuItem>
2266 )}
2267 <DropdownMenuSeparator />
2268 </>
Agents as a team: lifecycle, merge queue, billing and a new shell2269 )}
2270 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2271 <Link to="/?agent=new">
2272 <Sparkles />
2273 Put an agent on it
2274 </Link>
2275 </DropdownMenuItem>
2276 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2277 <Link to="/new">
Projects: what a workspace builds and runs, first on every page2278 <Box />
2279 New project
Agents as a team: lifecycle, merge queue, billing and a new shell2280 </Link>
2281 </DropdownMenuItem>
2282 <DropdownMenuItem asChild>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2283 <Link to="/new/github">
2284 <GitBranch />
2285 Import from GitHub
2286 </Link>
2287 </DropdownMenuItem>
2288 <DropdownMenuSeparator />
2289 <DropdownMenuItem asChild>
Agents as a team: lifecycle, merge queue, billing and a new shell2290 <Link to="/workspaces/new">
2291 <Users />
2292 New workspace
2293 </Link>
2294 </DropdownMenuItem>
Merge branch 'worktree-agent-ad7c6d88d93adc817'2295 {/* A team in the workspace the sidebar is about, for whoever it lets create one. */}
2296 {shell.workspace &&
2297 (mayCreateTeams(shell.workspace.team_creation, shell.workspace.role) ? (
2298 <DropdownMenuItem asChild>
2299 <Link to={`/${shell.workspace.slug}/-/teams/new`}>
2300 <UsersRound />
2301 New team
2302 </Link>
2303 </DropdownMenuItem>
2304 ) : (
2305 <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 bar2306 <UsersRound />
2307 New team
Merge branch 'worktree-agent-ad7c6d88d93adc817'2308 <span className="ml-auto pl-3 text-xs text-faint">Owners only</span>
2309 </DropdownMenuItem>
2310 ))}
Agents as a team: lifecycle, merge queue, billing and a new shell2311 </DropdownMenuContent>
2312 </DropdownMenu>
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar2313 </>
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put2314 )}
Agents as a team: lifecycle, merge queue, billing and a new shell2315 </div>
2316 </header>
2317 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2318 <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 in2319 <InMain.Provider value={true}>{children}</InMain.Provider>
Merge branch 'worktree-agent-aca4790ea223aa235'2320 </main>
Agents as a team: lifecycle, merge queue, billing and a new shell2321 </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)2322 {user && ws && <MobileTabBar workspace={ws} unread={{ inbox: inboxUnread, chat: chatUnread.unread, mentions: chatUnread.mentions }} />}
Search across all of g1t, Explore, and a command palette2323 <CommandPalette
2324 open={palette}
2325 onOpenChange={setPalette}
2326 commands={commands}
2327 repo={shell.repo ? `${shell.repo.namespace}/${shell.repo.name}` : null}
2328 />
Agents as a team: lifecycle, merge queue, billing and a new shell2329 </div>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit2330 </Sheet>
Agents as a team: lifecycle, merge queue, billing and a new shell2331 );
2332}

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