Skip to content
306 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.

Navigation: the workspace's own page with tabs, pins and recent in the sidebar1/**
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 sidebar7 */
8
Merge branch 'worktree-agent-ad7c6d88d93adc817'9/** The workspace's pages a person moves between, and which the sidebar lists. */
10export type WorkspacePageKey = "overview" | "projects" | "packages" | "teams" | "people" | "insights";
Navigation: the workspace's own page with tabs, pins and recent in the sidebar11
Merge branch 'worktree-agent-ad7c6d88d93adc817'12/** Those pages under `-/`: the overview is the workspace's own address. */
13export const WORKSPACE_PAGES = ["projects", "packages", "teams", "people", "insights"] as const;
Navigation: the workspace's own page with tabs, pins and recent in the sidebar14
Merge branch 'worktree-agent-ad7c6d88d93adc817'15/**
16 * Which of those pages a path is, under `/<workspace>`: null for the
17 * workspace's other pages (settings, Agent fleet, Usage and the rest), for
18 * one package's or one team's page, and for anything else.
19 */
20export 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 sidebar27
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 sidebar31 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'32export 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. */
50export const SETTINGS_PAGES = [
51 "settings",
52 "repositories",
53 "tokens",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge54 "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. */
67const 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 tokens78 // 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 sidebar81
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 sidebar86 */
Merge branch 'worktree-agent-ad7c6d88d93adc817'87export 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 sidebar94 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 sidebar99}
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 sidebar102const 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 sidebar113};
114
115/** Workspace pages that moved, by their old name under `-/`. */
116const 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 charging122/** The page a request is for: a click's data request ends in `.data`. */
123export 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 sidebar127/**
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 */
133export 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 sidebar144 * 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 sidebar147 */
148export function workspaceRedirect(pathname: string, search = ""): string | null {
Billing: credits with a kind and expiry, discounts instead of comped, and safer charging149 // 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 sidebar155 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 to169
170/**
Code sits second in the rail and on a phone's tab bar: for members who have it, it is the everyday mode171 * 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 project174 * 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 to178 */
Chat controls, public profiles, shadcn selects, and no Docs tab in a project179export type ModeKey = "home" | "chat" | "docs" | "agents" | "code" | "inbox" | "workspace" | "account" | "site";
Chat and workspace agents: channels, DMs and named agents you talk to180
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. */
182const 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 project211/** 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)212const 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 project217 * 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 to219export 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 to224 const parts = path.split("/").filter(Boolean);
Chat controls, public profiles, shadcn selects, and no Docs tab in a project225 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 to233 }
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)238export function modeHome(mode: ModeKey, slug: string): string {
Chat and workspace agents: channels, DMs and named agents you talk to239 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 to242 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 project256 case "site":
257 return "/explore";
Chat and workspace agents: channels, DMs and named agents you talk to258 }
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 */
266export 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 to275/** The page a member without Code access sees in place of anything of Code's. */
276export 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)281const 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 to282
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 */
290export 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 to302 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.