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.
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 1 | import { |
| 2 | BookOpen, | |
| 3 | ChevronDown, | |
| Search across all of g1t, Explore, and a command palette | 4 | Compass, |
| 5 | CreditCard, | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 6 | LayoutDashboard, |
| Search across all of g1t, Explore, and a command palette | 7 | LogIn, |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 8 | LogOut, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 9 | Menu, |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 10 | Plus, |
| 11 | Search, | |
| 12 | Settings, | |
| 13 | } from "lucide-react"; | |
| 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 today | 14 | import { useEffect, useRef, useState } from "react"; |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 15 | import { |
| 16 | Form, | |
| 17 | isRouteErrorResponse, | |
| 18 | Link, | |
| 19 | Links, | |
| 20 | Meta, | |
| 21 | NavLink, | |
| 22 | Outlet, | |
| 23 | Scripts, | |
| 24 | ScrollRestoration, | |
| 25 | type ShouldRevalidateFunctionArgs, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 26 | useLocation, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 27 | useMatches, |
| Search across all of g1t, Explore, and a command palette | 28 | useParams, |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 29 | useRouteError, |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 30 | useRouteLoaderData, |
| 31 | useSubmit, | |
| 32 | } from "react-router"; | |
| 33 | ||
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 34 | import { type User, awaitsConfirmation, hasAccessIn, sharedWorkspaces } from "@g1t/contracts"; |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 35 | |
| 36 | import type { Route } from "./+types/root"; | |
| Load the stylesheet first; plain legal contact details | 37 | import appCss from "./app.css?url"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 38 | import displayFont from "@g1t/theme/fonts/bricolage-grotesque-latin.woff2?url"; |
| 39 | import sansFont from "@g1t/theme/fonts/hanken-grotesk-latin.woff2?url"; | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 40 | import { Logo } from "./components/logo"; |
| Merge branch 'worktree-agent-a985c01f43538af01' | 41 | import { Avatar, ButtonLink, SubmitButton, notACredential } from "./components/ui"; |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 42 | import { |
| 43 | DropdownMenu, | |
| 44 | DropdownMenuContent, | |
| 45 | DropdownMenuItem, | |
| 46 | DropdownMenuLabel, | |
| 47 | DropdownMenuSeparator, | |
| 48 | DropdownMenuTrigger, | |
| 49 | } from "./components/ui/dropdown-menu"; | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 50 | import { AppShell, Progress, type ShellData, useLeaving } from "./components/shell"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 51 | import { SiteFooter } from "./components/footer"; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 52 | import { SpikeBanner } from "./components/spike-banner"; |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 53 | import { 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 today | 54 | import { identify } from "./lib/analytics.client"; |
| 55 | import { visitorAsksFirst } from "./lib/analytics-consent"; | |
| 56 | import { AnalyticsConsent } from "./components/analytics-consent"; | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 57 | import { readCookie } from "./lib/mission"; |
| 58 | import { 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 in | 59 | import { PageMain } from "./components/landmark"; |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 60 | import { NotFound } from "./components/not-found"; |
| 61 | import { usesAppShell } from "./lib/chrome"; | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 62 | import { CommandPalette, type PaletteCommand, PaletteKey, usePaletteShortcut } from "./components/command-palette"; |
| 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) | 63 | import { billing, chat, inbox, projects, workspaceAgents } from "./lib/services.server"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 64 | import { unreadTotals } from "./lib/chat"; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 65 | import { countsFor, readableRepos } from "./lib/access.server"; |
| 66 | import { shortCache } from "./lib/cache.server"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 67 | import { getViewer, viewerMiddleware } from "./lib/session.server"; |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 68 | import { shortcutOf, workspaceProjects } from "./lib/workspace-projects.server"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 69 | import { registrationMode } from "./lib/registration.server"; |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 70 | import { addresses } from "./lib/addresses.server"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 71 | import { useSignUpCopy } from "./lib/registration"; |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 72 | import { RELOADED_KEY, reloadFixes } 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 address | 73 | import { useNonce } from "./lib/nonce"; |
| Merge main into Artifacts Phase 2 | 74 | import { isNeedsSignIn } from "./lib/website-token"; |
| A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works. | 75 | import { 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) | 76 | import { LiveNotifications } from "./components/notifications/live-notifications"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 77 | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 78 | |
| 79 | export const links: Route.LinksFunction = () => [ | |
| The fleet: g1t's new mark | 80 | { rel: "icon", href: "/favicon.ico", sizes: "32x32" }, |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 81 | { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }, |
| The fleet: g1t's new mark | 82 | { 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 managers | 83 | { rel: "icon", type: "image/png", sizes: "512x512", href: "/icon-512.png" }, |
| 84 | // Home screens and password managers look for a square, opaque 180px tile; | |
| 85 | // the "-precomposed" name is served too, for the ones that ask for it. | |
| 86 | { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" }, | |
| 87 | // Installable: name, icons and colours for "Add to Home Screen" (public/site.webmanifest; | |
| 88 | // manifest.webmanifest is the same file, kept for installs that already point at it). | |
| 89 | { rel: "manifest", href: "/site.webmanifest" }, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 90 | // The text and headline faces are wanted on every page, so they start |
| 91 | // loading with the stylesheet; mono waits until something uses it. | |
| 92 | { rel: "preload", href: sansFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, | |
| 93 | { rel: "preload", href: displayFont, as: "font", type: "font/woff2", crossOrigin: "anonymous" }, | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 94 | ]; |
| 95 | ||
| 96 | export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware]; | |
| 97 | ||
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 98 | export async function loader({ context, params, request }: Route.LoaderArgs) { |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 99 | const user = getViewer(context); |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 100 | const chosen = readCookie(request.headers.get("cookie"), WORKSPACE_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) | 101 | // Whether sign-up takes an invite: the sign-up page says so, and |
| 102 | // 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 look | 103 | const [shell, mode] = await Promise.all([ |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 104 | // An account still confirming its address sees only the pages that |
| 105 | // allows (lib/confirm-gate.ts), in the visitor's frame. | |
| 106 | user && !awaitsConfirmation(user) | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 107 | ? shellFor(user, params, chosen, context).catch((error: unknown) => { |
| 108 | // A sidebar that could not be read is no reason to lose the page, | |
| 109 | // or to draw it as if they were signed out. | |
| 110 | console.error("root: the sidebar could not be read", error); | |
| 111 | return bareShell(user, params, chosen); | |
| 112 | }) | |
| 113 | : 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) | 114 | registrationMode(), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 115 | ]); |
| Merge branch 'worktree-agent-aaf03bdceac799c89' | 116 | // 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 today | 117 | return { |
| 118 | user, | |
| 119 | shell, | |
| 120 | inviteOnly: mode !== "open", | |
| 121 | addresses: addresses(), | |
| 122 | // Visitors from where the law asks first are asked before analytics runs. | |
| 123 | analyticsConsent: visitorAsksFirst(request), | |
| 124 | }; | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 125 | } |
| 126 | ||
| 127 | /** | |
| 128 | * The sidebar for someone not signed in: nothing of their own, only the | |
| 129 | * open counts of the project being looked at. One call, and only in a | |
| 130 | * project; it answers the same for a private project as a missing one. | |
| 131 | */ | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 132 | async 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 put | 133 | const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null; |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 134 | 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 put | 135 | return { |
| 136 | workspace: null, | |
| 137 | repos: [], | |
| 138 | repo: path && counts?.ok ? { ...path, member: false, issues: counts.value.issues, pulls: counts.value.pulls } : null, | |
| 139 | limit: null, | |
| 140 | monthUsageMicros: null, | |
| 141 | }; | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 142 | } |
| 143 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 144 | /** The sidebar from what the session already says, with no service asked. */ |
| 145 | function bareShell(user: User, params: { owner?: string; repo?: string }, chosen: string | null): ShellData { | |
| 146 | return { | |
| 147 | workspace: workspaceFor(user.workspaces ?? [], chosen, params), | |
| 148 | repos: [], | |
| 149 | repo: null, | |
| 150 | limit: null, | |
| 151 | monthUsageMicros: null, | |
| 152 | }; | |
| 153 | } | |
| 154 | ||
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 155 | /** |
| 156 | * The sidebar only changes with the workspace or repository being looked | |
| 157 | * at, or after something was submitted: not on every page within them. | |
| 158 | */ | |
| 159 | export function shouldRevalidate({ | |
| 160 | currentParams, | |
| 161 | nextParams, | |
| 162 | formMethod, | |
| 163 | defaultShouldRevalidate, | |
| 164 | }: ShouldRevalidateFunctionArgs) { | |
| 165 | if (formMethod && formMethod !== "GET") return defaultShouldRevalidate; | |
| 166 | return currentParams.owner !== nextParams.owner || currentParams.repo !== nextParams.repo; | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 170 | * The sidebar: the workspace whose pages or project these are when it is |
| 171 | * one of yours, else the one you chose (lib/workspace-choice.ts); its | |
| 172 | * projects; and the repository being looked at. | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 173 | */ |
| 174 | async function shellFor( | |
| 175 | user: User, | |
| 176 | params: { owner?: string; repo?: string }, | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 177 | chosen: string | null, |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 178 | context: Route.LoaderArgs["context"], |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 179 | ): Promise<ShellData> { |
| 180 | const memberships = user.workspaces ?? []; | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 181 | const workspace = workspaceFor(memberships, chosen, params); |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 182 | const path = params.owner && params.repo ? { namespace: params.owner, name: params.repo } : null; |
| 183 | const now = new Date(); | |
| 184 | const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString(); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 185 | // What the sidebar shows of the workspace changes rarely: kept for a few |
| 186 | // seconds per person and workspace, except just after they changed | |
| 187 | // something (lib/cache.server.ts). | |
| 188 | const kept = <T,>(what: string, load: () => Promise<T>) => | |
| 189 | workspace ? shortCache(`shell:${what}:${user.id}:${workspace.slug}`, SHELL_TTL_MS, load) : 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) | 190 | const [listed, counts, status, usage, limit, entitlements, shared, unread, shortcuts, chatUnread, teamAgents] = await Promise.all([ |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 191 | // Every project, for the palette and the count; the sidebar lists only |
| 192 | // the person's pinned and recent ones (lib/pins.ts). | |
| 193 | workspace ? workspaceProjects(workspace.slug, user) : Promise.resolve(null), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 194 | 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 processor | 195 | // Whether billing is on, without reading the account: that asks the |
| 196 | // card processor about the workspace's cards, too slow for every page. | |
| 197 | kept("status", () => billing.status()).catch(() => null), | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 198 | kept(`usage:${monthStart}`, () => billing.usage(workspace!.slug, user, monthStart)).catch(() => null), |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 199 | kept("limit", () => billing.limit(workspace!.slug, user)).catch(() => null), |
| 200 | kept("entitlements", () => billing.entitlements(workspace!.slug)).catch(() => null), | |
| 201 | sharedRepos(user), | |
| Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all | 202 | // The bell's count: one query, never kept, so marking an item shows at once. |
| 203 | inbox.counts(user.username).catch(() => null), | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 204 | // Never kept: opening a project moves it up Recent. |
| 205 | workspace ? projects.shortcuts(workspace.slug, user).catch(() => null) : Promise.resolve(null), | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 206 | // The rail's Chat badge: never kept, and never waited on for long. |
| 207 | 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) | 208 | // Home's Recent and the Agents sidebar, on every page: never waited on for long. |
| 209 | workspace ? agentsFor(workspace.slug, user) : Promise.resolve(null), | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 210 | ]); |
| 211 | return { | |
| 212 | workspace, | |
| Projects: what a workspace builds and runs, first on every page | 213 | // 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 sidebar | 214 | repos: listed?.ok ? listed.value.map(shortcutOf) : [], |
| 215 | pinned: (shortcuts?.pinned ?? []).map(shortcutOf), | |
| 216 | recent: (shortcuts?.recent ?? []).map(shortcutOf), | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 217 | repo: |
| 218 | path && counts?.ok | |
| 219 | ? { | |
| 220 | ...path, | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 221 | // Until the repository's own page says what their role is. |
| 222 | member: hasAccessIn(user, path.namespace), | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 223 | issues: counts.value.issues, |
| 224 | pulls: counts.value.pulls, | |
| 225 | } | |
| 226 | : null, | |
| Billing accounts, terms and enterprises; g1t is no longer free | 227 | // Where the workspace stands against its usage limit, once billing is on. |
| 228 | limit: | |
| The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor | 229 | status?.enabled && limit?.ok |
| Billing accounts, terms and enterprises; g1t is no longer free | 230 | ? { |
| 231 | exposureMicros: limit.value.exposureMicros, | |
| 232 | ceilingMicros: limit.value.ceilingMicros, | |
| 233 | state: limit.value.state, | |
| 234 | comped: limit.value.trust === "internal", | |
| 235 | } | |
| 236 | : null, | |
| The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor | 237 | free: Boolean(status?.free), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 238 | // A spend spike or a hold pauses new compute: the shell says so on every page. |
| 239 | compute: | |
| 240 | workspace && entitlements && (entitlements.paused || entitlements.spike?.status === "open") | |
| 241 | ? { paused: entitlements.paused, spike: entitlements.spike ?? null, owner: workspace.role === "owner" } | |
| 242 | : null, | |
| 243 | shared, | |
| Inbox: a bell in the top bar opens it beside the page, and /inbox holds it all | 244 | inbox: unread, |
| Chat and workspace agents: channels, DMs and named agents you talk to | 245 | 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) | 246 | agents: teamAgents, |
| Usage while free is shown at cost; agents get rustfmt and clippy | 247 | // 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 audit | 248 | // Usage at price, the one figure every page shows. |
| 249 | monthUsageMicros: usage?.ok ? (usage.value.free ? usage.value.usedMicros : (usage.value.priceMicros ?? usage.value.spentMicros)) : null, | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 250 | }; |
| 251 | } | |
| 252 | ||
| 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) | 253 | /** Starred and recent conversations Home's sidebar lists. */ |
| 254 | const SHELL_CONVERSATIONS = 8; | |
| 255 | ||
| 256 | /** | |
| 257 | * What is unread in chat, for the rail, and the starred and latest | |
| 258 | * conversations, for Home's sidebar; null when chat is slow to answer or | |
| 259 | * does not. | |
| 260 | */ | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 261 | async function chatUnreadFor(slug: string, user: User): Promise<ShellData["chat"]> { |
| 262 | const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS)); | |
| 263 | const read = chat | |
| 264 | .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) | 265 | .then((result) => { |
| 266 | if (!result.ok) return null; | |
| 267 | const entries = result.value.entries; | |
| 268 | const latest = [...entries].sort((a, b) => (b.channel.last_message_at ?? "").localeCompare(a.channel.last_message_at ?? "")); | |
| 269 | return { | |
| 270 | ...unreadTotals(entries), | |
| 271 | starred: entries.filter((e) => e.starred).slice(0, SHELL_CONVERSATIONS), | |
| 272 | recent: latest.slice(0, SHELL_CONVERSATIONS), | |
| 273 | }; | |
| 274 | }) | |
| 275 | .catch(() => null); | |
| 276 | return Promise.race([read, timeout]); | |
| 277 | } | |
| 278 | ||
| 279 | /** The workspace's agents, as the shell lists them; null when the agents service is slow or down. */ | |
| 280 | async function agentsFor(slug: string, user: User): Promise<ShellData["agents"]> { | |
| 281 | const timeout = new Promise<null>((resolve) => setTimeout(() => resolve(null), CHAT_BADGE_WAIT_MS)); | |
| 282 | const read = workspaceAgents | |
| 283 | .list(slug, user) | |
| 284 | .then((result) => | |
| 285 | result.ok | |
| 286 | ? result.value | |
| 287 | .filter((agent) => !agent.archived_at) | |
| 288 | .map((agent) => ({ | |
| 289 | id: agent.id, | |
| 290 | handle: agent.handle, | |
| 291 | display_name: agent.display_name, | |
| 292 | avatar: agent.avatar, | |
| 293 | avatar_seed: agent.avatar_seed, | |
| 294 | role: agent.role, | |
| 295 | title: agent.title, | |
| 296 | team: agent.team, | |
| 297 | department: agent.department, | |
| 298 | status: agent.status, | |
| 299 | builtin: agent.builtin === true, | |
| 300 | })) | |
| 301 | : null, | |
| 302 | ) | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 303 | .catch(() => null); |
| 304 | return Promise.race([read, timeout]); | |
| 305 | } | |
| 306 | ||
| 307 | /** The longest the rail's Chat badge holds up a page. */ | |
| 308 | const CHAT_BADGE_WAIT_MS = 300; | |
| 309 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 310 | /** |
| 311 | * How long the sidebar's workspace answers are kept: as long as someone's | |
| 312 | * own changes read fresh anyway (lib/perf.ts `PRIMARY_WINDOW_SECONDS`). | |
| 313 | */ | |
| 314 | const SHELL_TTL_MS = 30_000; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 315 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 316 | /** Most repositories listed under Shared with you. */ |
| 317 | const MAX_SHARED = 20; | |
| 318 | ||
| 319 | /** | |
| 320 | * Repositories shared with the user in workspaces they do not belong to, | |
| 321 | * by their grants: only those, never the workspace's other repositories. | |
| 322 | */ | |
| 323 | async function sharedRepos(user: User): Promise<ShellData["shared"]> { | |
| 324 | const outside = new Set(sharedWorkspaces(user)); | |
| 325 | if (outside.size === 0) return []; | |
| 326 | 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 incidents | 327 | const found = await readableRepos(ids, user); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 328 | return found |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 329 | .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 look | 330 | .sort((a, b) => `${a.namespace}/${a.name}`.localeCompare(`${b.namespace}/${b.name}`)); |
| 331 | } | |
| 332 | ||
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 333 | function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) { |
| 334 | return ( | |
| 335 | <NavLink | |
| 336 | to={to} | |
| 337 | className={({ isActive }) => | |
| 338 | `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${ | |
| 339 | isActive ? "text-fg" : "text-muted" | |
| 340 | }` | |
| 341 | } | |
| 342 | > | |
| 343 | {children} | |
| 344 | </NavLink> | |
| 345 | ); | |
| 346 | } | |
| 347 | ||
| Search across all of g1t, Explore, and a command palette | 348 | /** What the palette offers someone without the app's sidebar. */ |
| 349 | const PUBLIC_COMMANDS: PaletteCommand[] = [ | |
| 350 | { label: "Explore", hint: "Public projects", to: "/explore", icon: <Compass size={15} /> }, | |
| 351 | { label: "Search g1t", hint: "Repositories, code, issues, people", to: "/search", icon: <Search size={15} /> }, | |
| 352 | { label: "Pricing", to: "/pricing", icon: <CreditCard size={15} /> }, | |
| 353 | { label: "Documentation", to: "https://docs.g1t.sh/", icon: <BookOpen size={15} /> }, | |
| 354 | { label: "Sign in", to: "/login", icon: <LogIn size={15} /> }, | |
| 355 | { label: "Sign up", to: "/register", icon: <Plus size={15} /> }, | |
| 356 | ]; | |
| 357 | ||
| Sign-up buttons say Sign up everywhere; only the sign-up page says registration takes an invite | 358 | /** Sign up: the sign-up page says whether it takes an invite. */ |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 359 | function SignUpButton() { |
| 360 | const copy = useSignUpCopy(); | |
| 361 | return <ButtonLink to="/register">{copy.primary}</ButtonLink>; | |
| 362 | } | |
| 363 | ||
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 364 | function Header({ user }: { user: User | null | undefined }) { |
| 365 | const submit = useSubmit(); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 366 | const signUp = useSignUpCopy(); |
| Search across all of g1t, Explore, and a command palette | 367 | const [palette, setPalette] = useState(false); |
| 368 | usePaletteShortcut(() => setPalette((open) => !open)); | |
| 369 | const params = useParams(); | |
| 370 | const repo = params.owner && params.repo ? `${params.owner}/${params.repo}` : null; | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 371 | return ( |
| 372 | <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur"> | |
| 373 | <div className="mx-auto flex h-14 max-w-6xl items-center gap-2 px-4"> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 374 | <Link to="/" aria-label="g1t home" className="mr-2 flex"> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 375 | <Logo /> |
| 376 | </Link> | |
| 377 | <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs"> | |
| 378 | <Search | |
| 379 | size={14} | |
| 380 | className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint" | |
| 381 | /> | |
| 382 | <input | |
| 383 | name="q" | |
| 384 | {...notACredential()} | |
| Search across all of g1t, Explore, and a command palette | 385 | placeholder="Search g1t" |
| 386 | aria-label="Search g1t" | |
| 387 | className="w-full rounded-md border border-line bg-bg py-1.5 pr-12 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim" | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 388 | /> |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 389 | <PaletteKey className="pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 rounded bg-raised px-1.5 font-mono text-[0.625rem] text-muted ring-1 ring-line" /> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 390 | </Form> |
| Search across all of g1t, Explore, and a command palette | 391 | <button |
| 392 | type="button" | |
| 393 | aria-label="Search g1t" | |
| 394 | onClick={() => setPalette(true)} | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 395 | className="flex size-10 items-center justify-center rounded-md text-muted transition-colors hover:bg-raised hover:text-fg sm:hidden" |
| Search across all of g1t, Explore, and a command palette | 396 | > |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 397 | <Search size={18} /> |
| Search across all of g1t, Explore, and a command palette | 398 | </button> |
| 399 | <CommandPalette | |
| 400 | open={palette} | |
| 401 | onOpenChange={setPalette} | |
| 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) | 402 | commands={user ? [{ label: "Home", to: "/", icon: <LayoutDashboard size={15} /> }, ...PUBLIC_COMMANDS.slice(0, 4)] : PUBLIC_COMMANDS.map((command) => (command.to === "/register" ? { ...command, label: signUp.primary } : command))} |
| Search across all of g1t, Explore, and a command palette | 403 | repo={repo} |
| 404 | /> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 405 | <nav aria-label="Main" className="hidden items-center gap-0.5 sm:flex"> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 406 | <HeaderLink to="/explore">Explore</HeaderLink> |
| Prices keep themselves current with what g1t pays | 407 | <HeaderLink to="/pricing">Pricing</HeaderLink> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 408 | <HeaderLink to="https://docs.g1t.sh/">Docs</HeaderLink> |
| 409 | </nav> | |
| 410 | <div className="ml-auto flex items-center gap-2"> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 411 | {/* On a phone the links fold into one menu, so the bar fits. */} |
| 412 | <DropdownMenu> | |
| 413 | <DropdownMenuTrigger | |
| 414 | aria-label="Menu" | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 415 | className="flex size-10 items-center justify-center rounded-md text-muted outline-none transition-colors hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised data-[state=open]:text-fg sm:hidden" |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 416 | > |
| 417 | <Menu size={18} /> | |
| 418 | </DropdownMenuTrigger> | |
| 419 | <DropdownMenuContent align="end" className="w-52"> | |
| 420 | <DropdownMenuItem asChild className="min-h-11"> | |
| 421 | <Link to="/explore"> | |
| 422 | <Compass /> | |
| 423 | Explore | |
| 424 | </Link> | |
| 425 | </DropdownMenuItem> | |
| 426 | <DropdownMenuItem asChild className="min-h-11"> | |
| 427 | <Link to="/pricing"> | |
| 428 | <CreditCard /> | |
| 429 | Pricing | |
| 430 | </Link> | |
| 431 | </DropdownMenuItem> | |
| 432 | <DropdownMenuItem asChild className="min-h-11"> | |
| 433 | <Link to="https://docs.g1t.sh/"> | |
| 434 | <BookOpen /> | |
| 435 | Docs | |
| 436 | </Link> | |
| 437 | </DropdownMenuItem> | |
| 438 | {!user && ( | |
| 439 | <> | |
| 440 | <DropdownMenuSeparator /> | |
| 441 | <DropdownMenuItem asChild className="min-h-11"> | |
| 442 | <Link to="/login"> | |
| 443 | <LogIn /> | |
| 444 | Sign in | |
| 445 | </Link> | |
| 446 | </DropdownMenuItem> | |
| 447 | </> | |
| 448 | )} | |
| 449 | </DropdownMenuContent> | |
| 450 | </DropdownMenu> | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 451 | {user ? ( |
| 452 | <> | |
| 453 | <Link | |
| 454 | to="/new" | |
| Projects live: fixes from walking them end to end | 455 | aria-label="New project" |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 456 | className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg" |
| 457 | > | |
| 458 | <Plus size={16} /> | |
| 459 | </Link> | |
| 460 | <DropdownMenu> | |
| 461 | <DropdownMenuTrigger | |
| 462 | aria-label="Account menu" | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 463 | className="flex items-center gap-1.5 rounded-md p-1 outline-none transition-colors hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent data-[state=open]:bg-raised" |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 464 | > |
| Workspace names and icons, and a component kit for every control | 465 | <Avatar name={user.username} image={user.avatar} size={24} /> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 466 | <ChevronDown size={14} className="text-faint" /> |
| 467 | </DropdownMenuTrigger> | |
| 468 | <DropdownMenuContent align="end" className="w-56"> | |
| 469 | <DropdownMenuLabel> | |
| 470 | Signed in as{" "} | |
| 471 | <span className="font-mono font-medium text-fg"> | |
| 472 | {user.username} | |
| 473 | </span> | |
| 474 | </DropdownMenuLabel> | |
| 475 | <DropdownMenuSeparator /> | |
| 476 | {(user.workspaces ?? []).map((membership) => ( | |
| 477 | <DropdownMenuItem asChild key={membership.slug}> | |
| 478 | <Link to={`/${membership.slug}`}> | |
| Workspace names and icons, and a component kit for every control | 479 | <Avatar name={membership.slug} image={membership.avatar} size={16} square /> |
| 480 | <span className="min-w-0 truncate">{membership.name || membership.slug}</span> | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 481 | </Link> |
| 482 | </DropdownMenuItem> | |
| 483 | ))} | |
| 484 | <DropdownMenuItem asChild> | |
| 485 | <Link to="/workspaces/new"> | |
| 486 | <Plus /> | |
| 487 | New workspace | |
| 488 | </Link> | |
| 489 | </DropdownMenuItem> | |
| 490 | <DropdownMenuSeparator /> | |
| 491 | <DropdownMenuItem asChild> | |
| 492 | <Link to="/"> | |
| 493 | <LayoutDashboard /> | |
| 494 | Mission control | |
| 495 | </Link> | |
| 496 | </DropdownMenuItem> | |
| 497 | <DropdownMenuItem asChild> | |
| 498 | <Link to="/new"> | |
| 499 | <Plus /> | |
| Projects live: fixes from walking them end to end | 500 | New project |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 501 | </Link> |
| 502 | </DropdownMenuItem> | |
| 503 | <DropdownMenuSeparator /> | |
| 504 | <DropdownMenuItem asChild> | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 505 | <Link to="/settings/profile"> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 506 | <Settings /> |
| 507 | Settings | |
| 508 | </Link> | |
| 509 | </DropdownMenuItem> | |
| 510 | <DropdownMenuItem asChild> | |
| 511 | <Link to="https://docs.g1t.sh/quickstart/"> | |
| 512 | <BookOpen /> | |
| 513 | Documentation | |
| 514 | </Link> | |
| 515 | </DropdownMenuItem> | |
| 516 | <DropdownMenuSeparator /> | |
| 517 | {/* Submitted from here: the menu closes on select, and a button | |
| 518 | that has left the page cannot submit a form. */} | |
| 519 | <DropdownMenuItem | |
| 520 | onSelect={() => submit(null, { method: "post", action: "/logout" })} | |
| 521 | > | |
| 522 | <LogOut /> | |
| 523 | Sign out | |
| 524 | </DropdownMenuItem> | |
| 525 | </DropdownMenuContent> | |
| 526 | </DropdownMenu> | |
| 527 | </> | |
| 528 | ) : ( | |
| 529 | <> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 530 | <span className="hidden sm:contents"> |
| 531 | <HeaderLink to="/login">Sign in</HeaderLink> | |
| 532 | </span> | |
| 533 | <SignUpButton /> | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 534 | </> |
| 535 | )} | |
| 536 | </div> | |
| 537 | </div> | |
| 538 | </header> | |
| 539 | ); | |
| 540 | } | |
| 541 | ||
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 542 | /** |
| 543 | * The root's data as the browser last had it. When a navigation fails in a | |
| 544 | * way that takes the root's data with it, the error is still drawn in the | |
| 545 | * person's own frame, signed in, not the visitor's. Never kept on the | |
| 546 | * server, where one module serves everyone. | |
| 547 | */ | |
| 548 | let lastRoot: Awaited<ReturnType<typeof loader>> | undefined; | |
| 549 | ||
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 550 | export 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 address | 551 | const nonce = useNonce(); |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 552 | // Undefined when the root loader itself failed. |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 553 | const loaded = useRouteLoaderData<typeof loader>("root"); |
| 554 | const inBrowser = typeof document !== "undefined"; | |
| 555 | if (loaded && inBrowser) lastRoot = loaded; | |
| 556 | const root = loaded ?? (inBrowser ? lastRoot : undefined); | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 557 | const user = root?.user; |
| A page opened with an access token keeps its live sockets connected: just before it opens the feed, a conversation or an artifact's room, it asks GET /-/live/ticket with the token for a socket ticket and adds it to the socket's address, because a browser cannot put the Authorization header on a WebSocket. A ticket seals the token and its owner with a key derived from USERCONTENT_KEY, lasts 60 seconds, opens only the socket path it was made for, is read only by a WebSocket upgrade and never by a page, data request, form post or the API, and the token is checked again when the socket opens, so one deleted, expired, revoked or without Use the website as you opens nothing. Sessions open their sockets as before, with no ticket, and the authentication guide and the rate limits notes say how it works. | 558 | // A page opened with an access token signs its live sockets in with tickets (lib/live-socket.ts). |
| 559 | if (inBrowser) setLiveViaToken(Boolean(user?.token?.website)); | |
| Merge branch 'worktree-agent-a985c01f43538af01' | 560 | const { pathname, search } = useLocation(); |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 561 | // Drawn around the error page too: a 404 keeps the sidebar out of a |
| 562 | // project or workspace the viewer cannot see. | |
| 563 | const error = useRouteError(); | |
| 564 | const missing = isRouteErrorResponse(error) && error.status === 404; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 565 | // The billing page shows the full banner itself. |
| 566 | const paused = root?.shell?.compute && root.shell.workspace && !pathname.endsWith("/-/billing") && ( | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 567 | <SpikeBanner |
| 568 | compact | |
| 569 | slug={root.shell.workspace.slug} | |
| 570 | entitlements={{ paused: root.shell.compute.paused, spike: root.shell.compute.spike }} | |
| 571 | owner={root.shell.compute.owner} | |
| 572 | /> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 573 | ); |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 574 | // On the few pages an account still confirming its address can open |
| 575 | // besides /confirm-email (policies, support, signing in): the way back. | |
| 576 | const verify = user && awaitsConfirmation(user) && pathname !== "/confirm-email" && ( | |
| 577 | <p | |
| 578 | role="status" | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 579 | 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" |
| 580 | > | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 581 | <span>Confirm your email address to start using g1t.</span> |
| 582 | <Link to="/confirm-email" className="font-medium underline underline-offset-4"> | |
| 583 | Enter your code | |
| 584 | </Link> | |
| 585 | </p> | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 586 | ); |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 587 | // A workspace that requires two-factor authentication of someone who |
| 588 | // has not turned it on: they keep their place, and cannot use it yet. | |
| 589 | const held = user?.held && user.held.length > 0 && pathname !== "/settings/two-factor" && <PolicyNotice held={user.held} />; | |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 590 | const leaving = useLeaving(); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 591 | const banner = |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 592 | paused || verify || held ? ( |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 593 | <> |
| 594 | {paused} | |
| 595 | {verify} | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 596 | {held} |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 597 | </> |
| 598 | ) : null; | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 599 | return ( |
| 600 | <html lang="en"> | |
| 601 | <head> | |
| 602 | <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) | 603 | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 604 | <meta name="theme-color" content="#0f0f11" /> |
| Merge branch 'mirroring' into artifacts-mode | 605 | <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) | 606 | <meta name="apple-mobile-web-app-capable" content="yes" /> |
| 607 | <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 preloads | 608 | {/* The stylesheet before everything React Router preloads, so a slow |
| 609 | connection paints sooner (docs/research/css-shipping.md). */} | |
| Give the stylesheet a precedence so React places it before preloads | 610 | <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 today | 611 | {root?.analyticsConsent && <meta name="g1t-analytics" content="consent" />} |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 612 | <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 address | 613 | <Links nonce={nonce} /> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 614 | </head> |
| 615 | <body className="flex min-h-screen flex-col"> | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 616 | {/* The first stop for the keyboard: past the menus, to the page. */} |
| 617 | <a | |
| 618 | href="#content" | |
| 619 | 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]" | |
| 620 | > | |
| 621 | Skip to content | |
| 622 | </a> | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 623 | {root?.shell && usesAppShell(pathname, user != null && !awaitsConfirmation(user)) ? ( |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 624 | <AppShell user={user ?? null} shell={root.shell} missing={missing} banner={banner}> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 625 | {children} |
| 626 | </AppShell> | |
| 627 | ) : ( | |
| 628 | <> | |
| 629 | <Progress /> | |
| 630 | <Header user={user} /> | |
| 631 | {banner} | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 632 | <div id="content" tabIndex={-1} {...leaving} className={`grow outline-none ${leaving.className}`}> |
| Merge branch 'worktree-agent-aca4790ea223aa235' | 633 | {children} |
| 634 | </div> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 635 | <SiteFooter user={user} /> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 636 | </> |
| 637 | )} | |
| 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) | 638 | {/* Live notifications for the signed-in: the feed socket, toasts, the tab's count (components/notifications). */} |
| 639 | {user && !awaitsConfirmation(user) && ( | |
| 640 | <LiveNotifications workspace={root?.shell?.workspace?.slug ?? null} inbox={root?.shell?.inbox?.unread ?? null} /> | |
| 641 | )} | |
| 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 today | 642 | <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 address | 643 | <ScrollRestoration nonce={nonce} /> |
| 644 | <Scripts nonce={nonce} /> | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 645 | </body> |
| 646 | </html> | |
| 647 | ); | |
| 648 | } | |
| 649 | ||
| 650 | export 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 today | 651 | const userId = useRouteLoaderData<typeof loader>("root")?.user?.id ?? null; |
| 652 | // Tell analytics who is signed in, once per change; signing out forgets them. | |
| 653 | const lastUserId = useRef<string | null>(null); | |
| 654 | useEffect(() => { | |
| 655 | identify(userId, lastUserId.current); | |
| 656 | lastUserId.current = userId; | |
| 657 | }, [userId]); | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 658 | return <Outlet />; |
| 659 | } | |
| 660 | ||
| 661 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 662 | const location = useLocation(); |
| 663 | const matches = useMatches(); | |
| 664 | // A tab left open across a deploy: load the address again as a whole | |
| 665 | // page, once, rather than show an error the reload fixes. | |
| 666 | const reload = reloadFixes({ | |
| 667 | error, | |
| 668 | status: isRouteErrorResponse(error) ? error.status : undefined, | |
| 669 | // The first page a tab loads has the key "default"; a client navigation's is its own. | |
| 670 | clientNavigation: location.key !== "default", | |
| 671 | caughtByCatchAll: matches.at(-1)?.id === "routes/not-found", | |
| 672 | }); | |
| 673 | const href = location.pathname + location.search + location.hash; | |
| 674 | useEffect(() => { | |
| 675 | if (!reload) return; | |
| 676 | try { | |
| 677 | if (sessionStorage.getItem(RELOADED_KEY) === href) return; | |
| 678 | sessionStorage.setItem(RELOADED_KEY, href); | |
| 679 | } catch { | |
| 680 | // No session storage: reload anyway; a document load never asks again. | |
| 681 | } | |
| 682 | window.location.assign(href); | |
| 683 | }, [reload, href]); | |
| 684 | if (reload) { | |
| 685 | return ( | |
| Not found and error pages have one main landmark: their own when signed out, the shell's when signed in | 686 | <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 audit | 687 | <title>Loading · g1t</title> |
| 688 | 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 in | 689 | </PageMain> |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 690 | ); |
| 691 | } | |
| 692 | ||
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 693 | let title = "Something went wrong"; |
| 694 | let details = "An unexpected error occurred. Try again in a moment."; | |
| 695 | let stack: string | undefined; | |
| 696 | ||
| 697 | if (isRouteErrorResponse(error)) { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 698 | // The same page for something private and something missing. |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 699 | if (error.status === 404) { |
| 700 | return ( | |
| 701 | <> | |
| 702 | <title>Not found · g1t</title> | |
| 703 | <NotFound data={error.data} /> | |
| 704 | </> | |
| 705 | ); | |
| 706 | } | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 707 | if (typeof error.data === "string" && error.data) { |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 708 | details = error.data; |
| 709 | } | |
| Merge main into Artifacts Phase 2 | 710 | // A token asked for what needs a real sign-in (lib/website-token.ts). |
| 711 | if (isNeedsSignIn(error.data)) { | |
| 712 | title = "This needs you to sign in"; | |
| 713 | details = error.data.message; | |
| 714 | } | |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 715 | } else if (import.meta.env.DEV && error instanceof Error) { |
| 716 | details = error.message; | |
| 717 | stack = error.stack; | |
| 718 | } | |
| 719 | ||
| 720 | return ( | |
| Not found and error pages have one main landmark: their own when signed out, the shell's when signed in | 721 | <PageMain className="mx-auto max-w-xl px-4 py-32 text-center"> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 722 | <h1 className="text-2xl font-semibold tracking-tight">{title}</h1> |
| 723 | <p className="mt-3 text-muted">{details}</p> | |
| 724 | <div className="mt-8"> | |
| 725 | <ButtonLink to="/" variant="quiet"> | |
| 726 | Back to home | |
| 727 | </ButtonLink> | |
| 728 | </div> | |
| 729 | {stack && ( | |
| 730 | <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs"> | |
| 731 | <code>{stack}</code> | |
| 732 | </pre> | |
| 733 | )} | |
| Not found and error pages have one main landmark: their own when signed out, the shell's when signed in | 734 | </PageMain> |
| Model providers: gateway tokens for endpoints, tidier rows, and the docs | 735 | ); |
| 736 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.