Skip to content
712 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;
Chat and workspace agents: channels, DMs and named agents you talk to57};
58
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar59/**
60 * A member's handle as it shows (`@Ana`, without the `@`): a person's
61 * username in its chosen case, an agent's handle.
62 */
63export function memberHandle(member: { name: string; display_username?: string | null }): string {
64 const display = member.display_username;
65 return display && display.toLowerCase() === member.name.toLowerCase() ? display : member.name;
66}
67
68/**
69 * How a member's name is shown everywhere in chat (messages, the
70 * sidebar, direct-message titles, typing, notifications and pushes): their
71 * display name, else their handle in its chosen case. One rule, so every
72 * surface agrees.
73 */
74export function memberName(member: { name: string; display_name?: string | null; display_username?: string | null }): string {
75 return member.display_name?.trim() || memberHandle(member);
76}
77
Chat and workspace agents: channels, DMs and named agents you talk to78export type ChannelKind = "channel" | "dm";
79
80export type Channel = {
81 id: string;
82 workspace_id: string;
83 kind: ChannelKind;
84 /** Lowercase, no `#`. Null for a direct message. */
85 name: string | null;
86 topic: string | null;
87 private: boolean;
88 created_by: Principal;
89 created_at: string;
90 archived_at: string | null;
91 last_message_at: string | null;
92};
93
94export type ChannelMember = {
95 channel_id: string;
96 member: MemberProfile;
97 role: "owner" | "member";
98 starred: boolean;
99 muted: boolean;
100 last_read_id: string | null;
101 joined_at: string;
102};
103
104/** A card g1t or an agent posts: an event, a task, an approval. */
105export type MessageCard = {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar106 /** 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 to107 kind: string;
108 title: string;
109 /** A short line under the title: "3/3 checks · +214 −87". */
110 detail: string | null;
111 /** A status shown on the right: "Needs approval", "Merged". */
112 state: string | null;
113 /** Where clicking the card goes, relative to the site. */
114 href: string | null;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar115 /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */
116 body?: string | null;
117 /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */
118 fields?: CardField[];
119 /**
120 * What people can do right here. Pressing one goes to the service that
121 * owns the card (`owner`), which checks the person may, does it, and
122 * updates the card in place. A card without `owner` has no actions.
123 */
124 actions?: CardAction[];
125 /** The service whose card this is and that answers its actions: `agents`. */
126 owner?: "agents" | null;
127 /** What the card is about, for its owner: a session id, a draft id. */
128 ref?: string | null;
129};
130
131export type CardField = { label: string; value: string };
132
133/** A button on a card. */
134export type CardAction = {
135 /** Unique on the card: `stop`, `approve`, `file`. */
136 id: string;
137 label: string;
138 style?: "primary" | "danger" | "default";
139 /** Asked before it runs: "Stop this session and everything under it?". */
140 confirm?: string | null;
141 /** A value it needs first, asked inline: an amount in dollars, or a line of text. */
142 input?: { kind: "money" | "text"; label: string; placeholder?: string | null; initial?: string | null } | null;
143 /** A link instead of an action: opens this place in the site. */
144 href?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to145};
146
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar147/** What pressing a card's action did, as the person who pressed it is told. */
148export type CardActionResult = { ok: boolean; message: string | null };
149
Chat and workspace agents: channels, DMs and named agents you talk to150export type ChatMessage = {
151 /** Time-sortable (ULID-like), so ordering by id is ordering by time. */
152 id: string;
153 channel_id: string;
154 author: MemberProfile;
155 kind: "text" | "card";
156 body: string;
157 card: MessageCard | null;
158 /** The message this replies under, or null for a top-level message. */
159 thread_root: string | null;
160 reply_count: number;
161 last_reply_at: string | null;
162 created_at: string;
163 edited_at: string | null;
164 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)165 /**
166 * Its reactions, one per emoji in the order each was first used. Chat
167 * always sets it; optional so messages a page makes for itself need not.
168 */
169 reactions?: ChatReaction[];
Chat and workspace agents: channels, DMs and named agents you talk to170};
171
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)172/**
173 * One emoji's reactions on a message. `emoji` is a Unicode emoji or
174 * `:name:` for one of the workspace's own (`CustomEmoji`).
175 */
176export type ChatReaction = {
177 emoji: string;
178 /** Everyone who reacted with it, people and agents alike. */
179 count: number;
180 /**
181 * Whether the viewer did. In live `message.*` events, which everyone in
182 * the room gets, it is always false: keep your own from what you know.
183 */
184 me: boolean;
185 /** The first ten who reacted with it, for the hover list. */
186 by: MemberProfile[];
187};
188
189/** Who may add a workspace's emoji: every member (the default), or only its owners. */
190export type EmojiUpload = "members" | "admins";
191
192/**
193 * One of a workspace's own emoji, used as `:name:`. Its image is served
194 * from the usercontent origin at `/emoji/<file>` (never from the site).
195 */
196export type CustomEmoji = {
197 name: string;
198 /** For an alias, the emoji it is another name for; it shows that one's image. */
199 alias_of: string | null;
200 /** The SHA-256 of the image's bytes. */
201 file: string;
202 content_type: "image/png" | "image/gif" | "image/webp";
203 bytes: number;
204 created_by: MemberProfile;
205 created_at: string;
206};
207
208export type EmojiList = {
209 emoji: CustomEmoji[];
210 emoji_upload: EmojiUpload;
211 /** Whether the viewer may add emoji and aliases. */
212 can_upload: boolean;
213 /** Whether the viewer is an owner: may remove any emoji and change `emoji_upload`. */
214 can_manage: boolean;
215};
216
Chat controls, public profiles, shadcn selects, and no Docs tab in a project217/** Who may do something in a workspace's chat: every member, or only its owners. */
218export type ChatAllowed = "members" | "owners";
219
220/**
221 * Who may rename and archive a channel: its owners (whoever made it) and
222 * the workspace's owners (the default), or the workspace's owners only.
223 */
224export type ChannelManagers = "channel_owners" | "owners";
225
226/**
227 * A workspace's chat settings, which its owners choose (Workspace,
228 * Settings, Chat). The chat service enforces each one; the page only
229 * hides what the viewer may not do.
230 */
231export type ChatSettings = {
232 /** Who may create public channels. */
233 public_channels: ChatAllowed;
234 /** Who may create private channels. */
235 private_channels: ChatAllowed;
236 /** Who may rename, archive and unarchive channels. */
237 manage_channels: ChannelManagers;
238 /** Who may add custom emoji (`admins` is the workspace's owners). */
239 emoji_upload: EmojiUpload;
240 /**
241 * The public channels someone new is put in the first time they open
242 * Chat, by id. #general unless the owners chose otherwise.
243 */
244 default_channels: string[];
245};
246
247/** What the viewer may do in a workspace's chat, from its settings and their role. */
248export type ChatPermissions = {
249 create_public_channels: boolean;
250 create_private_channels: boolean;
251 add_emoji: boolean;
252 /** Whether they may change the workspace's chat settings: owners only. */
253 manage_settings: boolean;
254};
255
256/** The chat settings page: the settings, what the viewer may do, and the public channels to choose defaults from. */
257export type ChatSettingsView = {
258 settings: ChatSettings;
259 can: ChatPermissions;
260 channels: Channel[];
261};
262
263/** A change to a channel: its name, its topic, or whether it is archived. Anything left out stays. */
264export type ChannelChange = { name?: string; topic?: string | null; archived?: boolean };
265
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)266/** An image for a new emoji, as base64. Its type is read from its bytes, never taken from here. */
267export type EmojiFile = { data: string };
268
269/** The largest custom emoji image, in bytes, and the widest or tallest, in pixels. */
270export const MAX_EMOJI_BYTES = 256 * 1024;
271export const MAX_EMOJI_SIDE = 512;
272
Chat and workspace agents: channels, DMs and named agents you talk to273/** One row of the Chat sidebar. */
274export type ChatSidebarEntry = {
275 channel: Channel;
276 /** "g1t-core", or the other members' names for a direct message. */
277 title: string;
278 /** For a direct message: who else is in it (up to four). */
279 others: MemberProfile[];
280 starred: boolean;
281 muted: boolean;
282 unread: number;
283 mentions: number;
284};
285
286export type ChatSidebar = {
287 entries: ChatSidebarEntry[];
288 /** Public channels in the workspace the viewer has not joined. */
289 browsable: number;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project290 /** What the viewer may do, from the workspace's chat settings: the page hides what they may not. */
291 can: ChatPermissions;
Chat and workspace agents: channels, DMs and named agents you talk to292};
293
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.294/**
295 * `activity`: what was said over a span, in the conversations the viewer
296 * can read (every public channel, and the private channels and direct
297 * messages they are in). Messages are text messages and thread replies
298 * that were not deleted; cards agents post are not messages. Home reads it
299 * for what people and agents said since you were last there.
300 */
301export type ChatActivity = {
302 /** RFC 3339: `[from, until)`. */
303 from: string;
304 until: string;
305 messages: number;
306 /** Conversations with at least one message. */
307 channels: number;
308 /** Who said how much: member keys (`user:<id>`, `agent:<id>`), most first. */
309 authors: { key: string; messages: number }[];
310};
311
Chat and workspace agents: channels, DMs and named agents you talk to312export type MessagePage = {
313 messages: ChatMessage[];
314 /** Pass as `before` to read further back; null at the beginning. */
315 older: string | null;
316 /**
Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread317 * For a thread's first page: the message the thread is under, however
318 * many replies it has, so a thread panel always shows it (a session's
319 * live card, say) at its top.
320 */
321 root?: ChatMessage | null;
322 /**
Chat and workspace agents: channels, DMs and named agents you talk to323 * Set when the page was read with `after`: pass it as `after` again for
324 * the next messages, or null when this page reached the newest.
325 */
326 newer?: string | null;
327};
328
329export type NewChannel = { name: string; topic?: string | null; private?: boolean };
330
331export type PostMessage = { body: string; thread_root?: string | null };
332
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)333/**
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.334 * Who will read what is said in a conversation: a direct message's or
335 * private channel's people, or, for a public channel, the whole workspace.
336 * 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)337 */
338export type ChatAudience = {
339 kind: "dm" | "private" | "public";
340 /** The people in it (agents left out). For a public channel, its current members, for reference only. */
341 member_user_ids: string[];
342 member_count: number;
343};
344
345/** A message an agent found by searching, with the conversation it is in. */
346export type AgentFoundMessage = {
347 channel_id: string;
348 /** The channel's name, or null for a direct message. */
349 channel: string | null;
350 message: ChatMessage;
351};
352
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.353/** The most agent-to-agent hops one person's request may start. */
Chat and workspace agents: channels, DMs and named agents you talk to354export const CHAT_MAX_HOPS = 6;
355
356/**
Merge branch 'worktree-agent-a1398e81ad1a64c5f'357 * What an agent posts. An agent's message never wakes another agent, even
358 * when it @mentions one: only a hand-off does (`handOffAsAgent`). Its
359 * mentions of anyone who is not a member of the conversation lose their
360 * `@`, so they show as plain names and notify nobody. When it answers a
361 * delivery, it passes that delivery's `hops`, `asked_by`, `asker` and
362 * `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 to363 * by the person who created the agent.
364 */
365export type AgentPostMessage = PostMessage & {
366 card?: MessageCard | null;
367 hops?: number;
368 asked_by?: string | null;
369 /** The delivery's `asker`, handed on to agents this message wakes. Absent: none (they treat the asker as unable to change code). */
370 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)371 /**
372 * The delivery's `chain`: the agents that handled this request before
373 * the one posting, oldest first. Chat adds the poster, and never hands
374 * the message to the agent that sent the work to it (no ping-pong).
375 */
376 chain?: string[];
Chat and workspace agents: channels, DMs and named agents you talk to377};
378
379/**
Merge branch 'worktree-agent-a1398e81ad1a64c5f'380 * A conversation as an agent is told about it before every turn: what it
381 * is and who is in it, so it knows who reads what it says and who doesn't.
382 * Every agent member is listed; people up to `CONVERSATION_PEOPLE_SHOWN`
383 * (the person who asked always among them), with the totals beside.
384 */
385export type ConversationForAgent = {
386 channel: Channel;
387 /** Agents first, then people. */
388 members: MemberProfile[];
389 /** How many people and agents are in it, listed or not. */
390 people: number;
391 agents: number;
392};
393
394/** The most people `conversationForAgent` lists; the rest are a count. */
395export const CONVERSATION_PEOPLE_SHOWN = 20;
396
397/**
398 * 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.399 * (docs.g1t.sh/guides/agents/, "Hand off"). The fields after `brief`
Merge branch 'worktree-agent-a1398e81ad1a64c5f'400 * are the delivery the agent is answering, passed back as for a post.
401 */
402export type AgentHandOff = {
403 /** The colleague, by agent id. */
404 colleague_id: string;
405 /** What they are asked to do, addressed to them. */
406 brief: string;
407 thread_root?: string | null;
408 hops?: number;
409 asked_by: string;
410 asker?: AskerAccess | null;
411 chain?: string[];
412};
413
414/**
415 * Where a hand-off went. `here`: the colleague is in this channel or group
416 * direct message, and the brief was posted here. `group_dm`: the brief was
417 * posted in the direct message of the person who asked, the agent and the
418 * colleague (`opened` when it was new), and a card linking to it was
419 * posted here.
420 */
421export type HandOffResult = { where: "here" | "group_dm"; channel_id: string; message_id: string; opened: boolean };
422
423/**
Chat and workspace agents: channels, DMs and named agents you talk to424 * What the live socket sends. The site opens
425 * `wss://<site>/<workspace>/chat/live?channel=<id>`; the site checks the
426 * session and forwards the upgrade to the chat service with the viewer.
427 */
428export type ChatLiveEvent =
429 | { type: "message.created"; message: ChatMessage }
430 | { type: "message.updated"; message: ChatMessage }
431 | { type: "message.deleted"; channel_id: string; id: string }
432 | { 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)433 | { type: "read"; channel_id: string; principal: Principal; last_read_id: string }
Chat controls, public profiles, shadcn selects, and no Docs tab in a project434 | { 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)435 | { 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 to436
437/**
438 * Header the site sets on a forwarded live socket: the viewer, as JSON.
439 * The site forwards the upgrade to the chat service's
440 * `GET /live?workspace=<slug>&channel=<id>` (`workspace` may be left out,
441 * at the cost of looking up each of the viewer's workspaces).
442 */
443export const CHAT_VIEWER_HEADER = "x-g1t-chat-viewer";
444
Chat controls, public profiles, shadcn selects, and no Docs tab in a project445/** A channel with its members, and whether the viewer may rename or archive it. */
446export type ChannelDetail = { channel: Channel; members: ChannelMember[]; can_manage: boolean };
447
Chat and workspace agents: channels, DMs and named agents you talk to448export type ChatApi = {
449 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.450 /** What was said in `[from, until)` (RFC 3339) where the viewer can read, counted. */
451 activity(workspace: string, viewer: User, span: { from: string; until: string }): Promise<Result<ChatActivity>>;
Chat and workspace agents: channels, DMs and named agents you talk to452 channel(
453 workspace: string,
454 channelId: string,
455 viewer: User,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project456 ): Promise<Result<ChannelDetail>>;
Chat and workspace agents: channels, DMs and named agents you talk to457 /** The same, found by its name in the workspace (`#general` or `general`), as the site's URLs name channels. */
458 channelByName(
459 workspace: string,
460 name: string,
461 viewer: User,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project462 ): Promise<Result<ChannelDetail>>;
463 /**
464 * Browse channels: every public channel, and the private ones the viewer
465 * is in. With `archived`, the archived ones instead.
466 */
467 browse(workspace: string, viewer: User, options?: { archived?: boolean }): Promise<Result<Channel[]>>;
468 /** 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 to469 createChannel(workspace: string, viewer: User, input: NewChannel): Promise<Result<Channel>>;
470 /**
Chat controls, public profiles, shadcn selects, and no Docs tab in a project471 * Renames, archives or unarchives a channel (the workspace's
472 * `manage_channels` setting says who may), or changes its topic (any
473 * member). #general is never renamed or archived. Everyone looking at it
474 * gets `channel.updated`.
475 */
476 updateChannel(workspace: string, channelId: string, viewer: User, change: ChannelChange): Promise<Result<Channel>>;
477 /** The workspace's chat settings, for any member; only owners may change them. */
478 chatSettings(workspace: string, viewer: User): Promise<Result<ChatSettingsView>>;
479 /** Owners only: changes some of the workspace's chat settings, and answers all of them. */
480 setChatSettings(workspace: string, viewer: User, change: Partial<ChatSettings>): Promise<Result<ChatSettings>>;
481 /**
Chat and workspace agents: channels, DMs and named agents you talk to482 * The direct message between the viewer and these members, created on
483 * first use. The same set of members always gets the same channel.
484 */
485 openDm(workspace: string, viewer: User, members: Principal[]): Promise<Result<Channel>>;
486 join(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
487 leave(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
488 /** Adds a person or an agent. An invite grants read, never write. */
489 invite(workspace: string, channelId: string, viewer: User, member: Principal): Promise<Result<null>>;
490 /**
491 * Newest first. Without `thread_root`, the channel's top-level messages;
492 * with it, that thread's replies, plus the message they reply to as the
493 * oldest once the page reaches the start of the thread (`older` null).
494 * With `after` (catching up after a reconnect): the messages after that
495 * id instead, oldest first, deleted ones included so the client can
496 * drop them; `newer` says whether there are more.
497 * Limit 50 by default, 200 at most.
498 */
499 messages(
500 workspace: string,
501 channelId: string,
502 viewer: User,
503 page?: { before?: string | null; after?: string | null; limit?: number; thread_root?: string | null },
504 ): Promise<Result<MessagePage>>;
505 post(workspace: string, channelId: string, viewer: User, message: PostMessage): Promise<Result<ChatMessage>>;
506 edit(workspace: string, channelId: string, viewer: User, id: string, body: string): Promise<Result<ChatMessage>>;
507 remove(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
508 markRead(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
509 setPreferences(
510 workspace: string,
511 channelId: string,
512 viewer: User,
513 prefs: { starred?: boolean; muted?: boolean },
514 ): Promise<Result<null>>;
515 /**
516 * Posts as an agent. Only the agents service calls this, for replies and
517 * cards; the agent must be a member of the channel.
518 */
519 postAsAgent(
520 workspace: string,
521 channelId: string,
522 agentId: string,
523 message: AgentPostMessage,
524 ): Promise<Result<ChatMessage>>;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project525 /**
526 * Changes a message an agent posted: its body, its card, or both. Only
527 * the agents service calls this, to keep a session's live card current.
528 * Changing a message wakes nobody.
529 */
530 updateAsAgent(
531 workspace: string,
532 channelId: string,
533 agentId: string,
534 id: string,
535 change: { body?: string; card?: MessageCard | null },
536 ): Promise<Result<ChatMessage>>;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar537 /**
538 * A person presses an action on a card in a conversation they can read.
539 * Chat hands it to the card's owner with the person, and the owner
540 * decides, acts and updates the card.
541 */
542 cardAction(
543 workspace: string,
544 channelId: string,
545 viewer: User,
546 messageId: string,
547 actionId: string,
548 input?: string | null,
549 ): Promise<Result<CardActionResult>>;
Chat and workspace agents: channels, DMs and named agents you talk to550 /** Shows "is typing" for an agent while it works on a reply. */
551 agentTyping(workspace: string, channelId: string, agentId: string): Promise<Result<null>>;
552 /**
553 * What an agent reads before it replies, oldest first: with
554 * `thread_root`, that thread (its root, then its replies); without, the
555 * channel's or direct message's latest top-level messages. Only the
556 * agents service calls this; the agent must be a member of the channel,
557 * so it reads only what was said where it was invited. `limit` defaults
558 * to 30, at most 100.
559 */
560 historyForAgent(
561 workspace: string,
562 channelId: string,
563 agentId: string,
564 page?: { thread_root?: string | null; limit?: number },
565 ): 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)566 /** Internal: who reads a conversation. */
567 audience(workspace: string, channelId: string): Promise<Result<ChatAudience>>;
568 /**
Merge branch 'worktree-agent-a1398e81ad1a64c5f'569 * Internal, for an agent about to answer in `channelId`, which it must be
570 * a member of: the conversation and its members (`ConversationForAgent`).
571 * `askedBy` (a user id) is always among the people listed.
572 */
573 conversationForAgent(workspace: string, channelId: string, agentId: string, askedBy?: string | null): Promise<Result<ConversationForAgent>>;
574 /**
575 * Internal: `agentId`, answering in `channelId`, hands work to a
576 * colleague agent for the person who asked. If the colleague is in this
577 * channel or group direct message, the brief is posted here; otherwise in
578 * the group direct message of the person, the agent and the colleague
579 * (opened on first use), with a card here linking to it. Either way the
580 * brief wakes the colleague, one hop further along the same chain, and
581 * nobody else. Refused for an agent that is not of the workspace, the
582 * agent itself, @g1t, one already in the chain, past the hop limit, or
583 * when the person who asked is not in this conversation.
584 */
585 handOffAsAgent(workspace: string, channelId: string, agentId: string, handOff: AgentHandOff): Promise<Result<HandOffResult>>;
586 /**
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)587 * Internal, for an agent replying in `channelId`: messages matching
588 * `query` (newest first, at most 20) from conversations every person in
589 * that conversation's audience is in, and from public channels. Never
590 * another direct message unless it has exactly the same people. The
591 * audience is worked out here from `channelId`, never taken from the
592 * caller.
593 */
594 searchForAgent(workspace: string, channelId: string, query: string, limit?: number): Promise<Result<AgentFoundMessage[]>>;
595 /**
596 * Internal, for an agent replying in `channelId`: a thread (`id`, its
597 * root or any reply) in `targetChannelId`, oldest first, under the same
598 * rule. A conversation the audience may not read is not found, exactly
599 * as one that does not exist.
600 */
601 threadForAgent(workspace: string, channelId: string, targetChannelId: string, id: string): Promise<Result<AgentFoundMessage[]>>;
602 /**
603 * Reacts to a message with `emoji` (one Unicode emoji, or `:name:` for
604 * one of the workspace's own). Once per member per emoji; at most 50
605 * different emoji on a message. Answers the message's reactions now.
606 */
607 react(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
608 unreact(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
609 /** Internal, for the agents service: an agent reacts (or, with `remove`, takes it back). It must be in the channel. */
610 reactAsAgent(
611 workspace: string,
612 channelId: string,
613 agentId: string,
614 messageId: string,
615 emoji: string,
616 remove?: boolean,
617 ): Promise<Result<ChatReaction[]>>;
618 /** The workspace's own emoji, by name, and who may add them. */
619 listEmoji(workspace: string, viewer: User): Promise<Result<EmojiList>>;
620 /** Adds an emoji: a PNG, GIF or WebP of at most 256 KB and 512×512. */
621 addEmoji(workspace: string, viewer: User, name: string, file: EmojiFile): Promise<Result<CustomEmoji>>;
622 /** Gives an existing emoji (`target`) another name. */
623 aliasEmoji(workspace: string, viewer: User, name: string, target: string): Promise<Result<CustomEmoji>>;
624 /** Removes an emoji, and its aliases with it. Its creator or an owner may. */
625 removeEmoji(workspace: string, viewer: User, name: string): Promise<Result<null>>;
626 /** Owners only: who may add emoji. */
627 setEmojiUpload(workspace: string, viewer: User, value: EmojiUpload): Promise<Result<EmojiUpload>>;
Chat and workspace agents: channels, DMs and named agents you talk to628};
629
630async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
631 const response = await service.fetch(`https://service/rpc/${method}`, {
632 method: "POST",
633 headers: { "content-type": "application/json" },
634 body: JSON.stringify(args),
635 });
636 if (!response.ok) {
637 throw new Error(`${method} failed with status ${response.status}`);
638 }
639 return (await response.json()) as T;
640}
641
642export function chatClient(service: ServiceBinding): ChatApi {
643 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
644 return {
645 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.646 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 to647 channel: (workspace, channelId, viewer) => call("channel", { workspace, channel_id: channelId, viewer }),
648 channelByName: (workspace, name, viewer) => call("channel_by_name", { workspace, name, viewer }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project649 browse: (workspace, viewer, options) => call("browse", { workspace, viewer, archived: options?.archived ?? false }),
Chat and workspace agents: channels, DMs and named agents you talk to650 createChannel: (workspace, viewer, input) => call("create_channel", { workspace, viewer, input }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project651 updateChannel: (workspace, channelId, viewer, change) => call("update_channel", { workspace, channel_id: channelId, viewer, change }),
652 chatSettings: (workspace, viewer) => call("chat_settings", { workspace, viewer }),
653 setChatSettings: (workspace, viewer, change) => call("set_chat_settings", { workspace, viewer, change }),
Chat and workspace agents: channels, DMs and named agents you talk to654 openDm: (workspace, viewer, members) => call("open_dm", { workspace, viewer, members }),
655 join: (workspace, channelId, viewer) => call("join", { workspace, channel_id: channelId, viewer }),
656 leave: (workspace, channelId, viewer) => call("leave", { workspace, channel_id: channelId, viewer }),
657 invite: (workspace, channelId, viewer, member) =>
658 call("invite", { workspace, channel_id: channelId, viewer, member }),
659 messages: (workspace, channelId, viewer, page) =>
660 call("messages", {
661 workspace,
662 channel_id: channelId,
663 viewer,
664 before: page?.before ?? null,
665 after: page?.after ?? null,
666 limit: page?.limit ?? null,
667 thread_root: page?.thread_root ?? null,
668 }),
669 post: (workspace, channelId, viewer, message) => call("post", { workspace, channel_id: channelId, viewer, message }),
670 edit: (workspace, channelId, viewer, id, body) => call("edit", { workspace, channel_id: channelId, viewer, id, body }),
671 remove: (workspace, channelId, viewer, id) => call("remove", { workspace, channel_id: channelId, viewer, id }),
672 markRead: (workspace, channelId, viewer, id) => call("mark_read", { workspace, channel_id: channelId, viewer, id }),
673 setPreferences: (workspace, channelId, viewer, prefs) =>
674 call("set_preferences", { workspace, channel_id: channelId, viewer, prefs }),
675 postAsAgent: (workspace, channelId, agentId, message) =>
676 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 calendar677 cardAction: (workspace, channelId, viewer, messageId, actionId, input) =>
678 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 project679 updateAsAgent: (workspace, channelId, agentId, id, change) =>
680 call("update_as_agent", { workspace, channel_id: channelId, agent_id: agentId, id, change }),
Chat and workspace agents: channels, DMs and named agents you talk to681 agentTyping: (workspace, channelId, agentId) =>
682 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)683 audience: (workspace, channelId) => call("audience", { workspace, channel_id: channelId }),
Merge branch 'worktree-agent-a1398e81ad1a64c5f'684 conversationForAgent: (workspace, channelId, agentId, askedBy) =>
685 call("conversation_for_agent", { workspace, channel_id: channelId, agent_id: agentId, asked_by: askedBy ?? null }),
686 handOffAsAgent: (workspace, channelId, agentId, handOff) =>
687 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)688 searchForAgent: (workspace, channelId, query, limit) =>
689 call("search_for_agent", { workspace, channel_id: channelId, query, limit: limit ?? null }),
690 threadForAgent: (workspace, channelId, targetChannelId, id) =>
691 call("thread_for_agent", { workspace, channel_id: channelId, target_channel_id: targetChannelId, id }),
692 react: (workspace, channelId, viewer, messageId, emoji) =>
693 call("react", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
694 unreact: (workspace, channelId, viewer, messageId, emoji) =>
695 call("unreact", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
696 reactAsAgent: (workspace, channelId, agentId, messageId, emoji, remove) =>
697 call("react_as_agent", { workspace, channel_id: channelId, agent_id: agentId, message_id: messageId, emoji, remove: remove ?? false }),
698 listEmoji: (workspace, viewer) => call("list_emoji", { workspace, viewer }),
699 addEmoji: (workspace, viewer, name, file) => call("add_emoji", { workspace, viewer, name, file }),
700 aliasEmoji: (workspace, viewer, name, target) => call("alias_emoji", { workspace, viewer, name, target }),
701 removeEmoji: (workspace, viewer, name) => call("remove_emoji", { workspace, viewer, name }),
702 setEmojiUpload: (workspace, viewer, value) => call("set_emoji_upload", { workspace, viewer, value }),
Chat and workspace agents: channels, DMs and named agents you talk to703 historyForAgent: (workspace, channelId, agentId, page) =>
704 call("history_for_agent", {
705 workspace,
706 channel_id: channelId,
707 agent_id: agentId,
708 thread_root: page?.thread_root ?? null,
709 limit: page?.limit ?? null,
710 }),
711 };
712}

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