Skip to content
541 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";
Automations: rules in .g1t/automations that act when something happens2import {
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,
Automations: rules in .g1t/automations that act when something happens14 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";
Automations: rules in .g1t/automations that act when something happens18
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";
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.53import { THEME_COLOR, THEME_COOKIE, readTheme, themeAttribute, useThemeChoice } from "./lib/theme";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look54
Automations: rules in .g1t/automations that act when something happens55
56export const links: Route.LinksFunction = () => [
The fleet: g1t's new mark57 { rel: "icon", href: "/favicon.ico", sizes: "32x32" },
Automations: rules in .g1t/automations that act when something happens58 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
The fleet: g1t's new mark59 { 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 managers60 { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" },
61 // Home screens and password managers look for a square, opaque 180px tile;
62 // the "-precomposed" name is served too, for the ones that ask for it.
63 { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
64 // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest;
65 // manifest.webmanifest is the same file, kept for installs that already point at it).
66 { rel: "manifest", href: "/site.webmanifest" },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67 // The text and headline faces are wanted on every page, so they start
68 // loading with the stylesheet; mono waits until something uses it.
69 { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
70 { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
Automations: rules in .g1t/automations that act when something happens71];
72
73export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
74
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put75export async function loader({ context, params, request }: Route.LoaderArgs) {
Automations: rules in .g1t/automations that act when something happens76 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.77 const cookies = request.headers.get("cookie");
78 const chosen = readCookie(cookies, WORKSPACE_COOKIE);
79 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)80 // Whether sign-up takes an invite: the sign-up page says so, and
81 // 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 look82 const [shell, mode] = await Promise.all([
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)83 // An account still confirming its address sees only the pages that
84 // allows (lib/confirm-gate.ts), in the visitor's frame.
85 user && !awaitsConfirmation(user)
Usage, Billing settings and prepaid AI credit; fixes from the UX audit86 ? shellFor(user, params, chosen, context).catch((error: unknown) => {
87 // A sidebar that could not be read is no reason to lose the page,
88 // or to draw it as if they were signed out.
89 console.error("root: the sidebar could not be read", error);
90 return bareShell(user, params, chosen);
91 })
92 : 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)93 registrationMode(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look94 ]);
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.95 // The apps this person pinned to their dock here: as their account keeps
96 // them (read with the rest of the shell), else as this device's cookie
97 // remembers them (lib/apps.ts).
98 if (shell.workspace) shell.pins = pinsToShow(shell.pins, dock, shell.workspace.slug);
Merge branch 'worktree-agent-aaf03bdceac799c89'99 // 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 today100 return {
101 user,
102 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.103 // Whether they folded the sidebar away, so the page is drawn that way from the start.
104 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.105 // Their Appearance, so the page is drawn in it from the first byte (lib/theme.ts).
106 theme: readTheme(readCookie(cookies, THEME_COOKIE)),
Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today107 inviteOnly: mode !== "open",
108 addresses: addresses(),
109 // Visitors from where the law asks first are asked before analytics runs.
110 analyticsConsent: visitorAsksFirst(request),
111 };
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put112}
113
114/**
115 * The sidebar for someone not signed in: nothing of their own, only the
116 * open counts of the project being looked at. One call, and only in a
117 * project; it answers the same for a private project as a missing one.
118 */
Fast pages, required checks on the branch, self-hosted runners, honest incidents119async 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 put120 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
Fast pages, required checks on the branch, self-hosted runners, honest incidents121 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 put122 return {
123 workspace: null,
124 repos: [],
125 repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null,
126 limit: null,
127 monthUsageMicros: null,
128 };
Automations: rules in .g1t/automations that act when something happens129}
130
Usage, Billing settings and prepaid AI credit; fixes from the UX audit131/** The sidebar from what the session already says, with no service asked. */
132function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData {
133 return {
134 workspace: workspaceFor(user.workspaces ?? [], chosen, params),
135 repos: [],
136 repo: null,
137 limit: null,
138 monthUsageMicros: null,
139 };
140}
141
Automations: rules in .g1t/automations that act when something happens142/**
143 * The sidebar only changes with the workspace or repository being looked
144 * at, or after something was submitted: not on every page within them.
145 */
146export function shouldRevalidate({
147 currentParams,
148 nextParams,
149 formMethod,
150 defaultShouldRevalidate,
151}: ShouldRevalidateFunctionArgs) {
152 if (formMethod && formMethod !== "GET") return defaultShouldRevalidate;
153 return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo;
154}
155
156/**
Usage, Billing settings and prepaid AI credit; fixes from the UX audit157 * The sidebar: the workspace whose pages or project these are when it is
158 * one of yours, else the one you chose (lib/workspace-choice.ts); its
159 * projects; and the repository being looked at.
Automations: rules in .g1t/automations that act when something happens160 */
161async function shellFor(
162 user: User,
163 params: { owner?: string; repo?: string },
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put164 chosen: string | null,
Fast pages, required checks on the branch, self-hosted runners, honest incidents165 context: Route.LoaderArgs["context"],
Automations: rules in .g1t/automations that act when something happens166): Promise<ShellData> {
167 const memberships = user.workspaces ?? [];
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put168 const workspace = workspaceFor(memberships, chosen, params);
Automations: rules in .g1t/automations that act when something happens169 const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null;
170 const now = new Date();
171 const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
Fast pages, required checks on the branch, self-hosted runners, honest incidents172 // What the sidebar shows of the workspace changes rarely: kept for a few
173 // seconds per person and workspace, except just after they changed
174 // something (lib/cache.server.ts).
175 const kept = <T,>(what: string, load: () => Promise<T>) =>
176 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.177 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 sidebar178 // Every project, for the palette and the count; the sidebar lists only
179 // the person's pinned and recent ones (lib/pins.ts).
180 workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents181 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 processor182 // Whether billing is on, without reading the account: that asks the
183 // card processor about the workspace's cards, too slow for every page.
184 kept("status", () => billing.status()).catch(() => null),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit185 kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null),
Fast pages, required checks on the branch, self-hosted runners, honest incidents186 kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null),
187 kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null),
188 sharedRepos(user),
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all189 // The bell's count: one query, never kept, so marking an item shows at once.
190 inbox.counts(user.username).catch(() => null),
Navigation: the workspace's own page with tabs, pins and recent in the sidebar191 // Never kept: opening a project moves it up Recent.
192 workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null),
Chat and workspace agents: channels, DMs and named agents you talk to193 // The rail's Chat badge: never kept, and never waited on for long.
194 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)195 // Home's Recent and the Agents sidebar, on every page: never waited on for long.
196 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.197 // The dock's pins, kept with the account so every device shows the same
198 // dock; never kept here, so a pin shows the moment it is made. Null
199 // falls back to the cookie (lib/dock.server.ts).
200 workspace ? savedPins(user, workspace.slug) : Promise.resolve(null),
Automations: rules in .g1t/automations that act when something happens201 ]);
202 return {
203 workspace,
Projects: what a workspace builds and runs, first on every page204 // 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 sidebar205 repos: listed?.ok ? listed.value.map(shortcutOf) : [],
206 pinned: (shortcuts?.pinned ?? []).map(shortcutOf),
207 recent: (shortcuts?.recent ?? []).map(shortcutOf),
Automations: rules in .g1t/automations that act when something happens208 repo:
209 path && counts?.ok
210 ? {
211 ...path,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look212 // Until the repository's own page says what their role is.
213 member: hasAccessIn(user, path.namespace),
Automations: rules in .g1t/automations that act when something happens214 issues: counts.value.issues,
215 pulls: counts.value.pulls,
216 }
217 : null,
Billing accounts, terms and enterprises; g1t is no longer free218 // Where the workspace stands against its usage limit, once billing is on.
219 limit:
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor220 status?.enabled && limit?.ok
Billing accounts, terms and enterprises; g1t is no longer free221 ? {
222 exposureMicros: limit.value.exposureMicros,
223 ceilingMicros: limit.value.ceilingMicros,
224 state: limit.value.state,
225 comped: limit.value.trust === "internal",
226 }
227 : null,
The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor228 free: Boolean(status?.free),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look229 // A spend spike or a hold pauses new compute: the shell says so on every page.
230 compute:
231 workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open")
232 ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" }
233 : null,
234 shared,
Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all235 inbox: unread,
Chat and workspace agents: channels, DMs and named agents you talk to236 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)237 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.238 pins: dockPins ?? undefined,
Usage while free is shown at cost; agents get rustfmt and clippy239 // 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 audit240 // Usage at price, the one figure every page shows.
241 monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null,
Automations: rules in .g1t/automations that act when something happens242 };
243}
244
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)245/** Starred and recent conversations Home's sidebar lists. */
246const SHELL_CONVERSATIONS = 8;
247
248/**
249 * What is unread in chat, for the rail, and the starred and latest
250 * conversations, for Home's sidebar; null when chat is slow to answer or
251 * does not.
252 */
Chat and workspace agents: channels, DMs and named agents you talk to253async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> {
254 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
255 const read = chat
256 .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)257 .then((result) => {
258 if (!result.ok) return null;
259 const entries = result.value.entries;
260 const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? ""));
261 return {
262 ...unreadTotals(entries),
263 starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS),
264 recent: latest.slice(0, SHELL_CONVERSATIONS),
265 };
266 })
267 .catch(() => null);
268 return Promise.race([read, timeout]);
269}
270
271/** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */
272async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> {
273 const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS));
274 const read = workspaceAgents
275 .list(slug, user)
276 .then((result) =>
277 result.ok
278 ? result.value
279 .filter((agent) => !agent.archived_at)
280 .map((agent) => ({
281 id: agent.id,
282 handle: agent.handle,
283 display_name: agent.display_name,
284 avatar: agent.avatar,
285 avatar_seed: agent.avatar_seed,
286 role: agent.role,
287 title: agent.title,
288 team: agent.team,
289 department: agent.department,
290 status: agent.status,
291 builtin: agent.builtin === true,
292 }))
293 : null,
294 )
Chat and workspace agents: channels, DMs and named agents you talk to295 .catch(() => null);
296 return Promise.race([read, timeout]);
297}
298
299/** The longest the rail's Chat badge holds up a page. */
300const CHAT_BADGE_WAIT_MS = 300;
301
Usage, Billing settings and prepaid AI credit; fixes from the UX audit302/**
303 * How long the sidebar's workspace answers are kept: as long as someone's
304 * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`).
305 */
306const SHELL_TTL_MS = 30_000;
Fast pages, required checks on the branch, self-hosted runners, honest incidents307
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look308/** Most repositories listed under Shared with you. */
309const MAX_SHARED = 20;
310
311/**
312 * Repositories shared with the user in workspaces they do not belong to,
313 * by their grants: only those, never the workspace's other repositories.
314 */
315async function sharedRepos(user: User): Promise<ShellData["shared"]> {
316 const outside = new Set(sharedWorkspaces(user));
317 if (outside.size === 0) return [];
318 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 incidents319 const found = await readableRepos(ids, user);
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look320 return found
Fast pages, required checks on the branch, self-hosted runners, honest incidents321 .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 look322 .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`));
323}
324
Usage, Billing settings and prepaid AI credit; fixes from the UX audit325/**
326 * The root's data as the browser last had it. When a navigation fails in a
327 * way that takes the root's data with it, the error is still drawn in the
328 * person's own frame, signed in, not the visitor's. Never kept on the
329 * server, where one module serves everyone.
330 */
331let lastRoot: Awaited<ReturnType<typeof loader>> | undefined;
332
Automations: rules in .g1t/automations that act when something happens333export 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 address334 const nonce = useNonce();
Automations: rules in .g1t/automations that act when something happens335 // Undefined when the root loader itself failed.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit336 const loaded = useRouteLoaderData<typeof loader>("root");
337 const inBrowser = typeof document !== "undefined";
338 if (loaded && inBrowser) lastRoot = loaded;
339 const root = loaded ?? (inBrowser ? lastRoot : undefined);
Automations: rules in .g1t/automations that act when something happens340 const user = root?.user;
Merge branch 'socket-tickets'341 // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts).
342 if (inBrowser) setLiveViaToken(Boolean(user?.token?.website));
Merge branch 'worktree-agent-a985c01f43538af01'343 const { pathname, search } = useLocation();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas344 // Drawn around the error page too: a 404 keeps the sidebar out of a
345 // project or workspace the viewer cannot see.
346 const error = useRouteError();
347 const missing = isRouteErrorResponse(error) && error.status === 404;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look348 // The billing page shows the full banner itself.
349 const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && (
Fast pages, required checks on the branch, self-hosted runners, honest incidents350 <SpikeBanner
351 compact
352 slug={root.shell.workspace.slug}
353 entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }}
354 owner={root.shell.compute.owner}
355 />
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look356 );
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)357 // On the few pages an account still confirming its address can open
358 // besides /confirm-email (policies, support, signing in): the way back.
359 const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && (
360 <p
361 role="status"
Automations: rules in .g1t/automations that act when something happens362 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"
363 >
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)364 <span>Confirm your email address to start using g1t.</span>
365 <Link to="/confirm-email" className="font-medium underline underline-offset-4">
366 Enter your code
367 </Link>
368 </p>
Automations: rules in .g1t/automations that act when something happens369 );
Merge main (membership, two-factor, GitHub repo roles) into tokens370 // A workspace that requires two-factor authentication of someone who
371 // has not turned it on: they keep their place, and cannot use it yet.
372 const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />;
Merge branch 'worktree-agent-aca4790ea223aa235'373 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.374 // Auto, Light or Dark: as the cookie said when the page was drawn, or as just switched.
375 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.376 // Which frame the page is drawn in (lib/chrome.ts): on its own, the app's, or the public one.
377 const frame = frameOf(pathname, {
378 signedIn: user != null && !awaitsConfirmation(user),
379 workspace: (user?.workspaces ?? []).length > 0,
380 });
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look381 const banner =
Merge main (membership, two-factor, GitHub repo roles) into tokens382 paused || verify || held ? (
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look383 <>
384 {paused}
385 {verify}
Merge main (membership, two-factor, GitHub repo roles) into tokens386 {held}
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look387 </>
388 ) : null;
Automations: rules in .g1t/automations that act when something happens389 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.390 <html lang="en" data-theme={themeAttribute(theme)}>
Automations: rules in .g1t/automations that act when something happens391 <head>
392 <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)393 <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
You choose how g1t looks: Auto follows your system, Light and Dark stay put, set from the account menu, the phone's You and help sheet, Settings → Account or the command palette; the page is drawn in your choice from the first byte, every colour has a light value that reads at 4.5:1 or better, code and diffs are highlighted for both, diagrams and the document editor follow along, the product tour and the g1t mark's avatar stay dark, and Dark is what you see until you choose; the workspaces guide says how.394 {/* The browser bar matches the page: one colour for a chosen theme, the system's for Auto. */}
395 {theme === "auto" ? (
396 <>
397 <meta name="theme-color" media="(prefers-color-scheme: light)" content={THEME_COLOR.light} />
398 <meta name="theme-color" media="(prefers-color-scheme: dark)" content={THEME_COLOR.dark} />
399 </>
400 ) : (
401 <meta name="theme-color" content={THEME_COLOR[theme]} />
402 )}
Merge branch 'mirroring' into artifacts-mode403 <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)404 <meta name="apple-mobile-web-app-capable" content="yes" />
405 <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 preloads406 {/* 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.407 connection paints sooner. */}
Give the stylesheet a precedence so React places it before preloads408 <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 today409 {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />}
Automations: rules in .g1t/automations that act when something happens410 <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 address411 <Links nonce={nonce} />
Automations: rules in .g1t/automations that act when something happens412 </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.413 <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 audit414 {/* The first stop for the keyboard: past the menus, to the page. */}
415 <a
416 href="#content"
417 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]"
418 >
419 Skip to content
420 </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.421 {frame === "standalone" ? (
422 <StandaloneFrame>{children}</StandaloneFrame>
423 ) : frame === "app" && user && root?.shell ? (
424 <AppShell user={user} shell={root.shell} missing={missing} banner={banner} sidebarClosed={root.sidebarClosed}>
Automations: rules in .g1t/automations that act when something happens425 {children}
426 </AppShell>
427 ) : (
428 <>
429 <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.430 <PublicHeader user={user} />
Automations: rules in .g1t/automations that act when something happens431 {banner}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit432 <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}>
Merge branch 'worktree-agent-aca4790ea223aa235'433 {children}
434 </div>
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look435 <SiteFooter user={user} />
Automations: rules in .g1t/automations that act when something happens436 </>
437 )}
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)438 {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */}
439 {user && !awaitsConfirmation(user) && (
440 <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} />
441 )}
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 today442 <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 address443 <ScrollRestoration nonce={nonce} />
444 <Scripts nonce={nonce} />
Automations: rules in .g1t/automations that act when something happens445 </body>
446 </html>
447 );
448}
449
450export 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 today451 const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null;
452 // Tell analytics who is signed in, once per change; signing out forgets them.
453 const lastUserId = useRef<string | null>(null);
454 useEffect(() => {
455 identify(userId, lastUserId.current);
456 lastUserId.current = userId;
457 }, [userId]);
Automations: rules in .g1t/automations that act when something happens458 return <Outlet />;
459}
460
461export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit462 const location = useLocation();
463 const matches = useMatches();
Merge branch 'worktree-agent-a64e51c8f386f46ac'464 const href = location.pathname + location.search + location.hash;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit465 // A tab left open across a deploy: load the address again as a whole
Merge branch 'worktree-agent-a64e51c8f386f46ac'466 // page, once, rather than show an error the reload fixes. A document load
467 // never reloads, so the server and the hydrating client render the same.
468 const reloadable = reloadFixes({
Usage, Billing settings and prepaid AI credit; fixes from the UX audit469 error,
470 status: isRouteErrorResponse(error) ? error.status : undefined,
Merge branch 'worktree-agent-a64e51c8f386f46ac'471 clientNavigation: clientNavigated(location.key),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit472 caughtByCatchAll: matches.at(-1)?.id === "routes/not-found",
473 });
Merge branch 'worktree-agent-a64e51c8f386f46ac'474 // Already loaded again once, or the reload never started: show the page.
475 const tried = useMemo(() => reloadable && reloadedBefore(href), [reloadable, href]);
476 const [gaveUp, setGaveUp] = useState<string | null>(null);
477 const reload = reloadable && !tried && gaveUp !== href;
Usage, Billing settings and prepaid AI credit; fixes from the UX audit478 useEffect(() => {
479 if (!reload) return;
480 try {
481 sessionStorage.setItem(RELOADED_KEY, href);
482 } catch {
483 // No session storage: reload anyway; a document load never asks again.
484 }
485 window.location.assign(href);
Merge branch 'worktree-agent-a64e51c8f386f46ac'486 const timer = window.setTimeout(() => setGaveUp(href), RELOAD_GIVE_UP_MS);
487 return () => window.clearTimeout(timer);
Usage, Billing settings and prepaid AI credit; fixes from the UX audit488 }, [reload, href]);
489 if (reload) {
490 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in491 <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 audit492 <title>Loading · g1t</title>
493 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 in494 </PageMain>
Usage, Billing settings and prepaid AI credit; fixes from the UX audit495 );
496 }
497
Automations: rules in .g1t/automations that act when something happens498 let title = "Something went wrong";
499 let details = "An unexpected error occurred. Try again in a moment.";
500 let stack: string | undefined;
501
502 if (isRouteErrorResponse(error)) {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put503 // The same page for something private and something missing.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit504 if (error.status === 404) {
505 return (
506 <>
507 <title>Not found · g1t</title>
508 <NotFound data={error.data} />
509 </>
510 );
511 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put512 if (typeof error.data === "string" && error.data) {
Automations: rules in .g1t/automations that act when something happens513 details = error.data;
514 }
Merge main into Artifacts Phase 2515 // A token asked for what needs a real sign-in (lib/website-token.ts).
516 if (isNeedsSignIn(error.data)) {
517 title = "This needs you to sign in";
518 details = error.data.message;
519 }
Automations: rules in .g1t/automations that act when something happens520 } else if (import.meta.env.DEV && error instanceof Error) {
521 details = error.message;
522 stack = error.stack;
523 }
524
525 return (
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in526 <PageMain className="mx-auto max-w-xl px-4 py-32 text-center">
Automations: rules in .g1t/automations that act when something happens527 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
528 <p className="mt-3 text-muted">{details}</p>
529 <div className="mt-8">
530 <ButtonLink to="/" variant="quiet">
531 Back to home
532 </ButtonLink>
533 </div>
534 {stack && (
535 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
536 <code>{stack}</code>
537 </pre>
538 )}
Not found and error pages have one main landmark: their own when signed out, the shell's when signed in539 </PageMain>
Automations: rules in .g1t/automations that act when something happens540 );
541}

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