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 to | 1 | /** |
| 2 | * Chat: channels, direct messages, threads and messages, kept by the chat | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 3 | * service (`services/chat`). Members are people; messages from the agents |
| 4 | * workspaces once had stay readable under the names they had. | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 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 | */ | |
| 9 | import type { ServiceBinding } from "./clients"; | |
| 10 | import type { User } from "./identity"; | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 11 | import type { AskPage } from "./integrations"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 12 | import type { Result } from "./result"; |
| 13 | ||
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 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 | */ | |
| 19 | export 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 | */ | |
| 28 | export const G1T_PRINCIPAL: Principal = { kind: "system", id: "g1t" }; | |
| 29 | /** `system:g1t`, as chat keeps members and authors. */ | |
| 30 | export const G1T_KEY = "system:g1t"; | |
| 31 | ||
| 32 | /** Whether a member is g1t itself. */ | |
| 33 | export function isG1t(member: { kind: string; id?: string } | null | undefined): boolean { | |
| 34 | return member?.kind === "system" && member.id === "g1t"; | |
| 35 | } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 36 | |
| 37 | export function principalKey(principal: Principal): string { | |
| 38 | return `${principal.kind}:${principal.id}`; | |
| 39 | } | |
| 40 | ||
| 41 | export function parsePrincipalKey(key: string): Principal | null { | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 42 | // g1t is the only `system` member there is. |
| 43 | if (key === G1T_KEY) return { ...G1T_PRINCIPAL }; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 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. */ | |
| 53 | export type MemberProfile = Principal & { | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 54 | /** `username` for a person (lowercased: what `@` mentions), `handle` for an agent. */ |
| Chat and workspace agents: channels, DMs and named agents you talk to | 55 | name: string; |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 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 | */ | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 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; | |
| 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) | 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; | |
| Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent. | 75 | /** What an agent's face is drawn from when it chose none; null for a person. Always set by chat. */ |
| Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002) | 76 | avatar_seed?: string | null; |
| An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so. | 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; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 87 | }; |
| 88 | ||
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 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 | */ | |
| 93 | export 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 | */ | |
| 104 | export function memberName(member: { name: string; display_name?: string | null; display_username?: string | null }): string { | |
| 105 | return member.display_name?.trim() || memberHandle(member); | |
| 106 | } | |
| 107 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 108 | export type ChannelKind = "channel" | "dm"; |
| 109 | ||
| 110 | export 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 | ||
| 124 | export 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. */ | |
| 135 | export type MessageCard = { | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 136 | /** 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 to | 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; | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 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[]; | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 155 | /** The service whose card this was and that answered its actions: `agents`, on older cards only. */ |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 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 | ||
| 161 | export type CardField = { label: string; value: string }; | |
| 162 | ||
| 163 | /** A button on a card. */ | |
| 164 | export 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; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 175 | }; |
| 176 | ||
| 177 | export 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; | |
| 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) | 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[]; | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 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 | */ | |
| 219 | export type ChatAnswerState = "thinking" | "streaming" | "done" | "stopped" | "error"; | |
| 220 | ||
| 221 | export 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 | /** `max_tokens`, `length`: cut off at its limit. */ | |
| 227 | stop_reason?: string | null; | |
| 228 | /** What went wrong, for `error`. */ | |
| 229 | error?: string | null; | |
| 230 | /** | |
| 231 | * Steps the answer took on its way (tool calls and the like), as the | |
| 232 | * integrations service streamed them: each `ask` event other than meta, | |
| 233 | * text, done and error, kept as it came. At most 50. | |
| 234 | */ | |
| 235 | steps?: Record<string, unknown>[]; | |
| 236 | /** Set when Ask is not set up in the workspace: the answer says where to set it up. */ | |
| 237 | setup?: "missing" | null; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 238 | }; |
| 239 | ||
| 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) | 240 | /** |
| 241 | * One emoji's reactions on a message. `emoji` is a Unicode emoji or | |
| 242 | * `:name:` for one of the workspace's own (`CustomEmoji`). | |
| 243 | */ | |
| 244 | export type ChatReaction = { | |
| 245 | emoji: string; | |
| 246 | /** Everyone who reacted with it, people and agents alike. */ | |
| 247 | count: number; | |
| 248 | /** | |
| 249 | * Whether the viewer did. In live `message.*` events, which everyone in | |
| 250 | * the room gets, it is always false: keep your own from what you know. | |
| 251 | */ | |
| 252 | me: boolean; | |
| 253 | /** The first ten who reacted with it, for the hover list. */ | |
| 254 | by: MemberProfile[]; | |
| 255 | }; | |
| 256 | ||
| 257 | /** Who may add a workspace's emoji: every member (the default), or only its owners. */ | |
| 258 | export type EmojiUpload = "members" | "admins"; | |
| 259 | ||
| 260 | /** | |
| 261 | * One of a workspace's own emoji, used as `:name:`. Its image is served | |
| 262 | * from the usercontent origin at `/emoji/<file>` (never from the site). | |
| 263 | */ | |
| 264 | export type CustomEmoji = { | |
| 265 | name: string; | |
| 266 | /** For an alias, the emoji it is another name for; it shows that one's image. */ | |
| 267 | alias_of: string | null; | |
| 268 | /** The SHA-256 of the image's bytes. */ | |
| 269 | file: string; | |
| 270 | content_type: "image/png" | "image/gif" | "image/webp"; | |
| 271 | bytes: number; | |
| 272 | created_by: MemberProfile; | |
| 273 | created_at: string; | |
| 274 | }; | |
| 275 | ||
| 276 | export type EmojiList = { | |
| 277 | emoji: CustomEmoji[]; | |
| 278 | emoji_upload: EmojiUpload; | |
| 279 | /** Whether the viewer may add emoji and aliases. */ | |
| 280 | can_upload: boolean; | |
| 281 | /** Whether the viewer is an owner: may remove any emoji and change `emoji_upload`. */ | |
| 282 | can_manage: boolean; | |
| 283 | }; | |
| 284 | ||
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 285 | /** Who may do something in a workspace's chat: every member, or only its owners. */ |
| 286 | export type ChatAllowed = "members" | "owners"; | |
| 287 | ||
| 288 | /** | |
| 289 | * Who may rename and archive a channel: its owners (whoever made it) and | |
| 290 | * the workspace's owners (the default), or the workspace's owners only. | |
| 291 | */ | |
| 292 | export type ChannelManagers = "channel_owners" | "owners"; | |
| 293 | ||
| 294 | /** | |
| 295 | * A workspace's chat settings, which its owners choose (Workspace, | |
| 296 | * Settings, Chat). The chat service enforces each one; the page only | |
| 297 | * hides what the viewer may not do. | |
| 298 | */ | |
| 299 | export type ChatSettings = { | |
| 300 | /** Who may create public channels. */ | |
| 301 | public_channels: ChatAllowed; | |
| 302 | /** Who may create private channels. */ | |
| 303 | private_channels: ChatAllowed; | |
| 304 | /** Who may rename, archive and unarchive channels. */ | |
| 305 | manage_channels: ChannelManagers; | |
| 306 | /** Who may add custom emoji (`admins` is the workspace's owners). */ | |
| 307 | emoji_upload: EmojiUpload; | |
| 308 | /** | |
| 309 | * The public channels someone new is put in the first time they open | |
| 310 | * Chat, by id. #general unless the owners chose otherwise. | |
| 311 | */ | |
| 312 | default_channels: string[]; | |
| 313 | }; | |
| 314 | ||
| 315 | /** What the viewer may do in a workspace's chat, from its settings and their role. */ | |
| 316 | export type ChatPermissions = { | |
| 317 | create_public_channels: boolean; | |
| 318 | create_private_channels: boolean; | |
| 319 | add_emoji: boolean; | |
| 320 | /** Whether they may change the workspace's chat settings: owners only. */ | |
| 321 | manage_settings: boolean; | |
| 322 | }; | |
| 323 | ||
| 324 | /** The chat settings page: the settings, what the viewer may do, and the public channels to choose defaults from. */ | |
| 325 | export type ChatSettingsView = { | |
| 326 | settings: ChatSettings; | |
| 327 | can: ChatPermissions; | |
| 328 | channels: Channel[]; | |
| 329 | }; | |
| 330 | ||
| 331 | /** A change to a channel: its name, its topic, or whether it is archived. Anything left out stays. */ | |
| 332 | export type ChannelChange = { name?: string; topic?: string | null; archived?: boolean }; | |
| 333 | ||
| 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 | /** An image for a new emoji, as base64. Its type is read from its bytes, never taken from here. */ |
| 335 | export type EmojiFile = { data: string }; | |
| 336 | ||
| 337 | /** The largest custom emoji image, in bytes, and the widest or tallest, in pixels. */ | |
| 338 | export const MAX_EMOJI_BYTES = 256 * 1024; | |
| 339 | export const MAX_EMOJI_SIDE = 512; | |
| 340 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 341 | /** One row of the Chat sidebar. */ |
| 342 | export type ChatSidebarEntry = { | |
| 343 | channel: Channel; | |
| 344 | /** "g1t-core", or the other members' names for a direct message. */ | |
| 345 | title: string; | |
| 346 | /** For a direct message: who else is in it (up to four). */ | |
| 347 | others: MemberProfile[]; | |
| 348 | starred: boolean; | |
| 349 | muted: boolean; | |
| 350 | unread: number; | |
| 351 | mentions: number; | |
| 352 | }; | |
| 353 | ||
| 354 | export type ChatSidebar = { | |
| 355 | entries: ChatSidebarEntry[]; | |
| 356 | /** Public channels in the workspace the viewer has not joined. */ | |
| 357 | browsable: number; | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 358 | /** What the viewer may do, from the workspace's chat settings: the page hides what they may not. */ |
| 359 | can: ChatPermissions; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 360 | }; |
| 361 | ||
| 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. | 362 | /** |
| 363 | * `activity`: what was said over a span, in the conversations the viewer | |
| 364 | * can read (every public channel, and the private channels and direct | |
| 365 | * messages they are in). Messages are text messages and thread replies | |
| 366 | * that were not deleted; cards agents post are not messages. Home reads it | |
| 367 | * for what people and agents said since you were last there. | |
| 368 | */ | |
| 369 | export type ChatActivity = { | |
| 370 | /** RFC 3339: `[from, until)`. */ | |
| 371 | from: string; | |
| 372 | until: string; | |
| 373 | messages: number; | |
| 374 | /** Conversations with at least one message. */ | |
| 375 | channels: number; | |
| 376 | /** Who said how much: member keys (`user:<id>`, `agent:<id>`), most first. */ | |
| 377 | authors: { key: string; messages: number }[]; | |
| 378 | }; | |
| 379 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 380 | export type MessagePage = { |
| 381 | messages: ChatMessage[]; | |
| 382 | /** Pass as `before` to read further back; null at the beginning. */ | |
| 383 | older: string | null; | |
| 384 | /** | |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 385 | * For a thread's first page: the message the thread is under, however |
| 386 | * many replies it has, so a thread panel always shows it (a session's | |
| 387 | * live card, say) at its top. | |
| 388 | */ | |
| 389 | root?: ChatMessage | null; | |
| 390 | /** | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 391 | * Set when the page was read with `after`: pass it as `after` again for |
| 392 | * the next messages, or null when this page reached the newest. | |
| 393 | */ | |
| 394 | newer?: string | null; | |
| 395 | }; | |
| 396 | ||
| 397 | export type NewChannel = { name: string; topic?: string | null; private?: boolean }; | |
| 398 | ||
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 399 | export type PostMessage = { |
| 400 | body: string; | |
| 401 | thread_root?: string | null; | |
| 402 | /** In a direct message with @g1t: the page the person asks from, which g1t is told. Ignored elsewhere. */ | |
| 403 | page?: AskPage | null; | |
| 404 | }; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 405 | |
| 406 | /** | |
| 407 | * What the live socket sends. The site opens | |
| 408 | * `wss://<site>/<workspace>/chat/live?channel=<id>`; the site checks the | |
| 409 | * session and forwards the upgrade to the chat service with the viewer. | |
| 410 | */ | |
| 411 | export type ChatLiveEvent = | |
| 412 | | { type: "message.created"; message: ChatMessage } | |
| 413 | | { type: "message.updated"; message: ChatMessage } | |
| 414 | | { type: "message.deleted"; channel_id: string; id: string } | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 415 | /** |
| 416 | * An answer from @g1t as it is written: the whole text so far (not a | |
| 417 | * delta, so a late or dropped frame never garbles it) and where it is. | |
| 418 | * The stored message catches up with a `message.updated` at the end. | |
| 419 | */ | |
| 420 | | { type: "message.streaming"; channel_id: string; id: string; body: string; answer: ChatAnswer } | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 421 | | { 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) | 422 | | { type: "read"; channel_id: string; principal: Principal; last_read_id: string } |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 423 | | { 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) | 424 | | { 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 to | 425 | |
| 426 | /** | |
| 427 | * Header the site sets on a forwarded live socket: the viewer, as JSON. | |
| 428 | * The site forwards the upgrade to the chat service's | |
| 429 | * `GET /live?workspace=<slug>&channel=<id>` (`workspace` may be left out, | |
| 430 | * at the cost of looking up each of the viewer's workspaces). | |
| 431 | */ | |
| 432 | export const CHAT_VIEWER_HEADER = "x-g1t-chat-viewer"; | |
| 433 | ||
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 434 | /** A channel with its members, and whether the viewer may rename or archive it. */ |
| 435 | export type ChannelDetail = { channel: Channel; members: ChannelMember[]; can_manage: boolean }; | |
| 436 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 437 | export type ChatApi = { |
| 438 | 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. | 439 | /** What was said in `[from, until)` (RFC 3339) where the viewer can read, counted. */ |
| 440 | activity(workspace: string, viewer: User, span: { from: string; until: string }): Promise<Result<ChatActivity>>; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 441 | channel( |
| 442 | workspace: string, | |
| 443 | channelId: string, | |
| 444 | viewer: User, | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 445 | ): Promise<Result<ChannelDetail>>; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 446 | /** The same, found by its name in the workspace (`#general` or `general`), as the site's URLs name channels. */ |
| 447 | channelByName( | |
| 448 | workspace: string, | |
| 449 | name: string, | |
| 450 | viewer: User, | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 451 | ): Promise<Result<ChannelDetail>>; |
| 452 | /** | |
| 453 | * Browse channels: every public channel, and the private ones the viewer | |
| 454 | * is in. With `archived`, the archived ones instead. | |
| 455 | */ | |
| 456 | browse(workspace: string, viewer: User, options?: { archived?: boolean }): Promise<Result<Channel[]>>; | |
| 457 | /** 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 to | 458 | createChannel(workspace: string, viewer: User, input: NewChannel): Promise<Result<Channel>>; |
| 459 | /** | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 460 | * Renames, archives or unarchives a channel (the workspace's |
| 461 | * `manage_channels` setting says who may), or changes its topic (any | |
| 462 | * member). #general is never renamed or archived. Everyone looking at it | |
| 463 | * gets `channel.updated`. | |
| 464 | */ | |
| 465 | updateChannel(workspace: string, channelId: string, viewer: User, change: ChannelChange): Promise<Result<Channel>>; | |
| 466 | /** The workspace's chat settings, for any member; only owners may change them. */ | |
| 467 | chatSettings(workspace: string, viewer: User): Promise<Result<ChatSettingsView>>; | |
| 468 | /** Owners only: changes some of the workspace's chat settings, and answers all of them. */ | |
| 469 | setChatSettings(workspace: string, viewer: User, change: Partial<ChatSettings>): Promise<Result<ChatSettings>>; | |
| 470 | /** | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 471 | * The direct message between the viewer and these members, created on |
| 472 | * first use. The same set of members always gets the same channel. | |
| 473 | */ | |
| 474 | openDm(workspace: string, viewer: User, members: Principal[]): Promise<Result<Channel>>; | |
| 475 | join(workspace: string, channelId: string, viewer: User): Promise<Result<null>>; | |
| 476 | leave(workspace: string, channelId: string, viewer: User): Promise<Result<null>>; | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 477 | /** Adds a person. An invite grants read, never write. */ |
| Chat and workspace agents: channels, DMs and named agents you talk to | 478 | invite(workspace: string, channelId: string, viewer: User, member: Principal): Promise<Result<null>>; |
| 479 | /** | |
| 480 | * Newest first. Without `thread_root`, the channel's top-level messages; | |
| 481 | * with it, that thread's replies, plus the message they reply to as the | |
| 482 | * oldest once the page reaches the start of the thread (`older` null). | |
| 483 | * With `after` (catching up after a reconnect): the messages after that | |
| 484 | * id instead, oldest first, deleted ones included so the client can | |
| 485 | * drop them; `newer` says whether there are more. | |
| 486 | * Limit 50 by default, 200 at most. | |
| 487 | */ | |
| 488 | messages( | |
| 489 | workspace: string, | |
| 490 | channelId: string, | |
| 491 | viewer: User, | |
| 492 | page?: { before?: string | null; after?: string | null; limit?: number; thread_root?: string | null }, | |
| 493 | ): Promise<Result<MessagePage>>; | |
| 494 | post(workspace: string, channelId: string, viewer: User, message: PostMessage): Promise<Result<ChatMessage>>; | |
| 495 | edit(workspace: string, channelId: string, viewer: User, id: string, body: string): Promise<Result<ChatMessage>>; | |
| 496 | remove(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>; | |
| 497 | markRead(workspace: string, channelId: string, viewer: User, id: string): Promise<Result<null>>; | |
| 498 | setPreferences( | |
| 499 | workspace: string, | |
| 500 | channelId: string, | |
| 501 | viewer: User, | |
| 502 | prefs: { starred?: boolean; muted?: boolean }, | |
| 503 | ): Promise<Result<null>>; | |
| 504 | /** | |
| 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) | 505 | * Reacts to a message with `emoji` (one Unicode emoji, or `:name:` for |
| 506 | * one of the workspace's own). Once per member per emoji; at most 50 | |
| 507 | * different emoji on a message. Answers the message's reactions now. | |
| 508 | */ | |
| 509 | react(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>; | |
| 510 | unreact(workspace: string, channelId: string, viewer: User, messageId: string, emoji: string): Promise<Result<ChatReaction[]>>; | |
| 511 | /** The workspace's own emoji, by name, and who may add them. */ | |
| 512 | listEmoji(workspace: string, viewer: User): Promise<Result<EmojiList>>; | |
| 513 | /** Adds an emoji: a PNG, GIF or WebP of at most 256 KB and 512×512. */ | |
| 514 | addEmoji(workspace: string, viewer: User, name: string, file: EmojiFile): Promise<Result<CustomEmoji>>; | |
| 515 | /** Gives an existing emoji (`target`) another name. */ | |
| 516 | aliasEmoji(workspace: string, viewer: User, name: string, target: string): Promise<Result<CustomEmoji>>; | |
| 517 | /** Removes an emoji, and its aliases with it. Its creator or an owner may. */ | |
| 518 | removeEmoji(workspace: string, viewer: User, name: string): Promise<Result<null>>; | |
| 519 | /** Owners only: who may add emoji. */ | |
| 520 | setEmojiUpload(workspace: string, viewer: User, value: EmojiUpload): Promise<Result<EmojiUpload>>; | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 521 | /** The viewer's direct message with @g1t, created on first use. */ |
| 522 | g1tDm(workspace: string, viewer: User): Promise<Result<Channel>>; | |
| 523 | /** | |
| 524 | * Starts a new conversation in the viewer's direct message with @g1t: a | |
| 525 | * mark after which answers read only what comes next. Stops an answer | |
| 526 | * still coming. | |
| 527 | */ | |
| 528 | g1tReset(workspace: string, channelId: string, viewer: User): Promise<Result<ChatMessage>>; | |
| 529 | /** Has @g1t answer the latest question again, in place of its last answer. */ | |
| 530 | g1tRetry(workspace: string, channelId: string, viewer: User): Promise<Result<null>>; | |
| 531 | /** Stops @g1t's answer still coming; what came stays. */ | |
| 532 | g1tStop(workspace: string, channelId: string, viewer: User): Promise<Result<null>>; | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 533 | }; |
| 534 | ||
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 535 | /** |
| 536 | * For g1t's own services only, never the site on a person's behalf: posts | |
| 537 | * and edits messages as @g1t in a direct message with it. The chat service | |
| 538 | * answers these at `POST /internal/<method>`, which no route from the site | |
| 539 | * reaches, and refuses anything that came in from the internet. | |
| 540 | */ | |
| 541 | export type ChatInternalApi = { | |
| 542 | /** Posts `body` as @g1t in a direct message with it, in the workspace with this slug. */ | |
| 543 | g1tPost(workspace: string, channelId: string, body: string): Promise<Result<ChatMessage>>; | |
| 544 | /** Changes the text of one of @g1t's messages there. */ | |
| 545 | g1tEdit(workspace: string, channelId: string, id: string, body: string): Promise<Result<ChatMessage>>; | |
| 546 | }; | |
| 547 | ||
| 548 | export function chatInternalClient(service: ServiceBinding): ChatInternalApi { | |
| 549 | const call = async <T>(method: string, args: object): Promise<T> => { | |
| 550 | const response = await service.fetch(`https://service/internal/${method}`, { | |
| 551 | method: "POST", | |
| 552 | headers: { "content-type": "application/json" }, | |
| 553 | body: JSON.stringify(args), | |
| 554 | }); | |
| 555 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 556 | return (await response.json()) as T; | |
| 557 | }; | |
| 558 | return { | |
| 559 | g1tPost: (workspace, channelId, body) => call("g1t_post", { workspace, channel_id: channelId, body }), | |
| 560 | g1tEdit: (workspace, channelId, id, body) => call("g1t_edit", { workspace, channel_id: channelId, id, body }), | |
| 561 | }; | |
| 562 | } | |
| 563 | ||
| Chat and workspace agents: channels, DMs and named agents you talk to | 564 | async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> { |
| 565 | const response = await service.fetch(`https://service/rpc/${method}`, { | |
| 566 | method: "POST", | |
| 567 | headers: { "content-type": "application/json" }, | |
| 568 | body: JSON.stringify(args), | |
| 569 | }); | |
| 570 | if (!response.ok) { | |
| 571 | throw new Error(`${method} failed with status ${response.status}`); | |
| 572 | } | |
| 573 | return (await response.json()) as T; | |
| 574 | } | |
| 575 | ||
| 576 | export function chatClient(service: ServiceBinding): ChatApi { | |
| 577 | const call = <T>(method: string, args: object) => rpc<T>(service, method, args); | |
| 578 | return { | |
| 579 | 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. | 580 | 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 to | 581 | channel: (workspace, channelId, viewer) => call("channel", { workspace, channel_id: channelId, viewer }), |
| 582 | channelByName: (workspace, name, viewer) => call("channel_by_name", { workspace, name, viewer }), | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 583 | browse: (workspace, viewer, options) => call("browse", { workspace, viewer, archived: options?.archived ?? false }), |
| Chat and workspace agents: channels, DMs and named agents you talk to | 584 | createChannel: (workspace, viewer, input) => call("create_channel", { workspace, viewer, input }), |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 585 | updateChannel: (workspace, channelId, viewer, change) => call("update_channel", { workspace, channel_id: channelId, viewer, change }), |
| 586 | chatSettings: (workspace, viewer) => call("chat_settings", { workspace, viewer }), | |
| 587 | setChatSettings: (workspace, viewer, change) => call("set_chat_settings", { workspace, viewer, change }), | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 588 | openDm: (workspace, viewer, members) => call("open_dm", { workspace, viewer, members }), |
| 589 | join: (workspace, channelId, viewer) => call("join", { workspace, channel_id: channelId, viewer }), | |
| 590 | leave: (workspace, channelId, viewer) => call("leave", { workspace, channel_id: channelId, viewer }), | |
| 591 | invite: (workspace, channelId, viewer, member) => | |
| 592 | call("invite", { workspace, channel_id: channelId, viewer, member }), | |
| 593 | messages: (workspace, channelId, viewer, page) => | |
| 594 | call("messages", { | |
| 595 | workspace, | |
| 596 | channel_id: channelId, | |
| 597 | viewer, | |
| 598 | before: page?.before ?? null, | |
| 599 | after: page?.after ?? null, | |
| 600 | limit: page?.limit ?? null, | |
| 601 | thread_root: page?.thread_root ?? null, | |
| 602 | }), | |
| 603 | post: (workspace, channelId, viewer, message) => call("post", { workspace, channel_id: channelId, viewer, message }), | |
| 604 | edit: (workspace, channelId, viewer, id, body) => call("edit", { workspace, channel_id: channelId, viewer, id, body }), | |
| 605 | remove: (workspace, channelId, viewer, id) => call("remove", { workspace, channel_id: channelId, viewer, id }), | |
| 606 | markRead: (workspace, channelId, viewer, id) => call("mark_read", { workspace, channel_id: channelId, viewer, id }), | |
| 607 | setPreferences: (workspace, channelId, viewer, prefs) => | |
| 608 | call("set_preferences", { workspace, channel_id: channelId, viewer, prefs }), | |
| 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) | 609 | react: (workspace, channelId, viewer, messageId, emoji) => |
| 610 | call("react", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }), | |
| 611 | unreact: (workspace, channelId, viewer, messageId, emoji) => | |
| 612 | call("unreact", { workspace, channel_id: channelId, viewer, message_id: messageId, emoji }), | |
| 613 | listEmoji: (workspace, viewer) => call("list_emoji", { workspace, viewer }), | |
| 614 | addEmoji: (workspace, viewer, name, file) => call("add_emoji", { workspace, viewer, name, file }), | |
| 615 | aliasEmoji: (workspace, viewer, name, target) => call("alias_emoji", { workspace, viewer, name, target }), | |
| 616 | removeEmoji: (workspace, viewer, name) => call("remove_emoji", { workspace, viewer, name }), | |
| 617 | setEmojiUpload: (workspace, viewer, value) => call("set_emoji_upload", { workspace, viewer, value }), | |
| g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other. | 618 | g1tDm: (workspace, viewer) => call("g1t_dm", { workspace, viewer }), |
| 619 | g1tReset: (workspace, channelId, viewer) => call("g1t_reset", { workspace, channel_id: channelId, viewer }), | |
| 620 | g1tRetry: (workspace, channelId, viewer) => call("g1t_retry", { workspace, channel_id: channelId, viewer }), | |
| 621 | g1tStop: (workspace, channelId, viewer) => call("g1t_stop", { workspace, channel_id: channelId, viewer }), | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 622 | }; |
| 623 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.