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

Send g1t's own runs through the model proxy too1import {
2 BookOpen,
3 ChevronDown,
Search across all of g1t, Explore, and a command palette4 Compass,
5 CreditCard,
Send g1t's own runs through the model proxy too6 LayoutDashboard,
Search across all of g1t, Explore, and a command palette7 LogIn,
Send g1t's own runs through the model proxy too8 LogOut,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9 Menu,
Send g1t's own runs through the model proxy too10 Plus,
11 Search,
12 Settings,
13} from "lucide-react";
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today14import { useEffect, useRef, useState } from "react";
Send g1t's own runs through the model proxy too15import {
16 Form,
17 isRouteErrorResponse,
18 Link,
19 Links,
20 Meta,
21 NavLink,
22 Outlet,
23 Scripts,
24 ScrollRestoration,
25 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,
Send g1t's own runs through the model proxy too30 useRouteLoaderData,
31 useSubmit,
32} 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";
Send g1t's own runs through the model proxy too35
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";
Send g1t's own runs through the model proxy too40import { Logo } from "./components/logo";
Merge branch 'worktree-agent-a985c01f43538af01'41import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui";
Send g1t's own runs through the model proxy too42import {
43 DropdownMenu,
44 DropdownMenuContent,
45 DropdownMenuItem,
46 DropdownMenuLabel,
47 DropdownMenuSeparator,
48 DropdownMenuTrigger,
49} from "./components/ui/dropdown-menu";
Merge branch 'worktree-agent-aca4790ea223aa235'50import { AppShell, Progress, type ShellData, useLeaving } from "./components/shell";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51import { SiteFooter } from "./components/footer";
Fast pages, required checks on the branch, self-hosted runners, honest incidents52import { SpikeBanner } from "./components/spike-banner";
Merge main (membership, two-factor, GitHub repo roles) into tokens53import { PolicyNotice } from "./components/policy-notice";
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today54import { identify } from "./lib/analytics.client";
55import { visitorAsksFirst } from "./lib/analytics-consent";
56import { AnalyticsConsent } from "./components/analytics-consent";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put57import { readCookie } from "./lib/mission";
58import { WORKSPACE_COOKIE, workspaceFor } from "./lib/workspace-choice";
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in59import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put60import { NotFound } from "./components/not-found";
61import { usesAppShell } from "./lib/chrome";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit62import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./components/command-palette";
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)63import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to64import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents65import { countsFor, readableRepos } from "./lib/access.server";
66import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar68import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look69import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'70import { addresses } from "./lib/addresses.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look71import { useSignUpCopy } from "./lib/registration";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit72import { RELOADED_KEY, reloadFixes } from "./lib/stale-build";
Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address73import { useNonce } from "./lib/nonce";
Merge branch 'worktree-agent-ad4439ce85a91ecb4' into integrate74import { isNeedsSignIn } from "./lib/website-token";
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)75import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look76
Send g1t's own runs through the model proxy too77
78export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark79 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Send g1t's own runs through the model proxy too80 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark81 { rel: "icon", type: "image/png", sizes: "192x192", href: "/icon-192.png" },
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers82 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
83 // Home screens and password managers look for a square, opaque 180px tile;
84 // the "-precomposed" name is served too, for the ones that ask for it.
85 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
86 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
87 // manifest.webmanifest is the same file, kept for installs that already point at it).
88 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look89 // The text and headline faces are wanted on every page, so they start
90 // loading with the stylesheet; mono waits until something uses it.
91 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
92 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Send g1t's own runs through the model proxy too93];
94
95export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
96
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put97export async function loader({ context, params, request }: Route.LoaderArgs) {
Send g1t's own runs through the model proxy too98 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put99 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)100 // Whether sign-up takes an invite: the sign-up page says so, and
101 // 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 look102 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)103 // An account still confirming its address sees only the pages that
104 // allows (lib/confirm-gate.ts), in the visitor's frame.
105 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit106 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
107 // A sidebar that could not be read is no reason to lose the page,
108 // or to draw it as if they were signed out.
109 console.error("root: the sidebar could not be read", error);
110 return bareShell(user, params, chosen);
111 })
112 : 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)113 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'115 // Where this g1t lives, for clone lines, agent setup and link previews.
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today116 return {
117 user,
118 shell,
119 inviteOnly: mode !== "open",
120 addresses: addresses(),
121 // Visitors from where the law asks first are asked before analytics runs.
122 analyticsConsent: visitorAsksFirst(request),
123 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put124}
125
126/**
127 * The sidebar for someone not signed in: nothing of their own, only the
128 * open counts of the project being looked at. One call, and only in a
129 * project; it answers the same for a private project as a missing one.
130 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents131async 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 put132 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents133 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 put134 return {
135 workspace: null,
136 repos: [],
137 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
138 limit: null,
139 monthUsageMicros: null,
140 };
Send g1t's own runs through the model proxy too141}
142
Usage, Billing settings and prepaid AI credit; fixes from the UX audit143/** The sidebar from what the session already says, with no service asked. */
144function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
145 return {
146 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
147 repos: [],
148 repo: null,
149 limit: null,
150 monthUsageMicros: null,
151 };
152}
153
Send g1t's own runs through the model proxy too154/**
155 * The sidebar only changes with the workspace or repository being looked
156 * at, or after something was submitted: not on every page within them.
157 */
158export function shouldRevalidate({
159 currentParams,
160 nextParams,
161 formMethod,
162 defaultShouldRevalidate,
163}: ShouldRevalidateFunctionArgs) {
164 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
165 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
166}
167
168/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit169 * The sidebar: the workspace whose pages or project these are when it is
170 * one of yours, else the one you chose (lib/workspace-choice.ts); its
171 * projects; and the repository being looked at.
Send g1t's own runs through the model proxy too172 */
173async function shellFor(
174 user: User,
175 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put176 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents177 context: Route.LoaderArgs["context"],
Send g1t's own runs through the model proxy too178): Promise<ShellData> {
179 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put180 const workspace = workspaceFor(memberships, chosen, params);
Send g1t's own runs through the model proxy too181 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
182 const now = new Date();
183 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents184 // What the sidebar shows of the workspace changes rarely: kept for a few
185 // seconds per person and workspace, except just after they changed
186 // something (lib/cache.server.ts).
187 const kept = <T,>(what: string, load: () => Promise<T>) =>
188 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)189 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 sidebar190 // Every project, for the palette and the count; the sidebar lists only
191 // the person's pinned and recent ones (lib/pins.ts).
192 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents193 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 processor194 // Whether billing is on, without reading the account: that asks the
195 // card processor about the workspace's cards, too slow for every page.
196 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit197 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents198 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
199 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
200 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all201 // The bell's count: one query, never kept, so marking an item shows at once.
202 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar203 // Never kept: opening a project moves it up Recent.
204 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to205 // The rail's Chat badge: never kept, and never waited on for long.
206 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)207 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
208 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Send g1t's own runs through the model proxy too209 ]);
210 return {
211 workspace,
Projects: what a workspace builds and runs, first on every page212 // 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 sidebar213 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
214 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
215 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Send g1t's own runs through the model proxy too216 repo:
217 path && counts?.ok
218 ? {
219 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look220 // Until the repository's own page says what their role is.
221 member: hasAccessIn(user, path.namespace),
Send g1t's own runs through the model proxy too222 issues: counts.value.issues,
223 pulls: counts.value.pulls,
224 }
225 : null,
Billing accounts, terms and enterprises; g1t is no longer free226 // Where the workspace stands against its usage limit, once billing is on.
227 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor228 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free229 ? {
230 exposureMicros: limit.value.exposureMicros,
231 ceilingMicros: limit.value.ceilingMicros,
232 state: limit.value.state,
233 comped: limit.value.trust === "internal",
234 }
235 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor236 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 // A spend spike or a hold pauses new compute: the shell says so on every page.
238 compute:
239 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
240 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
241 : null,
242 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all243 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to244 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)245 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy246 // 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 audit247 // Usage at price, the one figure every page shows.
248 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Send g1t's own runs through the model proxy too249 };
250}
251
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)252/** Starred and recent conversations Home's sidebar lists. */
253const SHELL_CONVERSATIONS = 8;
254
255/**
256 * What is unread in chat, for the rail, and the starred and latest
257 * conversations, for Home's sidebar; null when chat is slow to answer or
258 * does not.
259 */
Chat and workspace agents: channels, DMs and named agents you talk to260async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
261 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
262 const read = chat
263 .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)264 .then((result) => {
265 if (!result.ok) return null;
266 const entries = result.value.entries;
267 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
268 return {
269 ...unreadTotals(entries),
270 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
271 recent: latest.slice(0, SHELL_CONVERSATIONS),
272 };
273 })
274 .catch(() => null);
275 return Promise.race([read, timeout]);
276}
277
278/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
279async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
280 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
281 const read = workspaceAgents
282 .list(slug, user)
283 .then((result) =>
284 result.ok
285 ? result.value
286 .filter((agent) => !agent.archived_at)
287 .map((agent) => ({
288 id: agent.id,
289 handle: agent.handle,
290 display_name: agent.display_name,
291 avatar: agent.avatar,
292 avatar_seed: agent.avatar_seed,
293 role: agent.role,
294 title: agent.title,
295 team: agent.team,
296 department: agent.department,
297 status: agent.status,
298 builtin: agent.builtin === true,
299 }))
300 : null,
301 )
Chat and workspace agents: channels, DMs and named agents you talk to302 .catch(() => null);
303 return Promise.race([read, timeout]);
304}
305
306/** The longest the rail's Chat badge holds up a page. */
307const CHAT_BADGE_WAIT_MS = 300;
308
Usage, Billing settings and prepaid AI credit; fixes from the UX audit309/**
310 * How long the sidebar's workspace answers are kept: as long as someone's
311 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
312 */
313const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents314
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look315/** Most repositories listed under Shared with you. */
316const MAX_SHARED = 20;
317
318/**
319 * Repositories shared with the user in workspaces they do not belong to,
320 * by their grants: only those, never the workspace's other repositories.
321 */
322async function sharedRepos(user: User): Promise<ShellData["shared"]> {
323 const outside = new Set(sharedWorkspaces(user));
324 if (outside.size === 0) return [];
325 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 incidents326 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look327 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents328 .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 look329 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
330}
331
Send g1t's own runs through the model proxy too332function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
333 return (
334 <NavLink
335 to={to}
336 className={({ isActive }) =>
337 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
338 isActive ? "text-fg" : "text-muted"
339 }`
340 }
341 >
342 {children}
343 </NavLink>
344 );
345}
346
Search across all of g1t, Explore, and a command palette347/** What the palette offers someone without the app's sidebar. */
348const PUBLIC_COMMANDS: PaletteCommand[] = [
349 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
350 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
351 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
352 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
353 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
354 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
355];
356
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite357/** 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 look358function SignUpButton() {
359 const copy = useSignUpCopy();
360 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
361}
362
Send g1t's own runs through the model proxy too363function Header({ user }: { user: User | null | undefined }) {
364 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look365 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette366 const [palette, setPalette] = useState(false);
367 usePaletteShortcut(() => setPalette((open) => !open));
368 const params = useParams();
369 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
Send g1t's own runs through the model proxy too370 return (
371 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
372 <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 look373 <Link to="/" aria-label="g1t home" className="mr-2 flex">
Send g1t's own runs through the model proxy too374 <Logo />
375 </Link>
376 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
377 <Search
378 size={14}
379 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
380 />
381 <input
382 name="q"
383 {...notACredential()}
Search across all of g1t, Explore, and a command palette384 placeholder="Search g1t"
385 aria-label="Search g1t"
386 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"
Send g1t's own runs through the model proxy too387 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit388 <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" />
Send g1t's own runs through the model proxy too389 </Form>
Search across all of g1t, Explore, and a command palette390 <button
391 type="button"
392 aria-label="Search g1t"
393 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look394 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 palette395 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look396 <Search size={18} />
Search across all of g1t, Explore, and a command palette397 </button>
398 <CommandPalette
399 open={palette}
400 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)401 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 palette402 repo={repo}
403 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look404 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
Send g1t's own runs through the model proxy too405 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays406 <HeaderLink to="/pricing">Pricing</HeaderLink>
Send g1t's own runs through the model proxy too407 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
408 </nav>
409 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look410 {/* On a phone the links fold into one menu, so the bar fits. */}
411 <DropdownMenu>
412 <DropdownMenuTrigger
413 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit414 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 look415 >
416 <Menu size={18} />
417 </DropdownMenuTrigger>
418 <DropdownMenuContent align="end" className="w-52">
419 <DropdownMenuItem asChild className="min-h-11">
420 <Link to="/explore">
421 <Compass />
422 Explore
423 </Link>
424 </DropdownMenuItem>
425 <DropdownMenuItem asChild className="min-h-11">
426 <Link to="/pricing">
427 <CreditCard />
428 Pricing
429 </Link>
430 </DropdownMenuItem>
431 <DropdownMenuItem asChild className="min-h-11">
432 <Link to="https://docs.g1t.sh/">
433 <BookOpen />
434 Docs
435 </Link>
436 </DropdownMenuItem>
437 {!user && (
438 <>
439 <DropdownMenuSeparator />
440 <DropdownMenuItem asChild className="min-h-11">
441 <Link to="/login">
442 <LogIn />
443 Sign in
444 </Link>
445 </DropdownMenuItem>
446 </>
447 )}
448 </DropdownMenuContent>
449 </DropdownMenu>
Send g1t's own runs through the model proxy too450 {user ? (
451 <>
452 <Link
453 to="/new"
Projects live: fixes from walking them end to end454 aria-label="New project"
Send g1t's own runs through the model proxy too455 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
456 >
457 <Plus size={16} />
458 </Link>
459 <DropdownMenu>
460 <DropdownMenuTrigger
461 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit462 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"
Send g1t's own runs through the model proxy too463 >
Workspace names and icons, and a component kit for every control464 <Avatar name={user.username} image={user.avatar} size={24} />
Send g1t's own runs through the model proxy too465 <ChevronDown size={14} className="text-faint" />
466 </DropdownMenuTrigger>
467 <DropdownMenuContent align="end" className="w-56">
468 <DropdownMenuLabel>
469 Signed in as{" "}
470 <span className="font-mono font-medium text-fg">
471 {user.username}
472 </span>
473 </DropdownMenuLabel>
474 <DropdownMenuSeparator />
475 {(user.workspaces ?? []).map((membership) => (
476 <DropdownMenuItem asChild key={membership.slug}>
477 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control478 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
479 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Send g1t's own runs through the model proxy too480 </Link>
481 </DropdownMenuItem>
482 ))}
483 <DropdownMenuItem asChild>
484 <Link to="/workspaces/new">
485 <Plus />
486 New workspace
487 </Link>
488 </DropdownMenuItem>
489 <DropdownMenuSeparator />
490 <DropdownMenuItem asChild>
491 <Link to="/">
492 <LayoutDashboard />
493 Mission control
494 </Link>
495 </DropdownMenuItem>
496 <DropdownMenuItem asChild>
497 <Link to="/new">
498 <Plus />
Projects live: fixes from walking them end to end499 New project
Send g1t's own runs through the model proxy too500 </Link>
501 </DropdownMenuItem>
502 <DropdownMenuSeparator />
503 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas504 <Link to="/settings/profile">
Send g1t's own runs through the model proxy too505 <Settings />
506 Settings
507 </Link>
508 </DropdownMenuItem>
509 <DropdownMenuItem asChild>
510 <Link to="https://docs.g1t.sh/quickstart/">
511 <BookOpen />
512 Documentation
513 </Link>
514 </DropdownMenuItem>
515 <DropdownMenuSeparator />
516 {/* Submitted from here: the menu closes on select, and a button
517 that has left the page cannot submit a form. */}
518 <DropdownMenuItem
519 onSelect={() => submit(null, { method: "post", action: "/logout" })}
520 >
521 <LogOut />
522 Sign out
523 </DropdownMenuItem>
524 </DropdownMenuContent>
525 </DropdownMenu>
526 </>
527 ) : (
528 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look529 <span className="hidden sm:contents">
530 <HeaderLink to="/login">Sign in</HeaderLink>
531 </span>
532 <SignUpButton />
Send g1t's own runs through the model proxy too533 </>
534 )}
535 </div>
536 </div>
537 </header>
538 );
539}
540
Usage, Billing settings and prepaid AI credit; fixes from the UX audit541/**
542 * The root's data as the browser last had it. When a navigation fails in a
543 * way that takes the root's data with it, the error is still drawn in the
544 * person's own frame, signed in, not the visitor's. Never kept on the
545 * server, where one module serves everyone.
546 */
547let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
548
Send g1t's own runs through the model proxy too549export 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 address550 const nonce = useNonce();
Send g1t's own runs through the model proxy too551 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit552 const loaded = useRouteLoaderData<typeof loader>("root");
553 const inBrowser = typeof document !== "undefined";
554 if (loaded && inBrowser) lastRoot = loaded;
555 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Send g1t's own runs through the model proxy too556 const user = root?.user;
Merge branch 'worktree-agent-a985c01f43538af01'557 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas558 // Drawn around the error page too: a 404 keeps the sidebar out of a
559 // project or workspace the viewer cannot see.
560 const error = useRouteError();
561 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look562 // The billing page shows the full banner itself.
563 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents564 <SpikeBanner
565 compact
566 slug={root.shell.workspace.slug}
567 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
568 owner={root.shell.compute.owner}
569 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look570 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)571 // On the few pages an account still confirming its address can open
572 // besides /confirm-email (policies, support, signing in): the way back.
573 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
574 <p
575 role="status"
Send g1t's own runs through the model proxy too576 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"
577 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)578 <span>Confirm your email address to start using g1t.</span>
579 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
580 Enter your code
581 </Link>
582 </p>
Send g1t's own runs through the model proxy too583 );
Merge main (membership, two-factor, GitHub repo roles) into tokens584 // A workspace that requires two-factor authentication of someone who
585 // has not turned it on: they keep their place, and cannot use it yet.
586 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'587 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look588 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens589 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look590 <>
591 {paused}
592 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens593 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look594 </>
595 ) : null;
Send g1t's own runs through the model proxy too596 return (
597 <html lang="en">
598 <head>
599 <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)600 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Send g1t's own runs through the model proxy too601 <meta name="theme-color" content="#0f0f11" />
Merge branch 'mirroring' into artifacts-mode602 <meta name="mobile-web-app-capable" content="yes" />
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)603 <meta name="apple-mobile-web-app-capable" content="yes" />
604 <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 preloads605 {/* The stylesheet before everything React Router preloads, so a slow
606 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads607 <link rel="stylesheet" href={appCss} precedence="default" />
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today608 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Send g1t's own runs through the model proxy too609 <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 address610 <Links nonce={nonce} />
Send g1t's own runs through the model proxy too611 </head>
612 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit613 {/* The first stop for the keyboard: past the menus, to the page. */}
614 <a
615 href="#content"
616 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]"
617 >
618 Skip to content
619 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)620 {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 quotas621 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Send g1t's own runs through the model proxy too622 {children}
623 </AppShell>
624 ) : (
625 <>
626 <Progress />
627 <Header user={user} />
628 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit629 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'630 {children}
631 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look632 <SiteFooter user={user} />
Send g1t's own runs through the model proxy too633 </>
634 )}
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)635 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
636 {user && !awaitsConfirmation(user) && (
637 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
638 )}
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today639 <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 address640 <ScrollRestoration nonce={nonce} />
641 <Scripts nonce={nonce} />
Send g1t's own runs through the model proxy too642 </body>
643 </html>
644 );
645}
646
647export default function App() {
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today648 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
649 // Tell analytics who is signed in, once per change; signing out forgets them.
650 const lastUserId = useRef<string | null>(null);
651 useEffect(() => {
652 identify(userId, lastUserId.current);
653 lastUserId.current = userId;
654 }, [userId]);
Send g1t's own runs through the model proxy too655 return <Outlet />;
656}
657
658export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit659 const location = useLocation();
660 const matches = useMatches();
661 // A tab left open across a deploy: load the address again as a whole
662 // page, once, rather than show an error the reload fixes.
663 const reload = reloadFixes({
664 error,
665 status: isRouteErrorResponse(error) ? error.status : undefined,
666 // The first page a tab loads has the key "default"; a client navigation's is its own.
667 clientNavigation: location.key !== "default",
668 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
669 });
670 const href = location.pathname + location.search + location.hash;
671 useEffect(() => {
672 if (!reload) return;
673 try {
674 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
675 sessionStorage.setItem(RELOADED_KEY, href);
676 } catch {
677 // No session storage: reload anyway; a document load never asks again.
678 }
679 window.location.assign(href);
680 }, [reload, href]);
681 if (reload) {
682 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in683 <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 audit684 <title>Loading · g1t</title>
685 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 in686 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit687 );
688 }
689
Send g1t's own runs through the model proxy too690 let title = "Something went wrong";
691 let details = "An unexpected error occurred. Try again in a moment.";
692 let stack: string | undefined;
693
694 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put695 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit696 if (error.status === 404) {
697 return (
698 <>
699 <title>Not found · g1t</title>
700 <NotFound data={error.data} />
701 </>
702 );
703 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put704 if (typeof error.data === "string" && error.data) {
Send g1t's own runs through the model proxy too705 details = error.data;
706 }
Merge branch 'worktree-agent-ad4439ce85a91ecb4' into integrate707 // A token asked for what needs a real sign-in (lib/website-token.ts).
708 if (isNeedsSignIn(error.data)) {
709 title = "This needs you to sign in";
710 details = error.data.message;
711 }
Send g1t's own runs through the model proxy too712 } else if (import.meta.env.DEV && error instanceof Error) {
713 details = error.message;
714 stack = error.stack;
715 }
716
717 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in718 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Send g1t's own runs through the model proxy too719 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
720 <p className="mt-3 text-muted">{details}</p>
721 <div className="mt-8">
722 <ButtonLink to="/" variant="quiet">
723 Back to home
724 </ButtonLink>
725 </div>
726 {stack && (
727 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
728 <code>{stack}</code>
729 </pre>
730 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in731 </PageMain>
Send g1t's own runs through the model proxy too732 );
733}

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