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

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)1/**
2 * Notifications: the notify service (`services/notify`) keeps one feed per
3 * person, a Durable Object holding their open tabs' sockets, their latest
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)4 * notifications, live unread counts, their browser push subscriptions and
5 * the phones the g1t app pushes to.
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)6 *
7 * Chat tells it about every message a person should count or hear of, the
8 * events service about every inbox item, and the site forwards each tab's
9 * `/-/live` socket to it. Wire shapes are snake_case end to end.
10 */
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.11import type { AgentLook } from "./agent-look";
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread12import type { CardAction } from "./chat";
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)13import type { ServiceBinding } from "./clients";
14import type { User } from "./identity";
15
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.16/**
17 * What a notification is about; the preferences decide which ones toast and
18 * push. `security` is an urgent security alert (critical or high, a refused
19 * push, a vulnerability report), shown like a mention; the rest of the
20 * security items are ordinary `inbox` ones.
21 */
22export type NotificationKind = "dm" | "mention" | "thread_reply" | "inbox" | "agent_waiting" | "approval" | "security";
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)23
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.24export const NOTIFICATION_KINDS: readonly NotificationKind[] = ["dm", "mention", "thread_reply", "inbox", "agent_waiting", "approval", "security"];
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)25
26/** Who it is from: a person, an agent, or g1t itself. */
27export type NotificationActor = {
28 kind: "user" | "agent" | "system";
29 id: string;
30 /** How they show: a display name. */
31 name: string;
32 /** A person's uploaded avatar hash, or null for the letter avatar. */
33 avatar?: string | null;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.34 /** What an agent's face is drawn from when it chose none. */
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)35 avatar_seed?: string | null;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.36 /** An agent's chosen face (./agent-look.ts). */
37 look?: AgentLook | null;
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)38};
39
40export type FeedNotification = {
41 /** Unique per notification: the same id twice is told once. */
42 id: string;
43 kind: NotificationKind;
44 /** The workspace's slug. */
45 workspace: string;
46 title: string;
47 /** A short preview, a line or two. */
48 body: string;
49 /** Where clicking it goes, relative to the site. */
50 href: string;
51 actor: NotificationActor;
52 /** The conversation it is in, for a chat notification: toasts for the open one are skipped, and pushes collapse by it. */
53 channel_id?: string | null;
54 /** The thread it is in, for a reply: quick replies go there. */
55 thread_root?: string | null;
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread56 /**
57 * The chat card it is about, when that card has something to press: a
58 * session at its cap (Approve more, Stop, Open), a draft issue (File
59 * issue, Discard). The toast and the notifications panel show these
60 * actions, and pressing one goes to the site's `card_action`, exactly as
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.61 * on the card itself.
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread62 */
63 card?: NotificationCard | null;
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)64 created_at: string;
65};
66
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread67/** A card's place and its main actions, carried on a notification about it. */
68export type NotificationCard = {
69 /** The conversation the card is in. */
70 channel_id: string;
71 /** The message that is the card. */
72 message_id: string;
73 /** The card's actions as it offers them: ids, labels, inputs and links, the same as on the card. */
74 actions: CardAction[];
75};
76
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)77/** How much a person hears of. Counts always move; this governs toasts and pushes. */
78export type NotifyLevel = "all" | "dms_mentions" | "none";
79
80export const NOTIFY_LEVELS: readonly NotifyLevel[] = ["all", "dms_mentions", "none"];
81
82export type NotifyPreferences = {
83 level: NotifyLevel;
84 /** A different level for a workspace, by slug. */
85 workspaces: Record<string, NotifyLevel>;
86};
87
88/** A change to them: a workspace set to null goes back to the general level. */
89export type NotifyPreferencesChange = { level?: NotifyLevel; workspaces?: Record<string, NotifyLevel | null> };
90
91export const DEFAULT_NOTIFY_PREFERENCES: NotifyPreferences = { level: "dms_mentions", workspaces: {} };
92
93/** A browser's push subscription, as `PushSubscription.toJSON()` gives it. */
94export type PushSubscriptionJson = {
95 endpoint: string;
96 expiration_time?: number | null;
97 keys: { p256dh: string; auth: string };
98};
99
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)100/**
101 * A phone the g1t app runs on, as the app registers it: the push token
102 * Expo gave it (`ExponentPushToken[…]`), which platform, and a name to
103 * know it by (at most 100 characters).
104 */
105export type PushDevice = { token: string; platform: "android" | "ios"; name?: string | null };
106
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)107/** A conversation's unread counts for one person. */
108export type ChannelCounts = { channel_id: string; unread: number; mentions: number; muted: boolean };
109
110/**
111 * What a person has unread in a workspace. `chat_unread` leaves out muted
112 * conversations, as the rail's badge does; mentions count everywhere.
113 * `inbox_unread` is the person's whole inbox, which is not per workspace.
114 * `complete` says `per_channel` was read from chat itself (on connect), so
115 * a conversation left out of it has nothing unread.
116 */
117export type FeedCounts = {
118 workspace: string;
119 chat_unread: number;
120 chat_mentions: number;
121 inbox_unread: number;
122 per_channel: Omit<ChannelCounts, "muted">[];
123 complete: boolean;
124};
125
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers126// ── Presence and status ──────────────────────────────────────────────────
127//
128// Presence is worked out live by the person's feed from their open tabs:
129// `active` while a tab has been used in the last few minutes, `away` when
130// every tab has sat idle (or they set themselves away), `offline` once no
131// tab is open. Status is what they say about themselves: an emoji and a
132// few words, cleared at a time they chose. Do Not Disturb silences toasts
133// and pushes until a time; counts still move.
134//
135// Both are kept by the notify service (the feed, one per person) and told
136// to everyone who shares a workspace with them, over the same socket,
137// through one presence room per workspace. Nothing polls.
138
139export type Presence = "active" | "away" | "offline";
140
141/**
142 * Who set a status: the person, or something acting for them. Calendars
143 * and other integrations set `calendar` or `integration` (with their own
144 * `clear_at`); a status the person set by hand is never replaced by one.
145 */
146export type StatusSource = "manual" | "calendar" | "integration";
147
148export const STATUS_SOURCES: readonly StatusSource[] = ["manual", "calendar", "integration"];
149
150export type PersonStatus = {
151 /** One emoji, or a custom emoji's `:name:`; null for none. */
152 emoji: string | null;
153 /** A few words: "In a meeting". At most 100 characters. */
154 text: string;
155 /** When it clears itself (RFC 3339); null keeps it until changed. */
156 clear_at: string | null;
157 source: StatusSource;
158 /** When it was set (RFC 3339). */
159 set_at: string;
160};
161
162/** How a person shows to the people who share a workspace with them. */
163export type PresenceEntry = {
164 user_id: string;
165 username: string;
166 presence: Presence;
167 /** Do Not Disturb, until then (RFC 3339); null when off. */
168 dnd_until: string | null;
169 status: PersonStatus | null;
170 /** When this last changed, in ms: a later word wins. */
171 at: number;
172};
173
174/** The signed-in person's own: what everyone sees, and whether they set themselves away. */
175export type OwnPresence = PresenceEntry & { away_manual: boolean };
176
177/**
178 * A change to your own. `status: null` clears it; `away` sets (or ends)
179 * being away by hand; `dnd_until: null` resumes notifications.
180 */
181export type PresenceChange = {
182 status?: { emoji?: string | null; text: string; clear_at?: string | null; source?: StatusSource } | null;
183 away?: boolean;
184 dnd_until?: string | null;
185};
186
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)187/** What the feed socket sends a tab. */
188export type FeedEvent =
189 | { type: "hello"; notifications: FeedNotification[]; vapid_public_key: string | null; preferences: NotifyPreferences }
190 | { type: "notification"; notification: FeedNotification; toast: boolean }
191 | ({ type: "counts" } & FeedCounts)
192 | { type: "preferences"; preferences: NotifyPreferences }
193 /** The person's inbox count as it now is, after items arrive or are marked anywhere. */
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers194 | { type: "inbox"; unread: number }
195 /**
196 * People in a workspace: everyone known on connect (`full`), then each
197 * one as they change.
198 */
199 | { type: "presence"; workspace: string; people: PresenceEntry[]; full: boolean }
200 /** Your own presence, status and Do Not Disturb, on connect and after every change. */
201 | { type: "me"; me: OwnPresence };
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)202
203/**
204 * What a tab sends over the feed socket: plain `ping` every 25 s (answered
205 * without waking the feed), and a state frame whenever it gains or loses
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers206 * focus, moves to another page, or goes idle (no input for a while) or
207 * back, and the inbox count the page last read.
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)208 */
209export type FeedClientFrame =
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers210 | { type: "state"; focused: boolean; path: string; idle?: boolean }
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)211 | { type: "inbox"; unread: number };
212
213/**
214 * One delivery from chat: counts to move for one person in one
215 * conversation, and maybe a notification. `set` replaces the counts (after
216 * reading, or writing); otherwise they are added.
217 */
218export type FeedDelivery = {
219 user_id: string;
220 workspace: string;
221 counts?: {
222 channel_id: string;
223 unread: number;
224 mentions: number;
225 muted?: boolean | null;
226 set?: boolean;
227 } | null;
228 notification?: FeedNotification | null;
229};
230
231/** What the site hands the feed with a socket: who, and the counts read from chat and the inbox just now. */
232export type FeedSeed = {
233 workspace: string | null;
234 per_channel: ChannelCounts[] | null;
235 inbox_unread: number | null;
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers236 /** Every workspace the person belongs to, by slug: whose rooms hear of their presence. */
237 workspaces?: string[] | null;
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)238};
239
240/** Headers the site sets on a forwarded feed socket. */
241export const NOTIFY_VIEWER_HEADER = "x-g1t-notify-viewer";
242export const NOTIFY_SEED_HEADER = "x-g1t-notify-seed";
243
244export type NotifyStatus = {
245 preferences: NotifyPreferences;
246 /** How many browsers get pushes. */
247 subscriptions: number;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)248 /** How many phones the g1t app pushes to (absent from a service older than them). */
249 devices?: number;
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)250 /** Whether this browser's endpoint is one of them, when it was asked about. */
251 subscribed: boolean;
252 /** The public key a browser subscribes with; null when push is not set up. */
253 vapid_public_key: string | null;
254};
255
256export type NotifyApi = {
257 /** Tells a person: their open tabs at once, and a push when none is in front of them. */
258 notify(target: { user_id?: string; username?: string }, notification: FeedNotification): Promise<{ ok: boolean }>;
259 /** Many at once, as chat sends them for a message. */
260 deliver(items: FeedDelivery[]): Promise<{ ok: boolean }>;
261 subscribe(user: User, subscription: PushSubscriptionJson, userAgent?: string | null): Promise<{ ok: boolean }>;
262 unsubscribe(user: User, endpoint: string): Promise<{ ok: boolean }>;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)263 /** A phone the app runs on gets pushes too; the newest 10 are kept. `ok` is false for a token that is not Expo's. */
The phone app with your own g1t, on g1t.sh/download until Google Play, and Node 24 (#5)264 /** `off`: this g1t sends phones nothing (PHONE_PUSH=off). */
265 registerDevice(user: User, device: PushDevice): Promise<{ ok: boolean; off?: boolean }>;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)266 /** That phone stops getting them: the app signed out. */
267 unregisterDevice(user: User, token: string): Promise<{ ok: boolean }>;
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)268 status(user: User, endpoint?: string | null): Promise<NotifyStatus>;
269 setPreferences(user: User, preferences: NotifyPreferencesChange): Promise<NotifyPreferences>;
270 /** Sends the person a test notification, toasted and pushed whatever their focus. */
271 test(user: User): Promise<{ ok: boolean; pushed: number }>;
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers272 /** Your own presence, status and Do Not Disturb. */
273 presence(user: User): Promise<OwnPresence>;
274 /**
275 * Changes your own, and tells everyone who shares a workspace with you.
276 * Integrations set a status for someone the same way, with their own
277 * `source`; one the person set by hand is kept over theirs.
278 */
279 setPresence(user: Pick<User, "id" | "username">, change: PresenceChange): Promise<OwnPresence>;
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.280 /**
281 * For services: how a workspace's people show now (`userIds` narrows it).
282 * Someone the room has never heard from is not listed: they are offline.
283 */
284 workspacePresence(workspace: string, userIds?: string[] | null): Promise<PresenceEntry[]>;
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.285 /**
286 * When the person was last on the workspace's Home page (RFC 3339), or
287 * null when they never were. Kept with them, so it is the same on every
288 * device.
289 */
290 lastVisit(user: Pick<User, "id">, workspace: string): Promise<{ seen_at: string | null }>;
291 /**
292 * Marks that visit at `at` (RFC 3339): Home sends the time the page
293 * loaded, once the person has looked at it. It only moves forward, never
294 * past now nor more than a day behind it.
295 */
296 markVisit(user: Pick<User, "id">, workspace: string, at: string): Promise<{ seen_at: string | null }>;
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)297};
298
299async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
300 const response = await service.fetch(`https://service/rpc/${method}`, {
301 method: "POST",
302 headers: { "content-type": "application/json" },
303 body: JSON.stringify(args),
304 });
305 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
306 return (await response.json()) as T;
307}
308
309export function notifyClient(service: ServiceBinding): NotifyApi {
310 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
311 return {
312 notify: (target, notification) => call("notify", { user_id: target.user_id ?? null, username: target.username ?? null, notification }),
313 deliver: (items) => call("deliver", { items }),
314 subscribe: (user, subscription, userAgent) => call("subscribe", { user_id: user.id, subscription, user_agent: userAgent ?? null }),
315 unsubscribe: (user, endpoint) => call("unsubscribe", { user_id: user.id, endpoint }),
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)316 registerDevice: (user, device) => call("register_device", { user_id: user.id, device }),
317 unregisterDevice: (user, token) => call("unregister_device", { user_id: user.id, token }),
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)318 status: (user, endpoint) => call("status", { user_id: user.id, endpoint: endpoint ?? null }),
319 setPreferences: (user, preferences) => call("set_preferences", { user_id: user.id, preferences }),
320 test: (user) => call("test", { user_id: user.id, username: user.username }),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers321 presence: (user) => call("presence", { user_id: user.id, username: user.username }),
322 setPresence: (user, change) => call("set_presence", { user_id: user.id, username: user.username, change }),
People and teams are front and centre: one directory of people and agents with presence, local time, titles, teams and what each owns; profiles with manager and reports and the agents they work with; an org chart with each team's agents beside the person who leads it; and teams of any mix, with a lead, a channel, a budget agents keep to and the agents on them. Every agent is told its teams each turn (who leads, who owns what, who's around and who to page), and the team page shows exactly what. Member management is Members and invites; the people and teams guide says how.323 workspacePresence: (workspace, userIds) => call("workspace_presence", { workspace, user_ids: userIds ?? null }),
The front page is Home: where you're needed comes first, however long ago it started, with what's at stake and how long it has waited; then what happened since you were last here, in words, with the agent work that settled, what landed, what deployed, what was decided and what was spent, switchable to the last 24 hours or 7 days; then what's running now. Your last visit is kept with your account, so it's the same on every device; Today's old address leads here. The home guide says how.324 lastVisit: (user, workspace) => call("last_visit", { user_id: user.id, workspace }),
325 markVisit: (user, workspace, at) => call("mark_visit", { user_id: user.id, workspace, at }),
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)326 };
327}

This file's history is long; its oldest lines are credited to the oldest commit read.