Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
735 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";
ARTIFACTS.md: R8 (shallow sandbox clones) is built2import {
3 isRouteErrorResponse,
4 Link,
5 Links,
6 Meta,
7 Outlet,
8 Scripts,
9 ScrollRestoration,
10 type ShouldRevalidateFunctionArgs,
11 useLocation,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit12 useMatches,
ARTIFACTS.md: R8 (shallow sandbox clones) is built13 useRouteError,
14 useRouteLoaderData,
15} from "react-router";
16
tailwind17import {
18 type User,
19 awaitsConfirmation,
20 hasAccessIn,
21 sharedWorkspaces,
22} from "@g1t/contracts";
ARTIFACTS.md: R8 (shallow sandbox clones) is built23
24import type { Route } from "./+types/root";
25import appCss from "./app.css?url";
26import displayFont from "@g1t/theme/fonts/bricolage-grotesque-latin.woff2?url";
27import 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.28import { ButtonLink } from "./components/ui";
tailwind29import {
30 AppShell,
31 Progress,
32 type ShellData,
33 useLeaving,
34} from "./components/shell";
ARTIFACTS.md: R8 (shallow sandbox clones) is built35import { 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.36import { PublicHeader } from "./components/public-header";
ARTIFACTS.md: R8 (shallow sandbox clones) is built37import { SpikeBanner } from "./components/spike-banner";
Merge main (membership, two-factor, GitHub repo roles) into tokens38import { 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 today39import { identify } from "./lib/analytics.client";
40import { visitorAsksFirst } from "./lib/analytics-consent";
41import { AnalyticsConsent } from "./components/analytics-consent";
ARTIFACTS.md: R8 (shallow sandbox clones) is built42import { readCookie } from "./lib/mission";
43import { 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 in44import { PageMain } from "./components/landmark";
ARTIFACTS.md: R8 (shallow sandbox clones) is built45import { 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.46import { frameOf } from "./lib/chrome";
tailwind47import {
48 DOCK_COOKIE,
49 SIDEBAR_COOKIE,
50 pinsToShow,
51 sidebarClosed,
52} from "./lib/apps";
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.53import { 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.54import { StandaloneFrame } from "./components/standalone";
tailwind55import {
56 billing,
57 chat,
58 identity,
59 inbox,
60 projects,
61} from "./lib/services.server";
Chat and workspace agents: channels, DMs and named agents you talk to62import { unreadTotals } from "./lib/chat";
ARTIFACTS.md: R8 (shallow sandbox clones) is built63import { countsFor, readableRepos } from "./lib/access.server";
64import { shortCache } from "./lib/cache.server";
65import { getViewer, viewerMiddleware } from "./lib/session.server";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar66import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server";
Merge branch 'worktree-agent-aaf03bdceac799c89'67import { addresses } from "./lib/addresses.server";
tailwind68import {
69 RELOADED_KEY,
70 RELOAD_GIVE_UP_MS,
71 clientNavigated,
72 reloadFixes,
73 reloadedBefore,
74} 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 address75import { useNonce } from "./lib/nonce";
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r976import { useStuckNavigationRescue } from "./lib/stuck";
Merge main into Artifacts Phase 277import { isNeedsSignIn } from "./lib/website-token";
A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers.78import { offerTicket, setLiveViaToken } from "./lib/live-socket";
79import { socketTicketFor } from "./lib/socket-ticket.server";
Merge branch 'main' into rm-agents80import { DesktopApp } from "./components/desktop-app";
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.81import { DesktopRunnerLink } from "./components/desktop-runner-link";
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)82import { LiveNotifications } from "./components/notifications/live-notifications";
tailwind83import {
84 THEME_COLOR,
85 THEME_COOKIE,
86 readTheme,
87 themeAttribute,
88 useThemeChoice,
89} from "./lib/theme";
90import {
91 DEVELOPER_COOKIE,
92 readDeveloperMode,
93 useDeveloperMode,
94} from "./lib/developer-mode";
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.95import { DebugDetails } from "./components/debug-details";
ARTIFACTS.md: R8 (shallow sandbox clones) is built96
97export const links: Route.LinksFunction = () => [
98 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
99 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
100 { 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 managers101 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
102 // Home screens and password managers look for a square, opaque 180px tile;
103 // the "-precomposed" name is served too, for the ones that ask for it.
104 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
105 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
106 // manifest.webmanifest is the same file, kept for installs that already point at it).
107 { rel: "manifest", href: "/site.webmanifest" },
ARTIFACTS.md: R8 (shallow sandbox clones) is built108 // The text and headline faces are wanted on every page, so they start
109 // loading with the stylesheet; mono waits until something uses it.
tailwind110 {
111 rel: "preload",
112 href: sansFont,
113 as: "font",
114 type: "font/woff2",
115 crossOrigin: "anonymous",
116 },
117 {
118 rel: "preload",
119 href: displayFont,
120 as: "font",
121 type: "font/woff2",
122 crossOrigin: "anonymous",
123 },
ARTIFACTS.md: R8 (shallow sandbox clones) is built124];
125
126export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
127
128export async function loader({ context, params, request }: Route.LoaderArgs) {
129 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.130 const cookies = request.headers.get("cookie");
131 const chosen = readCookie(cookies, WORKSPACE_COOKIE);
132 const dock = readCookie(cookies, DOCK_COOKIE);
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.133 const [shell, sounds] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)134 // An account still confirming its address sees only the pages that
135 // allows (lib/confirm-gate.ts), in the visitor's frame.
136 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit137 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
138 // A sidebar that could not be read is no reason to lose the page,
139 // or to draw it as if they were signed out.
140 console.error("root: the sidebar could not be read", error);
141 return bareShell(user, params, chosen);
142 })
143 : visitorShell(params, context),
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.144 // Their chat sounds, with the page, so the first live event already
145 // respects them (lib/sounds.ts); the defaults when identity is slow.
tailwind146 user && !awaitsConfirmation(user)
147 ? identity.soundSettings(user).catch(() => null)
148 : Promise.resolve(null),
ARTIFACTS.md: R8 (shallow sandbox clones) is built149 ]);
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.150 // The apps this person pinned to their rail here: as their account keeps
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.151 // them (read with the rest of the shell), else as this device's cookie
152 // remembers them (lib/apps.ts).
tailwind153 if (shell.workspace)
154 shell.pins = pinsToShow(shell.pins, dock, shell.workspace.slug);
Merge branch 'worktree-agent-aaf03bdceac799c89'155 // 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 today156 return {
157 user,
158 shell,
A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers.159 // For a page opened with an access token: the feed socket's ticket,
160 // minted here so the feed opens without asking for one (lib/live-socket.ts).
tailwind161 liveTicket: user?.token?.website
162 ? await socketTicketFor(context, request, "/-/live")
163 : null,
g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.164 // Whether they folded the sidebar away, so the page is drawn that way from the start.
165 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.166 // Their Appearance, so the page is drawn in it from the first byte (lib/theme.ts).
167 theme: readTheme(readCookie(cookies, THEME_COOKIE)),
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.168 // Whether a page that fails says why, in full (lib/developer-mode.ts).
169 developer: readDeveloperMode(readCookie(cookies, DEVELOPER_COOKIE)),
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.170 // What they hear when chat moves (components/notifications/sounds.tsx).
171 sounds,
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 today172 addresses: addresses(),
173 // Visitors from where the law asks first are asked before analytics runs.
174 analyticsConsent: visitorAsksFirst(request),
175 };
ARTIFACTS.md: R8 (shallow sandbox clones) is built176}
177
178/**
179 * The sidebar for someone not signed in: nothing of their own, only the
180 * open counts of the project being looked at. One call, and only in a
181 * project; it answers the same for a private project as a missing one.
182 */
tailwind183async function visitorShell(
184 params: { owner?: string; repo?: string },
185 context: Route.LoaderArgs["context"],
186): Promise<ShellData> {
187 const path =
188 params.owner && params.repo
189 ? { namespace: params.owner, name: params.repo }
190 : null;
ARTIFACTS.md: R8 (shallow sandbox clones) is built191 const counts = path ? await countsFor(context, params) : null;
192 return {
193 workspace: null,
194 repos: [],
tailwind195 repo:
196 path && counts?.ok
197 ? {
198 ...path,
199 member: false,
200 issues: counts.value.issues,
201 pulls: counts.value.pulls,
202 }
203 : null,
ARTIFACTS.md: R8 (shallow sandbox clones) is built204 limit: null,
205 monthUsageMicros: null,
206 };
207}
208
Usage, Billing settings and prepaid AI credit; fixes from the UX audit209/** The sidebar from what the session already says, with no service asked. */
tailwind210function bareShell(
211 user: User,
212 params: { owner?: string; repo?: string },
213 chosen: string | null,
214): ShellData {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit215 return {
216 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
217 repos: [],
218 repo: null,
219 limit: null,
220 monthUsageMicros: null,
221 };
222}
223
ARTIFACTS.md: R8 (shallow sandbox clones) is built224/**
225 * The sidebar only changes with the workspace or repository being looked
226 * at, or after something was submitted: not on every page within them.
227 */
228export function shouldRevalidate({
229 currentParams,
230 nextParams,
231 formMethod,
232 defaultShouldRevalidate,
233}: ShouldRevalidateFunctionArgs) {
234 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
tailwind235 return (
236 currentParams.owner !== nextParams.owner ||
237 currentParams.repo !== nextParams.repo
238 );
ARTIFACTS.md: R8 (shallow sandbox clones) is built239}
240
241/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit242 * The sidebar: the workspace whose pages or project these are when it is
243 * one of yours, else the one you chose (lib/workspace-choice.ts); its
244 * projects; and the repository being looked at.
ARTIFACTS.md: R8 (shallow sandbox clones) is built245 */
246async function shellFor(
247 user: User,
248 params: { owner?: string; repo?: string },
249 chosen: string | null,
250 context: Route.LoaderArgs["context"],
251): Promise<ShellData> {
252 const memberships = user.workspaces ?? [];
253 const workspace = workspaceFor(memberships, chosen, params);
tailwind254 const path =
255 params.owner && params.repo
256 ? { namespace: params.owner, name: params.repo }
257 : null;
ARTIFACTS.md: R8 (shallow sandbox clones) is built258 const now = new Date();
tailwind259 const monthStart = new Date(
260 Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),
261 ).toISOString();
ARTIFACTS.md: R8 (shallow sandbox clones) is built262 // What the sidebar shows of the workspace changes rarely: kept for a few
263 // seconds per person and workspace, except just after they changed
264 // something (lib/cache.server.ts).
265 const kept = <T,>(what: string, load: () => Promise<T>) =>
tailwind266 workspace
267 ? shortCache(
268 `shell:${what}:${user.id}:${workspace.slug}`,
269 SHELL_TTL_MS,
270 load,
271 )
272 : Promise.resolve(null);
273 const [
274 listed,
275 counts,
276 status,
277 usage,
278 limit,
279 entitlements,
280 shared,
281 unread,
282 shortcuts,
283 chatUnread,
284 dockPins,
285 ] = await Promise.all([
Navigation: the workspace's own page with tabs, pins and recent in the sidebar286 // Every project, for the palette and the count; the sidebar lists only
287 // the person's pinned and recent ones (lib/pins.ts).
288 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
ARTIFACTS.md: R8 (shallow sandbox clones) is built289 path ? countsFor(context, params) : Promise.resolve(null),
290 // Whether billing is on, without reading the account: that asks the
291 // card processor about the workspace's cards, too slow for every page.
292 kept("status", () => billing.status()).catch(() => null),
tailwind293 kept(`usage:${monthStart}`, () =>
294 billing.usage(workspace!.slug, user, monthStart),
295 ).catch(() => null),
ARTIFACTS.md: R8 (shallow sandbox clones) is built296 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
tailwind297 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(
298 () => null,
299 ),
ARTIFACTS.md: R8 (shallow sandbox clones) is built300 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all301 // The bell's count: one query, never kept, so marking an item shows at once.
302 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar303 // Never kept: opening a project moves it up Recent.
tailwind304 workspace
305 ? projects.shortcuts(workspace.slug, user).catch(() => null)
306 : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to307 // The rail's Chat badge: never kept, and never waited on for long.
308 workspace ? chatUnreadFor(workspace.slug, user) : Promise.resolve(null),
The left-hand navigation takes less room and reads at a glance. The dock becomes a rail: a 56-pixel column of icons with the g1t mark on top, the current app marked by a filled square, hover hints naming each with its shortcut, the apps you pinned in a scrolling band, and Apps, People, Workspace and your account at the foot. Each mode's sidebar sits flush against it, 240 pixels wide, opening on Search or jump to and listing its groups under small labels in 36-pixel rows; Ctrl B hides it and the page grows into the room. Every mode's list is built on the shadcn Sidebar now in the kit, so Workspace, Code, People, Notifications, account settings, Chat, Artifacts and Agents all share one frame; the phone keeps its bottom bar and drawer. The workspaces guide describes the rail, the sidebar and the page.309 // The rail's pins, kept with the account so every device shows the same
310 // rail; never kept here, so a pin shows the moment it is made. Null
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.311 // falls back to the cookie (lib/dock.server.ts).
312 workspace ? savedPins(user, workspace.slug) : Promise.resolve(null),
ARTIFACTS.md: R8 (shallow sandbox clones) is built313 ]);
314 return {
315 workspace,
316 // 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 sidebar317 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
318 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
319 recent: (shortcuts?.recent ?? []).map(shortcutOf),
ARTIFACTS.md: R8 (shallow sandbox clones) is built320 repo:
321 path && counts?.ok
322 ? {
323 ...path,
324 // Until the repository's own page says what their role is.
325 member: hasAccessIn(user, path.namespace),
326 issues: counts.value.issues,
327 pulls: counts.value.pulls,
328 }
329 : null,
330 // Where the workspace stands against its usage limit, once billing is on.
331 limit:
332 status?.enabled && limit?.ok
333 ? {
334 exposureMicros: limit.value.exposureMicros,
335 ceilingMicros: limit.value.ceilingMicros,
336 state: limit.value.state,
337 comped: limit.value.trust === "internal",
338 }
339 : null,
340 free: Boolean(status?.free),
341 // A spend spike or a hold pauses new compute: the shell says so on every page.
342 compute:
tailwind343 workspace &&
344 entitlements &&
345 (entitlements.paused || entitlements.spike?.status === "open")
346 ? {
347 paused: entitlements.paused,
348 spike: entitlements.spike ?? null,
349 owner: workspace.role === "owner",
350 }
ARTIFACTS.md: R8 (shallow sandbox clones) is built351 : null,
352 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all353 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to354 chat: chatUnread,
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.355 pins: dockPins ?? undefined,
ARTIFACTS.md: R8 (shallow sandbox clones) is built356 // 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 audit357 // Usage at price, the one figure every page shows.
tailwind358 monthUsageMicros: usage?.ok
359 ? usage.value.free
360 ? usage.value.usedMicros
361 : (usage.value.priceMicros ?? usage.value.spentMicros)
362 : null,
ARTIFACTS.md: R8 (shallow sandbox clones) is built363 };
364}
365
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)366/** Starred and recent conversations Home's sidebar lists. */
367const SHELL_CONVERSATIONS = 8;
368
369/**
370 * What is unread in chat, for the rail, and the starred and latest
371 * conversations, for Home's sidebar; null when chat is slow to answer or
372 * does not.
373 */
tailwind374async function chatUnreadFor(
375 slug: string,
376 user: User,
377): Promise<ShellData["chat"]> {
378 const timeout = new Promise<null>((resolve) =>
379 setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS),
380 );
Chat and workspace agents: channels, DMs and named agents you talk to381 const read = chat
382 .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)383 .then((result) => {
384 if (!result.ok) return null;
385 const entries = result.value.entries;
tailwind386 const latest = [...entries].sort((a, b) =>
387 (b.channel.last_message_at ?? "").localeCompare(
388 a.channel.last_message_at ?? "",
389 ),
390 );
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 return {
392 ...unreadTotals(entries),
393 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
394 recent: latest.slice(0, SHELL_CONVERSATIONS),
395 };
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.396 })
Chat and workspace agents: channels, DMs and named agents you talk to397 .catch(() => null);
398 return Promise.race([read, timeout]);
399}
400
401/** The longest the rail's Chat badge holds up a page. */
402const CHAT_BADGE_WAIT_MS = 300;
403
Usage, Billing settings and prepaid AI credit; fixes from the UX audit404/**
405 * How long the sidebar's workspace answers are kept: as long as someone's
406 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
407 */
408const SHELL_TTL_MS = 30_000;
ARTIFACTS.md: R8 (shallow sandbox clones) is built409
410/** Most repositories listed under Shared with you. */
411const MAX_SHARED = 20;
412
413/**
414 * Repositories shared with the user in workspaces they do not belong to,
415 * by their grants: only those, never the workspace's other repositories.
416 */
417async function sharedRepos(user: User): Promise<ShellData["shared"]> {
418 const outside = new Set(sharedWorkspaces(user));
419 if (outside.size === 0) return [];
tailwind420 const ids = (user.grants ?? [])
421 .filter((grant) => outside.has(grant.workspace))
422 .map((grant) => grant.repo_id)
423 .slice(0, MAX_SHARED);
ARTIFACTS.md: R8 (shallow sandbox clones) is built424 const found = await readableRepos(ids, user);
425 return found
tailwind426 .map((repo) => ({
427 namespace: repo.namespace,
428 name: repo.name,
429 isPrivate: repo.isPrivate,
430 }))
431 .sort((a, b) =>
432 `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`),
433 );
ARTIFACTS.md: R8 (shallow sandbox clones) is built434}
435
Usage, Billing settings and prepaid AI credit; fixes from the UX audit436/**
437 * The root's data as the browser last had it. When a navigation fails in a
438 * way that takes the root's data with it, the error is still drawn in the
439 * person's own frame, signed in, not the visitor's. Never kept on the
440 * server, where one module serves everyone.
441 */
442let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
443
ARTIFACTS.md: R8 (shallow sandbox clones) is built444export 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 address445 const nonce = useNonce();
ARTIFACTS.md: R8 (shallow sandbox clones) is built446 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit447 const loaded = useRouteLoaderData<typeof loader>("root");
448 const inBrowser = typeof document !== "undefined";
449 if (loaded && inBrowser) lastRoot = loaded;
450 const root = loaded ?? (inBrowser ? lastRoot : undefined);
ARTIFACTS.md: R8 (shallow sandbox clones) is built451 const user = root?.user;
Merge branch 'socket-tickets'452 // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts).
A doc opens at once: the page carries the document's saved state, so the editor mounts without waiting for its live room, which connects in the background and sends only what changed, with a quiet dot that turns green when it's synced; edits made before then are kept and sent once. The editor's code is preloaded from the page and fetched early when you hover a doc, the app's shared code ships in a few chunks instead of a hundred small ones, socket tickets come with the page instead of a round trip, and the artifacts service answers a page, a sidebar and a room in parallel lookups instead of a dozen in a row, reporting its database round trips in Server-Timing. The artifacts guide says how a doc opens, and CONTRIBUTING's Speed section has the numbers.453 if (inBrowser) {
454 setLiveViaToken(Boolean(user?.token?.website));
455 offerTicket("/-/live", root?.liveTicket);
456 }
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9457 const { pathname } = useLocation();
ARTIFACTS.md: R8 (shallow sandbox clones) is built458 // Drawn around the error page too: a 404 keeps the sidebar out of a
459 // project or workspace the viewer cannot see.
460 const error = useRouteError();
461 const missing = isRouteErrorResponse(error) && error.status === 404;
462 // The billing page shows the full banner itself.
tailwind463 const paused = root?.shell?.compute &&
464 root.shell.workspace &&
465 !pathname.endsWith("/-/billing") && (
466 <SpikeBanner
467 compact
468 slug={root.shell.workspace.slug}
469 entitlements={{
470 paused: root.shell.compute.paused,
471 spike: root.shell.compute.spike,
472 }}
473 owner={root.shell.compute.owner}
474 />
475 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)476 // On the few pages an account still confirming its address can open
477 // besides /confirm-email (policies, support, signing in): the way back.
tailwind478 const verify = user &&
479 awaitsConfirmation(user) &&
480 pathname !== "/confirm-email" && (
481 <p
482 role="status"
483 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"
484 >
485 <span>Confirm your email address to start using g1t.</span>
486 <Link
487 to="/confirm-email"
488 className="font-medium underline underline-offset-4"
489 >
490 Enter your code
491 </Link>
492 </p>
493 );
Merge main (membership, two-factor, GitHub repo roles) into tokens494 // A workspace that requires two-factor authentication of someone who
495 // has not turned it on: they keep their place, and cannot use it yet.
tailwind496 const held = user?.held &&
497 user.held.length > 0 &&
498 pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'499 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.500 // Auto, Light or Dark: as the cookie said when the page was drawn, or as just switched.
501 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.502 // Which frame the page is drawn in (lib/chrome.ts): on its own, the app's, or the public one.
503 const frame = frameOf(pathname, {
504 signedIn: user != null && !awaitsConfirmation(user),
505 workspace: (user?.workspaces ?? []).length > 0,
506 });
ARTIFACTS.md: R8 (shallow sandbox clones) is built507 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens508 paused || verify || held ? (
ARTIFACTS.md: R8 (shallow sandbox clones) is built509 <>
510 {paused}
511 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens512 {held}
ARTIFACTS.md: R8 (shallow sandbox clones) is built513 </>
514 ) : null;
515 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.516 <html lang="en" data-theme={themeAttribute(theme)}>
ARTIFACTS.md: R8 (shallow sandbox clones) is built517 <head>
518 <meta charSet="utf-8" />
tailwind519 <meta
520 name="viewport"
521 content="width=device-width, initial-scale=1, viewport-fit=cover"
522 />
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.523 {/* The browser bar matches the page: one colour for a chosen theme, the system's for Auto. */}
524 {theme === "auto" ? (
525 <>
tailwind526 <meta
527 name="theme-color"
528 media="(prefers-color-scheme: light)"
529 content={THEME_COLOR.light}
530 />
531 <meta
532 name="theme-color"
533 media="(prefers-color-scheme: dark)"
534 content={THEME_COLOR.dark}
535 />
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.536 </>
537 ) : (
538 <meta name="theme-color" content={THEME_COLOR[theme]} />
539 )}
Merge branch 'mirroring' into artifacts-mode540 <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)541 <meta name="apple-mobile-web-app-capable" content="yes" />
tailwind542 <meta
543 name="apple-mobile-web-app-status-bar-style"
544 content="black-translucent"
545 />
ARTIFACTS.md: R8 (shallow sandbox clones) is built546 {/* 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.547 connection paints sooner. */}
ARTIFACTS.md: R8 (shallow sandbox clones) is built548 <link rel="stylesheet" href={appCss} precedence="default" />
tailwind549 {root?.analyticsConsent && (
550 <meta name="g1t-analytics" content="consent" />
551 )}
ARTIFACTS.md: R8 (shallow sandbox clones) is built552 <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 address553 <Links nonce={nonce} />
ARTIFACTS.md: R8 (shallow sandbox clones) is built554 </head>
tailwind555 <body
556 className="flex min-h-screen flex-col"
557 data-frame={frame === "app" && user && root?.shell ? "app" : frame}
558 >
Usage, Billing settings and prepaid AI credit; fixes from the UX audit559 {/* The first stop for the keyboard: past the menus, to the page. */}
560 <a
561 href="#content"
tailwind562 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"
Usage, Billing settings and prepaid AI credit; fixes from the UX audit563 >
564 Skip to content
565 </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.566 {frame === "standalone" ? (
567 <StandaloneFrame>{children}</StandaloneFrame>
568 ) : frame === "app" && user && root?.shell ? (
tailwind569 <AppShell
570 user={user}
571 shell={root.shell}
572 missing={missing}
573 banner={banner}
574 sidebarClosed={root.sidebarClosed}
575 >
ARTIFACTS.md: R8 (shallow sandbox clones) is built576 {children}
577 </AppShell>
578 ) : (
579 <>
580 <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.581 <PublicHeader user={user} />
ARTIFACTS.md: R8 (shallow sandbox clones) is built582 {banner}
tailwind583 <div
584 id="content"
585 tabIndex={-1}
586 {...leaving}
587 className={`grow outline-none ${leaving.className}`}
588 >
Merge branch 'worktree-agent-aca4790ea223aa235'589 {children}
590 </div>
ARTIFACTS.md: R8 (shallow sandbox clones) is built591 <SiteFooter user={user} />
592 </>
593 )}
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)594 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
595 {user && !awaitsConfirmation(user) && (
Chat has sounds. A short warm cue plays for a message in a conversation you have open but aren't looking at, a direct message, a mention of you and an agent finishing something for you, with a soft tick for sending that is off until you turn it on; never for your own messages, a muted conversation, a conversation you are looking at in a window that has the front, or while you have paused notifications, and never twice for one message or more than once in a second and a half. The cues are made by the browser itself in two sets, Soft and Bright, so nothing is downloaded; the Sounds and notifications section of your notification settings turns them on and off, picks the set and the volume, plays each one, and offers desktop notifications for when the window is behind; Do not disturb is the one pause everyone sees, set from the chat sidebar's bell, the settings, or your avatar menu, now with a two-hour choice, and the top bar's bell shows a dot while it's on. Your choices are kept with your account. The sound layer sits behind one small interface so the desktop app can play them natively later. The chat and notifications guides say the rules.596 <LiveNotifications
597 workspace={root?.shell?.workspace?.slug ?? null}
598 inbox={root?.shell?.inbox?.unread ?? null}
599 sounds={root?.sounds ?? null}
600 me={{ id: user.id, username: user.username }}
601 />
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)602 )}
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.603 {/* Inside the desktop app: who its runner runs for, and its registration (lib/desktop-runner.ts). */}
604 {(!user || !awaitsConfirmation(user)) && (
605 <DesktopRunnerLink
606 offer={{
607 signedIn: user != null,
tailwind608 workspaces: (user?.workspaces ?? [])
609 .filter((m) => m.role === "owner")
610 .map((m) => ({ slug: m.slug, name: m.name ?? null })),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.611 current: root?.shell?.workspace?.slug ?? null,
612 }}
613 />
614 )}
Merge branch 'main' into rm-agents615 {/* Inside the desktop app: its settings dialog, and the runner's menu under the title bar's dot. */}
616 <DesktopApp />
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 today617 <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 address618 <ScrollRestoration nonce={nonce} />
619 <Scripts nonce={nonce} />
ARTIFACTS.md: R8 (shallow sandbox clones) is built620 </body>
621 </html>
622 );
623}
624
625export 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 today626 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
627 // Tell analytics who is signed in, once per change; signing out forgets them.
628 const lastUserId = useRef<string | null>(null);
629 useEffect(() => {
630 identify(userId, lastUserId.current);
631 lastUserId.current = userId;
632 }, [userId]);
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9633 // A navigation that never arrives is loaded as a whole page (lib/stuck.ts).
634 useStuckNavigationRescue();
ARTIFACTS.md: R8 (shallow sandbox clones) is built635 return <Outlet />;
636}
637
638export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit639 const location = useLocation();
640 const matches = useMatches();
Merge branch 'worktree-agent-a64e51c8f386f46ac'641 const href = location.pathname + location.search + location.hash;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit642 // A tab left open across a deploy: load the address again as a whole
Merge branch 'worktree-agent-a64e51c8f386f46ac'643 // page, once, rather than show an error the reload fixes. A document load
644 // never reloads, so the server and the hydrating client render the same.
645 const reloadable = reloadFixes({
Usage, Billing settings and prepaid AI credit; fixes from the UX audit646 error,
647 status: isRouteErrorResponse(error) ? error.status : undefined,
Merge branch 'worktree-agent-a64e51c8f386f46ac'648 clientNavigation: clientNavigated(location.key),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit649 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
650 });
Merge branch 'worktree-agent-a64e51c8f386f46ac'651 // Already loaded again once, or the reload never started: show the page.
tailwind652 const tried = useMemo(
653 () => reloadable && reloadedBefore(href),
654 [reloadable, href],
655 );
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.656 // The error in full, for reporting it (lib/developer-mode.ts).
tailwind657 const developer =
658 useDeveloperMode(useRouteLoaderData<typeof loader>("root")?.developer) ||
659 import.meta.env.DEV;
Merge branch 'worktree-agent-a64e51c8f386f46ac'660 const [gaveUp, setGaveUp] = useState<string | null>(null);
661 const reload = reloadable && !tried && gaveUp !== href;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit662 useEffect(() => {
663 if (!reload) return;
664 try {
665 sessionStorage.setItem(RELOADED_KEY, href);
666 } catch {
667 // No session storage: reload anyway; a document load never asks again.
668 }
669 window.location.assign(href);
Merge branch 'worktree-agent-a64e51c8f386f46ac'670 const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
671 return () => window.clearTimeout(timer);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit672 }, [reload, href]);
673 if (reload) {
674 return (
tailwind675 <PageMain
676 className="mx-auto max-w-xl px-4 py-32 text-center text-sm text-muted"
677 aria-busy="true"
678 >
Usage, Billing settings and prepaid AI credit; fixes from the UX audit679 <title>Loading · g1t</title>
680 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 in681 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit682 );
683 }
684
ARTIFACTS.md: R8 (shallow sandbox clones) is built685 let title = "Something went wrong";
686 let details = "An unexpected error occurred. Try again in a moment.";
687
688 if (isRouteErrorResponse(error)) {
689 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit690 if (error.status === 404) {
691 return (
692 <>
693 <title>Not found · g1t</title>
694 <NotFound data={error.data} />
695 </>
696 );
697 }
ARTIFACTS.md: R8 (shallow sandbox clones) is built698 if (typeof error.data === "string" && error.data) {
699 details = error.data;
700 }
Merge main into Artifacts Phase 2701 // A token asked for what needs a real sign-in (lib/website-token.ts).
702 if (isNeedsSignIn(error.data)) {
703 title = "This needs you to sign in";
704 details = error.data.message;
705 }
ARTIFACTS.md: R8 (shallow sandbox clones) is built706 }
707
708 return (
tailwind709 <PageMain
710 className={`mx-auto px-4 text-center ${developer ? "max-w-3xl py-16" : "max-w-xl py-32"}`}
711 >
ARTIFACTS.md: R8 (shallow sandbox clones) is built712 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
713 <p className="mt-3 text-muted">{details}</p>
714 <div className="mt-8">
The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.715 <ButtonLink to="/" variant="outline">
ARTIFACTS.md: R8 (shallow sandbox clones) is built716 Back to home
717 </ButtonLink>
718 </div>
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.719 {developer ? (
720 <DebugDetails error={error} />
721 ) : (
722 <p className="mt-6 text-xs text-faint">
723 Turn on developer mode in{" "}
tailwind724 <Link
725 to="/settings/account#developer"
726 className="underline underline-offset-4 hover:text-fg"
727 >
Developer mode: a page that fails can say why. Under Settings, Account, a Developer mode switch (kept per browser in a g1t_dev cookie, like Appearance) makes the error page show the error itself, its message and stack, the address, the time, whether it came from a page the server drew or a navigation in the browser, the routes it was drawn from, the site's build (its stylesheet's hashed name), the browser and the desktop app's version, with Copy details to paste it all into a report; with it off, the page says where to turn it on. A signed-in request with developer mode on is answered by React Router in its development mode (workers/app.ts, a second handler made the first time it is wanted), so an error on the server comes through in full instead of "Unexpected Server Error"; that mode differs from production only in that and in hooks only Vite's dev server sets, and only ever covers the person's own requests. Checked in a production build: a failing repository page shows "counts failed with status 503" and its stack with developer mode on, and the usual page with it off. The workspaces guide covers it under Developer mode.728 Settings
729 </Link>{" "}
730 to see what went wrong.
731 </p>
ARTIFACTS.md: R8 (shallow sandbox clones) is built732 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in733 </PageMain>
ARTIFACTS.md: R8 (shallow sandbox clones) is built734 );
735}

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