chat 4.28.1 → 4.30.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 (48) hide show
  1. package/dist/ai/index.d.ts +501 -0
  2. package/dist/ai/index.js +500 -0
  3. package/dist/chat-BPjXsoIl.d.ts +3158 -0
  4. package/dist/chunk-HD375J7S.js +128 -0
  5. package/dist/index.d.ts +6 -3143
  6. package/dist/index.js +45 -139
  7. package/dist/{jsx-runtime-DxGwoLu2.d.ts → jsx-runtime-CnDs8rPr.d.ts} +1 -1
  8. package/dist/jsx-runtime.d.ts +1 -1
  9. package/docs/adapters.mdx +35 -5
  10. package/docs/ai/ai-sdk-tools.mdx +227 -0
  11. package/docs/ai/index.mdx +69 -0
  12. package/docs/ai/meta.json +4 -0
  13. package/docs/{api → ai}/to-ai-messages.mdx +16 -3
  14. package/docs/ai/types.mdx +243 -0
  15. package/docs/api/chat.mdx +50 -10
  16. package/docs/api/index.mdx +4 -6
  17. package/docs/api/message.mdx +1 -1
  18. package/docs/api/meta.json +0 -1
  19. package/docs/api/postable-message.mdx +3 -3
  20. package/docs/api/thread.mdx +1 -1
  21. package/docs/concurrency.mdx +54 -15
  22. package/docs/contributing/building.mdx +1 -1
  23. package/docs/contributing/testing.mdx +4 -0
  24. package/docs/direct-messages.mdx +10 -1
  25. package/docs/files.mdx +20 -0
  26. package/docs/getting-started.mdx +5 -1
  27. package/docs/handling-events.mdx +10 -7
  28. package/docs/index.mdx +4 -1
  29. package/docs/meta.json +4 -0
  30. package/docs/posting-messages.mdx +3 -1
  31. package/docs/slack-primitives.mdx +320 -0
  32. package/docs/slash-commands.mdx +4 -4
  33. package/docs/streaming.mdx +4 -4
  34. package/docs/subject.mdx +1 -1
  35. package/docs/testing.mdx +142 -0
  36. package/docs/threads-messages-channels.mdx +1 -1
  37. package/docs/usage.mdx +8 -4
  38. package/package.json +23 -2
  39. package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +5 -1
  40. package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +5 -1
  41. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +1 -1
  42. package/resources/guides/human-in-the-loop-with-chat-sdk-and-workflow-sdk.md +176 -0
  43. package/resources/guides/liveblocks-chat-sdk-ai-sdk.md +165 -0
  44. package/resources/guides/run-and-track-deploys-from-slack.md +7 -5
  45. package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +5 -1
  46. package/resources/guides/slack-bot-vercel-blob.md +254 -0
  47. package/resources/guides/triage-form-submissions-with-chat-sdk.md +3 -1
  48. package/resources/templates.json +5 -0
@@ -16,9 +16,10 @@ Chat SDK uses an event-driven architecture. You register handlers for different
16
16
 
17
17
  When a message arrives, the SDK evaluates handlers in this order:
18
18
 
19
- 1. **Subscribed threads** — if the thread is subscribed, `onSubscribedMessage` fires and no other message handler runs.
20
- 2. **Mentions** — if the bot is @-mentioned in an unsubscribed thread, `onNewMention` fires.
21
- 3. **Pattern matches** — if the message text matches any `onNewMessage` regex patterns, those handlers fire.
19
+ 1. **Direct messages** — if the thread is a DM and any `onDirectMessage` handlers are registered, they fire before `onSubscribedMessage`, `onNewMention`, and pattern handlers.
20
+ 2. **Subscribed threads** — if the thread is subscribed, `onSubscribedMessage` fires and no other message handler runs. DMs only reach this step when no `onDirectMessage` handlers are registered.
21
+ 3. **Mentions** — if the bot is @-mentioned in an unsubscribed thread, `onNewMention` fires. Unsubscribed DMs without direct handlers are treated as mentions for backward compatibility.
22
+ 4. **Pattern matches** — if the message text matches any `onNewMessage` regex patterns, those handlers fire.
22
23
 
23
24
  Reactions, slash commands, actions, and modals have their own dedicated routing and are not affected by subscription state.
24
25
 
@@ -68,7 +69,9 @@ bot.onNewMention(async (thread, message) => {
68
69
 
69
70
  ## Handling subscribed messages
70
71
 
71
- `onSubscribedMessage` fires for every new message in a thread your bot has subscribed to. Once subscribed, all messages (including @-mentions) route here instead of `onNewMention`.
72
+ `onSubscribedMessage` fires for every new message in a non-DM thread your bot has subscribed to. Once subscribed, messages (including @-mentions) route here instead of `onNewMention`.
73
+
74
+ If an `onDirectMessage` handler is registered, DM messages route there before subscription routing. Without a direct handler, subscribed DMs route to `onSubscribedMessage`.
72
75
 
73
76
  ```typescript title="lib/bot.ts" lineNumbers
74
77
  bot.onSubscribedMessage(async (thread, message) => {
@@ -94,7 +97,7 @@ Messages sent by the bot itself do not trigger this handler. You don't need to f
94
97
  ### Example: Conversational AI with history
95
98
 
96
99
  ```typescript title="lib/bot.ts" lineNumbers
97
- import { toAiMessages } from "chat";
100
+ import { toAiMessages } from "chat/ai";
98
101
 
99
102
  bot.onSubscribedMessage(async (thread, message) => {
100
103
  await thread.startTyping();
@@ -111,7 +114,7 @@ bot.onSubscribedMessage(async (thread, message) => {
111
114
  });
112
115
  ```
113
116
 
114
- See [`toAiMessages`](/docs/api/to-ai-messages) for all options including multi-user name prefixing, message transforms, and attachment handling.
117
+ See [`toAiMessages`](/docs/ai/to-ai-messages) for all options including multi-user name prefixing, message transforms, and attachment handling.
115
118
 
116
119
  ### Example: Unsubscribe on keyword
117
120
 
@@ -299,7 +302,7 @@ These handlers are specific to the Slack platform and require the Slack adapter.
299
302
 
300
303
  ### Handling assistant threads
301
304
 
302
- `onAssistantThreadStarted` fires when a user opens a new assistant thread in Slack. Use it with the [Slack Assistants API](/adapters/slack#slack-assistants-api) to set suggested prompts and status indicators.
305
+ `onAssistantThreadStarted` fires when a user opens a new assistant thread in Slack. Use it with the [Slack Assistants API](/adapters/official/slack#slack-assistants-api) to set suggested prompts and status indicators.
303
306
 
304
307
  ```typescript title="lib/bot.ts" lineNumbers
305
308
  bot.onAssistantThreadStarted(async (event) => {
package/docs/index.mdx CHANGED
@@ -55,10 +55,11 @@ Each adapter factory auto-detects credentials from environment variables (`SLACK
55
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
- | Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Post+Edit | Yes |
58
+ | Telegram | `@chat-adapter/telegram` | Yes | Yes | Partial | No | Private chat drafts / Post+Edit | Yes |
59
59
  | GitHub | `@chat-adapter/github` | Yes | Yes | No | No | Buffered | No |
60
60
  | Linear | `@chat-adapter/linear` | Yes | Yes | No | No | Agent sessions / Post+Edit | No |
61
61
  | WhatsApp | `@chat-adapter/whatsapp` | N/A | Yes | Partial | No | Buffered | Yes |
62
+ | Twilio | `@chat-adapter/twilio` | N/A | No | Fallback | No | Buffered | Yes |
62
63
  | Messenger | `@chat-adapter/messenger` | Yes | Receive-only | Partial | No | Buffered | Yes |
63
64
 
64
65
  ## AI coding agent support
@@ -78,6 +79,7 @@ The SDK is distributed as a set of packages you install based on your needs:
78
79
  | Package | Description |
79
80
  |---------|-------------|
80
81
  | `chat` | Core SDK with `Chat` class, types, JSX runtime, and utilities |
82
+ | `chat/ai` | [AI utilities](/docs/ai) — [`createChatTools`](/docs/ai/ai-sdk-tools) for agent operations and [`toAiMessages`](/docs/ai/to-ai-messages) for converting chat history into AI SDK prompts |
81
83
  | `@chat-adapter/slack` | Slack adapter |
82
84
  | `@chat-adapter/teams` | Microsoft Teams adapter |
83
85
  | `@chat-adapter/gchat` | Google Chat adapter |
@@ -86,6 +88,7 @@ The SDK is distributed as a set of packages you install based on your needs:
86
88
  | `@chat-adapter/github` | GitHub Issues adapter |
87
89
  | `@chat-adapter/linear` | Linear Issues adapter |
88
90
  | `@chat-adapter/whatsapp` | WhatsApp Business adapter |
91
+ | `@chat-adapter/twilio` | Twilio SMS and MMS adapter |
89
92
  | `@chat-adapter/messenger` | Facebook Messenger adapter |
90
93
  | `@chat-adapter/state-redis` | Redis state adapter (production) |
91
94
  | `@chat-adapter/state-ioredis` | ioredis state adapter (alternative) |
package/docs/meta.json CHANGED
@@ -9,8 +9,12 @@
9
9
  "handling-events",
10
10
  "posting-messages",
11
11
  "error-handling",
12
+ "testing",
13
+ "---AI---",
14
+ "...ai",
12
15
  "---Adapters---",
13
16
  "adapters",
17
+ "slack-primitives",
14
18
  "state",
15
19
  "---Messaging---",
16
20
  "streaming",
@@ -151,7 +151,7 @@ await thread.post(result.fullStream);
151
151
 
152
152
  Both `fullStream` and `textStream` are supported. Use `fullStream` with multi-step agents — it preserves paragraph breaks between steps. Any `AsyncIterable<string>` also works for custom streams.
153
153
 
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.
154
+ For multi-turn conversations, use [`toAiMessages()`](/docs/ai/to-ai-messages) to convert thread history into the `{ role, content }[]` format expected by AI SDKs.
155
155
 
156
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
157
 
@@ -168,6 +168,8 @@ await thread.post({
168
168
  });
169
169
  ```
170
170
 
171
+ Use `attachments` on `{ raw }`, `{ markdown }`, or `{ ast }` when an adapter supports typed media uploads, such as Telegram's single image/audio/video/file upload support.
172
+
171
173
  See the [Files](/docs/files) page for more on attachments.
172
174
 
173
175
  ## Choosing a format
@@ -0,0 +1,320 @@
1
+ ---
2
+ title: Slack Low-Level APIs
3
+ description: Use Slack request verification, formatting, Web API, and Block Kit helpers without the full Chat runtime.
4
+ type: guide
5
+ prerequisites:
6
+ - /adapters/official/slack
7
+ related:
8
+ - /docs/handling-events
9
+ - /docs/cards
10
+ - /docs/slash-commands
11
+ ---
12
+
13
+ The Slack adapter is the right default for most bots. It verifies requests, resolves tokens, parses Slack payloads, stores thread state, and routes events through `Chat`.
14
+
15
+ Use the low-level Slack subpaths when your app already owns routing, state, sessions, or workflow execution and only needs the Slack-specific primitives.
16
+
17
+ | Subpath | Use for |
18
+ |---------|---------|
19
+ | `@chat-adapter/slack/webhook` | Request verification, body parsing, Events API payloads, slash commands, interactions, and continuation data |
20
+ | `@chat-adapter/slack/format` | Slack mrkdwn tokens, text objects, dates, links, mentions, and simple mrkdwn to Markdown conversion |
21
+ | `@chat-adapter/slack/api` | Fetch-based Slack Web API calls, thread replies, views, and files without `@slack/web-api` |
22
+ | `@chat-adapter/slack/blocks` | Runtime-free conversion from simple card objects and input requests to Slack Block Kit |
23
+
24
+ <Callout type="info">
25
+ These subpaths are for custom runtimes. If you want Chat SDK to handle webhook routing, state, subscriptions, and platform normalization, use `createSlackAdapter` from `@chat-adapter/slack`.
26
+ </Callout>
27
+
28
+ ## Webhooks
29
+
30
+ [Slack signs incoming HTTP requests](https://docs.slack.dev/authentication/verifying-requests-from-slack/) with `x-slack-signature` and `x-slack-request-timestamp`. `verifySlackRequest` reads the request body, verifies the signature with your signing secret, and returns the raw body so you can parse it once.
31
+
32
+ ```typescript title="app/api/slack/route.ts" lineNumbers
33
+ import {
34
+ parseSlackWebhookBody,
35
+ verifySlackRequest,
36
+ } from "@chat-adapter/slack/webhook";
37
+ import { postSlackMessage } from "@chat-adapter/slack/api";
38
+
39
+ export async function POST(request: Request) {
40
+ const body = await verifySlackRequest(request, {
41
+ signingSecret: process.env.SLACK_SIGNING_SECRET!,
42
+ });
43
+
44
+ const payload = parseSlackWebhookBody(body, {
45
+ contentType: request.headers.get("content-type"),
46
+ headers: request.headers,
47
+ });
48
+
49
+ if (payload.kind === "url_verification") {
50
+ return Response.json({ challenge: payload.challenge });
51
+ }
52
+
53
+ if (payload.kind === "app_mention") {
54
+ await postSlackMessage({
55
+ channel: payload.continuation.channelId,
56
+ markdownText: `received: ${payload.text}`,
57
+ threadTs: payload.continuation.threadTs,
58
+ token: process.env.SLACK_BOT_TOKEN!,
59
+ });
60
+ }
61
+
62
+ return new Response(null, { status: 200 });
63
+ }
64
+ ```
65
+
66
+ [Slack slash commands](https://docs.slack.dev/interactivity/implementing-slash-commands/) and interactions should be acknowledged quickly. Slack documents a 3000 ms acknowledgement window for slash commands, so do slow work in your queue or workflow runtime after returning a 2xx response.
67
+
68
+ If you do not need direct access to the verified raw body, `readSlackWebhook` combines verification and parsing:
69
+
70
+ ```typescript
71
+ import { readSlackWebhook } from "@chat-adapter/slack/webhook";
72
+
73
+ const payload = await readSlackWebhook(request, {
74
+ signingSecret: process.env.SLACK_SIGNING_SECRET!,
75
+ });
76
+ ```
77
+
78
+ If your framework already buffered the request body, use `verifySlackSignature` with the raw body and headers, then pass that same body to `parseSlackWebhookBody`.
79
+
80
+ ### Payloads
81
+
82
+ `parseSlackWebhookBody` returns typed payloads:
83
+
84
+ | Kind | Slack surface |
85
+ |------|---------------|
86
+ | `url_verification` | Events API URL verification |
87
+ | `app_mention` | App mention events |
88
+ | `direct_message` | Direct message events |
89
+ | `slash_command` | Slash command form posts |
90
+ | `block_actions` | Button, select, and Block Kit action payloads |
91
+ | `block_suggestion` | External select suggestion payloads |
92
+ | `view_submission` | Modal submissions |
93
+ | `view_closed` | Modal close events |
94
+ | `unsupported` | Valid Slack payloads not normalized by this helper yet |
95
+
96
+ Message-like payloads include `continuation`, which contains provider-native reply context:
97
+
98
+ ```typescript
99
+ type SlackContinuation = {
100
+ channelId: string;
101
+ enterpriseId?: string;
102
+ teamId?: string;
103
+ threadTs: string;
104
+ };
105
+ ```
106
+
107
+ This is not a Chat SDK `Thread`. It is the durable Slack data you need to reply later with `@chat-adapter/slack/api`.
108
+
109
+ App mention and direct message payloads also include typed `files` parsed from Slack file objects. Each file keeps the raw Slack object plus common fields like `id`, `name`, `mimeType`, `size`, `url`, and `downloadUrl`.
110
+
111
+ Interaction payloads expose convenience fields from Slack's raw payload:
112
+
113
+ - `block_actions` includes `actions`, `messageBlocks`, `messagePromptBlock`, `messagePromptText`, `messageTs`, `triggerId`, `responseUrl`, `user`, and `continuation`
114
+ - `view_submission` includes `callbackId`, `privateMetadata`, `values`, `responseUrls`, and `user`
115
+
116
+ ## Formatting
117
+
118
+ Slack uses mrkdwn and special tokens for mentions, channels, dates, and links. The format subpath gives you small helpers for those strings.
119
+
120
+ The helper surface includes `escapeSlackText`, `unescapeSlackText`, `createSlackPlainText`, `createSlackMrkdwn`, `formatSlackUser`, `formatSlackChannel`, `formatSlackUserGroup`, `formatSlackSpecialMention`, `formatSlackLink`, `formatSlackDate`, and simple mrkdwn to Markdown normalization.
121
+
122
+ ```typescript title="format.ts" lineNumbers
123
+ import {
124
+ createSlackMrkdwn,
125
+ formatSlackDate,
126
+ formatSlackLink,
127
+ formatSlackUser,
128
+ slackMrkdwnToMarkdown,
129
+ } from "@chat-adapter/slack/format";
130
+
131
+ const text = createSlackMrkdwn(
132
+ `${formatSlackUser("U123")} approved ${formatSlackLink("https://example.com", "the deploy")}`
133
+ );
134
+
135
+ const when = formatSlackDate(
136
+ new Date("2026-05-27T12:00:00Z"),
137
+ "{date_short_pretty} at {time}",
138
+ "May 27 at 12:00"
139
+ );
140
+
141
+ const markdown = slackMrkdwnToMarkdown("hello <@U123|jane>, see <https://example.com|this>");
142
+ ```
143
+
144
+ `linkBareSlackMentions` only links Slack user IDs like `@U123`. It does not resolve display names, because Slack mentions are ID-based.
145
+
146
+ ## Web API
147
+
148
+ The API subpath calls [Slack Web API](https://docs.slack.dev/apis/web-api/) methods with `fetch`. It does not import `@slack/web-api`.
149
+
150
+ ```typescript title="slack.ts" lineNumbers
151
+ import {
152
+ postSlackMessage,
153
+ sendSlackResponseUrl,
154
+ updateSlackMessage,
155
+ } from "@chat-adapter/slack/api";
156
+
157
+ const posted = await postSlackMessage({
158
+ channel: "C123",
159
+ markdownText: "**hello**",
160
+ token: process.env.SLACK_BOT_TOKEN!,
161
+ });
162
+
163
+ await updateSlackMessage({
164
+ channel: "C123",
165
+ text: "updated",
166
+ token: process.env.SLACK_BOT_TOKEN!,
167
+ ts: posted.id,
168
+ });
169
+
170
+ await sendSlackResponseUrl("https://hooks.slack.com/actions/T/1/abc", {
171
+ replaceOriginal: true,
172
+ text: "done",
173
+ });
174
+ ```
175
+
176
+ Use `callSlackApi` when you need a Slack method that does not have a helper yet:
177
+
178
+ ```typescript
179
+ import { callSlackApi } from "@chat-adapter/slack/api";
180
+
181
+ const result = await callSlackApi(
182
+ "reactions.add",
183
+ { channel: "C123", name: "white_check_mark", timestamp: "1710000000.000001" },
184
+ { token: process.env.SLACK_BOT_TOKEN! }
185
+ );
186
+ ```
187
+
188
+ `markdownText` maps to the `markdown_text` field on [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage/) and cannot be combined with `text` or `blocks`. Use `text` with `blocks` when you need fallback text.
189
+
190
+ The subpath also includes `postSlackEphemeral`, `deleteSlackMessage`, `resolveSlackBotToken`, `encodeSlackApiBody`, and `assertSlackOk`.
191
+
192
+ Use `fetchSlackThreadReplies` when a custom runtime needs to refresh a thread with [`conversations.replies`](https://docs.slack.dev/reference/methods/conversations.replies/):
193
+
194
+ ```typescript
195
+ import { fetchSlackThreadReplies } from "@chat-adapter/slack/api";
196
+
197
+ const replies = await fetchSlackThreadReplies({
198
+ channel: payload.continuation.channelId,
199
+ limit: 50,
200
+ token: process.env.SLACK_BOT_TOKEN!,
201
+ ts: payload.continuation.threadTs,
202
+ });
203
+ ```
204
+
205
+ Use `openSlackView` to open a modal from an interaction `trigger_id`:
206
+
207
+ ```typescript
208
+ import { openSlackView } from "@chat-adapter/slack/api";
209
+
210
+ await openSlackView({
211
+ token: process.env.SLACK_BOT_TOKEN!,
212
+ triggerId: payload.triggerId,
213
+ view: {
214
+ type: "modal",
215
+ title: { type: "plain_text", text: "Answer" },
216
+ blocks: [],
217
+ },
218
+ });
219
+ ```
220
+
221
+ ### Files
222
+
223
+ [Slack's current external upload flow](https://docs.slack.dev/changelog/2024-04-a-better-way-to-upload-files-is-here-to-stay) uses `files.getUploadURLExternal`, then uploads bytes to the returned URL, then calls `files.completeUploadExternal`.
224
+
225
+ ```typescript
226
+ import { uploadSlackFiles } from "@chat-adapter/slack/api";
227
+
228
+ await uploadSlackFiles(
229
+ [{ data: new Uint8Array([1, 2, 3]), filename: "report.txt" }],
230
+ {
231
+ channelId: "C123",
232
+ initialComment: "report attached",
233
+ token: process.env.SLACK_BOT_TOKEN!,
234
+ }
235
+ );
236
+ ```
237
+
238
+ Use `fetchSlackFile` for private Slack file URLs that require bearer token authorization.
239
+
240
+ ## Blocks
241
+
242
+ The blocks subpath converts simple card objects into Slack Block Kit without importing the full `chat` JSX runtime.
243
+
244
+ It exports `cardToSlackBlocks`, `cardToBlockKit`, `cardToSlackFallbackText`, `cardToFallbackText`, and `convertSlackEmojiPlaceholders`.
245
+
246
+ ```typescript title="blocks.ts" lineNumbers
247
+ import {
248
+ cardToSlackBlocks,
249
+ cardToSlackFallbackText,
250
+ } from "@chat-adapter/slack/blocks";
251
+ import { postSlackMessage } from "@chat-adapter/slack/api";
252
+
253
+ const card = {
254
+ children: [
255
+ { content: "deploy v2.4.1?", type: "text" },
256
+ {
257
+ children: [
258
+ { id: "approve", label: "Approve", style: "primary", type: "button" },
259
+ { id: "deny", label: "Deny", style: "danger", type: "button" },
260
+ ],
261
+ type: "actions",
262
+ },
263
+ ],
264
+ title: "Deployment",
265
+ type: "card",
266
+ } as const;
267
+
268
+ await postSlackMessage({
269
+ blocks: cardToSlackBlocks(card),
270
+ channel: "C123",
271
+ text: cardToSlackFallbackText(card),
272
+ token: process.env.SLACK_BOT_TOKEN!,
273
+ });
274
+ ```
275
+
276
+ Use the full Chat SDK card JSX when you want cross-platform rendering. Use `@chat-adapter/slack/blocks` when you are building a Slack-only runtime and want Block Kit output directly.
277
+
278
+ The blocks subpath also includes small input request helpers for Slack-only runtimes:
279
+
280
+ ```typescript
281
+ import {
282
+ inputRequestToSlackBlocks,
283
+ parseSlackInputResponse,
284
+ } from "@chat-adapter/slack/blocks";
285
+ import { postSlackMessage } from "@chat-adapter/slack/api";
286
+
287
+ await postSlackMessage({
288
+ blocks: inputRequestToSlackBlocks({
289
+ options: [
290
+ { id: "approve", label: "Approve", style: "primary" },
291
+ { id: "deny", label: "Deny", style: "danger" },
292
+ ],
293
+ prompt: "Approve deploy?",
294
+ requestId: "deploy-1",
295
+ }),
296
+ channel: "C123",
297
+ text: "Approve deploy?",
298
+ token: process.env.SLACK_BOT_TOKEN!,
299
+ });
300
+
301
+ if (payload.kind === "block_actions") {
302
+ const action = payload.actions[0];
303
+ const response = action ? parseSlackInputResponse(action) : null;
304
+ }
305
+ ```
306
+
307
+ Set `display: "radio"` for radio buttons, or `display: "select"` for a static select menu. Set `allowFreeform: true` to add a "Type your answer" button next to the provided options.
308
+
309
+ For freeform answers, use `buildSlackFreeformView` with `openSlackView`, then read the submitted value from `payload.values` with `parseSlackFreeformValue`.
310
+
311
+ ## Import boundaries
312
+
313
+ The low-level Slack subpaths are designed to avoid the full runtime import graph:
314
+
315
+ - no `chat` import
316
+ - no `@chat-adapter/shared` import
317
+ - no `@slack/web-api` import
318
+ - no `@slack/socket-mode` import
319
+
320
+ The package still installs the full Slack adapter dependencies. The subpaths keep your source and bundle imports clean, but they are not a package-size split.
@@ -6,13 +6,13 @@ prerequisites:
6
6
  - /docs/getting-started
7
7
  related:
8
8
  - /docs/modals
9
- - /adapters/slack
10
- - /adapters/discord
9
+ - /adapters/official/slack
10
+ - /adapters/official/discord
11
11
  ---
12
12
 
13
13
  Slash commands let users invoke your bot with `/command` syntax. Register handlers with `onSlashCommand` to respond.
14
14
 
15
- Slash commands are supported on [Slack](/adapters/slack) and [Discord](/adapters/discord).
15
+ Slash commands are supported on [Slack](/adapters/official/slack) and [Discord](/adapters/official/discord).
16
16
 
17
17
  ## Handle a specific command
18
18
 
@@ -114,7 +114,7 @@ bot.onModalSubmit("feedback_form", async (event) => {
114
114
 
115
115
  ## Discord
116
116
 
117
- Discord slash commands are received via [HTTP Interactions](/adapters/discord#architecture-http-interactions-vs-gateway) — no Gateway connection is needed. The adapter automatically sends a deferred response to Discord, then resolves it when your handler calls `event.channel.post()`.
117
+ Discord slash commands are received via [HTTP Interactions](/adapters/official/discord#http-interactions-vs-gateway) — no Gateway connection is needed. The adapter automatically sends a deferred response to Discord, then resolves it when your handler calls `event.channel.post()`.
118
118
 
119
119
  ### Subcommands
120
120
 
@@ -59,10 +59,10 @@ await thread.post(stream);
59
59
  | Platform | Method | Description |
60
60
  |----------|--------|-------------|
61
61
  | Slack | Native streaming API | Uses Slack's `chatStream` for smooth, real-time updates |
62
+ | Telegram | Private chat draft previews | Uses Telegram's `sendMessageDraft` in private chats and falls back to post + edit elsewhere |
62
63
  | Teams | Native (DMs) / Buffered (group chats) | Uses the Teams SDK's native `stream.emit()` for direct messages; accumulates chunks and posts one final message when no native streamer is active |
63
64
  | Google Chat | Post + Edit | Posts a message then edits it as chunks arrive |
64
65
  | Discord | Post + Edit | Posts a message then edits it as chunks arrive |
65
- | Telegram | Post + Edit | Posts a message then edits it as chunks arrive |
66
66
  | GitHub | Buffered | Accumulates chunks and posts one final comment |
67
67
  | Linear | Agent sessions / Post + Edit | Uses agent session activities in agent-session threads; falls back to post+edit comments in issue threads |
68
68
  | WhatsApp | Buffered | Accumulates chunks and sends one final message |
@@ -240,10 +240,10 @@ Adapters that don't support PostableObject editing (e.g. WhatsApp) render the pl
240
240
  ## Streaming with conversation history
241
241
 
242
242
  Combine message history with streaming for multi-turn AI conversations.
243
- Use [`toAiMessages()`](/docs/api/to-ai-messages) to convert chat messages into the `{ role, content }` format expected by AI SDKs:
243
+ Use [`toAiMessages()`](/docs/ai/to-ai-messages) to convert chat messages into the `{ role, content }` format expected by AI SDKs:
244
244
 
245
245
  ```typescript title="lib/bot.ts" lineNumbers
246
- import { toAiMessages } from "chat";
246
+ import { toAiMessages } from "chat/ai";
247
247
 
248
248
  bot.onSubscribedMessage(async (thread, message) => {
249
249
  // Fetch recent messages for context
@@ -256,4 +256,4 @@ bot.onSubscribedMessage(async (thread, message) => {
256
256
  });
257
257
  ```
258
258
 
259
- See the [`toAiMessages` API reference](/docs/api/to-ai-messages) for all options including `includeNames`, `transformMessage`, and attachment handling.
259
+ See the [`toAiMessages` reference](/docs/ai/to-ai-messages) for all options including `includeNames`, `transformMessage`, and attachment handling.
package/docs/subject.mdx CHANGED
@@ -50,4 +50,4 @@ bot.onNewMention(async (thread, message) => {
50
50
  });
51
51
  ```
52
52
 
53
- For anything beyond `message.subject`, access the platform's typed API client via [`bot.getAdapter(...).client`](/docs/api/chat#getadapter).
53
+ For anything beyond `message.subject`, access the platform's typed API client via [`bot.getAdapter("github").octokit`](/docs/api/chat#getadapter) or [`bot.getAdapter("linear").linearClient`](/docs/api/chat#getadapter).
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Testing
3
+ description: Test your bot handlers and custom adapters with @chat-adapter/tests — Vitest factories, custom matchers, and a setup file.
4
+ type: guide
5
+ prerequisites:
6
+ - /docs/getting-started
7
+ related:
8
+ - /docs/state
9
+ - /docs/handling-events
10
+ - /docs/contributing/testing
11
+ ---
12
+
13
+ The [`@chat-adapter/tests`](https://www.npmjs.com/package/@chat-adapter/tests) package gives you Vitest factories, custom matchers, and a setup file for testing bots and custom adapters built on Chat SDK.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add -D @chat-adapter/tests
19
+ ```
20
+
21
+ `chat` and `vitest` are peer dependencies — they should already be in your project.
22
+
23
+ ## Setup file (recommended)
24
+
25
+ Auto-register all matchers by adding the package's setup file to your Vitest config:
26
+
27
+ ```typescript title="vitest.config.ts" lineNumbers
28
+ import { defineConfig } from "vitest/config";
29
+
30
+ export default defineConfig({
31
+ test: {
32
+ setupFiles: ["@chat-adapter/tests/setup"],
33
+ },
34
+ });
35
+ ```
36
+
37
+ Without the setup file, register matchers manually:
38
+
39
+ ```typescript
40
+ import { matchers } from "@chat-adapter/tests/matchers";
41
+ expect.extend(matchers);
42
+ ```
43
+
44
+ ## Mock factories
45
+
46
+ ```typescript
47
+ import {
48
+ createMockAdapter,
49
+ createMockChatInstance,
50
+ createMockState,
51
+ createTestMessage,
52
+ mockLogger,
53
+ } from "@chat-adapter/tests";
54
+ ```
55
+
56
+ | Factory | Returns | Notes |
57
+ |---|---|---|
58
+ | `createMockAdapter(name?, overrides?)` | `Adapter` | Every method is `vi.fn()` with sensible defaults |
59
+ | `createMockChatInstance(options?)` | `ChatInstance` | Every `process*` handler is `vi.fn()`; `getState`/`getUserName`/`getLogger` wired up |
60
+ | `createMockState()` | `MockStateAdapter` | In-memory `Map`s for subscriptions, locks, KV, lists, queues; `cache` exposes the underlying map |
61
+ | `createTestMessage(id, text, overrides?)` | `Message` | Markdown text is parsed into the formatted AST |
62
+ | `mockLogger` / `createMockLogger()` | `Logger` | Shared default vs fresh-per-call |
63
+
64
+ ## Matchers
65
+
66
+ | Matcher | Asserts |
67
+ |---|---|
68
+ | `expect(adapter).toHavePosted(threadId, textPattern?)` | `adapter.postMessage` was called for this thread |
69
+ | `expect(adapter).toHaveEdited(threadId, messageId, textPattern?)` | `adapter.editMessage` was called for this message |
70
+ | `expect(adapter).toHaveDeleted(threadId, messageId)` | `adapter.deleteMessage` was called for this message |
71
+ | `expect(adapter).toHaveReactedWith(threadId, messageId, emoji)` | `adapter.addReaction` was called with the emoji (string or `EmojiValue.name`) |
72
+ | `expect(adapter).toHaveStartedTyping(threadId)` | `adapter.startTyping` was called for this thread |
73
+ | `expect(adapter).toHavePostedToChannel(channelId, textPattern?)` | `adapter.postChannelMessage` was called for this channel |
74
+ | `expect(chat).toHaveDispatched(handler)` | The named `process*` handler on the mock `ChatInstance` was called |
75
+ | `expect(state).toBeSubscribedTo(threadId)` | `state.isSubscribed(threadId)` resolves to `true` (async — `await expect(...)`) |
76
+
77
+ Text-pattern matchers extract a comparable string from `AdapterPostableMessage` — strings directly, `PostableMarkdown.markdown`, `PostableRaw.raw`, and `PostableCard.fallbackText`. AST-shaped messages and cards without `fallbackText` aren't text-matchable; assert without `textPattern` and inspect `mock.calls` directly.
78
+
79
+ ## Bot authors: test your handlers
80
+
81
+ When you're building a bot on top of Chat SDK, the kit lets you exercise your handlers without a real Slack/Teams/etc. webhook on the wire:
82
+
83
+ ```typescript title="bot.test.ts"
84
+ import { describe, expect, it } from "vitest";
85
+ import { Chat } from "chat";
86
+ import { createMockAdapter, createMockState } from "@chat-adapter/tests";
87
+
88
+ describe("bot handlers", () => {
89
+ it("replies with a greeting on mention", async () => {
90
+ const slack = createMockAdapter("slack");
91
+ const state = createMockState();
92
+ const bot = new Chat({
93
+ userName: "mybot",
94
+ adapters: { slack },
95
+ state,
96
+ });
97
+
98
+ bot.onNewMention(async (thread) => {
99
+ await thread.post("hello there");
100
+ });
101
+
102
+ // Drive a synthesized mention through the bot…
103
+ // (use your adapter's webhook path or a thread-level call)
104
+
105
+ expect(slack).toHavePosted("slack:C1:t1", /hello there/);
106
+ });
107
+ });
108
+ ```
109
+
110
+ ## Adapter authors: test webhook → dispatch
111
+
112
+ When you're building a custom `Adapter`, the kit gives you a `ChatInstance` mock you can hand to your adapter and assert that webhooks route through the right `process*` hook with the right normalized payload:
113
+
114
+ ```typescript title="adapter.test.ts"
115
+ import { describe, expect, it } from "vitest";
116
+ import { createMockChatInstance } from "@chat-adapter/tests";
117
+ import { MyAdapter } from "./adapter";
118
+
119
+ describe("MyAdapter.handleWebhook", () => {
120
+ it("dispatches incoming messages through processMessage", async () => {
121
+ const chat = createMockChatInstance();
122
+ const adapter = new MyAdapter({ /* config */ });
123
+ await adapter.initialize(chat);
124
+
125
+ const request = new Request("https://example.com/webhook", {
126
+ method: "POST",
127
+ body: JSON.stringify({ /* platform-specific payload */ }),
128
+ headers: { "content-type": "application/json" },
129
+ });
130
+ const response = await adapter.handleWebhook(request);
131
+
132
+ expect(response.status).toBe(200);
133
+ expect(chat).toHaveDispatched("processMessage");
134
+ });
135
+ });
136
+ ```
137
+
138
+ ## Adapter-specific helpers
139
+
140
+ Helpers that depend on a specific platform's wire format (signed Slack webhooks, Teams claim builders, etc.) live in each adapter's own `/testing` subpath rather than in this kit, so adopting `@chat-adapter/tests` doesn't pull in adapter dependencies you don't use.
141
+
142
+ If you're contributing adapters or core to this repo, see the [Testing adapters contributing guide](/docs/contributing/testing) for hand-rolled patterns used inside `packages/`.
@@ -40,7 +40,7 @@ await thread.post({
40
40
 
41
41
  ### Subscribe and unsubscribe
42
42
 
43
- Subscriptions persist across restarts (stored in your state adapter). When a thread is subscribed, all messages route to `onSubscribedMessage`.
43
+ Subscriptions persist across restarts (stored in your state adapter). When a non-DM thread is subscribed, all messages route to `onSubscribedMessage`. DM threads route to `onDirectMessage` first when a direct message handler is registered.
44
44
 
45
45
  ```typescript title="lib/bot.ts" lineNumbers
46
46
  await thread.subscribe();