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

g1t is described as the product it is in alpha, and its shell takes the new shape: the README, the docs home and docs/PLAN.md label every feature Live, Preview or Coming; a floating dock with the g1t mark, Today, Chat, Notifications, Agents, Code, Artifacts, your pinned apps and the Apps launcher, with People, Workspace and the account menu (now holding help) at its foot; the workspace's logo and switcher heading a flat sidebar that folds away with Ctrl B, or leading the header's breadcrumbs where there is none; the page in a rounded panel with a full-width header; Today as the front page, with first-time acceptance of agents' pull requests, what needs you and what's at stake, spend today and one item to start with, every number from a service; Notifications in place of the Inbox at /notifications; a bottom bar and More sheet on phones; and sign-in, sign-up, two-factor, reset, invites and choosing a workspace on standalone pages with no app around them.1/**
2 * The apps a workspace has, and the ones each person pins to their dock.
3 *
4 * Built-in apps (Today, Chat, Notifications, Agents, Code, Artifacts,
5 * People and Workspace) are always in the dock. Every other app is a page
6 * of the workspace that people use on its own: Projects, Packages,
7 * Security, Context, Memory, Teams, Usage, the AI Gateway, Integrations
8 * and the audit log. Each person pins the ones they want, and nobody sees
9 * an app they cannot use: Code's apps are for members with Code access.
10 *
11 * Pins are each person's own, per workspace, and kept in a cookie
12 * (`g1t_dock`) so the page is drawn with the right dock from the server.
13 * No Workers or React imports, so it can be tested under Node.
14 */
15
16/** The apps that are always in the dock. */
17export type BuiltinApp = "today" | "chat" | "notifications" | "agents" | "code" | "artifacts" | "people" | "workspace";
18
19/** The apps people pin. */
20export type PinnableApp =
21 | "projects"
22 | "packages"
23 | "security"
24 | "context"
25 | "memory"
26 | "teams"
27 | "usage"
28 | "gateway"
29 | "integrations"
30 | "audit";
31
32export type AppKey = BuiltinApp | PinnableApp;
33
34export type AppInfo = {
35 key: AppKey;
36 name: string;
37 /** What it is, in a few words, for the Apps page. */
38 about: string;
39 /** Its page in the workspace `slug`. */
40 path: (slug: string) => string;
41 /** Always in the dock: it cannot be pinned or unpinned. */
42 builtin: boolean;
43 /** Part of Code: only for members with Code access. */
44 code?: boolean;
45};
46
47const under = (page: string) => (slug: string) => `/${slug}/-/${page}`;
48
49/** Every app, built-in first, in the order the launcher shows them. */
50export const APPS: AppInfo[] = [
51 { key: "today", name: "Today", about: "What needs you, across the workspace", path: under("today"), builtin: true },
52 { key: "chat", name: "Chat", about: "Channels and messages with people and agents", path: under("chat"), builtin: true },
53 { key: "notifications", name: "Notifications", about: "Reviews, mentions and what agents wait on", path: () => "/notifications", builtin: true },
54 { key: "agents", name: "Agents", about: "Your workspace's agents and their sessions", path: under("agents"), builtin: true },
55 { key: "code", name: "Code", about: "Projects, pull requests and checks", path: under("overview"), builtin: true, code: true },
56 { key: "artifacts", name: "Artifacts", about: "Documents, decks and pages", path: under("artifacts"), builtin: true },
57 { key: "people", name: "People", about: "Who belongs to the workspace", path: under("people"), builtin: true },
58 { key: "workspace", name: "Workspace", about: "Billing, policies and settings", path: under("workspace"), builtin: true },
59 { key: "projects", name: "Projects", about: "Every project in the workspace", path: under("projects"), builtin: false, code: true },
60 { key: "packages", name: "Packages", about: "Packages published from its projects", path: under("packages"), builtin: false, code: true },
61 { key: "security", name: "Security", about: "Alerts across its projects", path: under("security"), builtin: false, code: true },
62 { key: "context", name: "Context", about: "What agents can look up", path: under("context"), builtin: false, code: true },
63 { key: "memory", name: "Memory", about: "What agents remember about the workspace", path: under("memory"), builtin: false, code: true },
64 { key: "teams", name: "Teams", about: "Groups of people to mention and grant access", path: under("teams"), builtin: false },
65 { key: "usage", name: "Usage", about: "What the workspace has used this month", path: under("usage"), builtin: false },
66 { key: "gateway", name: "AI Gateway", about: "Every model request, with its cost", path: under("gateway"), builtin: false },
67 { key: "integrations", name: "Integrations", about: "Model providers, alerts and trackers", path: under("integrations"), builtin: false },
68 { key: "audit", name: "Audit log", about: "Who did what, and when", path: under("audit"), builtin: false },
69];
70
71const PINNABLE = new Set<string>(APPS.filter((app) => !app.builtin).map((app) => app.key));
72
73/** Whether `key` is an app people pin. */
74export function isPinnable(key: string): key is PinnableApp {
75 return PINNABLE.has(key);
76}
77
78/** The apps someone can use in a workspace: Code's only with Code access. */
79export function appsFor(code: boolean): AppInfo[] {
80 return APPS.filter((app) => code || !app.code);
81}
82
83/** The app `key`. */
84export function appOf(key: AppKey): AppInfo {
85 return APPS.find((app) => app.key === key)!;
86}
87
88/** The cookie that keeps each person's pins, per workspace. */
89export const DOCK_COOKIE = "g1t_dock";
90
91/** Most workspaces the cookie remembers pins for, and most pins in each. */
92const MAX_WORKSPACES = 20;
93export const MAX_APP_PINS = 12;
94
95/** Every workspace's pins, from the cookie's value: `acme:projects.usage,beta:teams`. */
96export function readDock(value: string | null | undefined): Record<string, PinnableApp[]> {
97 const dock: Record<string, PinnableApp[]> = {};
98 for (const entry of (value ?? "").split(",")) {
99 const [slug, keys] = entry.split(":");
100 if (!slug || !/^[a-z0-9-]+$/.test(slug) || keys === undefined) continue;
101 const pins = [...new Set(keys.split(".").filter(isPinnable))].slice(0, MAX_APP_PINS);
102 dock[slug] = pins;
103 }
104 return dock;
105}
106
107/** The pins in the workspace `slug`, in the order they were pinned. */
108export function pinsIn(value: string | null | undefined, slug: string): PinnableApp[] {
109 return readDock(value)[slug.toLowerCase()] ?? [];
110}
111
112/** The cookie's value with the workspace `slug`'s pins set to `pins`: the latest workspace first. */
113export function writeDock(value: string | null | undefined, slug: string, pins: PinnableApp[]): string {
114 const dock = readDock(value);
115 const key = slug.toLowerCase();
116 delete dock[key];
117 const entries = [[key, [...new Set(pins.filter(isPinnable))].slice(0, MAX_APP_PINS)] as const, ...Object.entries(dock)].slice(0, MAX_WORKSPACES);
118 return entries.map(([name, keys]) => `${name}:${keys.join(".")}`).join(",");
119}
120
121/** One change to someone's pins, as the pin buttons post it; null when it asks for nothing that makes sense. */
122export function appPinFromForm(form: FormData): { app: PinnableApp; pinned: boolean } | null {
123 const app = String(form.get("app") ?? "");
124 const intent = form.get("intent");
125 if (!isPinnable(app) || (intent !== "pin" && intent !== "unpin")) return null;
126 return { app, pinned: intent === "pin" };
127}
128
129/** The pins after pinning or unpinning `app`: a new pin goes at the end. */
130export function withPin(pins: PinnableApp[], app: PinnableApp, pinned: boolean): PinnableApp[] {
131 const rest = pins.filter((key) => key !== app);
132 return pinned ? [...rest, app] : rest;
133}
134
135/** The Set-Cookie header that keeps the dock, for a year. */
136export function dockCookie(value: string, secure: boolean): string {
137 return `${DOCK_COOKIE}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
138}
139
140/** The cookie that remembers whether the in-context sidebar is folded away on a computer. */
141export const SIDEBAR_COOKIE = "g1t_sidebar";
142
143/** Whether the sidebar is folded away, from the cookie's value. */
144export function sidebarClosed(value: string | null | undefined): boolean {
145 return value === "closed";
146}
147
148/** The cookie, as the page sets it when the sidebar is folded away or brought back. */
149export function sidebarCookie(closed: boolean, secure: boolean): string {
150 return `${SIDEBAR_COOKIE}=${closed ? "closed" : "open"}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
151}