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

Merge branch 'worktree-agent-a64e51c8f386f46ac'1import { useEffect, useMemo, useRef, useState } from "react";
Paid features: a workspace turns on Deployments with a monthly plan2import {
3 isRouteErrorResponse,
4 Link,
5 Links,
6 Meta,
7 Outlet,
8 Scripts,
9 ScrollRestoration,
10 type ShouldRevalidateFunctionArgs,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put11 useLocation,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit12 useMatches,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas13 useRouteError,
Paid features: a workspace turns on Deployments with a monthly plan14 useRouteLoaderData,
15} from "react-router";
16
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)17import { type User, awaitsConfirmation, hasAccessIn, sharedWorkspaces } from "@g1t/contracts";
Paid features: a workspace turns on Deployments with a monthly plan18
19import type { Route } from "./+types/root";
Load the stylesheet first; plain legal contact details20import appCss from "./app.css?url";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look21import displayFont from "@g1t/theme/fonts/bricolage-grotesque-latin.woff2?url";
22import sansFont from "@g1t/theme/fonts/hanken-grotesk-latin.woff2?url";
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.23import { ButtonLink } from "./components/ui";
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.24import { Card } from "./components/ui/card";
Merge branch 'worktree-agent-aca4790ea223aa235'25import { AppShell, Progress, type ShellData, useLeaving } from "./components/shell";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look26import { SiteFooter } from "./components/footer";
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.27import { PublicHeader } from "./components/public-header";
Fast pages, required checks on the branch, self-hosted runners, honest incidents28import { SpikeBanner } from "./components/spike-banner";
Merge main (membership, two-factor, GitHub repo roles) into tokens29import { 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 today30import { identify } from "./lib/analytics.client";
31import { visitorAsksFirst } from "./lib/analytics-consent";
32import { AnalyticsConsent } from "./components/analytics-consent";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put33import { readCookie } from "./lib/mission";
34import { 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 in35import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put36import { NotFound } from "./components/not-found";
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.37import { frameOf } from "./lib/chrome";
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.38import { DOCK_COOKIE, SIDEBAR_COOKIE, pinsToShow, sidebarClosed } from "./lib/apps";
39import { savedPins } from "./lib/dock.server";
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.40import { StandaloneFrame } from "./components/standalone";
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.41import { billing, chat, identity, inbox, projects, workspaceAgents } from "./lib/services.server";
42import { teamsByAgent } from "./lib/people";
Chat and workspace agents: channels, DMs and named agents you talk to43import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents44import { countsFor, readableRepos } from "./lib/access.server";
45import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar47import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look48import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'49import { addresses } from "./lib/addresses.server";
Merge branch 'worktree-agent-a64e51c8f386f46ac'50import { RELOADED_KEY, RELOAD_GIVE_UP_MS, clientNavigated, reloadFixes, reloadedBefore } 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 address51import { useNonce } from "./lib/nonce";
Merge main into Artifacts Phase 252import { isNeedsSignIn } from "./lib/website-token";
A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers.53import { offerTicket, setLiveViaToken } from "./lib/live-socket";
54import { socketTicketFor } from "./lib/socket-ticket.server";
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)55import { LiveNotifications } from "./components/notifications/live-notifications";
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.56import { THEME_COLOR, THEME_COOKIE, readTheme, themeAttribute, useThemeChoice } from "./lib/theme";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look57
Paid features: a workspace turns on Deployments with a monthly plan58
59export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark60 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Paid features: a workspace turns on Deployments with a monthly plan61 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark62 { 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 managers63 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
64 // Home screens and password managers look for a square, opaque 180px tile;
65 // the "-precomposed" name is served too, for the ones that ask for it.
66 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
67 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
68 // manifest.webmanifest is the same file, kept for installs that already point at it).
69 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look70 // The text and headline faces are wanted on every page, so they start
71 // loading with the stylesheet; mono waits until something uses it.
72 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
73 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Paid features: a workspace turns on Deployments with a monthly plan74];
75
76export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
77
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put78export async function loader({ context, params, request }: Route.LoaderArgs) {
Paid features: a workspace turns on Deployments with a monthly plan79 const user = getViewer(context);
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.80 const cookies = request.headers.get("cookie");
81 const chosen = readCookie(cookies, WORKSPACE_COOKIE);
82 const dock = readCookie(cookies, DOCK_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)83 // Whether sign-up takes an invite: the sign-up page says so, and
84 // 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 look85 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)86 // An account still confirming its address sees only the pages that
87 // allows (lib/confirm-gate.ts), in the visitor's frame.
88 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit89 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
90 // A sidebar that could not be read is no reason to lose the page,
91 // or to draw it as if they were signed out.
92 console.error("root: the sidebar could not be read", error);
93 return bareShell(user, params, chosen);
94 })
95 : 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)96 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look97 ]);
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.98 // The apps this person pinned to their rail here: as their account keeps
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.99 // them (read with the rest of the shell), else as this device's cookie
100 // remembers them (lib/apps.ts).
101 if (shell.workspace) shell.pins = pinsToShow(shell.pins, dock, shell.workspace.slug);
Merge branch 'worktree-agent-aaf03bdceac799c89'102 // 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 today103 return {
104 user,
105 shell,
A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers.106 // For a page opened with an access token: the feed socket's ticket,
107 // minted here so the feed opens without asking for one (lib/live-socket.ts).
108 liveTicket: user?.token?.website ? await socketTicketFor(context, request, "/-/live") : null,
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.109 // Whether they folded the sidebar away, so the page is drawn that way from the start.
110 sidebarClosed: sidebarClosed(readCookie(cookies, SIDEBAR_COOKIE)),
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.111 // Their Appearance, so the page is drawn in it from the first byte (lib/theme.ts).
112 theme: readTheme(readCookie(cookies, THEME_COOKIE)),
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 today113 inviteOnly: mode !== "open",
114 addresses: addresses(),
115 // Visitors from where the law asks first are asked before analytics runs.
116 analyticsConsent: visitorAsksFirst(request),
117 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put118}
119
120/**
121 * The sidebar for someone not signed in: nothing of their own, only the
122 * open counts of the project being looked at. One call, and only in a
123 * project; it answers the same for a private project as a missing one.
124 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents125async 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 put126 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents127 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 put128 return {
129 workspace: null,
130 repos: [],
131 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
132 limit: null,
133 monthUsageMicros: null,
134 };
Paid features: a workspace turns on Deployments with a monthly plan135}
136
Usage, Billing settings and prepaid AI credit; fixes from the UX audit137/** The sidebar from what the session already says, with no service asked. */
138function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
139 return {
140 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
141 repos: [],
142 repo: null,
143 limit: null,
144 monthUsageMicros: null,
145 };
146}
147
Paid features: a workspace turns on Deployments with a monthly plan148/**
149 * The sidebar only changes with the workspace or repository being looked
150 * at, or after something was submitted: not on every page within them.
151 */
152export function shouldRevalidate({
153 currentParams,
154 nextParams,
155 formMethod,
156 defaultShouldRevalidate,
157}: ShouldRevalidateFunctionArgs) {
158 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
159 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
160}
161
162/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit163 * The sidebar: the workspace whose pages or project these are when it is
164 * one of yours, else the one you chose (lib/workspace-choice.ts); its
165 * projects; and the repository being looked at.
Paid features: a workspace turns on Deployments with a monthly plan166 */
167async function shellFor(
168 user: User,
169 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put170 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents171 context: Route.LoaderArgs["context"],
Paid features: a workspace turns on Deployments with a monthly plan172): Promise<ShellData> {
173 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put174 const workspace = workspaceFor(memberships, chosen, params);
Paid features: a workspace turns on Deployments with a monthly plan175 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
176 const now = new Date();
177 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents178 // What the sidebar shows of the workspace changes rarely: kept for a few
179 // seconds per person and workspace, except just after they changed
180 // something (lib/cache.server.ts).
181 const kept = <T,>(what: string, load: () => Promise<T>) =>
182 workspace ? shortCache(`shell:${what}:${user.id}:${workspace.slug}`, SHELL_TTL_MS, load) : Promise.resolve(null);
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.183 const [listed, counts, status, usage, limit, entitlements, shared, unread, shortcuts, chatUnread, teamAgents, dockPins] = await Promise.all([
Navigation: the workspace's own page with tabs, pins and recent in the sidebar184 // Every project, for the palette and the count; the sidebar lists only
185 // the person's pinned and recent ones (lib/pins.ts).
186 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents187 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 processor188 // Whether billing is on, without reading the account: that asks the
189 // card processor about the workspace's cards, too slow for every page.
190 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit191 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents192 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
193 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
194 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all195 // The bell's count: one query, never kept, so marking an item shows at once.
196 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar197 // Never kept: opening a project moves it up Recent.
198 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to199 // The rail's Chat badge: never kept, and never waited on for long.
200 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)201 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
202 workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null),
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.203 // The rail's pins, kept with the account so every device shows the same
204 // rail; never kept here, so a pin shows the moment it is made. Null
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.205 // falls back to the cookie (lib/dock.server.ts).
206 workspace ? savedPins(user, workspace.slug) : Promise.resolve(null),
Paid features: a workspace turns on Deployments with a monthly plan207 ]);
208 return {
209 workspace,
Projects: what a workspace builds and runs, first on every page210 // 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 sidebar211 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
212 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
213 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Paid features: a workspace turns on Deployments with a monthly plan214 repo:
215 path && counts?.ok
216 ? {
217 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look218 // Until the repository's own page says what their role is.
219 member: hasAccessIn(user, path.namespace),
Paid features: a workspace turns on Deployments with a monthly plan220 issues: counts.value.issues,
221 pulls: counts.value.pulls,
222 }
223 : null,
Billing accounts, terms and enterprises; g1t is no longer free224 // Where the workspace stands against its usage limit, once billing is on.
225 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor226 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free227 ? {
228 exposureMicros: limit.value.exposureMicros,
229 ceilingMicros: limit.value.ceilingMicros,
230 state: limit.value.state,
231 comped: limit.value.trust === "internal",
232 }
233 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor234 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look235 // A spend spike or a hold pauses new compute: the shell says so on every page.
236 compute:
237 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
238 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
239 : null,
240 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all241 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to242 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)243 agents: teamAgents,
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.244 pins: dockPins ?? undefined,
Paid features: a workspace turns on Deployments with a monthly plan245 // 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 audit246 // Usage at price, the one figure every page shows.
247 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Paid features: a workspace turns on Deployments with a monthly plan248 };
249}
250
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)251/** Starred and recent conversations Home's sidebar lists. */
252const SHELL_CONVERSATIONS = 8;
253
254/**
255 * What is unread in chat, for the rail, and the starred and latest
256 * conversations, for Home's sidebar; null when chat is slow to answer or
257 * does not.
258 */
Chat and workspace agents: channels, DMs and named agents you talk to259async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
260 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
261 const read = chat
262 .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)263 .then((result) => {
264 if (!result.ok) return null;
265 const entries = result.value.entries;
266 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
267 return {
268 ...unreadTotals(entries),
269 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
270 recent: latest.slice(0, SHELL_CONVERSATIONS),
271 };
272 })
273 .catch(() => null);
274 return Promise.race([read, timeout]);
275}
276
277/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
278async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
279 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
An agent is on teams the way a person is: through team membership, with no team or department of its own. Add it to teams when you create it, from its profile or from the team's page; the Agents sidebar groups agents by the teams they're on, with those on none last; what an agent is told about its teammates, team budgets, skills reach, Spend's By team and the People pages all read memberships. Agents with an old team name matching a team are moved onto it once; the rest are left off a team. Templates name a role only. The agents, teams and people guides say how.280 // The workspace's agents and the person's own personal ones, for "Yours",
281 // with the teams each is on, for the Agents sidebar's groups.
282 const read = Promise.all([workspaceAgents.list(slug, user, { personal: "mine" }), identity.teamAgentIndex(slug).catch(() => null)])
283 .then(([result, index]) => {
284 if (!result.ok) return null;
285 const teams = index ? teamsByAgent(index) : null;
286 return 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 status: agent.status,
297 builtin: agent.builtin === true,
298 scope: agent.scope,
299 ...(teams ? { teams: teams[agent.id] ?? [] } : {}),
300 }));
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
Usage, Billing settings and prepaid AI credit; fixes from the UX audit332/**
333 * The root's data as the browser last had it. When a navigation fails in a
334 * way that takes the root's data with it, the error is still drawn in the
335 * person's own frame, signed in, not the visitor's. Never kept on the
336 * server, where one module serves everyone.
337 */
338let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
339
Paid features: a workspace turns on Deployments with a monthly plan340export 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 address341 const nonce = useNonce();
Paid features: a workspace turns on Deployments with a monthly plan342 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit343 const loaded = useRouteLoaderData<typeof loader>("root");
344 const inBrowser = typeof document !== "undefined";
345 if (loaded && inBrowser) lastRoot = loaded;
346 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Paid features: a workspace turns on Deployments with a monthly plan347 const user = root?.user;
Merge branch 'socket-tickets'348 // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts).
A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers.349 if (inBrowser) {
350 setLiveViaToken(Boolean(user?.token?.website));
351 offerTicket("/-/live", root?.liveTicket);
352 }
Merge branch 'worktree-agent-a985c01f43538af01'353 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas354 // Drawn around the error page too: a 404 keeps the sidebar out of a
355 // project or workspace the viewer cannot see.
356 const error = useRouteError();
357 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look358 // The billing page shows the full banner itself.
359 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents360 <SpikeBanner
361 compact
362 slug={root.shell.workspace.slug}
363 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
364 owner={root.shell.compute.owner}
365 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look366 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)367 // On the few pages an account still confirming its address can open
368 // besides /confirm-email (policies, support, signing in): the way back.
369 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
370 <p
371 role="status"
Paid features: a workspace turns on Deployments with a monthly plan372 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"
373 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)374 <span>Confirm your email address to start using g1t.</span>
375 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
376 Enter your code
377 </Link>
378 </p>
Paid features: a workspace turns on Deployments with a monthly plan379 );
Merge main (membership, two-factor, GitHub repo roles) into tokens380 // A workspace that requires two-factor authentication of someone who
381 // has not turned it on: they keep their place, and cannot use it yet.
382 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'383 const leaving = useLeaving();
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.384 // Auto, Light or Dark: as the cookie said when the page was drawn, or as just switched.
385 const theme = useThemeChoice(root?.theme);
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.386 // Which frame the page is drawn in (lib/chrome.ts): on its own, the app's, or the public one.
387 const frame = frameOf(pathname, {
388 signedIn: user != null && !awaitsConfirmation(user),
389 workspace: (user?.workspaces ?? []).length > 0,
390 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look391 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens392 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look393 <>
394 {paused}
395 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens396 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look397 </>
398 ) : null;
Paid features: a workspace turns on Deployments with a monthly plan399 return (
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.400 <html lang="en" data-theme={themeAttribute(theme)}>
Paid features: a workspace turns on Deployments with a monthly plan401 <head>
402 <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)403 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.404 {/* The browser bar matches the page: one colour for a chosen theme, the system's for Auto. */}
405 {theme === "auto" ? (
406 <>
407 <meta name="theme-color" media="(prefers-color-scheme: light)" content={THEME_COLOR.light} />
408 <meta name="theme-color" media="(prefers-color-scheme: dark)" content={THEME_COLOR.dark} />
409 </>
410 ) : (
411 <meta name="theme-color" content={THEME_COLOR[theme]} />
412 )}
Merge branch 'mirroring' into artifacts-mode413 <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)414 <meta name="apple-mobile-web-app-capable" content="yes" />
415 <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 preloads416 {/* The stylesheet before everything React Router preloads, so a slow
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.417 connection paints sooner. */}
Give the stylesheet a precedence so React places it before preloads418 <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 today419 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Paid features: a workspace turns on Deployments with a monthly plan420 <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 address421 <Links nonce={nonce} />
Paid features: a workspace turns on Deployments with a monthly plan422 </head>
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.423 <body className="flex min-h-screen flex-col" data-frame={frame === "app" && user && root?.shell ? "app" : frame}>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit424 {/* The first stop for the keyboard: past the menus, to the page. */}
425 <a
426 href="#content"
427 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]"
428 >
429 Skip to content
430 </a>
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.431 {frame === "standalone" ? (
432 <StandaloneFrame>{children}</StandaloneFrame>
433 ) : frame === "app" && user && root?.shell ? (
434 <AppShell user={user} shell={root.shell} missing={missing} banner={banner} sidebarClosed={root.sidebarClosed}>
Paid features: a workspace turns on Deployments with a monthly plan435 {children}
436 </AppShell>
437 ) : (
438 <>
439 <Progress />
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.440 <PublicHeader user={user} />
Paid features: a workspace turns on Deployments with a monthly plan441 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit442 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'443 {children}
444 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look445 <SiteFooter user={user} />
Paid features: a workspace turns on Deployments with a monthly plan446 </>
447 )}
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)448 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
449 {user && !awaitsConfirmation(user) && (
450 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
451 )}
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 today452 <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 address453 <ScrollRestoration nonce={nonce} />
454 <Scripts nonce={nonce} />
Paid features: a workspace turns on Deployments with a monthly plan455 </body>
456 </html>
457 );
458}
459
460export 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 today461 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
462 // Tell analytics who is signed in, once per change; signing out forgets them.
463 const lastUserId = useRef<string | null>(null);
464 useEffect(() => {
465 identify(userId, lastUserId.current);
466 lastUserId.current = userId;
467 }, [userId]);
Paid features: a workspace turns on Deployments with a monthly plan468 return <Outlet />;
469}
470
471export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit472 const location = useLocation();
473 const matches = useMatches();
Merge branch 'worktree-agent-a64e51c8f386f46ac'474 const href = location.pathname + location.search + location.hash;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit475 // A tab left open across a deploy: load the address again as a whole
Merge branch 'worktree-agent-a64e51c8f386f46ac'476 // page, once, rather than show an error the reload fixes. A document load
477 // never reloads, so the server and the hydrating client render the same.
478 const reloadable = reloadFixes({
Usage, Billing settings and prepaid AI credit; fixes from the UX audit479 error,
480 status: isRouteErrorResponse(error) ? error.status : undefined,
Merge branch 'worktree-agent-a64e51c8f386f46ac'481 clientNavigation: clientNavigated(location.key),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit482 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
483 });
Merge branch 'worktree-agent-a64e51c8f386f46ac'484 // Already loaded again once, or the reload never started: show the page.
485 const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]);
486 const [gaveUp, setGaveUp] = useState<string | null>(null);
487 const reload = reloadable && !tried && gaveUp !== href;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit488 useEffect(() => {
489 if (!reload) return;
490 try {
491 sessionStorage.setItem(RELOADED_KEY, href);
492 } catch {
493 // No session storage: reload anyway; a document load never asks again.
494 }
495 window.location.assign(href);
Merge branch 'worktree-agent-a64e51c8f386f46ac'496 const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
497 return () => window.clearTimeout(timer);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit498 }, [reload, href]);
499 if (reload) {
500 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in501 <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 audit502 <title>Loading · g1t</title>
503 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 in504 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit505 );
506 }
507
Paid features: a workspace turns on Deployments with a monthly plan508 let title = "Something went wrong";
509 let details = "An unexpected error occurred. Try again in a moment.";
510 let stack: string | undefined;
511
512 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put513 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit514 if (error.status === 404) {
515 return (
516 <>
517 <title>Not found · g1t</title>
518 <NotFound data={error.data} />
519 </>
520 );
521 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put522 if (typeof error.data === "string" && error.data) {
Paid features: a workspace turns on Deployments with a monthly plan523 details = error.data;
524 }
Merge main into Artifacts Phase 2525 // A token asked for what needs a real sign-in (lib/website-token.ts).
526 if (isNeedsSignIn(error.data)) {
527 title = "This needs you to sign in";
528 details = error.data.message;
529 }
Paid features: a workspace turns on Deployments with a monthly plan530 } else if (import.meta.env.DEV && error instanceof Error) {
531 details = error.message;
532 stack = error.stack;
533 }
534
535 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in536 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Paid features: a workspace turns on Deployments with a monthly plan537 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
538 <p className="mt-3 text-muted">{details}</p>
539 <div className="mt-8">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.540 <ButtonLink to="/" variant="outline">
Paid features: a workspace turns on Deployments with a monthly plan541 Back to home
542 </ButtonLink>
543 </div>
544 {stack && (
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.545 <Card asChild tone="plain" radius="lg" className="mt-8 overflow-x-auto p-4 text-left text-xs">
546 <pre>
547 <code>{stack}</code>
548 </pre>
549 </Card>
Paid features: a workspace turns on Deployments with a monthly plan550 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in551 </PageMain>
Paid features: a workspace turns on Deployments with a monthly plan552 );
553}

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