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. */ | |
| 21 | export type BuiltinApp = "today" | "chat" | "notifications" | "agents" | "code" | "artifacts" | "people" | "workspace"; | |
| 22 | ||
| 23 | /** The apps people pin. */ | |
| 24 | export type PinnableApp = | |
| 25 | | "projects" | |
| 26 | | "packages" | |
| 27 | | "security" | |
| 28 | | "context" | |
| 29 | | "memory" | |
| 30 | | "teams" | |
| 31 | | "usage" | |
| 32 | | "gateway" | |
| 33 | | "integrations" | |
| 34 | | "audit"; | |
| 35 | ||
| 36 | export type AppKey = BuiltinApp | PinnableApp; | |
| 37 | ||
| 38 | export 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 | ||
| 51 | const under = (page: string) => (slug: string) => `/${slug}/-/${page}`; | |
| 52 | ||
| 53 | /** Every app, built-in first, in the order the launcher shows them. */ | |
| 54 | export 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 | ||
| 75 | const PINNABLE = new Set<string>(APPS.filter((app) => !app.builtin).map((app) => app.key)); | |
| 76 | ||
| 77 | /** Whether `key` is an app people pin. */ | |
| 78 | export 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. */ | |
| 83 | export function appsFor(code: boolean): AppInfo[] { | |
| 84 | return APPS.filter((app) => code || !app.code); | |
| 85 | } | |
| 86 | ||
| 87 | /** The app `key`. */ | |
| 88 | export 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. | 93 | export 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. | 99 | const MAX_WORKSPACES = 20; |
| 100 | export 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 | */ | |
| 107 | export 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 | */ | |
| 116 | export 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`. */ |
| 121 | export 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. */ | |
| 133 | export 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. */ | |
| 138 | export 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. */ | |
| 147 | export 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. */ | |
| 155 | export 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. */ | |
| 161 | export 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. */ | |
| 166 | export const SIDEBAR_COOKIE = "g1t_sidebar"; | |
| 167 | ||
| 168 | /** Whether the sidebar is folded away, from the cookie's value. */ | |
| 169 | export 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. */ | |
| 174 | export function sidebarCookie(closed: boolean, secure: boolean): string { | |
| 175 | return `${SIDEBAR_COOKIE}=${closed ? "closed" : "open"}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`; | |
| 176 | } |