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

docs.g1t.sh, generated OpenAPI with an interactive reference, full footer1import {
2 BookOpen,
3 ChevronDown,
Search across all of g1t, Explore, and a command palette4 Compass,
5 CreditCard,
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer6 LayoutDashboard,
Search across all of g1t, Explore, and a command palette7 LogIn,
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer8 LogOut,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9 Menu,
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer10 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";
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer15import {
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,
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer30 useRouteLoaderData,
Acceptance checks in sandboxes, line comments and review verdicts31 useSubmit,
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer32} 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";
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer35
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";
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer40import { Logo } from "./components/logo";
Merge branch 'worktree-agent-a985c01f43538af01'41import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui";
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer42import {
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
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer73
74export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark75 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer76 { 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" },
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers78 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
79 // Home screens and password managers look for a square, opaque 180px tile;
80 // the "-precomposed" name is served too, for the ones that ask for it.
81 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
82 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
83 // manifest.webmanifest is the same file, kept for installs that already point at it).
84 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 // The text and headline faces are wanted on every page, so they start
86 // loading with the stylesheet; mono waits until something uses it.
87 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
88 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer89];
90
91export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
92
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put93export async function loader({ context, params, request }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell94 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put95 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)96 // Whether sign-up takes an invite: the sign-up page says so, and
97 // 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 look98 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)99 // An account still confirming its address sees only the pages that
100 // allows (lib/confirm-gate.ts), in the visitor's frame.
101 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit102 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
103 // A sidebar that could not be read is no reason to lose the page,
104 // or to draw it as if they were signed out.
105 console.error("root: the sidebar could not be read", error);
106 return bareShell(user, params, chosen);
107 })
108 : 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)109 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look110 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'111 // Where this g1t lives, for clone lines, agent setup and link previews.
112 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 put113}
114
115/**
116 * The sidebar for someone not signed in: nothing of their own, only the
117 * open counts of the project being looked at. One call, and only in a
118 * project; it answers the same for a private project as a missing one.
119 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents120async 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 put121 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents122 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 put123 return {
124 workspace: null,
125 repos: [],
126 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
127 limit: null,
128 monthUsageMicros: null,
129 };
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer130}
131
Usage, Billing settings and prepaid AI credit; fixes from the UX audit132/** The sidebar from what the session already says, with no service asked. */
133function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
134 return {
135 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
136 repos: [],
137 repo: null,
138 limit: null,
139 monthUsageMicros: null,
140 };
141}
142
Agents as a team: lifecycle, merge queue, billing and a new shell143/**
144 * The sidebar only changes with the workspace or repository being looked
145 * at, or after something was submitted: not on every page within them.
146 */
147export function shouldRevalidate({
148 currentParams,
149 nextParams,
150 formMethod,
151 defaultShouldRevalidate,
152}: ShouldRevalidateFunctionArgs) {
153 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
154 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
155}
156
157/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit158 * The sidebar: the workspace whose pages or project these are when it is
159 * one of yours, else the one you chose (lib/workspace-choice.ts); its
160 * projects; and the repository being looked at.
Agents as a team: lifecycle, merge queue, billing and a new shell161 */
162async function shellFor(
163 user: User,
164 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put165 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents166 context: Route.LoaderArgs["context"],
Agents as a team: lifecycle, merge queue, billing and a new shell167): Promise<ShellData> {
168 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put169 const workspace = workspaceFor(memberships, chosen, params);
Agents as a team: lifecycle, merge queue, billing and a new shell170 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 request171 const now = new Date();
172 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents173 // What the sidebar shows of the workspace changes rarely: kept for a few
174 // seconds per person and workspace, except just after they changed
175 // something (lib/cache.server.ts).
176 const kept = <T,>(what: string, load: () => Promise<T>) =>
177 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)178 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 sidebar179 // Every project, for the palette and the count; the sidebar lists only
180 // the person's pinned and recent ones (lib/pins.ts).
181 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents182 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 processor183 // Whether billing is on, without reading the account: that asks the
184 // card processor about the workspace's cards, too slow for every page.
185 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit186 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents187 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
188 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
189 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all190 // The bell's count: one query, never kept, so marking an item shows at once.
191 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar192 // Never kept: opening a project moves it up Recent.
193 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to194 // The rail's Chat badge: never kept, and never waited on for long.
195 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)196 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
197 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Agents as a team: lifecycle, merge queue, billing and a new shell198 ]);
199 return {
200 workspace,
Projects: what a workspace builds and runs, first on every page201 // 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 sidebar202 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
203 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
204 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Agents as a team: lifecycle, merge queue, billing and a new shell205 repo:
206 path && counts?.ok
207 ? {
208 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209 // Until the repository's own page says what their role is.
210 member: hasAccessIn(user, path.namespace),
Agents as a team: lifecycle, merge queue, billing and a new shell211 issues: counts.value.issues,
212 pulls: counts.value.pulls,
213 }
214 : null,
Billing accounts, terms and enterprises; g1t is no longer free215 // Where the workspace stands against its usage limit, once billing is on.
216 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor217 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free218 ? {
219 exposureMicros: limit.value.exposureMicros,
220 ceilingMicros: limit.value.ceilingMicros,
221 state: limit.value.state,
222 comped: limit.value.trust === "internal",
223 }
224 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor225 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look226 // A spend spike or a hold pauses new compute: the shell says so on every page.
227 compute:
228 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
229 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
230 : null,
231 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all232 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to233 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)234 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy235 // 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 audit236 // Usage at price, the one figure every page shows.
237 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 shell238 };
239}
240
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)241/** Starred and recent conversations Home's sidebar lists. */
242const SHELL_CONVERSATIONS = 8;
243
244/**
245 * What is unread in chat, for the rail, and the starred and latest
246 * conversations, for Home's sidebar; null when chat is slow to answer or
247 * does not.
248 */
Chat and workspace agents: channels, DMs and named agents you talk to249async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
250 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
251 const read = chat
252 .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)253 .then((result) => {
254 if (!result.ok) return null;
255 const entries = result.value.entries;
256 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
257 return {
258 ...unreadTotals(entries),
259 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
260 recent: latest.slice(0, SHELL_CONVERSATIONS),
261 };
262 })
263 .catch(() => null);
264 return Promise.race([read, timeout]);
265}
266
267/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
268async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
269 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
270 const read = workspaceAgents
271 .list(slug, user)
272 .then((result) =>
273 result.ok
274 ? result.value
275 .filter((agent) => !agent.archived_at)
276 .map((agent) => ({
277 id: agent.id,
278 handle: agent.handle,
279 display_name: agent.display_name,
280 avatar: agent.avatar,
281 avatar_seed: agent.avatar_seed,
282 role: agent.role,
283 title: agent.title,
284 team: agent.team,
285 department: agent.department,
286 status: agent.status,
287 builtin: agent.builtin === true,
288 }))
289 : null,
290 )
Chat and workspace agents: channels, DMs and named agents you talk to291 .catch(() => null);
292 return Promise.race([read, timeout]);
293}
294
295/** The longest the rail's Chat badge holds up a page. */
296const CHAT_BADGE_WAIT_MS = 300;
297
Usage, Billing settings and prepaid AI credit; fixes from the UX audit298/**
299 * How long the sidebar's workspace answers are kept: as long as someone's
300 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
301 */
302const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents303
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304/** Most repositories listed under Shared with you. */
305const MAX_SHARED = 20;
306
307/**
308 * Repositories shared with the user in workspaces they do not belong to,
309 * by their grants: only those, never the workspace's other repositories.
310 */
311async function sharedRepos(user: User): Promise<ShellData["shared"]> {
312 const outside = new Set(sharedWorkspaces(user));
313 if (outside.size === 0) return [];
314 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 incidents315 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look316 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents317 .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 look318 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
319}
320
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer321function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
322 return (
323 <NavLink
324 to={to}
325 className={({ isActive }) =>
326 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
327 isActive ? "text-fg" : "text-muted"
328 }`
329 }
330 >
331 {children}
332 </NavLink>
333 );
334}
335
Search across all of g1t, Explore, and a command palette336/** What the palette offers someone without the app's sidebar. */
337const PUBLIC_COMMANDS: PaletteCommand[] = [
338 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
339 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
340 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
341 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
342 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
343 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
344];
345
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite346/** 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 look347function SignUpButton() {
348 const copy = useSignUpCopy();
349 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
350}
351
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer352function Header({ user }: { user: User | null | undefined }) {
Acceptance checks in sandboxes, line comments and review verdicts353 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look354 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette355 const [palette, setPalette] = useState(false);
356 usePaletteShortcut(() => setPalette((open) => !open));
357 const params = useParams();
358 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer359 return (
360 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
361 <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 look362 <Link to="/" aria-label="g1t home" className="mr-2 flex">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer363 <Logo />
364 </Link>
365 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
366 <Search
367 size={14}
368 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
369 />
370 <input
371 name="q"
Agents as a team: lifecycle, merge queue, billing and a new shell372 {...notACredential()}
Search across all of g1t, Explore, and a command palette373 placeholder="Search g1t"
374 aria-label="Search g1t"
375 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"
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer376 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit377 <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" />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer378 </Form>
Search across all of g1t, Explore, and a command palette379 <button
380 type="button"
381 aria-label="Search g1t"
382 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look383 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 palette384 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look385 <Search size={18} />
Search across all of g1t, Explore, and a command palette386 </button>
387 <CommandPalette
388 open={palette}
389 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)390 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 palette391 repo={repo}
392 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look393 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer394 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays395 <HeaderLink to="/pricing">Pricing</HeaderLink>
Docs as their own app; shared theme package396 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer397 </nav>
398 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look399 {/* On a phone the links fold into one menu, so the bar fits. */}
400 <DropdownMenu>
401 <DropdownMenuTrigger
402 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit403 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 look404 >
405 <Menu size={18} />
406 </DropdownMenuTrigger>
407 <DropdownMenuContent align="end" className="w-52">
408 <DropdownMenuItem asChild className="min-h-11">
409 <Link to="/explore">
410 <Compass />
411 Explore
412 </Link>
413 </DropdownMenuItem>
414 <DropdownMenuItem asChild className="min-h-11">
415 <Link to="/pricing">
416 <CreditCard />
417 Pricing
418 </Link>
419 </DropdownMenuItem>
420 <DropdownMenuItem asChild className="min-h-11">
421 <Link to="https://docs.g1t.sh/">
422 <BookOpen />
423 Docs
424 </Link>
425 </DropdownMenuItem>
426 {!user && (
427 <>
428 <DropdownMenuSeparator />
429 <DropdownMenuItem asChild className="min-h-11">
430 <Link to="/login">
431 <LogIn />
432 Sign in
433 </Link>
434 </DropdownMenuItem>
435 </>
436 )}
437 </DropdownMenuContent>
438 </DropdownMenu>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer439 {user ? (
440 <>
441 <Link
442 to="/new"
Projects live: fixes from walking them end to end443 aria-label="New project"
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer444 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
445 >
446 <Plus size={16} />
447 </Link>
448 <DropdownMenu>
449 <DropdownMenuTrigger
450 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit451 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"
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer452 >
Workspace names and icons, and a component kit for every control453 <Avatar name={user.username} image={user.avatar} size={24} />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer454 <ChevronDown size={14} className="text-faint" />
455 </DropdownMenuTrigger>
456 <DropdownMenuContent align="end" className="w-56">
457 <DropdownMenuLabel>
458 Signed in as{" "}
459 <span className="font-mono font-medium text-fg">
460 {user.username}
461 </span>
462 </DropdownMenuLabel>
463 <DropdownMenuSeparator />
Workspaces own repositories464 {(user.workspaces ?? []).map((membership) => (
465 <DropdownMenuItem asChild key={membership.slug}>
466 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control467 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
468 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Workspaces own repositories469 </Link>
470 </DropdownMenuItem>
471 ))}
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer472 <DropdownMenuItem asChild>
Workspaces own repositories473 <Link to="/workspaces/new">
474 <Plus />
475 New workspace
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer476 </Link>
477 </DropdownMenuItem>
Workspaces own repositories478 <DropdownMenuSeparator />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer479 <DropdownMenuItem asChild>
480 <Link to="/">
481 <LayoutDashboard />
482 Mission control
483 </Link>
484 </DropdownMenuItem>
485 <DropdownMenuItem asChild>
486 <Link to="/new">
487 <Plus />
Projects live: fixes from walking them end to end488 New project
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer489 </Link>
490 </DropdownMenuItem>
491 <DropdownMenuSeparator />
492 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas493 <Link to="/settings/profile">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer494 <Settings />
495 Settings
496 </Link>
497 </DropdownMenuItem>
498 <DropdownMenuItem asChild>
Docs as their own app; shared theme package499 <Link to="https://docs.g1t.sh/quickstart/">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer500 <BookOpen />
501 Documentation
502 </Link>
503 </DropdownMenuItem>
504 <DropdownMenuSeparator />
Acceptance checks in sandboxes, line comments and review verdicts505 {/* Submitted from here: the menu closes on select, and a button
506 that has left the page cannot submit a form. */}
507 <DropdownMenuItem
508 onSelect={() => submit(null, { method: "post", action: "/logout" })}
509 >
510 <LogOut />
511 Sign out
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer512 </DropdownMenuItem>
513 </DropdownMenuContent>
514 </DropdownMenu>
515 </>
516 ) : (
517 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look518 <span className="hidden sm:contents">
519 <HeaderLink to="/login">Sign in</HeaderLink>
520 </span>
521 <SignUpButton />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer522 </>
523 )}
524 </div>
525 </div>
526 </header>
527 );
528}
529
Usage, Billing settings and prepaid AI credit; fixes from the UX audit530/**
531 * The root's data as the browser last had it. When a navigation fails in a
532 * way that takes the root's data with it, the error is still drawn in the
533 * person's own frame, signed in, not the visitor's. Never kept on the
534 * server, where one module serves everyone.
535 */
536let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
537
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer538export 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 address539 const nonce = useNonce();
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer540 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit541 const loaded = useRouteLoaderData<typeof loader>("root");
542 const inBrowser = typeof document !== "undefined";
543 if (loaded && inBrowser) lastRoot = loaded;
544 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Agents as a team: lifecycle, merge queue, billing and a new shell545 const user = root?.user;
Merge branch 'worktree-agent-a985c01f43538af01'546 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas547 // Drawn around the error page too: a 404 keeps the sidebar out of a
548 // project or workspace the viewer cannot see.
549 const error = useRouteError();
550 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look551 // The billing page shows the full banner itself.
552 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents553 <SpikeBanner
554 compact
555 slug={root.shell.workspace.slug}
556 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
557 owner={root.shell.compute.owner}
558 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look559 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)560 // On the few pages an account still confirming its address can open
561 // besides /confirm-email (policies, support, signing in): the way back.
562 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
563 <p
564 role="status"
Agents as a team: lifecycle, merge queue, billing and a new shell565 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"
566 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)567 <span>Confirm your email address to start using g1t.</span>
568 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
569 Enter your code
570 </Link>
571 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell572 );
Merge main (membership, two-factor, GitHub repo roles) into tokens573 // A workspace that requires two-factor authentication of someone who
574 // has not turned it on: they keep their place, and cannot use it yet.
575 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'576 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look577 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens578 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look579 <>
580 {paused}
581 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens582 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look583 </>
584 ) : null;
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer585 return (
586 <html lang="en">
587 <head>
588 <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)589 <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 plans590 <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)591 <meta name="apple-mobile-web-app-capable" content="yes" />
592 <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 preloads593 {/* The stylesheet before everything React Router preloads, so a slow
594 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads595 <link rel="stylesheet" href={appCss} precedence="default" />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer596 <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 address597 <Links nonce={nonce} />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer598 </head>
599 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit600 {/* The first stop for the keyboard: past the menus, to the page. */}
601 <a
602 href="#content"
603 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]"
604 >
605 Skip to content
606 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)607 {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 quotas608 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Agents as a team: lifecycle, merge queue, billing and a new shell609 {children}
610 </AppShell>
611 ) : (
612 <>
613 <Progress />
614 <Header user={user} />
615 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit616 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'617 {children}
618 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look619 <SiteFooter user={user} />
Agents as a team: lifecycle, merge queue, billing and a new shell620 </>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer621 )}
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)622 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
623 {user && !awaitsConfirmation(user) && (
624 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
625 )}
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 address626 <ScrollRestoration nonce={nonce} />
627 <Scripts nonce={nonce} />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer628 </body>
629 </html>
630 );
631}
632
633export default function App() {
634 return <Outlet />;
635}
636
637export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit638 const location = useLocation();
639 const matches = useMatches();
640 // A tab left open across a deploy: load the address again as a whole
641 // page, once, rather than show an error the reload fixes.
642 const reload = reloadFixes({
643 error,
644 status: isRouteErrorResponse(error) ? error.status : undefined,
645 // The first page a tab loads has the key "default"; a client navigation's is its own.
646 clientNavigation: location.key !== "default",
647 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
648 });
649 const href = location.pathname + location.search + location.hash;
650 useEffect(() => {
651 if (!reload) return;
652 try {
653 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
654 sessionStorage.setItem(RELOADED_KEY, href);
655 } catch {
656 // No session storage: reload anyway; a document load never asks again.
657 }
658 window.location.assign(href);
659 }, [reload, href]);
660 if (reload) {
661 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in662 <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 audit663 <title>Loading · g1t</title>
664 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 in665 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit666 );
667 }
668
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer669 let title = "Something went wrong";
670 let details = "An unexpected error occurred. Try again in a moment.";
671 let stack: string | undefined;
672
673 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put674 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit675 if (error.status === 404) {
676 return (
677 <>
678 <title>Not found · g1t</title>
679 <NotFound data={error.data} />
680 </>
681 );
682 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put683 if (typeof error.data === "string" && error.data) {
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer684 details = error.data;
685 }
686 } else if (import.meta.env.DEV && error instanceof Error) {
687 details = error.message;
688 stack = error.stack;
689 }
690
691 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in692 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer693 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
694 <p className="mt-3 text-muted">{details}</p>
695 <div className="mt-8">
696 <ButtonLink to="/" variant="quiet">
697 Back to home
698 </ButtonLink>
699 </div>
700 {stack && (
701 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
702 <code>{stack}</code>
703 </pre>
704 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in705 </PageMain>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer706 );
707}

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