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
@@ -15,6 +15,25 @@ import {
15
15
  } from "chat";
16
16
  ```
17
17
 
18
+ ## Type re-exports
19
+
20
+ The chat package re-exports mdast's union and content types so adapters and downstream code can build exhaustively-typed AST walkers without depending on `mdast` directly:
21
+
22
+ ```typescript
23
+ import type { Nodes, Root, Content } from "chat";
24
+
25
+ function render(node: Nodes): string {
26
+ switch (node.type) {
27
+ case "text": return node.value;
28
+ case "strong": return node.children.map(render).join("");
29
+ // ...
30
+ default: throw new Error(`Unhandled: ${node satisfies never}`);
31
+ }
32
+ }
33
+ ```
34
+
35
+ Adapters use this pattern to make the type checker reject the build when a new mdast node type is introduced upstream.
36
+
18
37
  ## Node builders
19
38
 
20
39
  ### root
@@ -222,7 +241,7 @@ const value = getNodeValue(node); // string | undefined
222
241
 
223
242
  ### tableToAscii
224
243
 
225
- Render an mdast `Table` node as a padded ASCII table string. Used by adapters that lack native table support (Slack, Google Chat, Discord, Telegram).
244
+ Render an mdast `Table` node as a padded ASCII table string. Used by adapters that lack native table support (Google Chat, Discord, Telegram).
226
245
 
227
246
  ```typescript
228
247
  import { parseMarkdown, tableToAscii, isTableNode } from "chat";
@@ -261,17 +280,21 @@ The SDK uses mdast as the canonical format and each adapter converts it to the p
261
280
 
262
281
  | Feature | Slack | Teams | Google Chat |
263
282
  |---------|-------|-------|-------------|
264
- | Bold | `*text*` | `**text**` | `*text*` |
283
+ | Bold | `**text**` | `**text**` | `*text*` |
265
284
  | Italic | `_text_` | `_text_` | `_text_` |
266
- | Strikethrough | `~text~` | `~~text~~` | `~text~` |
285
+ | Strikethrough | `~~text~~` | `~~text~~` | `~text~` |
267
286
  | Code | `` `code` `` | `` `code` `` | `` `code` `` |
268
287
  | Code blocks | ```` ``` ```` | ```` ``` ```` | ```` ``` ```` |
269
- | Links | `<url\|text>` | `[text](url)` | `[text](url)` |
288
+ | Links | `[text](url)` | `[text](url)` | `[text](url)` |
270
289
  | Lists | Supported | Supported | Supported |
271
290
  | Blockquotes | `>` | `>` | Simulated with `>` prefix |
272
- | Tables | ASCII fallback | Native GFM | ASCII fallback |
291
+ | Tables | Native (markdown_text) | Native GFM | ASCII fallback |
273
292
  | Mentions | `<@USER>` | `<at>name</at>` | `<users/{id}>` |
274
293
 
294
+ <Callout type="info">
295
+ Slack accepts standard markdown via the `markdown_text` field on `chat.postMessage` and friends, so the SDK passes markdown through directly. Incoming Slack messages still arrive as legacy mrkdwn (`*bold*`, `<url|text>`) and are parsed transparently. If you need to send mrkdwn yourself, use `{ raw: "..." }`.
296
+ </Callout>
297
+
275
298
  <Callout type="info">
276
299
  You don't need to worry about these differences when using the SDK — the AST builders and `parseMarkdown` handle conversion automatically. This table is useful if you're working with `raw` platform payloads or debugging formatting issues.
277
300
  </Callout>
@@ -54,6 +54,10 @@ import { Message } from "chat";
54
54
  description: 'Whether the bot was @-mentioned in this message.',
55
55
  type: 'boolean | undefined',
56
56
  },
57
+ subject: {
58
+ description: 'Resolves the parent resource (issue, PR) this message is about. Returns null on chat platforms. See [Message Subject](/docs/subject).',
59
+ type: 'Promise<MessageSubject | null>',
60
+ },
57
61
  }}
58
62
  />
59
63
 
@@ -149,6 +153,10 @@ All adapters return `false` if the bot ID isn't known yet. This is a safe defaul
149
153
  description: 'Fetch the attachment data. Handles platform auth automatically.',
150
154
  type: '() => Promise<Buffer> | undefined',
151
155
  },
156
+ fetchMetadata: {
157
+ description: 'Platform-specific IDs for reconstructing fetchData after serialization (e.g. WhatsApp mediaId, Telegram fileId).',
158
+ type: 'Record<string, string> | undefined',
159
+ },
152
160
  }}
153
161
  />
154
162
 
@@ -196,6 +204,55 @@ When using [`toAiMessages()`](/docs/api/to-ai-messages), link metadata is automa
196
204
  | Slack | URLs from `rich_text` blocks or `<url>` text patterns | Slack message links (`*.slack.com/archives/...`) |
197
205
  | Others | Not yet — `links` is always `[]` | — |
198
206
 
207
+ ## MessageSubject
208
+
209
+ Returned by `message.subject` on platforms with parent resources. See [Message Subject](/docs/subject) for usage.
210
+
211
+ <TypeTable
212
+ type={{
213
+ type: {
214
+ description: 'Resource kind, e.g. "issue" or "pull_request".',
215
+ type: 'string',
216
+ },
217
+ id: {
218
+ description: 'Resource identifier (e.g. "ENG-123" or "42").',
219
+ type: 'string',
220
+ },
221
+ title: {
222
+ description: 'Resource title.',
223
+ type: 'string | undefined',
224
+ },
225
+ description: {
226
+ description: 'Full description/body in markdown.',
227
+ type: 'string | undefined',
228
+ },
229
+ status: {
230
+ description: 'Current status (e.g. "In Progress", "open").',
231
+ type: 'string | undefined',
232
+ },
233
+ url: {
234
+ description: 'Web URL to the resource.',
235
+ type: 'string | undefined',
236
+ },
237
+ author: {
238
+ description: 'Resource creator.',
239
+ type: '{ id: string; name: string } | undefined',
240
+ },
241
+ assignee: {
242
+ description: 'Current assignee.',
243
+ type: '{ id: string; name: string } | undefined',
244
+ },
245
+ labels: {
246
+ description: 'Labels/tags.',
247
+ type: 'string[] | undefined',
248
+ },
249
+ raw: {
250
+ description: 'Full platform API response.',
251
+ type: 'unknown',
252
+ },
253
+ }}
254
+ />
255
+
199
256
  ## Serialization
200
257
 
201
258
  Messages can be serialized for workflow engines and external systems.
@@ -208,4 +265,4 @@ const json = message.toJSON();
208
265
  const restored = Message.fromJSON(json);
209
266
  ```
210
267
 
211
- The serialized format converts `Date` fields to ISO strings and omits non-serializable fields like `data` buffers and `fetchData` functions.
268
+ 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.
@@ -6,7 +6,9 @@
6
6
  "thread",
7
7
  "channel",
8
8
  "message",
9
+ "to-ai-messages",
9
10
  "postable-message",
11
+ "transcripts",
10
12
  "cards",
11
13
  "markdown",
12
14
  "modals"
@@ -54,6 +54,10 @@ bot.onAction("open-form", async (event) => {
54
54
  type: 'boolean',
55
55
  default: 'false',
56
56
  },
57
+ callbackUrl: {
58
+ description: 'URL to POST form values to when the modal is submitted.',
59
+ type: 'string',
60
+ },
57
61
  privateMetadata: {
58
62
  description: 'Arbitrary string passed through the modal lifecycle (e.g., JSON context).',
59
63
  type: 'string',
@@ -167,6 +171,52 @@ Select({
167
171
  }}
168
172
  />
169
173
 
174
+ ## ExternalSelect
175
+
176
+ Dropdown that loads options dynamically from a handler as the user types. Slack-only. Pair with [`bot.onOptionsLoad`](/docs/api/chat#onoptionsload) to supply options. See [Modals → ExternalSelect](/docs/modals#externalselect) for a full example, grouped-options support, and Slack setup notes.
177
+
178
+ ```typescript
179
+ ExternalSelect({
180
+ id: "assignee",
181
+ label: "Assignee",
182
+ placeholder: "Search people",
183
+ minQueryLength: 1,
184
+ initialOption: { label: "Alice", value: "U123" },
185
+ })
186
+ ```
187
+
188
+ <TypeTable
189
+ type={{
190
+ id: {
191
+ description: 'Input ID — used as the key in event.values.',
192
+ type: 'string',
193
+ },
194
+ label: {
195
+ description: 'Label displayed above the select.',
196
+ type: 'string',
197
+ },
198
+ placeholder: {
199
+ description: 'Placeholder text.',
200
+ type: 'string',
201
+ },
202
+ minQueryLength: {
203
+ description: 'Minimum characters before the loader fires (Slack default: 3).',
204
+ type: 'number',
205
+ },
206
+ initialOption: {
207
+ description: 'Pre-selected option when the modal opens. Unlike static Select where initialOption is a value string, ExternalSelect needs the full label/value object since the loader has not run yet.',
208
+ type: '{ label: string, value: string }',
209
+ },
210
+ optional: {
211
+ description: 'Whether the field can be left empty.',
212
+ type: 'boolean',
213
+ default: 'false',
214
+ },
215
+ }}
216
+ />
217
+
218
+ The loader registered via `bot.onOptionsLoad("assignee", handler)` returns either a flat `SelectOptionElement[]` or `OptionsLoadGroup[]` (`{ label, options }[]`) for grouped options.
219
+
170
220
  ## RadioSelect
171
221
 
172
222
  Radio button group for mutually exclusive choices.
@@ -7,9 +7,14 @@ type: reference
7
7
  `PostableMessage` is the union of all message formats accepted by `thread.post()` and `sent.edit()`.
8
8
 
9
9
  ```typescript
10
- type PostableMessage = AdapterPostableMessage | AsyncIterable<string | StreamChunk | StreamEvent>;
10
+ type PostableMessage =
11
+ | AdapterPostableMessage
12
+ | AsyncIterable<string | StreamChunk | StreamEvent>
13
+ | PostableObject;
11
14
  ```
12
15
 
16
+ `PostableObject` covers `Plan` (mutable task lists) and `StreamingPlan` (streams with platform-specific options) — both documented below.
17
+
13
18
  ## String
14
19
 
15
20
  Raw text passed through as-is to the platform.
@@ -133,6 +138,55 @@ await thread.post({
133
138
  }}
134
139
  />
135
140
 
141
+ ## Plan
142
+
143
+ A `Plan` is a step-by-step task list that mutates after posting. Each `addTask` / `updateTask` / `complete` call re-renders the same message in place. See [Plan API](/docs/streaming#plan-api) for full usage.
144
+
145
+ ```typescript
146
+ import { Plan } from "chat";
147
+
148
+ const plan = new Plan({ initialMessage: "Researching options..." });
149
+ await thread.post(plan);
150
+ await plan.addTask({ title: "Look up records" });
151
+ await plan.complete({ completeMessage: "Done!" });
152
+ ```
153
+
154
+ Adapters that don't support `PostableObject` editing render the plan as fallback text and ignore subsequent mutations.
155
+
156
+ ## StreamingPlan
157
+
158
+ Wraps an async iterable with platform-specific streaming options. Use this when you need to pass options like task grouping or stop blocks through `thread.post()`. See [Streaming with options](/docs/streaming#streaming-with-options).
159
+
160
+ ```typescript
161
+ import { StreamingPlan } from "chat";
162
+
163
+ const planned = new StreamingPlan(stream, {
164
+ groupTasks: "plan",
165
+ endWith: [feedbackBlock],
166
+ updateIntervalMs: 750,
167
+ });
168
+
169
+ await thread.post(planned);
170
+ ```
171
+
172
+ <TypeTable
173
+ type={{
174
+ groupTasks: {
175
+ description: 'Slack: render task_update chunks as `"plan"` (single grouped block) or `"timeline"` (inline cards, default).',
176
+ type: '"plan" | "timeline" | undefined',
177
+ },
178
+ endWith: {
179
+ description: 'Slack: Block Kit elements appended when the stream stops (e.g. retry / feedback buttons).',
180
+ type: 'unknown[] | undefined',
181
+ },
182
+ updateIntervalMs: {
183
+ description: 'Post+edit adapters: minimum interval between update cycles in ms.',
184
+ type: 'number | undefined',
185
+ default: '500',
186
+ },
187
+ }}
188
+ />
189
+
136
190
  ## AsyncIterable (streaming)
137
191
 
138
192
  An async iterable of strings, `StreamChunk` objects, or stream events. The SDK streams the message in real time using platform-native APIs where available.
@@ -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
 
@@ -39,7 +39,7 @@ A `Thread` is provided to your event handlers and represents a conversation thre
39
39
 
40
40
  ## post
41
41
 
42
- Post a message to the thread. Accepts strings, structured messages, cards, and streams.
42
+ Post a message to the thread. Accepts strings, structured messages, cards, streams, and `PostableObject` instances (`Plan`, `StreamingPlan`).
43
43
 
44
44
  ```typescript
45
45
  // Plain text
@@ -56,11 +56,19 @@ await thread.post(Card({ title: "Hi", children: [Text("Hello")] }));
56
56
 
57
57
  // Stream (fullStream recommended for multi-step agents)
58
58
  await thread.post(result.fullStream);
59
+
60
+ // Plan (mutable task list)
61
+ const plan = new Plan({ initialMessage: "Working..." });
62
+ await thread.post(plan);
63
+ await plan.addTask({ title: "Step 1" });
64
+
65
+ // Streaming with platform options
66
+ await thread.post(new StreamingPlan(stream, { groupTasks: "plan" }));
59
67
  ```
60
68
 
61
69
  **Parameters:** `message: string | PostableMessage | CardJSXElement`
62
70
 
63
- **Returns:** `Promise<SentMessage>` — the sent message with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods.
71
+ **Returns:** `Promise<SentMessage | PostableObject>` — for plain messages and streams, a `SentMessage` with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods; for `Plan` / `StreamingPlan` inputs, the same object is returned so you can keep mutating it.
64
72
 
65
73
  See [Posting Messages](/docs/posting-messages) for details on each format.
66
74
 
@@ -114,6 +122,28 @@ await scheduled.cancel();
114
122
  Streaming and file uploads are not supported in scheduled messages.
115
123
  </Callout>
116
124
 
125
+ ## getParticipants
126
+
127
+ 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.
128
+
129
+ ```typescript
130
+ const participants = await thread.getParticipants();
131
+
132
+ // Subscribe only when one person is talking to the bot
133
+ if (participants.length === 1) {
134
+ await thread.subscribe();
135
+ }
136
+
137
+ // Unsubscribe when the thread becomes a group conversation
138
+ if (participants.length > 1) {
139
+ await thread.unsubscribe();
140
+ }
141
+ ```
142
+
143
+ <Callout type="warn">
144
+ 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.
145
+ </Callout>
146
+
117
147
  ## subscribe / unsubscribe
118
148
 
119
149
  Manage thread subscriptions. Subscribed threads route all messages to `onSubscribedMessage` handlers.
@@ -0,0 +1,220 @@
1
+ ---
2
+ title: Transcripts
3
+ description: Cross-platform per-user transcript persistence — configuration, methods, and entry shape.
4
+ type: reference
5
+ ---
6
+
7
+ `bot.transcripts` provides per-user message persistence keyed by a stable cross-platform identifier. See the [Conversation history](/docs/conversation-history) guide for usage patterns.
8
+
9
+ ```typescript
10
+ import { Chat } from "chat";
11
+ ```
12
+
13
+ ## Configuration
14
+
15
+ `transcripts` and `identity` are configured on `ChatConfig`. Both must be set together — passing `transcripts` without `identity` throws at construction.
16
+
17
+ ### ChatConfig.transcripts
18
+
19
+ <TypeTable
20
+ type={{
21
+ retention: {
22
+ description: 'List TTL, refreshed on every append. Accepts ms or a duration string ("45s", "30m", "6h", "7d"). Omit for no expiry.',
23
+ type: 'number | DurationString | undefined',
24
+ },
25
+ maxPerUser: {
26
+ description: 'Hard cap per user. Older entries are evicted on append.',
27
+ type: 'number',
28
+ default: '200',
29
+ },
30
+ storeFormatted: {
31
+ description: 'Persist the mdast `formatted` field alongside `text`. Off by default to keep storage small.',
32
+ type: 'boolean',
33
+ default: 'false',
34
+ },
35
+ }}
36
+ />
37
+
38
+ ### ChatConfig.identity
39
+
40
+ ```typescript
41
+ identity: (context: IdentityContext) => string | null | Promise<string | null>;
42
+ ```
43
+
44
+ Called once per inbound message during dispatch. The result is attached to the `Message` instance as `message.userKey`. Return `null` to skip persistence for an event.
45
+
46
+ #### IdentityContext
47
+
48
+ <TypeTable
49
+ type={{
50
+ adapter: {
51
+ description: 'Adapter name (e.g. "slack", "discord").',
52
+ type: 'string',
53
+ },
54
+ author: {
55
+ description: 'Message author info.',
56
+ type: 'Author',
57
+ },
58
+ message: {
59
+ description: 'The inbound message.',
60
+ type: 'Message',
61
+ },
62
+ }}
63
+ />
64
+
65
+ ## Methods
66
+
67
+ Access via `bot.transcripts`. Throws if `transcripts` was not configured on the `Chat` instance.
68
+
69
+ ### append
70
+
71
+ Persist a `Message` (typically the inbound user message) or an `AppendInput` (typically a bot reply you just posted).
72
+
73
+ ```typescript
74
+ append(
75
+ thread: Postable,
76
+ message: Message | AppendInput,
77
+ options?: AppendOptions,
78
+ ): Promise<TranscriptEntry | null>;
79
+ ```
80
+
81
+ When `message` is a `Message`, `userKey` is read from the instance. If it's `undefined` (the resolver returned `null`), the call is a no-op and returns `null`. When `message` is an `AppendInput`, `options.userKey` is required.
82
+
83
+ #### AppendInput
84
+
85
+ <TypeTable
86
+ type={{
87
+ role: {
88
+ description: 'Role tag for the entry.',
89
+ type: '"user" | "assistant" | "system"',
90
+ },
91
+ text: {
92
+ description: 'Plain-text body.',
93
+ type: 'string',
94
+ },
95
+ formatted: {
96
+ description: 'Optional mdast AST. Only stored when `transcripts.storeFormatted` is true.',
97
+ type: 'FormattedContent | undefined',
98
+ },
99
+ platformMessageId: {
100
+ description: 'Platform-native message ID, when known.',
101
+ type: 'string | undefined',
102
+ },
103
+ }}
104
+ />
105
+
106
+ #### AppendOptions
107
+
108
+ <TypeTable
109
+ type={{
110
+ userKey: {
111
+ description: 'Required when appending an `AppendInput` (assistant or system role); ignored when appending a `Message`.',
112
+ type: 'string | undefined',
113
+ },
114
+ }}
115
+ />
116
+
117
+ ### list
118
+
119
+ Returns entries in chronological order (oldest first). When `limit` is set, returns the newest `N` entries — still chronologically.
120
+
121
+ ```typescript
122
+ list(query: ListQuery): Promise<TranscriptEntry[]>;
123
+ ```
124
+
125
+ #### ListQuery
126
+
127
+ <TypeTable
128
+ type={{
129
+ userKey: {
130
+ description: 'Cross-platform user key.',
131
+ type: 'string',
132
+ },
133
+ limit: {
134
+ description: 'Maximum entries returned. Cannot exceed `maxPerUser` because that is the storage cap.',
135
+ type: 'number',
136
+ default: '50',
137
+ },
138
+ platforms: {
139
+ description: 'Filter to a subset of adapter names.',
140
+ type: 'string[] | undefined',
141
+ },
142
+ threadId: {
143
+ description: 'Filter to a single thread.',
144
+ type: 'string | undefined',
145
+ },
146
+ roles: {
147
+ description: 'Filter to specific roles.',
148
+ type: '("user" | "assistant" | "system")[] | undefined',
149
+ },
150
+ }}
151
+ />
152
+
153
+ ### count
154
+
155
+ ```typescript
156
+ count(query: CountQuery): Promise<number>;
157
+ ```
158
+
159
+ Returns the total number of entries stored under the user key. `CountQuery` has a single field, `userKey: string`.
160
+
161
+ ### delete
162
+
163
+ ```typescript
164
+ delete(target: { userKey: string }): Promise<{ deleted: number }>;
165
+ ```
166
+
167
+ Wipes every entry stored under the user key. Returns the count that was removed. Single-entry and time-range deletes are not supported — the underlying `appendToList` primitive can't support them safely under concurrent writes.
168
+
169
+ ## TranscriptEntry
170
+
171
+ Returned by `append` and `list`.
172
+
173
+ <TypeTable
174
+ type={{
175
+ id: {
176
+ description: 'UUID assigned by the SDK at append time.',
177
+ type: 'string',
178
+ },
179
+ userKey: {
180
+ description: 'Cross-platform user key from the IdentityResolver.',
181
+ type: 'string',
182
+ },
183
+ role: {
184
+ description: 'Role tag.',
185
+ type: '"user" | "assistant" | "system"',
186
+ },
187
+ text: {
188
+ description: 'Plain-text body — canonical for prompt building.',
189
+ type: 'string',
190
+ },
191
+ formatted: {
192
+ description: 'mdast AST. Only present when `transcripts.storeFormatted` is true.',
193
+ type: 'FormattedContent | undefined',
194
+ },
195
+ platform: {
196
+ description: 'Originating adapter name.',
197
+ type: 'string',
198
+ },
199
+ threadId: {
200
+ description: 'Originating thread ID.',
201
+ type: 'string',
202
+ },
203
+ platformMessageId: {
204
+ description: 'Platform-native message ID, when known.',
205
+ type: 'string | undefined',
206
+ },
207
+ timestamp: {
208
+ description: 'ms-since-epoch, set at append time on the SDK side.',
209
+ type: 'number',
210
+ },
211
+ }}
212
+ />
213
+
214
+ ## Storage
215
+
216
+ Backed by `StateAdapter.appendToList` / `getList` / `delete`. Every built-in state adapter (`memory`, `redis`, `ioredis`, `pg`) supports these primitives.
217
+
218
+ Entries are stored under `transcripts:user:{userKey}` as a capped list. `appendToList` is atomic, so concurrent inbound messages don't race.
219
+
220
+ The `retention` value is applied as the list TTL and refreshed on every append. With `retention: "30d"`, a user who hasn't talked to the bot in 30 days has their transcript expire automatically.
package/docs/cards.mdx CHANGED
@@ -115,6 +115,12 @@ Set `actionType="modal"` to indicate the button opens a [modal](/docs/modals). T
115
115
  <Button id="open-feedback" actionType="modal">Give Feedback</Button>
116
116
  ```
117
117
 
118
+ Optional `callbackUrl` causes the action data to be POSTed to a URL when clicked. See [Callback URLs](/docs/actions#callback-urls) for details.
119
+
120
+ ```tsx title="lib/bot.tsx"
121
+ <Button callbackUrl={webhook.url} id="approve" style="primary">Approve</Button>
122
+ ```
123
+
118
124
  ### CardLink
119
125
 
120
126
  Inline hyperlink rendered as text. Unlike `LinkButton` (which must be inside `Actions`), `CardLink` can be placed directly in a card alongside other content.
@@ -124,6 +124,10 @@ const bot = new Chat({
124
124
  | `debounceMs` | debounce | `1500` | Debounce window in milliseconds |
125
125
  | `maxConcurrent` | concurrent | `Infinity` | Max concurrent handlers per thread |
126
126
 
127
+ <Callout type="warn">
128
+ `maxConcurrent` only applies to the `concurrent` strategy. Pairing it with any other strategy logs a warning and the value is ignored. Setting `maxConcurrent` to a value less than `1` throws at construction time — `0` would deadlock the strategy and is rejected up front.
129
+ </Callout>
130
+
127
131
  ## MessageContext
128
132
 
129
133
  All handler types (`onNewMention`, `onSubscribedMessage`, `onNewMessage`) accept an optional `MessageContext` as their last parameter. It is only populated when using the `queue` strategy and messages were skipped.