Skip to content
176 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 *
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.11 * Pins are each person's own, per workspace, in the order they set, and
12 * kept with their account by the identity service (`dock_pins`), so the
13 * dock is the same on every device (lib/dock.server.ts). A cookie
14 * (`g1t_dock`) keeps a copy on each device: the dock still draws from it
15 * when identity cannot be reached, and pins from before they were kept
16 * with the account carry over from it until the first change.
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.17 * No Workers or React imports, so it can be tested under Node.
18 */
19
20/** The apps that are always in the dock. */
21export type BuiltinApp = "today" | "chat" | "notifications" | "agents" | "code" | "artifacts" | "people" | "workspace";
22
23/** The apps people pin. */
24export type PinnableApp =
25 | "projects"
26 | "packages"
27 | "security"
28 | "context"
29 | "memory"
30 | "teams"
31 | "usage"
32 | "gateway"
33 | "integrations"
34 | "audit";
35
36export type AppKey = BuiltinApp | PinnableApp;
37
38export type AppInfo = {
39 key: AppKey;
40 name: string;
41 /** What it is, in a few words, for the Apps page. */
42 about: string;
43 /** Its page in the workspace `slug`. */
44 path: (slug: string) => string;
45 /** Always in the dock: it cannot be pinned or unpinned. */
46 builtin: boolean;
47 /** Part of Code: only for members with Code access. */
48 code?: boolean;
49};
50
51const under = (page: string) => (slug: string) => `/${slug}/-/${page}`;
52
53/** Every app, built-in first, in the order the launcher shows them. */
54export const APPS: AppInfo[] = [
55 { key: "today", name: "Today", about: "What needs you, across the workspace", path: under("today"), builtin: true },
56 { key: "chat", name: "Chat", about: "Channels and messages with people and agents", path: under("chat"), builtin: true },
57 { key: "notifications", name: "Notifications", about: "Reviews, mentions and what agents wait on", path: () => "/notifications", builtin: true },
58 { key: "agents", name: "Agents", about: "Your workspace's agents and their sessions", path: under("agents"), builtin: true },
59 { key: "code", name: "Code", about: "Projects, pull requests and checks", path: under("overview"), builtin: true, code: true },
60 { key: "artifacts", name: "Artifacts", about: "Documents, decks and pages", path: under("artifacts"), builtin: true },
61 { key: "people", name: "People", about: "Who belongs to the workspace", path: under("people"), builtin: true },
62 { key: "workspace", name: "Workspace", about: "Billing, policies and settings", path: under("workspace"), builtin: true },
63 { key: "projects", name: "Projects", about: "Every project in the workspace", path: under("projects"), builtin: false, code: true },
64 { key: "packages", name: "Packages", about: "Packages published from its projects", path: under("packages"), builtin: false, code: true },
65 { key: "security", name: "Security", about: "Alerts across its projects", path: under("security"), builtin: false, code: true },
66 { key: "context", name: "Context", about: "What agents can look up", path: under("context"), builtin: false, code: true },
67 { key: "memory", name: "Memory", about: "What agents remember about the workspace", path: under("memory"), builtin: false, code: true },
68 { key: "teams", name: "Teams", about: "Groups of people to mention and grant access", path: under("teams"), builtin: false },
69 { key: "usage", name: "Usage", about: "What the workspace has used this month", path: under("usage"), builtin: false },
70 { key: "gateway", name: "AI Gateway", about: "Every model request, with its cost", path: under("gateway"), builtin: false },
71 { key: "integrations", name: "Integrations", about: "Model providers, alerts and trackers", path: under("integrations"), builtin: false },
72 { key: "audit", name: "Audit log", about: "Who did what, and when", path: under("audit"), builtin: false },
73];
74
75const PINNABLE = new Set<string>(APPS.filter((app) => !app.builtin).map((app) => app.key));
76
77/** Whether `key` is an app people pin. */
78export function isPinnable(key: string): key is PinnableApp {
79 return PINNABLE.has(key);
80}
81
82/** The apps someone can use in a workspace: Code's only with Code access. */
83export function appsFor(code: boolean): AppInfo[] {
84 return APPS.filter((app) => code || !app.code);
85}
86
87/** The app `key`. */
88export function appOf(key: AppKey): AppInfo {
89 return APPS.find((app) => app.key === key)!;
90}
91
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.92/** The cookie that keeps this device's copy of each person's pins, per workspace. */
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.93export const DOCK_COOKIE = "g1t_dock";
94
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.95/**
96 * Most workspaces the cookie remembers pins for, and most pins in each.
97 * Identity keeps up to 24 (`MAX_DOCK_PINS`); this stays under it.
98 */
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.99const MAX_WORKSPACES = 20;
100export const MAX_APP_PINS = 12;
101
The apps you pin to your dock are kept with your account, per workspace and in your order, so the dock is the same on every device: identity keeps them in dock_pins and answers dock_pins and set_dock_pins, the dock reads them with the rest of the page, pins kept only on this device carry over with your first change, this device's copy still draws the dock when identity can't be reached, a pin that can't be saved says so, and they go when you or the workspace do; the workspaces guide says how.102/**
103 * Pins as identity keeps them, read against the apps there are: keys that
104 * are not an app people pin (one since retired, say) are dropped, repeats
105 * too, in the order they were set.
106 */
107export function pinsFrom(keys: readonly string[]): PinnableApp[] {
108 return [...new Set(keys.filter(isPinnable))].slice(0, MAX_APP_PINS);
109}
110
111/**
112 * The pins to draw in the workspace `slug`: the account's when identity
113 * has them (`stored`), else this device's cookie, for pins never saved to
114 * the account or when identity could not be asked.
115 */
116export function pinsToShow(stored: readonly string[] | null | undefined, cookie: string | null | undefined, slug: string): PinnableApp[] {
117 return stored ? pinsFrom(stored) : pinsIn(cookie, slug);
118}
119
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.120/** Every workspace's pins, from the cookie's value: `acme:projects.usage,beta:teams`. */
121export function readDock(value: string | null | undefined): Record<string, PinnableApp[]> {
122 const dock: Record<string, PinnableApp[]> = {};
123 for (const entry of (value ?? "").split(",")) {
124 const [slug, keys] = entry.split(":");
125 if (!slug || !/^[a-z0-9-]+$/.test(slug) || keys === undefined) continue;
126 const pins = [...new Set(keys.split(".").filter(isPinnable))].slice(0, MAX_APP_PINS);
127 dock[slug] = pins;
128 }
129 return dock;
130}
131
132/** The pins in the workspace `slug`, in the order they were pinned. */
133export function pinsIn(value: string | null | undefined, slug: string): PinnableApp[] {
134 return readDock(value)[slug.toLowerCase()] ?? [];
135}
136
137/** The cookie's value with the workspace `slug`'s pins set to `pins`: the latest workspace first. */
138export function writeDock(value: string | null | undefined, slug: string, pins: PinnableApp[]): string {
139 const dock = readDock(value);
140 const key = slug.toLowerCase();
141 delete dock[key];
142 const entries = [[key, [...new Set(pins.filter(isPinnable))].slice(0, MAX_APP_PINS)] as const, ...Object.entries(dock)].slice(0, MAX_WORKSPACES);
143 return entries.map(([name, keys]) => `${name}:${keys.join(".")}`).join(",");
144}
145
146/** One change to someone's pins, as the pin buttons post it; null when it asks for nothing that makes sense. */
147export function appPinFromForm(form: FormData): { app: PinnableApp; pinned: boolean } | null {
148 const app = String(form.get("app") ?? "");
149 const intent = form.get("intent");
150 if (!isPinnable(app) || (intent !== "pin" && intent !== "unpin")) return null;
151 return { app, pinned: intent === "pin" };
152}
153
154/** The pins after pinning or unpinning `app`: a new pin goes at the end. */
155export function withPin(pins: PinnableApp[], app: PinnableApp, pinned: boolean): PinnableApp[] {
156 const rest = pins.filter((key) => key !== app);
157 return pinned ? [...rest, app] : rest;
158}
159
160/** The Set-Cookie header that keeps the dock, for a year. */
161export function dockCookie(value: string, secure: boolean): string {
162 return `${DOCK_COOKIE}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
163}
164
165/** The cookie that remembers whether the in-context sidebar is folded away on a computer. */
166export const SIDEBAR_COOKIE = "g1t_sidebar";
167
168/** Whether the sidebar is folded away, from the cookie's value. */
169export function sidebarClosed(value: string | null | undefined): boolean {
170 return value === "closed";
171}
172
173/** The cookie, as the page sets it when the sidebar is folded away or brought back. */
174export function sidebarCookie(closed: boolean, secure: boolean): string {
175 return `${SIDEBAR_COOKIE}=${closed ? "closed" : "open"}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`;
176}