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

API and MCP server, Rust identity service, registration, site redesign1import {
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)2 BookOpen,
3 ChevronDown,
Search across all of g1t, Explore, and a command palette4 Compass,
5 CreditCard,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)6 LayoutDashboard,
Search across all of g1t, Explore, and a command palette7 LogIn,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)8 LogOut,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9 Menu,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)10 Plus,
11 Search,
12 Settings,
13} from "lucide-react";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit14import { useEffect, useState } from "react";
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)15import {
API and MCP server, Rust identity service, registration, site redesign16 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,
API and MCP server, Rust identity service, registration, site redesign30 useRouteLoaderData,
Acceptance checks in sandboxes, line comments and review verdicts31 useSubmit,
API and MCP server, Rust identity service, registration, site redesign32} 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";
Email verification, password reset, and Git for AI scale positioning35
API and MCP server, Rust identity service, registration, site redesign36import 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";
API and MCP server, Rust identity service, registration, site redesign40import { Logo } from "./components/logo";
Merge branch 'worktree-agent-a985c01f43538af01'41import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui";
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)42import {
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";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put54import { readCookie } from "./lib/mission";
55import { 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 in56import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put57import { NotFound } from "./components/not-found";
58import { usesAppShell } from "./lib/chrome";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit59import { 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)60import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to61import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents62import { countsFor, readableRepos } from "./lib/access.server";
63import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look64import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar65import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look66import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'67import { addresses } from "./lib/addresses.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look68import { useSignUpCopy } from "./lib/registration";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit69import { 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 address70import { 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)71import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look72
API and MCP server, Rust identity service, registration, site redesign73
74export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark75 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
API and MCP server, Rust identity service, registration, site redesign76 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark77 { rel: "icon", type: "image/png", sizes: "192x192", href: "/icon-192.png" },
78 { rel: "apple-touch-icon", href: "/apple-touch-icon.png" },
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)79 // Installable: name, icons and colours for "Add to Home Screen" (public/manifest.webmanifest).
80 { rel: "manifest", href: "/manifest.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look81 // The text and headline faces are wanted on every page, so they start
82 // loading with the stylesheet; mono waits until something uses it.
83 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
84 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
API and MCP server, Rust identity service, registration, site redesign85];
86
87export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
88
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put89export async function loader({ context, params, request }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell90 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put91 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)92 // Whether sign-up takes an invite: the sign-up page says so, and
93 // 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 look94 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)95 // An account still confirming its address sees only the pages that
96 // allows (lib/confirm-gate.ts), in the visitor's frame.
97 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit98 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
99 // A sidebar that could not be read is no reason to lose the page,
100 // or to draw it as if they were signed out.
101 console.error("root: the sidebar could not be read", error);
102 return bareShell(user, params, chosen);
103 })
104 : 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)105 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look106 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'107 // Where this g1t lives, for clone lines, agent setup and link previews.
108 return { user, shell, inviteOnly: mode !== "open", addresses: addresses() };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put109}
110
111/**
112 * The sidebar for someone not signed in: nothing of their own, only the
113 * open counts of the project being looked at. One call, and only in a
114 * project; it answers the same for a private project as a missing one.
115 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents116async 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 put117 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents118 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 put119 return {
120 workspace: null,
121 repos: [],
122 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
123 limit: null,
124 monthUsageMicros: null,
125 };
API and MCP server, Rust identity service, registration, site redesign126}
127
Usage, Billing settings and prepaid AI credit; fixes from the UX audit128/** The sidebar from what the session already says, with no service asked. */
129function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
130 return {
131 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
132 repos: [],
133 repo: null,
134 limit: null,
135 monthUsageMicros: null,
136 };
137}
138
Agents as a team: lifecycle, merge queue, billing and a new shell139/**
140 * The sidebar only changes with the workspace or repository being looked
141 * at, or after something was submitted: not on every page within them.
142 */
143export function shouldRevalidate({
144 currentParams,
145 nextParams,
146 formMethod,
147 defaultShouldRevalidate,
148}: ShouldRevalidateFunctionArgs) {
149 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
150 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
151}
152
153/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit154 * The sidebar: the workspace whose pages or project these are when it is
155 * one of yours, else the one you chose (lib/workspace-choice.ts); its
156 * projects; and the repository being looked at.
Agents as a team: lifecycle, merge queue, billing and a new shell157 */
158async function shellFor(
159 user: User,
160 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put161 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents162 context: Route.LoaderArgs["context"],
Agents as a team: lifecycle, merge queue, billing and a new shell163): Promise<ShellData> {
164 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put165 const workspace = workspaceFor(memberships, chosen, params);
Agents as a team: lifecycle, merge queue, billing and a new shell166 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 request167 const now = new Date();
168 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents169 // What the sidebar shows of the workspace changes rarely: kept for a few
170 // seconds per person and workspace, except just after they changed
171 // something (lib/cache.server.ts).
172 const kept = <T,>(what: string, load: () => Promise<T>) =>
173 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)174 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 sidebar175 // Every project, for the palette and the count; the sidebar lists only
176 // the person's pinned and recent ones (lib/pins.ts).
177 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents178 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 processor179 // Whether billing is on, without reading the account: that asks the
180 // card processor about the workspace's cards, too slow for every page.
181 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit182 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents183 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
184 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
185 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all186 // The bell's count: one query, never kept, so marking an item shows at once.
187 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar188 // Never kept: opening a project moves it up Recent.
189 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to190 // The rail's Chat badge: never kept, and never waited on for long.
191 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)192 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
193 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Agents as a team: lifecycle, merge queue, billing and a new shell194 ]);
195 return {
196 workspace,
Projects: what a workspace builds and runs, first on every page197 // 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 sidebar198 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
199 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
200 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Agents as a team: lifecycle, merge queue, billing and a new shell201 repo:
202 path && counts?.ok
203 ? {
204 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look205 // Until the repository's own page says what their role is.
206 member: hasAccessIn(user, path.namespace),
Agents as a team: lifecycle, merge queue, billing and a new shell207 issues: counts.value.issues,
208 pulls: counts.value.pulls,
209 }
210 : null,
Billing accounts, terms and enterprises; g1t is no longer free211 // Where the workspace stands against its usage limit, once billing is on.
212 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor213 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free214 ? {
215 exposureMicros: limit.value.exposureMicros,
216 ceilingMicros: limit.value.ceilingMicros,
217 state: limit.value.state,
218 comped: limit.value.trust === "internal",
219 }
220 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor221 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look222 // A spend spike or a hold pauses new compute: the shell says so on every page.
223 compute:
224 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
225 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
226 : null,
227 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all228 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to229 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)230 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy231 // 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 audit232 // Usage at price, the one figure every page shows.
233 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 shell234 };
235}
236
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)237/** Starred and recent conversations Home's sidebar lists. */
238const SHELL_CONVERSATIONS = 8;
239
240/**
241 * What is unread in chat, for the rail, and the starred and latest
242 * conversations, for Home's sidebar; null when chat is slow to answer or
243 * does not.
244 */
Chat and workspace agents: channels, DMs and named agents you talk to245async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
246 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
247 const read = chat
248 .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)249 .then((result) => {
250 if (!result.ok) return null;
251 const entries = result.value.entries;
252 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
253 return {
254 ...unreadTotals(entries),
255 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
256 recent: latest.slice(0, SHELL_CONVERSATIONS),
257 };
258 })
259 .catch(() => null);
260 return Promise.race([read, timeout]);
261}
262
263/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
264async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
265 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
266 const read = workspaceAgents
267 .list(slug, user)
268 .then((result) =>
269 result.ok
270 ? result.value
271 .filter((agent) => !agent.archived_at)
272 .map((agent) => ({
273 id: agent.id,
274 handle: agent.handle,
275 display_name: agent.display_name,
276 avatar: agent.avatar,
277 avatar_seed: agent.avatar_seed,
278 role: agent.role,
279 title: agent.title,
280 team: agent.team,
281 department: agent.department,
282 status: agent.status,
283 builtin: agent.builtin === true,
284 }))
285 : null,
286 )
Chat and workspace agents: channels, DMs and named agents you talk to287 .catch(() => null);
288 return Promise.race([read, timeout]);
289}
290
291/** The longest the rail's Chat badge holds up a page. */
292const CHAT_BADGE_WAIT_MS = 300;
293
Usage, Billing settings and prepaid AI credit; fixes from the UX audit294/**
295 * How long the sidebar's workspace answers are kept: as long as someone's
296 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
297 */
298const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents299
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look300/** Most repositories listed under Shared with you. */
301const MAX_SHARED = 20;
302
303/**
304 * Repositories shared with the user in workspaces they do not belong to,
305 * by their grants: only those, never the workspace's other repositories.
306 */
307async function sharedRepos(user: User): Promise<ShellData["shared"]> {
308 const outside = new Set(sharedWorkspaces(user));
309 if (outside.size === 0) return [];
310 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 incidents311 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look312 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents313 .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 look314 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
315}
316
API and MCP server, Rust identity service, registration, site redesign317function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
318 return (
319 <NavLink
320 to={to}
321 className={({ isActive }) =>
322 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
323 isActive ? "text-fg" : "text-muted"
324 }`
325 }
326 >
327 {children}
328 </NavLink>
329 );
330}
331
Search across all of g1t, Explore, and a command palette332/** What the palette offers someone without the app's sidebar. */
333const PUBLIC_COMMANDS: PaletteCommand[] = [
334 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
335 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
336 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
337 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
338 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
339 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
340];
341
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite342/** 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 look343function SignUpButton() {
344 const copy = useSignUpCopy();
345 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
346}
347
Email verification, password reset, and Git for AI scale positioning348function Header({ user }: { user: User | null | undefined }) {
Acceptance checks in sandboxes, line comments and review verdicts349 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look350 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette351 const [palette, setPalette] = useState(false);
352 usePaletteShortcut(() => setPalette((open) => !open));
353 const params = useParams();
354 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
API and MCP server, Rust identity service, registration, site redesign355 return (
356 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
357 <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 look358 <Link to="/" aria-label="g1t home" className="mr-2 flex">
API and MCP server, Rust identity service, registration, site redesign359 <Logo />
360 </Link>
361 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
362 <Search
363 size={14}
364 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
365 />
366 <input
367 name="q"
Agents as a team: lifecycle, merge queue, billing and a new shell368 {...notACredential()}
Search across all of g1t, Explore, and a command palette369 placeholder="Search g1t"
370 aria-label="Search g1t"
371 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"
API and MCP server, Rust identity service, registration, site redesign372 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit373 <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" />
API and MCP server, Rust identity service, registration, site redesign374 </Form>
Search across all of g1t, Explore, and a command palette375 <button
376 type="button"
377 aria-label="Search g1t"
378 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look379 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 palette380 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look381 <Search size={18} />
Search across all of g1t, Explore, and a command palette382 </button>
383 <CommandPalette
384 open={palette}
385 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)386 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 palette387 repo={repo}
388 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look389 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
API and MCP server, Rust identity service, registration, site redesign390 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays391 <HeaderLink to="/pricing">Pricing</HeaderLink>
Docs as their own app; shared theme package392 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
API and MCP server, Rust identity service, registration, site redesign393 </nav>
394 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look395 {/* On a phone the links fold into one menu, so the bar fits. */}
396 <DropdownMenu>
397 <DropdownMenuTrigger
398 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit399 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 look400 >
401 <Menu size={18} />
402 </DropdownMenuTrigger>
403 <DropdownMenuContent align="end" className="w-52">
404 <DropdownMenuItem asChild className="min-h-11">
405 <Link to="/explore">
406 <Compass />
407 Explore
408 </Link>
409 </DropdownMenuItem>
410 <DropdownMenuItem asChild className="min-h-11">
411 <Link to="/pricing">
412 <CreditCard />
413 Pricing
414 </Link>
415 </DropdownMenuItem>
416 <DropdownMenuItem asChild className="min-h-11">
417 <Link to="https://docs.g1t.sh/">
418 <BookOpen />
419 Docs
420 </Link>
421 </DropdownMenuItem>
422 {!user && (
423 <>
424 <DropdownMenuSeparator />
425 <DropdownMenuItem asChild className="min-h-11">
426 <Link to="/login">
427 <LogIn />
428 Sign in
429 </Link>
430 </DropdownMenuItem>
431 </>
432 )}
433 </DropdownMenuContent>
434 </DropdownMenu>
API and MCP server, Rust identity service, registration, site redesign435 {user ? (
436 <>
437 <Link
438 to="/new"
Projects live: fixes from walking them end to end439 aria-label="New project"
API and MCP server, Rust identity service, registration, site redesign440 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
441 >
442 <Plus size={16} />
443 </Link>
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)444 <DropdownMenu>
445 <DropdownMenuTrigger
446 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit447 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"
API and MCP server, Rust identity service, registration, site redesign448 >
Workspace names and icons, and a component kit for every control449 <Avatar name={user.username} image={user.avatar} size={24} />
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)450 <ChevronDown size={14} className="text-faint" />
451 </DropdownMenuTrigger>
452 <DropdownMenuContent align="end" className="w-56">
453 <DropdownMenuLabel>
454 Signed in as{" "}
455 <span className="font-mono font-medium text-fg">
456 {user.username}
457 </span>
458 </DropdownMenuLabel>
459 <DropdownMenuSeparator />
Workspaces own repositories460 {(user.workspaces ?? []).map((membership) => (
461 <DropdownMenuItem asChild key={membership.slug}>
462 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control463 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
464 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Workspaces own repositories465 </Link>
466 </DropdownMenuItem>
467 ))}
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)468 <DropdownMenuItem asChild>
Workspaces own repositories469 <Link to="/workspaces/new">
470 <Plus />
471 New workspace
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)472 </Link>
473 </DropdownMenuItem>
Workspaces own repositories474 <DropdownMenuSeparator />
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)475 <DropdownMenuItem asChild>
476 <Link to="/">
477 <LayoutDashboard />
478 Mission control
479 </Link>
480 </DropdownMenuItem>
481 <DropdownMenuItem asChild>
482 <Link to="/new">
483 <Plus />
Projects live: fixes from walking them end to end484 New project
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)485 </Link>
486 </DropdownMenuItem>
487 <DropdownMenuSeparator />
488 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas489 <Link to="/settings/profile">
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)490 <Settings />
491 Settings
492 </Link>
493 </DropdownMenuItem>
494 <DropdownMenuItem asChild>
Docs as their own app; shared theme package495 <Link to="https://docs.g1t.sh/quickstart/">
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)496 <BookOpen />
497 Documentation
498 </Link>
499 </DropdownMenuItem>
500 <DropdownMenuSeparator />
Acceptance checks in sandboxes, line comments and review verdicts501 {/* Submitted from here: the menu closes on select, and a button
502 that has left the page cannot submit a form. */}
503 <DropdownMenuItem
504 onSelect={() => submit(null, { method: "post", action: "/logout" })}
505 >
506 <LogOut />
507 Sign out
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)508 </DropdownMenuItem>
509 </DropdownMenuContent>
510 </DropdownMenu>
API and MCP server, Rust identity service, registration, site redesign511 </>
512 ) : (
513 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look514 <span className="hidden sm:contents">
515 <HeaderLink to="/login">Sign in</HeaderLink>
516 </span>
517 <SignUpButton />
API and MCP server, Rust identity service, registration, site redesign518 </>
519 )}
520 </div>
521 </div>
522 </header>
523 );
524}
525
Usage, Billing settings and prepaid AI credit; fixes from the UX audit526/**
527 * The root's data as the browser last had it. When a navigation fails in a
528 * way that takes the root's data with it, the error is still drawn in the
529 * person's own frame, signed in, not the visitor's. Never kept on the
530 * server, where one module serves everyone.
531 */
532let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
533
API and MCP server, Rust identity service, registration, site redesign534export 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 address535 const nonce = useNonce();
API and MCP server, Rust identity service, registration, site redesign536 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit537 const loaded = useRouteLoaderData<typeof loader>("root");
538 const inBrowser = typeof document !== "undefined";
539 if (loaded && inBrowser) lastRoot = loaded;
540 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Agents as a team: lifecycle, merge queue, billing and a new shell541 const user = root?.user;
Merge branch 'worktree-agent-a985c01f43538af01'542 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas543 // Drawn around the error page too: a 404 keeps the sidebar out of a
544 // project or workspace the viewer cannot see.
545 const error = useRouteError();
546 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look547 // The billing page shows the full banner itself.
548 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents549 <SpikeBanner
550 compact
551 slug={root.shell.workspace.slug}
552 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
553 owner={root.shell.compute.owner}
554 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look555 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)556 // On the few pages an account still confirming its address can open
557 // besides /confirm-email (policies, support, signing in): the way back.
558 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
559 <p
560 role="status"
Agents as a team: lifecycle, merge queue, billing and a new shell561 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"
562 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)563 <span>Confirm your email address to start using g1t.</span>
564 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
565 Enter your code
566 </Link>
567 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell568 );
Merge main (membership, two-factor, GitHub repo roles) into tokens569 // A workspace that requires two-factor authentication of someone who
570 // has not turned it on: they keep their place, and cannot use it yet.
571 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'572 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look573 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens574 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look575 <>
576 {paused}
577 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens578 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look579 </>
580 ) : null;
API and MCP server, Rust identity service, registration, site redesign581 return (
582 <html lang="en">
583 <head>
584 <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)585 <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 plans586 <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)587 <meta name="apple-mobile-web-app-capable" content="yes" />
588 <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 preloads589 {/* The stylesheet before everything React Router preloads, so a slow
590 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads591 <link rel="stylesheet" href={appCss} precedence="default" />
API and MCP server, Rust identity service, registration, site redesign592 <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 address593 <Links nonce={nonce} />
API and MCP server, Rust identity service, registration, site redesign594 </head>
595 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit596 {/* The first stop for the keyboard: past the menus, to the page. */}
597 <a
598 href="#content"
599 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]"
600 >
601 Skip to content
602 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)603 {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 quotas604 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Agents as a team: lifecycle, merge queue, billing and a new shell605 {children}
606 </AppShell>
607 ) : (
608 <>
609 <Progress />
610 <Header user={user} />
611 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit612 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'613 {children}
614 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look615 <SiteFooter user={user} />
Agents as a team: lifecycle, merge queue, billing and a new shell616 </>
Email verification, password reset, and Git for AI scale positioning617 )}
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)618 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
619 {user && !awaitsConfirmation(user) && (
620 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
621 )}
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 address622 <ScrollRestoration nonce={nonce} />
623 <Scripts nonce={nonce} />
API and MCP server, Rust identity service, registration, site redesign624 </body>
625 </html>
626 );
627}
628
629export default function App() {
630 return <Outlet />;
631}
632
633export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit634 const location = useLocation();
635 const matches = useMatches();
636 // A tab left open across a deploy: load the address again as a whole
637 // page, once, rather than show an error the reload fixes.
638 const reload = reloadFixes({
639 error,
640 status: isRouteErrorResponse(error) ? error.status : undefined,
641 // The first page a tab loads has the key "default"; a client navigation's is its own.
642 clientNavigation: location.key !== "default",
643 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
644 });
645 const href = location.pathname + location.search + location.hash;
646 useEffect(() => {
647 if (!reload) return;
648 try {
649 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
650 sessionStorage.setItem(RELOADED_KEY, href);
651 } catch {
652 // No session storage: reload anyway; a document load never asks again.
653 }
654 window.location.assign(href);
655 }, [reload, href]);
656 if (reload) {
657 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in658 <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 audit659 <title>Loading · g1t</title>
660 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 in661 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit662 );
663 }
664
API and MCP server, Rust identity service, registration, site redesign665 let title = "Something went wrong";
666 let details = "An unexpected error occurred. Try again in a moment.";
667 let stack: string | undefined;
668
669 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put670 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit671 if (error.status === 404) {
672 return (
673 <>
674 <title>Not found · g1t</title>
675 <NotFound data={error.data} />
676 </>
677 );
678 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put679 if (typeof error.data === "string" && error.data) {
API and MCP server, Rust identity service, registration, site redesign680 details = error.data;
681 }
682 } else if (import.meta.env.DEV && error instanceof Error) {
683 details = error.message;
684 stack = error.stack;
685 }
686
687 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in688 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
API and MCP server, Rust identity service, registration, site redesign689 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
690 <p className="mt-3 text-muted">{details}</p>
691 <div className="mt-8">
692 <ButtonLink to="/" variant="quiet">
693 Back to home
694 </ButtonLink>
695 </div>
696 {stack && (
697 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
698 <code>{stack}</code>
699 </pre>
700 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in701 </PageMain>
API and MCP server, Rust identity service, registration, site redesign702 );
703}

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