Commit Graph

303 Commits

Author SHA1 Message Date
josh 745fdf5a97 fix(adapters): harden Telegram streaming and XChat read receipts (#826)
## summary

- pace Telegram post-and-edit streams for private and non-private chat
limits, including the final edit
- respect Telegram `retry_after` cooldowns and reject when the complete
response cannot be delivered
- prevent explicit XChat read receipts from advancing past an unresolved
message
- preserve latest-event fallback for delivered XChat messages without a
sequence id
- update adapter documentation and regression coverage

---------

Signed-off-by: dancer <josh@afterima.ge>
2026-08-14 19:34:05 +01:00
josh 3bbf3ff542 fix(telegram): make native draft streaming opt-in (#822)
- use post-and-edit streaming by default to avoid leaked draft previews
in Telegram clients
- add `nativeStreaming: true` for explicitly enabling native draft
previews in private chats
- preserve existing native streaming behavior when enabled
- document the client compatibility tradeoff
- closes #782

before: private chat streams used native Telegram drafts by default,
which could remain visible over the final message on Telegram macOS

after: streams use post-and-edit by default across Telegram clients,
while native drafts remain available as an opt-in

---------

Signed-off-by: dancer <josh@afterima.ge>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-14 15:21:48 +10:00
josh 83ede7eab2 feat(chat): add message reply support (#819)
- add `thread.reply()` for sending messages with native references to
existing messages
- accept either a message object from the same thread or a message id as
the reply target
- support text, markdown, AST, cards, files, and buffered streams
- add WhatsApp contextual replies using the Cloud API
`context.message_id` field
- apply reply context only to the first outgoing message when content is
split across multiple sends
- preserve the target message through sent message edits and thread
history
- throw `NotImplementedError` for adapters without native reply support
- document the API and add message replies to the adapter feature matrix

fixes #786

---------

Signed-off-by: dancer <josh@afterima.ge>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com>
2026-08-14 14:32:27 +10:00
josh 18d4a230d7 feat(chat): add mark as read support (#820)
- add `thread.markAsRead()` for the current message, an explicit
`Message`, or a message ID
- expose read receipts as an optional adapter capability with explicit
unsupported and thread mismatch errors
- support WhatsApp read acknowledgements, Messenger `mark_seen`, and
XChat read watermarks
- preserve automatic XChat receipts while allowing manual timing and
surfacing explicit failures
- document provider-specific behavior and capability support
- closes #785

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Aradhya C P <135510032+aradhyacp@users.noreply.github.com>
2026-08-14 14:09:23 +10:00
Ben Sabic 7a1150ce23 Add Vercel Connect support to Telegram (#813)
Adds function-backed Telegram bot-token resolution so the adapter can
use short-lived Vercel Connect credentials for every Bot API and
file-download request. Static tokens retain their existing synchronous
behavior, while native Telegram webhook verification or polling remains
unchanged.

```ts
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { connectTelegramAdapter } from "@vercel/connect/chat";

createTelegramAdapter({
  ...connectTelegramAdapter("telegram/acme-telegram"),
  secretToken: process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN,
});
```

`create-chat-sdk` now recognizes Telegram as Connect-capable, preserves
`TELEGRAM_WEBHOOK_SECRET_TOKEN`, and emits native-webhook guidance:

```bash
npm create chat-sdk@latest -- my-bot --adapter telegram memory --connect -y
```

This PR is stacked on the Notion Connect work in #812. Validated with
the Telegram adapter suite (251 tests), create-chat-sdk suite (211
tests), package type checks/builds, and repository lint/format checks.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-12 11:49:29 +10:00
Ben Sabic 06b04ac4d9 Add Vercel Connect support to Notion (#812)
Adds function-backed Notion access-token resolution so the adapter can
use short-lived Vercel Connect credentials for every API request, retry,
and multipart upload. Direct Notion webhooks continue to use
`NOTION_VERIFICATION_TOKEN` and native HMAC verification because Connect
does not forward Notion triggers.

```ts
import { createNotionAdapter } from "@chat-adapter/notion";
import { connectNotionAdapter } from "@vercel/connect/chat";

createNotionAdapter({
  ...connectNotionAdapter("notion/acme-notion"),
  verificationToken: process.env.NOTION_VERIFICATION_TOKEN,
});
```

`create-chat-sdk` now recognizes Notion as Connect-capable, preserves
the native webhook verification token, and emits direct-webhook
guidance:

```bash
npm create chat-sdk@latest -- my-bot --adapter notion memory --connect -y
```

Validated with the Notion adapter suite (71 tests), create-chat-sdk
suite (209 tests), package type checks/builds, and repository
lint/format checks.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-12 11:00:21 +10:00
Max 0f24cc3062 feat(chat): preserve replied-to message context (#802)
## Summary

- add optional, normalized `Message.replyTo` context that survives JSON
and workflow serialization, queue rehydration, thread history, and
`SentMessage` reconstruction
- populate it from Telegram's `reply_to_message`, including combined
media groups, so handlers don't need raw Telegram payloads
- keep the core contract adapter-neutral while Telegram owns only its
platform mapping, allowing other adapters to populate it when they
receive full replied-to messages


Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-11 17:56:40 +01:00
Max 1d2b78d933 Deduplicate repeated Telegram webhook updates (#799)
## Summary

Telegram retries webhook deliveries after non-2xx responses, and its
`update_id` field is explicitly intended for ignoring repeated updates.
The Telegram adapter previously routed every webhook delivery
independently.

This change atomically claims each integer `update_id` through the
configured `StateAdapter` before routing the update. Repeated deliveries
return 200 without reaching bot handlers, while state failures return
503 without dispatching so Telegram can retry. Updates without an
integer `update_id` keep their existing behavior, and polling remains
unchanged.

Claims expire after 24 hours because Telegram retains incoming updates
for no longer than 24 hours. This is a bounded retention choice, not a
documented retry timeout. Cross-instance deduplication requires shared
durable state; in-memory state only protects one process. The change
provides webhook-delivery idempotency, not end-to-end exactly-once
handler completion.

Telegram contract: [Update](https://core.telegram.org/bots/api#update)
and [setWebhook](https://core.telegram.org/bots/api#setwebhook).

## Test plan

- `pnpm --filter @chat-adapter/telegram test`
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm check`
- `pnpm konsistent`
- `TURBO_CONCURRENCY=1 pnpm validate`

Regression coverage verifies sequential and concurrent repeated
deliveries, distinct update IDs, missing update IDs, duplicate 200
responses, and state-failure retry behavior. GitHub CI also passes on
Node 22 and Node 24.

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
Signed-off-by: dancer <josh@afterima.ge>
Co-authored-by: dancer <josh@afterima.ge>
2026-08-11 17:37:50 +01:00
Ben Sabic 927d0dbd7d docs: add cross-link card sections and page-level SEO metadata (#804)
Many docs pages are orphaned: nothing links to them apart from the
sidebar, so readers and crawlers rarely find them. This PR gives every
docs page a Read more section with four cards at the bottom of the
article, above the prev/next footer.

Cards are picked deterministically in lib/read-more.ts: the page's
related frontmatter first, then prerequisites, then siblings from the
same sidebar section, then the rest of the page tree, so every page
always fills all four slots. Card titles and descriptions come from the
target page's own frontmatter, nothing is duplicated. The section is
injected through the MDX wrapper slot in the docs route, so it applies
to all pages without touching content.

To make the links topical rather than positional, 26 pages get related
frontmatter additions. The 20 pages that no other page referenced (all
ten api/ pages among them) now each have at least one inbound link,
generally pairing guides with their API reference and back. The bundled
copy of create-chat-sdk.mdx is synced to keep the byte-match test green.

Official adapter pages get the same treatment with a More adapters
section: same-type adapters first (platform or state, using the catalog
order), topped up from the other official group. Vendor-official and
community adapters are never shown, and their pages don't render the
section. It reuses AdapterCard, so logos and package names match the
listing page.

Two small SEO fixes ride along. JSON-LD was allowlisted to three docs
pages; the allowlist is gone, so all 45 now emit HowTo or TechArticle
plus a BreadcrumbList. Docs and adapter detail pages also emit canonical
URLs now, resolved against the existing metadataBase.

Verified against the production build: all 45 docs pages and all 19
official adapter pages render exactly four cards, no page is left
unreferenced, canonicals and JSON-LD are present everywhere, and pnpm
validate passes. Docs-only, so no changeset.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-11 08:12:05 +10:00
Ben Sabic a0cba0288a Add Vercel Connect support to Discord (#808)
Adds function-backed Discord bot token and application ID resolvers,
plus custom webhook verification for Vercel Connect trigger-forwarded
interactions. Native Discord Ed25519 verification remains the default
when no custom verifier is configured.

```ts
import { createDiscordAdapter } from "@chat-adapter/discord";
import { connectDiscordAdapter } from "@vercel/connect/chat";

createDiscordAdapter({
  ...connectDiscordAdapter("discord/acme-discord"),
});
```

`create-chat-sdk` now recognizes Discord as Connect-capable, generates
`DISCORD_CONNECTOR` instead of native credential variables, and
preserves `CRON_SECRET` for Gateway forwarding:

```bash
npm create chat-sdk@latest -- my-bot --adapter discord memory --connect -y
```

Validated with the Discord adapter suite (284 tests), create-chat-sdk
suite (206 tests), package type checks/builds, and repository
lint/format checks.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-11 08:11:32 +10:00
Bryan Hunter bdeb2bf1b1 fix(workflow): isolate chat serializers from node runtime (#806)
## Failure

Workflow SDK `5.0.0-beta.40` produces an invalid workflow bundle when a
Chat SDK serializable class such as `Message`, `ThreadImpl`, or
`ChannelImpl` crosses a workflow step boundary.

The Workflow compiler imports the emitted module containing each class
to register its `@workflow/serde` methods. In Chat SDK `4.37.0`, tsup
emits those classes in `dist/index.js`. The root entry also imports the
conversation-scoping implementation added in #751, which uses
`AsyncLocalStorage` from `node:async_hooks`. Serializer registration
therefore pulls Node-only code into the sandboxed workflow bundle before
any workflow or step executes.

Build warning:

```text
Serde warning for classes "ChannelImpl", "Message", "ThreadImpl":
Workflow bundle contains Node.js built-in imports: async_hooks.
These will fail at runtime in the workflow sandbox.
```

Deployed workflows then fail during module initialization:

```text
var import_async_hooks = require("async_hooks");
                         ^

ReferenceError: require is not defined
```

## Minimal reproduction

```json
{
  "dependencies": {
    "chat": "4.37.0",
    "workflow": "5.0.0-beta.40"
  }
}
```

```ts
import { Message } from "chat";

async function createMessageStep(value: string): Promise<Message> {
  "use step";

  return new Message({
    id: "message",
    threadId: "slack:C123:123.456",
    text: value,
    formatted: {
      type: "root",
      children: [
        {
          type: "paragraph",
          children: [{ type: "text", value }],
        },
      ],
    },
    raw: {},
    author: {
      userId: "U123",
      userName: "user",
      fullName: "User",
      isBot: false,
      isMe: false,
    },
    metadata: { dateSent: new Date(), edited: false },
    attachments: [],
  });
}

export async function testWorkflow(value: string): Promise<string> {
  "use workflow";

  const message = await createMessageStep(value);
  return message.text;
}
```

Running `workflow build` on `4.37.0` emits the warning; deploying the
output produces the runtime failure above.

## Fix

- Add a dedicated `chat/serialization` package entry for `Message`,
`ThreadImpl`, `ChannelImpl`, `reviver`, and their serialized DTO types.
- Make serializer code a second tsup entry and explicitly enable
splitting. The serializer-bearing classes are now emitted into a shared
chunk with no dependency on `Chat` or its Node-only conversation
context.
- Preserve the existing root exports and automatic `@workflow/serde`
behavior. Existing `import { Message } from "chat"` workflow code
remains valid.
- Add a post-build module-graph assertion that fails if any emitted
serializer registration can transitively import a Node.js builtin.
- Test against Workflow SDK `5.0.0-beta.40`, the compiler version that
exposed the invalid bundle.
- Add a minor changeset for the fixed-version Chat SDK packages,
producing the `4.38.0` release line.

After the change, the emitted serializer classes live in a sandbox-safe
shared chunk while `AsyncLocalStorage` remains in a separate Node
runtime chunk. The exact reproduction compiles successfully with `5
steps, 1 workflow` and no Serde warning.

## Control cases

The failure requires a serializable Chat class to cross a durable
boundary. These cases were already safe and remain unchanged:

- `AsyncLocalStorage` used entirely inside a `"use step"` function.
- A Chat `Message` created and consumed within one step while returning
plain data.
- Request handlers that convert Chat objects to plain workflow DTOs
before starting a workflow.
- `@vercel/sandbox` used entirely inside a step.

## Validation

- Committed beta.40 reproduction fixture: type-correct and compiled
during every Chat package build with no Node builtin / Serde warning.
- Emitted serializer module graph: no transitive Node.js builtins.
- Chat package: 1,113 tests pass.
- Chat package typecheck passes.
- Repository formatting and lint checks pass.
- Package build passes.

Full repository validation reaches the pre-existing `knip` baseline and
reports unrelated unused dependencies and unlisted binaries in examples
and adapter packages.

---------

Signed-off-by: bryan-hunter <bryan.hunter@vercel.com>
2026-08-10 09:03:49 -05:00
Rich Haines 9188fd7ed4 chore(docs): update @vercel/geistdocs to 1.19.6 (#807)
Updates `@vercel/geistdocs` from 1.19.4 to 1.19.6.
2026-08-10 19:33:57 +10:00
josh c3b5a08e7e fix(gchat): bind Pub/Sub push verification to a configured identity (#797)
## summary

Pub/Sub push verification checked the token's `aud` and nothing else.
[Google's
guidance](https://docs.cloud.google.com/pubsub/docs/authenticate-push-subscriptions)
is explicit that signature and audience verification are not sufficient
on their own, and that the `email` and `email_verified` claims must be
checked alongside them

adds `pubsubServiceAccountEmail` (env
`GOOGLE_CHAT_PUBSUB_SERVICE_ACCOUNT_EMAIL`), the identity in the
subscription's push auth settings. a push is accepted only when
`email_verified` is true and `email` matches exactly. when the option is
unset, pushes are rejected rather than trusted on their audience alone

direct webhooks are untouched, and the project-number path already bound
to an exact issuer

### how it happened

`verifyBearerToken` took the claim validator as an optional parameter,
so a call site could simply omit it, and the Pub/Sub one did while the
direct-webhook one did not. that is now required:

```diff
-    validatePayload?: (payload: {
+    validatePayload: (payload: {
```

both call sites pass one and the type system enforces it, so the
omission cannot recur

## test plan

- a token from a different service account is rejected
- a token is rejected when no identity is configured
- a token is rejected when `email_verified` is not true
- a token with no `email` claim is rejected
- a matching identity with a verified email is accepted
- direct-webhook and project-number verification are unchanged

docs cover the new option in the README and adapter page, including the
push-subscription authentication step that produces the token
2026-08-07 17:54:40 +01:00
Ben Sabic 2a2b2c5500 feat(instagram): add native DM adapter (#770)
Adds a first-party Instagram Direct Messages adapter backed by Meta's
Instagram API with Instagram Login.

- Verifies webhook challenges and HMAC signatures, then normalizes DMs,
story replies, media, quick replies, postbacks, and reactions.
- Sends plain text, cards, quick replies, typing indicators, URL
attachments, and uploaded media through `graph.instagram.com`.
- Maps authentication, rate-limit, and 24-hour messaging-window failures
to typed adapter errors.
- Registers Instagram in the adapter catalog, CLI scaffold, official
docs, replay suite, and Next.js example.

## Usage

```ts
import { createInstagramAdapter } from "@chat-adapter/instagram";
import { Chat } from "chat";

const bot = new Chat({
  userName: "mystore",
  adapters: { instagram: createInstagramAdapter() },
});
```

## Webhook

```ts
export async function POST(request: Request) {
  return bot.webhooks.instagram(request);
}
```

## Verification

- `pnpm --filter @chat-adapter/instagram test`
- `pnpm --filter @chat-adapter/instagram typecheck`
- `pnpm --filter example-nextjs-chat typecheck`
- `pnpm --filter example-nextjs-chat build`
- `pnpm check`
- `pnpm konsistent`

## Live Testing

<table>
  <tr>
<td><img width="1440" height="2109" alt="1000000502"
src="https://github.com/user-attachments/assets/9fdb8c3b-4e41-4c81-9426-08756a5e4201"
/></td>
<td><img width="1440" height="1995" alt="1000000503"
src="https://github.com/user-attachments/assets/8a572493-c57a-4412-9049-5737aaa9dfd0"
/></td>
  </tr>
</table>

Closes #729 / Co-Authored by @ivandujaut

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-07 17:38:15 +01:00
Aradhya C P 6abf4807db feat(whatsapp): Add native LinkButton support for WhatsApp CTA URL messages (#781)
Adds native `LinkButton` support to the WhatsApp adapter by
mapping Chat SDK `LinkButton` actions to WhatsApp Cloud API CTA URL
interactive messages.

Previously, WhatsApp cards containing only `LinkButton` actions were
rendered as plain text with the URL exposed. WhatsApp supports native
CTA URL buttons through `interactive.type: "cta_url"`, so this change
enables the adapter to use that native capability.

Closes #780

## Changes Made

- Added support for converting a single `LinkButton` action into a
WhatsApp CTA URL interactive message.
- Added the `cta_url` interactive message shape to the WhatsApp adapter
types.
- Preserved existing reply button behavior and fallback handling for
unsupported card configurations.
- Added test coverage for:
  - Single `LinkButton` → native CTA URL message conversion.
  - Existing reply button behavior remaining unchanged.
  - Multiple `LinkButton` fallback behavior.

### Test Coverage

Added tests covering the new CTA URL conversion path and verified the
generated WhatsApp payload contains:

- `interactive.type: "cta_url"`
- `action.name: "cta_url"`
- `action.parameters.display_text`
- `action.parameters.url`

## Screenshots/Demos

<img width="864" height="338" alt="image"
src="https://github.com/user-attachments/assets/cc58a76b-5a96-406a-9f79-ca7a2725836b"
/>

## Additional Notes

WhatsApp CTA URL messages only support a single URL button per
interactive message. The implementation intentionally only promotes
cards with exactly one `LinkButton` into a CTA URL message and keeps
existing fallback behavior for unsupported combinations.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-07 15:17:47 +10:00
Rich Haines e8cc4bc930 docs: add inbound cross-links to orphaned pages (#789)
These docs pages had no inbound links from other pages, so they're only
reachable via the sidebar. Adds minimal cross-links from related pages.

---------

Signed-off-by: molebox <rich@vercel.com>
2026-08-06 16:58:15 +10:00
Matias Gonzalez cd4a655844 fix(docs): remove hardcoded bg-background from CodePreview pre element (#790)
## Problem

On the GetStarted cards in the home page, the `<pre>` inside
`CodePreview` had `bg-background` hardcoded. When hovering a card
(`hover:bg-muted/40`), the code block retained its own opaque
background, so the hover tint only showed through around the text — the
dark/muted fill appeared clipped to the text container rather than
filling the whole rotated card.

## Fix

Removed `bg-background` from the `<pre>` element so it inherits the
parent card's background. The default state is unchanged since the card
itself already has `bg-background`.

---------

Co-authored-by: v0 <it+v0agent@vercel.com>
Co-authored-by: Matias Gonzalez <29680544+matiasngf@users.noreply.github.com>
2026-08-05 14:47:00 -03:00
Ben Sabic 0ec6a7361b feat(notion): add Notion comments adapter (#689)
Adds `@chat-adapter/notion`, an official adapter that lets a Chat SDK
bot take part in **Notion comment discussions** (page-level and
block/discussion threads) with the same handler code used for Slack,
Linear, GitHub, etc. Inbound events arrive via Notion webhooks
(`comment.created`) with HMAC signature verification; outbound actions
use the Comments REST API. Because Notion lets a connection edit its own
comments, the adapter supports **Post+Edit streaming**.

### Highlights

- **Webhooks** — `comment.created` verified with `X-Notion-Signature`
HMAC over the raw body (timing-safe), plus the one-time
`verification_token` handshake. Returns a fast 200 with idempotent,
state-backed dedupe.
- **Post+Edit streaming** — posts the first chunk, then `PATCH`es the
comment as tokens arrive, throttled to Notion's ~3 req/s limit (global
token bucket, `Retry-After` aware). Long bodies are split into
sequential comments to stay under the 2000-char rich-text cap.
- **Mentions** — three modes: `mention` (default; plain-text `@userName`
/ `@botUserId`), `all-comments`, and `keyword`.
- **`message.subject`** — resolves the parent page via the Pages API
(title, url, archived status, author).
- **File uploads** — up to 3 native attachments via the File Uploads API
(binary `single_part`; public URLs via `external_url` with bounded
polling); overflow and failures fall back to markdown links.
- **History** — `fetchMessages` over list-comments (open comments only),
direction-aware.
- Cards render as markdown fallback; reactions / typing / DMs are typed
no-ops or errors. Registered in the `chat/adapters` catalog and the
`create-chat-sdk` scaffold; pinned to `Notion-Version: 2026-03-11`.

### Usage

```ts
// lib/bot.ts
import { Chat } from "chat";
import { createNotionAdapter } from "@chat-adapter/notion";
import { createRedisState } from "@chat-adapter/state-redis";

export const bot = new Chat({
  userName: "notion-bot",
  adapters: { notion: createNotionAdapter() }, // reads NOTION_TOKEN + NOTION_VERIFICATION_TOKEN
  state: createRedisState(),
});

bot.onNewMention(async (thread, message) => {
  const subject = await message.subject; // parent page metadata (title, url, …)
  await thread.post(`Thanks for the mention on **${subject?.title ?? "this page"}**!`);
});
```

```ts
// app/api/webhooks/notion/route.ts
import { bot } from "@/lib/bot";

export const POST = (request: Request): Promise<Response> => bot.webhooks.notion(request);
```

### Configuration

Auto-detects `NOTION_TOKEN` and `NOTION_VERIFICATION_TOKEN`, plus
optional `NOTION_BOT_USERNAME`, `NOTION_MENTION_MODE`,
`NOTION_KEYWORDS`, and `NOTION_VERSION`; everything is overridable via
`createNotionAdapter({ … })`. The docs page covers the full connection +
webhook setup (capabilities, content access, and the webhook-URL-lock
warning).

Changeset bumps `@chat-adapter/notion`, `chat`, and `create-chat-sdk`
(minor). Layered as four commits: `feat` (adapter +
catalog/scaffold/emoji), `docs`, `test`, `chore(example)`.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-08-05 14:15:44 +01:00
josh 4ac0455134 feat(chat): add message update and delete lifecycle callbacks (#788)
## summary

adds `onMessageUpdated` and `onMessageDeleted`, so a bot can react when
a message is edited or removed. Slack dispatches both today; other
adapters can opt in later

supersedes #549, which was verified there against real Slack webhooks.
reopened from a branch in this repo with the original commits preserved
and signed

```typescript
bot.onMessageUpdated(async (thread, message, previousMessage) => {
  await mirror.update(message.id, message.text);
});

bot.onMessageDeleted(async (event) => {
  await mirror.remove(event.messageId);
});
```

both are lifecycle events: they never route through `onNewMessage`,
`onNewMention`, or `onSubscribedMessage`, and the concurrency strategies
do not apply

### notes

- **the bot's own edits are filtered.** slack sends a `message_changed`
for every `chat.update`, and post-and-edit streaming calls it once per
delta, so without this a single streamed reply would call the handler
back repeatedly on its own message
- **`previousMessage` is forwarded on edits.** slack sends the pre-edit
message and it was being dropped. an edit handler usually needs the
before to know what changed, so it is the optional third argument
- **the two shapes differ deliberately.** an edit carries a full
replacement message, so it gets `(thread, message, previousMessage?)`. a
delete has no message, only the id of what was removed, so it gets an
event. use `chat.thread(event.threadId)` when a delete handler needs one
- **one thread id helper** now serves message, edit, and delete, so an
edit cannot resolve to a different thread than the message it edits

## test plan

core:

- an edit dispatches to `onMessageUpdated` and not to the normal message
handlers
- the handler receives the pre-edit message as its third argument
- the bot's own edits are skipped
- a delete dispatches with normalized event data
- both run inside the active conversation, so read tools built in these
handlers stay scoped

slack:

- `message_changed` dispatches as an update, `message_deleted` as a
delete
- `previous_message` is forwarded, and left undefined when slack omits
it
- hidden unfurl updates stay ignored, hidden real edits still dispatch
- message, edit, and delete resolve to one thread id in a flat DM and in
a threaded `agent_view` DM

verified against a real slack workspace over socket mode: editing and
deleting a DM both routed to the same thread id as the original message

---------

Co-authored-by: Miłosz Lenczewski <m.lenczewski@tidio.net>
2026-08-05 13:22:54 +01:00
josh 7a1922357c fix(gchat): bind add-on webhook verification to a configured identity (#787)
## summary

endpoint-URL webhook verification accepted any `email` claim matching
the generic Workspace Add-on shape:

```ts
/^service-\d+@gcp-sa-gsuiteaddons\.iam\.gserviceaccount\.com$/
```

the `\d+` is a GCP project number, and service agents are
`service-{PROJECT_NUMBER}@gcp-sa-{SERVICE}...` for the project that owns
them. so that shape identifies "some Workspace Add-on", not *this* app's
add-on, and it was the only thing standing between a public endpoint URL
and a verified request. the method's own doc comment already stated the
correct invariant, that the token is only trustworthy if it was issued
to Google Chat itself

adds `workspaceAddOnServiceAccountEmail` (env
`GOOGLE_CHAT_WORKSPACE_ADDON_SERVICE_ACCOUNT_EMAIL`) and compares add-on
identities exactly. when it is unset, add-on-shaped tokens are rejected
rather than trusted by shape, with a log naming the option to set

`chat@system.gserviceaccount.com` is untouched, so standalone Chat apps
behave exactly as before. the project-number and Pub/Sub paths were
already bound to exact identities and are unchanged

### behavior

| token `email` | before | after |
| --- | --- | --- |
| `chat@system.gserviceaccount.com` | accept | accept |
| add-on shape, matches configured identity | accept | accept |
| add-on shape, different project | accept | **reject** |
| add-on shape, option unset | accept | **reject** |

<details>
<summary>why not reject at construction</summary>

refusing to initialize when the option is absent would be the
stricter-looking choice, but the adapter cannot tell Workspace Add-on
mode from config alone, it only sees `endpointUrl`. throwing there would
break every ordinary endpoint-URL Chat app. rejecting add-on-shaped
tokens at verification is the precise equivalent without the collateral

</details>

## test plan

- an add-on token matching the configured identity is accepted
- an add-on token from a different project is rejected, the case the
generic shape allowed
- an add-on token is rejected when no identity is configured
- `chat@system.gserviceaccount.com` is still accepted with no add-on
config
- suffixed and prefixed lookalike domains, an uppercase variant, and
trailing whitespace are all rejected
- a matching identity with `email_verified: false` is rejected

the two rejection cases above returned 200 before this change and 401
after
2026-08-05 13:16:07 +01:00
Ben Sabic 258a7312ba docs: add vendor-official guide and refresh adapter docs (#784)
Adds a vendor-official contributing guide covering qualifications,
listing terms, and the PR checklist for platform vendors. Contributing
and adapter overview pages point to that guide for listing details
instead of repeating them.

Moves Slack and Teams low-level API docs onto their adapter pages, with
permanent redirects from `/docs/slack-primitives` and
`/docs/teams-primitives`.

Trims stale hand-maintained comparison tables from the docs intro and
platform adapters overview. Those pages now link to `/adapters` and the
generated official feature matrix. Adds contributing CTAs for building
an adapter and listing a vendor-official one.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-05 14:08:46 +10:00
christopherkindl 0334e97b62 chore(docs): upgrade geistdocs to 1.19.4 (#783)
Bumps `@vercel/geistdocs` in the docs app from 1.19.2 to 1.19.4 (to fix
safari logo bug)
2026-08-05 11:48:40 +10:00
Ben Sabic fe4ed11ea9 docs: add XChat branding and clarify X vs XChat adapters (#777)
- Add a dedicated XChat speech-bubble logo for the docs hero and
`/adapters` card
- Point XChat docs and `adapters.json` at the new `xchat` icon instead
of reusing `x`
- Update the XChat OG image
- Add reciprocal “X Adapter vs XChat Adapter” / “XChat Adapter vs X
Adapter” sections on both docs pages
- Rename remaining “X Chat” references to “XChat” in the adapter package
README and comments

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-03 23:53:24 +10:00
Mukund Sarma 85e3d22ba1 fix(chat): follow-up hardening and docs for agent read-tool scoping (#774)
Follow-up hardening and updated docs for the agent read-tool scoping in
`createChatTools`.

## What changed

- Wrap the remaining dispatch paths (modal submit/close,
assistant-thread, assistant-context, app-home, app-context,
member-joined) in `runInConversation` so read tools built inside those
handlers inherit the active conversation.
- Log a warning when a read runs with no resolvable scope, instead of
failing open silently.
- Keep scoping channel-level by default; add opt-in `strictScope: true`
to confine a thread scope to that thread alone (rejects sibling threads
on per-thread-ACL platforms like Discord and GitHub).
- Update the AI SDK tools docs to cover the channel-level default, what
`scope` does and does not do, and the `strictScope` opt-in.

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-08-03 13:50:05 +01:00
Aradhya C P 0642ce335f docs: document WhatsApp typing indicator support (#772)
This PR updates the WhatsApp adapter documentation to reflect the
existing typing indicator support through `thread.startTyping()`.

The feature was already implemented in the adapter but was missing from
the documentation and feature matrix, making it difficult for users to
discover.

Fixes #771
2026-08-03 14:26:14 +10:00
Max 629e655578 fix(telegram): combine incoming media groups (#760)
- buffer incoming Telegram updates that share a `media_group_id` and
dispatch them once the album settles
- coordinate through the configured `StateAdapter` so separate
serverless instances still produce one message
- preserve the shared caption and order attachments by Telegram message
ID

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-08-01 11:52:04 +10:00
dependabot[bot] 7cda0e008e build(deps-dev): bump postcss from 8.5.16 to 8.5.18 (#744)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.16 to
8.5.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/4c0d194c136fd374495d0993c890d794cab65b81"><code>4c0d194</code></a>
Release 8.5.18 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/92b4e7891ec7b811821d01acc8aa0f010caf41e2"><code>92b4e78</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/95663d3eb7ba26f4854dd19d3b4f4425760cf56c"><code>95663d3</code></a>
Limit where source map can be loaded for security reasons</li>
<li><a
href="https://github.com/postcss/postcss/commit/74e25ae9f4efaa56a41a449064a655d7da78072c"><code>74e25ae</code></a>
Release 8.5.17 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/d1518afd5a88f42728b30b87f8917210f363f9f1"><code>d1518af</code></a>
Fix Maximum call stack size exceeded error</li>
<li><a
href="https://github.com/postcss/postcss/commit/2421312ffea96ba77b35ce24a1b2d9c2e22b5e83"><code>2421312</code></a>
Fix linter</li>
<li><a
href="https://github.com/postcss/postcss/commit/a50352c583df991710f92ccac25b36304695161a"><code>a50352c</code></a>
Fix CI</li>
<li><a
href="https://github.com/postcss/postcss/commit/33948f0969bb858acdd52c9692e3a785a3ed0a73"><code>33948f0</code></a>
Prevent prototype hijacking in fromJSON</li>
<li><a
href="https://github.com/postcss/postcss/commit/2131909351161cd2c5fc2be58b14919a873ea824"><code>2131909</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/93440abcca92793b31c5d1fdf5f2da7b58b27599"><code>93440ab</code></a>
Fix non-closed <code>\&lt;div align=&quot;center&quot;&gt;</code> in
README (<a
href="https://redirect.github.com/postcss/postcss/issues/2110">#2110</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.16...8.5.18">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 00:43:07 +01:00
dependabot[bot] 379842f2b2 build(deps): bump next from 16.2.6 to 16.2.11 (#740)
Bumps [next](https://github.com/vercel/next.js) from 16.2.6 to 16.2.11.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vercel/next.js/releases">next's
releases</a>.</em></p>
<blockquote>
<h2>v16.2.11</h2>
<p>This release contains security fixes for the following
advisories:</p>
<p>High:</p>
<ul>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-m99w-x7hq-7vfj">Denial
of Service in App Router using Server Actions</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-6gpp-xcg3-4w24">Middleware
/ Proxy bypass in App Router applications using Turbopack and single
locale</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-p9j2-gv94-2wf4">Server-Side
Request Forgery in rewrites via attacker-controlled destination
hostname</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-89xv-2m56-2m9x">Server-Side
Request Forgery in Server Actions on custom servers</a></li>
</ul>
<p>Moderate:</p>
<ul>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-68g3-v927-f742">Cache
confusion of response bodies for requests with bodies</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-4633-3j49-mh5q">Cache
confusion of response bodies for requests with bodies containing invalid
UTF-8 byte sequences</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-q8wf-6r8g-63ch">Denial
of Service in the Image Optimization API using SVGs</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-955p-x3mx-jcvp">Unauthenticated
disclosure of internal Server Function endpoints</a></li>
<li><a
href="https://github.com/vercel/next.js/security/advisories/GHSA-4c39-4ccg-62r3">Unbounded
Server Action payload in Edge runtime</a></li>
</ul>
<h2>v16.2.10</h2>
<p>Contains no changes except publishing <code>@next/swc-wasm-web</code>
which was accidentally not published since 16.2.4.</p>
<h2>v16.2.9</h2>
<p>Empty release to ensure <code>next@latest</code> points at a stable
release. Next.js only allows publishing with Trusted Publishing enabled.
In order to fix NPM dist-tags, we have to release a new version.
Updating dist-tags is not possible with Trusted Publishing.</p>
<h2>v16.2.8</h2>
<p>Release with no changes in an attempt to fix <code>next@latest</code>
pointing at a prerelease version.</p>
<h2>v16.2.7</h2>
<blockquote>
<p>[!NOTE]
This release is backporting bug fixes. It does <strong>not</strong>
include all pending features/changes on canary.</p>
</blockquote>
<h3>Core Changes</h3>
<ul>
<li>Backport documentation fixes for v16.2 (<a
href="https://redirect.github.com/vercel/next.js/issues/93804">#93804</a>)</li>
<li>[backport] Patch <code>playwright-core</code> to resolve
<code>_finishedPromise</code> on <code>requestFailed</code> (<a
href="https://redirect.github.com/vercel/next.js/issues/93920">#93920</a>)</li>
<li>[backport] Fix dev mode hydration failure when page is served from
HTTP cache (<a
href="https://redirect.github.com/vercel/next.js/issues/93492">#93492</a>)</li>
<li>[backport] Fix catch-all <code>router.query</code> corruption with
<code>basePath</code> + <code>rewrites</code> (<a
href="https://redirect.github.com/vercel/next.js/issues/93917">#93917</a>)</li>
<li>[backport] Encode non-ASCII characters in cache tags at construction
(<a
href="https://redirect.github.com/vercel/next.js/issues/93918">#93918</a>)</li>
<li>[backport] Fix server action forwarding loop with middleware
rewrites (<a
href="https://redirect.github.com/vercel/next.js/issues/93919">#93919</a>)</li>
<li>[backport] Turbopack: switch from base40 to base38 hash encoding (<a
href="https://redirect.github.com/vercel/next.js/issues/93932">#93932</a>)</li>
<li>[ci] Disable hanging node 24 typescript tests on 16.2 backport
branch (<a
href="https://redirect.github.com/vercel/next.js/issues/94164">#94164</a>)</li>
<li>[backport] Fix &quot;type: module&quot; in project dir when using
standalone or adapters (<a
href="https://redirect.github.com/vercel/next.js/issues/94050">#94050</a>)</li>
<li>[backport] Propagate adapter preferred regions (<a
href="https://redirect.github.com/vercel/next.js/issues/94200">#94200</a>)</li>
<li>[16.2.x] Don't drop <code>FormData</code> entries (<a
href="https://redirect.github.com/vercel/next.js/issues/94240">#94240</a>)</li>
<li>[backport] feat(turbopack): add LocalPathOrProjectPath PostCSS
config resolution (<a
href="https://redirect.github.com/vercel/next.js/issues/94284">#94284</a>)</li>
</ul>
<h3>Credits</h3>
<p>Huge thanks to <a
href="https://github.com/eps1lon"><code>@​eps1lon</code></a>, <a
href="https://github.com/icyJoseph"><code>@​icyJoseph</code></a>, <a
href="https://github.com/unstubbable"><code>@​unstubbable</code></a>, <a
href="https://github.com/mischnic"><code>@​mischnic</code></a>, <a
href="https://github.com/bgw"><code>@​bgw</code></a>, <a
href="https://github.com/timneutkens"><code>@​timneutkens</code></a>,
and <a
href="https://github.com/lukesandberg"><code>@​lukesandberg</code></a>
for helping!</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/next.js/commit/9beca0821cf4606ae33466ed6f4fc75f2887a4da"><code>9beca08</code></a>
v16.2.11</li>
<li><a
href="https://github.com/vercel/next.js/commit/3c48c7af78f2c01691065cb303da1b107a2c8617"><code>3c48c7a</code></a>
[16.x] Fix Turbopack middleware matcher with i18n single locale</li>
<li><a
href="https://github.com/vercel/next.js/commit/ac1eff3f7a7285176396ecc69c3b160a3d6ad1a2"><code>ac1eff3</code></a>
[16.x] Improve performance of checking valid MPA form submissions</li>
<li><a
href="https://github.com/vercel/next.js/commit/9a4651e754f70b12e397694ffc41f44c3ba8cc17"><code>9a4651e</code></a>
[16.x] Enforce <code>serverActions.bodySizeLimit</code> for Server
Actions in Edge runtime</li>
<li><a
href="https://github.com/vercel/next.js/commit/b51206321854193208c0805ba42acc49287f942b"><code>b512063</code></a>
[16.x] Set correct origin for internal redirects in custom server</li>
<li><a
href="https://github.com/vercel/next.js/commit/d3033266c6dff23f7be71e19341fe3a8c6e2c599"><code>d303326</code></a>
[16.x] Ensure exotic rewrite param values are properly encoded</li>
<li><a
href="https://github.com/vercel/next.js/commit/73b94872bc343d09494b50394d8c08eb9fc8e56a"><code>73b9487</code></a>
[16.x] fix(fetch-cache): key fetch(Request, init) by the effective
request</li>
<li><a
href="https://github.com/vercel/next.js/commit/bf9d17fb30501829f6fd7c0ee8e44e2794565742"><code>bf9d17f</code></a>
[16.x] fix(incremental-cache): byte-exact fetch cache key for binary
bodies</li>
<li><a
href="https://github.com/vercel/next.js/commit/fe28768f533582ea8f6ee7d7a7498715927d45f5"><code>fe28768</code></a>
[16.x] fix(next/image): improve performance of detectContentType()</li>
<li><a
href="https://github.com/vercel/next.js/commit/d8afb8d550ac4ac5c106ea1410c3af43eaf1d469"><code>d8afb8d</code></a>
[16.x] Performance improvements when decoding React Server function
payloads</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/next.js/compare/v16.2.6...v16.2.11">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-01 00:42:41 +01:00
christopherkindl 0e62da79a4 chore(docs): use Geist heading tokens on adapter pages (#764)
Follow-up to #762 — this commit was left out when that PR merged
(Docs-only styling change)

Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>
2026-08-01 00:40:20 +01:00
christopherkindl 3ae34b3ed1 feat(docs): migrate the homepage to geistdocs 1.19 and the Geist design system (#762)
Third repo in the design sync, after `vercel/geistdocs#216`/`#218` and
`vercel/flags#457`. Upgrades the docs site to `@vercel/geistdocs@1.19.2`
and brings the homepage onto the Geist design system.

`apps/docs` is `private: true`, so no changeset.

## Dependency

`1.19.2` peers on `next: ^16.2.11` and the app pinned `16.2.6`, so
**next moves to `16.2.12`** alongside it — without that pnpm reports an
unmet peer. Installed via `pnpm add --save-exact` per AGENTS.md. All 20
geistdocs subpaths this app imports still exist in 1.19.2; no API
breakage.

The footer needed no work: 1.16 already shipped the prop-less
Vercel-directory `<Footer />`.

## Layout — `home-grid.css` is gone

Deleted `app/styles/home-grid.css` (368 lines) and its `global.css`
import, and rebuilt each section on `grid-cols-12` / `col-span-*`:

| section | before (CSS) | after |
|---|---|---|
| OSS stats | 2×2 → 4×1 @768 | `col-span-6 min-[768px]:col-span-3` |
| Features | 2-up + full-width 3rd → 3×1 @961 | `col-span-12
sm:col-span-6 lg:col-span-4` |
| Code | stacked → sidebar 1/3 + code 2/3 @961 | `lg:col-span-8` /
`lg:col-span-4`, pinned with `col-start` + `row-start` |
| Integrations | 1×5 → tall left + 2×2 @961 | `lg:col-span-4
lg:row-span-2` + four `lg:col-span-4` |

The code section needs explicit `col-start`/`row-start` because the
sidebar follows the code in the DOM but sits left of it from `lg`.

Other layout changes:

- **Single gutter at the page root** (`mx-auto w-full max-w-[1448px]
px-4 sm:px-6`); removed the per-section horizontal padding that
duplicated it, so every section's content lands on the navbar/footer
content edge.
- **Content widened 1114px → 1400px**, the navbar's content span (1448 −
2×24).
- **Bottom gap above the footer trimmed ~320px → ~176px** — layout
`pb-32` → `pb-16` and page `pb-24 sm:pb-36` → `pb-12 sm:pb-16`. Three
paddings were stacking.

The 768px stats breakpoint is preserved with `min-[768px]:` — there's no
Tailwind equivalent here (`md`=601, `lg`=961) and four KPI columns at
601px would be ~140px each.

## Design — ported from vercel.com/ai-sdk

Read off the flagged source in `front/apps/vercel-marketing/.../ai-sdk`,
not the live site.

- **Code showcase tabs** → the `SlidingTabs` primitive: pill labels with
an animated indicator, full keyboard nav (arrows/Home/End, roving
tabindex), and an invisible-bold label so the tab doesn't shift width
when it bolds. Four tabs per group with dot pagination for the rest,
tabs above the code block. Copied into `components/ui/sliding-tabs.tsx`
with `cn` rewired and the `no-scrollbar` utility inlined (geistdocs
doesn't define it).
- **"Scale with confidence"** → heading and paragraph on one
bottom-aligned row (cols 1–4 / 8–12), then four bordered cards
`col-span-12 md:col-span-6 lg:col-span-3`. Type mapped from their
primitives: `SectionHeading size="48"` → `text-heading-40
lg:text-heading-48`, `SectionParagraph size="18"` → `text-copy-16
lg:text-copy-18`.
- **Feature row** → icon + muted eyebrow over a prominent statement.
Note this **inverts the previous emphasis**: the heading is now the
small muted label and the description the larger line, matching the
reference. Icons come from geistdocs' own set so they match Geist's line
weight: `IconLinked`, `IconWorkflow`, `IconAcronymTs`.
- **Get-started install snippet** → the shared `CommandPrompt`, with its
buttons on one row from `lg`.
- Remaining headings converted to `text-heading-*`.
- Navbar logo drops `height={22}` to take `LogoChatSdk`'s new 18px
default (renders 106.9×22 → 87.4×18).

## Two fixes worth calling out

**`lib/utils.ts` — `cn` was silently dropping typography.** Geist's
`text-copy-*`/`text-heading-*` share the `text-` prefix with colour
utilities, so stock `tailwind-merge` classifies them as colours and
drops the size whenever both appear in one `cn()` call. geistdocs ships
a `cn` that registers them as `font-size` for exactly this reason but
doesn't export it, so the config is replicated here. This was a latent
bug across the app, not just the new code.

**`Analytics`/`SpeedInsights` moved out of the `"use client"` provider**
into the server layout. Both emit `<script>`, and scripts rendered
inside a client tree never execute — so analytics wasn't firing on
client navigations. React 19.2.7 (pulled in by this bump) now warns
about it; the bug predates it.

## Verification

- `pnpm --filter docs build` passes (270 pages), `tsc --noEmit` clean,
`biome check` clean.
- Rendered output spot-checked for the tab strip, dot pagination, card
classes, and feature icons.

**`pnpm validate` could not be run** — it needs Node ≥20.19 and this
machine is on v20.11.1 (`pnpm check` dies on `styleText` from
`node:util`). Biome, tsc and build were run directly instead, but the
knip and test legs are unrun and should be confirmed in CI.

Signed-off-by: christopherkindl <53372002+christopherkindl@users.noreply.github.com>
2026-07-31 23:53:17 +01:00
Santiago Medina caa63253c5 feat(x): add XChat encrypted messaging support (#745)
## summary

new `@chat-adapter/xchat` adapter for XChat, X's encrypted messaging.
write bot logic once and hold encrypted 1:1 and group conversations like
the other Chat SDK adapters — all crypto handled inside the adapter via
`@xdevplatform/chat-xdk` (wasm), all REST via the typed
`@xdevplatform/xdk` client.

## background: chat-xdk


[`@xdevplatform/chat-xdk`](https://www.npmjs.com/package/@xdevplatform/chat-xdk)
is the official XChat cryptography SDK — a Rust core compiled to
WebAssembly that implements the XChat encryption protocol. it handles
per-conversation symmetric keys and key exchange, message
encryption/decryption, event signing and signature verification, and
encrypted media (secretstream). the bot's private keys live in a
PIN-protected [Juicebox](https://juicebox.xyz) store (secret-shared
across independent realms), so no key material sits in env vars or on
disk — the adapter unlocks with a PIN at startup. this adapter is the
glue: chat-xdk produces and consumes the encrypted envelopes, the typed
`@xdevplatform/xdk` client moves them over the X API, and everything is
normalized to the Chat SDK's `Thread`/`Message` model.

what it supports:
- encrypted send/receive in DMs and groups (webhook push + polling),
signature verification on by default
- mention detection from structured mention entities, swipe-replies to
the bot, and a plain-text `@handle` fallback; group replies go out as
quoted replies with TTL propagated
- `openDM(userId)`: starts (or reuses) an encrypted 1:1 —
cached/history-recovered conversation key, else a full key exchange so
the bot can message first
- media both ways: inbound attachments with lazy download+decrypt,
outbound encrypted (secretstream) via the 3-step upload flow
- edit and delete of the bot's own messages: edits are encrypted events
targeting the original's sequence id; deletes are locally signed
delete-for-all actions recipients verify
- reactions in and out, typing keep-alive while handlers run,
configurable group welcome message
- read receipts sent per delivered inbound message (`sendReadReceipts`,
default on)
- cards by degradation: text + tappable entities, link buttons as
`label: url` lines, primary link as a URL preview attachment with
optional encrypted banner

key design decisions:
- mdast stays the canonical format; markdown passes through as raw text
(XChat clients render plain text — no markdown), with URLs and @mentions
made tappable via entity spans and tables degraded to ASCII code blocks
- thread ids are `xchat:{conversationId}` (groups `g…`, 1:1s the sorted
participant pair)
- the first edit of a fresh message is age-gated (`editSafetyDelayMs`,
default 5000ms): receiving clients park an edit whose original hasn't
arrived, leaving the message permanently invisible — the gate prevents
that race
- undecryptable or unverified events are dropped, never delivered as
empty messages
- no core changes: the adapter implements the standard `Adapter`
interface only

also includes the `chat/adapters` catalog entry, docs page (with OG
image), `adapters.json` registry entry, and `create-chat-sdk` scaffold
spec, modeled on the `x` adapter's registration.

<details><summary>usage</summary>

```bash
XCHAT_BOT_TOKEN=...    # OAuth2 user access token (identity resolved from GET /2/users/me)
XCHAT_PIN=...          # Juicebox PIN that unlocks the bot's keys
X_CONSUMER_SECRET=...  # optional: verifies webhook signatures
```

```typescript
import { Chat } from "chat";
import { createXchatAdapter } from "@chat-adapter/xchat";
import { createMemoryState } from "@chat-adapter/state-memory";

const bot = new Chat({
  userName: "mybot",
  adapters: { xchat: createXchatAdapter() }, // credentials from env
  state: createMemoryState(),
});

// DMs always
bot.onDirectMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});

// group chats when the bot is @mentioned
bot.onNewMention(async (thread, message) => {
  await thread.post("You rang?");
});

// wire the webhook (e.g. a Next.js route)
export async function POST(request: Request) {
  return bot.webhooks.xchat(request);
}
```

</details>

testing: 109 unit tests, including real-wasm-crypto round trips against
vendored fixture vectors (decrypt + signature verification, webhook
delivery, read receipts, edit age-gating, signed deletes). verified live
against production XChat: DMs, group mentions, media, reactions, edits,
deletes, openDM, cards.

note on the lockfile: `@xdevplatform/xdk@0.6.6` was published <48h ago,
so it was resolved with a one-shot `--config.minimumReleaseAge=0`
override; the locked integrity hash was verified against the npm
registry. the repo policy file is untouched.

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-07-31 23:52:18 +01:00
Max 53bf73db22 fix(telegram): preserve media identity and MIME metadata (#752)
## Summary

Preserve Telegram's stable `file_unique_id` alongside the current
downloadable `file_id` in normalized attachment metadata. Telegram photo
attachments now report `image/jpeg`, including rich-message photos.

This keeps adapter normalization in `@chat-adapter/telegram`;
deduplication remains consumer-owned. After upgrading to the release
containing this patch, Calories can remove
`patches/@chat-adapter__telegram@4.35.0.patch` while retaining its
perceptual-hash fallback for recompressed images.

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-31 17:49:26 +01:00
Max 3c37cfbc15 fix(teams): authenticate protected inline attachments (#749)
## Summary

Authenticates connector-hosted Teams inline attachments through the
configured Bot Framework client, so `message.attachments[].fetchData()`
can retrieve protected content. Teams file download cards use their
direct `content.downloadUrl` anonymously, and serialized attachments
reconstruct the same routing during rehydration.

Bot credentials are limited to non-redirecting HTTPS requests whose
origin exactly matches the Activity connector origin. Cross-origin URLs,
HTTP URLs, and file-card download URLs never enter the authenticated
client path.

File-card MIME inference covers common image and text formats plus PDF,
XLS, and XLSX attachments.

The implementation follows [Microsoft's inline-image access-token
sample](https://learn.microsoft.com/en-us/samples/officedev/microsoft-teams-samples/officedev-microsoft-teams-samples-bot-file-upload-nodejs/)
and [Teams file-card
contract](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4).


Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-31 17:36:36 +01:00
Demo Macro a8867a0abb docs(adapters): add QQ Bot and WeCom community adapters (#753)
Adds two community platform adapters to the docs and `adapters.json`
registry:

- `@agentor/chat-qq` — QQ Bot. WebSocket or webhook (Ed25519) modes; QQ
DM,
  group, and text-channel scenes; rich media.
- `@agentor/chat-wecom` — WeCom (企业微信). Group webhook bots, smart bots
  (callback or WebSocket), and apps; WeCom Template Cards (5 types) and
  AES-256-CBC callback encryption.

Follows the community-adapter flow: docs MDX pages, `meta.json`,
`adapters.json` entries, and `VALID_DOC_PACKAGES`. Community-only
adapters
intentionally omit a `chat/adapters` catalog entry, a `create-chat-sdk`
scaffold-spec, and a changeset.

Signed-off-by: Demo Macro <abc@imst.xyz>
2026-07-31 18:14:05 +10:00
CamdenA21 0153a39f7b feat(modals): add DateInput and NumberInput modal children (#757)
`ModalChild` is `TextInput | Select | ExternalSelect | RadioSelect |
Text | Fields` — there is no date or number primitive. A bot collecting
a renewal date or a quantity has to render a text input with a
`YYYY-MM-DD` hint and validate the string on submit, on every platform,
even though neither platform is the constraint:

- Slack Block Kit has a native
[`datepicker`](https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element)
and
[`number_input`](https://docs.slack.dev/reference/block-kit/block-elements/number-input-element).
- Adaptive Cards has `Input.Date` and `Input.Number`, both already
exported by `@microsoft/teams.cards`.

One addition to the union lifts both surfaces. `ModalSubmitEvent.values`
stays `Record<string, string>`, so this is additive for existing
handlers.

## Changes

- `DateInput` / `NumberInput` element types, builders, and options in
`packages/chat/src/modals.ts`, added to `ModalChild` +
`VALID_MODAL_CHILD_TYPES`.
- JSX/React support: props, component overloads, `modalComponentMap`,
and `fromReactModalElement` branches.
- Slack renderer: `datepicker` (`initial_date`, `placeholder`) and
`number_input` (`is_decimal_allowed`, `initial_value`, `min_value`,
`max_value` — Slack takes these as strings).
- Teams renderer: `Input.Date` / `Input.Number`, in both `modals.ts`
(`@microsoft/teams.cards`) and the dependency-free `modals-primitives`.
- Docs: `docs/modals.mdx` component tables and `docs/api/modals.mdx`
reference + `ModalChild` table.
- Changeset (`chat`, `@chat-adapter/slack`, `@chat-adapter/teams`:
minor).

### Two runtime decode gaps this had to close

- Slack reports a datepicker as `selected_date`, not `value`, so
view-submission flattening now reads `value ?? selected_date ??
selected_option?.value`. `number_input` already arrives as `value`.
- Teams' `Input.Number` submits a JSON **number**, which the previous
`typeof val === "string"` filter dropped silently. Numbers are now
stringified in both `parseDialogSubmitValues` and
`parseTeamsDialogSubmitValues`. This applies to any numeric value a
Teams dialog submits, not only `Input.Number` — a key that used to be
absent from `event.values` is now present as a string. Called out in the
changeset; one existing test updated. Non-scalar values are still
dropped.

### Deliberate asymmetries

- `DateInput` has no `min`/`max` — Slack's `datepicker` has no bounds,
and a prop that silently does nothing on one platform is worse than its
absence.
- `NumberInput.decimal` maps to Slack's required `is_decimal_allowed`.
Adaptive Cards has no decimal switch, so Teams accepts decimals either
way; this is called out in the docs.
- A `DateInput` `initialValue` that is not a real `YYYY-MM-DD` date is
dropped with a warning instead of forwarded. Slack rejects a malformed
`initial_date` by failing the entire `views.open` with
`invalid_arguments` — the modal never opens, and the error surfaces as a
JSON pointer rather than anything actionable. Adaptive Cards just
renders the field empty, so forwarding verbatim would make the same
modal work on Teams and die on Slack. The drop matches how
`filterModalChildren` handles unsupported children. Validation
round-trips through `Date` because it rolls impossible dates over
(`2026-02-31` → Mar 3) instead of rejecting them.

Signed-off-by: CamdenA21 <camden@sandstone.com>
2026-07-31 15:00:44 +10:00
josh c5d86b103e feat(chat): scope agent read tools to the active conversation (#751)
built-in agent read tools now stay inside the conversation they are
handling, so a thread or channel id the model supplies that resolves
elsewhere is rejected before the adapter is called

- `Chat` tracks the conversation being handled across message, action,
slash command and reaction dispatch, using `AsyncLocalStorage`
- read tools (`fetchMessages`, `fetchChannelMessages`, `fetchThread`,
`listThreads`, `getThreadParticipants`, `getChannelInfo`) inherit that
conversation, so existing handlers get this with no code change
- scoping is per channel, so an agent can still follow other threads in
its own conversation
- pass `scope` to set it explicitly, or `scope: false` for
workspace-wide reads

this brings read tools in line with the least-privilege defaults write
tools already have, where `needsApproval` is on unless you opt out

**not breaking:** `scope` is optional and every existing call site keeps
working. agents that run outside a handler, such as a queued job or a
resumed workflow step, have no conversation to inherit and should pass
`scope`

## test plan

- read tools reject out-of-conversation ids and still serve
in-conversation ones, per tool
- the conversation is inherited correctly through message, action, slash
command and reaction dispatch
- concurrent conversations stay isolated, so one agent cannot inherit
another's scope
- an explicit `scope` overrides the handled conversation, and `scope:
false` restores workspace-wide reads
- ids resolve through `adapter.channelIdFromThreadId`, verified against
the slack, teams, google chat, discord, telegram and whatsapp id shapes
- `pnpm validate` clean: workspace tests, typecheck, lint, knip and
build all pass
2026-07-29 09:41:58 +10:00
Max 257a32d01c fix(teams): classify group chats by conversation type (#746)
Fixes `a:`-prefixed Teams group chats being treated as DMs, so they use
non-DM routing and buffered responses.

[Microsoft defines conversation IDs as opaque strings and
`conversationType` as the channel-provided conversation
discriminator](https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#conversation-account).
The adapter therefore stores a classification override only when
`conversationType` disagrees with the legacy ID-prefix heuristic.
Existing subscription and history keys remain stable when the heuristic
was already correct. Explicit group chats also bypass stale DM Graph
context, and Graph-listed child threads retain the override.

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-28 00:34:29 +10:00
christopherkindl f6b64318d9 chore(docs): upgrade geistdocs to 1.16.0 (#747)
Upgrades the docs to `@vercel/geistdocs@1.16.0`.

- Bump `@vercel/geistdocs` 1.15.5 → 1.16.0
- Drop the removed `config` prop from `<Footer />` (1.16.0 replaces the
footer with the Vercel product directory and no longer accepts props)
- Load Geist Sans from the `geist` npm package so the new `ss11`
stylistic set (alternate "I") renders — Google Fonts strips it
2026-07-28 00:27:52 +10:00
Max 54eea71501 feat(telegram): add user allowlist (#742)
## Summary

Add an opt-in `allowedUserIds` Telegram adapter option, with
`TELEGRAM_ALLOWED_USER_IDS` as a comma-separated environment fallback.
Updates from other or unidentified users are ignored before dispatch.

This follows the adapter-level targeting pattern from [the Discord
channel response allowlist](https://github.com/vercel/chat/pull/715),
while enforcing an ingress allowlist instead of expanding mention
routing.

## Test plan

- `pnpm --filter @chat-adapter/telegram test`
- `pnpm --filter @chat-adapter/telegram typecheck`
- `pnpm check`
- `pnpm konsistent`
- `TURBO_CONCURRENCY=2 pnpm validate`

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
2026-07-24 21:06:44 +01:00
christopherkindl 1d0295f996 chore(docs): upgrade geistdocs to 1.15.5 (#743)
- Upgrade `@vercel/geistdocs` to 1.15.5
- New navbar flyout menu style + nav items in flyout menu are rendered
server-side to be in the html for crawlers (as requested by Malte)
- Homepage section titles use Geist sans heading tokens (new vercel.com
style)

| **New flyout menu**  | **Improved docs mobile layout** |
| ------------- | ------------- |
| <video
src="https://github.com/user-attachments/assets/7baf37d9-7a87-4853-91fb-6c3febf974cb"
/> | <img width="499" height="747" alt="image"
src="https://github.com/user-attachments/assets/b48a955e-0e15-4e03-ada2-fbc52c2fdcb7"
/> |

**Preview:** https://chat-git-chore-geistdocs-1155.vercel.sh/
2026-07-24 21:03:56 +01:00
Aamir Jawaid 160140e32b feat(teams): add targeted ephemeral messages (#737)
## Summary

Microsoft Teams supports targeted messages that are visible only to a
selected conversation member, but the Teams adapter did not expose that
native behavior through the SDK's ephemeral-message API. This PR wires
`postEphemeral` for Teams to send native targeted messages while
preserving normal `postMessage` behavior by default.

The adapter now creates explicit targeted outbound activities with
`MessageActivity.withRecipient(recipient, true)` for text and
adaptive-card messages, returns `usedFallback: false`, and keeps the
feature gated behind `thread.postEphemeral()` /
`channel.postEphemeral()`. It also bumps the Teams SDK packages to
`^2.0.13`, adds targeted coverage, updates public docs/matrices, and
includes a changeset.

Live verification found that Teams targeted messages require the app to
be installed in the shared conversation. Group chats and channels both
worked after using the Teams install picker with `Open -> select
placement -> Go`; personal bot chat targeted sends returned a Teams
`BadArgument` response.

## Test plan

Previously validated with:

- `corepack pnpm --filter @chat-adapter/teams exec vitest run
src/index.test.ts --coverage.enabled=false`
- `corepack pnpm --filter @chat-adapter/teams exec tsc --noEmit`
- `corepack pnpm --filter example-nextjs-chat exec tsc --noEmit`
- `corepack pnpm --filter chat exec vitest run src/emoji.test.ts
--coverage.enabled=false`
- `corepack pnpm --filter @chat-adapter/teams exec tsup`
- Targeted `ultracite check` on changed files

Live verified `TeamsAdapter.postEphemeral(...)` in:

- Group chat `Demo Test 2`: Teams UI showed `Only you can see this
message`.
- Channel `General / Teams SDK`: Teams returned message ID
`1784749118197`, and the UI showed `Only you can see this message`.

<img width="884" height="299" alt="Screenshot 2026-07-22 at 12 41 41 PM"
src="https://github.com/user-attachments/assets/cd350ac8-c158-4779-8028-3450eb8670f2"
/>
<img width="1098" height="559" alt="Screenshot 2026-07-22 at 12 41
33 PM"
src="https://github.com/user-attachments/assets/bf6ea12b-ab11-46ee-a3a8-ff5e9583066d"
/>


## Checklist

- [ ] All commits are signed and verified - unsigned commit created
after local GPG/SSH signing was unavailable and user approved continuing
- [ ] `pnpm validate` passes - full validate not run; targeted
validation listed above
- [x] Changeset added (or N/A - see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Co-authored-by: dancer <josh@afterima.ge>
Copilot-Session: 601a7414-48f0-4e6d-ba03-28fa4d2d5c0a
2026-07-23 19:53:13 +01:00
Aradhya C P 09b72e9dd3 fix(whatsapp): stop duplicating card title when posting Card with files (#736)
## Description

Fixes a WhatsApp adapter bug where posting a **Card together with
files** caused the card title (and other card text) to appear twice:
once in the media caption (from `cardToFallbackText`) and again in the
interactive message header/body (from `cardToWhatsApp`).

`postMessageWithMedia` now checks whether the card will be sent as an
interactive message first. If so, it skips using the full card fallback
as the media caption and lets the interactive message own the title,
body, and buttons. Text-fallback cards + files keep the previous caption
behavior (single message, no duplicate text).

## Type of Change

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Refactoring (no functional changes)

## Related Issues

Fixes #735
Closes #735
Related to #735

<!-- Replace # with the issue number after opening the bug report -->

## Changes Made

- In `postMessageWithMedia`, compute `cardToWhatsApp(card)` first and
reuse that result.
- When the card is **interactive**, do not set media caption from
`cardToFallbackText` (empty caption text) so title/body are not
duplicated on the image.
- When the card is **text fallback**, keep captioning media with
`cardToFallbackText` and avoid sending a second text message (existing
behavior).
- Expand unit coverage in `index.test.ts` for interactive + files (title
once, no caption duplication for text/fields, multi-file, audio, HTTPS
attachment) and text-fallback caption behavior.

## Testing

- [x] All existing tests pass
- [x] Added new tests for the changes
- [x] Manually tested the changes

### Test Coverage

- Built the package, then used `pnpm link` to link the built `dist` into
a separate test project.
- Exercised `thread.post({ card, files })` against the reported bug
scenario and confirmed the title no longer appears twice (caption empty
for interactive cards; title only on the interactive message).
- Added / updated unit tests in
`packages/adapter-whatsapp/src/index.test.ts`; all related tests pass
locally (`pnpm --filter @chat-adapter/whatsapp test`).

## Screenshots/Demos

<!-- Paste before/after WhatsApp screenshots here -->

**Before (title duplicated on caption + interactive header):**

<img width="433" height="428" alt="image"
src="https://github.com/user-attachments/assets/37c776fd-b4f6-48f3-a365-6e2073316576"
/>



**After (title only on interactive message; media uncaptioned):**

<img width="428" height="390" alt="image"
src="https://github.com/user-attachments/assets/fb1f43d9-623a-4e22-83ea-dd96cd6d3877"
/>

<img width="408" height="405" alt="image"
src="https://github.com/user-attachments/assets/3959eb04-e722-43f4-8b85-91b724faebcb"
/>


## Checklist

- [x] My code follows the project's code style
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings or errors
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have created a changeset (`pnpm changeset`)
- [x] All commits are signed and verified
- [ ] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [ ] Documentation updated (or N/A)

## Changeset

- [x] I have created a changeset for these changes

<!-- Reminder: behavioural package changes need `pnpm changeset` for
`@chat-adapter/whatsapp` -->

## Additional Notes

- No change to `@chat-adapter/shared`’s `cardToFallbackText` — it
remains correct as a full text fallback. The bug was reusing that full
fallback as a caption while also sending a full interactive card.
- Card-only posts (no files) are unchanged.

---------
2026-07-23 19:18:25 +01:00
Utopia 5eb8b846a7 feat(teams): support outbound reactions (#734)
Outbound Teams reactions were originally implemented as part of #302,
then removed because the Teams feature was not fully rolled out. In [the
follow-up
discussion](https://github.com/vercel/chat/pull/302#issuecomment-4147056867),
the Teams SDK maintainer said they were happy to add the support back
once the rollout was ready. Microsoft now documents agent reaction
support without a preview caveat.

This PR restores that support against the current Teams SDK API:

- implement `addReaction` and `removeReaction` with
`conversations.addReaction` / `conversations.deleteReaction`
- pass native Teams reaction IDs through unchanged and map common
normalized Chat SDK emoji names to their Teams IDs
- upgrade the aligned `@microsoft/teams.*` dependencies to 2.0.14
- update the Teams feature matrices and add a minor changeset

The implementation stays within the existing adapter methods and does
not add another abstraction or affect streaming behavior.

---------

Signed-off-by: Utopia <154325211+Utopi-a@users.noreply.github.com>
2026-07-23 10:52:00 +10:00
Cole 25f30998ce fix(chat): keep attachment/link-only messages in toAiMessages (#713)
- `toAiMessages` previously filtered out any message with empty or
whitespace-only text (`sorted.filter((msg) => msg.text.trim())`). This
discarded messages that carry meaningful content without text — e.g. an
image uploaded with no caption, a file-only upload, or a link-only
message.
- Now messages are kept as long as they have usable content (text,
image/file attachments, or links). Only messages with *none* of those
are skipped.
- When an attachment-only message is included, no empty `text` part is
prepended (an empty text part would be rejected by the AI SDK).
Link-only messages render a standalone `Links:\n...` block.

## Changes

- `packages/chat/src/ai/messages.ts` — drop the text-only pre-filter;
build text conditionally; skip only truly empty messages.
- `packages/chat/src/ai/messages.test.ts` — add tests for image-only,
link-only, interleaved, whitespace-with-attachment, and fully-empty
cases.
- `apps/docs/content/docs/ai/to-ai-messages.mdx` — update the documented
filtering behavior.
- Changeset added (`chat`: patch).

---------

Co-authored-by: Cole Corrente <cole.corrente@snowflake.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-07-22 14:40:41 +10:00
CamdenA21 e06b4b606b feat(teams): forward a custom token factory to the Teams SDK (#732)
`TeamsAdapterConfig` never forwards a `token` field through to the
underlying `@microsoft/teams.apps` `AppOptions.token`, even though the
Teams SDK already supports it as a genuine "bring your own credentials"
escape hatch (`TokenCredentials['token']`).

The only non-secret auth path currently exposed is `federated`, which
maps to `managedIdentityClientId` and only resolves via Azure-native
managed-identity sources (IMDS, AppService, CloudShell, MachineLearning,
ServiceFabric). That's unreachable from serverless/edge runtimes (e.g.
Vercel) that can't hit Azure IMDS but still need to mint access tokens
through an external mechanism (e.g. a workload-identity federation
bridge exchanging a platform-native OIDC token for an Azure AD token).

We've been carrying a local patch on `@chat-adapter/teams` doing exactly
this forwarding to unblock a production Teams bot running on Vercel with
a user-assigned managed identity. Opening this as a proper PR instead of
staying on the patch indefinitely.

## Changes

- `TeamsAdapterConfig.token?: (scope: string | string[], tenantId?:
string) => string | Promise<string>` — matches
`TokenCredentials['token']`'s real signature.
- `toAppOptions` forwards `config.token` straight through.
- `clientSecret` resolution now also short-circuits when `token` is
provided (alongside the existing `federated` check) —
`TokenManager.initializeCredentials` checks `clientId && clientSecret`
before `clientId && token`, so a stray
`appPassword`/`TEAMS_APP_PASSWORD` would otherwise silently win over an
explicitly configured token factory.
- Unit tests in `config.test.ts` and a `createTeamsAdapter` factory test
in `index.test.ts`.
- Docs: added the `token` config option and a third "Authentication
methods" example in `apps/docs/content/adapters/official/teams.mdx`.
- Changeset (`@chat-adapter/teams`: minor).

---------

Signed-off-by: CamdenA21 <camden@sandstone.ai>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-07-22 13:18:59 +10:00
Ben Sabic 4cb7e5d58e feat(chat): durable human-in-the-loop approvals via chat/workflow (#728)
Adds a `chat/workflow` subpath export with `requestApproval()`. This is
the DX from #284, rebuilt on Workflow SDK so the approval survives
deploys, restarts, and arbitrarily long waits. No in-memory promises, no
approvals registry, no restart-recovery machinery: the workflow suspends
on a webhook and resumes when a button is clicked.

`requestApproval()` posts a card with Approve/Deny buttons whose
`callbackUrl` targets a `createWebhook()` URL, suspends the workflow
until a decision (or optional durable-sleep timeout), validates
approvers, finalizes the card in place with the outcome (removing the
buttons, leaving an audit trail), and returns the decision.

```typescript
import { requestApproval } from "chat/workflow";
import type { Thread } from "chat";
export async function deployApproval(opts: { thread: Thread; version: string }) {
  "use workflow";
  const { approved, user, timedOut } = await requestApproval(opts.thread, {
    title: `Deploy ${opts.version}?`,
    fields: { Version: opts.version },
    timeout: "24h",
    approvers: ["U_ALICE", "U_BOB"],
  });
  if (approved) {
    await deploy(opts.version);
  }
}
```

Starting it from a handler is one line. `Thread` instances serialize
across the workflow boundary automatically via the existing
`@workflow/serde` hooks on `ThreadImpl` (requires
`chat.registerSingleton()`):

```typescript
import { start } from "workflow/api";
bot.onNewMention(async (thread, message) => {
  await start(deployApproval, [{ thread, version: parseVersion(message.text) }]);
});
```

**Details**

- `workflow` is a new **optional** peer dependency (same pattern as
`ai`); the subpath is the only code that imports it
- Unauthorized clicks (when `approvers` is set) and unrecognizable
payloads post a notice / are ignored, and the workflow keeps waiting
- On timeout the card is finalized as timed out and the result has
`timedOut: true`
- Card builders (`buildApprovalCard`, `buildResolvedCard`) are exported
for custom flows
- Verified the published `dist` preserves the `"use step"` directives
and down-levels `using` correctly, so the app-side Workflow SDK compiler
handles the library code
- Docs page under Interactivity; changeset (`chat` minor); 8 unit tests
mocking the `workflow` primitives

**Deliberate deviation from #284:** no `thread.requestApproval()`
method. The function must suspend at workflow level, so hanging it off
`ThreadImpl` would make `workflow` a hard dependency of core (or require
prototype patching). The standalone `requestApproval(thread, options)`
keeps the dependency optional.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-07-22 10:18:38 +10:00
Ben Sabic 907450d73f fix(slack): Enterprise Grid support (#724)
## Slack Enterprise Grid Support

Fixes a set of Enterprise Grid gaps in the Slack adapter, found by
auditing the adapter against Slack's current Enterprise documentation
([Developing for Enterprise
orgs](https://docs.slack.dev/enterprise/developing-for-enterprise-orgs/),
[Events API](https://docs.slack.dev/apis/events-api/),
[oauth.v2.access](https://docs.slack.dev/reference/methods/oauth.v2.access/)).

The headline bug: org-wide installs completing OAuth through
`handleOAuthCallback` were stored under a key that webhook token
resolution never looks up, so every subsequent event failed with "no
installation found."

### Org-wide OAuth installs

`oauth.v2.access` returns `team: null` and a populated `enterprise`
object for org-wide installs (`is_enterprise_install: true`). The
callback previously keyed every installation by `result.team.id`. It now
keys org-wide installs by `enterprise.id` — the same key incoming
webhooks resolve tokens by — and records `enterpriseId` /
`isEnterpriseInstall` on `SlackInstallation`. The returned `teamId` is
always the storage key, so it keeps round-tripping with
`getInstallation` / `deleteInstallation` for both install types.

### Socket mode token resolution

Socket-mode events (live and forwarded from a serverless listener)
bypassed the multi-workspace token-resolution block in `handleWebhook`
entirely and dropped `enterprise_id` / `is_enterprise_install` when
rebuilding the payload. The resolution logic is now factored into
helpers (`resolveEventRequestContext`, `runSlashCommand`,
`extractInstallationFromInteractivePayload`) shared by both paths, so
events, slash commands, and interactive payloads resolve
per-installation tokens identically over HTTP and socket. Socket JSON
delivers `is_enterprise_install` as a boolean where form-encoded
webhooks deliver `"true"`; both shapes are handled.

### Event routing via `authorizations[0]`

Slack documents the envelope's `authorizations[0]` — not the top-level
fields — as the authoritative installation identity for an event. The
top-level `team_id` / `enterprise_id` can name a different workspace for
Slack Connect shared-channel events (slackapi/bolt-js#935), and org-wide
envelopes may omit the top-level flags. Token resolution now prefers
`authorizations[0]` with top-level fallback, matching Bolt's behavior.

### Installation-scoped user caches

The user profile cache (`slack:user:*`) and display-name mention reverse
index (`slack:user-by-name:*`) were global across installations. In
multi-workspace deployments, one tenant's cached profile bled into
another, and mention resolution could pick a same-named user from a
different workspace. Both are now prefixed with the current installation
ID (enterprise ID for org-wide installs — correct on Grid, where users
are org-global). Single-workspace keys are unchanged; scoped entries
repopulate on first lookup.

### `team_id` on org-token API calls

Org-wide tokens span every workspace in the org, so workspace-scoped Web
API methods (`conversations.list`, `usergroups.list`, …) require an
explicit `team_id`. `withToken` now injects the event's `team_id` on
calls made under an org-wide install when the caller didn't set one —
Slack documents always passing it as safe ("accepted, but ignored"
elsewhere). When an event arrives from a shared channel hosted on an
"away" workspace, its `context_team_id` is echoed back as
`client_context_team_id`, scoped to calls targeting the originating
channel.

### Event retry deduplication

Retried deliveries (`x-slack-retry-num` header, socket `retry_num`) are
dropped when the original delivery was already dispatched, using an
`event_id` marker in the state adapter (24 h TTL, covering Slack's
opt-in Delayed Events redeliveries). First deliveries pay no state read,
and events whose first delivery never arrived are still recovered via
the retry — preserving the existing missed-event recovery semantics.

### W-prefixed user IDs

Grid users can have `W…` IDs anywhere `U…` appears. The outgoing
bare-mention scanner only recognized `@U…` as a raw user ID; it now
accepts both. This was the only `^U` assumption in the package —
incoming mention parsing was already prefix-agnostic.

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-07-21 09:10:52 +10:00
Ben Sabic bb7cd1241d feat(slack): expose author email on incoming messages (#716)
Slack counterpart to #711: populates `message.author.email` on
normalized incoming Slack messages.

Unlike Teams, no new lookup was needed — `parseSlackMessage` already
resolves the sender via the state-cached `users.info` call
(`lookupUser`), and the cached profile already carried `email`. This
change threads that value onto the author, so there is no additional API
call per message. The email is only present when the app has the
`users:read.email` scope; otherwise (or when the lookup is skipped, e.g.
webhook posts with a `username`) the field stays `undefined`.

Also documents the optional scope in the adapter README and the docs
site manifest section. The core `Author.email` field and serialization
already landed in #711, so this is a Slack-only changeset.

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-07-21 07:44:08 +10:00
Rich Haines d616072f45 chore(docs): update @vercel/geistdocs to 1.13.0 (#727)
## Summary

- update the Chat SDK docs app to `@vercel/geistdocs` 1.13.0
- use the package-owned Tailwind sources introduced since 1.10.0
- align docs content spacing and breadcrumb visibility with the new
container-responsive sidebar


https://chat-git-chore-update-geistdocs.vercel.sh/docs


## Testing

- `pnpm install --force --frozen-lockfile`
- `pnpm --filter docs exec tsc --noEmit`
- `pnpm --filter docs build`
- `pnpm exec turbo typecheck --filter='!example-nuxt-chat'`
- `pnpm exec turbo test --filter='!example-nextjs-chat' --filter='!docs'
--filter='!example-nuxt-chat'`
- production route smoke tests for HTML, `.md`, `Accept: text/markdown`,
agent requests, `llms.txt`, `sitemap.md`, `agents.md`, and `/api/search`
- desktop and mobile browser checks with no runtime errors or horizontal
overflow

`pnpm validate` is locally blocked by the unrelated `example-nuxt-chat`
`oxc-parser` native resolution failure; all remaining workspace
typechecks and tests pass.

Upstream release: https://github.com/vercel/geistdocs/pull/160

Signed-off-by: molebox <rich@vercel.com>
2026-07-20 11:09:18 +02:00
Max 26c052258c feat(discord): add channel response allowlist (#715)
## Summary

Adds an opt-in `respondToChannelIds` Discord adapter option. Non-bot
messages in configured parent channels and their child threads are
routed through mention handlers without requiring an @mention; top-level
messages keep the adapter's existing automatic thread creation, and
forwarded Gateway packets preserve the parent channel for thread
replies.

I understand this might be something you want to keep out but I find it
very useful for my own "Hermes-like" agent :)

## Test plan

- `pnpm --filter @chat-adapter/discord test`
- `pnpm --filter @chat-adapter/discord typecheck`
- `pnpm check`
- `pnpm konsistent`
- `pnpm typecheck`
- `pnpm validate`

## Checklist

- [x] All commits are signed and verified
- [x] All commits are signed off for the DCO (`git commit -s`)
- [x] `pnpm validate` passes
- [x] Changeset added (or N/A — see
[CONTRIBUTING.md](./CONTRIBUTING.md))
- [x] Documentation updated (or N/A)

---------

Signed-off-by: onmax <maximogarciamtnez@gmail.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-07-18 22:29:56 +10:00