chat 4.26.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.
Files changed (50) hide show
  1. package/dist/{chunk-OPV5U4WG.js → chunk-V25FKIIL.js} +44 -1
  2. package/dist/index.d.ts +485 -33
  3. package/dist/index.js +862 -135
  4. package/dist/{jsx-runtime-DxATbnrP.d.ts → jsx-runtime-DxGwoLu2.d.ts} +49 -5
  5. package/dist/jsx-runtime.d.ts +1 -1
  6. package/dist/jsx-runtime.js +1 -1
  7. package/docs/actions.mdx +52 -1
  8. package/docs/adapters.mdx +43 -37
  9. package/docs/api/cards.mdx +4 -0
  10. package/docs/api/chat.mdx +172 -6
  11. package/docs/api/index.mdx +2 -0
  12. package/docs/api/markdown.mdx +28 -5
  13. package/docs/api/message.mdx +58 -1
  14. package/docs/api/meta.json +2 -0
  15. package/docs/api/modals.mdx +50 -0
  16. package/docs/api/postable-message.mdx +55 -1
  17. package/docs/api/thread.mdx +33 -3
  18. package/docs/api/transcripts.mdx +220 -0
  19. package/docs/cards.mdx +6 -0
  20. package/docs/concurrency.mdx +4 -0
  21. package/docs/contributing/building.mdx +73 -1
  22. package/docs/contributing/publishing.mdx +33 -0
  23. package/docs/conversation-history.mdx +137 -0
  24. package/docs/direct-messages.mdx +13 -4
  25. package/docs/ephemeral-messages.mdx +1 -1
  26. package/docs/error-handling.mdx +15 -3
  27. package/docs/files.mdx +2 -1
  28. package/docs/getting-started.mdx +1 -11
  29. package/docs/index.mdx +7 -5
  30. package/docs/meta.json +14 -5
  31. package/docs/modals.mdx +97 -1
  32. package/docs/posting-messages.mdx +7 -3
  33. package/docs/streaming.mdx +74 -18
  34. package/docs/subject.mdx +53 -0
  35. package/docs/threads-messages-channels.mdx +43 -0
  36. package/docs/usage.mdx +11 -2
  37. package/package.json +3 -2
  38. package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +180 -0
  39. package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +134 -0
  40. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +220 -0
  41. package/resources/guides/run-and-track-deploys-from-slack.md +270 -0
  42. package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +147 -0
  43. package/resources/guides/triage-form-submissions-with-chat-sdk.md +178 -0
  44. package/resources/templates.json +19 -0
  45. package/docs/guides/code-review-hono.mdx +0 -241
  46. package/docs/guides/discord-nuxt.mdx +0 -227
  47. package/docs/guides/durable-chat-sessions-nextjs.mdx +0 -337
  48. package/docs/guides/meta.json +0 -10
  49. package/docs/guides/scheduled-posts-neon.mdx +0 -447
  50. package/docs/guides/slack-nextjs.mdx +0 -234
@@ -414,6 +414,62 @@ async deleteMessage(threadId: string, messageId: string): Promise<void> {
414
414
  }
415
415
  ```
416
416
 
417
+ ### Buttons and callback URLs
418
+
419
+ When the host app passes a `Card` with `<Button callbackUrl={...}>`, the SDK rewrites each such button **before** your adapter sees the postable: the `callbackUrl` is stored in the state adapter under a short token, and the button's `value` field is replaced with `__cb:<16-hex-chars>` (21 characters total). Your adapter does not need to know this happens — just round-trip `button.value` through your platform's button payload.
420
+
421
+ What this means in practice:
422
+
423
+ 1. **Send side**: when rendering a `ButtonElement` to your platform's button payload, encode both `button.id` (the action ID) and `button.value` (which may be `undefined`, a user-supplied value, or a callback token). Pick a delimiter that cannot appear in either, and validate the encoded string fits the platform's limit.
424
+ 2. **Receive side**: when the user clicks a button, decode the platform payload back into `actionId` and `value`, and pass them to `chat.processAction({ actionId, value, ... })`. The SDK will detect the `__cb:` prefix, look up the stored callback URL, POST to it, and pass the original value (if any) to user `onAction` handlers.
425
+
426
+ Discord's adapter is a good reference — it joins the action ID and value with `\n` and validates against Discord's 100-character `custom_id` limit:
427
+
428
+ ```typescript title="src/cards.ts (discord)" lineNumbers
429
+ const DISCORD_CUSTOM_ID_DELIMITER = "\n";
430
+ const DISCORD_CUSTOM_ID_MAX_LENGTH = 100;
431
+
432
+ export function encodeDiscordCustomId(
433
+ actionId: string,
434
+ value?: string
435
+ ): string {
436
+ if (value == null || value === "") {
437
+ validateLength(actionId);
438
+ return actionId;
439
+ }
440
+ const encoded = `${actionId}${DISCORD_CUSTOM_ID_DELIMITER}${value}`;
441
+ validateLength(encoded);
442
+ return encoded;
443
+ }
444
+
445
+ export function decodeDiscordCustomId(customId: string): {
446
+ actionId: string;
447
+ value: string | undefined;
448
+ } {
449
+ const idx = customId.indexOf(DISCORD_CUSTOM_ID_DELIMITER);
450
+ if (idx === -1) {
451
+ return { actionId: customId, value: undefined };
452
+ }
453
+ // Use the FIRST delimiter only — values may legitimately contain "\n".
454
+ return {
455
+ actionId: customId.slice(0, idx),
456
+ value: customId.slice(idx + 1),
457
+ };
458
+ }
459
+ ```
460
+
461
+ <Callout type="info">
462
+ Platform button-data limits are the main constraint to plan for. Discord's
463
+ `custom_id` is 100 chars; Telegram's `callback_data` is 64 bytes. The SDK's
464
+ callback token is fixed at 21 chars (`__cb:` + 16 hex), so the worst-case
465
+ payload your encoding must fit is `actionId + delimiter + 21`. If the
466
+ encoded string exceeds the platform limit, throw a `ValidationError` from
467
+ `@chat-adapter/shared` so the host app fails fast at post time rather than
468
+ silently truncating.
469
+ </Callout>
470
+
471
+ Modals with `callbackUrl` are handled entirely inside the SDK via stored modal context — your adapter does not need any special handling. Just call `chat.processModalSubmit(event, contextId, { waitUntil })` from your webhook handler and the SDK will POST to the modal's `callbackUrl` (using `waitUntil` if you provide it, so the response is not blocked).
472
+
417
473
  ### Reactions
418
474
 
419
475
  Handle both `EmojiValue` objects and plain strings. `EmojiValue` has a `name` property and `toString()` method — there is no `unicode` field.
@@ -535,7 +591,7 @@ export class MatrixFormatConverter extends BaseFormatConverter {
535
591
  }
536
592
  ```
537
593
 
538
- For platforms with non-standard formatting (e.g., Slack's `mrkdwn`), implement custom parsing in `toAst()` and rendering in `fromAst()`. See the [Discord adapter](https://github.com/vercel/chat/blob/main/packages/adapter-discord/src/markdown.ts) for an example of handling platform-specific mention syntax.
594
+ For platforms with non-standard formatting, implement custom parsing in `toAst()` and rendering in `fromAst()`. See the [Discord adapter](https://github.com/vercel/chat/blob/main/packages/adapter-discord/src/markdown.ts) for an example of handling platform-specific mention syntax.
539
595
 
540
596
  ## Optional methods
541
597
 
@@ -639,3 +695,19 @@ import {
639
695
  cardToFallbackText, // Convert card to plain text
640
696
  } from "@chat-adapter/shared";
641
697
  ```
698
+
699
+ ### Token encryption
700
+
701
+ If your adapter persists OAuth tokens to a `StateAdapter`, encrypt them at rest with the shared AES-256-GCM helpers instead of rolling your own:
702
+
703
+ ```typescript
704
+ import {
705
+ encryptToken, // Encrypt a string into an EncryptedTokenData envelope
706
+ decryptToken, // Decrypt an envelope back to the original string
707
+ decodeKey, // Decode a hex-64 or base64-44 32-byte key (throws on wrong length)
708
+ isEncryptedTokenData, // Type guard for tolerating legacy plaintext records
709
+ type EncryptedTokenData,
710
+ } from "@chat-adapter/shared";
711
+ ```
712
+
713
+ Accept the key as an optional `encryptionKey` config field (auto-detected from a `*_ENCRYPTION_KEY` env var), encrypt on `setInstallation()`, decrypt on `getInstallation()`, and use `isEncryptedTokenData` to keep accepting plaintext records so operators can roll the key in without flushing existing installs. See `@chat-adapter/slack` and `@chat-adapter/linear` for reference implementations.
@@ -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.
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: Conversation History
3
+ description: Persist messages per user across every platform — for LLM context, audit, or compliance.
4
+ type: guide
5
+ prerequisites:
6
+ - /docs/state
7
+ related:
8
+ - /docs/handling-events
9
+ - /docs/api/transcripts
10
+ ---
11
+
12
+ Bots that hold context across a user's conversations need somewhere to store it. The platform's own message history won't do — a user might talk to your bot in Slack today and Discord tomorrow, and you want the same memory to follow them.
13
+
14
+ `bot.transcripts` keeps a per-user transcript in your state adapter, keyed by a stable identifier you choose (an email, an internal user ID, anything that's the same person no matter where they are).
15
+
16
+ ## Setup
17
+
18
+ You opt in by setting two fields on `ChatConfig`:
19
+
20
+ ```typescript title="lib/bot.ts" lineNumbers
21
+ import { Chat } from "chat";
22
+ import { createSlackAdapter } from "@chat-adapter/slack";
23
+ import { createDiscordAdapter } from "@chat-adapter/discord";
24
+ import { createRedisState } from "@chat-adapter/state-redis";
25
+
26
+ const bot = new Chat({
27
+ userName: "mybot",
28
+ adapters: {
29
+ slack: createSlackAdapter(),
30
+ discord: createDiscordAdapter(),
31
+ },
32
+ state: createRedisState({ url: process.env.REDIS_URL! }),
33
+
34
+ // Resolve the cross-platform identifier for an inbound message.
35
+ // Return null for messages you don't want to remember.
36
+ identity: ({ author }) => author.email ?? null,
37
+
38
+ // Storage tuning. retention is the list TTL, refreshed on every append.
39
+ transcripts: {
40
+ retention: "30d",
41
+ maxPerUser: 200,
42
+ },
43
+ });
44
+ ```
45
+
46
+ `transcripts` and `identity` are paired — set one without the other and the constructor throws. This keeps the API loud rather than silently no-op'ing on every call.
47
+
48
+ ## Building LLM context
49
+
50
+ The most common pattern: append the user's message, build a prompt from recent transcript entries, post the reply, append the reply too.
51
+
52
+ ```typescript title="lib/bot.ts" lineNumbers
53
+ bot.onSubscribedMessage(async (thread, msg) => {
54
+ await bot.transcripts.append(thread, msg);
55
+
56
+ const recent = await bot.transcripts.list({
57
+ userKey: msg.userKey!,
58
+ limit: 20,
59
+ });
60
+
61
+ const reply = await generateReply(recent, msg);
62
+ await thread.post(reply);
63
+
64
+ await bot.transcripts.append(
65
+ thread,
66
+ { role: "assistant", text: reply },
67
+ { userKey: msg.userKey! }
68
+ );
69
+ });
70
+ ```
71
+
72
+ A few things worth knowing:
73
+
74
+ - **`msg.userKey`** is set automatically from your `identity` resolver before your handler runs. If the resolver returned `null`, it stays `undefined` and the `append` call no-ops.
75
+ - **Bot replies are explicit.** The SDK doesn't auto-capture `thread.post()` output — you decide what gets remembered. That's important for retries, intermediate streaming chunks, and anything you don't want feeding back into the model later.
76
+ - **Order is chronological.** `list` returns oldest-first, ready to feed into a model. Set `limit` to keep prompts bounded.
77
+
78
+ ## Identity resolution
79
+
80
+ `identity` runs once per inbound message during dispatch. The `author`, `message`, and `adapter` name are all available:
81
+
82
+ ```typescript
83
+ identity: async ({ adapter, author, message }) => {
84
+ // Look up by email when the platform exposes it
85
+ if (author.email) {
86
+ return author.email;
87
+ }
88
+ // Or map a platform user to an internal ID
89
+ return await lookupUser(adapter, author.userId);
90
+ }
91
+ ```
92
+
93
+ Return `null` when you can't resolve a key. The SDK won't fall back to a platform-specific ID — that would silently fragment a user's transcript across platforms, which is exactly what this feature is here to prevent.
94
+
95
+ If your resolver throws, the SDK logs a warning and dispatches the message without a `userKey`. Handlers still run; only the persistence is skipped.
96
+
97
+ ## Filtering entries
98
+
99
+ `list` accepts a few filters. They compose, and they're applied after `getList` — useful for narrowing prompts without restructuring storage.
100
+
101
+ ```typescript
102
+ // Recent N across all platforms
103
+ await bot.transcripts.list({ userKey: "mike@acme.com", limit: 50 });
104
+
105
+ // Single platform
106
+ await bot.transcripts.list({ userKey: "mike@acme.com", platforms: ["slack"] });
107
+
108
+ // Single thread
109
+ await bot.transcripts.list({
110
+ userKey: "mike@acme.com",
111
+ threadId: "slack:C123:1234.5678",
112
+ });
113
+
114
+ // Only the user's own messages
115
+ await bot.transcripts.list({ userKey: "mike@acme.com", roles: ["user"] });
116
+ ```
117
+
118
+ ## Deleting a user's transcript
119
+
120
+ For data-subject requests or simple "forget me" flows:
121
+
122
+ ```typescript
123
+ await bot.transcripts.delete({ userKey: "mike@acme.com" });
124
+ // → { deleted: 47 }
125
+ ```
126
+
127
+ This wipes every entry stored under the key. Single-entry and time-range deletes aren't part of the API — `appendToList` doesn't support them safely under concurrent writes.
128
+
129
+ ## Where it's stored
130
+
131
+ `bot.transcripts` is backed by `StateAdapter.appendToList` / `getList` / `delete`. Every built-in state adapter (`memory`, `redis`, `ioredis`, `pg`) supports these primitives, so this works on whichever one you've already configured.
132
+
133
+ Entries are written under the key `transcripts:user:{userKey}` as a capped list. `appendToList` is atomic, so concurrent inbound messages on the same user don't race.
134
+
135
+ ## Reference
136
+
137
+ See [Transcripts](/docs/api/transcripts) for full type signatures, configuration options, and the entry shape.
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: Direct messages
2
+ title: Direct Messages
3
3
  description: Initiate DM conversations with users programmatically.
4
4
  type: guide
5
5
  prerequisites:
6
6
  - /docs/usage
7
7
  ---
8
8
 
9
- Open direct message conversations with users using `bot.openDM()`. The adapter is automatically inferred from the user ID format.
9
+ Open direct message conversations with users using `bot.openDM()`. For globally recognizable user IDs, the adapter is automatically inferred from the ID format.
10
10
 
11
11
  ## DM behavior
12
12
 
@@ -40,10 +40,19 @@ const dmThread = await bot.openDM("U1234567890"); // Slack
40
40
 
41
41
  | Format | Platform |
42
42
  |--------|----------|
43
- | `U...` | Slack |
43
+ | `U...` / `W...` | Slack |
44
44
  | `29:...` | Teams |
45
45
  | `users/...` | Google Chat |
46
- | Snowflake (numeric) | Discord |
46
+ | Numeric ID | Discord or Telegram |
47
+
48
+ <Callout type="info">
49
+ Numeric IDs can be ambiguous when multiple numeric-ID adapters are registered. For platforms whose user IDs are not globally distinguishable, call the adapter directly and wrap the returned thread ID with `bot.thread()`.
50
+ </Callout>
51
+
52
+ ```typescript title="lib/bot.ts"
53
+ const threadId = await bot.getAdapter("whatsapp").openDM("15551234567");
54
+ const dmThread = bot.thread(threadId);
55
+ ```
47
56
 
48
57
  ## Check if a thread is a DM
49
58
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Ephemeral messages
2
+ title: Ephemeral Messages
3
3
  description: Send messages visible only to a specific user.
4
4
  type: guide
5
5
  prerequisites:
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Error handling
2
+ title: Error Handling
3
3
  description: Handle rate limits, unsupported features, and other errors from adapters.
4
4
  type: guide
5
5
  prerequisites:
@@ -16,7 +16,19 @@ import { ChatError, RateLimitError, NotImplementedError, LockError } from "chat"
16
16
 
17
17
  ### ChatError
18
18
 
19
- Base error class for all SDK errors. Every error below extends `ChatError`.
19
+ Base error class for all SDK errors. Every error below extends `ChatError`. The `code` property carries a machine-readable identifier you can branch on:
20
+
21
+ | Code | Thrown by | Meaning |
22
+ |------|-----------|---------|
23
+ | `NOT_SUPPORTED` | `bot.openDM`, `bot.getUser` | The resolved adapter doesn't implement this method |
24
+ | `INVALID_THREAD_ID` | `bot.thread`, internal routing | Thread ID does not match the `adapter:channel:thread` shape |
25
+ | `INVALID_CHANNEL_ID` | `bot.channel` | Channel ID does not match the `adapter:channel` shape |
26
+ | `ADAPTER_NOT_FOUND` | `bot.thread`, `bot.channel` | Thread/channel ID references an adapter that wasn't registered on this `Chat` instance |
27
+ | `AMBIGUOUS_USER_ID` | `bot.getUser`, `bot.openDM` | Numeric user ID could match more than one registered adapter (Discord/Telegram/GitHub) |
28
+ | `UNKNOWN_USER_ID_FORMAT` | `bot.getUser`, `bot.openDM` | The `userId` doesn't match any platform's known ID format |
29
+ | `RATE_LIMITED` | Any platform call | Platform returned 429; see `RateLimitError` below |
30
+ | `NOT_IMPLEMENTED` | Any platform call | The adapter doesn't implement this feature; see `NotImplementedError` below |
31
+ | `LOCK_FAILED` | Inbound message routing | Distributed lock was busy; see `LockError` below |
20
32
 
21
33
  <TypeTable
22
34
  type={{
@@ -25,7 +37,7 @@ Base error class for all SDK errors. Every error below extends `ChatError`.
25
37
  type: 'string',
26
38
  },
27
39
  code: {
28
- description: 'Machine-readable error code.',
40
+ description: 'Machine-readable error code (see table above).',
29
41
  type: 'string',
30
42
  },
31
43
  cause: {
package/docs/files.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: File uploads
2
+ title: File Uploads
3
3
  description: Send and receive files across chat platforms.
4
4
  type: guide
5
5
  prerequisites:
@@ -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/index.mdx CHANGED
@@ -4,7 +4,7 @@ description: A unified SDK for building chat bots across Slack, Microsoft Teams,
4
4
  type: overview
5
5
  ---
6
6
 
7
- Chat SDK is a TypeScript library for building chat bots that work across multiple platforms with a single codebase. Write your bot logic once and deploy it to Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp.
7
+ Chat SDK is a TypeScript library for building chat bots that work across multiple platforms with a single codebase. Write your bot logic once and deploy it to Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, WhatsApp, and Messenger.
8
8
 
9
9
  ## Why Chat SDK?
10
10
 
@@ -52,13 +52,14 @@ Each adapter factory auto-detects credentials from environment variables (`SLACK
52
52
  | Platform | Package | Mentions | Reactions | Cards | Modals | Streaming | DMs |
53
53
  |----------|---------|----------|-----------|-------|--------|-----------|-----|
54
54
  | Slack | `@chat-adapter/slack` | Yes | Yes | Yes | Yes | Native | Yes |
55
- | Microsoft Teams | `@chat-adapter/teams` | Yes | Read-only | Yes | No | Post+Edit | Yes |
55
+ | Microsoft Teams | `@chat-adapter/teams` | Yes | Read-only | Yes | Yes | Native (DMs) / Buffered | Yes |
56
56
  | Google Chat | `@chat-adapter/gchat` | Yes | Yes | Yes | No | Post+Edit | Yes |
57
57
  | Discord | `@chat-adapter/discord` | Yes | Yes | Yes | No | Post+Edit | Yes |
58
58
  | Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Post+Edit | Yes |
59
- | GitHub | `@chat-adapter/github` | Yes | Yes | No | No | No | No |
60
- | Linear | `@chat-adapter/linear` | Yes | Yes | No | No | No | No |
61
- | WhatsApp | `@chat-adapter/whatsapp` | N/A | Yes | Partial | No | No | Yes |
59
+ | GitHub | `@chat-adapter/github` | Yes | Yes | No | No | Buffered | No |
60
+ | Linear | `@chat-adapter/linear` | Yes | Yes | No | No | Agent sessions / Post+Edit | No |
61
+ | WhatsApp | `@chat-adapter/whatsapp` | N/A | Yes | Partial | No | Buffered | Yes |
62
+ | Messenger | `@chat-adapter/messenger` | Yes | Receive-only | Partial | No | Buffered | Yes |
62
63
 
63
64
  ## AI coding agent support
64
65
 
@@ -85,6 +86,7 @@ The SDK is distributed as a set of packages you install based on your needs:
85
86
  | `@chat-adapter/github` | GitHub Issues adapter |
86
87
  | `@chat-adapter/linear` | Linear Issues adapter |
87
88
  | `@chat-adapter/whatsapp` | WhatsApp Business adapter |
89
+ | `@chat-adapter/messenger` | Facebook Messenger adapter |
88
90
  | `@chat-adapter/state-redis` | Redis state adapter (production) |
89
91
  | `@chat-adapter/state-ioredis` | ioredis state adapter (alternative) |
90
92
  | `@chat-adapter/state-pg` | PostgreSQL state adapter (production) |
package/docs/meta.json CHANGED
@@ -8,15 +8,24 @@
8
8
  "threads-messages-channels",
9
9
  "handling-events",
10
10
  "posting-messages",
11
+ "error-handling",
11
12
  "---Adapters---",
12
13
  "adapters",
13
14
  "state",
14
- "---Features---",
15
+ "---Messaging---",
16
+ "streaming",
17
+ "direct-messages",
18
+ "ephemeral-messages",
19
+ "files",
20
+ "conversation-history",
21
+ "subject",
15
22
  "concurrency",
16
- "...",
17
- "error-handling",
18
- "---Guides---",
19
- "...guides",
23
+ "---Interactivity---",
24
+ "cards",
25
+ "modals",
26
+ "actions",
27
+ "slash-commands",
28
+ "emoji",
20
29
  "---API Reference---",
21
30
  "...api",
22
31
  "---Contributing---",
package/docs/modals.mdx CHANGED
@@ -59,6 +59,7 @@ The top-level container for the form.
59
59
  | `submitLabel` | `string` (optional) | Submit button text (defaults to "Submit") |
60
60
  | `closeLabel` | `string` (optional) | Cancel button text (defaults to "Cancel") |
61
61
  | `notifyOnClose` | `boolean` (optional) | Fire `onModalClose` when user cancels |
62
+ | `callbackUrl` | `string` (optional) | URL to POST form values to on submit |
62
63
  | `privateMetadata` | `string` (optional) | Custom context passed through to handlers |
63
64
 
64
65
  ### TextInput
@@ -87,6 +88,77 @@ A dropdown for selecting a single option.
87
88
  | `initialOption` | `string` (optional) | Pre-selected value |
88
89
  | `optional` | `boolean` (optional) | Allow empty submission |
89
90
 
91
+ ### ExternalSelect
92
+
93
+ 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.
94
+
95
+ | Prop | Type | Description |
96
+ |------|------|-------------|
97
+ | `id` | `string` | Field identifier (key in `event.values`) |
98
+ | `label` | `string` | Field label |
99
+ | `placeholder` | `string` (optional) | Placeholder text |
100
+ | `minQueryLength` | `number` (optional) | Minimum characters before the loader fires (Slack default: 3) |
101
+ | `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. |
102
+ | `optional` | `boolean` (optional) | Allow empty submission |
103
+
104
+ Register the loader with `onOptionsLoad`:
105
+
106
+ ```tsx title="lib/bot.tsx" lineNumbers
107
+ import { ExternalSelect, Modal } from "chat";
108
+
109
+ bot.onAction("assign", async (event) => {
110
+ await event.openModal(
111
+ <Modal callbackId="assign_form" title="Assign to…">
112
+ <ExternalSelect
113
+ id="assignee"
114
+ label="Assignee"
115
+ placeholder="Search people"
116
+ minQueryLength={1}
117
+ />
118
+ </Modal>
119
+ );
120
+ });
121
+
122
+ bot.onOptionsLoad("assignee", async (event) => {
123
+ const people = await peopleService.search(event.query);
124
+ return people.map((p) => ({ label: p.fullName, value: p.id }));
125
+ });
126
+
127
+ bot.onModalSubmit("assign_form", async (event) => {
128
+ const assigneeId = event.values.assignee;
129
+ // …
130
+ });
131
+ ```
132
+
133
+ The selected value arrives in `event.values` on submit just like a static `<Select>`.
134
+
135
+ #### Grouped options
136
+
137
+ Return an array of groups instead of a flat options array to render headers between sections (e.g. "Recent" / "All"):
138
+
139
+ ```tsx
140
+ bot.onOptionsLoad("assignee", async (event) => {
141
+ const [recent, all] = await Promise.all([
142
+ peopleService.recent(event.user.userId),
143
+ peopleService.search(event.query),
144
+ ]);
145
+ return [
146
+ { label: "Recent", options: recent.map((p) => ({ label: p.fullName, value: p.id })) },
147
+ { label: "All", options: all.map((p) => ({ label: p.fullName, value: p.id })) },
148
+ ];
149
+ });
150
+ ```
151
+
152
+ Slack limits: max 100 groups, max 100 options per group, group label max 75 characters.
153
+
154
+ <Callout type="warn">
155
+ 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).
156
+ </Callout>
157
+
158
+ <Callout type="info">
159
+ **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.
160
+ </Callout>
161
+
90
162
  ### RadioSelect
91
163
 
92
164
  A radio button group for mutually exclusive options.
@@ -142,7 +214,8 @@ bot.onModalSubmit("feedback_form", async (event) => {
142
214
 
143
215
  | Response | Description |
144
216
  |----------|-------------|
145
- | `undefined` or `{ action: "close" }` | Close the modal |
217
+ | `undefined` or `{ action: "close" }` | Close the current view (goes back one level in the stack) |
218
+ | `{ action: "clear" }` | Close all views and dismiss the modal entirely |
146
219
  | `{ action: "errors", errors: { fieldId: "message" } }` | Show validation errors on specific fields |
147
220
  | `{ action: "update", modal: ModalElement }` | Replace the modal content |
148
221
  | `{ action: "push", modal: ModalElement }` | Push a new modal view onto the stack |
@@ -176,6 +249,29 @@ bot.onModalClose("feedback_form", async (event) => {
176
249
  });
177
250
  ```
178
251
 
252
+ ## Callback URLs
253
+
254
+ Like buttons, modals accept a `callbackUrl`. When the modal is submitted, the form values are POSTed to the URL:
255
+
256
+ ```tsx title="lib/bot.tsx" lineNumbers
257
+ await event.openModal(
258
+ <Modal callbackUrl={webhook.url} callbackId="intake" title="Request Access" submitLabel="Submit">
259
+ <TextInput id="reason" label="Reason" multiline />
260
+ </Modal>
261
+ );
262
+ ```
263
+
264
+ The POST body for modal submissions:
265
+
266
+ ```json
267
+ {
268
+ "type": "modal_submit",
269
+ "callbackId": "intake",
270
+ "values": { "reason": "Need access to production logs" },
271
+ "user": { "id": "U123", "name": "alice" }
272
+ }
273
+ ```
274
+
179
275
  ## Pass context with privateMetadata
180
276
 
181
277
  Use `privateMetadata` to carry context from the button click through to the submit handler:
@@ -24,7 +24,7 @@ This sends the string directly without any formatting conversion.
24
24
 
25
25
  ## Markdown
26
26
 
27
- Pass a `{ markdown }` object to have the SDK convert standard markdown to each platform's native format mrkdwn for Slack, HTML for Teams, and so on.
27
+ Pass a `{ markdown }` object to have the SDK render standard markdown on each platform — passed through to Slack's native `markdown_text` field, converted to HTML for Teams, and so on.
28
28
 
29
29
  ```typescript title="lib/bot.ts" lineNumbers
30
30
  await thread.post({
@@ -32,7 +32,7 @@ await thread.post({
32
32
  });
33
33
  ```
34
34
 
35
- Under the hood, the SDK parses the markdown into an mdast AST, then each adapter converts it to the platform's format.
35
+ Under the hood, the SDK parses the markdown into an mdast AST, then each adapter handles it natively or converts it to the platform's format.
36
36
 
37
37
  ## AST builders
38
38
 
@@ -139,7 +139,7 @@ See the [Cards](/docs/cards) page for the full list of card components.
139
139
 
140
140
  ## Streaming
141
141
 
142
- Pass an AI SDK stream to `thread.post()` to stream a message in real time. The SDK uses platform-native streaming where available and falls back to post-then-edit on other platforms.
142
+ Pass an AI SDK stream to `thread.post()` to stream a message in real time. The SDK uses platform-native streaming where available and falls back to post-then-edit or buffered delivery depending on the platform.
143
143
 
144
144
  ```typescript title="lib/bot.ts" lineNumbers
145
145
  import { ToolLoopAgent } from "ai";
@@ -153,6 +153,8 @@ Both `fullStream` and `textStream` are supported. Use `fullStream` with multi-st
153
153
 
154
154
  For multi-turn conversations, use [`toAiMessages()`](/docs/api/to-ai-messages) to convert thread history into the `{ role, content }[]` format expected by AI SDKs.
155
155
 
156
+ To pass platform-specific streaming options (e.g. Slack task grouping or stop blocks), wrap the stream in a [`StreamingPlan`](/docs/streaming#streaming-with-options) and post that.
157
+
156
158
  See the [Streaming](/docs/streaming) page for details on platform behavior and configuration.
157
159
 
158
160
  ## Attachments and files
@@ -178,5 +180,7 @@ See the [Files](/docs/files) page for more on attachments.
178
180
  | Card (function) | You need buttons, fields, or structured layouts | Approval flows, dashboards |
179
181
  | Card (JSX) | Same as above, with JSX syntax preference | Same use cases as function cards |
180
182
  | `AsyncIterable` | Streaming AI responses | Chat with LLMs |
183
+ | [`Plan`](/docs/streaming#plan-api) | Step-by-step tasks that mutate after posting | Multi-step agents, deploy progress |
184
+ | [`StreamingPlan`](/docs/streaming#streaming-with-options) | Streaming with platform-specific options | Slack streaming with grouped tasks or stop blocks |
181
185
 
182
186
  For most cases, **AST builders** give the best balance of control and simplicity. Reach for **cards** when you need interactive elements like buttons or dropdowns.