* feat: add LinkPreview to Message for URL and embedded message support
Add `links: LinkPreview[]` to `Message` so handlers can access URLs
shared in messages. Each LinkPreview contains the URL and optional
unfurl metadata (title, description, siteName, imageUrl).
On Slack, links are extracted from rich_text block elements (falling
back to <url> patterns in text). Links pointing to other Slack messages
(*.slack.com/archives/{channel}/p{ts}) include a `fetchMessage()`
callback that retrieves and parses the linked message.
`toAiMessages()` now appends link metadata to message content
automatically, labeling embedded message links distinctly so AI models
understand the context.
- Add LinkPreview interface to core types
- Add links field to Message, MessageData, SerializedMessage
- Extract links in Slack adapter (blocks + text fallback)
- Provide fetchMessage for Slack message URLs
- Set links: [] in all other adapters
- Include link metadata in toAiMessages() output
- Document LinkPreview in message API docs
- Document toAiMessages() in streaming and handling-events docs
- Add toAiMessages to API overview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove redundant links: [] from adapters for backwards compatibility
The Message constructor already defaults links to [] when not provided,
so adapters that don't support link extraction don't need to pass it
explicitly. This makes the change backwards-compatible for third-party
adapters — they get an empty links array without any code changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: avoid polynomial regex in Slack link extraction
Replace `[^>|]+` pattern (which backtracks on `|`) with `[^>]+`
and a programmatic indexOf split. This prevents ReDoS on untrusted
message text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add attachment support and mention tests to toAiMessages
toAiMessages now includes image and text-file attachments as multipart
content compatible with AI SDK's UserContent type:
- Images → ImagePart (via fetchData base64 or URL fallback)
- Text files (text/*, application/json, etc.) → FilePart
- Video/audio → warns via onUnsupportedAttachment callback
- Other file types → silently skipped
The function is now async to support fetchData() calls for inlining
attachment data as base64 data URIs. When fetchData fails, falls back
to the attachment URL.
Also adds mention rendering tests verifying that @mentions appear as
@name (not Slack's <@U123> syntax) in toAiMessages output, both in
plain messages and with links/includeNames enabled.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make AiMessage types structurally compatible with AI SDK
Use a discriminated union (AiUserMessage | AiAssistantMessage) so
AiMessage[] is directly assignable to ModelMessage[] without casts.
Match DataContent type (string | Uint8Array | ArrayBuffer | Buffer)
for image/file parts to ensure structural compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(example): pass image attachments to AI via toAiMessages
The onNewMention handler was passing message.text directly to the AI
agent, dropping any image attachments. Now uses toAiMessages([message])
which includes images via fetchData as base64 inline data, enabling
the AI to actually see uploaded images.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass image data as Buffer, not data URI
The AI SDK expects DataContent (Buffer/Uint8Array/base64 string) for
image and file parts, not data URIs. Passing `data:image/png;base64,...`
caused "Could not process image" errors from the API. Now passes the
raw Buffer from fetchData() directly, with mediaType set separately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: require fetchData for attachments, no URL fallback
Slack's url_private requires Bearer token auth that AI providers can't
provide. Remove URL fallback — attachments are only included when
fetchData() succeeds (which handles auth internally). Log errors
instead of silently falling through.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* debug: add temporary logging to toAiMessages image handling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use data URIs for image/file attachments in toAiMessages
The AI SDK's convertToLanguageModelV2DataContent parses data: URIs
to extract both the base64 content and media type. Raw base64 strings
lose the media type (returns mediaType: void 0), and raw Buffers may
not serialize correctly across network boundaries. Data URIs are the
most reliable format.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* debug: log fetched image size to diagnose API rejection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* debug: log prompt structure to diagnose image rejection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use FilePart instead of ImagePart for image attachments
The AI SDK's ImagePart with data URI strings doesn't work correctly
through the AI Gateway. Use FilePart (type: "file") with data URI
in the data field instead — this matches the working pattern used by
other projects and handles image data correctly across all providers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* debug: comprehensive logging at every decision point in toAiMessages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* debug: log data prefix to verify content format
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* debug: log token capture and detect HTML responses from Slack file fetch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: improve Slack file fetch error message and remove debug logging
When Slack returns an HTML login page instead of file data (typically
due to missing "files:read" OAuth scope), the error message now
explicitly tells the user what scope to add. Also removes all
temporary debug logging from toAiMessages and createAttachment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add transform support
* changeset
* address-feedback
* lint
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js Chat Example
A full-featured example app demonstrating the Chat SDK with Next.js. Integrates with Slack, Microsoft Teams, Google Chat, Discord, GitHub, and Linear — configure whichever platforms you need via environment variables.
Getting started
Prerequisites
- Node.js 20+
- pnpm 9+
- Redis (for state persistence)
- At least one platform configured (see Environment variables)
Setup
- Install dependencies from the monorepo root:
pnpm install
- Copy the example environment file and fill in your platform credentials:
cp .env.example .env.local
- Start the dev server:
pnpm dev
The app runs at http://localhost:3000. Platform webhooks should point to /api/webhooks/{platform} (e.g. /api/webhooks/slack).
For local development with real webhooks, use a tunneling tool like ngrok or
localtunnel.
What it demonstrates
- Event handlers — mentions, thread subscriptions, pattern matching, reactions
- AI mode —
@mention AIto enable streaming LLM responses via the Vercel AI SDK - Cards — interactive JSX-based cards with buttons, dropdowns, and fields
- Modals — form dialogs with text inputs, validation, and private metadata
- Actions — button clicks and dropdown selections with response handlers
- Slash commands — platform-specific command handling
- Ephemeral messages — user-only visible messages with DM fallback
- DMs — programmatic direct message initiation
- File uploads — attachment detection and display
- Multi-platform — same bot logic across all six platforms
Project structure
src/
├── app/
│ ├── api/
│ │ ├── webhooks/[platform]/route.ts # Main webhook entry point
│ │ ├── slack/install/ # Slack OAuth flow
│ │ └── discord/gateway/route.ts # Discord gateway cron
│ ├── settings/page.tsx # Preview branch config UI
│ └── page.tsx # Home page
├── lib/
│ ├── bot.tsx # Bot logic and handlers
│ ├── adapters.ts # Adapter initialization
│ └── recorder.ts # Webhook recording system
└── middleware.ts # Preview branch proxy
Environment variables
Copy .env.example for the full list. At minimum, set BOT_USERNAME and credentials for one platform:
| Variable | Description |
|---|---|
BOT_USERNAME |
Bot display name |
SLACK_BOT_TOKEN |
Slack bot token (single-workspace mode) |
SLACK_SIGNING_SECRET |
Slack request verification |
TEAMS_APP_ID |
Teams app ID |
TEAMS_APP_PASSWORD |
Teams app password |
GOOGLE_CHAT_CREDENTIALS |
Google Chat service account JSON |
DISCORD_BOT_TOKEN |
Discord bot token |
DISCORD_PUBLIC_KEY |
Discord interaction verification key |
GITHUB_TOKEN |
GitHub PAT or App credentials |
LINEAR_API_KEY |
Linear API key |
REDIS_URL |
Redis connection string |
See the Chat SDK docs for full platform setup guides.
Recording and replay
The app includes a recording system for capturing production webhook interactions and converting them into replay tests.
# Enable recording in your environment
RECORDING_ENABLED=true
# List recorded sessions
pnpm recording:list
# Export a session
pnpm recording:export <session-id>
See packages/integration-tests/fixtures/replay/README.md for the full workflow.
Preview branch testing
Test PRs with real webhook traffic by proxying requests from production to a preview deployment:
- Deploy a preview branch to Vercel
- Go to
/settingson the production deployment - Enter the preview branch URL and save
All webhook requests are proxied until the URL is cleared.