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

Email verification, password reset, and Git for AI scale positioning1import {
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)2 BookOpen,
3 ChevronDown,
Search across all of g1t, Explore, and a command palette4 Compass,
5 CreditCard,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)6 LayoutDashboard,
Search across all of g1t, Explore, and a command palette7 LogIn,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)8 LogOut,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9 Menu,
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)10 Plus,
11 Search,
12 Settings,
13} from "lucide-react";
Site analytics with HeyCatch on g1t.sh, with names removed inside the app; the privacy policy says so14import { useEffect, useRef, useState } from "react";
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)15import {
Email verification, password reset, and Git for AI scale positioning16 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,
Email verification, password reset, and Git for AI scale positioning30 useRouteLoaderData,
Acceptance checks in sandboxes, line comments and review verdicts31 useSubmit,
Email verification, password reset, and Git for AI scale positioning32} from "react-router";
33
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)34import { type User, awaitsConfirmation, hasAccessIn, sharedWorkspaces } from "@g1t/contracts";
Email verification, password reset, and Git for AI scale positioning35
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";
Email verification, password reset, and Git for AI scale positioning40import { Logo } from "./components/logo";
Merge branch 'worktree-agent-a985c01f43538af01'41import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui";
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)42import {
43 DropdownMenu,
44 DropdownMenuContent,
45 DropdownMenuItem,
46 DropdownMenuLabel,
47 DropdownMenuSeparator,
48 DropdownMenuTrigger,
49} from "./components/ui/dropdown-menu";
Merge branch 'worktree-agent-aca4790ea223aa235'50import { AppShell, Progress, type ShellData, useLeaving } from "./components/shell";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51import { SiteFooter } from "./components/footer";
Fast pages, required checks on the branch, self-hosted runners, honest incidents52import { SpikeBanner } from "./components/spike-banner";
Merge main (membership, two-factor, GitHub repo roles) into tokens53import { PolicyNotice } from "./components/policy-notice";
Site analytics with HeyCatch on g1t.sh, with names removed inside the app; the privacy policy says so54import { identify } from "./lib/analytics.client";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put55import { readCookie } from "./lib/mission";
56import { 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 in57import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put58import { NotFound } from "./components/not-found";
59import { usesAppShell } from "./lib/chrome";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit60import { 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)61import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to62import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents63import { countsFor, readableRepos } from "./lib/access.server";
64import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look65import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar66import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'68import { addresses } from "./lib/addresses.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69import { useSignUpCopy } from "./lib/registration";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit70import { 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 address71import { 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)72import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look73
Email verification, password reset, and Git for AI scale positioning74
75export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark76 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Email verification, password reset, and Git for AI scale positioning77 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark78 { rel: "icon", type: "image/png", sizes: "192x192", href: "/icon-192.png" },
79 { 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)80 // Installable: name, icons and colours for "Add to Home Screen" (public/manifest.webmanifest).
81 { rel: "manifest", href: "/manifest.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 // The text and headline faces are wanted on every page, so they start
83 // loading with the stylesheet; mono waits until something uses it.
84 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
85 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Email verification, password reset, and Git for AI scale positioning86];
87
88export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
89
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put90export async function loader({ context, params, request }: Route.LoaderArgs) {
Agents as a team: lifecycle, merge queue, billing and a new shell91 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put92 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)93 // Whether sign-up takes an invite: the sign-up page says so, and
94 // 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 look95 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)96 // An account still confirming its address sees only the pages that
97 // allows (lib/confirm-gate.ts), in the visitor's frame.
98 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit99 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
100 // A sidebar that could not be read is no reason to lose the page,
101 // or to draw it as if they were signed out.
102 console.error("root: the sidebar could not be read", error);
103 return bareShell(user, params, chosen);
104 })
105 : 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)106 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look107 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'108 // Where this g1t lives, for clone lines, agent setup and link previews.
109 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 put110}
111
112/**
113 * The sidebar for someone not signed in: nothing of their own, only the
114 * open counts of the project being looked at. One call, and only in a
115 * project; it answers the same for a private project as a missing one.
116 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents117async 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 put118 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents119 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 put120 return {
121 workspace: null,
122 repos: [],
123 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
124 limit: null,
125 monthUsageMicros: null,
126 };
Email verification, password reset, and Git for AI scale positioning127}
128
Usage, Billing settings and prepaid AI credit; fixes from the UX audit129/** The sidebar from what the session already says, with no service asked. */
130function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
131 return {
132 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
133 repos: [],
134 repo: null,
135 limit: null,
136 monthUsageMicros: null,
137 };
138}
139
Agents as a team: lifecycle, merge queue, billing and a new shell140/**
141 * The sidebar only changes with the workspace or repository being looked
142 * at, or after something was submitted: not on every page within them.
143 */
144export function shouldRevalidate({
145 currentParams,
146 nextParams,
147 formMethod,
148 defaultShouldRevalidate,
149}: ShouldRevalidateFunctionArgs) {
150 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
151 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
152}
153
154/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit155 * The sidebar: the workspace whose pages or project these are when it is
156 * one of yours, else the one you chose (lib/workspace-choice.ts); its
157 * projects; and the repository being looked at.
Agents as a team: lifecycle, merge queue, billing and a new shell158 */
159async function shellFor(
160 user: User,
161 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put162 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents163 context: Route.LoaderArgs["context"],
Agents as a team: lifecycle, merge queue, billing and a new shell164): Promise<ShellData> {
165 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put166 const workspace = workspaceFor(memberships, chosen, params);
Agents as a team: lifecycle, merge queue, billing and a new shell167 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 request168 const now = new Date();
169 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents170 // What the sidebar shows of the workspace changes rarely: kept for a few
171 // seconds per person and workspace, except just after they changed
172 // something (lib/cache.server.ts).
173 const kept = <T,>(what: string, load: () => Promise<T>) =>
174 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)175 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 sidebar176 // Every project, for the palette and the count; the sidebar lists only
177 // the person's pinned and recent ones (lib/pins.ts).
178 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents179 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 processor180 // Whether billing is on, without reading the account: that asks the
181 // card processor about the workspace's cards, too slow for every page.
182 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit183 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents184 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
185 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
186 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all187 // The bell's count: one query, never kept, so marking an item shows at once.
188 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar189 // Never kept: opening a project moves it up Recent.
190 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to191 // The rail's Chat badge: never kept, and never waited on for long.
192 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)193 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
194 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Agents as a team: lifecycle, merge queue, billing and a new shell195 ]);
196 return {
197 workspace,
Projects: what a workspace builds and runs, first on every page198 // 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 sidebar199 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
200 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
201 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Agents as a team: lifecycle, merge queue, billing and a new shell202 repo:
203 path && counts?.ok
204 ? {
205 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look206 // Until the repository's own page says what their role is.
207 member: hasAccessIn(user, path.namespace),
Agents as a team: lifecycle, merge queue, billing and a new shell208 issues: counts.value.issues,
209 pulls: counts.value.pulls,
210 }
211 : null,
Billing accounts, terms and enterprises; g1t is no longer free212 // Where the workspace stands against its usage limit, once billing is on.
213 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor214 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free215 ? {
216 exposureMicros: limit.value.exposureMicros,
217 ceilingMicros: limit.value.ceilingMicros,
218 state: limit.value.state,
219 comped: limit.value.trust === "internal",
220 }
221 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor222 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look223 // A spend spike or a hold pauses new compute: the shell says so on every page.
224 compute:
225 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
226 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
227 : null,
228 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all229 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to230 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)231 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy232 // 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 audit233 // Usage at price, the one figure every page shows.
234 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 shell235 };
236}
237
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)238/** Starred and recent conversations Home's sidebar lists. */
239const SHELL_CONVERSATIONS = 8;
240
241/**
242 * What is unread in chat, for the rail, and the starred and latest
243 * conversations, for Home's sidebar; null when chat is slow to answer or
244 * does not.
245 */
Chat and workspace agents: channels, DMs and named agents you talk to246async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
247 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
248 const read = chat
249 .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)250 .then((result) => {
251 if (!result.ok) return null;
252 const entries = result.value.entries;
253 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
254 return {
255 ...unreadTotals(entries),
256 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
257 recent: latest.slice(0, SHELL_CONVERSATIONS),
258 };
259 })
260 .catch(() => null);
261 return Promise.race([read, timeout]);
262}
263
264/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
265async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
266 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
267 const read = workspaceAgents
268 .list(slug, user)
269 .then((result) =>
270 result.ok
271 ? result.value
272 .filter((agent) => !agent.archived_at)
273 .map((agent) => ({
274 id: agent.id,
275 handle: agent.handle,
276 display_name: agent.display_name,
277 avatar: agent.avatar,
278 avatar_seed: agent.avatar_seed,
279 role: agent.role,
280 title: agent.title,
281 team: agent.team,
282 department: agent.department,
283 status: agent.status,
284 builtin: agent.builtin === true,
285 }))
286 : null,
287 )
Chat and workspace agents: channels, DMs and named agents you talk to288 .catch(() => null);
289 return Promise.race([read, timeout]);
290}
291
292/** The longest the rail's Chat badge holds up a page. */
293const CHAT_BADGE_WAIT_MS = 300;
294
Usage, Billing settings and prepaid AI credit; fixes from the UX audit295/**
296 * How long the sidebar's workspace answers are kept: as long as someone's
297 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
298 */
299const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents300
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look301/** Most repositories listed under Shared with you. */
302const MAX_SHARED = 20;
303
304/**
305 * Repositories shared with the user in workspaces they do not belong to,
306 * by their grants: only those, never the workspace's other repositories.
307 */
308async function sharedRepos(user: User): Promise<ShellData["shared"]> {
309 const outside = new Set(sharedWorkspaces(user));
310 if (outside.size === 0) return [];
311 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 incidents312 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look313 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents314 .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 look315 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
316}
317
Email verification, password reset, and Git for AI scale positioning318function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
319 return (
320 <NavLink
321 to={to}
322 className={({ isActive }) =>
323 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
324 isActive ? "text-fg" : "text-muted"
325 }`
326 }
327 >
328 {children}
329 </NavLink>
330 );
331}
332
Search across all of g1t, Explore, and a command palette333/** What the palette offers someone without the app's sidebar. */
334const PUBLIC_COMMANDS: PaletteCommand[] = [
335 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
336 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
337 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
338 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
339 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
340 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
341];
342
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite343/** 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 look344function SignUpButton() {
345 const copy = useSignUpCopy();
346 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
347}
348
Email verification, password reset, and Git for AI scale positioning349function Header({ user }: { user: User | null | undefined }) {
Acceptance checks in sandboxes, line comments and review verdicts350 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look351 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette352 const [palette, setPalette] = useState(false);
353 usePaletteShortcut(() => setPalette((open) => !open));
354 const params = useParams();
355 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
Email verification, password reset, and Git for AI scale positioning356 return (
357 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
358 <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 look359 <Link to="/" aria-label="g1t home" className="mr-2 flex">
Email verification, password reset, and Git for AI scale positioning360 <Logo />
361 </Link>
362 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
363 <Search
364 size={14}
365 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
366 />
367 <input
368 name="q"
Agents as a team: lifecycle, merge queue, billing and a new shell369 {...notACredential()}
Search across all of g1t, Explore, and a command palette370 placeholder="Search g1t"
371 aria-label="Search g1t"
372 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"
Email verification, password reset, and Git for AI scale positioning373 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit374 <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" />
Email verification, password reset, and Git for AI scale positioning375 </Form>
Search across all of g1t, Explore, and a command palette376 <button
377 type="button"
378 aria-label="Search g1t"
379 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look380 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 palette381 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look382 <Search size={18} />
Search across all of g1t, Explore, and a command palette383 </button>
384 <CommandPalette
385 open={palette}
386 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)387 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 palette388 repo={repo}
389 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look390 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
Email verification, password reset, and Git for AI scale positioning391 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays392 <HeaderLink to="/pricing">Pricing</HeaderLink>
Docs as their own app; shared theme package393 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
Email verification, password reset, and Git for AI scale positioning394 </nav>
395 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look396 {/* On a phone the links fold into one menu, so the bar fits. */}
397 <DropdownMenu>
398 <DropdownMenuTrigger
399 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit400 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 look401 >
402 <Menu size={18} />
403 </DropdownMenuTrigger>
404 <DropdownMenuContent align="end" className="w-52">
405 <DropdownMenuItem asChild className="min-h-11">
406 <Link to="/explore">
407 <Compass />
408 Explore
409 </Link>
410 </DropdownMenuItem>
411 <DropdownMenuItem asChild className="min-h-11">
412 <Link to="/pricing">
413 <CreditCard />
414 Pricing
415 </Link>
416 </DropdownMenuItem>
417 <DropdownMenuItem asChild className="min-h-11">
418 <Link to="https://docs.g1t.sh/">
419 <BookOpen />
420 Docs
421 </Link>
422 </DropdownMenuItem>
423 {!user && (
424 <>
425 <DropdownMenuSeparator />
426 <DropdownMenuItem asChild className="min-h-11">
427 <Link to="/login">
428 <LogIn />
429 Sign in
430 </Link>
431 </DropdownMenuItem>
432 </>
433 )}
434 </DropdownMenuContent>
435 </DropdownMenu>
Email verification, password reset, and Git for AI scale positioning436 {user ? (
437 <>
438 <Link
439 to="/new"
Projects live: fixes from walking them end to end440 aria-label="New project"
Email verification, password reset, and Git for AI scale positioning441 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
442 >
443 <Plus size={16} />
444 </Link>
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)445 <DropdownMenu>
446 <DropdownMenuTrigger
447 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit448 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"
Email verification, password reset, and Git for AI scale positioning449 >
Workspace names and icons, and a component kit for every control450 <Avatar name={user.username} image={user.avatar} size={24} />
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)451 <ChevronDown size={14} className="text-faint" />
452 </DropdownMenuTrigger>
453 <DropdownMenuContent align="end" className="w-56">
454 <DropdownMenuLabel>
455 Signed in as{" "}
456 <span className="font-mono font-medium text-fg">
457 {user.username}
458 </span>
459 </DropdownMenuLabel>
460 <DropdownMenuSeparator />
Workspaces own repositories461 {(user.workspaces ?? []).map((membership) => (
462 <DropdownMenuItem asChild key={membership.slug}>
463 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control464 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
465 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Workspaces own repositories466 </Link>
467 </DropdownMenuItem>
468 ))}
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)469 <DropdownMenuItem asChild>
Workspaces own repositories470 <Link to="/workspaces/new">
471 <Plus />
472 New workspace
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)473 </Link>
474 </DropdownMenuItem>
Workspaces own repositories475 <DropdownMenuSeparator />
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)476 <DropdownMenuItem asChild>
477 <Link to="/">
478 <LayoutDashboard />
479 Mission control
480 </Link>
481 </DropdownMenuItem>
482 <DropdownMenuItem asChild>
483 <Link to="/new">
484 <Plus />
Projects live: fixes from walking them end to end485 New project
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)486 </Link>
487 </DropdownMenuItem>
488 <DropdownMenuSeparator />
489 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas490 <Link to="/settings/profile">
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)491 <Settings />
492 Settings
493 </Link>
494 </DropdownMenuItem>
495 <DropdownMenuItem asChild>
Docs as their own app; shared theme package496 <Link to="https://docs.g1t.sh/quickstart/">
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)497 <BookOpen />
498 Documentation
499 </Link>
500 </DropdownMenuItem>
501 <DropdownMenuSeparator />
Acceptance checks in sandboxes, line comments and review verdicts502 {/* Submitted from here: the menu closes on select, and a button
503 that has left the page cannot submit a form. */}
504 <DropdownMenuItem
505 onSelect={() => submit(null, { method: "post", action: "/logout" })}
506 >
507 <LogOut />
508 Sign out
Account dropdown, llms.txt onboarding, hosted agent runner (not yet deployed)509 </DropdownMenuItem>
510 </DropdownMenuContent>
511 </DropdownMenu>
Email verification, password reset, and Git for AI scale positioning512 </>
513 ) : (
514 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look515 <span className="hidden sm:contents">
516 <HeaderLink to="/login">Sign in</HeaderLink>
517 </span>
518 <SignUpButton />
Email verification, password reset, and Git for AI scale positioning519 </>
520 )}
521 </div>
522 </div>
523 </header>
524 );
525}
526
Usage, Billing settings and prepaid AI credit; fixes from the UX audit527/**
528 * The root's data as the browser last had it. When a navigation fails in a
529 * way that takes the root's data with it, the error is still drawn in the
530 * person's own frame, signed in, not the visitor's. Never kept on the
531 * server, where one module serves everyone.
532 */
533let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
534
Email verification, password reset, and Git for AI scale positioning535export 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 address536 const nonce = useNonce();
Email verification, password reset, and Git for AI scale positioning537 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit538 const loaded = useRouteLoaderData<typeof loader>("root");
539 const inBrowser = typeof document !== "undefined";
540 if (loaded && inBrowser) lastRoot = loaded;
541 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Agents as a team: lifecycle, merge queue, billing and a new shell542 const user = root?.user;
Merge branch 'worktree-agent-a985c01f43538af01'543 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas544 // Drawn around the error page too: a 404 keeps the sidebar out of a
545 // project or workspace the viewer cannot see.
546 const error = useRouteError();
547 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look548 // The billing page shows the full banner itself.
549 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents550 <SpikeBanner
551 compact
552 slug={root.shell.workspace.slug}
553 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
554 owner={root.shell.compute.owner}
555 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look556 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)557 // On the few pages an account still confirming its address can open
558 // besides /confirm-email (policies, support, signing in): the way back.
559 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
560 <p
561 role="status"
Agents as a team: lifecycle, merge queue, billing and a new shell562 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"
563 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)564 <span>Confirm your email address to start using g1t.</span>
565 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
566 Enter your code
567 </Link>
568 </p>
Agents as a team: lifecycle, merge queue, billing and a new shell569 );
Merge main (membership, two-factor, GitHub repo roles) into tokens570 // A workspace that requires two-factor authentication of someone who
571 // has not turned it on: they keep their place, and cannot use it yet.
572 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'573 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look574 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens575 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look576 <>
577 {paused}
578 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens579 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look580 </>
581 ) : null;
Email verification, password reset, and Git for AI scale positioning582 return (
583 <html lang="en">
584 <head>
585 <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)586 <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 plans587 <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)588 <meta name="apple-mobile-web-app-capable" content="yes" />
589 <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 preloads590 {/* The stylesheet before everything React Router preloads, so a slow
591 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads592 <link rel="stylesheet" href={appCss} precedence="default" />
Email verification, password reset, and Git for AI scale positioning593 <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 address594 <Links nonce={nonce} />
Email verification, password reset, and Git for AI scale positioning595 </head>
596 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit597 {/* The first stop for the keyboard: past the menus, to the page. */}
598 <a
599 href="#content"
600 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]"
601 >
602 Skip to content
603 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)604 {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 quotas605 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Agents as a team: lifecycle, merge queue, billing and a new shell606 {children}
607 </AppShell>
608 ) : (
609 <>
610 <Progress />
611 <Header user={user} />
612 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit613 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'614 {children}
615 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look616 <SiteFooter user={user} />
Agents as a team: lifecycle, merge queue, billing and a new shell617 </>
Email verification, password reset, and Git for AI scale positioning618 )}
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)619 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
620 {user && !awaitsConfirmation(user) && (
621 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
622 )}
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 address623 <ScrollRestoration nonce={nonce} />
624 <Scripts nonce={nonce} />
Email verification, password reset, and Git for AI scale positioning625 </body>
626 </html>
627 );
628}
629
630export default function App() {
Site analytics with HeyCatch on g1t.sh, with names removed inside the app; the privacy policy says so631 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
632 // Tell analytics who is signed in, once per change; signing out forgets them.
633 const lastUserId = useRef<string | null>(null);
634 useEffect(() => {
635 identify(userId, lastUserId.current);
636 lastUserId.current = userId;
637 }, [userId]);
Email verification, password reset, and Git for AI scale positioning638 return <Outlet />;
639}
640
641export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit642 const location = useLocation();
643 const matches = useMatches();
644 // A tab left open across a deploy: load the address again as a whole
645 // page, once, rather than show an error the reload fixes.
646 const reload = reloadFixes({
647 error,
648 status: isRouteErrorResponse(error) ? error.status : undefined,
649 // The first page a tab loads has the key "default"; a client navigation's is its own.
650 clientNavigation: location.key !== "default",
651 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
652 });
653 const href = location.pathname + location.search + location.hash;
654 useEffect(() => {
655 if (!reload) return;
656 try {
657 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
658 sessionStorage.setItem(RELOADED_KEY, href);
659 } catch {
660 // No session storage: reload anyway; a document load never asks again.
661 }
662 window.location.assign(href);
663 }, [reload, href]);
664 if (reload) {
665 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in666 <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 audit667 <title>Loading · g1t</title>
668 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 in669 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit670 );
671 }
672
Email verification, password reset, and Git for AI scale positioning673 let title = "Something went wrong";
674 let details = "An unexpected error occurred. Try again in a moment.";
675 let stack: string | undefined;
676
677 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put678 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit679 if (error.status === 404) {
680 return (
681 <>
682 <title>Not found · g1t</title>
683 <NotFound data={error.data} />
684 </>
685 );
686 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put687 if (typeof error.data === "string" && error.data) {
Email verification, password reset, and Git for AI scale positioning688 details = error.data;
689 }
690 } else if (import.meta.env.DEV && error instanceof Error) {
691 details = error.message;
692 stack = error.stack;
693 }
694
695 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in696 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Email verification, password reset, and Git for AI scale positioning697 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
698 <p className="mt-3 text-muted">{details}</p>
699 <div className="mt-8">
700 <ButtonLink to="/" variant="quiet">
701 Back to home
702 </ButtonLink>
703 </div>
704 {stack && (
705 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
706 <code>{stack}</code>
707 </pre>
708 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in709 </PageMain>
Email verification, password reset, and Git for AI scale positioning710 );
711}

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