Skip to content

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

639 linesCodeBlameRaw
1/**
2 * Chat: channels, direct messages, threads and messages, kept by the chat
3 * service (`services/chat`). Members are people; messages from the agents
4 * workspaces once had stay readable under the names they had.
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 { AskPage } from "./integrations";
12import type { Result } from "./result";
13
14/**
15 * Who is speaking: a person (by user id), g1t itself (`system:g1t`, the one
16 * reserved actor all of g1t's own work shows as), or, on older messages, a
17 * former workspace agent (by agent id).
18 */
19export type Principal = { kind: "user" | "agent" | "system"; id: string };
20
21/**
22 * g1t in chat: @g1t, the same reserved `system` actor (id `g1t`) the
23 * runner and the feed show g1t's own work as. Every member can talk with
24 * it in a direct message, where it answers with the workspace's own AI
25 * provider (docs.g1t.sh/guides/ask/). It is never a person: the People
26 * directory and member pickers never list it.
27 */
28export const G1T_PRINCIPAL: Principal = { kind: "system", id: "g1t" };
29/** `system:g1t`, as chat keeps members and authors. */
30export const G1T_KEY = "system:g1t";
31
32/** Whether a member is g1t itself. */
33export function isG1t(member: { kind: string; id?: string } | null | undefined): boolean {
34 return member?.kind === "system" && member.id === "g1t";
35}
36
37export function principalKey(principal: Principal): string {
38 return `${principal.kind}:${principal.id}`;
39}
40
41export function parsePrincipalKey(key: string): Principal | null {
42 // g1t is the only `system` member there is.
43 if (key === G1T_KEY) return { ...G1T_PRINCIPAL };
44 const at = key.indexOf(":");
45 if (at < 0) return null;
46 const kind = key.slice(0, at);
47 const id = key.slice(at + 1);
48 if ((kind !== "user" && kind !== "agent") || !id) return null;
49 return { kind, id };
50}
51
52/** How a member shows: resolved by the chat service when it answers. */
53export type MemberProfile = Principal & {
54 /** `username` for a person (lowercased: what `@` mentions), `handle` for an agent. */
55 name: string;
56 /**
57 * A person's username as they wrote it (`Ana`), when that differs from
58 * `name`; absent for an agent. Shown in their card and in autocomplete.
59 */
60 display_username?: string | null;
61 /**
62 * What chat shows them as: a person's display name, else their username
63 * in its chosen case; an agent's display name. Read it with `memberName`.
64 */
65 display_name: string;
66 /** Uploaded avatar hash for a person, or null for the letter avatar. */
67 avatar: string | null;
68 /** An agent's one-line role ("Release manager for g1t"). */
69 role: string | null;
70 /**
71 * An agent's title ("QA Engineer"); null for a person. Chat always sets
72 * it; optional so profiles a page makes for itself need not.
73 */
74 title?: string | null;
75 /** What an agent's face is drawn from when it chose none; null for a person. Always set by chat. */
76 avatar_seed?: string | null;
77 /**
78 * An archived agent (docs.g1t.sh/guides/chat/, "Archived and deleted
79 * agents"): when, and by whom. Its conversations stay readable; it is
80 * never woken and never listed to talk to. Null or absent for a live
81 * agent and for a person.
82 */
83 archived_at?: string | null;
84 archived_by?: string | null;
85 /** An agent deleted for good: its messages stay under the name it had. Absent otherwise. */
86 deleted?: boolean;
87};
88
89/**
90 * A member's handle as it shows (`@Ana`, without the `@`): a person's
91 * username in its chosen case, an agent's handle.
92 */
93export function memberHandle(member: { name: string; display_username?: string | null }): string {
94 const display = member.display_username;
95 return display && display.toLowerCase() === member.name.toLowerCase() ? display : member.name;
96}
97
98/**
99 * How a member's name is shown everywhere in chat (messages, the
100 * sidebar, direct-message titles, typing, notifications and pushes): their
101 * display name, else their handle in its chosen case. One rule, so every
102 * surface agrees.
103 */
104export function memberName(member: { name: string; display_name?: string | null; display_username?: string | null }): string {
105 return member.display_name?.trim() || memberHandle(member);
106}
107
108export type ChannelKind = "channel" | "dm";
109
110export type Channel = {
111 id: string;
112 workspace_id: string;
113 kind: ChannelKind;
114 /** Lowercase, no `#`. Null for a direct message. */
115 name: string | null;
116 topic: string | null;
117 private: boolean;
118 created_by: Principal;
119 created_at: string;
120 archived_at: string | null;
121 last_message_at: string | null;
122};
123
124export type ChannelMember = {
125 channel_id: string;
126 member: MemberProfile;
127 role: "owner" | "member";
128 starred: boolean;
129 muted: boolean;
130 last_read_id: string | null;
131 joined_at: string;
132};
133
134/** A card g1t or an agent posts: an event, a task, an approval. */
135export type MessageCard = {
136 /** What it is about, e.g. `pull`, `issue`, `task`, `deploy`, `approval`, `session`, `draft_issue`. */
137 kind: string;
138 title: string;
139 /** A short line under the title: "3/3 checks · +214 −87". */
140 detail: string | null;
141 /** A status shown on the right: "Needs approval", "Merged". */
142 state: string | null;
143 /** Where clicking the card goes, relative to the site. */
144 href: string | null;
145 /** A preview in Markdown under the title: a draft issue's body, a report's first lines. */
146 body?: string | null;
147 /** Labelled facts shown in two columns: "Repository · acme/web", "Cap · $2.00". */
148 fields?: CardField[];
149 /**
150 * What people can do right here. Pressing one goes to the service that
151 * owns the card (`owner`), which checks the person may, does it, and
152 * updates the card in place. A card without `owner` has no actions.
153 */
154 actions?: CardAction[];
155 /** The service whose card this was and that answered its actions: `agents`, on older cards only. */
156 owner?: "agents" | null;
157 /** What the card is about, for its owner: a session id, a draft id. */
158 ref?: string | null;
159};
160
161export type CardField = { label: string; value: string };
162
163/** A button on a card. */
164export type CardAction = {
165 /** Unique on the card: `stop`, `approve`, `file`. */
166 id: string;
167 label: string;
168 style?: "primary" | "danger" | "default";
169 /** Asked before it runs: "Stop this session and everything under it?". */
170 confirm?: string | null;
171 /** A value it needs first, asked inline: an amount in dollars, or a line of text. */
172 input?: { kind: "money" | "text"; label: string; placeholder?: string | null; initial?: string | null } | null;
173 /** A link instead of an action: opens this place in the site. */
174 href?: string | null;
175};
176
177export type ChatMessage = {
178 /** Time-sortable (ULID-like), so ordering by id is ordering by time. */
179 id: string;
180 channel_id: string;
181 author: MemberProfile;
182 kind: "text" | "card";
183 body: string;
184 card: MessageCard | null;
185 /** The message this replies under, or null for a top-level message. */
186 thread_root: string | null;
187 reply_count: number;
188 last_reply_at: string | null;
189 created_at: string;
190 edited_at: string | null;
191 deleted_at: string | null;
192 /**
193 * Its reactions, one per emoji in the order each was first used. Chat
194 * always sets it; optional so messages a page makes for itself need not.
195 */
196 reactions?: ChatReaction[];
197 /**
198 * On a message from @g1t that answers someone: how the answer went, who
199 * wrote it, and the steps it took. Absent on every other message.
200 */
201 answer?: ChatAnswer | null;
202 /**
203 * On a question someone asked @g1t from a page (the top bar's Ask): the
204 * page they were on, which g1t is told. Absent otherwise.
205 */
206 page?: AskPage | null;
207 /**
208 * A "New conversation" mark in a direct message with @g1t: answers after
209 * it read only what was said after it. Absent otherwise.
210 */
211 reset?: boolean;
212};
213
214/**
215 * Where an answer from @g1t is: `thinking` until its first words,
216 * `streaming` while they come (live as `message.streaming`), then `done`,
217 * `stopped` (Stop, or a newer question took over) or `error`.
218 */
219export type ChatAnswerState = "thinking" | "streaming" | "done" | "stopped" | "error";
220
221export type ChatAnswer = {
222 state: ChatAnswerState;
223 /** The provider's name and the model, once the provider said. */
224 provider_label?: string | null;
225 model?: string | null;
226 /** Auto chose `model` for this question. */
227 auto?: boolean;
228 /** `max_tokens`, `length`: cut off at its limit. */
229 stop_reason?: string | null;
230 /** What went wrong, for `error`. */
231 error?: string | null;
232 /**
233 * Steps the answer took on its way (tool calls and the like), as the
234 * integrations service streamed them: each `ask` event other than meta,
235 * text, done and error, kept as it came. At most 50.
236 */
237 steps?: Record<string, unknown>[];
238 /** Set when Ask is not set up in the workspace: the answer says where to set it up. */
239 setup?: "missing" | null;
240};
241
242/**
243 * One emoji's reactions on a message. `emoji` is a Unicode emoji or
244 * `:name:` for one of the workspace's own (`CustomEmoji`).
245 */
246export type ChatReaction = {
247 emoji: string;
248 /** Everyone who reacted with it, people and agents alike. */
249 count: number;
250 /**
251 * Whether the viewer did. In live `message.*` events, which everyone in
252 * the room gets, it is always false: keep your own from what you know.
253 */
254 me: boolean;
255 /** The first ten who reacted with it, for the hover list. */
256 by: MemberProfile[];
257};
258
259/** Who may add a workspace's emoji: every member (the default), or only its owners. */
260export type EmojiUpload = "members" | "admins";
261
262/**
263 * One of a workspace's own emoji, used as `:name:`. Its image is served
264 * from the usercontent origin at `/emoji/<file>` (never from the site).
265 */
266export type CustomEmoji = {
267 name: string;
268 /** For an alias, the emoji it is another name for; it shows that one's image. */
269 alias_of: string | null;
270 /** The SHA-256 of the image's bytes. */
271 file: string;
272 content_type: "image/png" | "image/gif" | "image/webp";
273 bytes: number;
274 created_by: MemberProfile;
275 created_at: string;
276};
277
278export type EmojiList = {
279 emoji: CustomEmoji[];
280 emoji_upload: EmojiUpload;
281 /** Whether the viewer may add emoji and aliases. */
282 can_upload: boolean;
283 /** Whether the viewer is an owner: may remove any emoji and change `emoji_upload`. */
284 can_manage: boolean;
285};
286
287/** Who may do something in a workspace's chat: every member, or only its owners. */
288export type ChatAllowed = "members" | "owners";
289
290/**
291 * Who may rename and archive a channel: its owners (whoever made it) and
292 * the workspace's owners (the default), or the workspace's owners only.
293 */
294export type ChannelManagers = "channel_owners" | "owners";
295
296/**
297 * A workspace's chat settings, which its owners choose (Workspace,
298 * Settings, Chat). The chat service enforces each one; the page only
299 * hides what the viewer may not do.
300 */
301export type ChatSettings = {
302 /** Who may create public channels. */
303 public_channels: ChatAllowed;
304 /** Who may create private channels. */
305 private_channels: ChatAllowed;
306 /** Who may rename, archive and unarchive channels. */
307 manage_channels: ChannelManagers;
308 /** Who may add custom emoji (`admins` is the workspace's owners). */
309 emoji_upload: EmojiUpload;
310 /**
311 * The public channels someone new is put in the first time they open
312 * Chat, by id. #general unless the owners chose otherwise.
313 */
314 default_channels: string[];
315};
316
317/** What the viewer may do in a workspace's chat, from its settings and their role. */
318export type ChatPermissions = {
319 create_public_channels: boolean;
320 create_private_channels: boolean;
321 add_emoji: boolean;
322 /** Whether they may change the workspace's chat settings: owners only. */
323 manage_settings: boolean;
324};
325
326/** The chat settings page: the settings, what the viewer may do, and the public channels to choose defaults from. */
327export type ChatSettingsView = {
328 settings: ChatSettings;
329 can: ChatPermissions;
330 channels: Channel[];
331};
332
333/** A change to a channel: its name, its topic, or whether it is archived. Anything left out stays. */
334export type ChannelChange = { name?: string; topic?: string | null; archived?: boolean };
335
336/** An image for a new emoji, as base64. Its type is read from its bytes, never taken from here. */
337export type EmojiFile = { data: string };
338
339/** The largest custom emoji image, in bytes, and the widest or tallest, in pixels. */
340export const MAX_EMOJI_BYTES = 256 * 1024;
341export const MAX_EMOJI_SIDE = 512;
342
343/** One row of the Chat sidebar. */
344export type ChatSidebarEntry = {
345 channel: Channel;
346 /** "g1t-core", or the other members' names for a direct message. */
347 title: string;
348 /** For a direct message: who else is in it (up to four). */
349 others: MemberProfile[];
350 starred: boolean;
351 muted: boolean;
352 unread: number;
353 mentions: number;
354};
355
356export type ChatSidebar = {
357 entries: ChatSidebarEntry[];
358 /** Public channels in the workspace the viewer has not joined. */
359 browsable: number;
360 /** What the viewer may do, from the workspace's chat settings: the page hides what they may not. */
361 can: ChatPermissions;
362};
363
364/**
365 * `activity`: what was said over a span, in the conversations the viewer
366 * can read (every public channel, and the private channels and direct
367 * messages they are in). Messages are text messages and thread replies
368 * that were not deleted; cards agents post are not messages. Home reads it
369 * for what people and agents said since you were last there.
370 */
371export type ChatActivity = {
372 /** RFC 3339: `[from, until)`. */
373 from: string;
374 until: string;
375 messages: number;
376 /** Conversations with at least one message. */
377 channels: number;
378 /** Who said how much: member keys (`user:<id>`, `agent:<id>`), most first. */
379 authors: { key: string; messages: number }[];
380};
381
382export type MessagePage = {
383 messages: ChatMessage[];
384 /** Pass as `before` to read further back; null at the beginning. */
385 older: string | null;
386 /**
387 * For a thread's first page: the message the thread is under, however
388 * many replies it has, so a thread panel always shows it (a session's
389 * live card, say) at its top.
390 */
391 root?: ChatMessage | null;
392 /**
393 * Set when the page was read with `after`: pass it as `after` again for
394 * the next messages, or null when this page reached the newest.
395 */
396 newer?: string | null;
397};
398
399export type NewChannel = { name: string; topic?: string | null; private?: boolean };
400
401export type PostMessage = {
402 body: string;
403 thread_root?: string | null;
404 /** In a direct message with @g1t: the page the person asks from, which g1t is told. Ignored elsewhere. */
405 page?: AskPage | null;
406};
407
408/**
409 * What the live socket sends. The site opens
410 * `wss://<site>/<workspace>/chat/live?channel=<id>`; the site checks the
411 * session and forwards the upgrade to the chat service with the viewer.
412 */
413export type ChatLiveEvent =
414 | { type: "message.created"; message: ChatMessage }
415 | { type: "message.updated"; message: ChatMessage }
416 | { type: "message.deleted"; channel_id: string; id: string }
417 /**
418 * An answer from @g1t as it is written: the whole text so far (not a
419 * delta, so a late or dropped frame never garbles it) and where it is.
420 * The stored message catches up with a `message.updated` at the end.
421 */
422 | { type: "message.streaming"; channel_id: string; id: string; body: string; answer: ChatAnswer }
423 | { type: "typing"; channel_id: string; member: MemberProfile; until: string }
424 | { type: "read"; channel_id: string; principal: Principal; last_read_id: string }
425 | { type: "channel.updated"; channel: Channel }
426 /**
427 * Everything in the conversation was deleted for good: the person
428 * cleared their direct message with @g1t. Every message shown goes.
429 */
430 | { type: "channel.cleared"; channel_id: string }
431 | { type: "reaction.added" | "reaction.removed"; channel_id: string; message_id: string; emoji: string; member: MemberProfile };
432
433/**
434 * Header the site sets on a forwarded live socket: the viewer, as JSON.
435 * The site forwards the upgrade to the chat service's
436 * `GET /live?workspace=<slug>&channel=<id>` (`workspace` may be left out,
437 * at the cost of looking up each of the viewer's workspaces).
438 */
439export const CHAT_VIEWER_HEADER = "x-g1t-chat-viewer";
440
441/** A channel with its members, and whether the viewer may rename or archive it. */
442export type ChannelDetail = { channel: Channel; members: ChannelMember[]; can_manage: boolean };
443
444export type ChatApi = {
445 sidebar(workspace: string, viewer: User): Promise<Result<ChatSidebar>>;
446 /** What was said in `[from, until)` (RFC 3339) where the viewer can read, counted. */
447 activity(workspace: string, viewer: User, span: { from: string; until: string }): Promise<Result<ChatActivity>>;
448 channel(
449 workspace: string,
450 channelId: string,
451 viewer: User,
452 ): Promise<Result<ChannelDetail>>;
453 /** The same, found by its name in the workspace (`#general` or `general`), as the site's URLs name channels. */
454 channelByName(
455 workspace: string,
456 name: string,
457 viewer: User,
458 ): Promise<Result<ChannelDetail>>;
459 /**
460 * Browse channels: every public channel, and the private ones the viewer
461 * is in. With `archived`, the archived ones instead.
462 */
463 browse(workspace: string, viewer: User, options?: { archived?: boolean }): Promise<Result<Channel[]>>;
464 /** Makes a channel. Who may make a public or a private one is the workspace's setting. */
465 createChannel(workspace: string, viewer: User, input: NewChannel): Promise<Result<Channel>>;
466 /**
467 * Renames, archives or unarchives a channel (the workspace's
468 * `manage_channels` setting says who may), or changes its topic (any
469 * member). #general is never renamed or archived. Everyone looking at it
470 * gets `channel.updated`.
471 */
472 updateChannel(workspace: string, channelId: string, viewer: User, change: ChannelChange): Promise<Result<Channel>>;
473 /** The workspace's chat settings, for any member; only owners may change them. */
474 chatSettings(workspace: string, viewer: User): Promise<Result<ChatSettingsView>>;
475 /** Owners only: changes some of the workspace's chat settings, and answers all of them. */
476 setChatSettings(workspace: string, viewer: User, change: Partial<ChatSettings>): Promise<Result<ChatSettings>>;
477 /**
478 * The direct message between the viewer and these members, created on
479 * first use. The same set of members always gets the same channel.
480 */
481 openDm(workspace: string, viewer: User, members: Principal[]): Promise<Result<Channel>>;
482 join(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
483 leave(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
484 /** Adds a person. An invite grants read, never write. */
485 invite(workspace: string, channelId: string, viewer: User, member: Principal): Promise<Result<null>>;
486 /**
487 * Newest first. Without `thread_root`, the channel's top-level messages;
488 * with it, that thread's replies, plus the message they reply to as the
489 * oldest once the page reaches the start of the thread (`older` null).
490 * With `after` (catching up after a reconnect): the messages after that
491 * id instead, oldest first, deleted ones included so the client can
492 * drop them; `newer` says whether there are more.
493 * Limit 50 by default, 200 at most.
494 */
495 messages(
496 workspace: string,
497 channelId: string,
498 viewer: User,
499 page?: { before?: string | null; after?: string | null; limit?: number; thread_root?: string | null },
500 ): Promise<Result<MessagePage>>;
501 post(workspace: string, channelId: string, viewer: User, message: PostMessage): Promise<Result<ChatMessage>>;
502 edit(workspace: string, channelId: string, viewer: User, id: string, body: string): Promise<Result<ChatMessage>>;
503 remove(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
504 markRead(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>;
505 setPreferences(
506 workspace: string,
507 channelId: string,
508 viewer: User,
509 prefs: { starred?: boolean; muted?: boolean },
510 ): Promise<Result<null>>;
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 /** The workspace's own emoji, by name, and who may add them. */
519 listEmoji(workspace: string, viewer: User): Promise<Result<EmojiList>>;
520 /** Adds an emoji: a PNG, GIF or WebP of at most 256 KB and 512×512. */
521 addEmoji(workspace: string, viewer: User, name: string, file: EmojiFile): Promise<Result<CustomEmoji>>;
522 /** Gives an existing emoji (`target`) another name. */
523 aliasEmoji(workspace: string, viewer: User, name: string, target: string): Promise<Result<CustomEmoji>>;
524 /** Removes an emoji, and its aliases with it. Its creator or an owner may. */
525 removeEmoji(workspace: string, viewer: User, name: string): Promise<Result<null>>;
526 /** Owners only: who may add emoji. */
527 setEmojiUpload(workspace: string, viewer: User, value: EmojiUpload): Promise<Result<EmojiUpload>>;
528 /** The viewer's direct message with @g1t, created on first use. */
529 g1tDm(workspace: string, viewer: User): Promise<Result<Channel>>;
530 /**
531 * Starts a new conversation in the viewer's direct message with @g1t: a
532 * mark after which answers read only what comes next. Stops an answer
533 * still coming.
534 */
535 g1tReset(workspace: string, channelId: string, viewer: User): Promise<Result<ChatMessage>>;
536 /** Has @g1t answer the latest question again, in place of its last answer. */
537 g1tRetry(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
538 /** Stops @g1t's answer still coming; what came stays. */
539 g1tStop(workspace: string, channelId: string, viewer: User): Promise<Result<null>>;
540 /**
541 * Clears the viewer's own direct message with @g1t: every message in
542 * it, g1t's answers and their steps, reactions and the notifications
543 * from it are deleted for good, and an answer still coming is thrown
544 * away. `cleared` is how many messages went; 0 when it was empty, so
545 * clearing again is harmless. Refused for any other conversation.
546 */
547 g1tClear(workspace: string, channelId: string, viewer: User): Promise<Result<{ cleared: number }>>;
548};
549
550/**
551 * For g1t's own services only, never the site on a person's behalf: posts
552 * and edits messages as @g1t in a direct message with it. The chat service
553 * answers these at `POST /internal/<method>`, which no route from the site
554 * reaches, and refuses anything that came in from the internet.
555 */
556export type ChatInternalApi = {
557 /** Posts `body` as @g1t in a direct message with it, in the workspace with this slug. */
558 g1tPost(workspace: string, channelId: string, body: string): Promise<Result<ChatMessage>>;
559 /** Changes the text of one of @g1t's messages there. */
560 g1tEdit(workspace: string, channelId: string, id: string, body: string): Promise<Result<ChatMessage>>;
561};
562
563export function chatInternalClient(service: ServiceBinding): ChatInternalApi {
564 const call = async <T>(method: string, args: object): Promise<T> => {
565 const response = await service.fetch(`https://service/internal/${method}`, {
566 method: "POST",
567 headers: { "content-type": "application/json" },
568 body: JSON.stringify(args),
569 });
570 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
571 return (await response.json()) as T;
572 };
573 return {
574 g1tPost: (workspace, channelId, body) => call("g1t_post", { workspace, channel_id: channelId, body }),
575 g1tEdit: (workspace, channelId, id, body) => call("g1t_edit", { workspace, channel_id: channelId, id, body }),
576 };
577}
578
579async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
580 const response = await service.fetch(`https://service/rpc/${method}`, {
581 method: "POST",
582 headers: { "content-type": "application/json" },
583 body: JSON.stringify(args),
584 });
585 if (!response.ok) {
586 throw new Error(`${method} failed with status ${response.status}`);
587 }
588 return (await response.json()) as T;
589}
590
591export function chatClient(service: ServiceBinding): ChatApi {
592 const call = <T>(method: string, args: object) => rpc<T>(service, method, args);
593 return {
594 sidebar: (workspace, viewer) => call("sidebar", { workspace, viewer }),
595 activity: (workspace, viewer, span) => call("activity", { workspace, viewer, from: span.from, until: span.until }),
596 channel: (workspace, channelId, viewer) => call("channel", { workspace, channel_id: channelId, viewer }),
597 channelByName: (workspace, name, viewer) => call("channel_by_name", { workspace, name, viewer }),
598 browse: (workspace, viewer, options) => call("browse", { workspace, viewer, archived: options?.archived ?? false }),
599 createChannel: (workspace, viewer, input) => call("create_channel", { workspace, viewer, input }),
600 updateChannel: (workspace, channelId, viewer, change) => call("update_channel", { workspace, channel_id: channelId, viewer, change }),
601 chatSettings: (workspace, viewer) => call("chat_settings", { workspace, viewer }),
602 setChatSettings: (workspace, viewer, change) => call("set_chat_settings", { workspace, viewer, change }),
603 openDm: (workspace, viewer, members) => call("open_dm", { workspace, viewer, members }),
604 join: (workspace, channelId, viewer) => call("join", { workspace, channel_id: channelId, viewer }),
605 leave: (workspace, channelId, viewer) => call("leave", { workspace, channel_id: channelId, viewer }),
606 invite: (workspace, channelId, viewer, member) =>
607 call("invite", { workspace, channel_id: channelId, viewer, member }),
608 messages: (workspace, channelId, viewer, page) =>
609 call("messages", {
610 workspace,
611 channel_id: channelId,
612 viewer,
613 before: page?.before ?? null,
614 after: page?.after ?? null,
615 limit: page?.limit ?? null,
616 thread_root: page?.thread_root ?? null,
617 }),
618 post: (workspace, channelId, viewer, message) => call("post", { workspace, channel_id: channelId, viewer, message }),
619 edit: (workspace, channelId, viewer, id, body) => call("edit", { workspace, channel_id: channelId, viewer, id, body }),
620 remove: (workspace, channelId, viewer, id) => call("remove", { workspace, channel_id: channelId, viewer, id }),
621 markRead: (workspace, channelId, viewer, id) => call("mark_read", { workspace, channel_id: channelId, viewer, id }),
622 setPreferences: (workspace, channelId, viewer, prefs) =>
623 call("set_preferences", { workspace, channel_id: channelId, viewer, prefs }),
624 react: (workspace, channelId, viewer, messageId, emoji) =>
625 call("react", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
626 unreact: (workspace, channelId, viewer, messageId, emoji) =>
627 call("unreact", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }),
628 listEmoji: (workspace, viewer) => call("list_emoji", { workspace, viewer }),
629 addEmoji: (workspace, viewer, name, file) => call("add_emoji", { workspace, viewer, name, file }),
630 aliasEmoji: (workspace, viewer, name, target) => call("alias_emoji", { workspace, viewer, name, target }),
631 removeEmoji: (workspace, viewer, name) => call("remove_emoji", { workspace, viewer, name }),
632 setEmojiUpload: (workspace, viewer, value) => call("set_emoji_upload", { workspace, viewer, value }),
633 g1tDm: (workspace, viewer) => call("g1t_dm", { workspace, viewer }),
634 g1tReset: (workspace, channelId, viewer) => call("g1t_reset", { workspace, channel_id: channelId, viewer }),
635 g1tRetry: (workspace, channelId, viewer) => call("g1t_retry", { workspace, channel_id: channelId, viewer }),
636 g1tStop: (workspace, channelId, viewer) => call("g1t_stop", { workspace, channel_id: channelId, viewer }),
637 g1tClear: (workspace, channelId, viewer) => call("g1t_clear", { workspace, channel_id: channelId, viewer }),
638 };
639}