chat 4.20.0 → 4.20.2

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.
@@ -20,6 +20,12 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat";
20
20
  | [`Message`](/docs/api/message) | Normalized message with text, AST, author, and metadata |
21
21
  | [`ScheduledMessage`](/docs/api/thread#scheduledmessage) | Returned by `thread.schedule()` / `channel.schedule()` with `cancel()` |
22
22
 
23
+ ## Utilities
24
+
25
+ | Export | Description |
26
+ |--------|-------------|
27
+ | [`toAiMessages`](/docs/streaming#toaimessagesmessages-options) | Convert `Message[]` to AI SDK `{ role, content }[]` format |
28
+
23
29
  ## Message formats
24
30
 
25
31
  | Export | Description |
@@ -46,6 +46,10 @@ import { Message } from "chat";
46
46
  description: 'File attachments.',
47
47
  type: 'Attachment[]',
48
48
  },
49
+ links: {
50
+ description: 'Links found in the message, with optional preview metadata.',
51
+ type: 'LinkPreview[]',
52
+ },
49
53
  isMention: {
50
54
  description: 'Whether the bot was @-mentioned in this message.',
51
55
  type: 'boolean | undefined',
@@ -148,6 +152,50 @@ All adapters return `false` if the bot ID isn't known yet. This is a safe defaul
148
152
  }}
149
153
  />
150
154
 
155
+ ## LinkPreview
156
+
157
+ Links found in incoming messages are extracted and exposed as `LinkPreview` objects. On platforms that support it (currently Slack), links pointing to other chat messages include a `fetchMessage()` callback to retrieve the full linked message.
158
+
159
+ <TypeTable
160
+ type={{
161
+ url: {
162
+ description: 'The URL.',
163
+ type: 'string',
164
+ },
165
+ title: {
166
+ description: 'Title from unfurl metadata (if available).',
167
+ type: 'string | undefined',
168
+ },
169
+ description: {
170
+ description: 'Description from unfurl metadata (if available).',
171
+ type: 'string | undefined',
172
+ },
173
+ imageUrl: {
174
+ description: 'Preview image URL (if available).',
175
+ type: 'string | undefined',
176
+ },
177
+ siteName: {
178
+ description: 'Site name, e.g. "Vercel" (if available).',
179
+ type: 'string | undefined',
180
+ },
181
+ 'fetchMessage()': {
182
+ description: 'Fetch the linked chat message. Available when the URL points to a message on the same platform (e.g. a Slack message link).',
183
+ type: '() => Promise<Message> | undefined',
184
+ },
185
+ }}
186
+ />
187
+
188
+ <Callout type="info">
189
+ When using [`toAiMessages()`](/docs/streaming#toaimessagesmessages-options), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context.
190
+ </Callout>
191
+
192
+ ### Platform support
193
+
194
+ | Platform | Link extraction | `fetchMessage()` |
195
+ |----------|----------------|-------------------|
196
+ | Slack | URLs from `rich_text` blocks or `<url>` text patterns | Slack message links (`*.slack.com/archives/...`) |
197
+ | Others | Not yet — `links` is always `[]` | — |
198
+
151
199
  ## Serialization
152
200
 
153
201
  Messages can be serialized for workflow engines and external systems.
@@ -94,18 +94,18 @@ Messages sent by the bot itself do not trigger this handler. You don't need to f
94
94
  ### Example: Conversational AI with history
95
95
 
96
96
  ```typescript title="lib/bot.ts" lineNumbers
97
+ import { toAiMessages } from "chat";
98
+
97
99
  bot.onSubscribedMessage(async (thread, message) => {
98
100
  await thread.startTyping();
99
101
 
100
102
  // Build conversation history from thread messages
101
- const history = [];
103
+ const messages = [];
102
104
  for await (const msg of thread.allMessages) {
103
- history.push({
104
- role: msg.author.isMe ? "assistant" : "user",
105
- content: msg.text,
106
- });
105
+ messages.push(msg);
107
106
  }
108
107
 
108
+ const history = await toAiMessages(messages);
109
109
  const response = await generateAIResponse(history);
110
110
  await thread.post(response);
111
111
  });
@@ -183,21 +183,68 @@ await thread.stream(textStream, {
183
183
 
184
184
  ## Streaming with conversation history
185
185
 
186
- Combine message history with streaming for multi-turn AI conversations:
186
+ Combine message history with streaming for multi-turn AI conversations.
187
+ Use `toAiMessages()` to convert chat messages into the `{ role, content }` format expected by AI SDKs:
187
188
 
188
189
  ```typescript title="lib/bot.ts" lineNumbers
190
+ import { toAiMessages } from "chat";
191
+
189
192
  bot.onSubscribedMessage(async (thread, message) => {
190
193
  // Fetch recent messages for context
191
194
  const result = await thread.adapter.fetchMessages(thread.id, { limit: 20 });
192
195
 
193
- const history = result.messages
194
- .filter((msg) => msg.text.trim())
195
- .map((msg) => ({
196
- role: msg.author.isMe ? "assistant" as const : "user" as const,
197
- content: msg.text,
198
- }));
196
+ const history = await toAiMessages(result.messages);
199
197
 
200
198
  const response = await agent.stream({ prompt: history });
201
199
  await thread.post(response.fullStream);
202
200
  });
203
201
  ```
202
+
203
+ ### `toAiMessages(messages, options?)`
204
+
205
+ Converts an array of `Message` objects into AI SDK conversation format:
206
+
207
+ - Maps `author.isMe` to `"assistant"` role, all others to `"user"`
208
+ - Filters out empty messages
209
+ - Sorts chronologically (oldest first)
210
+ - Appends link metadata (URLs, titles, descriptions) when present
211
+ - Labels embedded message links (e.g. shared Slack messages) as `[Embedded message: ...]`
212
+
213
+ | Option | Type | Default | Description |
214
+ |--------|------|---------|-------------|
215
+ | `includeNames` | `boolean` | `false` | Prefix user messages with `[username]: ` for multi-user context |
216
+ | `transformMessage` | `(aiMessage, source) => AiMessage \| Promise<AiMessage \| null> \| null` | — | Transform or filter each message after default processing. Return `null` to skip. |
217
+ | `onUnsupportedAttachment` | `(attachment, message) => void` | `console.warn` | Called when an attachment type is not supported |
218
+
219
+ ### Customizing messages with `transformMessage`
220
+
221
+ Use `transformMessage` to modify, enrich, or filter messages after default processing:
222
+
223
+ ```typescript title="lib/bot.ts" lineNumbers
224
+ import { toAiMessages } from "chat";
225
+
226
+ const history = await toAiMessages(result.messages, {
227
+ transformMessage: (aiMessage, source) => {
228
+ // Replace bot user IDs with readable names
229
+ if (typeof aiMessage.content === "string") {
230
+ return {
231
+ ...aiMessage,
232
+ content: aiMessage.content.replace(/<@U123>/g, "@VercelBot"),
233
+ };
234
+ }
235
+ return aiMessage;
236
+ },
237
+ });
238
+ ```
239
+
240
+ Return `null` to skip a message entirely:
241
+
242
+ ```typescript
243
+ const history = await toAiMessages(result.messages, {
244
+ transformMessage: (aiMessage, source) => {
245
+ // Skip messages from a specific user
246
+ if (source.author.userId === "U_NOISY_BOT") return null;
247
+ return aiMessage;
248
+ },
249
+ });
250
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat",
3
- "version": "4.20.0",
3
+ "version": "4.20.2",
4
4
  "description": "Unified chat abstraction for Slack, Teams, Google Chat, and Discord",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",