Skip to content
726 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.

Workspaces own repositories1import {
2 BookOpen,
3 ChevronDown,
Search across all of g1t, Explore, and a command palette4 Compass,
5 CreditCard,
Workspaces own repositories6 LayoutDashboard,
Search across all of g1t, Explore, and a command palette7 LogIn,
Workspaces own repositories8 LogOut,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9 Menu,
Workspaces own repositories10 Plus,
11 Search,
12 Settings,
13} from "lucide-react";
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today14import { useEffect, useRef, useState } from "react";
Workspaces own repositories15import {
16 Form,
17 isRouteErrorResponse,
18 Link,
19 Links,
20 Meta,
21 NavLink,
22 Outlet,
23 Scripts,
24 ScrollRestoration,
Agents as a team: lifecycle, merge queue, billing and a new shell25 type ShouldRevalidateFunctionArgs,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put26 useLocation,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit27 useMatches,
Search across all of g1t, Explore, and a command palette28 useParams,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas29 useRouteError,
Workspaces own repositories30 useRouteLoaderData,
Acceptance checks in sandboxes, line comments and review verdicts31 useSubmit,
Workspaces own repositories32} from "react-router";
33
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)34import { type User, awaitsConfirmation, hasAccessIn, sharedWorkspaces } from "@g1t/contracts";
Workspaces own repositories35
36import type { Route } from "./+types/root";
Load the stylesheet first; plain legal contact details37import appCss from "./app.css?url";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look38import displayFont from "@g1t/theme/fonts/bricolage-grotesque-latin.woff2?url";
39import sansFont from "@g1t/theme/fonts/hanken-grotesk-latin.woff2?url";
Workspaces own repositories40import { Logo } from "./components/logo";
Merge branch 'worktree-agent-a985c01f43538af01'41import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui";
Workspaces own repositories42import {
43 DropdownMenu,
44 DropdownMenuContent,
45 DropdownMenuItem,
46 DropdownMenuLabel,
47 DropdownMenuSeparator,
48 DropdownMenuTrigger,
49} from "./components/ui/dropdown-menu";
Merge branch 'worktree-agent-aca4790ea223aa235'50import { AppShell, Progress, type ShellData, useLeaving } from "./components/shell";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51import { SiteFooter } from "./components/footer";
Fast pages, required checks on the branch, self-hosted runners, honest incidents52import { SpikeBanner } from "./components/spike-banner";
Merge main (membership, two-factor, GitHub repo roles) into tokens53import { PolicyNotice } from "./components/policy-notice";
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today54import { identify } from "./lib/analytics.client";
55import { visitorAsksFirst } from "./lib/analytics-consent";
56import { AnalyticsConsent } from "./components/analytics-consent";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put57import { readCookie } from "./lib/mission";
58import { WORKSPACE_COOKIE, workspaceFor } from "./lib/workspace-choice";
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in59import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put60import { NotFound } from "./components/not-found";
61import { usesAppShell } from "./lib/chrome";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit62import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./components/command-palette";
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)63import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to64import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents65import { countsFor, readableRepos } from "./lib/access.server";
66import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar68import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'70import { addresses } from "./lib/addresses.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look71import { useSignUpCopy } from "./lib/registration";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit72import { RELOADED_KEY, reloadFixes } from "./lib/stale-build";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address73import { useNonce } from "./lib/nonce";
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)74import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75
Workspaces own repositories76
77export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark78 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Workspaces own repositories79 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark80 { rel: "icon", type: "image/png", sizes: "192x192", href: "/icon-192.png" },
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers81 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
82 // Home screens and password managers look for a square, opaque 180px tile;
83 // the "-precomposed" name is served too, for the ones that ask for it.
84 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
85 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
86 // manifest.webmanifest is the same file, kept for installs that already point at it).
87 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look88 // The text and headline faces are wanted on every page, so they start
89 // loading with the stylesheet; mono waits until something uses it.
90 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
91 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Workspaces own repositories92];
93
94export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
95
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put96export async function loader({ context, params, request }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell97 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put98 const chosen = readCookie(request.headers.get("cookie"), WORKSPACE_COOKIE);
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)99 // Whether sign-up takes an invite: the sign-up page says so, and
100 // Settings → Invites offers invites to g1t only then. Cached per isolate.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look101 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)102 // An account still confirming its address sees only the pages that
103 // allows (lib/confirm-gate.ts), in the visitor's frame.
104 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit105 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
106 // A sidebar that could not be read is no reason to lose the page,
107 // or to draw it as if they were signed out.
108 console.error("root: the sidebar could not be read", error);
109 return bareShell(user, params, chosen);
110 })
111 : visitorShell(params, context),
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)112 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look113 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'114 // Where this g1t lives, for clone lines, agent setup and link previews.
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today115 return {
116 user,
117 shell,
118 inviteOnly: mode !== "open",
119 addresses: addresses(),
120 // Visitors from where the law asks first are asked before analytics runs.
121 analyticsConsent: visitorAsksFirst(request),
122 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put123}
124
125/**
126 * The sidebar for someone not signed in: nothing of their own, only the
127 * open counts of the project being looked at. One call, and only in a
128 * project; it answers the same for a private project as a missing one.
129 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents130async function visitorShell(params: { owner?: string; repo?: string }, context: Route.LoaderArgs["context"]): Promise<ShellData> {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put131 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents132 const counts = path ? await countsFor(context, params) : null;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put133 return {
134 workspace: null,
135 repos: [],
136 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
137 limit: null,
138 monthUsageMicros: null,
139 };
Workspaces own repositories140}
141
Usage, Billing settings and prepaid AI credit; fixes from the UX audit142/** The sidebar from what the session already says, with no service asked. */
143function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
144 return {
145 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
146 repos: [],
147 repo: null,
148 limit: null,
149 monthUsageMicros: null,
150 };
151}
152
Agents as a team: lifecycle, merge queue, billing and a new shell153/**
154 * The sidebar only changes with the workspace or repository being looked
155 * at, or after something was submitted: not on every page within them.
156 */
157export function shouldRevalidate({
158 currentParams,
159 nextParams,
160 formMethod,
161 defaultShouldRevalidate,
162}: ShouldRevalidateFunctionArgs) {
163 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
164 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
165}
166
167/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit168 * The sidebar: the workspace whose pages or project these are when it is
169 * one of yours, else the one you chose (lib/workspace-choice.ts); its
170 * projects; and the repository being looked at.
Agents as a team: lifecycle, merge queue, billing and a new shell171 */
172async function shellFor(
173 user: User,
174 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put175 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents176 context: Route.LoaderArgs["context"],
Agents as a team: lifecycle, merge queue, billing and a new shell177): Promise<ShellData> {
178 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put179 const workspace = workspaceFor(memberships, chosen, params);
Agents as a team: lifecycle, merge queue, billing and a new shell180 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request181 const now = new Date();
182 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents183 // What the sidebar shows of the workspace changes rarely: kept for a few
184 // seconds per person and workspace, except just after they changed
185 // something (lib/cache.server.ts).
186 const kept = <T,>(what: string, load: () => Promise<T>) =>
187 workspace ? shortCache(`shell:${what}:${user.id}:${workspace.slug}`, SHELL_TTL_MS, load) : Promise.resolve(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)188 const [listed, counts, status, usage, limit, entitlements, shared, unread, shortcuts, chatUnread, teamAgents] = await Promise.all([
Navigation: the workspace's own page with tabs, pins and recent in the sidebar189 // Every project, for the palette and the count; the sidebar lists only
190 // the person's pinned and recent ones (lib/pins.ts).
191 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents192 path ? countsFor(context, params) : Promise.resolve(null),
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor193 // Whether billing is on, without reading the account: that asks the
194 // card processor about the workspace's cards, too slow for every page.
195 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit196 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents197 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
198 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
199 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all200 // The bell's count: one query, never kept, so marking an item shows at once.
201 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar202 // Never kept: opening a project moves it up Recent.
203 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to204 // The rail's Chat badge: never kept, and never waited on for long.
205 workspace ? chatUnreadFor(workspace.slug, user) : Promise.resolve(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)206 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
207 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Agents as a team: lifecycle, merge queue, billing and a new shell208 ]);
209 return {
210 workspace,
Projects: what a workspace builds and runs, first on every page211 // Projects are what the sidebar lists: what the workspace builds and runs.
Navigation: the workspace's own page with tabs, pins and recent in the sidebar212 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
213 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
214 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Agents as a team: lifecycle, merge queue, billing and a new shell215 repo:
216 path && counts?.ok
217 ? {
218 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look219 // Until the repository's own page says what their role is.
220 member: hasAccessIn(user, path.namespace),
Agents as a team: lifecycle, merge queue, billing and a new shell221 issues: counts.value.issues,
222 pulls: counts.value.pulls,
223 }
224 : null,
Billing accounts, terms and enterprises; g1t is no longer free225 // Where the workspace stands against its usage limit, once billing is on.
226 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor227 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free228 ? {
229 exposureMicros: limit.value.exposureMicros,
230 ceilingMicros: limit.value.ceilingMicros,
231 state: limit.value.state,
232 comped: limit.value.trust === "internal",
233 }
234 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor235 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look236 // A spend spike or a hold pauses new compute: the shell says so on every page.
237 compute:
238 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
239 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
240 : null,
241 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all242 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to243 chat: chatUnread,
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)244 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy245 // While g1t is free every charge is zero, so usage is shown at cost.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit246 // Usage at price, the one figure every page shows.
247 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Agents as a team: lifecycle, merge queue, billing and a new shell248 };
249}
250
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)251/** Starred and recent conversations Home's sidebar lists. */
252const SHELL_CONVERSATIONS = 8;
253
254/**
255 * What is unread in chat, for the rail, and the starred and latest
256 * conversations, for Home's sidebar; null when chat is slow to answer or
257 * does not.
258 */
Chat and workspace agents: channels, DMs and named agents you talk to259async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
260 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
261 const read = chat
262 .sidebar(slug, user)
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)263 .then((result) => {
264 if (!result.ok) return null;
265 const entries = result.value.entries;
266 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
267 return {
268 ...unreadTotals(entries),
269 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
270 recent: latest.slice(0, SHELL_CONVERSATIONS),
271 };
272 })
273 .catch(() => null);
274 return Promise.race([read, timeout]);
275}
276
277/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
278async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
279 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
280 const read = workspaceAgents
281 .list(slug, user)
282 .then((result) =>
283 result.ok
284 ? result.value
285 .filter((agent) => !agent.archived_at)
286 .map((agent) => ({
287 id: agent.id,
288 handle: agent.handle,
289 display_name: agent.display_name,
290 avatar: agent.avatar,
291 avatar_seed: agent.avatar_seed,
292 role: agent.role,
293 title: agent.title,
294 team: agent.team,
295 department: agent.department,
296 status: agent.status,
297 builtin: agent.builtin === true,
298 }))
299 : null,
300 )
Chat and workspace agents: channels, DMs and named agents you talk to301 .catch(() => null);
302 return Promise.race([read, timeout]);
303}
304
305/** The longest the rail's Chat badge holds up a page. */
306const CHAT_BADGE_WAIT_MS = 300;
307
Usage, Billing settings and prepaid AI credit; fixes from the UX audit308/**
309 * How long the sidebar's workspace answers are kept: as long as someone's
310 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
311 */
312const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents313
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look314/** Most repositories listed under Shared with you. */
315const MAX_SHARED = 20;
316
317/**
318 * Repositories shared with the user in workspaces they do not belong to,
319 * by their grants: only those, never the workspace's other repositories.
320 */
321async function sharedRepos(user: User): Promise<ShellData["shared"]> {
322 const outside = new Set(sharedWorkspaces(user));
323 if (outside.size === 0) return [];
324 const ids = (user.grants ?? []).filter((grant) => outside.has(grant.workspace)).map((grant) => grant.repo_id).slice(0, MAX_SHARED);
Fast pages, required checks on the branch, self-hosted runners, honest incidents325 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look326 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents327 .map((repo) => ({ namespace: repo.namespace, name: repo.name, isPrivate: repo.isPrivate }))
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look328 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
329}
330
Workspaces own repositories331function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
332 return (
333 <NavLink
334 to={to}
335 className={({ isActive }) =>
336 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
337 isActive ? "text-fg" : "text-muted"
338 }`
339 }
340 >
341 {children}
342 </NavLink>
343 );
344}
345
Search across all of g1t, Explore, and a command palette346/** What the palette offers someone without the app's sidebar. */
347const PUBLIC_COMMANDS: PaletteCommand[] = [
348 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
349 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
350 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
351 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
352 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
353 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
354];
355
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite356/** Sign up: the sign-up page says whether it takes an invite. */
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look357function SignUpButton() {
358 const copy = useSignUpCopy();
359 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
360}
361
Workspaces own repositories362function Header({ user }: { user: User | null | undefined }) {
Acceptance checks in sandboxes, line comments and review verdicts363 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look364 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette365 const [palette, setPalette] = useState(false);
366 usePaletteShortcut(() => setPalette((open) => !open));
367 const params = useParams();
368 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
Workspaces own repositories369 return (
370 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
371 <div className="mx-auto flex h-14 max-w-6xl items-center gap-2 px-4">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look372 <Link to="/" aria-label="g1t home" className="mr-2 flex">
Workspaces own repositories373 <Logo />
374 </Link>
375 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
376 <Search
377 size={14}
378 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
379 />
380 <input
381 name="q"
Agents as a team: lifecycle, merge queue, billing and a new shell382 {...notACredential()}
Search across all of g1t, Explore, and a command palette383 placeholder="Search g1t"
384 aria-label="Search g1t"
385 className="w-full rounded-md border border-line bg-bg py-1.5 pr-12 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
Workspaces own repositories386 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit387 <PaletteKey className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" />
Workspaces own repositories388 </Form>
Search across all of g1t, Explore, and a command palette389 <button
390 type="button"
391 aria-label="Search g1t"
392 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look393 className="flex size-10 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg sm:hidden"
Search across all of g1t, Explore, and a command palette394 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look395 <Search size={18} />
Search across all of g1t, Explore, and a command palette396 </button>
397 <CommandPalette
398 open={palette}
399 onOpenChange={setPalette}
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)400 commands={user ? [{ label: "Home", to: "/", icon: <LayoutDashboard size={15} /> }, ...PUBLIC_COMMANDS.slice(0, 4)] : PUBLIC_COMMANDS.map((command) => (command.to === "/register" ? { ...command, label: signUp.primary } : command))}
Search across all of g1t, Explore, and a command palette401 repo={repo}
402 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look403 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
Workspaces own repositories404 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays405 <HeaderLink to="/pricing">Pricing</HeaderLink>
Workspaces own repositories406 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
407 </nav>
408 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look409 {/* On a phone the links fold into one menu, so the bar fits. */}
410 <DropdownMenu>
411 <DropdownMenuTrigger
412 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit413 className="flex size-10 items-center justify-center rounded-md text-muted 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 sm:hidden"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look414 >
415 <Menu size={18} />
416 </DropdownMenuTrigger>
417 <DropdownMenuContent align="end" className="w-52">
418 <DropdownMenuItem asChild className="min-h-11">
419 <Link to="/explore">
420 <Compass />
421 Explore
422 </Link>
423 </DropdownMenuItem>
424 <DropdownMenuItem asChild className="min-h-11">
425 <Link to="/pricing">
426 <CreditCard />
427 Pricing
428 </Link>
429 </DropdownMenuItem>
430 <DropdownMenuItem asChild className="min-h-11">
431 <Link to="https://docs.g1t.sh/">
432 <BookOpen />
433 Docs
434 </Link>
435 </DropdownMenuItem>
436 {!user && (
437 <>
438 <DropdownMenuSeparator />
439 <DropdownMenuItem asChild className="min-h-11">
440 <Link to="/login">
441 <LogIn />
442 Sign in
443 </Link>
444 </DropdownMenuItem>
445 </>
446 )}
447 </DropdownMenuContent>
448 </DropdownMenu>
Workspaces own repositories449 {user ? (
450 <>
451 <Link
452 to="/new"
Projects live: fixes from walking them end to end453 aria-label="New project"
Workspaces own repositories454 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
455 >
456 <Plus size={16} />
457 </Link>
458 <DropdownMenu>
459 <DropdownMenuTrigger
460 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit461 className="flex items-center gap-1.5 rounded-md p-1 outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised"
Workspaces own repositories462 >
Workspace names and icons, and a component kit for every control463 <Avatar name={user.username} image={user.avatar} size={24} />
Workspaces own repositories464 <ChevronDown size={14} className="text-faint" />
465 </DropdownMenuTrigger>
466 <DropdownMenuContent align="end" className="w-56">
467 <DropdownMenuLabel>
468 Signed in as{" "}
469 <span className="font-mono font-medium text-fg">
470 {user.username}
471 </span>
472 </DropdownMenuLabel>
473 <DropdownMenuSeparator />
474 {(user.workspaces ?? []).map((membership) => (
475 <DropdownMenuItem asChild key={membership.slug}>
476 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control477 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
478 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Workspaces own repositories479 </Link>
480 </DropdownMenuItem>
481 ))}
482 <DropdownMenuItem asChild>
483 <Link to="/workspaces/new">
484 <Plus />
485 New workspace
486 </Link>
487 </DropdownMenuItem>
488 <DropdownMenuSeparator />
489 <DropdownMenuItem asChild>
490 <Link to="/">
491 <LayoutDashboard />
492 Mission control
493 </Link>
494 </DropdownMenuItem>
495 <DropdownMenuItem asChild>
496 <Link to="/new">
497 <Plus />
Projects live: fixes from walking them end to end498 New project
Workspaces own repositories499 </Link>
500 </DropdownMenuItem>
501 <DropdownMenuSeparator />
502 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas503 <Link to="/settings/profile">
Workspaces own repositories504 <Settings />
505 Settings
506 </Link>
507 </DropdownMenuItem>
508 <DropdownMenuItem asChild>
509 <Link to="https://docs.g1t.sh/quickstart/">
510 <BookOpen />
511 Documentation
512 </Link>
513 </DropdownMenuItem>
514 <DropdownMenuSeparator />
Acceptance checks in sandboxes, line comments and review verdicts515 {/* Submitted from here: the menu closes on select, and a button
516 that has left the page cannot submit a form. */}
517 <DropdownMenuItem
518 onSelect={() => submit(null, { method: "post", action: "/logout" })}
519 >
520 <LogOut />
521 Sign out
Workspaces own repositories522 </DropdownMenuItem>
523 </DropdownMenuContent>
524 </DropdownMenu>
525 </>
526 ) : (
527 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look528 <span className="hidden sm:contents">
529 <HeaderLink to="/login">Sign in</HeaderLink>
530 </span>
531 <SignUpButton />
Workspaces own repositories532 </>
533 )}
534 </div>
535 </div>
536 </header>
537 );
538}
539
Usage, Billing settings and prepaid AI credit; fixes from the UX audit540/**
541 * The root's data as the browser last had it. When a navigation fails in a
542 * way that takes the root's data with it, the error is still drawn in the
543 * person's own frame, signed in, not the visitor's. Never kept on the
544 * server, where one module serves everyone.
545 */
546let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
547
Workspaces own repositories548export function Layout({ children }: { children: React.ReactNode }) {
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address549 const nonce = useNonce();
Workspaces own repositories550 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit551 const loaded = useRouteLoaderData<typeof loader>("root");
552 const inBrowser = typeof document !== "undefined";
553 if (loaded && inBrowser) lastRoot = loaded;
554 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Agents as a team: lifecycle, merge queue, billing and a new shell555 const user = root?.user;
Merge branch 'worktree-agent-a985c01f43538af01'556 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas557 // Drawn around the error page too: a 404 keeps the sidebar out of a
558 // project or workspace the viewer cannot see.
559 const error = useRouteError();
560 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look561 // The billing page shows the full banner itself.
562 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents563 <SpikeBanner
564 compact
565 slug={root.shell.workspace.slug}
566 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
567 owner={root.shell.compute.owner}
568 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look569 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)570 // On the few pages an account still confirming its address can open
571 // besides /confirm-email (policies, support, signing in): the way back.
572 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
573 <p
574 role="status"
Agents as a team: lifecycle, merge queue, billing and a new shell575 className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-b border-warn/30 bg-warn/10 px-4 py-2 text-sm"
576 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)577 <span>Confirm your email address to start using g1t.</span>
578 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
579 Enter your code
580 </Link>
581 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell582 );
Merge main (membership, two-factor, GitHub repo roles) into tokens583 // A workspace that requires two-factor authentication of someone who
584 // has not turned it on: they keep their place, and cannot use it yet.
585 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'586 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look587 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens588 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look589 <>
590 {paused}
591 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens592 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look593 </>
594 ) : null;
Workspaces own repositories595 return (
596 <html lang="en">
597 <head>
598 <meta charSet="utf-8" />
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)599 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Dark gray base with lavender as an accent, and a live outcome view for plans600 <meta name="theme-color" content="#0f0f11" />
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)601 <meta name="apple-mobile-web-app-capable" content="yes" />
602 <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
Put the stylesheet at the top of the head, before React Router's preloads603 {/* The stylesheet before everything React Router preloads, so a slow
604 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads605 <link rel="stylesheet" href={appCss} precedence="default" />
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today606 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Workspaces own repositories607 <Meta />
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address608 <Links nonce={nonce} />
Workspaces own repositories609 </head>
610 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit611 {/* The first stop for the keyboard: past the menus, to the page. */}
612 <a
613 href="#content"
614 className="sr-only rounded-md bg-fg px-3 py-2 text-sm font-medium text-bg focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[70]"
615 >
616 Skip to content
617 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)618 {root?.shell && usesAppShell(pathname, user != null && !awaitsConfirmation(user)) ? (
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas619 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Agents as a team: lifecycle, merge queue, billing and a new shell620 {children}
621 </AppShell>
622 ) : (
623 <>
624 <Progress />
625 <Header user={user} />
626 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit627 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'628 {children}
629 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look630 <SiteFooter user={user} />
Agents as a team: lifecycle, merge queue, billing and a new shell631 </>
Workspaces own repositories632 )}
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)633 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
634 {user && !awaitsConfirmation(user) && (
635 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
636 )}
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today637 <AnalyticsConsent />
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address638 <ScrollRestoration nonce={nonce} />
639 <Scripts nonce={nonce} />
Workspaces own repositories640 </body>
641 </html>
642 );
643}
644
645export default function App() {
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today646 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
647 // Tell analytics who is signed in, once per change; signing out forgets them.
648 const lastUserId = useRef<string | null>(null);
649 useEffect(() => {
650 identify(userId, lastUserId.current);
651 lastUserId.current = userId;
652 }, [userId]);
Workspaces own repositories653 return <Outlet />;
654}
655
656export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit657 const location = useLocation();
658 const matches = useMatches();
659 // A tab left open across a deploy: load the address again as a whole
660 // page, once, rather than show an error the reload fixes.
661 const reload = reloadFixes({
662 error,
663 status: isRouteErrorResponse(error) ? error.status : undefined,
664 // The first page a tab loads has the key "default"; a client navigation's is its own.
665 clientNavigation: location.key !== "default",
666 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
667 });
668 const href = location.pathname + location.search + location.hash;
669 useEffect(() => {
670 if (!reload) return;
671 try {
672 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
673 sessionStorage.setItem(RELOADED_KEY, href);
674 } catch {
675 // No session storage: reload anyway; a document load never asks again.
676 }
677 window.location.assign(href);
678 }, [reload, href]);
679 if (reload) {
680 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in681 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center text-sm text-muted" aria-busy="true">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit682 <title>Loading · g1t</title>
683 Loading the latest version of g1t…
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in684 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit685 );
686 }
687
Workspaces own repositories688 let title = "Something went wrong";
689 let details = "An unexpected error occurred. Try again in a moment.";
690 let stack: string | undefined;
691
692 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put693 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit694 if (error.status === 404) {
695 return (
696 <>
697 <title>Not found · g1t</title>
698 <NotFound data={error.data} />
699 </>
700 );
701 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put702 if (typeof error.data === "string" && error.data) {
Workspaces own repositories703 details = error.data;
704 }
705 } else if (import.meta.env.DEV && error instanceof Error) {
706 details = error.message;
707 stack = error.stack;
708 }
709
710 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in711 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Workspaces own repositories712 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
713 <p className="mt-3 text-muted">{details}</p>
714 <div className="mt-8">
715 <ButtonLink to="/" variant="quiet">
716 Back to home
717 </ButtonLink>
718 </div>
719 {stack && (
720 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
721 <code>{stack}</code>
722 </pre>
723 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in724 </PageMain>
Workspaces own repositories725 );
726}

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