chat 4.26.0 → 4.27.0

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.
Files changed (31) hide show
  1. package/dist/{chunk-OPV5U4WG.js → chunk-AN7MRAVW.js} +39 -0
  2. package/dist/index.d.ts +220 -6
  3. package/dist/index.js +321 -50
  4. package/dist/{jsx-runtime-DxATbnrP.d.ts → jsx-runtime-Co9uV6l7.d.ts} +39 -5
  5. package/dist/jsx-runtime.d.ts +1 -1
  6. package/dist/jsx-runtime.js +1 -1
  7. package/docs/adapters.mdx +28 -28
  8. package/docs/api/chat.mdx +85 -1
  9. package/docs/api/message.mdx +5 -1
  10. package/docs/api/thread.mdx +23 -1
  11. package/docs/contributing/publishing.mdx +33 -0
  12. package/docs/files.mdx +1 -0
  13. package/docs/getting-started.mdx +1 -11
  14. package/docs/meta.json +0 -2
  15. package/docs/modals.mdx +73 -1
  16. package/docs/streaming.mdx +13 -5
  17. package/docs/threads-messages-channels.mdx +34 -0
  18. package/package.json +3 -2
  19. package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +180 -0
  20. package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +134 -0
  21. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +220 -0
  22. package/resources/guides/run-and-track-deploys-from-slack.md +270 -0
  23. package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +147 -0
  24. package/resources/guides/triage-form-submissions-with-chat-sdk.md +178 -0
  25. package/resources/templates.json +19 -0
  26. package/docs/guides/code-review-hono.mdx +0 -241
  27. package/docs/guides/discord-nuxt.mdx +0 -227
  28. package/docs/guides/durable-chat-sessions-nextjs.mdx +0 -337
  29. package/docs/guides/meta.json +0 -10
  30. package/docs/guides/scheduled-posts-neon.mdx +0 -447
  31. package/docs/guides/slack-nextjs.mdx +0 -234
@@ -7,7 +7,7 @@ import {
7
7
  jsxs,
8
8
  toCardElement,
9
9
  toModalElement
10
- } from "./chunk-OPV5U4WG.js";
10
+ } from "./chunk-AN7MRAVW.js";
11
11
  export {
12
12
  Fragment,
13
13
  isCardLinkProps,
package/docs/adapters.mdx CHANGED
@@ -16,52 +16,52 @@ Ready to build your own? Follow the [building](/docs/contributing/building) guid
16
16
 
17
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
18
  |---------|-------|-------|-------------|---------|---------|--------|--------|-----------|
19
- | Post message | | | | | | | | |
20
- | Edit message | | | | | | | | |
21
- | Delete message | | | | | | | | |
22
- | File uploads | | | | | ⚠️ Single file | | | Images, audio, docs |
23
- | Streaming | Native | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | ⚠️ Post+Edit | | | |
24
- | Scheduled messages | Native | | | | | | | |
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 /> |
25
25
 
26
26
  ### Rich content
27
27
 
28
28
  | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
29
29
  |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
30
30
  | Card format | Block Kit | Adaptive Cards | Google Chat Cards | Embeds | Markdown + inline keyboard buttons | GFM Markdown | Markdown | WhatsApp templates |
31
- | Buttons | | | | | ⚠️ Inline keyboard callbacks | | | Interactive replies |
32
- | Link buttons | | | | | ⚠️ Inline keyboard URLs | | | |
33
- | Select menus | | | | | | | | |
34
- | Tables | Block Kit | GFM | ⚠️ ASCII | GFM | ⚠️ ASCII | GFM | GFM | |
35
- | Fields | | | | | | | | ⚠️ Template variables |
36
- | Images in cards | | | | | | | | |
37
- | Modals | | | | | | | | |
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 /> |
38
38
 
39
39
  ### Conversations
40
40
 
41
41
  | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
42
42
  |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
43
- | Slash commands | | | | | | | | |
44
- | Mentions | | | | | | | | |
45
- | Add reactions | | | | | | | | |
46
- | Remove reactions | | | | | | ⚠️ | ⚠️ | |
47
- | Typing indicator | | | | | | | | |
48
- | DMs | | | | | | | | |
49
- | Ephemeral messages | Native | | Native | | | | | |
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 /> |
50
50
 
51
51
  ### Message history
52
52
 
53
53
  | Feature | Slack | Teams | Google Chat | Discord | Telegram | GitHub | Linear | WhatsApp |
54
54
  |---------|-------|-------|-------------|---------|----------|--------|--------|-----------|
55
- | Fetch messages | | | | | ⚠️ Cached | | | ⚠️ Cached sent messages only |
56
- | Fetch single message | | | | | ⚠️ Cached | | | ⚠️ Cached sent messages only |
57
- | Fetch thread info | | | | | | | | |
58
- | Fetch channel messages | | | | | ⚠️ Cached | | | ⚠️ Cached sent messages only |
59
- | List threads | | | | | | | | |
60
- | Fetch channel info | | | | | | | | |
61
- | Post channel message | | | | | | | | |
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 /> |
62
62
 
63
63
  <Callout type="info">
64
- ⚠️ indicates partial support — the feature works with limitations. See individual adapter pages for details.
64
+ <Warn /> indicates partial support — the feature works with limitations. See individual adapter pages for details.
65
65
  </Callout>
66
66
 
67
67
  ## How adapters work
package/docs/api/chat.mdx CHANGED
@@ -255,7 +255,8 @@ bot.onModalSubmit("feedback", async (event) => {
255
255
 
256
256
  Returns `ModalResponse | undefined` to control the modal after submission:
257
257
 
258
- - `{ action: "close" }` — close the modal
258
+ - `{ action: "close" }` — close the current view (goes back one level in the stack)
259
+ - `{ action: "clear" }` — close all views and dismiss the modal entirely
259
260
  - `{ action: "errors", errors: { fieldId: "message" } }` — show validation errors
260
261
  - `{ action: "update", modal: ModalElement }` — replace the modal content
261
262
  - `{ action: "push", modal: ModalElement }` — push a new modal view onto the stack
@@ -443,6 +444,89 @@ await dm.post("Hello via DM!");
443
444
  const dm = await bot.openDM(message.author);
444
445
  ```
445
446
 
447
+ ### getUser
448
+
449
+ Look up user information by user ID. Returns a `UserInfo` object with name, email, avatar, and bot status, or `null` if the user was not found. Supported on Slack, Microsoft Teams, Discord, Google Chat, GitHub, Linear, and Telegram. Other adapters will throw `NOT_SUPPORTED`.
450
+
451
+ ```typescript
452
+ const user = await bot.getUser("U123456");
453
+ console.log(user?.email); // "alice@company.com"
454
+ console.log(user?.fullName); // "Alice Smith"
455
+ ```
456
+
457
+ ```typescript
458
+ // Or with an Author object from a message handler
459
+ const user = await bot.getUser(message.author);
460
+ ```
461
+
462
+ <TypeTable
463
+ type={{
464
+ userId: {
465
+ description: 'Platform-specific user ID.',
466
+ type: 'string',
467
+ },
468
+ userName: {
469
+ description: 'Username/handle.',
470
+ type: 'string',
471
+ },
472
+ fullName: {
473
+ description: 'Display name / full name.',
474
+ type: 'string',
475
+ },
476
+ isBot: {
477
+ description: 'Whether the user is a bot.',
478
+ type: 'boolean',
479
+ },
480
+ email: {
481
+ description: 'Email address (requires scopes on some platforms).',
482
+ type: 'string | undefined',
483
+ },
484
+ avatarUrl: {
485
+ description: 'Profile image URL.',
486
+ type: 'string | undefined',
487
+ },
488
+ }}
489
+ />
490
+
491
+ <Callout type="info">
492
+ **Per-platform constraints:**
493
+ - **Slack** — requires both `users:read` and `users:read.email` scopes (the email scope must be granted at OAuth install time).
494
+ - **Discord** — bot tokens never see email (the `email` OAuth scope only applies in user-context auth).
495
+ - **Telegram** — bots can only look up users who have previously messaged them.
496
+ - **Microsoft Teams** — only works for users who previously interacted with the bot (cached from webhook activity). `avatarUrl` is not returned (Graph API requires a separate photo call).
497
+ - **Google Chat** — same caching constraint as Teams: only users seen in prior webhooks.
498
+ - **GitHub** — `email` is `null` unless the user made it public, or you authenticated with the `user:email` scope.
499
+ - **Linear** — full profile (incl. email + avatar) for any active workspace member.
500
+
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.
502
+ </Callout>
503
+
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:
505
+
506
+ ```typescript
507
+ import { ChatError } from "chat";
508
+
509
+ try {
510
+ const user = await bot.getUser(userId);
511
+ if (!user) {
512
+ // User not found on this platform
513
+ }
514
+ } catch (error) {
515
+ if (error instanceof ChatError && error.code === "NOT_SUPPORTED") {
516
+ // This adapter doesn't support user lookups
517
+ }
518
+ }
519
+ ```
520
+
521
+ ### thread
522
+
523
+ Get a Thread handle by its thread ID. Useful for posting to threads outside of webhook contexts (e.g. cron jobs, external triggers).
524
+
525
+ ```typescript
526
+ const thread = bot.thread("slack:C123ABC:1234567890.123456");
527
+ await thread.post("Hello from a cron job!");
528
+ ```
529
+
446
530
  ### channel
447
531
 
448
532
  Get a Channel by its channel ID.
@@ -149,6 +149,10 @@ All adapters return `false` if the bot ID isn't known yet. This is a safe defaul
149
149
  description: 'Fetch the attachment data. Handles platform auth automatically.',
150
150
  type: '() => Promise<Buffer> | undefined',
151
151
  },
152
+ fetchMetadata: {
153
+ description: 'Platform-specific IDs for reconstructing fetchData after serialization (e.g. WhatsApp mediaId, Telegram fileId).',
154
+ type: 'Record<string, string> | undefined',
155
+ },
152
156
  }}
153
157
  />
154
158
 
@@ -208,4 +212,4 @@ const json = message.toJSON();
208
212
  const restored = Message.fromJSON(json);
209
213
  ```
210
214
 
211
- The serialized format converts `Date` fields to ISO strings and omits non-serializable fields like `data` buffers and `fetchData` functions.
215
+ The serialized format converts `Date` fields to ISO strings and omits non-serializable fields like `data` buffers and `fetchData` functions. The `fetchMetadata` field is preserved so that adapters can reconstruct `fetchData` when the message is rehydrated from a queue.
@@ -4,7 +4,7 @@ description: Represents a conversation thread with methods for posting, subscrib
4
4
  type: reference
5
5
  ---
6
6
 
7
- A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You don't create threads directly they come from handler callbacks or `chat.openDM()`.
7
+ A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You can also create thread handles directly using `chat.thread()` or `chat.openDM()`.
8
8
 
9
9
  ## Properties
10
10
 
@@ -114,6 +114,28 @@ await scheduled.cancel();
114
114
  Streaming and file uploads are not supported in scheduled messages.
115
115
  </Callout>
116
116
 
117
+ ## getParticipants
118
+
119
+ Get the unique human participants in a thread. Returns deduplicated authors, excluding all bots. Useful for subscribing only to 1:1 conversations and unsubscribing when others join.
120
+
121
+ ```typescript
122
+ const participants = await thread.getParticipants();
123
+
124
+ // Subscribe only when one person is talking to the bot
125
+ if (participants.length === 1) {
126
+ await thread.subscribe();
127
+ }
128
+
129
+ // Unsubscribe when the thread becomes a group conversation
130
+ if (participants.length > 1) {
131
+ await thread.unsubscribe();
132
+ }
133
+ ```
134
+
135
+ <Callout type="warn">
136
+ Each call fetches the full message history to find all participants. On threads with long history this makes multiple API calls to the platform. Consider checking `message.author` against a known set before calling `getParticipants()` on every incoming message.
137
+ </Callout>
138
+
117
139
  ## subscribe / unsubscribe
118
140
 
119
141
  Manage thread subscriptions. Subscribed threads route all messages to `onSubscribedMessage` handlers.
@@ -159,3 +159,36 @@ You should see your exported symbols (`createMatrixAdapter`, `MatrixAdapter`, et
159
159
  - Watch the [Chat SDK changelog](https://github.com/vercel/chat/releases) for new features and breaking changes
160
160
  - Run your test suite against new Chat SDK releases before they ship to catch compatibility issues early
161
161
  - When the `Adapter` interface adds new optional methods, consider implementing them to keep your adapter feature-complete
162
+
163
+ ## Listing on chat-sdk.dev
164
+
165
+ Community adapters can be listed on the [Adapters](https://chat-sdk.dev/adapters) page by opening a PR that adds an entry to `apps/docs/adapters.json` in the [Chat SDK repo](https://github.com/vercel/chat). Your adapter's README is fetched from GitHub at build time and rendered on its dedicated page.
166
+
167
+ ### Pin your README to a commit or tag
168
+
169
+ <Callout type="warn">
170
+ The `readme` field **must** reference a specific commit SHA or tag — not a branch name like `main`.
171
+ </Callout>
172
+
173
+ The docs site re-renders on every deploy, so an unpinned `readme` would serve whatever currently sits at your default branch — including edits made after the listing PR was reviewed. Pinning freezes the rendered content at the state we approved; new content goes live through a follow-up PR that bumps the ref.
174
+
175
+ ```json title="apps/docs/adapters.json"
176
+ {
177
+ "name": "My Adapter",
178
+ "slug": "my-adapter",
179
+ "type": "platform",
180
+ "community": true,
181
+ "packageName": "chat-adapter-my-thing",
182
+ "readme": "https://github.com/your-org/chat-adapter-my-thing/tree/v1.2.0"
183
+ }
184
+ ```
185
+
186
+ Accepted `readme` formats:
187
+
188
+ | Format | Example |
189
+ |--------|---------|
190
+ | Repo root at a tag | `https://github.com/owner/repo/tree/v1.0.0` |
191
+ | Repo root at a commit | `https://github.com/owner/repo/tree/abc1234...` |
192
+ | Subpath in a monorepo | `https://github.com/owner/repo/tree/<ref>/packages/adapter` |
193
+
194
+ Unpinned refs (e.g., `tree/main`, or omitting `/tree/<ref>` entirely) will emit a build warning and are rejected during PR review.
package/docs/files.mdx CHANGED
@@ -75,3 +75,4 @@ bot.onSubscribedMessage(async (thread, message) => {
75
75
  | `width` | `number` (optional) | Image width |
76
76
  | `height` | `number` (optional) | Image height |
77
77
  | `fetchData` | `() => Promise<Buffer>` (optional) | Download the file data |
78
+ | `fetchMetadata` | `Record<string, string>` (optional) | Platform-specific IDs for reconstructing `fetchData` after serialization |
@@ -25,14 +25,4 @@ Connect your bot to chat platforms and persist state across restarts.
25
25
 
26
26
  Browse all official and community adapters on the [Adapters](/adapters) page.
27
27
 
28
- ## Guides
29
-
30
- Step-by-step tutorials to get up and running on your platform of choice.
31
-
32
- <Cards>
33
- <Card title="Slack bot with Next.js and Redis" description="Build a Slack bot from scratch using Chat SDK, Next.js, and Redis." href="/docs/guides/slack-nextjs" />
34
- <Card title="Durable chat sessions with Next.js, Workflow, and Redis" description="Build a bot whose thread sessions survive restarts by combining Chat SDK with Workflow." href="/docs/guides/durable-chat-sessions-nextjs" />
35
- <Card title="Schedule Slack posts with Next.js, Workflow, and Neon" description="Build durable scheduled posts backed by Neon and Workflow timers." href="/docs/guides/scheduled-posts-neon" />
36
- <Card title="Code review GitHub bot with Hono and Redis" description="Build a GitHub bot that reviews pull requests using AI SDK, Vercel Sandbox, and Chat SDK." href="/docs/guides/code-review-hono" />
37
- <Card title="Discord support bot with Nuxt and Redis" description="Build a Discord support bot using Chat SDK, Nuxt, and AI SDK." href="/docs/guides/discord-nuxt" />
38
- </Cards>
28
+ Step-by-step guides and starter templates are available on the [Resources](/resources) page.
package/docs/meta.json CHANGED
@@ -15,8 +15,6 @@
15
15
  "concurrency",
16
16
  "...",
17
17
  "error-handling",
18
- "---Guides---",
19
- "...guides",
20
18
  "---API Reference---",
21
19
  "...api",
22
20
  "---Contributing---",
package/docs/modals.mdx CHANGED
@@ -87,6 +87,77 @@ A dropdown for selecting a single option.
87
87
  | `initialOption` | `string` (optional) | Pre-selected value |
88
88
  | `optional` | `boolean` (optional) | Allow empty submission |
89
89
 
90
+ ### ExternalSelect
91
+
92
+ A dropdown that loads its options dynamically from a handler as the user types. Useful for large or remote-backed option sets (people, tickets, records) where a static `<Select>` would be impractical. Slack-only.
93
+
94
+ | Prop | Type | Description |
95
+ |------|------|-------------|
96
+ | `id` | `string` | Field identifier (key in `event.values`) |
97
+ | `label` | `string` | Field label |
98
+ | `placeholder` | `string` (optional) | Placeholder text |
99
+ | `minQueryLength` | `number` (optional) | Minimum characters before the loader fires (Slack default: 3) |
100
+ | `initialOption` | `{ label, value }` (optional) | Pre-selected option when the modal opens (must match an option returned by the loader). For static `<Select>`, `initialOption` is just the value string — for `<ExternalSelect>` it's the full `{ label, value }` object since the loader hasn't run yet. |
101
+ | `optional` | `boolean` (optional) | Allow empty submission |
102
+
103
+ Register the loader with `onOptionsLoad`:
104
+
105
+ ```tsx title="lib/bot.tsx" lineNumbers
106
+ import { ExternalSelect, Modal } from "chat";
107
+
108
+ bot.onAction("assign", async (event) => {
109
+ await event.openModal(
110
+ <Modal callbackId="assign_form" title="Assign to…">
111
+ <ExternalSelect
112
+ id="assignee"
113
+ label="Assignee"
114
+ placeholder="Search people"
115
+ minQueryLength={1}
116
+ />
117
+ </Modal>
118
+ );
119
+ });
120
+
121
+ bot.onOptionsLoad("assignee", async (event) => {
122
+ const people = await peopleService.search(event.query);
123
+ return people.map((p) => ({ label: p.fullName, value: p.id }));
124
+ });
125
+
126
+ bot.onModalSubmit("assign_form", async (event) => {
127
+ const assigneeId = event.values.assignee;
128
+ // …
129
+ });
130
+ ```
131
+
132
+ The selected value arrives in `event.values` on submit just like a static `<Select>`.
133
+
134
+ #### Grouped options
135
+
136
+ Return an array of groups instead of a flat options array to render headers between sections (e.g. "Recent" / "All"):
137
+
138
+ ```tsx
139
+ bot.onOptionsLoad("assignee", async (event) => {
140
+ const [recent, all] = await Promise.all([
141
+ peopleService.recent(event.user.userId),
142
+ peopleService.search(event.query),
143
+ ]);
144
+ return [
145
+ { label: "Recent", options: recent.map((p) => ({ label: p.fullName, value: p.id })) },
146
+ { label: "All", options: all.map((p) => ({ label: p.fullName, value: p.id })) },
147
+ ];
148
+ });
149
+ ```
150
+
151
+ Slack limits: max 100 groups, max 100 options per group, group label max 75 characters.
152
+
153
+ <Callout type="warn">
154
+ Slack requires a response within 3 seconds for options requests. The adapter caps the loader at ~2.5s and returns an empty result on timeout — keep your loader fast (cache, prefetch, or narrow the query server-side).
155
+ </Callout>
156
+
157
+ <Callout type="info">
158
+ **Slack setup:** `ExternalSelect` uses Slack's `block_suggestion` payload, which is dispatched to the **Options Load URL**. In your [Slack app settings](https://api.slack.com/apps) go to **Interactivity & Shortcuts** → **Select Menus** and set the **Options Load URL** to the same endpoint as your Interactivity Request URL (e.g. `https://your-domain.com/api/webhooks/slack`). Without this, typing into an external select will silently return no results.
159
+ </Callout>
160
+
90
161
  ### RadioSelect
91
162
 
92
163
  A radio button group for mutually exclusive options.
@@ -142,7 +213,8 @@ bot.onModalSubmit("feedback_form", async (event) => {
142
213
 
143
214
  | Response | Description |
144
215
  |----------|-------------|
145
- | `undefined` or `{ action: "close" }` | Close the modal |
216
+ | `undefined` or `{ action: "close" }` | Close the current view (goes back one level in the stack) |
217
+ | `{ action: "clear" }` | Close all views and dismiss the modal entirely |
146
218
  | `{ action: "errors", errors: { fieldId: "message" } }` | Show validation errors on specific fields |
147
219
  | `{ action: "update", modal: ModalElement }` | Replace the modal content |
148
220
  | `{ action: "push", modal: ModalElement }` | Push a new modal view onto the stack |
@@ -118,6 +118,7 @@ const stream = (async function* () {
118
118
  type: "task_update",
119
119
  id: "search-1",
120
120
  title: "Searching documents",
121
+ details: "Querying internal docs and ranking the best matches",
121
122
  status: "in_progress",
122
123
  } satisfies StreamChunk;
123
124
 
@@ -127,6 +128,7 @@ const stream = (async function* () {
127
128
  type: "task_update",
128
129
  id: "search-1",
129
130
  title: "Searching documents",
131
+ details: "Ranked 3 relevant results",
130
132
  status: "complete",
131
133
  output: "Found 3 results",
132
134
  } satisfies StreamChunk;
@@ -142,16 +144,19 @@ await thread.post(stream);
142
144
  | Type | Fields | Description |
143
145
  |------|--------|-------------|
144
146
  | `markdown_text` | `text` | Streamed text content |
145
- | `task_update` | `id`, `title`, `status`, `output?` | Tool/step progress cards (`pending`, `in_progress`, `complete`, `error`) |
147
+ | `task_update` | `id`, `title`, `status`, `details?`, `output?` | Tool/step progress cards (`pending`, `in_progress`, `complete`, `error`) with optional extra task context |
146
148
  | `plan_update` | `title` | Plan title updates |
147
149
 
148
150
  ### Task display mode
149
151
 
150
- Control how `task_update` chunks render in Slack by passing `taskDisplayMode` in stream options:
152
+ Control how `task_update` chunks render in Slack by passing `taskDisplayMode` via the adapter's streaming API. These options are currently only available through `adapter.stream()` directly:
151
153
 
152
154
  ```typescript
153
- await thread.stream(stream, {
154
- taskDisplayMode: "plan", // Group all tasks into a single plan block
155
+ const raw = message.raw as { team_id?: string; team?: string };
156
+ await thread.adapter.stream(thread.id, stream, {
157
+ recipientUserId: message.author.userId,
158
+ recipientTeamId: raw.team_id ?? raw.team,
159
+ taskDisplayMode: "plan",
155
160
  });
156
161
  ```
157
162
 
@@ -167,7 +172,10 @@ Adapters without structured chunk support extract text from `markdown_text` chun
167
172
  When streaming in Slack, you can attach Block Kit elements to the final message using `stopBlocks`. This is useful for adding action buttons after a streamed response completes:
168
173
 
169
174
  ```typescript title="lib/bot.ts" lineNumbers
170
- await thread.stream(textStream, {
175
+ const raw = message.raw as { team_id?: string; team?: string };
176
+ await thread.adapter.stream(thread.id, textStream, {
177
+ recipientUserId: message.author.userId,
178
+ recipientTeamId: raw.team_id ?? raw.team,
171
179
  stopBlocks: [
172
180
  {
173
181
  type: "actions",
@@ -40,6 +40,33 @@ await thread.unsubscribe();
40
40
  const subscribed = await thread.isSubscribed();
41
41
  ```
42
42
 
43
+ ### Participants
44
+
45
+ Get the unique human participants in a thread. Returns deduplicated authors, excluding all bots. Useful for deciding whether to subscribe based on how many humans are in the conversation.
46
+
47
+ ```typescript title="lib/bot.ts" lineNumbers
48
+ bot.onNewMention(async (thread) => {
49
+ const participants = await thread.getParticipants();
50
+ if (participants.length === 1) {
51
+ await thread.subscribe();
52
+ await thread.post("I'm here to help!");
53
+ }
54
+ });
55
+
56
+ bot.onSubscribedMessage(async (thread) => {
57
+ const participants = await thread.getParticipants();
58
+ if (participants.length > 1) {
59
+ await thread.unsubscribe();
60
+ return;
61
+ }
62
+ // respond...
63
+ });
64
+ ```
65
+
66
+ <Callout type="warn">
67
+ Each call fetches the full message history to find all participants. On threads with long history this makes multiple API calls to the platform. Consider checking `message.author` against a known set before calling `getParticipants()` on every incoming message.
68
+ </Callout>
69
+
43
70
  ### Typing indicator
44
71
 
45
72
  ```typescript title="lib/bot.ts"
@@ -144,6 +171,13 @@ interface Author {
144
171
  }
145
172
  ```
146
173
 
174
+ For richer user info (email, avatar), use [`chat.getUser()`](/docs/api/chat#getuser):
175
+
176
+ ```typescript title="lib/bot.ts"
177
+ const user = await bot.getUser(message.author);
178
+ console.log(user?.email); // "alice@company.com"
179
+ ```
180
+
147
181
  ### Sent messages
148
182
 
149
183
  When you post a message, you get back a `SentMessage` with methods to edit, delete, and react:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat",
3
- "version": "4.26.0",
3
+ "version": "4.27.0",
4
4
  "description": "Unified chat abstraction for Slack, Teams, Google Chat, and Discord",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,8 @@
22
22
  },
23
23
  "files": [
24
24
  "dist",
25
- "docs"
25
+ "docs",
26
+ "resources"
26
27
  ],
27
28
  "dependencies": {
28
29
  "@workflow/serde": "4.1.0-beta.2",