Skip to content
543 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";
Docs: usage is shown at cost while g1t is free2import {
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,
Docs: usage is shown at cost while g1t is free14 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";
Docs: usage is shown at cost while g1t is free18
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";
Merge branch 'worktree-agent-aca4790ea223aa235'24import { AppShell, Progress, type ShellData, useLeaving } from "./components/shell";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25import { 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.26import { PublicHeader } from "./components/public-header";
Fast pages, required checks on the branch, self-hosted runners, honest incidents27import { SpikeBanner } from "./components/spike-banner";
Merge main (membership, two-factor, GitHub repo roles) into tokens28import { 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 today29import { identify } from "./lib/analytics.client";
30import { visitorAsksFirst } from "./lib/analytics-consent";
31import { AnalyticsConsent } from "./components/analytics-consent";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put32import { readCookie } from "./lib/mission";
33import { 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 in34import { PageMain } from "./components/landmark";
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put35import { 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.36import { 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.37import { DOCK_COOKIE, SIDEBAR_COOKIE, pinsToShow, sidebarClosed } from "./lib/apps";
38import { 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.39import { 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.40import { billing, chat, identity, inbox, projects, workspaceAgents } from "./lib/services.server";
41import { teamsByAgent } from "./lib/people";
Chat and workspace agents: channels, DMs and named agents you talk to42import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents43import { countsFor, readableRepos } from "./lib/access.server";
44import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look45import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar46import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look47import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'48import { addresses } from "./lib/addresses.server";
Merge branch 'worktree-agent-a64e51c8f386f46ac'49import { 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 address50import { useNonce } from "./lib/nonce";
Merge main into Artifacts Phase 251import { isNeedsSignIn } from "./lib/website-token";
Merge branch 'socket-tickets'52import { 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)53import { 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.54import { 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 look55
Docs: usage is shown at cost while g1t is free56
57export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark58 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Docs: usage is shown at cost while g1t is free59 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark60 { 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 managers61 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
62 // Home screens and password managers look for a square, opaque 180px tile;
63 // the "-precomposed" name is served too, for the ones that ask for it.
64 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
65 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
66 // manifest.webmanifest is the same file, kept for installs that already point at it).
67 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look68 // The text and headline faces are wanted on every page, so they start
69 // loading with the stylesheet; mono waits until something uses it.
70 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
71 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Docs: usage is shown at cost while g1t is free72];
73
74export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
75
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put76export async function loader({ context, params, request }: Route.LoaderArgs) {
Docs: usage is shown at cost while g1t is free77 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.78 const cookies = request.headers.get("cookie");
79 const chosen = readCookie(cookies, WORKSPACE_COOKIE);
80 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)81 // Whether sign-up takes an invite: the sign-up page says so, and
82 // 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 look83 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)84 // An account still confirming its address sees only the pages that
85 // allows (lib/confirm-gate.ts), in the visitor's frame.
86 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit87 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
88 // A sidebar that could not be read is no reason to lose the page,
89 // or to draw it as if they were signed out.
90 console.error("root: the sidebar could not be read", error);
91 return bareShell(user, params, chosen);
92 })
93 : 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)94 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look95 ]);
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.96 // The apps this person pinned to their dock here: as their account keeps
97 // them (read with the rest of the shell), else as this device's cookie
98 // remembers them (lib/apps.ts).
99 if (shell.workspace) shell.pins = pinsToShow(shell.pins, dock, shell.workspace.slug);
Merge branch 'worktree-agent-aaf03bdceac799c89'100 // 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 today101 return {
102 user,
103 shell,
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.104 // Whether they folded the sidebar away, so the page is drawn that way from the start.
105 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.106 // Their Appearance, so the page is drawn in it from the first byte (lib/theme.ts).
107 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 today108 inviteOnly: mode !== "open",
109 addresses: addresses(),
110 // Visitors from where the law asks first are asked before analytics runs.
111 analyticsConsent: visitorAsksFirst(request),
112 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put113}
114
115/**
116 * The sidebar for someone not signed in: nothing of their own, only the
117 * open counts of the project being looked at. One call, and only in a
118 * project; it answers the same for a private project as a missing one.
119 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents120async 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 put121 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents122 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 put123 return {
124 workspace: null,
125 repos: [],
126 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
127 limit: null,
128 monthUsageMicros: null,
129 };
Docs: usage is shown at cost while g1t is free130}
131
Usage, Billing settings and prepaid AI credit; fixes from the UX audit132/** The sidebar from what the session already says, with no service asked. */
133function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
134 return {
135 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
136 repos: [],
137 repo: null,
138 limit: null,
139 monthUsageMicros: null,
140 };
141}
142
Docs: usage is shown at cost while g1t is free143/**
144 * The sidebar only changes with the workspace or repository being looked
145 * at, or after something was submitted: not on every page within them.
146 */
147export function shouldRevalidate({
148 currentParams,
149 nextParams,
150 formMethod,
151 defaultShouldRevalidate,
152}: ShouldRevalidateFunctionArgs) {
153 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
154 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
155}
156
157/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit158 * The sidebar: the workspace whose pages or project these are when it is
159 * one of yours, else the one you chose (lib/workspace-choice.ts); its
160 * projects; and the repository being looked at.
Docs: usage is shown at cost while g1t is free161 */
162async function shellFor(
163 user: User,
164 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put165 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents166 context: Route.LoaderArgs["context"],
Docs: usage is shown at cost while g1t is free167): Promise<ShellData> {
168 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put169 const workspace = workspaceFor(memberships, chosen, params);
Docs: usage is shown at cost while g1t is free170 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
171 const now = new Date();
172 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents173 // What the sidebar shows of the workspace changes rarely: kept for a few
174 // seconds per person and workspace, except just after they changed
175 // something (lib/cache.server.ts).
176 const kept = <T,>(what: string, load: () => Promise<T>) =>
177 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.178 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 sidebar179 // Every project, for the palette and the count; the sidebar lists only
180 // the person's pinned and recent ones (lib/pins.ts).
181 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents182 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 processor183 // Whether billing is on, without reading the account: that asks the
184 // card processor about the workspace's cards, too slow for every page.
185 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit186 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents187 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
188 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
189 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all190 // The bell's count: one query, never kept, so marking an item shows at once.
191 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar192 // Never kept: opening a project moves it up Recent.
193 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to194 // The rail's Chat badge: never kept, and never waited on for long.
195 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)196 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
197 workspace ? agentsFor(workspace.slug, user) : 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.198 // The dock's pins, kept with the account so every device shows the same
199 // dock; never kept here, so a pin shows the moment it is made. Null
200 // falls back to the cookie (lib/dock.server.ts).
201 workspace ? savedPins(user, workspace.slug) : Promise.resolve(null),
Docs: usage is shown at cost while g1t is free202 ]);
203 return {
204 workspace,
Projects: what a workspace builds and runs, first on every page205 // 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 sidebar206 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
207 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
208 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Docs: usage is shown at cost while g1t is free209 repo:
210 path && counts?.ok
211 ? {
212 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look213 // Until the repository's own page says what their role is.
214 member: hasAccessIn(user, path.namespace),
Docs: usage is shown at cost while g1t is free215 issues: counts.value.issues,
216 pulls: counts.value.pulls,
217 }
218 : null,
Billing accounts, terms and enterprises; g1t is no longer free219 // Where the workspace stands against its usage limit, once billing is on.
220 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor221 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free222 ? {
223 exposureMicros: limit.value.exposureMicros,
224 ceilingMicros: limit.value.ceilingMicros,
225 state: limit.value.state,
226 comped: limit.value.trust === "internal",
227 }
228 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor229 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look230 // A spend spike or a hold pauses new compute: the shell says so on every page.
231 compute:
232 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
233 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
234 : null,
235 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all236 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to237 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)238 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.239 pins: dockPins ?? undefined,
Docs: usage is shown at cost while g1t is free240 // 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 audit241 // Usage at price, the one figure every page shows.
242 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Docs: usage is shown at cost while g1t is free243 };
244}
245
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/** Starred and recent conversations Home's sidebar lists. */
247const SHELL_CONVERSATIONS = 8;
248
249/**
250 * What is unread in chat, for the rail, and the starred and latest
251 * conversations, for Home's sidebar; null when chat is slow to answer or
252 * does not.
253 */
Chat and workspace agents: channels, DMs and named agents you talk to254async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
255 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
256 const read = chat
257 .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)258 .then((result) => {
259 if (!result.ok) return null;
260 const entries = result.value.entries;
261 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
262 return {
263 ...unreadTotals(entries),
264 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
265 recent: latest.slice(0, SHELL_CONVERSATIONS),
266 };
267 })
268 .catch(() => null);
269 return Promise.race([read, timeout]);
270}
271
272/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
273async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
274 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.275 // The workspace's agents and the person's own personal ones, for "Yours",
276 // with the teams each is on, for the Agents sidebar's groups.
277 const read = Promise.all([workspaceAgents.list(slug, user, { personal: "mine" }), identity.teamAgentIndex(slug).catch(() => null)])
278 .then(([result, index]) => {
279 if (!result.ok) return null;
280 const teams = index ? teamsByAgent(index) : null;
281 return result.value
282 .filter((agent) => !agent.archived_at)
283 .map((agent) => ({
284 id: agent.id,
285 handle: agent.handle,
286 display_name: agent.display_name,
287 avatar: agent.avatar,
288 avatar_seed: agent.avatar_seed,
289 role: agent.role,
290 title: agent.title,
291 status: agent.status,
292 builtin: agent.builtin === true,
293 scope: agent.scope,
294 ...(teams ? { teams: teams[agent.id] ?? [] } : {}),
295 }));
296 })
Chat and workspace agents: channels, DMs and named agents you talk to297 .catch(() => null);
298 return Promise.race([read, timeout]);
299}
300
301/** The longest the rail's Chat badge holds up a page. */
302const CHAT_BADGE_WAIT_MS = 300;
303
Usage, Billing settings and prepaid AI credit; fixes from the UX audit304/**
305 * How long the sidebar's workspace answers are kept: as long as someone's
306 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
307 */
308const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents309
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look310/** Most repositories listed under Shared with you. */
311const MAX_SHARED = 20;
312
313/**
314 * Repositories shared with the user in workspaces they do not belong to,
315 * by their grants: only those, never the workspace's other repositories.
316 */
317async function sharedRepos(user: User): Promise<ShellData["shared"]> {
318 const outside = new Set(sharedWorkspaces(user));
319 if (outside.size === 0) return [];
320 const ids = (user.grants ?? []).filter((grant) => outside.has(grant.workspace)).map((grant) => grant.repo_id).slice(0, MAX_SHARED);
Fast pages, required checks on the branch, self-hosted runners, honest incidents321 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents323 .map((repo) => ({ namespace: repo.namespace, name: repo.name, isPrivate: repo.isPrivate }))
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look324 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
325}
326
Usage, Billing settings and prepaid AI credit; fixes from the UX audit327/**
328 * The root's data as the browser last had it. When a navigation fails in a
329 * way that takes the root's data with it, the error is still drawn in the
330 * person's own frame, signed in, not the visitor's. Never kept on the
331 * server, where one module serves everyone.
332 */
333let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
334
Docs: usage is shown at cost while g1t is free335export 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 address336 const nonce = useNonce();
Docs: usage is shown at cost while g1t is free337 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit338 const loaded = useRouteLoaderData<typeof loader>("root");
339 const inBrowser = typeof document !== "undefined";
340 if (loaded && inBrowser) lastRoot = loaded;
341 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Docs: usage is shown at cost while g1t is free342 const user = root?.user;
Merge branch 'socket-tickets'343 // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts).
344 if (inBrowser) setLiveViaToken(Boolean(user?.token?.website));
Merge branch 'worktree-agent-a985c01f43538af01'345 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas346 // Drawn around the error page too: a 404 keeps the sidebar out of a
347 // project or workspace the viewer cannot see.
348 const error = useRouteError();
349 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look350 // The billing page shows the full banner itself.
351 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents352 <SpikeBanner
353 compact
354 slug={root.shell.workspace.slug}
355 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
356 owner={root.shell.compute.owner}
357 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look358 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)359 // On the few pages an account still confirming its address can open
360 // besides /confirm-email (policies, support, signing in): the way back.
361 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
362 <p
363 role="status"
Docs: usage is shown at cost while g1t is free364 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"
365 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)366 <span>Confirm your email address to start using g1t.</span>
367 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
368 Enter your code
369 </Link>
370 </p>
Docs: usage is shown at cost while g1t is free371 );
Merge main (membership, two-factor, GitHub repo roles) into tokens372 // A workspace that requires two-factor authentication of someone who
373 // has not turned it on: they keep their place, and cannot use it yet.
374 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'375 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.376 // Auto, Light or Dark: as the cookie said when the page was drawn, or as just switched.
377 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.378 // Which frame the page is drawn in (lib/chrome.ts): on its own, the app's, or the public one.
379 const frame = frameOf(pathname, {
380 signedIn: user != null && !awaitsConfirmation(user),
381 workspace: (user?.workspaces ?? []).length > 0,
382 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look383 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens384 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look385 <>
386 {paused}
387 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens388 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look389 </>
390 ) : null;
Docs: usage is shown at cost while g1t is free391 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.392 <html lang="en" data-theme={themeAttribute(theme)}>
Docs: usage is shown at cost while g1t is free393 <head>
394 <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)395 <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.396 {/* The browser bar matches the page: one colour for a chosen theme, the system's for Auto. */}
397 {theme === "auto" ? (
398 <>
399 <meta name="theme-color" media="(prefers-color-scheme: light)" content={THEME_COLOR.light} />
400 <meta name="theme-color" media="(prefers-color-scheme: dark)" content={THEME_COLOR.dark} />
401 </>
402 ) : (
403 <meta name="theme-color" content={THEME_COLOR[theme]} />
404 )}
Merge branch 'mirroring' into artifacts-mode405 <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)406 <meta name="apple-mobile-web-app-capable" content="yes" />
407 <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 preloads408 {/* 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.409 connection paints sooner. */}
Give the stylesheet a precedence so React places it before preloads410 <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 today411 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Docs: usage is shown at cost while g1t is free412 <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 address413 <Links nonce={nonce} />
Docs: usage is shown at cost while g1t is free414 </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.415 <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 audit416 {/* The first stop for the keyboard: past the menus, to the page. */}
417 <a
418 href="#content"
419 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]"
420 >
421 Skip to content
422 </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.423 {frame === "standalone" ? (
424 <StandaloneFrame>{children}</StandaloneFrame>
425 ) : frame === "app" && user && root?.shell ? (
426 <AppShell user={user} shell={root.shell} missing={missing} banner={banner} sidebarClosed={root.sidebarClosed}>
Docs: usage is shown at cost while g1t is free427 {children}
428 </AppShell>
429 ) : (
430 <>
431 <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.432 <PublicHeader user={user} />
Docs: usage is shown at cost while g1t is free433 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit434 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'435 {children}
436 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look437 <SiteFooter user={user} />
Docs: usage is shown at cost while g1t is free438 </>
439 )}
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)440 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
441 {user && !awaitsConfirmation(user) && (
442 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
443 )}
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 today444 <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 address445 <ScrollRestoration nonce={nonce} />
446 <Scripts nonce={nonce} />
Docs: usage is shown at cost while g1t is free447 </body>
448 </html>
449 );
450}
451
452export 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 today453 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
454 // Tell analytics who is signed in, once per change; signing out forgets them.
455 const lastUserId = useRef<string | null>(null);
456 useEffect(() => {
457 identify(userId, lastUserId.current);
458 lastUserId.current = userId;
459 }, [userId]);
Docs: usage is shown at cost while g1t is free460 return <Outlet />;
461}
462
463export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit464 const location = useLocation();
465 const matches = useMatches();
Merge branch 'worktree-agent-a64e51c8f386f46ac'466 const href = location.pathname + location.search + location.hash;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit467 // A tab left open across a deploy: load the address again as a whole
Merge branch 'worktree-agent-a64e51c8f386f46ac'468 // page, once, rather than show an error the reload fixes. A document load
469 // never reloads, so the server and the hydrating client render the same.
470 const reloadable = reloadFixes({
Usage, Billing settings and prepaid AI credit; fixes from the UX audit471 error,
472 status: isRouteErrorResponse(error) ? error.status : undefined,
Merge branch 'worktree-agent-a64e51c8f386f46ac'473 clientNavigation: clientNavigated(location.key),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit474 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
475 });
Merge branch 'worktree-agent-a64e51c8f386f46ac'476 // Already loaded again once, or the reload never started: show the page.
477 const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]);
478 const [gaveUp, setGaveUp] = useState<string | null>(null);
479 const reload = reloadable && !tried && gaveUp !== href;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit480 useEffect(() => {
481 if (!reload) return;
482 try {
483 sessionStorage.setItem(RELOADED_KEY, href);
484 } catch {
485 // No session storage: reload anyway; a document load never asks again.
486 }
487 window.location.assign(href);
Merge branch 'worktree-agent-a64e51c8f386f46ac'488 const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
489 return () => window.clearTimeout(timer);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit490 }, [reload, href]);
491 if (reload) {
492 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in493 <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 audit494 <title>Loading · g1t</title>
495 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 in496 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit497 );
498 }
499
Docs: usage is shown at cost while g1t is free500 let title = "Something went wrong";
501 let details = "An unexpected error occurred. Try again in a moment.";
502 let stack: string | undefined;
503
504 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put505 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit506 if (error.status === 404) {
507 return (
508 <>
509 <title>Not found · g1t</title>
510 <NotFound data={error.data} />
511 </>
512 );
513 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put514 if (typeof error.data === "string" && error.data) {
Docs: usage is shown at cost while g1t is free515 details = error.data;
516 }
Merge main into Artifacts Phase 2517 // A token asked for what needs a real sign-in (lib/website-token.ts).
518 if (isNeedsSignIn(error.data)) {
519 title = "This needs you to sign in";
520 details = error.data.message;
521 }
Docs: usage is shown at cost while g1t is free522 } else if (import.meta.env.DEV && error instanceof Error) {
523 details = error.message;
524 stack = error.stack;
525 }
526
527 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in528 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Docs: usage is shown at cost while g1t is free529 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
530 <p className="mt-3 text-muted">{details}</p>
531 <div className="mt-8">
532 <ButtonLink to="/" variant="quiet">
533 Back to home
534 </ButtonLink>
535 </div>
536 {stack && (
537 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
538 <code>{stack}</code>
539 </pre>
540 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in541 </PageMain>
Docs: usage is shown at cost while g1t is free542 );
543}

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