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

Model providers: gateway tokens for endpoints, tidier rows, and the docs1import {
2 BookOpen,
3 ChevronDown,
Search across all of g1t, Explore, and a command palette4 Compass,
5 CreditCard,
Model providers: gateway tokens for endpoints, tidier rows, and the docs6 LayoutDashboard,
Search across all of g1t, Explore, and a command palette7 LogIn,
Model providers: gateway tokens for endpoints, tidier rows, and the docs8 LogOut,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look9 Menu,
Model providers: gateway tokens for endpoints, tidier rows, and the docs10 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";
Model providers: gateway tokens for endpoints, tidier rows, and the docs15import {
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,
Model providers: gateway tokens for endpoints, tidier rows, and the docs30 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";
Model providers: gateway tokens for endpoints, tidier rows, and the docs35
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";
Model providers: gateway tokens for endpoints, tidier rows, and the docs40import { Logo } from "./components/logo";
Merge branch 'worktree-agent-a985c01f43538af01'41import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui";
Model providers: gateway tokens for endpoints, tidier rows, and the docs42import {
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 main into Artifacts Phase 274import { isNeedsSignIn } from "./lib/website-token";
A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works.75import { setLiveViaToken } from "./lib/live-socket";
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)76import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look77
Model providers: gateway tokens for endpoints, tidier rows, and the docs78
79export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark80 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Model providers: gateway tokens for endpoints, tidier rows, and the docs81 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark82 { 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 managers83 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
84 // Home screens and password managers look for a square, opaque 180px tile;
85 // the "-precomposed" name is served too, for the ones that ask for it.
86 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
87 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
88 // manifest.webmanifest is the same file, kept for installs that already point at it).
89 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look90 // The text and headline faces are wanted on every page, so they start
91 // loading with the stylesheet; mono waits until something uses it.
92 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
93 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Model providers: gateway tokens for endpoints, tidier rows, and the docs94];
95
96export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
97
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put98export async function loader({ context, params, request }: Route.LoaderArgs) {
Model providers: gateway tokens for endpoints, tidier rows, and the docs99 const user = getViewer(context);
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put100 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)101 // Whether sign-up takes an invite: the sign-up page says so, and
102 // 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 look103 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)104 // An account still confirming its address sees only the pages that
105 // allows (lib/confirm-gate.ts), in the visitor's frame.
106 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit107 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
108 // A sidebar that could not be read is no reason to lose the page,
109 // or to draw it as if they were signed out.
110 console.error("root: the sidebar could not be read", error);
111 return bareShell(user, params, chosen);
112 })
113 : 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)114 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look115 ]);
Merge branch 'worktree-agent-aaf03bdceac799c89'116 // 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 today117 return {
118 user,
119 shell,
120 inviteOnly: mode !== "open",
121 addresses: addresses(),
122 // Visitors from where the law asks first are asked before analytics runs.
123 analyticsConsent: visitorAsksFirst(request),
124 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put125}
126
127/**
128 * The sidebar for someone not signed in: nothing of their own, only the
129 * open counts of the project being looked at. One call, and only in a
130 * project; it answers the same for a private project as a missing one.
131 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents132async 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 put133 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents134 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 put135 return {
136 workspace: null,
137 repos: [],
138 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
139 limit: null,
140 monthUsageMicros: null,
141 };
Model providers: gateway tokens for endpoints, tidier rows, and the docs142}
143
Usage, Billing settings and prepaid AI credit; fixes from the UX audit144/** The sidebar from what the session already says, with no service asked. */
145function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
146 return {
147 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
148 repos: [],
149 repo: null,
150 limit: null,
151 monthUsageMicros: null,
152 };
153}
154
Model providers: gateway tokens for endpoints, tidier rows, and the docs155/**
156 * The sidebar only changes with the workspace or repository being looked
157 * at, or after something was submitted: not on every page within them.
158 */
159export function shouldRevalidate({
160 currentParams,
161 nextParams,
162 formMethod,
163 defaultShouldRevalidate,
164}: ShouldRevalidateFunctionArgs) {
165 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
166 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
167}
168
169/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit170 * The sidebar: the workspace whose pages or project these are when it is
171 * one of yours, else the one you chose (lib/workspace-choice.ts); its
172 * projects; and the repository being looked at.
Model providers: gateway tokens for endpoints, tidier rows, and the docs173 */
174async function shellFor(
175 user: User,
176 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put177 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents178 context: Route.LoaderArgs["context"],
Model providers: gateway tokens for endpoints, tidier rows, and the docs179): Promise<ShellData> {
180 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put181 const workspace = workspaceFor(memberships, chosen, params);
Model providers: gateway tokens for endpoints, tidier rows, and the docs182 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
183 const now = new Date();
184 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents185 // What the sidebar shows of the workspace changes rarely: kept for a few
186 // seconds per person and workspace, except just after they changed
187 // something (lib/cache.server.ts).
188 const kept = <T,>(what: string, load: () => Promise<T>) =>
189 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)190 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 sidebar191 // Every project, for the palette and the count; the sidebar lists only
192 // the person's pinned and recent ones (lib/pins.ts).
193 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents194 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 processor195 // Whether billing is on, without reading the account: that asks the
196 // card processor about the workspace's cards, too slow for every page.
197 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit198 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents199 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
200 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
201 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all202 // The bell's count: one query, never kept, so marking an item shows at once.
203 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar204 // Never kept: opening a project moves it up Recent.
205 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to206 // The rail's Chat badge: never kept, and never waited on for long.
207 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)208 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
209 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
Model providers: gateway tokens for endpoints, tidier rows, and the docs210 ]);
211 return {
212 workspace,
Projects: what a workspace builds and runs, first on every page213 // 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 sidebar214 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
215 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
216 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Model providers: gateway tokens for endpoints, tidier rows, and the docs217 repo:
218 path && counts?.ok
219 ? {
220 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look221 // Until the repository's own page says what their role is.
222 member: hasAccessIn(user, path.namespace),
Model providers: gateway tokens for endpoints, tidier rows, and the docs223 issues: counts.value.issues,
224 pulls: counts.value.pulls,
225 }
226 : null,
Billing accounts, terms and enterprises; g1t is no longer free227 // Where the workspace stands against its usage limit, once billing is on.
228 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor229 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free230 ? {
231 exposureMicros: limit.value.exposureMicros,
232 ceilingMicros: limit.value.ceilingMicros,
233 state: limit.value.state,
234 comped: limit.value.trust === "internal",
235 }
236 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor237 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look238 // A spend spike or a hold pauses new compute: the shell says so on every page.
239 compute:
240 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
241 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
242 : null,
243 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all244 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to245 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)246 agents: teamAgents,
Usage while free is shown at cost; agents get rustfmt and clippy247 // 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 audit248 // Usage at price, the one figure every page shows.
249 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Model providers: gateway tokens for endpoints, tidier rows, and the docs250 };
251}
252
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)253/** Starred and recent conversations Home's sidebar lists. */
254const SHELL_CONVERSATIONS = 8;
255
256/**
257 * What is unread in chat, for the rail, and the starred and latest
258 * conversations, for Home's sidebar; null when chat is slow to answer or
259 * does not.
260 */
Chat and workspace agents: channels, DMs and named agents you talk to261async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
262 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
263 const read = chat
264 .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)265 .then((result) => {
266 if (!result.ok) return null;
267 const entries = result.value.entries;
268 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
269 return {
270 ...unreadTotals(entries),
271 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
272 recent: latest.slice(0, SHELL_CONVERSATIONS),
273 };
274 })
275 .catch(() => null);
276 return Promise.race([read, timeout]);
277}
278
279/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
280async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
281 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
282 const read = workspaceAgents
283 .list(slug, user)
284 .then((result) =>
285 result.ok
286 ? result.value
287 .filter((agent) => !agent.archived_at)
288 .map((agent) => ({
289 id: agent.id,
290 handle: agent.handle,
291 display_name: agent.display_name,
292 avatar: agent.avatar,
293 avatar_seed: agent.avatar_seed,
294 role: agent.role,
295 title: agent.title,
296 team: agent.team,
297 department: agent.department,
298 status: agent.status,
299 builtin: agent.builtin === true,
300 }))
301 : null,
302 )
Chat and workspace agents: channels, DMs and named agents you talk to303 .catch(() => null);
304 return Promise.race([read, timeout]);
305}
306
307/** The longest the rail's Chat badge holds up a page. */
308const CHAT_BADGE_WAIT_MS = 300;
309
Usage, Billing settings and prepaid AI credit; fixes from the UX audit310/**
311 * How long the sidebar's workspace answers are kept: as long as someone's
312 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
313 */
314const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents315
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look316/** Most repositories listed under Shared with you. */
317const MAX_SHARED = 20;
318
319/**
320 * Repositories shared with the user in workspaces they do not belong to,
321 * by their grants: only those, never the workspace's other repositories.
322 */
323async function sharedRepos(user: User): Promise<ShellData["shared"]> {
324 const outside = new Set(sharedWorkspaces(user));
325 if (outside.size === 0) return [];
326 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 incidents327 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look328 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents329 .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 look330 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
331}
332
Model providers: gateway tokens for endpoints, tidier rows, and the docs333function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
334 return (
335 <NavLink
336 to={to}
337 className={({ isActive }) =>
338 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
339 isActive ? "text-fg" : "text-muted"
340 }`
341 }
342 >
343 {children}
344 </NavLink>
345 );
346}
347
Search across all of g1t, Explore, and a command palette348/** What the palette offers someone without the app's sidebar. */
349const PUBLIC_COMMANDS: PaletteCommand[] = [
350 { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> },
351 { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> },
352 { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> },
353 { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> },
354 { label: "Sign in", to: "/login", icon: <LogIn size={15} /> },
355 { label: "Sign up", to: "/register", icon: <Plus size={15} /> },
356];
357
Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite358/** 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 look359function SignUpButton() {
360 const copy = useSignUpCopy();
361 return <ButtonLink to="/register">{copy.primary}</ButtonLink>;
362}
363
Model providers: gateway tokens for endpoints, tidier rows, and the docs364function Header({ user }: { user: User | null | undefined }) {
365 const submit = useSubmit();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look366 const signUp = useSignUpCopy();
Search across all of g1t, Explore, and a command palette367 const [palette, setPalette] = useState(false);
368 usePaletteShortcut(() => setPalette((open) => !open));
369 const params = useParams();
370 const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null;
Model providers: gateway tokens for endpoints, tidier rows, and the docs371 return (
372 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
373 <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 look374 <Link to="/" aria-label="g1t home" className="mr-2 flex">
Model providers: gateway tokens for endpoints, tidier rows, and the docs375 <Logo />
376 </Link>
377 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
378 <Search
379 size={14}
380 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
381 />
382 <input
383 name="q"
384 {...notACredential()}
Search across all of g1t, Explore, and a command palette385 placeholder="Search g1t"
386 aria-label="Search g1t"
387 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"
Model providers: gateway tokens for endpoints, tidier rows, and the docs388 />
Usage, Billing settings and prepaid AI credit; fixes from the UX audit389 <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" />
Model providers: gateway tokens for endpoints, tidier rows, and the docs390 </Form>
Search across all of g1t, Explore, and a command palette391 <button
392 type="button"
393 aria-label="Search g1t"
394 onClick={() => setPalette(true)}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look395 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 palette396 >
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look397 <Search size={18} />
Search across all of g1t, Explore, and a command palette398 </button>
399 <CommandPalette
400 open={palette}
401 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)402 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 palette403 repo={repo}
404 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look405 <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex">
Model providers: gateway tokens for endpoints, tidier rows, and the docs406 <HeaderLink to="/explore">Explore</HeaderLink>
Prices keep themselves current with what g1t pays407 <HeaderLink to="/pricing">Pricing</HeaderLink>
Model providers: gateway tokens for endpoints, tidier rows, and the docs408 <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink>
409 </nav>
410 <div className="ml-auto flex items-center gap-2">
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look411 {/* On a phone the links fold into one menu, so the bar fits. */}
412 <DropdownMenu>
413 <DropdownMenuTrigger
414 aria-label="Menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit415 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 look416 >
417 <Menu size={18} />
418 </DropdownMenuTrigger>
419 <DropdownMenuContent align="end" className="w-52">
420 <DropdownMenuItem asChild className="min-h-11">
421 <Link to="/explore">
422 <Compass />
423 Explore
424 </Link>
425 </DropdownMenuItem>
426 <DropdownMenuItem asChild className="min-h-11">
427 <Link to="/pricing">
428 <CreditCard />
429 Pricing
430 </Link>
431 </DropdownMenuItem>
432 <DropdownMenuItem asChild className="min-h-11">
433 <Link to="https://docs.g1t.sh/">
434 <BookOpen />
435 Docs
436 </Link>
437 </DropdownMenuItem>
438 {!user && (
439 <>
440 <DropdownMenuSeparator />
441 <DropdownMenuItem asChild className="min-h-11">
442 <Link to="/login">
443 <LogIn />
444 Sign in
445 </Link>
446 </DropdownMenuItem>
447 </>
448 )}
449 </DropdownMenuContent>
450 </DropdownMenu>
Model providers: gateway tokens for endpoints, tidier rows, and the docs451 {user ? (
452 <>
453 <Link
454 to="/new"
Projects live: fixes from walking them end to end455 aria-label="New project"
Model providers: gateway tokens for endpoints, tidier rows, and the docs456 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
457 >
458 <Plus size={16} />
459 </Link>
460 <DropdownMenu>
461 <DropdownMenuTrigger
462 aria-label="Account menu"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit463 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"
Model providers: gateway tokens for endpoints, tidier rows, and the docs464 >
Workspace names and icons, and a component kit for every control465 <Avatar name={user.username} image={user.avatar} size={24} />
Model providers: gateway tokens for endpoints, tidier rows, and the docs466 <ChevronDown size={14} className="text-faint" />
467 </DropdownMenuTrigger>
468 <DropdownMenuContent align="end" className="w-56">
469 <DropdownMenuLabel>
470 Signed in as{" "}
471 <span className="font-mono font-medium text-fg">
472 {user.username}
473 </span>
474 </DropdownMenuLabel>
475 <DropdownMenuSeparator />
476 {(user.workspaces ?? []).map((membership) => (
477 <DropdownMenuItem asChild key={membership.slug}>
478 <Link to={`/${membership.slug}`}>
Workspace names and icons, and a component kit for every control479 <Avatar name={membership.slug} image={membership.avatar} size={16} square />
480 <span className="min-w-0 truncate">{membership.name || membership.slug}</span>
Model providers: gateway tokens for endpoints, tidier rows, and the docs481 </Link>
482 </DropdownMenuItem>
483 ))}
484 <DropdownMenuItem asChild>
485 <Link to="/workspaces/new">
486 <Plus />
487 New workspace
488 </Link>
489 </DropdownMenuItem>
490 <DropdownMenuSeparator />
491 <DropdownMenuItem asChild>
492 <Link to="/">
493 <LayoutDashboard />
494 Mission control
495 </Link>
496 </DropdownMenuItem>
497 <DropdownMenuItem asChild>
498 <Link to="/new">
499 <Plus />
Projects live: fixes from walking them end to end500 New project
Model providers: gateway tokens for endpoints, tidier rows, and the docs501 </Link>
502 </DropdownMenuItem>
503 <DropdownMenuSeparator />
504 <DropdownMenuItem asChild>
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas505 <Link to="/settings/profile">
Model providers: gateway tokens for endpoints, tidier rows, and the docs506 <Settings />
507 Settings
508 </Link>
509 </DropdownMenuItem>
510 <DropdownMenuItem asChild>
511 <Link to="https://docs.g1t.sh/quickstart/">
512 <BookOpen />
513 Documentation
514 </Link>
515 </DropdownMenuItem>
516 <DropdownMenuSeparator />
517 {/* Submitted from here: the menu closes on select, and a button
518 that has left the page cannot submit a form. */}
519 <DropdownMenuItem
520 onSelect={() => submit(null, { method: "post", action: "/logout" })}
521 >
522 <LogOut />
523 Sign out
524 </DropdownMenuItem>
525 </DropdownMenuContent>
526 </DropdownMenu>
527 </>
528 ) : (
529 <>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look530 <span className="hidden sm:contents">
531 <HeaderLink to="/login">Sign in</HeaderLink>
532 </span>
533 <SignUpButton />
Model providers: gateway tokens for endpoints, tidier rows, and the docs534 </>
535 )}
536 </div>
537 </div>
538 </header>
539 );
540}
541
Usage, Billing settings and prepaid AI credit; fixes from the UX audit542/**
543 * The root's data as the browser last had it. When a navigation fails in a
544 * way that takes the root's data with it, the error is still drawn in the
545 * person's own frame, signed in, not the visitor's. Never kept on the
546 * server, where one module serves everyone.
547 */
548let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
549
Model providers: gateway tokens for endpoints, tidier rows, and the docs550export 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 address551 const nonce = useNonce();
Model providers: gateway tokens for endpoints, tidier rows, and the docs552 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit553 const loaded = useRouteLoaderData<typeof loader>("root");
554 const inBrowser = typeof document !== "undefined";
555 if (loaded && inBrowser) lastRoot = loaded;
556 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Model providers: gateway tokens for endpoints, tidier rows, and the docs557 const user = root?.user;
A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works.558 // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts).
559 if (inBrowser) setLiveViaToken(Boolean(user?.token?.website));
Merge branch 'worktree-agent-a985c01f43538af01'560 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas561 // Drawn around the error page too: a 404 keeps the sidebar out of a
562 // project or workspace the viewer cannot see.
563 const error = useRouteError();
564 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look565 // The billing page shows the full banner itself.
566 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents567 <SpikeBanner
568 compact
569 slug={root.shell.workspace.slug}
570 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
571 owner={root.shell.compute.owner}
572 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look573 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)574 // On the few pages an account still confirming its address can open
575 // besides /confirm-email (policies, support, signing in): the way back.
576 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
577 <p
578 role="status"
Model providers: gateway tokens for endpoints, tidier rows, and the docs579 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"
580 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)581 <span>Confirm your email address to start using g1t.</span>
582 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
583 Enter your code
584 </Link>
585 </p>
Model providers: gateway tokens for endpoints, tidier rows, and the docs586 );
Merge main (membership, two-factor, GitHub repo roles) into tokens587 // A workspace that requires two-factor authentication of someone who
588 // has not turned it on: they keep their place, and cannot use it yet.
589 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'590 const leaving = useLeaving();
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look591 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens592 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look593 <>
594 {paused}
595 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens596 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look597 </>
598 ) : null;
Model providers: gateway tokens for endpoints, tidier rows, and the docs599 return (
600 <html lang="en">
601 <head>
602 <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)603 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Model providers: gateway tokens for endpoints, tidier rows, and the docs604 <meta name="theme-color" content="#0f0f11" />
Merge branch 'mirroring' into artifacts-mode605 <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)606 <meta name="apple-mobile-web-app-capable" content="yes" />
607 <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 preloads608 {/* The stylesheet before everything React Router preloads, so a slow
609 connection paints sooner (docs/research/css-shipping.md). */}
Give the stylesheet a precedence so React places it before preloads610 <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 today611 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Model providers: gateway tokens for endpoints, tidier rows, and the docs612 <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 address613 <Links nonce={nonce} />
Model providers: gateway tokens for endpoints, tidier rows, and the docs614 </head>
615 <body className="flex min-h-screen flex-col">
Usage, Billing settings and prepaid AI credit; fixes from the UX audit616 {/* The first stop for the keyboard: past the menus, to the page. */}
617 <a
618 href="#content"
619 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]"
620 >
621 Skip to content
622 </a>
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)623 {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 quotas624 <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}>
Model providers: gateway tokens for endpoints, tidier rows, and the docs625 {children}
626 </AppShell>
627 ) : (
628 <>
629 <Progress />
630 <Header user={user} />
631 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit632 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'633 {children}
634 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look635 <SiteFooter user={user} />
Model providers: gateway tokens for endpoints, tidier rows, and the docs636 </>
637 )}
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)638 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
639 {user && !awaitsConfirmation(user) && (
640 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
641 )}
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 today642 <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 address643 <ScrollRestoration nonce={nonce} />
644 <Scripts nonce={nonce} />
Model providers: gateway tokens for endpoints, tidier rows, and the docs645 </body>
646 </html>
647 );
648}
649
650export 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 today651 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
652 // Tell analytics who is signed in, once per change; signing out forgets them.
653 const lastUserId = useRef<string | null>(null);
654 useEffect(() => {
655 identify(userId, lastUserId.current);
656 lastUserId.current = userId;
657 }, [userId]);
Model providers: gateway tokens for endpoints, tidier rows, and the docs658 return <Outlet />;
659}
660
661export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit662 const location = useLocation();
663 const matches = useMatches();
664 // A tab left open across a deploy: load the address again as a whole
665 // page, once, rather than show an error the reload fixes.
666 const reload = reloadFixes({
667 error,
668 status: isRouteErrorResponse(error) ? error.status : undefined,
669 // The first page a tab loads has the key "default"; a client navigation's is its own.
670 clientNavigation: location.key !== "default",
671 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
672 });
673 const href = location.pathname + location.search + location.hash;
674 useEffect(() => {
675 if (!reload) return;
676 try {
677 if (sessionStorage.getItem(RELOADED_KEY) === href) return;
678 sessionStorage.setItem(RELOADED_KEY, href);
679 } catch {
680 // No session storage: reload anyway; a document load never asks again.
681 }
682 window.location.assign(href);
683 }, [reload, href]);
684 if (reload) {
685 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in686 <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 audit687 <title>Loading · g1t</title>
688 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 in689 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit690 );
691 }
692
Model providers: gateway tokens for endpoints, tidier rows, and the docs693 let title = "Something went wrong";
694 let details = "An unexpected error occurred. Try again in a moment.";
695 let stack: string | undefined;
696
697 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put698 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit699 if (error.status === 404) {
700 return (
701 <>
702 <title>Not found · g1t</title>
703 <NotFound data={error.data} />
704 </>
705 );
706 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put707 if (typeof error.data === "string" && error.data) {
Model providers: gateway tokens for endpoints, tidier rows, and the docs708 details = error.data;
709 }
Merge main into Artifacts Phase 2710 // A token asked for what needs a real sign-in (lib/website-token.ts).
711 if (isNeedsSignIn(error.data)) {
712 title = "This needs you to sign in";
713 details = error.data.message;
714 }
Model providers: gateway tokens for endpoints, tidier rows, and the docs715 } else if (import.meta.env.DEV && error instanceof Error) {
716 details = error.message;
717 stack = error.stack;
718 }
719
720 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in721 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Model providers: gateway tokens for endpoints, tidier rows, and the docs722 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
723 <p className="mt-3 text-muted">{details}</p>
724 <div className="mt-8">
725 <ButtonLink to="/" variant="quiet">
726 Back to home
727 </ButtonLink>
728 </div>
729 {stack && (
730 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
731 <code>{stack}</code>
732 </pre>
733 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in734 </PageMain>
Model providers: gateway tokens for endpoints, tidier rows, and the docs735 );
736}

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