Skip to content
616 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
3 * service (`services/chat`). People and agents are members alike. Plan:
4 * docs/WORKSPACE.md.
5 *
6 * Wire shapes are snake_case end to end, so the site, the public API and
7 * the live socket all carry the same objects.
8 */
9import type { ServiceBinding } from "./clients";
10import type { User } from "./identity";
11import type { Result } from "./result";
12import 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;
53 /** What an agent's pixel creature is drawn from; null for a person. Always set by chat. */
54 avatar_seed?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to55};
56
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar57/**
58 * A member's handle as it shows (`@Ana`, without the `@`): a person's
59 * username in its chosen case, an agent's handle.
60 */
61export function memberHandle(member: { name: string; display_username?: string | null }): string {
62 const display = member.display_username;
63 return display && display.toLowerCase() === member.name.toLowerCase() ? display : member.name;
64}
65
66/**
67 * How a member's name is shown everywhere in chat (messages, the
68 * sidebar, direct-message titles, typing, notifications and pushes): their
69 * display name, else their handle in its chosen case. One rule, so every
70 * surface agrees.
71 */
72export function memberName(member: { name: string; display_name?: string | null; display_username?: string | null }): string {
73 return member.display_name?.trim() || memberHandle(member);
74}
75
Chat and workspace agents: channels, DMs and named agents you talk to76export type ChannelKind = "channel" | "dm";
77
78export type Channel = {
79 id: string;
80 workspace_id: string;
81 kind: ChannelKind;
82 /** Lowercase, no `#`. Null for a direct message. */
83 name: string | null;
84 topic: string | null;
85 private: boolean;
86 created_by: Principal;
87 created_at: string;
88 archived_at: string | null;
89 last_message_at: string | null;
90};
91
92export type ChannelMember = {
93 channel_id: string;
94 member: MemberProfile;
95 role: "owner" | "member";
96 starred: boolean;
97 muted: boolean;
98 last_read_id: string | null;
99 joined_at: string;
100};
101
102/** A card g1t or an agent posts: an event, a task, an approval. */
103export type MessageCard = {
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar104 /** 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 to105 kind: string;
106 title: string;
107 /** A short line under the title: "3/3 checks · +214 −87". */
108 detail: string | null;
109 /** A status shown on the right: "Needs approval", "Merged". */
110 state: string | null;
111 /** Where clicking the card goes, relative to the site. */
112 href: string | null;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar113 /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */
114 body?: string | null;
115 /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */
116 fields?: CardField[];
117 /**
118 * What people can do right here. Pressing one goes to the service that
119 * owns the card (`owner`), which checks the person may, does it, and
120 * updates the card in place. A card without `owner` has no actions.
121 */
122 actions?: CardAction[];
123 /** The service whose card this is and that answers its actions: `agents`. */
124 owner?: "agents" | null;
125 /** What the card is about, for its owner: a session id, a draft id. */
126 ref?: string | null;
127};
128
129export type CardField = { label: string; value: string };
130
131/** A button on a card. */
132export type CardAction = {
133 /** Unique on the card: `stop`, `approve`, `file`. */
134 id: string;
135 label: string;
136 style?: "primary" | "danger" | "default";
137 /** Asked before it runs: "Stop this session and everything under it?". */
138 confirm?: string | null;
139 /** A value it needs first, asked inline: an amount in dollars, or a line of text. */
140 input?: { kind: "money" | "text"; label: string; placeholder?: string | null; initial?: string | null } | null;
141 /** A link instead of an action: opens this place in the site. */
142 href?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to143};
144
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar145/** What pressing a card's action did, as the person who pressed it is told. */
146export type CardActionResult = { ok: boolean; message: string | null };
147
Chat and workspace agents: channels, DMs and named agents you talk to148export type ChatMessage = {
149 /** Time-sortable (ULID-like), so ordering by id is ordering by time. */
150 id: string;
151 channel_id: string;
152 author: MemberProfile;
153 kind: "text" | "card";
154 body: string;
155 card: MessageCard | null;
156 /** The message this replies under, or null for a top-level message. */
157 thread_root: string | null;
158 reply_count: number;
159 last_reply_at: string | null;
160 created_at: string;
161 edited_at: string | null;
162 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)163 /**
164 * Its reactions, one per emoji in the order each was first used. Chat
165 * always sets it; optional so messages a page makes for itself need not.
166 */
167 reactions?: ChatReaction[];
Chat and workspace agents: channels, DMs and named agents you talk to168};
169
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)170/**
171 * One emoji's reactions on a message. `emoji` is a Unicode emoji or
172 * `:name:` for one of the workspace's own (`CustomEmoji`).
173 */
174export type ChatReaction = {
175 emoji: string;
176 /** Everyone who reacted with it, people and agents alike. */
177 count: number;
178 /**
179 * Whether the viewer did. In live `message.*` events, which everyone in
180 * the room gets, it is always false: keep your own from what you know.
181 */
182 me: boolean;
183 /** The first ten who reacted with it, for the hover list. */
184 by: MemberProfile[];
185};
186
187/** Who may add a workspace's emoji: every member (the default), or only its owners. */
188export type EmojiUpload = "members" | "admins";
189
190/**
191 * One of a workspace's own emoji, used as `:name:`. Its image is served
192 * from the usercontent origin at `/emoji/<file>` (never from the site).
193 */
194export type CustomEmoji = {
195 name: string;
196 /** For an alias, the emoji it is another name for; it shows that one's image. */
197 alias_of: string | null;
198 /** The SHA-256 of the image's bytes. */
199 file: string;
200 content_type: "image/png" | "image/gif" | "image/webp";
201 bytes: number;
202 created_by: MemberProfile;
203 created_at: string;
204};
205
206export type EmojiList = {
207 emoji: CustomEmoji[];
208 emoji_upload: EmojiUpload;
209 /** Whether the viewer may add emoji and aliases. */
210 can_upload: boolean;
211 /** Whether the viewer is an owner: may remove any emoji and change `emoji_upload`. */
212 can_manage: boolean;
213};
214
Chat controls, public profiles, shadcn selects, and no Docs tab in a project215/** Who may do something in a workspace's chat: every member, or only its owners. */
216export type ChatAllowed = "members" | "owners";
217
218/**
219 * Who may rename and archive a channel: its owners (whoever made it) and
220 * the workspace's owners (the default), or the workspace's owners only.
221 */
222export type ChannelManagers = "channel_owners" | "owners";
223
224/**
225 * A workspace's chat settings, which its owners choose (Workspace,
226 * Settings, Chat). The chat service enforces each one; the page only
227 * hides what the viewer may not do.
228 */
229export type ChatSettings = {
230 /** Who may create public channels. */
231 public_channels: ChatAllowed;
232 /** Who may create private channels. */
233 private_channels: ChatAllowed;
234 /** Who may rename, archive and unarchive channels. */
235 manage_channels: ChannelManagers;
236 /** Who may add custom emoji (`admins` is the workspace's owners). */
237 emoji_upload: EmojiUpload;
238 /**
239 * The public channels someone new is put in the first time they open
240 * Chat, by id. #general unless the owners chose otherwise.
241 */
242 default_channels: string[];
243};
244
245/** What the viewer may do in a workspace's chat, from its settings and their role. */
246export type ChatPermissions = {
247 create_public_channels: boolean;
248 create_private_channels: boolean;
249 add_emoji: boolean;
250 /** Whether they may change the workspace's chat settings: owners only. */
251 manage_settings: boolean;
252};
253
254/** The chat settings page: the settings, what the viewer may do, and the public channels to choose defaults from. */
255export type ChatSettingsView = {
256 settings: ChatSettings;
257 can: ChatPermissions;
258 channels: Channel[];
259};
260
261/** A change to a channel: its name, its topic, or whether it is archived. Anything left out stays. */
262export type ChannelChange = { name?: string; topic?: string | null; archived?: boolean };
263
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)264/** An image for a new emoji, as base64. Its type is read from its bytes, never taken from here. */
265export type EmojiFile = { data: string };
266
267/** The largest custom emoji image, in bytes, and the widest or tallest, in pixels. */
268export const MAX_EMOJI_BYTES = 256 * 1024;
269export const MAX_EMOJI_SIDE = 512;
270
Chat and workspace agents: channels, DMs and named agents you talk to271/** One row of the Chat sidebar. */
272export type ChatSidebarEntry = {
273 channel: Channel;
274 /** "g1t-core", or the other members' names for a direct message. */
275 title: string;
276 /** For a direct message: who else is in it (up to four). */
277 others: MemberProfile[];
278 starred: boolean;
279 muted: boolean;
280 unread: number;
281 mentions: number;
282};
283
284export type ChatSidebar = {
285 entries: ChatSidebarEntry[];
286 /** Public channels in the workspace the viewer has not joined. */
287 browsable: number;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project288 /** What the viewer may do, from the workspace's chat settings: the page hides what they may not. */
289 can: ChatPermissions;
Chat and workspace agents: channels, DMs and named agents you talk to290};
291
292export type MessagePage = {
293 messages: ChatMessage[];
294 /** Pass as `before` to read further back; null at the beginning. */
295 older: string | null;
296 /**
297 * Set when the page was read with `after`: pass it as `after` again for
298 * the next messages, or null when this page reached the newest.
299 */
300 newer?: string | null;
301};
302
303export type NewChannel = { name: string; topic?: string | null; private?: boolean };
304
305export type PostMessage = { body: string; thread_root?: string | null };
306
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)307/**
308 * Who will read what is said in a conversation (docs/WORKSPACE.md, "What
309 * an agent can and can't know"): a direct message's or private channel's
310 * people, or, for a public channel, the whole workspace. An agent answers
311 * there only with what every one of them may see.
312 */
313export type ChatAudience = {
314 kind: "dm" | "private" | "public";
315 /** The people in it (agents left out). For a public channel, its current members, for reference only. */
316 member_user_ids: string[];
317 member_count: number;
318};
319
320/** A message an agent found by searching, with the conversation it is in. */
321export type AgentFoundMessage = {
322 channel_id: string;
323 /** The channel's name, or null for a direct message. */
324 channel: string | null;
325 message: ChatMessage;
326};
327
Chat and workspace agents: channels, DMs and named agents you talk to328/** The most agent-to-agent hops one person's request may start (docs/WORKSPACE.md, "Hop limit"). */
329export const CHAT_MAX_HOPS = 6;
330
331/**
332 * What an agent posts. When it answers a delivery, it passes that
333 * delivery's `hops`, `asked_by` and `asker` back, so another agent it @mentions is
334 * handed the message one hop further along the same person's request, and
335 * the chain stops at `CHAT_MAX_HOPS`. Left out: a new chain (hops 0) asked
336 * by the person who created the agent.
337 */
338export type AgentPostMessage = PostMessage & {
339 card?: MessageCard | null;
340 hops?: number;
341 asked_by?: string | null;
342 /** The delivery's `asker`, handed on to agents this message wakes. Absent: none (they treat the asker as unable to change code). */
343 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)344 /**
345 * The delivery's `chain`: the agents that handled this request before
346 * the one posting, oldest first. Chat adds the poster, and never hands
347 * the message to the agent that sent the work to it (no ping-pong).
348 */
349 chain?: string[];
Chat and workspace agents: channels, DMs and named agents you talk to350};
351
352/**
353 * What the live socket sends. The site opens
354 * `wss://<site>/<workspace>/chat/live?channel=<id>`; the site checks the
355 * session and forwards the upgrade to the chat service with the viewer.
356 */
357export type ChatLiveEvent =
358 | { type: "message.created"; message: ChatMessage }
359 | { type: "message.updated"; message: ChatMessage }
360 | { type: "message.deleted"; channel_id: string; id: string }
361 | { 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)362 | { type: "read"; channel_id: string; principal: Principal; last_read_id: string }
Chat controls, public profiles, shadcn selects, and no Docs tab in a project363 | { 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)364 | { 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 to365
366/**
367 * Header the site sets on a forwarded live socket: the viewer, as JSON.
368 * The site forwards the upgrade to the chat service's
369 * `GET /live?workspace=<slug>&channel=<id>` (`workspace` may be left out,
370 * at the cost of looking up each of the viewer's workspaces).
371 */
372export const CHAT_VIEWER_HEADER = "x-g1t-chat-viewer";
373
Chat controls, public profiles, shadcn selects, and no Docs tab in a project374/** A channel with its members, and whether the viewer may rename or archive it. */
375export type ChannelDetail = { channel: Channel; members: ChannelMember[]; can_manage: boolean };
376
Chat and workspace agents: channels, DMs and named agents you talk to377export type ChatApi = {
378 sidebar(workspace: string, viewer: User): Promise<Result<ChatSidebar>>;
379 channel(
380 workspace: string,
381 channelId: string,
382 viewer: User,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project383 ): Promise<Result<ChannelDetail>>;
Chat and workspace agents: channels, DMs and named agents you talk to384 /** The same, found by its name in the workspace (`#general` or `general`), as the site's URLs name channels. */
385 channelByName(
386 workspace: string,
387 name: string,
388 viewer: User,
Chat controls, public profiles, shadcn selects, and no Docs tab in a project389 ): Promise<Result<ChannelDetail>>;
390 /**
391 * Browse channels: every public channel, and the private ones the viewer
392 * is in. With `archived`, the archived ones instead.
393 */
394 browse(workspace: string, viewer: User, options?: { archived?: boolean }): Promise<Result<Channel[]>>;
395 /** 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 to396 createChannel(workspace: string, viewer: User, input: NewChannel): Promise<Result<Channel>>;
397 /**
Chat controls, public profiles, shadcn selects, and no Docs tab in a project398 * Renames, archives or unarchives a channel (the workspace's
399 * `manage_channels` setting says who may), or changes its topic (any
400 * member). #general is never renamed or archived. Everyone looking at it
401 * gets `channel.updated`.
402 */
403 updateChannel(workspace: string, channelId: string, viewer: User, change: ChannelChange): Promise<Result<Channel>>;
404 /** The workspace's chat settings, for any member; only owners may change them. */
405 chatSettings(workspace: string, viewer: User): Promise<Result<ChatSettingsView>>;
406 /** Owners only: changes some of the workspace's chat settings, and answers all of them. */
407 setChatSettings(workspace: string, viewer: User, change: Partial<ChatSettings>): Promise<Result<ChatSettings>>;
408 /**
Chat and workspace agents: channels, DMs and named agents you talk to409 * The direct message between the viewer and these members, created on
410 * first use. The same set of members always gets the same channel.
411 */
412 openDm(workspace: string, viewer: User, members: Principal[]): Promise<Result<Channel>>;
413 join(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
414 leave(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
415 /** Adds a person or an agent. An invite grants read, never write. */
416 invite(workspace: string, channelId: string, viewer: User, member: Principal): Promise<Result<null>>;
417 /**
418 * Newest first. Without `thread_root`, the channel's top-level messages;
419 * with it, that thread's replies, plus the message they reply to as the
420 * oldest once the page reaches the start of the thread (`older` null).
421 * With `after` (catching up after a reconnect): the messages after that
422 * id instead, oldest first, deleted ones included so the client can
423 * drop them; `newer` says whether there are more.
424 * Limit 50 by default, 200 at most.
425 */
426 messages(
427 workspace: string,
428 channelId: string,
429 viewer: User,
430 page?: { before?: string | null; after?: string | null; limit?: number; thread_root?: string | null },
431 ): Promise<Result<MessagePage>>;
432 post(workspace: string, channelId: string, viewer: User, message: PostMessage): Promise<Result<ChatMessage>>;
433 edit(workspace: string, channelId: string, viewer: User, id: string, body: string): Promise<Result<ChatMessage>>;
434 remove(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
435 markRead(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
436 setPreferences(
437 workspace: string,
438 channelId: string,
439 viewer: User,
440 prefs: { starred?: boolean; muted?: boolean },
441 ): Promise<Result<null>>;
442 /**
443 * Posts as an agent. Only the agents service calls this, for replies and
444 * cards; the agent must be a member of the channel.
445 */
446 postAsAgent(
447 workspace: string,
448 channelId: string,
449 agentId: string,
450 message: AgentPostMessage,
451 ): Promise<Result<ChatMessage>>;
Chat controls, public profiles, shadcn selects, and no Docs tab in a project452 /**
453 * Changes a message an agent posted: its body, its card, or both. Only
454 * the agents service calls this, to keep a session's live card current.
455 * Changing a message wakes nobody.
456 */
457 updateAsAgent(
458 workspace: string,
459 channelId: string,
460 agentId: string,
461 id: string,
462 change: { body?: string; card?: MessageCard | null },
463 ): Promise<Result<ChatMessage>>;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar464 /**
465 * A person presses an action on a card in a conversation they can read.
466 * Chat hands it to the card's owner with the person, and the owner
467 * decides, acts and updates the card.
468 */
469 cardAction(
470 workspace: string,
471 channelId: string,
472 viewer: User,
473 messageId: string,
474 actionId: string,
475 input?: string | null,
476 ): Promise<Result<CardActionResult>>;
Chat and workspace agents: channels, DMs and named agents you talk to477 /** Shows "is typing" for an agent while it works on a reply. */
478 agentTyping(workspace: string, channelId: string, agentId: string): Promise<Result<null>>;
479 /**
480 * What an agent reads before it replies, oldest first: with
481 * `thread_root`, that thread (its root, then its replies); without, the
482 * channel's or direct message's latest top-level messages. Only the
483 * agents service calls this; the agent must be a member of the channel,
484 * so it reads only what was said where it was invited. `limit` defaults
485 * to 30, at most 100.
486 */
487 historyForAgent(
488 workspace: string,
489 channelId: string,
490 agentId: string,
491 page?: { thread_root?: string | null; limit?: number },
492 ): 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)493 /** Internal: who reads a conversation. */
494 audience(workspace: string, channelId: string): Promise<Result<ChatAudience>>;
495 /**
496 * Internal, for an agent replying in `channelId`: messages matching
497 * `query` (newest first, at most 20) from conversations every person in
498 * that conversation's audience is in, and from public channels. Never
499 * another direct message unless it has exactly the same people. The
500 * audience is worked out here from `channelId`, never taken from the
501 * caller.
502 */
503 searchForAgent(workspace: string, channelId: string, query: string, limit?: number): Promise<Result<AgentFoundMessage[]>>;
504 /**
505 * Internal, for an agent replying in `channelId`: a thread (`id`, its
506 * root or any reply) in `targetChannelId`, oldest first, under the same
507 * rule. A conversation the audience may not read is not found, exactly
508 * as one that does not exist.
509 */
510 threadForAgent(workspace: string, channelId: string, targetChannelId: string, id: string): Promise<Result<AgentFoundMessage[]>>;
511 /**
512 * Reacts to a message with `emoji` (one Unicode emoji, or `:name:` for
513 * one of the workspace's own). Once per member per emoji; at most 50
514 * different emoji on a message. Answers the message's reactions now.
515 */
516 react(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
517 unreact(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>;
518 /** Internal, for the agents service: an agent reacts (or, with `remove`, takes it back). It must be in the channel. */
519 reactAsAgent(
520 workspace: string,
521 channelId: string,
522 agentId: string,
523 messageId: string,
524 emoji: string,
525 remove?: boolean,
526 ): Promise<Result<ChatReaction[]>>;
527 /** The workspace's own emoji, by name, and who may add them. */
528 listEmoji(workspace: string, viewer: User): Promise<Result<EmojiList>>;
529 /** Adds an emoji: a PNG, GIF or WebP of at most 256 KB and 512×512. */
530 addEmoji(workspace: string, viewer: User, name: string, file: EmojiFile): Promise<Result<CustomEmoji>>;
531 /** Gives an existing emoji (`target`) another name. */
532 aliasEmoji(workspace: string, viewer: User, name: string, target: string): Promise<Result<CustomEmoji>>;
533 /** Removes an emoji, and its aliases with it. Its creator or an owner may. */
534 removeEmoji(workspace: string, viewer: User, name: string): Promise<Result<null>>;
535 /** Owners only: who may add emoji. */
536 setEmojiUpload(workspace: string, viewer: User, value: EmojiUpload): Promise<Result<EmojiUpload>>;
Chat and workspace agents: channels, DMs and named agents you talk to537};
538
539async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
540 const response = await service.fetch(`https://service/rpc/${method}`, {
541 method: "POST",
542 headers: { "content-type": "application/json" },
543 body: JSON.stringify(args),
544 });
545 if (!response.ok) {
546 throw new Error(`${method} failed with status ${response.status}`);
547 }
548 return (await response.json()) as T;
549}
550
551export function chatClient(service: ServiceBinding): ChatApi {
552 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
553 return {
554 sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }),
555 channel: (workspace, channelId, viewer) => call("channel", { workspace, channel_id: channelId, viewer }),
556 channelByName: (workspace, name, viewer) => call("channel_by_name", { workspace, name, viewer }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project557 browse: (workspace, viewer, options) => call("browse", { workspace, viewer, archived: options?.archived ?? false }),
Chat and workspace agents: channels, DMs and named agents you talk to558 createChannel: (workspace, viewer, input) => call("create_channel", { workspace, viewer, input }),
Chat controls, public profiles, shadcn selects, and no Docs tab in a project559 updateChannel: (workspace, channelId, viewer, change) => call("update_channel", { workspace, channel_id: channelId, viewer, change }),
560 chatSettings: (workspace, viewer) => call("chat_settings", { workspace, viewer }),
561 setChatSettings: (workspace, viewer, change) => call("set_chat_settings", { workspace, viewer, change }),
Chat and workspace agents: channels, DMs and named agents you talk to562 openDm: (workspace, viewer, members) => call("open_dm", { workspace, viewer, members }),
563 join: (workspace, channelId, viewer) => call("join", { workspace, channel_id: channelId, viewer }),
564 leave: (workspace, channelId, viewer) => call("leave", { workspace, channel_id: channelId, viewer }),
565 invite: (workspace, channelId, viewer, member) =>
566 call("invite", { workspace, channel_id: channelId, viewer, member }),
567 messages: (workspace, channelId, viewer, page) =>
568 call("messages", {
569 workspace,
570 channel_id: channelId,
571 viewer,
572 before: page?.before ?? null,
573 after: page?.after ?? null,
574 limit: page?.limit ?? null,
575 thread_root: page?.thread_root ?? null,
576 }),
577 post: (workspace, channelId, viewer, message) => call("post", { workspace, channel_id: channelId, viewer, message }),
578 edit: (workspace, channelId, viewer, id, body) => call("edit", { workspace, channel_id: channelId, viewer, id, body }),
579 remove: (workspace, channelId, viewer, id) => call("remove", { workspace, channel_id: channelId, viewer, id }),
580 markRead: (workspace, channelId, viewer, id) => call("mark_read", { workspace, channel_id: channelId, viewer, id }),
581 setPreferences: (workspace, channelId, viewer, prefs) =>
582 call("set_preferences", { workspace, channel_id: channelId, viewer, prefs }),
583 postAsAgent: (workspace, channelId, agentId, message) =>
584 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 calendar585 cardAction: (workspace, channelId, viewer, messageId, actionId, input) =>
586 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 project587 updateAsAgent: (workspace, channelId, agentId, id, change) =>
588 call("update_as_agent", { workspace, channel_id: channelId, agent_id: agentId, id, change }),
Chat and workspace agents: channels, DMs and named agents you talk to589 agentTyping: (workspace, channelId, agentId) =>
590 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)591 audience: (workspace, channelId) => call("audience", { workspace, channel_id: channelId }),
592 searchForAgent: (workspace, channelId, query, limit) =>
593 call("search_for_agent", { workspace, channel_id: channelId, query, limit: limit ?? null }),
594 threadForAgent: (workspace, channelId, targetChannelId, id) =>
595 call("thread_for_agent", { workspace, channel_id: channelId, target_channel_id: targetChannelId, id }),
596 react: (workspace, channelId, viewer, messageId, emoji) =>
597 call("react", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
598 unreact: (workspace, channelId, viewer, messageId, emoji) =>
599 call("unreact", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
600 reactAsAgent: (workspace, channelId, agentId, messageId, emoji, remove) =>
601 call("react_as_agent", { workspace, channel_id: channelId, agent_id: agentId, message_id: messageId, emoji, remove: remove ?? false }),
602 listEmoji: (workspace, viewer) => call("list_emoji", { workspace, viewer }),
603 addEmoji: (workspace, viewer, name, file) => call("add_emoji", { workspace, viewer, name, file }),
604 aliasEmoji: (workspace, viewer, name, target) => call("alias_emoji", { workspace, viewer, name, target }),
605 removeEmoji: (workspace, viewer, name) => call("remove_emoji", { workspace, viewer, name }),
606 setEmojiUpload: (workspace, viewer, value) => call("set_emoji_upload", { workspace, viewer, value }),
Chat and workspace agents: channels, DMs and named agents you talk to607 historyForAgent: (workspace, channelId, agentId, page) =>
608 call("history_for_agent", {
609 workspace,
610 channel_id: channelId,
611 agent_id: agentId,
612 thread_root: page?.thread_root ?? null,
613 limit: page?.limit ?? null,
614 }),
615 };
616}

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