chat 4.27.0 → 4.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,6 +52,8 @@ type TextStyle = "plain" | "bold" | "muted";
52
52
  interface ButtonElement {
53
53
  /** Whether this button triggers a regular action or opens a modal dialog. Default: "action" */
54
54
  actionType?: "action" | "modal";
55
+ /** URL to POST action data to when this button is clicked */
56
+ callbackUrl?: string;
55
57
  /** If true, the button is displayed in an inactive state and doesn't respond to user actions */
56
58
  disabled?: boolean;
57
59
  /** Unique action ID for callback routing */
@@ -241,6 +243,8 @@ declare function Actions(children: (ButtonElement | LinkButtonElement | SelectEl
241
243
  interface ButtonOptions {
242
244
  /** Whether this button triggers a regular action or opens a modal dialog. Default: "action" */
243
245
  actionType?: "action" | "modal";
246
+ /** URL to POST action data to when this button is clicked */
247
+ callbackUrl?: string;
244
248
  /** If true, the button is displayed in an inactive state and doesn't respond to user actions */
245
249
  disabled?: boolean;
246
250
  /** Unique action ID for callback routing */
@@ -374,6 +378,8 @@ declare function cardChildToFallbackText(child: CardChild): string | null;
374
378
  type ModalChild = TextInputElement | SelectElement | ExternalSelectElement | RadioSelectElement | TextElement | FieldsElement;
375
379
  interface ModalElement {
376
380
  callbackId: string;
381
+ /** URL to POST form values to when this modal is submitted */
382
+ callbackUrl?: string;
377
383
  children: ModalChild[];
378
384
  closeLabel?: string;
379
385
  notifyOnClose?: boolean;
@@ -427,6 +433,8 @@ interface RadioSelectElement {
427
433
  declare function isModalElement(value: unknown): value is ModalElement;
428
434
  interface ModalOptions {
429
435
  callbackId: string;
436
+ /** URL to POST form values to when this modal is submitted */
437
+ callbackUrl?: string;
430
438
  children?: ModalChild[];
431
439
  closeLabel?: string;
432
440
  notifyOnClose?: boolean;
@@ -527,6 +535,7 @@ interface TextProps {
527
535
  /** Props for Button component in JSX */
528
536
  interface ButtonProps {
529
537
  actionType?: "action" | "modal";
538
+ callbackUrl?: string;
530
539
  children?: string | number | (string | number | undefined)[];
531
540
  disabled?: boolean;
532
541
  id: string;
@@ -566,6 +575,7 @@ type DividerProps = Record<string, never>;
566
575
  /** Props for Modal component in JSX */
567
576
  interface ModalProps {
568
577
  callbackId: string;
578
+ callbackUrl?: string;
569
579
  children?: unknown;
570
580
  closeLabel?: string;
571
581
  notifyOnClose?: boolean;
@@ -1 +1 @@
1
- export { A as ActionsComponent, B as ButtonComponent, X as ButtonProps, c as CardComponent, Y as CardJSXElement, Z as CardJSXProps, e as CardLinkComponent, _ as CardLinkProps, $ as CardProps, C as ChatElement, a0 as ContainerProps, D as DividerComponent, a1 as DividerProps, E as ExternalSelectComponent, a2 as ExternalSelectProps, F as FieldComponent, a3 as FieldProps, f as FieldsComponent, ar as Fragment, I as ImageComponent, a4 as ImageProps, as as JSX, L as LinkButtonComponent, a5 as LinkButtonProps, o as ModalComponent, a6 as ModalProps, R as RadioSelectComponent, j as SectionComponent, p as SelectComponent, q as SelectOptionComponent, a7 as SelectOptionProps, a8 as SelectProps, am as TableComponent, al as TableProps, T as TextComponent, r as TextInputComponent, a9 as TextInputProps, aa as TextProps, an as isCardLinkProps, h as isJSX, ao as jsx, aq as jsxDEV, ap as jsxs, t as toCardElement, l as toModalElement } from './jsx-runtime-Co9uV6l7.js';
1
+ export { A as ActionsComponent, B as ButtonComponent, X as ButtonProps, c as CardComponent, Y as CardJSXElement, Z as CardJSXProps, e as CardLinkComponent, _ as CardLinkProps, $ as CardProps, C as ChatElement, a0 as ContainerProps, D as DividerComponent, a1 as DividerProps, E as ExternalSelectComponent, a2 as ExternalSelectProps, F as FieldComponent, a3 as FieldProps, f as FieldsComponent, ar as Fragment, I as ImageComponent, a4 as ImageProps, as as JSX, L as LinkButtonComponent, a5 as LinkButtonProps, o as ModalComponent, a6 as ModalProps, R as RadioSelectComponent, j as SectionComponent, p as SelectComponent, q as SelectOptionComponent, a7 as SelectOptionProps, a8 as SelectProps, am as TableComponent, al as TableProps, T as TextComponent, r as TextInputComponent, a9 as TextInputProps, aa as TextProps, an as isCardLinkProps, h as isJSX, ao as jsx, aq as jsxDEV, ap as jsxs, t as toCardElement, l as toModalElement } from './jsx-runtime-DxGwoLu2.js';
@@ -7,7 +7,7 @@ import {
7
7
  jsxs,
8
8
  toCardElement,
9
9
  toModalElement
10
- } from "./chunk-AN7MRAVW.js";
10
+ } from "./chunk-V25FKIIL.js";
11
11
  export {
12
12
  Fragment,
13
13
  isCardLinkProps,
package/docs/actions.mdx CHANGED
@@ -94,5 +94,56 @@ bot.onAction("feedback", async (event) => {
94
94
  ```
95
95
 
96
96
  <Callout type="info">
97
- Modals are currently supported on Slack. Other platforms will receive a no-op or fallback behavior.
97
+ Modals are currently supported on Slack and Teams. Other platforms will receive a no-op
98
+ or fallback behavior.
98
99
  </Callout>
100
+
101
+ ## Callback URLs
102
+
103
+ Buttons accept a `callbackUrl` prop. When clicked, the action data is POSTed to that URL in addition to firing any `onAction` handler. This pairs naturally with webhook-based workflow engines to build approval flows without any `onAction` handler at all:
104
+
105
+ ```tsx title="lib/bot.tsx" lineNumbers
106
+ bot.onNewMention(async (thread) => {
107
+ const approveUrl = "https://example.com/webhook/approve";
108
+ const denyUrl = "https://example.com/webhook/deny";
109
+
110
+ await thread.post(
111
+ <Card title="Deploy v2.4.1?">
112
+ <Actions>
113
+ <Button callbackUrl={approveUrl} id="approve" style="primary">
114
+ Approve
115
+ </Button>
116
+ <Button callbackUrl={denyUrl} id="deny" style="danger">
117
+ Deny
118
+ </Button>
119
+ </Actions>
120
+ </Card>
121
+ );
122
+ });
123
+ ```
124
+
125
+ ### Callback payload
126
+
127
+ The POST body sent to the `callbackUrl`:
128
+
129
+ ```json
130
+ {
131
+ "type": "action",
132
+ "actionId": "approve",
133
+ "user": { "id": "U123", "name": "alice" },
134
+ "threadId": "slack:C123:1234567890.123",
135
+ "messageId": "1234567890.456"
136
+ }
137
+ ```
138
+
139
+ If the button also has a `value` prop, it is included in the payload as `"value"`.
140
+
141
+ <Callout type="info">
142
+ Platform limits apply to encoded button data. Discord's `custom_id` has a 100
143
+ character limit - if the action ID plus callback token exceed this, posting
144
+ the card throws a `ValidationError`. Telegram's `callback_data` has a 64 byte
145
+ limit - buttons that exceed this will throw a `ValidationError`. Keep action
146
+ IDs short when using `callbackUrl` on these platforms.
147
+ </Callout>
148
+
149
+ For modals, see [callbackUrl on modals](/docs/modals#callback-urls).
package/docs/adapters.mdx CHANGED
@@ -8,57 +8,63 @@ prerequisites:
8
8
 
9
9
  Adapters handle webhook verification, message parsing, and API calls for each platform. Install only the adapters you need. Browse all available adapters — including community-built ones — on the [Adapters](/adapters) page.
10
10
 
11
+ Need a browser chat UI? See the [Web adapter](/adapters/web) — it speaks the AI SDK `useChat` protocol so the same bot serves Slack, Teams, **and** a `<Conversation>` from `ai-elements` out of the box.
12
+
11
13
  Ready to build your own? Follow the [building](/docs/contributing/building) guide.
12
14
 
13
15
  ## Feature matrix
14
16
 
15
17
  ### Messaging
16
18
 
17
- | Feature | [Slack](/adapters/slack) | [Teams](/adapters/teams) | [Google Chat](/adapters/google-chat) | [Discord](/adapters/discord) | [Telegram](/adapters/telegram) | [GitHub](/adapters/github) | [Linear](/adapters/linear) | [WhatsApp](/adapters/whatsapp) |
18
- |---------|-------|-------|-------------|---------|---------|--------|--------|-----------|
19
- | Post message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
20
- | Edit message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
21
- | Delete message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
22
- | File uploads | <Check /> | <Check /> | <Cross /> | <Check /> | <Warn /> Single file | <Cross /> | <Cross /> | <Check /> Images, audio, docs |
23
- | Streaming | <Check /> Native | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Cross /> | <Cross /> | <Cross /> |
24
- | Scheduled messages | <Check /> Native | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
19
+ | Feature | [Slack](/adapters/slack) | [Teams](/adapters/teams) | [Google Chat](/adapters/google-chat) | [Discord](/adapters/discord) | [Telegram](/adapters/telegram) | [GitHub](/adapters/github) | [Linear](/adapters/linear) | [WhatsApp](/adapters/whatsapp) | [Messenger](/adapters/messenger) |
20
+ |---------|-------|-------|-------------|---------|---------|--------|--------|-----------|-----------|
21
+ | Post message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
22
+ | Edit message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Partial | <Cross /> | <Cross /> |
23
+ | Delete message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Partial | <Cross /> | <Cross /> |
24
+ | File uploads | <Check /> | <Check /> | <Cross /> | <Check /> | <Warn /> Single file | <Cross /> | <Cross /> | <Check /> Images, audio, docs | <Cross /> |
25
+ | Streaming | <Check /> Native | <Warn /> Native (DMs) / Buffered | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Post+Edit | <Warn /> Buffered | <Warn /> Agent sessions / Post+Edit | <Warn /> Buffered | <Warn /> Buffered |
26
+ | Scheduled messages | <Check /> Native | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
25
27
 
26
28
  ### Rich content
27
29
 
28
- | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
29
- |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
30
- | Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | WhatsApp templates |
31
- | Buttons | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Inline keyboard callbacks | <Cross /> | <Cross /> | <Check /> Interactive replies |
32
- | Link buttons | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Inline keyboard URLs | <Cross /> | <Cross /> | <Cross /> |
33
- | Select menus | <Check /> | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
34
- | Tables | <Check /> Block Kit | <Check /> GFM | <Warn /> ASCII | <Check /> GFM | <Warn /> ASCII | <Check /> GFM | <Check /> GFM | <Cross /> |
35
- | Fields | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Template variables |
36
- | Images in cards | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Check /> | <Cross /> | <Check /> |
37
- | Modals | <Check /> | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
30
+ | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp | Messenger |
31
+ |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|-----------|
32
+ | Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | WhatsApp templates | Generic/Button Templates |
33
+ | Buttons | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Inline keyboard callbacks | <Cross /> | <Cross /> | <Check /> Interactive replies | <Warn /> Max 3, postback |
34
+ | Link buttons | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Inline keyboard URLs | <Cross /> | <Cross /> | <Cross /> | <Check /> |
35
+ | Select menus | <Check /> | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
36
+ | Tables | <Check /> Block Kit | <Check /> GFM | <Warn /> ASCII | <Check /> GFM | <Warn /> ASCII | <Check /> GFM | <Check /> GFM | <Cross /> | <Warn /> ASCII |
37
+ | Fields | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Template variables | <Warn /> ASCII |
38
+ | Images in cards | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Check /> | <Cross /> | <Check /> | <Check /> |
39
+ | Modals | <Check /> | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
38
40
 
39
41
  ### Conversations
40
42
 
41
- | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
42
- |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
43
- | Slash commands | <Check /> | <Cross /> | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
44
- | Mentions | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
45
- | Add reactions | <Check /> | <Cross /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
46
- | Remove reactions | <Check /> | <Cross /> | <Check /> | <Check /> | <Check /> | <Warn /> | <Warn /> | <Cross /> |
47
- | Typing indicator | <Cross /> | <Check /> | <Cross /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Cross /> |
48
- | DMs | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> |
49
- | Ephemeral messages | <Check /> Native | <Cross /> | <Check /> Native | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
43
+ | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp | Messenger |
44
+ |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|-----------|
45
+ | Slash commands | <Check /> | <Cross /> | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
46
+ | Mentions | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Check /> |
47
+ | Add reactions | <Check /> | <Cross /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
48
+ | Remove reactions | <Check /> | <Cross /> | <Check /> | <Check /> | <Check /> | <Warn /> | <Warn /> | <Check /> | <Cross /> |
49
+ | Typing indicator | <Check /> | <Check /> | <Cross /> | <Check /> | <Check /> | <Cross /> | <Warn /> Agent sessions | <Cross /> | <Check /> |
50
+ | DMs | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> | <Check /> |
51
+ | Ephemeral messages | <Check /> Native | <Cross /> | <Check /> Native | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> |
52
+ | User lookup ([`getUser`](/docs/api/chat#getuser)) | <Check /> | <Warn /> Cached | <Warn /> Cached | <Check /> | <Warn /> Seen users | <Check /> | <Check /> | <Cross /> | <Cross /> |
53
+ | Parent subject ([`message.subject`](/docs/subject)) | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Check /> | <Check /> | <Cross /> | <Cross /> |
54
+ | Native client ([`.client`](/docs/api/chat#getadapter)) | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Cross /> | <Check /> | <Check /> | <Cross /> | <Cross /> |
55
+ | Custom API endpoint (`apiUrl`) | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
50
56
 
51
57
  ### Message history
52
58
 
53
- | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
54
- |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
55
- | Fetch messages | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Cached | <Check /> | <Check /> | <Warn /> Cached sent messages only |
56
- | Fetch single message | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Warn /> Cached | <Cross /> | <Cross /> | <Warn /> Cached sent messages only |
57
- | Fetch thread info | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
58
- | Fetch channel messages | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Cached | <Check /> | <Cross /> | <Warn /> Cached sent messages only |
59
- | List threads | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Check /> | <Cross /> | <Cross /> |
60
- | Fetch channel info | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> |
61
- | Post channel message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> |
59
+ | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp | Messenger |
60
+ |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|-----------|
61
+ | Fetch messages | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Cached | <Check /> | <Check /> | <Warn /> Cached sent messages only | <Warn /> Cached sent messages only |
62
+ | Fetch single message | <Check /> | <Cross /> | <Cross /> | <Cross /> | <Warn /> Cached | <Cross /> | <Cross /> | <Cross /> | <Warn /> Cached |
63
+ | Fetch thread info | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
64
+ | Fetch channel messages | <Check /> | <Check /> | <Check /> | <Check /> | <Warn /> Cached | <Check /> | <Cross /> | <Cross /> | <Warn /> Cached |
65
+ | List threads | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Check /> | <Cross /> | <Cross /> | <Cross /> |
66
+ | Fetch channel info | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> |
67
+ | Post channel message | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> | <Cross /> | <Check /> | <Check /> |
62
68
 
63
69
  <Callout type="info">
64
70
  <Warn /> indicates partial support — the feature works with limitations. See individual adapter pages for details.
@@ -100,6 +100,10 @@ Button({ id: "delete", label: "Delete", style: "danger", value: "item-123" })
100
100
  type: '"action" | "modal"',
101
101
  default: '"action"',
102
102
  },
103
+ callbackUrl: {
104
+ description: 'URL to POST action data to when this button is clicked.',
105
+ type: 'string',
106
+ },
103
107
  }}
104
108
  />
105
109
 
package/docs/api/chat.mdx CHANGED
@@ -168,7 +168,9 @@ Fires when a user clicks a button or selects an option in a card.
168
168
  ```typescript
169
169
  // Single action
170
170
  bot.onAction("approve", async (event) => {
171
- await event.thread.post("Approved!");
171
+ if (event.thread) {
172
+ await event.thread.post("Approved!");
173
+ }
172
174
  });
173
175
 
174
176
  // Multiple actions
@@ -193,8 +195,8 @@ bot.onAction(async (event) => { /* ... */ });
193
195
  type: 'Author',
194
196
  },
195
197
  'event.thread': {
196
- description: 'The thread containing the card.',
197
- type: 'Thread',
198
+ description: 'The thread containing the card, or null for view-based actions.',
199
+ type: 'Thread | null',
198
200
  },
199
201
  'event.triggerId': {
200
202
  description: 'Trigger ID for opening modals (platform-specific, may expire quickly).',
@@ -261,9 +263,47 @@ Returns `ModalResponse | undefined` to control the modal after submission:
261
263
  - `{ action: "update", modal: ModalElement }` — replace the modal content
262
264
  - `{ action: "push", modal: ModalElement }` — push a new modal view onto the stack
263
265
 
266
+ ### onOptionsLoad
267
+
268
+ Fires when an `ExternalSelect` requests options dynamically. The handler is keyed on the select's `id` and must return options synchronously enough for Slack's 3-second budget (the adapter caps the loader at ~2.5s and substitutes an empty result on timeout). Slack-only.
269
+
270
+ ```typescript
271
+ bot.onOptionsLoad("assignee", async (event) => {
272
+ const people = await peopleService.search(event.query);
273
+ return people.map((p) => ({ label: p.fullName, value: p.id }));
274
+ });
275
+ ```
276
+
277
+ Return an array of `OptionsLoadGroup` (`{ label, options }[]`) instead of a flat array to render grouped headers (e.g. "Recent" / "All"). Slack limits: max 100 groups, max 100 options per group.
278
+
279
+ <TypeTable
280
+ type={{
281
+ 'event.actionId': {
282
+ description: 'The id of the select requesting options (matches the id passed to bot.onOptionsLoad).',
283
+ type: 'string',
284
+ },
285
+ 'event.query': {
286
+ description: 'The text the user has typed so far.',
287
+ type: 'string',
288
+ },
289
+ 'event.user': {
290
+ description: 'The user requesting options.',
291
+ type: 'Author',
292
+ },
293
+ 'event.adapter': {
294
+ description: 'The adapter that received this event.',
295
+ type: 'Adapter',
296
+ },
297
+ 'event.raw': {
298
+ description: 'Raw platform-specific payload.',
299
+ type: 'unknown',
300
+ },
301
+ }}
302
+ />
303
+
264
304
  ### onSlashCommand
265
305
 
266
- Fires when a user invokes a `/command` in the message composer. Currently supported on Slack.
306
+ Fires when a user invokes a `/command` in the message composer. Currently supported on Slack and Discord.
267
307
 
268
308
  ```typescript
269
309
  // Specific command
@@ -426,12 +466,42 @@ bot.webhooks.teams(request, { waitUntil });
426
466
 
427
467
  ### getAdapter
428
468
 
429
- Get an adapter instance by name.
469
+ Get a typed adapter instance by name.
430
470
 
431
471
  ```typescript
432
472
  const slack = bot.getAdapter("slack");
433
473
  ```
434
474
 
475
+ #### Direct client access
476
+
477
+ Use `.client` to access the platform's typed native API client directly — available on Linear and GitHub:
478
+
479
+ ```typescript
480
+ // Linear - full LinearClient from @linear/sdk
481
+ const linear = bot.getAdapter("linear").client;
482
+ const issue = await linear.issue("ENG-123");
483
+ const project = await issue.project;
484
+
485
+ // GitHub - full Octokit from @octokit/rest
486
+ const github = bot.getAdapter("github").client;
487
+ const { data: pulls } = await github.rest.pulls.list({
488
+ owner: "vercel",
489
+ repo: "chat",
490
+ state: "open",
491
+ });
492
+ ```
493
+
494
+ The client uses the credentials from your adapter config. For multi-tenant adapters (Linear, GitHub), it returns the client for the current webhook request context.
495
+
496
+ <Callout type="warn">
497
+ For multi-tenant adapters (GitHub App without a fixed installation ID, Linear with per-org OAuth), `client` requires webhook handler context to resolve credentials. Calling it outside a handler throws. Single-tenant adapters (PAT, API key) work anywhere.
498
+ </Callout>
499
+
500
+ | Adapter | `client` type |
501
+ |---------|---------------|
502
+ | Linear | `LinearClient` from `@linear/sdk` |
503
+ | GitHub | `Octokit` from `@octokit/rest` |
504
+
435
505
  ### openDM
436
506
 
437
507
  Open a direct message thread with a user.
@@ -498,10 +568,16 @@ const user = await bot.getUser(message.author);
498
568
  - **GitHub** — `email` is `null` unless the user made it public, or you authenticated with the `user:email` scope.
499
569
  - **Linear** — full profile (incl. email + avatar) for any active workspace member.
500
570
 
501
- Fields that aren't available return `undefined`. Numeric user IDs (Discord/Telegram/GitHub) can be ambiguous when multiple of those adapters are registered — call the platform's adapter directly (`adapter.getUser(userId)`) in that case.
571
+ Fields that aren't available return `undefined`. Numeric user IDs (Discord/Telegram/GitHub) can be ambiguous when multiple of those adapters are registered — `bot.getUser` throws a `ChatError` with code `AMBIGUOUS_USER_ID` in that case. Pass an `Author` from a message handler (which already carries the adapter), or call the adapter directly (`adapter.getUser(userId)`).
502
572
  </Callout>
503
573
 
504
- Adapters that don't support user lookups will throw a `ChatError` with code `NOT_SUPPORTED`. Handle both cases if your bot runs on multiple platforms:
574
+ `bot.getUser` throws a `ChatError` in three cases. Handle them if your bot runs on multiple platforms:
575
+
576
+ | Code | When |
577
+ |------|------|
578
+ | `NOT_SUPPORTED` | The resolved adapter doesn't implement `getUser` (e.g. WhatsApp) |
579
+ | `AMBIGUOUS_USER_ID` | A numeric user ID could belong to more than one registered adapter (Discord/Telegram/GitHub) |
580
+ | `UNKNOWN_USER_ID_FORMAT` | The `userId` string doesn't match any registered platform's ID format |
505
581
 
506
582
  ```typescript
507
583
  import { ChatError } from "chat";
@@ -512,8 +588,14 @@ try {
512
588
  // User not found on this platform
513
589
  }
514
590
  } catch (error) {
515
- if (error instanceof ChatError && error.code === "NOT_SUPPORTED") {
516
- // This adapter doesn't support user lookups
591
+ if (error instanceof ChatError) {
592
+ if (error.code === "NOT_SUPPORTED") {
593
+ // This adapter doesn't support user lookups
594
+ } else if (error.code === "AMBIGUOUS_USER_ID") {
595
+ // Pass message.author or call adapter.getUser(userId) directly
596
+ } else if (error.code === "UNKNOWN_USER_ID_FORMAT") {
597
+ // userId doesn't match any known platform format
598
+ }
517
599
  }
518
600
  }
519
601
  ```
@@ -31,6 +31,8 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat";
31
31
  | Export | Description |
32
32
  |--------|-------------|
33
33
  | [`PostableMessage`](/docs/api/postable-message) | Union type accepted by `thread.post()` |
34
+ | [`Plan`](/docs/api/postable-message#plan) | Step-by-step task list that mutates after posting |
35
+ | [`StreamingPlan`](/docs/api/postable-message#streamingplan) | Wraps an async iterable with platform-specific streaming options |
34
36
  | [`Cards`](/docs/api/cards) | Rich card components — `Card`, `Text`, `Button`, `Actions`, etc. |
35
37
  | [`Markdown`](/docs/api/markdown) | AST builder functions — `root`, `paragraph`, `text`, `strong`, etc. |
36
38
  | [`Modals`](/docs/api/modals) | Modal form components — `Modal`, `TextInput`, `Select`, etc. |
@@ -15,6 +15,25 @@ import {
15
15
  } from "chat";
16
16
  ```
17
17
 
18
+ ## Type re-exports
19
+
20
+ The chat package re-exports mdast's union and content types so adapters and downstream code can build exhaustively-typed AST walkers without depending on `mdast` directly:
21
+
22
+ ```typescript
23
+ import type { Nodes, Root, Content } from "chat";
24
+
25
+ function render(node: Nodes): string {
26
+ switch (node.type) {
27
+ case "text": return node.value;
28
+ case "strong": return node.children.map(render).join("");
29
+ // ...
30
+ default: throw new Error(`Unhandled: ${node satisfies never}`);
31
+ }
32
+ }
33
+ ```
34
+
35
+ Adapters use this pattern to make the type checker reject the build when a new mdast node type is introduced upstream.
36
+
18
37
  ## Node builders
19
38
 
20
39
  ### root
@@ -222,7 +241,7 @@ const value = getNodeValue(node); // string | undefined
222
241
 
223
242
  ### tableToAscii
224
243
 
225
- Render an mdast `Table` node as a padded ASCII table string. Used by adapters that lack native table support (Slack, Google Chat, Discord, Telegram).
244
+ Render an mdast `Table` node as a padded ASCII table string. Used by adapters that lack native table support (Google Chat, Discord, Telegram).
226
245
 
227
246
  ```typescript
228
247
  import { parseMarkdown, tableToAscii, isTableNode } from "chat";
@@ -261,17 +280,21 @@ The SDK uses mdast as the canonical format and each adapter converts it to the p
261
280
 
262
281
  | Feature | Slack | Teams | Google Chat |
263
282
  |---------|-------|-------|-------------|
264
- | Bold | `*text*` | `**text**` | `*text*` |
283
+ | Bold | `**text**` | `**text**` | `*text*` |
265
284
  | Italic | `_text_` | `_text_` | `_text_` |
266
- | Strikethrough | `~text~` | `~~text~~` | `~text~` |
285
+ | Strikethrough | `~~text~~` | `~~text~~` | `~text~` |
267
286
  | Code | `` `code` `` | `` `code` `` | `` `code` `` |
268
287
  | Code blocks | ```` ``` ```` | ```` ``` ```` | ```` ``` ```` |
269
- | Links | `<url\|text>` | `[text](url)` | `[text](url)` |
288
+ | Links | `[text](url)` | `[text](url)` | `[text](url)` |
270
289
  | Lists | Supported | Supported | Supported |
271
290
  | Blockquotes | `>` | `>` | Simulated with `>` prefix |
272
- | Tables | ASCII fallback | Native GFM | ASCII fallback |
291
+ | Tables | Native (markdown_text) | Native GFM | ASCII fallback |
273
292
  | Mentions | `<@USER>` | `<at>name</at>` | `<users/{id}>` |
274
293
 
294
+ <Callout type="info">
295
+ Slack accepts standard markdown via the `markdown_text` field on `chat.postMessage` and friends, so the SDK passes markdown through directly. Incoming Slack messages still arrive as legacy mrkdwn (`*bold*`, `<url|text>`) and are parsed transparently. If you need to send mrkdwn yourself, use `{ raw: "..." }`.
296
+ </Callout>
297
+
275
298
  <Callout type="info">
276
299
  You don't need to worry about these differences when using the SDK — the AST builders and `parseMarkdown` handle conversion automatically. This table is useful if you're working with `raw` platform payloads or debugging formatting issues.
277
300
  </Callout>
@@ -54,6 +54,10 @@ import { Message } from "chat";
54
54
  description: 'Whether the bot was @-mentioned in this message.',
55
55
  type: 'boolean | undefined',
56
56
  },
57
+ subject: {
58
+ description: 'Resolves the parent resource (issue, PR) this message is about. Returns null on chat platforms. See [Message Subject](/docs/subject).',
59
+ type: 'Promise<MessageSubject | null>',
60
+ },
57
61
  }}
58
62
  />
59
63
 
@@ -200,6 +204,55 @@ When using [`toAiMessages()`](/docs/api/to-ai-messages), link metadata is automa
200
204
  | Slack | URLs from `rich_text` blocks or `<url>` text patterns | Slack message links (`*.slack.com/archives/...`) |
201
205
  | Others | Not yet — `links` is always `[]` | — |
202
206
 
207
+ ## MessageSubject
208
+
209
+ Returned by `message.subject` on platforms with parent resources. See [Message Subject](/docs/subject) for usage.
210
+
211
+ <TypeTable
212
+ type={{
213
+ type: {
214
+ description: 'Resource kind, e.g. "issue" or "pull_request".',
215
+ type: 'string',
216
+ },
217
+ id: {
218
+ description: 'Resource identifier (e.g. "ENG-123" or "42").',
219
+ type: 'string',
220
+ },
221
+ title: {
222
+ description: 'Resource title.',
223
+ type: 'string | undefined',
224
+ },
225
+ description: {
226
+ description: 'Full description/body in markdown.',
227
+ type: 'string | undefined',
228
+ },
229
+ status: {
230
+ description: 'Current status (e.g. "In Progress", "open").',
231
+ type: 'string | undefined',
232
+ },
233
+ url: {
234
+ description: 'Web URL to the resource.',
235
+ type: 'string | undefined',
236
+ },
237
+ author: {
238
+ description: 'Resource creator.',
239
+ type: '{ id: string; name: string } | undefined',
240
+ },
241
+ assignee: {
242
+ description: 'Current assignee.',
243
+ type: '{ id: string; name: string } | undefined',
244
+ },
245
+ labels: {
246
+ description: 'Labels/tags.',
247
+ type: 'string[] | undefined',
248
+ },
249
+ raw: {
250
+ description: 'Full platform API response.',
251
+ type: 'unknown',
252
+ },
253
+ }}
254
+ />
255
+
203
256
  ## Serialization
204
257
 
205
258
  Messages can be serialized for workflow engines and external systems.
@@ -6,7 +6,9 @@
6
6
  "thread",
7
7
  "channel",
8
8
  "message",
9
+ "to-ai-messages",
9
10
  "postable-message",
11
+ "transcripts",
10
12
  "cards",
11
13
  "markdown",
12
14
  "modals"
@@ -54,6 +54,10 @@ bot.onAction("open-form", async (event) => {
54
54
  type: 'boolean',
55
55
  default: 'false',
56
56
  },
57
+ callbackUrl: {
58
+ description: 'URL to POST form values to when the modal is submitted.',
59
+ type: 'string',
60
+ },
57
61
  privateMetadata: {
58
62
  description: 'Arbitrary string passed through the modal lifecycle (e.g., JSON context).',
59
63
  type: 'string',
@@ -167,6 +171,52 @@ Select({
167
171
  }}
168
172
  />
169
173
 
174
+ ## ExternalSelect
175
+
176
+ Dropdown that loads options dynamically from a handler as the user types. Slack-only. Pair with [`bot.onOptionsLoad`](/docs/api/chat#onoptionsload) to supply options. See [Modals → ExternalSelect](/docs/modals#externalselect) for a full example, grouped-options support, and Slack setup notes.
177
+
178
+ ```typescript
179
+ ExternalSelect({
180
+ id: "assignee",
181
+ label: "Assignee",
182
+ placeholder: "Search people",
183
+ minQueryLength: 1,
184
+ initialOption: { label: "Alice", value: "U123" },
185
+ })
186
+ ```
187
+
188
+ <TypeTable
189
+ type={{
190
+ id: {
191
+ description: 'Input ID — used as the key in event.values.',
192
+ type: 'string',
193
+ },
194
+ label: {
195
+ description: 'Label displayed above the select.',
196
+ type: 'string',
197
+ },
198
+ placeholder: {
199
+ description: 'Placeholder text.',
200
+ type: 'string',
201
+ },
202
+ minQueryLength: {
203
+ description: 'Minimum characters before the loader fires (Slack default: 3).',
204
+ type: 'number',
205
+ },
206
+ initialOption: {
207
+ description: 'Pre-selected option when the modal opens. Unlike static Select where initialOption is a value string, ExternalSelect needs the full label/value object since the loader has not run yet.',
208
+ type: '{ label: string, value: string }',
209
+ },
210
+ optional: {
211
+ description: 'Whether the field can be left empty.',
212
+ type: 'boolean',
213
+ default: 'false',
214
+ },
215
+ }}
216
+ />
217
+
218
+ The loader registered via `bot.onOptionsLoad("assignee", handler)` returns either a flat `SelectOptionElement[]` or `OptionsLoadGroup[]` (`{ label, options }[]`) for grouped options.
219
+
170
220
  ## RadioSelect
171
221
 
172
222
  Radio button group for mutually exclusive choices.