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. */ | |
| 17 | export type BuiltinApp = "today" | "chat" | "notifications" | "agents" | "code" | "artifacts" | "people" | "workspace"; | |
| 18 | ||
| 19 | /** The apps people pin. */ | |
| 20 | export type PinnableApp = | |
| 21 | | "projects" | |
| 22 | | "packages" | |
| 23 | | "security" | |
| 24 | | "context" | |
| 25 | | "memory" | |
| 26 | | "teams" | |
| 27 | | "usage" | |
| 28 | | "gateway" | |
| 29 | | "integrations" | |
| 30 | | "audit"; | |
| 31 | ||
| 32 | export type AppKey = BuiltinApp | PinnableApp; | |
| 33 | ||
| 34 | export 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 | ||
| 47 | const under = (page: string) => (slug: string) => `/${slug}/-/${page}`; | |
| 48 | ||
| 49 | /** Every app, built-in first, in the order the launcher shows them. */ | |
| 50 | export 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 | ||
| 71 | const PINNABLE = new Set<string>(APPS.filter((app) => !app.builtin).map((app) => app.key)); | |
| 72 | ||
| 73 | /** Whether `key` is an app people pin. */ | |
| 74 | export 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. */ | |
| 79 | export function appsFor(code: boolean): AppInfo[] { | |
| 80 | return APPS.filter((app) => code || !app.code); | |
| 81 | } | |
| 82 | ||
| 83 | /** The app `key`. */ | |
| 84 | export 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. */ | |
| 89 | export const DOCK_COOKIE = "g1t_dock"; | |
| 90 | ||
| 91 | /** Most workspaces the cookie remembers pins for, and most pins in each. */ | |
| 92 | const MAX_WORKSPACES = 20; | |
| 93 | export const MAX_APP_PINS = 12; | |
| 94 | ||
| 95 | /** Every workspace's pins, from the cookie's value: `acme:projects.usage,beta:teams`. */ | |
| 96 | export 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. */ | |
| 108 | export 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. */ | |
| 113 | export 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. */ | |
| 122 | export 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. */ | |
| 130 | export 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. */ | |
| 136 | export 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. */ | |
| 141 | export const SIDEBAR_COOKIE = "g1t_sidebar"; | |
| 142 | ||
| 143 | /** Whether the sidebar is folded away, from the cookie's value. */ | |
| 144 | export 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. */ | |
| 149 | export function sidebarCookie(closed: boolean, secure: boolean): string { | |
| 150 | return `${SIDEBAR_COOKIE}=${closed ? "closed" : "open"}; Path=/; Max-Age=31536000; SameSite=Lax${secure ? "; Secure" : ""}`; | |
| 151 | } |