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

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * Chat: channels, direct messages, threads and messages, kept by the chat
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.3 * service (`services/chat`). People and agents are members alike.
Chat and workspace agents: channels, DMs and named agents you talk to4 *
5 * Wire shapes are snake_case end to end, so the site, the public API and
6 * the live socket all carry the same objects.
7 */
8import type { ServiceBinding } from "./clients";
9import type { User } from "./identity";
10import type { Result } from "./result";
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";
Chat and workspace agents: channels, DMs and named agents you talk to12import type { AskerAccess } from "./workspace-agents";
13
14/** Who is speaking: a person (by user id) or a workspace agent (by agent id). */
15export type Principal = { kind: "user" | "agent"; id: string };
16
17export function principalKey(principal: Principal): string {
18 return `${principal.kind}:${principal.id}`;
19}
20
21export function parsePrincipalKey(key: string): Principal | null {
22 const at = key.indexOf(":");
23 if (at < 0) return null;
24 const kind = key.slice(0, at);
25 const id = key.slice(at + 1);
26 if ((kind !== "user" && kind !== "agent") || !id) return null;
27 return { kind, id };
28}
29
30/** How a member shows: resolved by the chat service when it answers. */
31export type MemberProfile = Principal & {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar32 /** `username` for a person (lowercased: what `@` mentions), `handle` for an agent. */
Chat and workspace agents: channels, DMs and named agents you talk to33 name: string;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar34 /**
35 * A person's username as they wrote it (`Ana`), when that differs from
36 * `name`; absent for an agent. Shown in their card and in autocomplete.
37 */
38 display_username?: string | null;
39 /**
40 * What chat shows them as: a person's display name, else their username
41 * in its chosen case; an agent's display name. Read it with `memberName`.
42 */
Chat and workspace agents: channels, DMs and named agents you talk to43 display_name: string;
44 /** Uploaded avatar hash for a person, or null for the letter avatar. */
45 avatar: string | null;
46 /** An agent's one-line role ("Release manager for g1t"). */
47 role: 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)48 /**
49 * An agent's title ("QA Engineer"); null for a person. Chat always sets
50 * it; optional so profiles a page makes for itself need not.
51 */
52 title?: 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.53 /** What an agent's face is drawn from when it chose none; null for a person. Always set by 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)54 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.55 /** An agent's chosen face (./agent-look.ts); null for a person, or an agent wearing its seed's face. */
56 look?: AgentLook | null;
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.57 /**
58 * An archived agent (docs.g1t.sh/guides/chat/, "Archived and deleted
59 * agents"): when, and by whom. Its conversations stay readable; it is
60 * never woken and never listed to talk to. Null or absent for a live
61 * agent and for a person.
62 */
63 archived_at?: string | null;
64 archived_by?: string | null;
65 /** An agent deleted for good: its messages stay under the name it had. Absent otherwise. */
66 deleted?: boolean;
Chat and workspace agents: channels, DMs and named agents you talk to67};
68
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar69/**
70 * A member's handle as it shows (`@Ana`, without the `@`): a person's
71 * username in its chosen case, an agent's handle.
72 */
73export function memberHandle(member: { name: string; display_username?: string | null }): string {
74 const display = member.display_username;
75 return display && display.toLowerCase() === member.name.toLowerCase() ? display : member.name;
76}
77
78/**
79 * How a member's name is shown everywhere in chat (messages, the
80 * sidebar, direct-message titles, typing, notifications and pushes): their
81 * display name, else their handle in its chosen case. One rule, so every
82 * surface agrees.
83 */
84export function memberName(member: { name: string; display_name?: string | null; display_username?: string | null }): string {
85 return member.display_name?.trim() || memberHandle(member);
86}
87
Chat and workspace agents: channels, DMs and named agents you talk to88export type ChannelKind = "channel" | "dm";
89
90export type Channel = {
91 id: string;
92 workspace_id: string;
93 kind: ChannelKind;
94 /** Lowercase, no `#`. Null for a direct message. */
95 name: string | null;
96 topic: string | null;
97 private: boolean;
98 created_by: Principal;
99 created_at: string;
100 archived_at: string | null;
101 last_message_at: string | null;
102};
103
104export type ChannelMember = {
105 channel_id: string;
106 member: MemberProfile;
107 role: "owner" | "member";
108 starred: boolean;
109 muted: boolean;
110 last_read_id: string | null;
111 joined_at: string;
112};
113
114/** A card g1t or an agent posts: an event, a task, an approval. */
115export type MessageCard = {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar116 /** What it is about, e.g. `pull`, `issue`, `task`, `deploy`, `approval`, `session`, `draft_issue`. */
Chat and workspace agents: channels, DMs and named agents you talk to117 kind: string;
118 title: string;
119 /** A short line under the title: "3/3 checks · +214 −87". */
120 detail: string | null;
121 /** A status shown on the right: "Needs approval", "Merged". */
122 state: string | null;
123 /** Where clicking the card goes, relative to the site. */
124 href: string | null;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar125 /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */
126 body?: string | null;
127 /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */
128 fields?: CardField[];
129 /**
130 * What people can do right here. Pressing one goes to the service that
131 * owns the card (`owner`), which checks the person may, does it, and
132 * updates the card in place. A card without `owner` has no actions.
133 */
134 actions?: CardAction[];
135 /** The service whose card this is and that answers its actions: `agents`. */
136 owner?: "agents" | null;
137 /** What the card is about, for its owner: a session id, a draft id. */
138 ref?: string | null;
139};
140
141export type CardField = { label: string; value: string };
142
143/** A button on a card. */
144export type CardAction = {
145 /** Unique on the card: `stop`, `approve`, `file`. */
146 id: string;
147 label: string;
148 style?: "primary" | "danger" | "default";
149 /** Asked before it runs: "Stop this session and everything under it?". */
150 confirm?: string | null;
151 /** A value it needs first, asked inline: an amount in dollars, or a line of text. */
152 input?: { kind: "money" | "text"; label: string; placeholder?: string | null; initial?: string | null } | null;
153 /** A link instead of an action: opens this place in the site. */
154 href?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to155};
156
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar157/** What pressing a card's action did, as the person who pressed it is told. */
158export type CardActionResult = { ok: boolean; message: string | null };
159
Chat and workspace agents: channels, DMs and named agents you talk to160export type ChatMessage = {
161 /** Time-sortable (ULID-like), so ordering by id is ordering by time. */
162 id: string;
163 channel_id: string;
164 author: MemberProfile;
165 kind: "text" | "card";
166 body: string;
167 card: MessageCard | null;
168 /** The message this replies under, or null for a top-level message. */
169 thread_root: string | null;
170 reply_count: number;
171 last_reply_at: string | null;
172 created_at: string;
173 edited_at: string | null;
174 deleted_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)175 /**
176 * Its reactions, one per emoji in the order each was first used. Chat
177 * always sets it; optional so messages a page makes for itself need not.
178 */
179 reactions?: ChatReaction[];
Chat and workspace agents: channels, DMs and named agents you talk to180};
181
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)182/**
183 * One emoji's reactions on a message. `emoji` is a Unicode emoji or
184 * `:name:` for one of the workspace's own (`CustomEmoji`).
185 */
186export type ChatReaction = {
187 emoji: string;
188 /** Everyone who reacted with it, people and agents alike. */
189 count: number;
190 /**
191 * Whether the viewer did. In live `message.*` events, which everyone in
192 * the room gets, it is always false: keep your own from what you know.
193 */
194 me: boolean;
195 /** The first ten who reacted with it, for the hover list. */
196 by: MemberProfile[];
197};
198
199/** Who may add a workspace's emoji: every member (the default), or only its owners. */
200export type EmojiUpload = "members" | "admins";
201
202/**
203 * One of a workspace's own emoji, used as `:name:`. Its image is served
204 * from the usercontent origin at `/emoji/<file>` (never from the site).
205 */
206export type CustomEmoji = {
207 name: string;
208 /** For an alias, the emoji it is another name for; it shows that one's image. */
209 alias_of: string | null;
210 /** The SHA-256 of the image's bytes. */
211 file: string;
212 content_type: "image/png" | "image/gif" | "image/webp";
213 bytes: number;
214 created_by: MemberProfile;
215 created_at: string;
216};
217
218export type EmojiList = {
219 emoji: CustomEmoji[];
220 emoji_upload: EmojiUpload;
221 /** Whether the viewer may add emoji and aliases. */
222 can_upload: boolean;
223 /** Whether the viewer is an owner: may remove any emoji and change `emoji_upload`. */
224 can_manage: boolean;
225};
226
Chat controls, public profiles, shadcn selects, and no Docs tab in a project227/** Who may do something in a workspace's chat: every member, or only its owners. */
228export type ChatAllowed = "members" | "owners";
229
230/**
231 * Who may rename and archive a channel: its owners (whoever made it) and
232 * the workspace's owners (the default), or the workspace's owners only.
233 */
234export type ChannelManagers = "channel_owners" | "owners";
235
236/**
237 * A workspace's chat settings, which its owners choose (Workspace,
238 * Settings, Chat). The chat service enforces each one; the page only
239 * hides what the viewer may not do.
240 */
241export type ChatSettings = {
242 /** Who may create public channels. */
243 public_channels: ChatAllowed;
244 /** Who may create private channels. */
245 private_channels: ChatAllowed;
246 /** Who may rename, archive and unarchive channels. */
247 manage_channels: ChannelManagers;
248 /** Who may add custom emoji (`admins` is the workspace's owners). */
249 emoji_upload: EmojiUpload;
250 /**
251 * The public channels someone new is put in the first time they open
252 * Chat, by id. #general unless the owners chose otherwise.
253 */
254 default_channels: string[];
255};
256
257/** What the viewer may do in a workspace's chat, from its settings and their role. */
258export type ChatPermissions = {
259 create_public_channels: boolean;
260 create_private_channels: boolean;
261 add_emoji: boolean;
262 /** Whether they may change the workspace's chat settings: owners only. */
263 manage_settings: boolean;
264};
265
266/** The chat settings page: the settings, what the viewer may do, and the public channels to choose defaults from. */
267export type ChatSettingsView = {
268 settings: ChatSettings;
269 can: ChatPermissions;
270 channels: Channel[];
271};
272
273/** A change to a channel: its name, its topic, or whether it is archived. Anything left out stays. */
274export type ChannelChange = { name?: string; topic?: string | null; archived?: boolean };
275
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)276/** An image for a new emoji, as base64. Its type is read from its bytes, never taken from here. */
277export type EmojiFile = { data: string };
278
279/** The largest custom emoji image, in bytes, and the widest or tallest, in pixels. */
280export const MAX_EMOJI_BYTES = 256 * 1024;
281export const MAX_EMOJI_SIDE = 512;
282
Chat and workspace agents: channels, DMs and named agents you talk to283/** One row of the Chat sidebar. */
284export type ChatSidebarEntry = {
285 channel: Channel;
286 /** "g1t-core", or the other members' names for a direct message. */
287 title: string;
288 /** For a direct message: who else is in it (up to four). */
289 others: MemberProfile[];
290 starred: boolean;
291 muted: boolean;
292 unread: number;
293 mentions: number;
294};
295
296export type ChatSidebar = {
297 entries: ChatSidebarEntry[];
298 /** Public channels in the workspace the viewer has not joined. */
299 browsable: number;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project300 /** What the viewer may do, from the workspace's chat settings: the page hides what they may not. */
301 can: ChatPermissions;
Chat and workspace agents: channels, DMs and named agents you talk to302};
303
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.304/**
305 * `activity`: what was said over a span, in the conversations the viewer
306 * can read (every public channel, and the private channels and direct
307 * messages they are in). Messages are text messages and thread replies
308 * that were not deleted; cards agents post are not messages. Home reads it
309 * for what people and agents said since you were last there.
310 */
311export type ChatActivity = {
312 /** RFC 3339: `[from, until)`. */
313 from: string;
314 until: string;
315 messages: number;
316 /** Conversations with at least one message. */
317 channels: number;
318 /** Who said how much: member keys (`user:<id>`, `agent:<id>`), most first. */
319 authors: { key: string; messages: number }[];
320};
321
Chat and workspace agents: channels, DMs and named agents you talk to322export type MessagePage = {
323 messages: ChatMessage[];
324 /** Pass as `before` to read further back; null at the beginning. */
325 older: string | null;
326 /**
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread327 * For a thread's first page: the message the thread is under, however
328 * many replies it has, so a thread panel always shows it (a session's
329 * live card, say) at its top.
330 */
331 root?: ChatMessage | null;
332 /**
Chat and workspace agents: channels, DMs and named agents you talk to333 * Set when the page was read with `after`: pass it as `after` again for
334 * the next messages, or null when this page reached the newest.
335 */
336 newer?: string | null;
337};
338
339export type NewChannel = { name: string; topic?: string | null; private?: boolean };
340
341export type PostMessage = { body: string; thread_root?: string | null };
342
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)343/**
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.344 * Who will read what is said in a conversation: a direct message's or
345 * private channel's people, or, for a public channel, the whole workspace.
346 * An agent answers there only with what every one of them may see.
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 */
348export type ChatAudience = {
349 kind: "dm" | "private" | "public";
350 /** The people in it (agents left out). For a public channel, its current members, for reference only. */
351 member_user_ids: string[];
352 member_count: number;
353};
354
355/** A message an agent found by searching, with the conversation it is in. */
356export type AgentFoundMessage = {
357 channel_id: string;
358 /** The channel's name, or null for a direct message. */
359 channel: string | null;
360 message: ChatMessage;
361};
362
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.363/** The most agent-to-agent hops one person's request may start. */
Chat and workspace agents: channels, DMs and named agents you talk to364export const CHAT_MAX_HOPS = 6;
365
366/**
Merge branch 'worktree-agent-a1398e81ad1a64c5f'367 * What an agent posts. An agent's message never wakes another agent, even
368 * when it @mentions one: only a hand-off does (`handOffAsAgent`). Its
369 * mentions of anyone who is not a member of the conversation lose their
370 * `@`, so they show as plain names and notify nobody. When it answers a
371 * delivery, it passes that delivery's `hops`, `asked_by`, `asker` and
372 * `chain` back, which a card that waits on the asker uses. Left out: asked
Chat and workspace agents: channels, DMs and named agents you talk to373 * by the person who created the agent.
374 */
375export type AgentPostMessage = PostMessage & {
376 card?: MessageCard | null;
377 hops?: number;
378 asked_by?: string | null;
379 /** The delivery's `asker`, handed on to agents this message wakes. Absent: none (they treat the asker as unable to change code). */
380 asker?: AskerAccess | 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)381 /**
382 * The delivery's `chain`: the agents that handled this request before
383 * the one posting, oldest first. Chat adds the poster, and never hands
384 * the message to the agent that sent the work to it (no ping-pong).
385 */
386 chain?: string[];
Chat and workspace agents: channels, DMs and named agents you talk to387};
388
389/**
Merge branch 'worktree-agent-a1398e81ad1a64c5f'390 * A conversation as an agent is told about it before every turn: what it
391 * is and who is in it, so it knows who reads what it says and who doesn't.
392 * Every agent member is listed; people up to `CONVERSATION_PEOPLE_SHOWN`
393 * (the person who asked always among them), with the totals beside.
394 */
395export type ConversationForAgent = {
396 channel: Channel;
397 /** Agents first, then people. */
398 members: MemberProfile[];
399 /** How many people and agents are in it, listed or not. */
400 people: number;
401 agents: number;
402};
403
404/** The most people `conversationForAgent` lists; the rest are a count. */
405export const CONVERSATION_PEOPLE_SHOWN = 20;
406
407/**
408 * An agent handing work to a colleague agent for the person who asked
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.409 * (docs.g1t.sh/guides/agents/, "Hand off"). The fields after `brief`
Merge branch 'worktree-agent-a1398e81ad1a64c5f'410 * are the delivery the agent is answering, passed back as for a post.
411 */
412export type AgentHandOff = {
413 /** The colleague, by agent id. */
414 colleague_id: string;
415 /** What they are asked to do, addressed to them. */
416 brief: string;
417 thread_root?: string | null;
418 hops?: number;
419 asked_by: string;
420 asker?: AskerAccess | null;
421 chain?: string[];
422};
423
424/**
425 * Where a hand-off went. `here`: the colleague is in this channel or group
426 * direct message, and the brief was posted here. `group_dm`: the brief was
427 * posted in the direct message of the person who asked, the agent and the
428 * colleague (`opened` when it was new), and a card linking to it was
429 * posted here.
430 */
431export type HandOffResult = { where: "here" | "group_dm"; channel_id: string; message_id: string; opened: boolean };
432
433/**
Chat and workspace agents: channels, DMs and named agents you talk to434 * What the live socket sends. The site opens
435 * `wss://<site>/<workspace>/chat/live?channel=<id>`; the site checks the
436 * session and forwards the upgrade to the chat service with the viewer.
437 */
438export type ChatLiveEvent =
439 | { type: "message.created"; message: ChatMessage }
440 | { type: "message.updated"; message: ChatMessage }
441 | { type: "message.deleted"; channel_id: string; id: string }
442 | { type: "typing"; channel_id: string; member: MemberProfile; until: string }
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)443 | { type: "read"; channel_id: string; principal: Principal; last_read_id: string }
Chat controls, public profiles, shadcn selects, and no Docs tab in a project444 | { type: "channel.updated"; channel: Channel }
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)445 | { type: "reaction.added" | "reaction.removed"; channel_id: string; message_id: string; emoji: string; member: MemberProfile };
Chat and workspace agents: channels, DMs and named agents you talk to446
447/**
448 * Header the site sets on a forwarded live socket: the viewer, as JSON.
449 * The site forwards the upgrade to the chat service's
450 * `GET /live?workspace=<slug>&channel=<id>` (`workspace` may be left out,
451 * at the cost of looking up each of the viewer's workspaces).
452 */
453export const CHAT_VIEWER_HEADER = "x-g1t-chat-viewer";
454
Chat controls, public profiles, shadcn selects, and no Docs tab in a project455/** A channel with its members, and whether the viewer may rename or archive it. */
456export type ChannelDetail = { channel: Channel; members: ChannelMember[]; can_manage: boolean };
457
Chat and workspace agents: channels, DMs and named agents you talk to458export type ChatApi = {
459 sidebar(workspace: string, viewer: User): Promise<Result<ChatSidebar>>;
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.460 /** What was said in `[from, until)` (RFC 3339) where the viewer can read, counted. */
461 activity(workspace: string, viewer: User, span: { from: string; until: string }): Promise<Result<ChatActivity>>;
Chat and workspace agents: channels, DMs and named agents you talk to462 channel(
463 workspace: string,
464 channelId: string,
465 viewer: User,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project466 ): Promise<Result<ChannelDetail>>;
Chat and workspace agents: channels, DMs and named agents you talk to467 /** The same, found by its name in the workspace (`#general` or `general`), as the site's URLs name channels. */
468 channelByName(
469 workspace: string,
470 name: string,
471 viewer: User,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project472 ): Promise<Result<ChannelDetail>>;
473 /**
474 * Browse channels: every public channel, and the private ones the viewer
475 * is in. With `archived`, the archived ones instead.
476 */
477 browse(workspace: string, viewer: User, options?: { archived?: boolean }): Promise<Result<Channel[]>>;
478 /** Makes a channel. Who may make a public or a private one is the workspace's setting. */
Chat and workspace agents: channels, DMs and named agents you talk to479 createChannel(workspace: string, viewer: User, input: NewChannel): Promise<Result<Channel>>;
480 /**
Chat controls, public profiles, shadcn selects, and no Docs tab in a project481 * Renames, archives or unarchives a channel (the workspace's
482 * `manage_channels` setting says who may), or changes its topic (any
483 * member). #general is never renamed or archived. Everyone looking at it
484 * gets `channel.updated`.
485 */
486 updateChannel(workspace: string, channelId: string, viewer: User, change: ChannelChange): Promise<Result<Channel>>;
487 /** The workspace's chat settings, for any member; only owners may change them. */
488 chatSettings(workspace: string, viewer: User): Promise<Result<ChatSettingsView>>;
489 /** Owners only: changes some of the workspace's chat settings, and answers all of them. */
490 setChatSettings(workspace: string, viewer: User, change: Partial<ChatSettings>): Promise<Result<ChatSettings>>;
491 /**
Chat and workspace agents: channels, DMs and named agents you talk to492 * The direct message between the viewer and these members, created on
493 * first use. The same set of members always gets the same channel.
494 */
495 openDm(workspace: string, viewer: User, members: Principal[]): Promise<Result<Channel>>;
496 join(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
497 leave(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.498 /**
499 * Internal, from the agents service: an agent was deleted for good. It
500 * leaves every channel of the workspace; its direct messages stay their
501 * people's, readable, and its messages everywhere stay, shown under the
502 * name it had (`former_agents`) and marked deleted.
503 */
504 agentDestroyed(input: { workspace_id: string; agent_id: string; handle: string; display_name: string; avatar_seed: string | null; look: AgentLook | null }): Promise<Result<null>>;
Chat and workspace agents: channels, DMs and named agents you talk to505 /** Adds a person or an agent. An invite grants read, never write. */
506 invite(workspace: string, channelId: string, viewer: User, member: Principal): Promise<Result<null>>;
507 /**
508 * Newest first. Without `thread_root`, the channel's top-level messages;
509 * with it, that thread's replies, plus the message they reply to as the
510 * oldest once the page reaches the start of the thread (`older` null).
511 * With `after` (catching up after a reconnect): the messages after that
512 * id instead, oldest first, deleted ones included so the client can
513 * drop them; `newer` says whether there are more.
514 * Limit 50 by default, 200 at most.
515 */
516 messages(
517 workspace: string,
518 channelId: string,
519 viewer: User,
520 page?: { before?: string | null; after?: string | null; limit?: number; thread_root?: string | null },
521 ): Promise<Result<MessagePage>>;
522 post(workspace: string, channelId: string, viewer: User, message: PostMessage): Promise<Result<ChatMessage>>;
523 edit(workspace: string, channelId: string, viewer: User, id: string, body: string): Promise<Result<ChatMessage>>;
524 remove(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
525 markRead(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
526 setPreferences(
527 workspace: string,
528 channelId: string,
529 viewer: User,
530 prefs: { starred?: boolean; muted?: boolean },
531 ): Promise<Result<null>>;
532 /**
533 * Posts as an agent. Only the agents service calls this, for replies and
534 * cards; the agent must be a member of the channel.
535 */
536 postAsAgent(
537 workspace: string,
538 channelId: string,
539 agentId: string,
540 message: AgentPostMessage,
541 ): Promise<Result<ChatMessage>>;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project542 /**
543 * Changes a message an agent posted: its body, its card, or both. Only
544 * the agents service calls this, to keep a session's live card current.
545 * Changing a message wakes nobody.
546 */
547 updateAsAgent(
548 workspace: string,
549 channelId: string,
550 agentId: string,
551 id: string,
552 change: { body?: string; card?: MessageCard | null },
553 ): Promise<Result<ChatMessage>>;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar554 /**
555 * A person presses an action on a card in a conversation they can read.
556 * Chat hands it to the card's owner with the person, and the owner
557 * decides, acts and updates the card.
558 */
559 cardAction(
560 workspace: string,
561 channelId: string,
562 viewer: User,
563 messageId: string,
564 actionId: string,
565 input?: string | null,
566 ): Promise<Result<CardActionResult>>;
Chat and workspace agents: channels, DMs and named agents you talk to567 /** Shows "is typing" for an agent while it works on a reply. */
568 agentTyping(workspace: string, channelId: string, agentId: string): Promise<Result<null>>;
569 /**
570 * What an agent reads before it replies, oldest first: with
571 * `thread_root`, that thread (its root, then its replies); without, the
572 * channel's or direct message's latest top-level messages. Only the
573 * agents service calls this; the agent must be a member of the channel,
574 * so it reads only what was said where it was invited. `limit` defaults
575 * to 30, at most 100.
576 */
577 historyForAgent(
578 workspace: string,
579 channelId: string,
580 agentId: string,
581 page?: { thread_root?: string | null; limit?: number },
582 ): Promise<Result<ChatMessage[]>>;
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)583 /** Internal: who reads a conversation. */
584 audience(workspace: string, channelId: string): Promise<Result<ChatAudience>>;
585 /**
Merge branch 'worktree-agent-a1398e81ad1a64c5f'586 * Internal, for an agent about to answer in `channelId`, which it must be
587 * a member of: the conversation and its members (`ConversationForAgent`).
588 * `askedBy` (a user id) is always among the people listed.
589 */
590 conversationForAgent(workspace: string, channelId: string, agentId: string, askedBy?: string | null): Promise<Result<ConversationForAgent>>;
591 /**
592 * Internal: `agentId`, answering in `channelId`, hands work to a
593 * colleague agent for the person who asked. If the colleague is in this
594 * channel or group direct message, the brief is posted here; otherwise in
595 * the group direct message of the person, the agent and the colleague
596 * (opened on first use), with a card here linking to it. Either way the
597 * brief wakes the colleague, one hop further along the same chain, and
598 * nobody else. Refused for an agent that is not of the workspace, the
599 * agent itself, @g1t, one already in the chain, past the hop limit, or
600 * when the person who asked is not in this conversation.
601 */
602 handOffAsAgent(workspace: string, channelId: string, agentId: string, handOff: AgentHandOff): Promise<Result<HandOffResult>>;
603 /**
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)604 * Internal, for an agent replying in `channelId`: messages matching
605 * `query` (newest first, at most 20) from conversations every person in
606 * that conversation's audience is in, and from public channels. Never
607 * another direct message unless it has exactly the same people. The
608 * audience is worked out here from `channelId`, never taken from the
609 * caller.
610 */
611 searchForAgent(workspace: string, channelId: string, query: string, limit?: number): Promise<Result<AgentFoundMessage[]>>;
612 /**
613 * Internal, for an agent replying in `channelId`: a thread (`id`, its
614 * root or any reply) in `targetChannelId`, oldest first, under the same
615 * rule. A conversation the audience may not read is not found, exactly
616 * as one that does not exist.
617 */
618 threadForAgent(workspace: string, channelId: string, targetChannelId: string, id: string): Promise<Result<AgentFoundMessage[]>>;
619 /**
620 * Reacts to a message with `emoji` (one Unicode emoji, or `:name:` for
621 * one of the workspace's own). Once per member per emoji; at most 50
622 * different emoji on a message. Answers the message's reactions now.
623 */
624 react(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
625 unreact(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
626 /** Internal, for the agents service: an agent reacts (or, with `remove`, takes it back). It must be in the channel. */
627 reactAsAgent(
628 workspace: string,
629 channelId: string,
630 agentId: string,
631 messageId: string,
632 emoji: string,
633 remove?: boolean,
634 ): Promise<Result<ChatReaction[]>>;
635 /** The workspace's own emoji, by name, and who may add them. */
636 listEmoji(workspace: string, viewer: User): Promise<Result<EmojiList>>;
637 /** Adds an emoji: a PNG, GIF or WebP of at most 256 KB and 512×512. */
638 addEmoji(workspace: string, viewer: User, name: string, file: EmojiFile): Promise<Result<CustomEmoji>>;
639 /** Gives an existing emoji (`target`) another name. */
640 aliasEmoji(workspace: string, viewer: User, name: string, target: string): Promise<Result<CustomEmoji>>;
641 /** Removes an emoji, and its aliases with it. Its creator or an owner may. */
642 removeEmoji(workspace: string, viewer: User, name: string): Promise<Result<null>>;
643 /** Owners only: who may add emoji. */
644 setEmojiUpload(workspace: string, viewer: User, value: EmojiUpload): Promise<Result<EmojiUpload>>;
Chat and workspace agents: channels, DMs and named agents you talk to645};
646
647async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
648 const response = await service.fetch(`https://service/rpc/${method}`, {
649 method: "POST",
650 headers: { "content-type": "application/json" },
651 body: JSON.stringify(args),
652 });
653 if (!response.ok) {
654 throw new Error(`${method} failed with status ${response.status}`);
655 }
656 return (await response.json()) as T;
657}
658
659export function chatClient(service: ServiceBinding): ChatApi {
660 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
661 return {
662 sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }),
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.663 activity: (workspace, viewer, span) => call("activity", { workspace, viewer, from: span.from, until: span.until }),
Chat and workspace agents: channels, DMs and named agents you talk to664 channel: (workspace, channelId, viewer) => call("channel", { workspace, channel_id: channelId, viewer }),
665 channelByName: (workspace, name, viewer) => call("channel_by_name", { workspace, name, viewer }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project666 browse: (workspace, viewer, options) => call("browse", { workspace, viewer, archived: options?.archived ?? false }),
Chat and workspace agents: channels, DMs and named agents you talk to667 createChannel: (workspace, viewer, input) => call("create_channel", { workspace, viewer, input }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project668 updateChannel: (workspace, channelId, viewer, change) => call("update_channel", { workspace, channel_id: channelId, viewer, change }),
669 chatSettings: (workspace, viewer) => call("chat_settings", { workspace, viewer }),
670 setChatSettings: (workspace, viewer, change) => call("set_chat_settings", { workspace, viewer, change }),
Chat and workspace agents: channels, DMs and named agents you talk to671 openDm: (workspace, viewer, members) => call("open_dm", { workspace, viewer, members }),
672 join: (workspace, channelId, viewer) => call("join", { workspace, channel_id: channelId, viewer }),
673 leave: (workspace, channelId, viewer) => call("leave", { workspace, channel_id: channelId, viewer }),
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.674 agentDestroyed: (input) => call("agent_destroyed", input),
Chat and workspace agents: channels, DMs and named agents you talk to675 invite: (workspace, channelId, viewer, member) =>
676 call("invite", { workspace, channel_id: channelId, viewer, member }),
677 messages: (workspace, channelId, viewer, page) =>
678 call("messages", {
679 workspace,
680 channel_id: channelId,
681 viewer,
682 before: page?.before ?? null,
683 after: page?.after ?? null,
684 limit: page?.limit ?? null,
685 thread_root: page?.thread_root ?? null,
686 }),
687 post: (workspace, channelId, viewer, message) => call("post", { workspace, channel_id: channelId, viewer, message }),
688 edit: (workspace, channelId, viewer, id, body) => call("edit", { workspace, channel_id: channelId, viewer, id, body }),
689 remove: (workspace, channelId, viewer, id) => call("remove", { workspace, channel_id: channelId, viewer, id }),
690 markRead: (workspace, channelId, viewer, id) => call("mark_read", { workspace, channel_id: channelId, viewer, id }),
691 setPreferences: (workspace, channelId, viewer, prefs) =>
692 call("set_preferences", { workspace, channel_id: channelId, viewer, prefs }),
693 postAsAgent: (workspace, channelId, agentId, message) =>
694 call("post_as_agent", { workspace, channel_id: channelId, agent_id: agentId, message }),
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar695 cardAction: (workspace, channelId, viewer, messageId, actionId, input) =>
696 call("card_action", { workspace, channel_id: channelId, viewer, message_id: messageId, action_id: actionId, input: input ?? null }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project697 updateAsAgent: (workspace, channelId, agentId, id, change) =>
698 call("update_as_agent", { workspace, channel_id: channelId, agent_id: agentId, id, change }),
Chat and workspace agents: channels, DMs and named agents you talk to699 agentTyping: (workspace, channelId, agentId) =>
700 call("agent_typing", { workspace, channel_id: channelId, agent_id: agentId }),
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)701 audience: (workspace, channelId) => call("audience", { workspace, channel_id: channelId }),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'702 conversationForAgent: (workspace, channelId, agentId, askedBy) =>
703 call("conversation_for_agent", { workspace, channel_id: channelId, agent_id: agentId, asked_by: askedBy ?? null }),
704 handOffAsAgent: (workspace, channelId, agentId, handOff) =>
705 call("hand_off_as_agent", { workspace, channel_id: channelId, agent_id: agentId, hand_off: handOff }),
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)706 searchForAgent: (workspace, channelId, query, limit) =>
707 call("search_for_agent", { workspace, channel_id: channelId, query, limit: limit ?? null }),
708 threadForAgent: (workspace, channelId, targetChannelId, id) =>
709 call("thread_for_agent", { workspace, channel_id: channelId, target_channel_id: targetChannelId, id }),
710 react: (workspace, channelId, viewer, messageId, emoji) =>
711 call("react", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
712 unreact: (workspace, channelId, viewer, messageId, emoji) =>
713 call("unreact", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
714 reactAsAgent: (workspace, channelId, agentId, messageId, emoji, remove) =>
715 call("react_as_agent", { workspace, channel_id: channelId, agent_id: agentId, message_id: messageId, emoji, remove: remove ?? false }),
716 listEmoji: (workspace, viewer) => call("list_emoji", { workspace, viewer }),
717 addEmoji: (workspace, viewer, name, file) => call("add_emoji", { workspace, viewer, name, file }),
718 aliasEmoji: (workspace, viewer, name, target) => call("alias_emoji", { workspace, viewer, name, target }),
719 removeEmoji: (workspace, viewer, name) => call("remove_emoji", { workspace, viewer, name }),
720 setEmojiUpload: (workspace, viewer, value) => call("set_emoji_upload", { workspace, viewer, value }),
Chat and workspace agents: channels, DMs and named agents you talk to721 historyForAgent: (workspace, channelId, agentId, page) =>
722 call("history_for_agent", {
723 workspace,
724 channel_id: channelId,
725 agent_id: agentId,
726 thread_root: page?.thread_root ?? null,
727 limit: page?.limit ?? null,
728 }),
729 };
730}

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