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.
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 1 | /** |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 2 | * A workspace's pages and where the sidebar finds them. `g1t.sh/<workspace>` |
| 3 | * is the workspace's own page, its header and overview; Projects, Packages, | |
| 4 | * Teams, People, Insights and Settings are pages of their own under `-/`, | |
| 5 | * each reached from the sidebar and each with its own heading. The sidebar | |
| 6 | * is always the current workspace's. | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 7 | */ |
| 8 | ||
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 9 | /** The workspace's pages a person moves between, and which the sidebar lists. */ |
| 10 | export type WorkspacePageKey = "overview" | "projects" | "packages" | "teams" | "people" | "insights"; | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 11 | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 12 | /** Those pages under `-/`: the overview is the workspace's own address. */ |
| 13 | export const WORKSPACE_PAGES = ["projects", "packages", "teams", "people", "insights"] as const; | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 14 | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 15 | /** |
| 16 | * Which of those pages a path is, under `/<workspace>`: null for the | |
| Agents mode: an overview of every agent's work and spend, and each agent's sessions, memory, routines, spend, activity and versions | 17 | * workspace's other pages (settings, Usage and the rest), for |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 18 | * one package's or one team's page, and for anything else. |
| 19 | */ | |
| 20 | export function workspacePage(pathname: string, slug: string): WorkspacePageKey | null { | |
| 21 | const parts = pagePath(pathname).split("/").filter(Boolean); | |
| 22 | if (parts[0]?.toLowerCase() !== slug.toLowerCase()) return null; | |
| 23 | if (parts.length === 1) return "overview"; | |
| 24 | if (parts[1] !== "-" || parts.length !== 3) return null; | |
| 25 | return (WORKSPACE_PAGES as readonly string[]).includes(parts[2]!) ? (parts[2] as WorkspacePageKey) : null; | |
| 26 | } | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 27 | |
| 28 | /** | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 29 | * The sidebar's rows. The first two are the person's own, whichever |
| 30 | * workspace they are in; the rest are the workspace's. | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 31 | */ |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 32 | export type SidebarKey = |
| 33 | | "mission" | |
| 34 | | "inbox" | |
| 35 | | "overview" | |
| 36 | | "projects" | |
| 37 | | "agents" | |
| 38 | | "context" | |
| 39 | | "memory" | |
| 40 | | "security" | |
| 41 | | "packages" | |
| 42 | | "insights" | |
| 43 | | "people" | |
| 44 | | "teams" | |
| 45 | | "usage" | |
| 46 | | "support" | |
| 47 | | "settings"; | |
| 48 | ||
| 49 | /** The workspace's pages that its Settings row drills into. */ | |
| 50 | export const SETTINGS_PAGES = [ | |
| 51 | "settings", | |
| 52 | "repositories", | |
| 53 | "tokens", | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 54 | "rules", |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 55 | "guardrails", |
| 56 | "secrets", | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 57 | "actions", |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 58 | "runners", |
| 59 | "integrations", | |
| 60 | "webhooks", | |
| 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) | 61 | "emoji", |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 62 | "billing", |
| 63 | "audit", | |
| 64 | ] as const; | |
| 65 | ||
| 66 | /** Pages under `-/` whose sidebar row has another name. */ | |
| 67 | const ROW_OF: Record<string, SidebarKey> = { | |
| 68 | projects: "projects", | |
| 69 | agents: "agents", | |
| 70 | context: "context", | |
| 71 | memory: "memory", | |
| 72 | security: "security", | |
| 73 | packages: "packages", | |
| 74 | insights: "insights", | |
| 75 | people: "people", | |
| 76 | teams: "teams", | |
| 77 | usage: "usage", | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 78 | // The AI Gateway's requests are usage. |
| 79 | gateway: "usage", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 80 | }; |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 81 | |
| 82 | /** | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 83 | * The sidebar row that is current on a path: one row at most, so the |
| 84 | * sidebar always says where you are. `slug` is the workspace the sidebar | |
| 85 | * is about; another workspace's pages light nothing. | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 86 | */ |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 87 | export function sidebarCurrent(pathname: string, slug: string | null): SidebarKey | null { |
| 88 | const path = pagePath(pathname); | |
| 89 | if (path === "/") return "mission"; | |
| 90 | if (path === "/inbox" || path.startsWith("/inbox/")) return "inbox"; | |
| 91 | if (path === "/support" || path.startsWith("/support/")) return "support"; | |
| 92 | const parts = path.split("/").filter(Boolean); | |
| 93 | if (!slug || parts[0]?.toLowerCase() !== slug.toLowerCase()) return null; | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 94 | if (parts.length === 1) return "overview"; |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 95 | if (parts[1] !== "-") return null; |
| 96 | const page = parts[2] ?? ""; | |
| 97 | if ((SETTINGS_PAGES as readonly string[]).includes(page)) return "settings"; | |
| 98 | return ROW_OF[page] ?? null; | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 99 | } |
| 100 | ||
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 101 | /** `?tab=` as people write it, from the tabs the workspace page once had: the page it means. */ |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 102 | const TAB_WORDS: Record<string, string> = { |
| 103 | overview: "", | |
| 104 | projects: "-/projects", | |
| 105 | repositories: "-/projects", | |
| 106 | repos: "-/projects", | |
| 107 | packages: "-/packages", | |
| 108 | people: "-/people", | |
| 109 | members: "-/people", | |
| 110 | teams: "-/teams", | |
| 111 | insights: "-/insights", | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 112 | settings: "-/settings", |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 113 | }; |
| 114 | ||
| 115 | /** Workspace pages that moved, by their old name under `-/`. */ | |
| 116 | const MOVED: Record<string, string> = { | |
| 117 | members: "-/people", | |
| 118 | "soon/teams": "-/teams", | |
| 119 | "soon/insights": "-/insights", | |
| 120 | }; | |
| 121 | ||
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 122 | /** The page a request is for: a click's data request ends in `.data`. */ |
| 123 | export function pagePath(pathname: string): string { | |
| 124 | return pathname.replace(/\.data$/, "").replace(/\/+$/, "") || "/"; | |
| 125 | } | |
| 126 | ||
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 127 | /** |
| Merge branch 'worktree-agent-a8385d293d42c913a' | 128 | * The page at `pathname` under the workspace `slug` instead of its first |
| 129 | * segment, keeping its query: where an old name or an alias leads. A click | |
| 130 | * asks for the page's data at `<path>.data?_routes=…`; the address given is | |
| 131 | * the page's own, which the browser then asks for as usual. | |
| 132 | */ | |
| 133 | export function underWorkspace(pathname: string, search: string, slug: string): string { | |
| 134 | const params = new URLSearchParams(search); | |
| 135 | params.delete("_routes"); | |
| 136 | const query = params.toString(); | |
| 137 | const segments = pagePath(pathname).split("/"); | |
| 138 | // segments[0] is the empty string before the leading slash. | |
| 139 | segments[1] = slug; | |
| 140 | return `${segments.join("/")}${query ? `?${query}` : ""}`; | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 144 | * Where an old address of a workspace's pages is now, keeping its query; |
| 145 | * null when it has not moved. `/<workspace>?tab=projects` and the like | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 146 | * open that page. |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 147 | */ |
| 148 | export function workspaceRedirect(pathname: string, search = ""): string | null { | |
| Billing: credits with a kind and expiry, discounts instead of comped, and safer charging | 149 | // A click asks for the page's data at `<path>.data?_routes=…`; the page |
| 150 | // is the same one a full load asks for. | |
| 151 | const params = new URLSearchParams(search); | |
| 152 | params.delete("_routes"); | |
| 153 | search = params.toString(); | |
| 154 | const parts = pagePath(pathname).split("/").filter(Boolean); | |
| Navigation: the workspace's own page with tabs, pins and recent in the sidebar | 155 | const slug = parts[0]; |
| 156 | if (!slug) return null; | |
| 157 | if (parts.length === 1 && params.has("tab")) { | |
| 158 | const to = TAB_WORDS[(params.get("tab") ?? "").toLowerCase()]; | |
| 159 | if (to === undefined) return null; | |
| 160 | params.delete("tab"); | |
| 161 | const rest = params.toString(); | |
| 162 | return `/${slug}${to ? `/${to}` : ""}${rest ? `?${rest}` : ""}`; | |
| 163 | } | |
| 164 | if (parts[1] !== "-") return null; | |
| 165 | const to = MOVED[parts.slice(2).join("/")]; | |
| 166 | if (to === undefined) return null; | |
| 167 | return `/${slug}${to ? `/${to}` : ""}${search && search !== "?" ? (search.startsWith("?") ? search : `?${search}`) : ""}`; | |
| 168 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 169 | |
| 170 | /** | |
| Code sits second in the rail and on a phone's tab bar: for members who have it, it is the everyday mode | 171 | * The rail's modes (docs/WORKSPACE.md, "Shell"). Home, Code, Chat, Docs |
| 172 | * and Agents are where work happens; the Inbox spans them; Workspace is the | |
| 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) | 173 | * workspace itself: its people, money, policies and settings. `account` is |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 174 | * your own settings, under the avatar. `site` is g1t's own public pages |
| 175 | * (a profile, Explore, Search): no workspace's, so no mode is lit and no | |
| 176 | * mode's sidebar sits beside them. Each mode has a sidebar of its own, or | |
| 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) | 177 | * none, and which one is lit follows the address. |
| Chat and workspace agents: channels, DMs and named agents you talk to | 178 | */ |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 179 | export type ModeKey = "home" | "chat" | "docs" | "agents" | "code" | "inbox" | "workspace" | "account" | "site"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 180 | |
| 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) | 181 | /** Workspace pages under `-/`, by the mode they belong to. Anything else of the workspace's is Code's. */ |
| 182 | const PAGE_MODES: Record<string, ModeKey> = { | |
| 183 | home: "home", | |
| 184 | chat: "chat", | |
| 185 | docs: "docs", | |
| 186 | agents: "agents", | |
| 187 | context: "agents", | |
| 188 | memory: "agents", | |
| 189 | workspace: "workspace", | |
| 190 | people: "workspace", | |
| 191 | members: "workspace", | |
| 192 | teams: "workspace", | |
| 193 | usage: "workspace", | |
| 194 | gateway: "workspace", | |
| 195 | billing: "workspace", | |
| 196 | integrations: "workspace", | |
| 197 | guardrails: "workspace", | |
| 198 | rules: "workspace", | |
| 199 | audit: "workspace", | |
| 200 | settings: "workspace", | |
| 201 | repositories: "workspace", | |
| 202 | tokens: "workspace", | |
| 203 | "personal-access-tokens": "workspace", | |
| 204 | secrets: "workspace", | |
| 205 | actions: "workspace", | |
| 206 | runners: "workspace", | |
| 207 | webhooks: "workspace", | |
| 208 | emoji: "workspace", | |
| 209 | }; | |
| 210 | ||
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 211 | /** First segments that are g1t's own pages, never a workspace: none is any mode's. */ |
| 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) | 212 | const SITE_PAGES = new Set(["explore", "search", "support", "policies", "security", "status", "u", "invite", "workspaces"]); |
| 213 | ||
| 214 | /** | |
| 215 | * The mode a path is in, for the workspace `slug`. A repository (anyone's) | |
| 216 | * and a new project are Code's; g1t's own pages (Explore, search, a | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 217 | * profile) are `site`: public, and no workspace's mode. |
| 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) | 218 | */ |
| Chat and workspace agents: channels, DMs and named agents you talk to | 219 | export function modeOf(pathname: string, slug: string | null): ModeKey { |
| 220 | const path = pagePath(pathname); | |
| 221 | if (path === "/") return "home"; | |
| 222 | if (path === "/inbox" || path.startsWith("/inbox/")) return "inbox"; | |
| 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) | 223 | if (path === "/settings" || path.startsWith("/settings/")) return "account"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 224 | const parts = path.split("/").filter(Boolean); |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 225 | if (SITE_PAGES.has(parts[0] ?? "")) return "site"; |
| 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) | 226 | if (slug && parts[0]?.toLowerCase() === slug.toLowerCase()) { |
| 227 | if (parts.length === 1) return "home"; | |
| 228 | if (parts[1] === "-") { | |
| 229 | // The workspace's security settings are a policy; its alerts are Code's. | |
| 230 | if (parts[2] === "security" && parts[3] === "settings") return "workspace"; | |
| 231 | return PAGE_MODES[parts[2] ?? ""] ?? "code"; | |
| 232 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 233 | } |
| 234 | return "code"; | |
| 235 | } | |
| 236 | ||
| 237 | /** Where each mode's rail button goes, in the workspace `slug`. */ | |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 238 | export function modeHome(mode: ModeKey, slug: string): string { |
| Chat and workspace agents: channels, DMs and named agents you talk to | 239 | switch (mode) { |
| 240 | case "home": | |
| 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) | 241 | return `/${slug}/-/home`; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 242 | case "inbox": |
| 243 | return "/inbox"; | |
| 244 | case "chat": | |
| 245 | return `/${slug}/-/chat`; | |
| 246 | case "docs": | |
| 247 | return `/${slug}/-/docs`; | |
| 248 | case "agents": | |
| 249 | return `/${slug}/-/agents`; | |
| 250 | case "code": | |
| 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) | 251 | return `/${slug}/-/overview`; |
| 252 | case "workspace": | |
| 253 | return `/${slug}/-/workspace`; | |
| 254 | case "account": | |
| 255 | return "/settings"; | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 256 | case "site": |
| 257 | return "/explore"; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 258 | } |
| 259 | } | |
| 260 | ||
| 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) | 261 | /** |
| 262 | * Where Mission control's old addresses (`/`, `/<workspace>`) lead in the | |
| 263 | * workspace `slug`: Home, or Code's Overview when the query asks for its | |
| 264 | * panels (a tab, or `?agent=new` to put an agent on something). | |
| 265 | */ | |
| 266 | export function homePath(slug: string, search = ""): string { | |
| 267 | const params = new URLSearchParams(search); | |
| 268 | params.delete("_routes"); | |
| 269 | params.delete("index"); | |
| 270 | const query = params.toString(); | |
| 271 | if (params.has("agent") || params.has("tab") || params.has("sort")) return `/${slug}/-/overview?${query}`; | |
| 272 | return `/${slug}/-/home`; | |
| 273 | } | |
| 274 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 275 | /** The page a member without Code access sees in place of anything of Code's. */ |
| 276 | export function codeAccessPath(slug: string, from?: string): string { | |
| 277 | return `/${slug}/-/code-access${from ? `?from=${encodeURIComponent(from)}` : ""}`; | |
| 278 | } | |
| 279 | ||
| 280 | /** The workspace pages under `-/` that are Code's: closed to a member without Code access. */ | |
| 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) | 281 | const CODE_PAGES = new Set(["overview", "projects", "repositories", "packages", "security", "rules", "runners", "actions", "context", "memory", "insights", "soon"]); |
| Chat and workspace agents: channels, DMs and named agents you talk to | 282 | |
| 283 | /** | |
| 284 | * Where a member without Code access goes instead of `pathname`, or null | |
| 285 | * when the page is open to them. `noCode` is the workspaces (by slug) where | |
| 286 | * they lack it; `chosen` is the one they are in. Mission control (`/`) is | |
| 287 | * their Home in that workspace; its overview, its projects and every | |
| 288 | * repository page is the page that says to ask an owner. | |
| 289 | */ | |
| 290 | export function codeGate(pathname: string, search: string, noCode: readonly string[], chosen: string | null): string | null { | |
| 291 | if (noCode.length === 0) return null; | |
| 292 | const path = pagePath(pathname); | |
| 293 | if (path === "/") { | |
| 294 | const slug = chosen && noCode.includes(chosen.toLowerCase()) ? chosen.toLowerCase() : null; | |
| 295 | return slug ? `/${slug}/-/home` : null; | |
| 296 | } | |
| 297 | const parts = path.split("/").filter(Boolean); | |
| 298 | const slug = parts[0]?.toLowerCase(); | |
| 299 | if (!slug || !noCode.includes(slug)) return null; | |
| 300 | const from = `${path}${search && search !== "?" ? search : ""}`; | |
| 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) | 301 | // The workspace's own page is their Home. |
| Chat and workspace agents: channels, DMs and named agents you talk to | 302 | if (parts.length === 1) return `/${slug}/-/home`; |
| 303 | if (parts[1] === "-") return CODE_PAGES.has(parts[2] ?? "") ? codeAccessPath(slug, from) : null; | |
| 304 | // A repository, and everything in it. | |
| 305 | return codeAccessPath(slug, from); | |
| 306 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.