Skip to content
722 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";
Site analytics with HeyCatch on g1t.sh, with names removed inside the app; the privacy policy says so14import { useEffect, useRef, 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";
Site analytics with HeyCatch on g1t.sh, with names removed inside the app; the privacy policy says so54import { identify } from "./lib/analytics.client";
Analytics asks first in the EU, EEA, UK and Switzerland, and loads only when the page is idle55import { visitorAsksFirst } from "./lib/analytics-consent";
56import { AnalyticsConsent } from "./components/analytics-consent";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put57import { readCookie } from "./lib/mission";
58import { WORKSPACE_COOKIE, workspaceFor } from "./lib/workspace-choice";
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in59import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put60import { NotFound } from "./components/not-found";
61import { usesAppShell } from "./lib/chrome";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit62import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./components/command-palette";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)63import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to64import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents65import { countsFor, readableRepos } from "./lib/access.server";
66import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar68import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'70import { addresses } from "./lib/addresses.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look71import { useSignUpCopy } from "./lib/registration";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit72import { RELOADED_KEY, reloadFixes } from "./lib/stale-build";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address73import { useNonce } from "./lib/nonce";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)74import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look75
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer76
77export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark78 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer79 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark80 { rel: "icon", type: "image/png", sizes: "192x192", href: "/icon-192.png" },
81 { 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)82 // Installable: name, icons and colours for "Add to Home Screen" (public/manifest.webmanifest).
83 { rel: "manifest", href: "/manifest.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84 // The text and headline faces are wanted on every page, so they start
85 // loading with the stylesheet; mono waits until something uses it.
86 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
87 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer88];
89
90export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
91
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put92export async function loader({ context, params, request }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell93 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put94 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)95 // Whether sign-up takes an invite: the sign-up page says so, and
96 // 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 look97 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)98 // An account still confirming its address sees only the pages that
99 // allows (lib/confirm-gate.ts), in the visitor's frame.
100 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit101 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
102 // A sidebar that could not be read is no reason to lose the page,
103 // or to draw it as if they were signed out.
104 console.error("root: the sidebar could not be read", error);
105 return bareShell(user, params, chosen);
106 })
107 : 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)108 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'110 // Where this g1t lives, for clone lines, agent setup and link previews.
Analytics asks first in the EU, EEA, UK and Switzerland, and loads only when the page is idle111 return {
112 user,
113 shell,
114 inviteOnly: mode !== "open",
115 addresses: addresses(),
116 // Visitors from where the law asks first are asked before analytics runs.
117 analyticsConsent: visitorAsksFirst(request),
118 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put119}
120
121/**
122 * The sidebar for someone not signed in: nothing of their own, only the
123 * open counts of the project being looked at. One call, and only in a
124 * project; it answers the same for a private project as a missing one.
125 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents126async 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 put127 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents128 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 put129 return {
130 workspace: null,
131 repos: [],
132 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
133 limit: null,
134 monthUsageMicros: null,
135 };
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer136}
137
Usage, Billing settings and prepaid AI credit; fixes from the UX audit138/** The sidebar from what the session already says, with no service asked. */
139function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
140 return {
141 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
142 repos: [],
143 repo: null,
144 limit: null,
145 monthUsageMicros: null,
146 };
147}
148
Agents as a team: lifecycle, merge queue, billing and a new shell149/**
150 * The sidebar only changes with the workspace or repository being looked
151 * at, or after something was submitted: not on every page within them.
152 */
153export function shouldRevalidate({
154 currentParams,
155 nextParams,
156 formMethod,
157 defaultShouldRevalidate,
158}: ShouldRevalidateFunctionArgs) {
159 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
160 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
161}
162
163/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit164 * The sidebar: the workspace whose pages or project these are when it is
165 * one of yours, else the one you chose (lib/workspace-choice.ts); its
166 * projects; and the repository being looked at.
Agents as a team: lifecycle, merge queue, billing and a new shell167 */
168async function shellFor(
169 user: User,
170 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put171 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents172 context: Route.LoaderArgs["context"],
Agents as a team: lifecycle, merge queue, billing and a new shell173): Promise<ShellData> {
174 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put175 const workspace = workspaceFor(memberships, chosen, params);
Agents as a team: lifecycle, merge queue, billing and a new shell176 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 request177 const now = new Date();
178 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents179 // What the sidebar shows of the workspace changes rarely: kept for a few
180 // seconds per person and workspace, except just after they changed
181 // something (lib/cache.server.ts).
182 const kept = <T,>(what: string, load: () => Promise<T>) =>
183 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)184 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 sidebar185 // Every project, for the palette and the count; the sidebar lists only
186 // the person's pinned and recent ones (lib/pins.ts).
187 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents188 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 processor189 // Whether billing is on, without reading the account: that asks the
190 // card processor about the workspace's cards, too slow for every page.
191 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit192 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents193 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
194 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
195 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all196 // The bell's count: one query, never kept, so marking an item shows at once.
197 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar198 // Never kept: opening a project moves it up Recent.
199 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to200 // The rail's Chat badge: never kept, and never waited on for long.
201 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)202 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
203 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Agents as a team: lifecycle, merge queue, billing and a new shell204 ]);
205 return {
206 workspace,
Projects: what a workspace builds and runs, first on every page207 // 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 sidebar208 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
209 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
210 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Agents as a team: lifecycle, merge queue, billing and a new shell211 repo:
212 path && counts?.ok
213 ? {
214 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look215 // Until the repository's own page says what their role is.
216 member: hasAccessIn(user, path.namespace),
Agents as a team: lifecycle, merge queue, billing and a new shell217 issues: counts.value.issues,
218 pulls: counts.value.pulls,
219 }
220 : null,
Billing accounts, terms and enterprises; g1t is no longer free221 // Where the workspace stands against its usage limit, once billing is on.
222 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor223 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free224 ? {
225 exposureMicros: limit.value.exposureMicros,
226 ceilingMicros: limit.value.ceilingMicros,
227 state: limit.value.state,
228 comped: limit.value.trust === "internal",
229 }
230 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor231 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look232 // A spend spike or a hold pauses new compute: the shell says so on every page.
233 compute:
234 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
235 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
236 : null,
237 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all238 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to239 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)240 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy241 // 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 audit242 // Usage at price, the one figure every page shows.
243 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 shell244 };
245}
246
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)247/** Starred and recent conversations Home's sidebar lists. */
248const SHELL_CONVERSATIONS = 8;
249
250/**
251 * What is unread in chat, for the rail, and the starred and latest
252 * conversations, for Home's sidebar; null when chat is slow to answer or
253 * does not.
254 */
Chat and workspace agents: channels, DMs and named agents you talk to255async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
256 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
257 const read = chat
258 .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)259 .then((result) => {
260 if (!result.ok) return null;
261 const entries = result.value.entries;
262 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
263 return {
264 ...unreadTotals(entries),
265 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
266 recent: latest.slice(0, SHELL_CONVERSATIONS),
267 };
268 })
269 .catch(() => null);
270 return Promise.race([read, timeout]);
271}
272
273/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
274async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
275 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
276 const read = workspaceAgents
277 .list(slug, user)
278 .then((result) =>
279 result.ok
280 ? result.value
281 .filter((agent) => !agent.archived_at)
282 .map((agent) => ({
283 id: agent.id,
284 handle: agent.handle,
285 display_name: agent.display_name,
286 avatar: agent.avatar,
287 avatar_seed: agent.avatar_seed,
288 role: agent.role,
289 title: agent.title,
290 team: agent.team,
291 department: agent.department,
292 status: agent.status,
293 builtin: agent.builtin === true,
294 }))
295 : null,
296 )
Chat and workspace agents: channels, DMs and named agents you talk to297 .catch(() => null);
298 return Promise.race([read, timeout]);
299}
300
301/** The longest the rail's Chat badge holds up a page. */
302const CHAT_BADGE_WAIT_MS = 300;
303
Usage, Billing settings and prepaid AI credit; fixes from the UX audit304/**
305 * How long the sidebar's workspace answers are kept: as long as someone's
306 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
307 */
308const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents309
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look310/** Most repositories listed under Shared with you. */
311const MAX_SHARED = 20;
312
313/**
314 * Repositories shared with the user in workspaces they do not belong to,
315 * by their grants: only those, never the workspace's other repositories.
316 */
317async function sharedRepos(user: User): Promise<ShellData["shared"]> {
318 const outside = new Set(sharedWorkspaces(user));
319 if (outside.size === 0) return [];
320 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 incidents321 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents323 .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 look324 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
325}
326
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer327function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
328 return (
329 <NavLink
330 to={to}
331 className={({ isActive }) =>
332 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
333 isActive ? "text-fg" : "text-muted"
334 }`
335 }
336 >
337 {children}
338 </NavLink>
339 );
340}
341
Search across all of g1t, Explore, and a command palette342/** What the palette offers someone without the app's sidebar. */
343const PUBLIC_COMMANDS: PaletteCommand[] = [
344 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
345 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
346 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
347 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
348 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
349 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
350];
351
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite352/** 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 look353function SignUpButton() {
354 const copy = useSignUpCopy();
355 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
356}
357
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer358function Header({ user }: { user: User | null | undefined }) {
Acceptance checks in sandboxes, line comments and review verdicts359 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look360 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette361 const [palette, setPalette] = useState(false);
362 usePaletteShortcut(() => setPalette((open) => !open));
363 const params = useParams();
364 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer365 return (
366 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
367 <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 look368 <Link to="/" aria-label="g1t home" className="mr-2 flex">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer369 <Logo />
370 </Link>
371 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
372 <Search
373 size={14}
374 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
375 />
376 <input
377 name="q"
Agents as a team: lifecycle, merge queue, billing and a new shell378 {...notACredential()}
Search across all of g1t, Explore, and a command palette379 placeholder="Search g1t"
380 aria-label="Search g1t"
381 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 footer382 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit383 <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 footer384 </Form>
Search across all of g1t, Explore, and a command palette385 <button
386 type="button"
387 aria-label="Search g1t"
388 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look389 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 palette390 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look391 <Search size={18} />
Search across all of g1t, Explore, and a command palette392 </button>
393 <CommandPalette
394 open={palette}
395 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)396 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 palette397 repo={repo}
398 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look399 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer400 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays401 <HeaderLink to="/pricing">Pricing</HeaderLink>
Docs as their own app; shared theme package402 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer403 </nav>
404 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look405 {/* On a phone the links fold into one menu, so the bar fits. */}
406 <DropdownMenu>
407 <DropdownMenuTrigger
408 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit409 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 look410 >
411 <Menu size={18} />
412 </DropdownMenuTrigger>
413 <DropdownMenuContent align="end" className="w-52">
414 <DropdownMenuItem asChild className="min-h-11">
415 <Link to="/explore">
416 <Compass />
417 Explore
418 </Link>
419 </DropdownMenuItem>
420 <DropdownMenuItem asChild className="min-h-11">
421 <Link to="/pricing">
422 <CreditCard />
423 Pricing
424 </Link>
425 </DropdownMenuItem>
426 <DropdownMenuItem asChild className="min-h-11">
427 <Link to="https://docs.g1t.sh/">
428 <BookOpen />
429 Docs
430 </Link>
431 </DropdownMenuItem>
432 {!user && (
433 <>
434 <DropdownMenuSeparator />
435 <DropdownMenuItem asChild className="min-h-11">
436 <Link to="/login">
437 <LogIn />
438 Sign in
439 </Link>
440 </DropdownMenuItem>
441 </>
442 )}
443 </DropdownMenuContent>
444 </DropdownMenu>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer445 {user ? (
446 <>
447 <Link
448 to="/new"
Projects live: fixes from walking them end to end449 aria-label="New project"
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer450 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
451 >
452 <Plus size={16} />
453 </Link>
454 <DropdownMenu>
455 <DropdownMenuTrigger
456 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit457 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 footer458 >
Workspace names and icons, and a component kit for every control459 <Avatar name={user.username} image={user.avatar} size={24} />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer460 <ChevronDown size={14} className="text-faint" />
461 </DropdownMenuTrigger>
462 <DropdownMenuContent align="end" className="w-56">
463 <DropdownMenuLabel>
464 Signed in as{" "}
465 <span className="font-mono font-medium text-fg">
466 {user.username}
467 </span>
468 </DropdownMenuLabel>
469 <DropdownMenuSeparator />
Workspaces own repositories470 {(user.workspaces ?? []).map((membership) => (
471 <DropdownMenuItem asChild key={membership.slug}>
472 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control473 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
474 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Workspaces own repositories475 </Link>
476 </DropdownMenuItem>
477 ))}
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer478 <DropdownMenuItem asChild>
Workspaces own repositories479 <Link to="/workspaces/new">
480 <Plus />
481 New workspace
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer482 </Link>
483 </DropdownMenuItem>
Workspaces own repositories484 <DropdownMenuSeparator />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer485 <DropdownMenuItem asChild>
486 <Link to="/">
487 <LayoutDashboard />
488 Mission control
489 </Link>
490 </DropdownMenuItem>
491 <DropdownMenuItem asChild>
492 <Link to="/new">
493 <Plus />
Projects live: fixes from walking them end to end494 New project
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer495 </Link>
496 </DropdownMenuItem>
497 <DropdownMenuSeparator />
498 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas499 <Link to="/settings/profile">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer500 <Settings />
501 Settings
502 </Link>
503 </DropdownMenuItem>
504 <DropdownMenuItem asChild>
Docs as their own app; shared theme package505 <Link to="https://docs.g1t.sh/quickstart/">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer506 <BookOpen />
507 Documentation
508 </Link>
509 </DropdownMenuItem>
510 <DropdownMenuSeparator />
Acceptance checks in sandboxes, line comments and review verdicts511 {/* Submitted from here: the menu closes on select, and a button
512 that has left the page cannot submit a form. */}
513 <DropdownMenuItem
514 onSelect={() => submit(null, { method: "post", action: "/logout" })}
515 >
516 <LogOut />
517 Sign out
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer518 </DropdownMenuItem>
519 </DropdownMenuContent>
520 </DropdownMenu>
521 </>
522 ) : (
523 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look524 <span className="hidden sm:contents">
525 <HeaderLink to="/login">Sign in</HeaderLink>
526 </span>
527 <SignUpButton />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer528 </>
529 )}
530 </div>
531 </div>
532 </header>
533 );
534}
535
Usage, Billing settings and prepaid AI credit; fixes from the UX audit536/**
537 * The root's data as the browser last had it. When a navigation fails in a
538 * way that takes the root's data with it, the error is still drawn in the
539 * person's own frame, signed in, not the visitor's. Never kept on the
540 * server, where one module serves everyone.
541 */
542let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
543
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer544export 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 address545 const nonce = useNonce();
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer546 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit547 const loaded = useRouteLoaderData<typeof loader>("root");
548 const inBrowser = typeof document !== "undefined";
549 if (loaded && inBrowser) lastRoot = loaded;
550 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Agents as a team: lifecycle, merge queue, billing and a new shell551 const user = root?.user;
Merge branch 'worktree-agent-a985c01f43538af01'552 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas553 // Drawn around the error page too: a 404 keeps the sidebar out of a
554 // project or workspace the viewer cannot see.
555 const error = useRouteError();
556 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look557 // The billing page shows the full banner itself.
558 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents559 <SpikeBanner
560 compact
561 slug={root.shell.workspace.slug}
562 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
563 owner={root.shell.compute.owner}
564 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look565 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)566 // On the few pages an account still confirming its address can open
567 // besides /confirm-email (policies, support, signing in): the way back.
568 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
569 <p
570 role="status"
Agents as a team: lifecycle, merge queue, billing and a new shell571 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"
572 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)573 <span>Confirm your email address to start using g1t.</span>
574 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
575 Enter your code
576 </Link>
577 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell578 );
Merge main (membership, two-factor, GitHub repo roles) into tokens579 // A workspace that requires two-factor authentication of someone who
580 // has not turned it on: they keep their place, and cannot use it yet.
581 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'582 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look583 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens584 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look585 <>
586 {paused}
587 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens588 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look589 </>
590 ) : null;
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer591 return (
592 <html lang="en">
593 <head>
594 <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)595 <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 plans596 <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)597 <meta name="apple-mobile-web-app-capable" content="yes" />
598 <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 preloads599 {/* The stylesheet before everything React Router preloads, so a slow
600 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads601 <link rel="stylesheet" href={appCss} precedence="default" />
Analytics asks first in the EU, EEA, UK and Switzerland, and loads only when the page is idle602 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer603 <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 address604 <Links nonce={nonce} />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer605 </head>
606 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit607 {/* The first stop for the keyboard: past the menus, to the page. */}
608 <a
609 href="#content"
610 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]"
611 >
612 Skip to content
613 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)614 {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 quotas615 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Agents as a team: lifecycle, merge queue, billing and a new shell616 {children}
617 </AppShell>
618 ) : (
619 <>
620 <Progress />
621 <Header user={user} />
622 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit623 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'624 {children}
625 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look626 <SiteFooter user={user} />
Agents as a team: lifecycle, merge queue, billing and a new shell627 </>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer628 )}
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)629 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
630 {user && !awaitsConfirmation(user) && (
631 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
632 )}
Analytics asks first in the EU, EEA, UK and Switzerland, and loads only when the page is idle633 <AnalyticsConsent />
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address634 <ScrollRestoration nonce={nonce} />
635 <Scripts nonce={nonce} />
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer636 </body>
637 </html>
638 );
639}
640
641export default function App() {
Site analytics with HeyCatch on g1t.sh, with names removed inside the app; the privacy policy says so642 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
643 // Tell analytics who is signed in, once per change; signing out forgets them.
644 const lastUserId = useRef<string | null>(null);
645 useEffect(() => {
646 identify(userId, lastUserId.current);
647 lastUserId.current = userId;
648 }, [userId]);
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer649 return <Outlet />;
650}
651
652export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit653 const location = useLocation();
654 const matches = useMatches();
655 // A tab left open across a deploy: load the address again as a whole
656 // page, once, rather than show an error the reload fixes.
657 const reload = reloadFixes({
658 error,
659 status: isRouteErrorResponse(error) ? error.status : undefined,
660 // The first page a tab loads has the key "default"; a client navigation's is its own.
661 clientNavigation: location.key !== "default",
662 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
663 });
664 const href = location.pathname + location.search + location.hash;
665 useEffect(() => {
666 if (!reload) return;
667 try {
668 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
669 sessionStorage.setItem(RELOADED_KEY, href);
670 } catch {
671 // No session storage: reload anyway; a document load never asks again.
672 }
673 window.location.assign(href);
674 }, [reload, href]);
675 if (reload) {
676 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in677 <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 audit678 <title>Loading · g1t</title>
679 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 in680 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit681 );
682 }
683
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer684 let title = "Something went wrong";
685 let details = "An unexpected error occurred. Try again in a moment.";
686 let stack: string | undefined;
687
688 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put689 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit690 if (error.status === 404) {
691 return (
692 <>
693 <title>Not found · g1t</title>
694 <NotFound data={error.data} />
695 </>
696 );
697 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put698 if (typeof error.data === "string" && error.data) {
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer699 details = error.data;
700 }
701 } else if (import.meta.env.DEV && error instanceof Error) {
702 details = error.message;
703 stack = error.stack;
704 }
705
706 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in707 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer708 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
709 <p className="mt-3 text-muted">{details}</p>
710 <div className="mt-8">
711 <ButtonLink to="/" variant="quiet">
712 Back to home
713 </ButtonLink>
714 </div>
715 {stack && (
716 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
717 <code>{stack}</code>
718 </pre>
719 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in720 </PageMain>
docs.g1t.sh, generated OpenAPI with an interactive reference, full footer721 );
722}

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