Skip to content
528 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";
Webhooks: every event, to your own addresses, signed and retried2import {
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,
Webhooks: every event, to your own addresses, signed and retried14 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";
Webhooks: every event, to your own addresses, signed and retried18
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";
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)40import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to41import { unreadTotals } from "./lib/chat";
Fast pages, required checks on the branch, self-hosted runners, honest incidents42import { countsFor, readableRepos } from "./lib/access.server";
43import { shortCache } from "./lib/cache.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar45import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look46import { registrationMode } from "./lib/registration.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'47import { addresses } from "./lib/addresses.server";
Merge branch 'worktree-agent-a64e51c8f386f46ac'48import { 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 address49import { useNonce } from "./lib/nonce";
Merge main into Artifacts Phase 250import { isNeedsSignIn } from "./lib/website-token";
Merge branch 'socket-tickets'51import { 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)52import { LiveNotifications } from "./components/notifications/live-notifications";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53
Webhooks: every event, to your own addresses, signed and retried54
55export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark56 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Webhooks: every event, to your own addresses, signed and retried57 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark58 { 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 managers59 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
60 // Home screens and password managers look for a square, opaque 180px tile;
61 // the "-precomposed" name is served too, for the ones that ask for it.
62 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
63 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
64 // manifest.webmanifest is the same file, kept for installs that already point at it).
65 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look66 // The text and headline faces are wanted on every page, so they start
67 // loading with the stylesheet; mono waits until something uses it.
68 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
69 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Webhooks: every event, to your own addresses, signed and retried70];
71
72export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
73
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put74export async function loader({ context, params, request }: Route.LoaderArgs) {
Webhooks: every event, to your own addresses, signed and retried75 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.76 const cookies = request.headers.get("cookie");
77 const chosen = readCookie(cookies, WORKSPACE_COOKIE);
78 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)79 // Whether sign-up takes an invite: the sign-up page says so, and
80 // 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 look81 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)82 // An account still confirming its address sees only the pages that
83 // allows (lib/confirm-gate.ts), in the visitor's frame.
84 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit85 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
86 // A sidebar that could not be read is no reason to lose the page,
87 // or to draw it as if they were signed out.
88 console.error("root: the sidebar could not be read", error);
89 return bareShell(user, params, chosen);
90 })
91 : 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)92 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look93 ]);
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.94 // The apps this person pinned to their dock here: as their account keeps
95 // them (read with the rest of the shell), else as this device's cookie
96 // remembers them (lib/apps.ts).
97 if (shell.workspace) shell.pins = pinsToShow(shell.pins, dock, shell.workspace.slug);
Merge branch 'worktree-agent-aaf03bdceac799c89'98 // 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 today99 return {
100 user,
101 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.102 // Whether they folded the sidebar away, so the page is drawn that way from the start.
103 sidebarClosed: sidebarClosed(readCookie(cookies, SIDEBAR_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 today104 inviteOnly: mode !== "open",
105 addresses: addresses(),
106 // Visitors from where the law asks first are asked before analytics runs.
107 analyticsConsent: visitorAsksFirst(request),
108 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put109}
110
111/**
112 * The sidebar for someone not signed in: nothing of their own, only the
113 * open counts of the project being looked at. One call, and only in a
114 * project; it answers the same for a private project as a missing one.
115 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents116async 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 put117 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents118 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 put119 return {
120 workspace: null,
121 repos: [],
122 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
123 limit: null,
124 monthUsageMicros: null,
125 };
Webhooks: every event, to your own addresses, signed and retried126}
127
Usage, Billing settings and prepaid AI credit; fixes from the UX audit128/** The sidebar from what the session already says, with no service asked. */
129function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
130 return {
131 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
132 repos: [],
133 repo: null,
134 limit: null,
135 monthUsageMicros: null,
136 };
137}
138
Webhooks: every event, to your own addresses, signed and retried139/**
140 * The sidebar only changes with the workspace or repository being looked
141 * at, or after something was submitted: not on every page within them.
142 */
143export function shouldRevalidate({
144 currentParams,
145 nextParams,
146 formMethod,
147 defaultShouldRevalidate,
148}: ShouldRevalidateFunctionArgs) {
149 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
150 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
151}
152
153/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit154 * The sidebar: the workspace whose pages or project these are when it is
155 * one of yours, else the one you chose (lib/workspace-choice.ts); its
156 * projects; and the repository being looked at.
Webhooks: every event, to your own addresses, signed and retried157 */
158async function shellFor(
159 user: User,
160 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put161 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents162 context: Route.LoaderArgs["context"],
Webhooks: every event, to your own addresses, signed and retried163): Promise<ShellData> {
164 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put165 const workspace = workspaceFor(memberships, chosen, params);
Webhooks: every event, to your own addresses, signed and retried166 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
167 const now = new Date();
168 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents169 // What the sidebar shows of the workspace changes rarely: kept for a few
170 // seconds per person and workspace, except just after they changed
171 // something (lib/cache.server.ts).
172 const kept = <T,>(what: string, load: () => Promise<T>) =>
173 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.174 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 sidebar175 // Every project, for the palette and the count; the sidebar lists only
176 // the person's pinned and recent ones (lib/pins.ts).
177 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents178 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 processor179 // Whether billing is on, without reading the account: that asks the
180 // card processor about the workspace's cards, too slow for every page.
181 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit182 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents183 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
184 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
185 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all186 // The bell's count: one query, never kept, so marking an item shows at once.
187 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar188 // Never kept: opening a project moves it up Recent.
189 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to190 // The rail's Chat badge: never kept, and never waited on for long.
191 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)192 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
193 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.194 // The dock's pins, kept with the account so every device shows the same
195 // dock; never kept here, so a pin shows the moment it is made. Null
196 // falls back to the cookie (lib/dock.server.ts).
197 workspace ? savedPins(user, workspace.slug) : Promise.resolve(null),
Webhooks: every event, to your own addresses, signed and retried198 ]);
199 return {
200 workspace,
Projects: what a workspace builds and runs, first on every page201 // 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 sidebar202 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
203 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
204 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Webhooks: every event, to your own addresses, signed and retried205 repo:
206 path && counts?.ok
207 ? {
208 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209 // Until the repository's own page says what their role is.
210 member: hasAccessIn(user, path.namespace),
Webhooks: every event, to your own addresses, signed and retried211 issues: counts.value.issues,
212 pulls: counts.value.pulls,
213 }
214 : null,
Billing accounts, terms and enterprises; g1t is no longer free215 // Where the workspace stands against its usage limit, once billing is on.
216 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor217 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free218 ? {
219 exposureMicros: limit.value.exposureMicros,
220 ceilingMicros: limit.value.ceilingMicros,
221 state: limit.value.state,
222 comped: limit.value.trust === "internal",
223 }
224 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor225 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look226 // A spend spike or a hold pauses new compute: the shell says so on every page.
227 compute:
228 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
229 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
230 : null,
231 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all232 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to233 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)234 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.235 pins: dockPins ?? undefined,
Usage while free is shown at cost; agents get rustfmt and clippy236 // 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 audit237 // Usage at price, the one figure every page shows.
238 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Webhooks: every event, to your own addresses, signed and retried239 };
240}
241
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)242/** Starred and recent conversations Home's sidebar lists. */
243const SHELL_CONVERSATIONS = 8;
244
245/**
246 * What is unread in chat, for the rail, and the starred and latest
247 * conversations, for Home's sidebar; null when chat is slow to answer or
248 * does not.
249 */
Chat and workspace agents: channels, DMs and named agents you talk to250async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
251 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
252 const read = chat
253 .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)254 .then((result) => {
255 if (!result.ok) return null;
256 const entries = result.value.entries;
257 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
258 return {
259 ...unreadTotals(entries),
260 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
261 recent: latest.slice(0, SHELL_CONVERSATIONS),
262 };
263 })
264 .catch(() => null);
265 return Promise.race([read, timeout]);
266}
267
268/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
269async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
270 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
271 const read = workspaceAgents
272 .list(slug, user)
273 .then((result) =>
274 result.ok
275 ? result.value
276 .filter((agent) => !agent.archived_at)
277 .map((agent) => ({
278 id: agent.id,
279 handle: agent.handle,
280 display_name: agent.display_name,
281 avatar: agent.avatar,
282 avatar_seed: agent.avatar_seed,
283 role: agent.role,
284 title: agent.title,
285 team: agent.team,
286 department: agent.department,
287 status: agent.status,
288 builtin: agent.builtin === true,
289 }))
290 : null,
291 )
Chat and workspace agents: channels, DMs and named agents you talk to292 .catch(() => null);
293 return Promise.race([read, timeout]);
294}
295
296/** The longest the rail's Chat badge holds up a page. */
297const CHAT_BADGE_WAIT_MS = 300;
298
Usage, Billing settings and prepaid AI credit; fixes from the UX audit299/**
300 * How long the sidebar's workspace answers are kept: as long as someone's
301 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
302 */
303const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents304
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look305/** Most repositories listed under Shared with you. */
306const MAX_SHARED = 20;
307
308/**
309 * Repositories shared with the user in workspaces they do not belong to,
310 * by their grants: only those, never the workspace's other repositories.
311 */
312async function sharedRepos(user: User): Promise<ShellData["shared"]> {
313 const outside = new Set(sharedWorkspaces(user));
314 if (outside.size === 0) return [];
315 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 incidents316 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look317 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents318 .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 look319 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
320}
321
Usage, Billing settings and prepaid AI credit; fixes from the UX audit322/**
323 * The root's data as the browser last had it. When a navigation fails in a
324 * way that takes the root's data with it, the error is still drawn in the
325 * person's own frame, signed in, not the visitor's. Never kept on the
326 * server, where one module serves everyone.
327 */
328let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
329
Webhooks: every event, to your own addresses, signed and retried330export 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 address331 const nonce = useNonce();
Webhooks: every event, to your own addresses, signed and retried332 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit333 const loaded = useRouteLoaderData<typeof loader>("root");
334 const inBrowser = typeof document !== "undefined";
335 if (loaded && inBrowser) lastRoot = loaded;
336 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Webhooks: every event, to your own addresses, signed and retried337 const user = root?.user;
Merge branch 'socket-tickets'338 // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts).
339 if (inBrowser) setLiveViaToken(Boolean(user?.token?.website));
Merge branch 'worktree-agent-a985c01f43538af01'340 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas341 // Drawn around the error page too: a 404 keeps the sidebar out of a
342 // project or workspace the viewer cannot see.
343 const error = useRouteError();
344 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look345 // The billing page shows the full banner itself.
346 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents347 <SpikeBanner
348 compact
349 slug={root.shell.workspace.slug}
350 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
351 owner={root.shell.compute.owner}
352 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look353 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)354 // On the few pages an account still confirming its address can open
355 // besides /confirm-email (policies, support, signing in): the way back.
356 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
357 <p
358 role="status"
Webhooks: every event, to your own addresses, signed and retried359 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"
360 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)361 <span>Confirm your email address to start using g1t.</span>
362 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
363 Enter your code
364 </Link>
365 </p>
Webhooks: every event, to your own addresses, signed and retried366 );
Merge main (membership, two-factor, GitHub repo roles) into tokens367 // A workspace that requires two-factor authentication of someone who
368 // has not turned it on: they keep their place, and cannot use it yet.
369 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'370 const leaving = useLeaving();
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.371 // Which frame the page is drawn in (lib/chrome.ts): on its own, the app's, or the public one.
372 const frame = frameOf(pathname, {
373 signedIn: user != null && !awaitsConfirmation(user),
374 workspace: (user?.workspaces ?? []).length > 0,
375 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look376 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens377 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look378 <>
379 {paused}
380 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens381 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look382 </>
383 ) : null;
Webhooks: every event, to your own addresses, signed and retried384 return (
385 <html lang="en">
386 <head>
387 <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)388 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Webhooks: every event, to your own addresses, signed and retried389 <meta name="theme-color" content="#0f0f11" />
Merge branch 'mirroring' into artifacts-mode390 <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)391 <meta name="apple-mobile-web-app-capable" content="yes" />
392 <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 preloads393 {/* 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.394 connection paints sooner. */}
Give the stylesheet a precedence so React places it before preloads395 <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 today396 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Webhooks: every event, to your own addresses, signed and retried397 <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 address398 <Links nonce={nonce} />
Webhooks: every event, to your own addresses, signed and retried399 </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.400 <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 audit401 {/* The first stop for the keyboard: past the menus, to the page. */}
402 <a
403 href="#content"
404 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]"
405 >
406 Skip to content
407 </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.408 {frame === "standalone" ? (
409 <StandaloneFrame>{children}</StandaloneFrame>
410 ) : frame === "app" && user && root?.shell ? (
411 <AppShell user={user} shell={root.shell} missing={missing} banner={banner} sidebarClosed={root.sidebarClosed}>
Webhooks: every event, to your own addresses, signed and retried412 {children}
413 </AppShell>
414 ) : (
415 <>
416 <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.417 <PublicHeader user={user} />
Webhooks: every event, to your own addresses, signed and retried418 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit419 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'420 {children}
421 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look422 <SiteFooter user={user} />
Webhooks: every event, to your own addresses, signed and retried423 </>
424 )}
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)425 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
426 {user && !awaitsConfirmation(user) && (
427 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
428 )}
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 today429 <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 address430 <ScrollRestoration nonce={nonce} />
431 <Scripts nonce={nonce} />
Webhooks: every event, to your own addresses, signed and retried432 </body>
433 </html>
434 );
435}
436
437export 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 today438 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
439 // Tell analytics who is signed in, once per change; signing out forgets them.
440 const lastUserId = useRef<string | null>(null);
441 useEffect(() => {
442 identify(userId, lastUserId.current);
443 lastUserId.current = userId;
444 }, [userId]);
Webhooks: every event, to your own addresses, signed and retried445 return <Outlet />;
446}
447
448export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit449 const location = useLocation();
450 const matches = useMatches();
Merge branch 'worktree-agent-a64e51c8f386f46ac'451 const href = location.pathname + location.search + location.hash;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit452 // A tab left open across a deploy: load the address again as a whole
Merge branch 'worktree-agent-a64e51c8f386f46ac'453 // page, once, rather than show an error the reload fixes. A document load
454 // never reloads, so the server and the hydrating client render the same.
455 const reloadable = reloadFixes({
Usage, Billing settings and prepaid AI credit; fixes from the UX audit456 error,
457 status: isRouteErrorResponse(error) ? error.status : undefined,
Merge branch 'worktree-agent-a64e51c8f386f46ac'458 clientNavigation: clientNavigated(location.key),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit459 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
460 });
Merge branch 'worktree-agent-a64e51c8f386f46ac'461 // Already loaded again once, or the reload never started: show the page.
462 const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]);
463 const [gaveUp, setGaveUp] = useState<string | null>(null);
464 const reload = reloadable && !tried && gaveUp !== href;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit465 useEffect(() => {
466 if (!reload) return;
467 try {
468 sessionStorage.setItem(RELOADED_KEY, href);
469 } catch {
470 // No session storage: reload anyway; a document load never asks again.
471 }
472 window.location.assign(href);
Merge branch 'worktree-agent-a64e51c8f386f46ac'473 const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
474 return () => window.clearTimeout(timer);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit475 }, [reload, href]);
476 if (reload) {
477 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in478 <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 audit479 <title>Loading · g1t</title>
480 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 in481 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit482 );
483 }
484
Webhooks: every event, to your own addresses, signed and retried485 let title = "Something went wrong";
486 let details = "An unexpected error occurred. Try again in a moment.";
487 let stack: string | undefined;
488
489 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put490 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit491 if (error.status === 404) {
492 return (
493 <>
494 <title>Not found · g1t</title>
495 <NotFound data={error.data} />
496 </>
497 );
498 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put499 if (typeof error.data === "string" && error.data) {
Webhooks: every event, to your own addresses, signed and retried500 details = error.data;
501 }
Merge main into Artifacts Phase 2502 // A token asked for what needs a real sign-in (lib/website-token.ts).
503 if (isNeedsSignIn(error.data)) {
504 title = "This needs you to sign in";
505 details = error.data.message;
506 }
Webhooks: every event, to your own addresses, signed and retried507 } else if (import.meta.env.DEV && error instanceof Error) {
508 details = error.message;
509 stack = error.stack;
510 }
511
512 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in513 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Webhooks: every event, to your own addresses, signed and retried514 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
515 <p className="mt-3 text-muted">{details}</p>
516 <div className="mt-8">
517 <ButtonLink to="/" variant="quiet">
518 Back to home
519 </ButtonLink>
520 </div>
521 {stack && (
522 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
523 <code>{stack}</code>
524 </pre>
525 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in526 </PageMain>
Webhooks: every event, to your own addresses, signed and retried527 );
528}

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