Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
379 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 */
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread11import 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)12import type { ServiceBinding } from "./clients";
13import type { User } from "./identity";
14
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r915/**
16 * What a notification is about; the preferences decide which ones toast and
17 * push. `security` is an urgent security alert (critical or high, a refused
18 * push, a vulnerability report), shown like a mention; the rest of the
19 * security items are ordinary `inbox` ones.
20 */
Merge branch 'worktree-agent-ac950b9c891220ad3'21export type NotificationKind =
22 | "dm"
23 | "mention"
24 | "thread_reply"
25 | "inbox"
26 /** Retired with g1t's own agents (a session at its cap); kept so stored items and preferences still read. */
27 | "approval"
28 | "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)29
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.30export const NOTIFICATION_KINDS: readonly NotificationKind[] = ["dm", "mention", "thread_reply", "inbox", "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)31
32/** Who it is from: a person, an agent, or g1t itself. */
33export type NotificationActor = {
34 kind: "user" | "agent" | "system";
35 id: string;
36 /** How they show: a display name. */
37 name: string;
38 /** A person's uploaded avatar hash, or null for the letter avatar. */
39 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.40 /** 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)41 avatar_seed?: string | null;
42};
43
44export type FeedNotification = {
45 /** Unique per notification: the same id twice is told once. */
46 id: string;
47 kind: NotificationKind;
48 /** The workspace's slug. */
49 workspace: string;
50 title: string;
51 /** A short preview, a line or two. */
52 body: string;
53 /** Where clicking it goes, relative to the site. */
54 href: string;
55 actor: NotificationActor;
56 /** The conversation it is in, for a chat notification: toasts for the open one are skipped, and pushes collapse by it. */
57 channel_id?: string | null;
58 /** The thread it is in, for a reply: quick replies go there. */
59 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 thread60 /**
Merge branch 'worktree-agent-ac950b9c891220ad3'61 * The chat card it is about, when that card has something to press,
62 * such as a draft issue (File issue, Discard). The toast and the notifications panel show these
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread63 * 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.64 * 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 thread65 */
66 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)67 created_at: string;
68};
69
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread70/** A card's place and its main actions, carried on a notification about it. */
71export type NotificationCard = {
72 /** The conversation the card is in. */
73 channel_id: string;
74 /** The message that is the card. */
75 message_id: string;
76 /** The card's actions as it offers them: ids, labels, inputs and links, the same as on the card. */
77 actions: CardAction[];
78};
79
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)80/** How much a person hears of. Counts always move; this governs toasts and pushes. */
81export type NotifyLevel = "all" | "dms_mentions" | "none";
82
83export const NOTIFY_LEVELS: readonly NotifyLevel[] = ["all", "dms_mentions", "none"];
84
85export type NotifyPreferences = {
86 level: NotifyLevel;
87 /** A different level for a workspace, by slug. */
88 workspaces: Record<string, NotifyLevel>;
89};
90
91/** A change to them: a workspace set to null goes back to the general level. */
92export type NotifyPreferencesChange = { level?: NotifyLevel; workspaces?: Record<string, NotifyLevel | null> };
93
94export const DEFAULT_NOTIFY_PREFERENCES: NotifyPreferences = { level: "dms_mentions", workspaces: {} };
95
96/** A browser's push subscription, as `PushSubscription.toJSON()` gives it. */
97export type PushSubscriptionJson = {
98 endpoint: string;
99 expiration_time?: number | null;
100 keys: { p256dh: string; auth: string };
101};
102
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)103/**
104 * A phone the g1t app runs on, as the app registers it: the push token
105 * Expo gave it (`ExponentPushToken[…]`), which platform, and a name to
106 * know it by (at most 100 characters).
107 */
108export type PushDevice = { token: string; platform: "android" | "ios"; name?: string | null };
109
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)110/** A conversation's unread counts for one person. */
111export type ChannelCounts = { channel_id: string; unread: number; mentions: number; muted: boolean };
112
113/**
114 * What a person has unread in a workspace. `chat_unread` leaves out muted
115 * conversations, as the rail's badge does; mentions count everywhere.
116 * `inbox_unread` is the person's whole inbox, which is not per workspace.
117 * `complete` says `per_channel` was read from chat itself (on connect), so
118 * a conversation left out of it has nothing unread.
119 */
120export type FeedCounts = {
121 workspace: string;
122 chat_unread: number;
123 chat_mentions: number;
124 inbox_unread: number;
125 per_channel: Omit<ChannelCounts, "muted">[];
126 complete: boolean;
127};
128
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers129// ── Presence and status ──────────────────────────────────────────────────
130//
131// Presence is worked out live by the person's feed from their open tabs:
132// `active` while a tab has been used in the last few minutes, `away` when
133// every tab has sat idle (or they set themselves away), `offline` once no
134// tab is open. Status is what they say about themselves: an emoji and a
135// few words, cleared at a time they chose. Do Not Disturb silences toasts
136// and pushes until a time; counts still move.
137//
138// Both are kept by the notify service (the feed, one per person) and told
139// to everyone who shares a workspace with them, over the same socket,
140// through one presence room per workspace. Nothing polls.
141
142export type Presence = "active" | "away" | "offline";
143
144/**
145 * Who set a status: the person, or something acting for them. Calendars
146 * and other integrations set `calendar` or `integration` (with their own
147 * `clear_at`); a status the person set by hand is never replaced by one.
148 */
149export type StatusSource = "manual" | "calendar" | "integration";
150
151export const STATUS_SOURCES: readonly StatusSource[] = ["manual", "calendar", "integration"];
152
153export type PersonStatus = {
154 /** One emoji, or a custom emoji's `:name:`; null for none. */
155 emoji: string | null;
156 /** A few words: "In a meeting". At most 100 characters. */
157 text: string;
158 /** When it clears itself (RFC 3339); null keeps it until changed. */
159 clear_at: string | null;
160 source: StatusSource;
161 /** When it was set (RFC 3339). */
162 set_at: string;
163};
164
165/** How a person shows to the people who share a workspace with them. */
166export type PresenceEntry = {
167 user_id: string;
168 username: string;
169 presence: Presence;
170 /** Do Not Disturb, until then (RFC 3339); null when off. */
171 dnd_until: string | null;
172 status: PersonStatus | null;
173 /** When this last changed, in ms: a later word wins. */
174 at: number;
175};
176
177/** The signed-in person's own: what everyone sees, and whether they set themselves away. */
178export type OwnPresence = PresenceEntry & { away_manual: boolean };
179
180/**
181 * A change to your own. `status: null` clears it; `away` sets (or ends)
182 * being away by hand; `dnd_until: null` resumes notifications.
183 */
184export type PresenceChange = {
185 status?: { emoji?: string | null; text: string; clear_at?: string | null; source?: StatusSource } | null;
186 away?: boolean;
187 dnd_until?: string | null;
188};
189
Merge branch 'worktree-agent-a5c94df323642630e'190// ── Live repository pages ────────────────────────────────────────────────
191//
192// A tab on a repository's page asks to watch it (`watchRepo`, through the
193// site, which first checks the person may read it). The events service
194// then tells notify of everything that happens there that a page shows
195// (`repoChanged`, from crates/contracts/src/live.rs), and the actions
196// service of runs' jobs starting and finishing; notify passes each change
197// to the feeds watching that repository (one topic per repository), whose
198// tabs refresh the page (apps/web/app/lib/repo-live.ts).
199
200/**
201 * Something that happened in a repository, as live pages hear of it: what
202 * kind of thing moved and where, never anything written in it.
203 */
204export type RepoChange = {
205 repo_id: string;
206 /** `owner/name`, as the person watching it asked for it; filled in by notify. */
207 repo?: string | null;
208 /** The event's type (`git.push`, `pull.updated`, `checks.completed`, …), or `run.updated` for a run's job starting or finishing. */
209 kind: string;
210 /** The full ref a push or deletion moved (`refs/heads/main`), or a run's. */
211 ref?: string | null;
212 /** The issue or pull request it is on, or the pull request a run ran for. */
213 number?: number | null;
214 /** The commit: where a push left its ref, the one checked or reported on. */
215 sha?: string | null;
216 /** A workflow run's id. */
217 run?: string | null;
218 /** How many commits a push brought to its branch, when it was counted. */
219 commits?: number | null;
220 /** RFC 3339. */
221 at: string;
222};
223
224/** How long one `watchRepo` lasts: a page open longer asks again before it runs out. */
225export const REPO_WATCH_MS = 15 * 60_000;
226
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)227/** What the feed socket sends a tab. */
228export type FeedEvent =
229 | { type: "hello"; notifications: FeedNotification[]; vapid_public_key: string | null; preferences: NotifyPreferences }
230 | { type: "notification"; notification: FeedNotification; toast: boolean }
231 | ({ type: "counts" } & FeedCounts)
232 | { type: "preferences"; preferences: NotifyPreferences }
233 /** 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 managers234 | { type: "inbox"; unread: number }
235 /**
236 * People in a workspace: everyone known on connect (`full`), then each
237 * one as they change.
238 */
239 | { type: "presence"; workspace: string; people: PresenceEntry[]; full: boolean }
240 /** Your own presence, status and Do Not Disturb, on connect and after every change. */
Merge branch 'worktree-agent-a5c94df323642630e'241 | { type: "me"; me: OwnPresence }
242 /** What changed in repositories you are watching the pages of (`watchRepo`). */
243 | { type: "repo"; changes: RepoChange[] };
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)244
245/**
246 * What a tab sends over the feed socket: plain `ping` every 25 s (answered
247 * 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 managers248 * focus, moves to another page, or goes idle (no input for a while) or
249 * 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)250 */
251export 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 managers252 | { 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)253 | { type: "inbox"; unread: number };
254
255/**
256 * One delivery from chat: counts to move for one person in one
257 * conversation, and maybe a notification. `set` replaces the counts (after
258 * reading, or writing); otherwise they are added.
259 */
260export type FeedDelivery = {
261 user_id: string;
262 workspace: string;
263 counts?: {
264 channel_id: string;
265 unread: number;
266 mentions: number;
267 muted?: boolean | null;
268 set?: boolean;
269 } | null;
270 notification?: FeedNotification | null;
271};
272
273/** What the site hands the feed with a socket: who, and the counts read from chat and the inbox just now. */
274export type FeedSeed = {
275 workspace: string | null;
276 per_channel: ChannelCounts[] | null;
277 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 managers278 /** Every workspace the person belongs to, by slug: whose rooms hear of their presence. */
279 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)280};
281
282/** Headers the site sets on a forwarded feed socket. */
283export const NOTIFY_VIEWER_HEADER = "x-g1t-notify-viewer";
284export const NOTIFY_SEED_HEADER = "x-g1t-notify-seed";
285
286export type NotifyStatus = {
287 preferences: NotifyPreferences;
288 /** How many browsers get pushes. */
289 subscriptions: number;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)290 /** How many phones the g1t app pushes to (absent from a service older than them). */
291 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)292 /** Whether this browser's endpoint is one of them, when it was asked about. */
293 subscribed: boolean;
294 /** The public key a browser subscribes with; null when push is not set up. */
295 vapid_public_key: string | null;
296};
297
298export type NotifyApi = {
299 /** Tells a person: their open tabs at once, and a push when none is in front of them. */
300 notify(target: { user_id?: string; username?: string }, notification: FeedNotification): Promise<{ ok: boolean }>;
301 /** Many at once, as chat sends them for a message. */
302 deliver(items: FeedDelivery[]): Promise<{ ok: boolean }>;
303 subscribe(user: User, subscription: PushSubscriptionJson, userAgent?: string | null): Promise<{ ok: boolean }>;
304 unsubscribe(user: User, endpoint: string): Promise<{ ok: boolean }>;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)305 /** 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)306 /** `off`: this g1t sends phones nothing (PHONE_PUSH=off). */
307 registerDevice(user: User, device: PushDevice): Promise<{ ok: boolean; off?: boolean }>;
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)308 /** That phone stops getting them: the app signed out. */
309 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)310 status(user: User, endpoint?: string | null): Promise<NotifyStatus>;
311 setPreferences(user: User, preferences: NotifyPreferencesChange): Promise<NotifyPreferences>;
312 /** Sends the person a test notification, toasted and pushed whatever their focus. */
313 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 managers314 /** Your own presence, status and Do Not Disturb. */
315 presence(user: User): Promise<OwnPresence>;
316 /**
317 * Changes your own, and tells everyone who shares a workspace with you.
318 * Integrations set a status for someone the same way, with their own
319 * `source`; one the person set by hand is kept over theirs.
320 */
321 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.322 /**
323 * For services: how a workspace's people show now (`userIds` narrows it).
324 * Someone the room has never heard from is not listed: they are offline.
325 */
326 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.327 /**
328 * When the person was last on the workspace's Home page (RFC 3339), or
329 * null when they never were. Kept with them, so it is the same on every
330 * device.
331 */
332 lastVisit(user: Pick<User, "id">, workspace: string): Promise<{ seen_at: string | null }>;
333 /**
334 * Marks that visit at `at` (RFC 3339): Home sends the time the page
335 * loaded, once the person has looked at it. It only moves forward, never
336 * past now nor more than a day behind it.
337 */
338 markVisit(user: Pick<User, "id">, workspace: string, at: string): Promise<{ seen_at: string | null }>;
Merge branch 'worktree-agent-a5c94df323642630e'339 /**
340 * The person's tabs hear what changes in the repository, for
341 * `REPO_WATCH_MS`. The caller must have checked they may read it: notify
342 * checks nothing. `repo` is its `owner/name`, passed on with each change.
343 */
344 watchRepo(user: Pick<User, "id">, repoId: string, repo: string): Promise<{ ok: boolean; until: string | null }>;
345 /** For services: what changed in repositories (`crates/contracts/src/live.rs`), told to whoever watches each. */
346 repoChanged(changes: RepoChange[]): 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)347};
348
349async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
350 const response = await service.fetch(`https://service/rpc/${method}`, {
351 method: "POST",
352 headers: { "content-type": "application/json" },
353 body: JSON.stringify(args),
354 });
355 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
356 return (await response.json()) as T;
357}
358
359export function notifyClient(service: ServiceBinding): NotifyApi {
360 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
361 return {
362 notify: (target, notification) => call("notify", { user_id: target.user_id ?? null, username: target.username ?? null, notification }),
363 deliver: (items) => call("deliver", { items }),
364 subscribe: (user, subscription, userAgent) => call("subscribe", { user_id: user.id, subscription, user_agent: userAgent ?? null }),
365 unsubscribe: (user, endpoint) => call("unsubscribe", { user_id: user.id, endpoint }),
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)366 registerDevice: (user, device) => call("register_device", { user_id: user.id, device }),
367 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)368 status: (user, endpoint) => call("status", { user_id: user.id, endpoint: endpoint ?? null }),
369 setPreferences: (user, preferences) => call("set_preferences", { user_id: user.id, preferences }),
370 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 managers371 presence: (user) => call("presence", { user_id: user.id, username: user.username }),
372 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.373 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.374 lastVisit: (user, workspace) => call("last_visit", { user_id: user.id, workspace }),
375 markVisit: (user, workspace, at) => call("mark_visit", { user_id: user.id, workspace, at }),
Merge branch 'worktree-agent-a5c94df323642630e'376 watchRepo: (user, repoId, repo) => call("watch_repo", { user_id: user.id, repo_id: repoId, repo }),
377 repoChanged: (changes) => call("repo_changed", { changes }),
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)378 };
379}

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