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

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