Commit Graph

129 Commits

Author SHA1 Message Date
github-actions[bot] 7f5bea3fe1 Version Packages (#368)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-13 22:08:02 -04:00
josh ddb084b3a6 fix(chat): guard fallback streaming against empty content (#357)
* fix(chat): guard fallback streaming against empty content

* test(chat): add tests for empty content guards in fallback streaming

* chore: add changeset
2026-04-13 18:45:44 +10:00
Hayden Bleasel 2235c160ca fix(chat): export standalone reviver for workflow-safe deserialization (#257)
* fix(chat): export standalone reviver for workflow-safe deserialization

Importing the Chat instance into Vercel Workflow files pulls in adapter
packages that depend on Node.js modules, which aren't available in the
workflow sandbox. This adds a standalone `reviver` function exported from
`chat` that deserializes Thread/Channel/Message objects without needing
a Chat instance or its adapter dependencies.

Also updates the durable chat sessions guide to use the standalone
reviver and dynamic bot imports inside step functions.

Closes #243

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(chat): preserve adapterName on deserialized toJSON and add standalone reviver docs

* fix: sort imports in index.ts

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: dancer <josh@afterima.ge>
Co-authored-by: Ben Sabic <27636870+bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-13 18:34:16 +10:00
github-actions[bot] 63515f2286 Version Packages (#351)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-10 17:01:43 -07:00
Raimond Lume 2700ce85b3 feat(chat): Enable disabling code-fencing for streamed tables per-adapter (#349)
* feat(chat): Enable disabling code-fencing for streamed tables per-adapter

As Slack now natively supports markdown tables, the code-fencing for
tables is not required and in fact stops them from being rendered
natively.

This PR adds an option to configure code-fencing tables per adapter.
By default true, false only for Slack.

* Update StreamingMarkdownRendererOptions comments

Removed comment about default behavior for wrapTablesForAppend option.
2026-04-09 11:21:53 -07:00
Vishal Yathish 097c9b4630 [chat] adding plan/update plan blocks for threads (#115)
* [chat] adding plan/update plan blocks for threads

* refactor plan impl into its own file

* thread.post() instead of thread.postPlan()

* fixes

* lint fixes

* refactor postable objects

* knip

* Refactor PostableObject into dedicated module and add fallback support (#191)

* feat: refactor adapter-slack and types based on Malte's comments

Move PostableObject to dedicated file, update type exports, add fallback posts, and make PostableObject generic.

Slack-Thread: https://vercel.slack.com/archives/C0977L169MW/p1772755978706959?thread_ts=1772755978.706959&cid=C0977L169MW
Co-authored-by: Vishal Yathish <135551666+visyat@users.noreply.github.com>

* fix: fallback path for PostableObject and clean up exports

- Plan mutations (addTask, updateTask, complete) now work in fallback
  mode by using editMessage with fallback text instead of no-oping
- Revert Slack adapter to throw on unsupported kinds (caller handles
  fallback, so adapter-level fallback strings are unnecessary)
- Remove redundant re-exports of isPostableObject/PostableObject from
  plan.ts — export from canonical postable-object.ts only
- Stop exporting POSTABLE_OBJECT symbol (internal implementation detail)
- Fix nested ternary lint error in getFallbackText
- Add tests for fallback post/edit path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Fall back to plain text instead of throwing for unsupported postable object kinds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(chat): deduplicate PostableObject handling, improve type safety and test coverage

---------

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-04-09 09:54:00 +10:00
github-actions[bot] a8dc522c4c Version Packages (#310)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-06 20:21:57 -07:00
Aamir Jawaid 4f5d20029f Add modal support for Teams (#325)
* feat(teams): add dialog (task module) support

Teams dialogs require modal content to be returned inline in the HTTP
response when a task/fetch invoke fires. This adds:

- `actionType: "modal"` on buttons to emit msteams task/fetch hint
- `onOpenModal` hook on WebhookOptions for inline modal interception
- dialog.open/dialog.submit handlers in Teams adapter with Promise.race
- Modal-to-AdaptiveCard converter (modals.ts)
- Bridge adapter sends empty body (not "{}") for dialog close responses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(teams): use @microsoft/teams.cards builders for Adaptive Cards

Replace hand-rolled plain JSON objects and local type definitions with
typed builder classes from @microsoft/teams.cards. This gives compile-time
type safety and eliminates the local AdaptiveCard/AdaptiveCardElement/
AdaptiveCardAction interfaces.

Also fix ephemeral modal button missing actionType="modal", which
prevented the dialog from opening on Teams.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(teams): preserve contextId in modal update/push responses

Pass the original contextId through to re-rendered modals so subsequent
submissions can still retrieve the stored thread/message/channel context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: address review feedback across dialog support

- Clean up timeout timer in handleDialogOpen to prevent resource leak
- Also race on actionPromise so errors surface instead of silently timing out
- Extract buildContinueResponse helper to deduplicate update/push cases
- Use typed TextInputOptions/ChoiceSetInputOptions instead of Record<string, unknown>
- Make processSlashCommand options parameter explicit (WebhookOptions | undefined)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(chat): await storeModalContext before opening modal

The state write was fire-and-forget, so a fast dialog.submit could
arrive before the context was persisted, causing retrieveModalContext
to return empty. Also adds changeset for the new public API surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(chat): add missing disabled prop to ButtonProps JSX interface

The ButtonElement and ButtonOptions already supported disabled, but the
JSX ButtonProps interface was missing it, causing <Button disabled> to
silently drop the prop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback for dialog support

- Add unit tests for modals.ts (16 tests) and modal button actionType in cards.test.ts
- Make dialog open timeout configurable via dialogOpenTimeoutMs in TeamsAdapterConfig
- Fix ModalSubmitHandler type to accept Promise<void> returns, remove @ts-expect-error from example
- Delete stored modal context after retrieval to prevent state adapter leaks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: lint fixes and add X-User-Agent header to Teams adapter

- Fix import ordering, formatting, and non-null assertions in modals.test.ts
- Sort interface members in TeamsAdapterConfig
- Add X-User-Agent: Vercel.ChatSDK header to Teams SDK App client

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:09:25 +10:00
josh 8d8927411d fix: disable source maps in published packages (#337)
* fix: disable source maps in published packages

* chore: add changeset
2026-04-07 09:02:36 +10:00
josh d3f04ec92b docs: add missing JSDoc and adapter-shared README (#339)
* docs(chat): add JSDoc to onModalSubmit and onModalClose

* docs(shared): add README for adapter-shared package
2026-04-06 12:46:13 -07:00
XLor 27b34e153b fix(chat): use adapter to parse channel id (#286)
* fix(chat): use adapter to parse channel id

* test: fix cases use adapter to parse channel id

* chore: update changeset
2026-03-30 09:16:23 -07:00
github-actions[bot] 91820173f4 Version Packages (#300) 2026-03-25 21:36:16 -04:00
Fernando Rojo 4166e09dad Add channelVisibility support to Thread, Channel, and Slack adapter (#51) 2026-03-25 17:21:46 -04:00
github-actions[bot] 434421c82f Version Packages (#295)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-24 14:22:44 -07:00
Malte Ubl c674284885 [RFC] add concurrency strategies for overlapping messages (queue, debounce, concurrent) (#277)
* feat: add concurrency strategies for overlapping messages (queue, debounce, concurrent)

## Problem

When multiple messages arrive on the same thread while a handler is still
processing, the SDK has only one behavior: **lock-and-drop**. The incoming
message is silently discarded (or force-released, which creates uncontrolled
concurrency). This is insufficient for most real-world use cases:

- **AI chatbots** lose user follow-up messages sent while the model is streaming
- **Customer support bots** miss messages entirely, breaking conversation flow
- **Collaborative editing bots** need to coalesce rapid corrections into one action

## Solution

Introduce a new `concurrency` option on `ChatConfig` with four strategies:

### `'drop'` (default, backward-compatible)

Existing behavior. Lock acquired or `LockError` thrown. No changes.

### `'queue'`

Messages that arrive while a handler is running are enqueued in the state
adapter. When the current handler finishes, the queue is drained: **only the
latest message is dispatched**, with all intermediate messages provided as
`context.skipped`. This gives the handler full visibility into what happened
while it was busy, without forcing it to re-process every message sequentially.

```typescript
const chat = new Chat({
  concurrency: 'queue',
  // ...
});

chat.onNewMention(async (thread, message, context) => {
  if (context && context.skipped.length > 0) {
    // "You sent 4 messages while I was thinking. Responding to your latest."
    const allMessages = [...context.skipped, message];
    // Pass all messages to the LLM for full context
  }
});
```

Flow:
```
A arrives  → acquire lock → process A
B arrives  → lock busy → enqueue B
C arrives  → lock busy → enqueue C
D arrives  → lock busy → enqueue D
A done     → drain: [B, C, D] → handler(D, { skipped: [B, C] })
D done     → queue empty → release lock
```

### `'debounce'`

Every message (including the first) starts or resets a debounce timer. Only the
**final message in a burst** is processed. The lock-holding function stays alive
through `waitUntil` during the debounce window.

```typescript
const chat = new Chat({
  concurrency: { strategy: 'debounce', debounceMs: 1500 },
  // ...
});
```

Flow:
```
A arrives  → acquire lock → store A as pending → sleep(debounceMs)
B arrives  → lock busy → overwrite pending with B (A dropped)
C arrives  → lock busy → overwrite pending with C (B dropped)
             ... debounceMs elapses with no new message ...
           → process C → release lock
```

### `'concurrent'`

No locking at all. Every message is processed immediately in its own handler
invocation. Suitable for stateless handlers (lookups, translations) where
thread ordering doesn't matter.

```typescript
const chat = new Chat({
  concurrency: 'concurrent',
  // ...
});
```

## API Surface

### ChatConfig

```typescript
interface ChatConfig {
  concurrency?: ConcurrencyStrategy | ConcurrencyConfig;
  /** @deprecated Use `concurrency` instead */
  onLockConflict?: 'force' | 'drop' | ((threadId, message) => ...);
}

type ConcurrencyStrategy = 'drop' | 'queue' | 'debounce' | 'concurrent';

interface ConcurrencyConfig {
  strategy: ConcurrencyStrategy;
  maxQueueSize?: number;           // Default: 10
  onQueueFull?: 'drop-oldest' | 'drop-newest';  // Default: 'drop-oldest'
  queueEntryTtlMs?: number;        // Default: 90_000 (90s)
  debounceMs?: number;              // Default: 1500
  maxConcurrent?: number;           // Default: Infinity
}
```

### MessageContext (new, passed to handlers)

```typescript
interface MessageContext {
  skipped: Message[];               // Intermediate messages, chronological
  totalSinceLastHandler: number;    // skipped.length + 1
}
```

All handler types (`MentionHandler`, `MessageHandler`, `SubscribedMessageHandler`,
`DirectMessageHandler`) now accept an optional `MessageContext` as their last
parameter. Existing handlers that don't use it are unaffected.

### StateAdapter (new methods)

```typescript
interface StateAdapter {
  enqueue(threadId: string, entry: QueueEntry, maxSize: number): Promise<number>;
  dequeue(threadId: string): Promise<QueueEntry | null>;
  queueDepth(threadId: string): Promise<number>;
}
```

Implemented across all four state adapters:
- **MemoryStateAdapter**: in-process array
- **RedisStateAdapter**: Lua script (RPUSH + LTRIM + PEXPIRE)
- **IoRedisStateAdapter**: same Lua approach
- **PostgresStateAdapter**: new `chat_state_queues` table with atomic dequeue

## Architecture

`handleIncomingMessage` was refactored into composable pieces:

- `dispatchToHandlers()` — shared handler dispatch logic (mention detection,
  subscription routing, pattern matching). Extracted from the old monolithic
  method so all strategies reuse it.
- `handleDrop()` — original lock-or-fail path (preserves `onLockConflict` compat)
- `handleQueueOrDebounce()` — enqueue if busy, drain or debounce after
- `handleConcurrent()` — skip locking entirely
- `drainQueue()` — collect all pending, dispatch latest with skipped context
- `debounceLoop()` — sleep/check/repeat until no new messages arrive

## Queue Entry TTL

Queued messages have a configurable TTL (`queueEntryTtlMs`, default 90s). Stale
entries are discarded on dequeue with a `message-expired` log event. This
prevents unbounded accumulation and ensures handlers don't process messages
that are no longer relevant.

## Observability

All strategies emit structured log events at `info` level:

| Event                 | Strategy         | Data                                  |
|-----------------------|------------------|---------------------------------------|
| `message-queued`      | queue            | threadId, messageId, queueDepth       |
| `message-dequeued`    | queue, debounce  | threadId, messageId, skippedCount     |
| `message-dropped`     | drop, queue      | threadId, messageId, reason           |
| `message-expired`     | queue, debounce  | threadId, messageId                   |
| `message-superseded`  | debounce         | threadId, droppedId                   |
| `message-debouncing`  | debounce         | threadId, messageId, debounceMs       |
| `message-debounce-reset` | debounce      | threadId, messageId                   |

## Backward Compatibility

- Default remains `'drop'` — zero breaking changes for existing users
- `onLockConflict` continues to work but is marked `@deprecated`
- Handler signatures are backward-compatible (new `context` param is optional)
- Deduplication always runs regardless of strategy

## Files Changed

- `packages/chat/src/types.ts` — new types, updated handler signatures
- `packages/chat/src/chat.ts` — strategy routing, drain/debounce loops
- `packages/chat/src/index.ts` — export new types
- `packages/chat/src/mock-adapter.ts` — queue methods for test mock
- `packages/state-memory/src/index.ts` — in-memory queue
- `packages/state-redis/src/index.ts` — Redis queue (Lua)
- `packages/state-ioredis/src/index.ts` — ioredis queue (Lua)
- `packages/state-pg/src/index.ts` — Postgres queue table
- `packages/chat/src/chat.test.ts` — tests for queue, debounce, concurrent

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: comprehensive test coverage for concurrency strategies and queue methods

Add tests across all state adapters and the Chat class:

**MemoryStateAdapter** (8 new tests):
- enqueue/dequeue single entry
- dequeue from empty queue returns null
- dequeue from nonexistent thread returns null
- queueDepth returns 0 for empty queue
- FIFO ordering across multiple entries
- maxSize trimming (keeps newest)
- maxSize=1 debounce behavior (last-write-wins)
- queue isolation by thread
- queue cleared on disconnect

**PostgresStateAdapter** (8 new tests):
- INSERT query for enqueue
- overflow trimming query
- depth return value
- parsed entry from dequeue
- null from empty dequeue
- atomic DELETE-RETURNING for dequeue
- queueDepth return value
- zero depth for empty queue

**RedisStateAdapter / IoRedisStateAdapter** (3+3 existence checks):
- enqueue, dequeue, queueDepth method existence

**Chat concurrency** (5 new tests):
- drop-newest policy when queue is full
- drop-oldest policy evicts oldest entries
- expired entries skipped during drain
- onNewMessage pattern handlers receive context
- onSubscribedMessage handlers receive skipped context

Total new tests: 27 (780 chat + 33 memory + 59 pg)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address feedback

* Support a channel locking strategy, make it default for WhatsApp and Telegram

* docs: fix typo "Committment" → "Commitment" (#274)

Co-authored-by: Arif Kobel <arif.kobel@phorax.com>

* Add webhook verification to GChat (#287)

- Issues a warning if required env vars are not present (also for telegram)
- Makes telegram use a time-safe verifier

* Make adapters depend on `chat` as a real dep (#289)

Without this, changeset will make any dep change a major change

* Version Packages (#290)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Fix serialization

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Arif Kobel <102538661+ArifKobel@users.noreply.github.com>
Co-authored-by: Arif Kobel <arif.kobel@phorax.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-24 13:46:38 -07:00
github-actions[bot] 80a8a34a73 Version Packages (#290)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-23 08:04:36 -07:00
XLor e45a67f491 feat: add adpater disconnect hook (#219)
* feat: add adpater disconnect hook

* fix: use Promise.allSettled for resilient shutdown, add docs and changeset

- Use Promise.allSettled so one failing adapter doesn't prevent others from disconnecting
- Add test for error resilience during shutdown
- Remove unnecessary type guards in tests
- Fix lint: sort interface members, format test file
- Add changeset (minor bump for chat)
- Document disconnect hook in API docs and adapter building guide

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 12:17:37 -07:00
Ray Arayilakath 95fd8ce055 fix(chat): correctly type thread and channel interface (#242)
* fix(chat): correctly type thread and channel interface

* fix(chat): nit make comment match `Message` comment for consistency

* chore: add changeset for thread/channel type fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 11:51:38 -07:00
Ben Sabic 13ba1c73b8 fix: rename step-finish to finish-step for AI SDK v5+ compatibility (#244)
AI SDK v5 renamed the `step-finish` event to `finish-step`, causing
`fromFullStream()` to silently ignore step boundaries and concatenate
multi-step agent output without paragraph separators.

Closes #238

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-03-16 11:41:12 -07:00
github-actions[bot] dcd7af3656 Version Packages (#251)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-16 03:57:36 -04:00
github-actions[bot] f7cc3fa00f Version Packages (#228)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-12 10:04:30 -07:00
Matan Kushner 8d88b8c0e2 fix(github): accumulate stream before posting, log fallback edit errors (#227)
* fix(github): accumulate stream before posting, log fallback edit errors

The GitHub adapter relied on chat-sdk's default fallbackStream which
posts a placeholder then edits it every 500ms. GitHub returns 422 on
these edits because body is empty during TTFT, and rapid edits risk
secondary rate limits.

Add stream() to GitHubAdapter that accumulates the full text before
posting once. Also log fallbackStream edit errors instead of silently
swallowing them.

* test: add tests for GitHub adapter stream() and fallbackStream logging

* style: format with biome

* refactor: use Logger instead of console.warn in fallbackStream

Plumb the Chat logger into ThreadImpl so fallbackStream uses the
structured logger instead of raw console.warn.

* test: simplify fallbackStream logging test

Reuse mockLogger from mock-adapter.ts and rely on createMockAdapter's
default editMessage mock instead of re-specifying the resolved value.

* test: use vi.mocked, drop redundant stream=undefined

* chore: add changeset

* refactor: extract accumulateStream utility, deduplicate GitHub and WhatsApp adapters

* Revert "refactor: extract accumulateStream utility, deduplicate GitHub and WhatsApp adapters"

This reverts commit 0a3ca3cc9a.
2026-03-12 08:31:42 -07:00
Malte Ubl e20637158d feat: add LinkPreview to Message for URL and embedded message support (#217)
* 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>
2026-03-12 07:23:09 -07:00
github-actions[bot] 472c1847cd Version Packages (#210)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-11 10:05:33 -07:00
Achraf Ghellach 60f5d8e19f feat: add WhatsApp Business Cloud API adapter (#102)
* feat: add WhatsApp Business Cloud API adapter

Add @chat-adapter/whatsapp with support for sending/receiving messages,
reactions, interactive reply buttons, typing indicators, and webhook
verification via the Meta Graph API. Includes full test suite,
documentation updates, and workspace/turbo configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add media download, attachments, and location support to WhatsApp adapter

- Add downloadMedia() public method for fetching images, documents,
  audio, video, and stickers via the Graph API (two-step: URL then binary)
- Populate message attachments with lazy fetchData() for all media types
- Add location support with Google Maps URL and structured text
- Add audio, video, sticker, and location fields to WhatsAppInboundMessage
- Set isMention: true on all messages (WhatsApp DMs are always direct)
- Update parseMessage to include attachments and isMention
- Add 10 new tests covering all media types, locations, and isMention
- Update docs feature matrix to reflect media receive support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address PR review feedback for WhatsApp adapter

- Validate Graph API response before accessing messages[0].id in
  sendTextMessage and sendInteractiveMessage
- Escape backticks and backslashes in escapeWhatsApp()
- Apply escapeWhatsApp() to renderText() content in all style branches
- Use webhook phoneNumberId in buildMessage() instead of this.phoneNumberId
- Encode proper threadId in parseMessage() instead of empty string
- Strict decodeThreadId() validation (exactly 2 segments after prefix)
- Add tests for extra segments in decodeThreadId and threadId in parseMessage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Migrate improvements from #179

Bring over several enhancements from chitru's WhatsApp adapter PR (#179):

- Voice message support (separate from audio)
- Legacy button response handling (template quick replies)
- Callback data encoding/decoding for interactive reply round-trips
- Message truncation at WhatsApp's 4096 char limit
- Example app integration (adapters, webhook route, package.json)
- GET webhook forwarding for WhatsApp verification challenges
- Package README and changeset
- Tests for all new functionality (68 total)

Co-Authored-By: Chitru Shrestha <chitra.shrestha@akuru.com.au>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): add error handling for inbound message processing

Wrap handleInboundMessage calls in try/catch to log errors if
synchronous processing fails (e.g., thread ID encoding). The async
processing already has its own error handling in Chat.processMessage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): prevent markdown regex from matching across newlines

Use [^\n*] and [^\n~] in fromWhatsAppFormat regex to prevent bold/strike
spans from merging across line boundaries. Adds a regression test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): use WhatsAppInteractiveMessage type instead of object

Replace the untyped `object` parameter in sendInteractiveMessage with
the proper WhatsAppInteractiveMessage type for full type safety.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): hoist emoji mapping to module-level constant

Move the emoji name-to-unicode mapping out of resolveEmoji() so it is
not re-allocated on every call.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): remove duplicate JSDoc comment in types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(example): add startTyping to WhatsApp recording methods

The adapter supports typing indicators but the method was missing from
the recording proxy list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): fix formatting and add package to readme test allowlist

Fix line-length formatting in markdown.ts regex and add
@chat-adapter/whatsapp to the valid packages list in readme tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): use defaultEmojiResolver instead of custom emoji map

Replace the hand-rolled EMOJI_MAP with the shared defaultEmojiResolver
from the chat SDK. WhatsApp uses unicode emoji like GChat, so toGChat()
provides the correct mapping with broader coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): make Graph API version configurable

Add apiVersion option to WhatsAppAdapterConfig (defaults to v21.0)
so users can upgrade without waiting for a package release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): validate lat/lng before constructing Google Maps URL

Coerce and validate latitude/longitude with Number.isFinite() to
prevent unexpected URL construction from malformed webhook payloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): throw on editMessage instead of silently sending new message

Callers expecting an edit would get duplicate messages with the silent
fallback. Throwing makes the unsupported operation explicit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): document regex asymmetry between toWhatsApp and fromWhatsApp

Explain why toWhatsAppFormat doesn't need newline guards like
fromWhatsAppFormat does — the standard markdown parser output
never produces spans crossing line boundaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): document callback data passthrough behavior

Add comments explaining that non-prefixed and malformed callback data
is intentionally passed through for legacy/external button IDs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): add editMessage and deleteMessage to recording methods

Include all adapter methods in the recording list for complete
debugging traces, even for unsupported operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): preserve escaped formatting chars in toWhatsAppFormat

Escaped asterisks and tildes in standard markdown (e.g. \* and \~) are
now preserved through the conversion pipeline so WhatsApp renders them
as literal characters instead of misinterpreting them as formatting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): split long messages instead of truncating

Replace silent truncation at 4096 chars with message splitting that
breaks on paragraph (\n\n) then line (\n) boundaries, sending multiple
messages so no content is lost. Adds 8 tests for the splitting logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(whatsapp): align editMessage/deleteMessage behavior and docs

- Fix README: editMessage/deleteMessage both throw, not fallback/no-op
- Fix editMessage JSDoc to reflect it throws
- Make deleteMessage throw instead of silently warning (consistent with editMessage)
- Bump @types/node to ^25.3.2 to match monorepo
- Add sample-messages.md with webhook payload examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(whatsapp): add adapter documentation page

Add whatsapp.mdx covering installation, usage, Meta app setup,
webhook config, interactive messages, media attachments, 24-hour
messaging window, configuration, features, and troubleshooting.
Also add WhatsApp to the adapters navigation in meta.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add whatsapp adapter debug logging and try/catch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(whatsapp): convert emoji placeholders in outgoing messages

WhatsApp adapter was sending raw {{emoji:wave}} placeholders instead of
Unicode emoji. Apply convertEmojiPlaceholders on all outgoing paths:
text messages, card fallback text, and interactive message fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix button rendering and streaming (needs to buffer)

* fix(example): handle editMessage failure on WhatsApp

WhatsApp Cloud API doesn't support message editing. Catch the error
in the demo "processing" animation and send a follow-up instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(chat): add onDirectMessage handler, stop treating DMs as mentions

DMs now route to dedicated onDirectMessage handlers instead of being
forced through onNewMention. If no DM handlers registered, DMs fall
through to onNewMention for backward compat. Adapters no longer set
isMention=true for DMs — the Chat SDK handles routing via adapter.isDM().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(chat): always route DMs to onDirectMessage regardless of subscription

Previously, onDirectMessage only fired for unsubscribed DM threads.
Subscribed DMs were routed to onSubscribedMessage, which was confusing
on non-threaded platforms (WhatsApp, Telegram) where all DMs share one
threadId — after the first message, onDirectMessage never fired again.

Now, DMs always route to onDirectMessage first, and onSubscribedMessage
only handles non-DM subscribed threads. Backward compat is preserved:
if no onDirectMessage handlers are registered, DMs fall through as
mentions.

The example bot is simplified accordingly — onDirectMessage now fetches
conversation history via fetchMessages each time instead of relying on
subscribe() and stored state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(chat): pass channel as third argument to DirectMessageHandler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(example): reply to channel instead of thread in DM handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(example): use thread instead of channel for DM operations

Channel ID is only two parts (whatsapp:{phoneNumberId}) which isn't a
valid conversation target on WhatsApp. The thread ID includes the user
phone and is required for startTyping/post/fetchMessages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix(example): use thread instead of channel for DM operations"

This reverts commit f4801d7015.

* fix(adapters): return valid thread IDs from channelIdFromThreadId

WhatsApp's channelIdFromThreadId was stripping the user WA ID, producing
an invalid ID that caused ValidationError on channel operations like
startTyping(). Since every WhatsApp conversation is a 1:1 DM, channel
and thread are identical.

Telegram's channelIdFromThreadId was returning a raw chatId without the
telegram: prefix, which is not a valid thread ID for adapter operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(chat): normalize fullStream in Channel.post() to extract text deltas

Channel.post() was coercing AI SDK fullStream objects to strings via +=,
producing "[object Object]" output. Now uses fromFullStream() to extract
text-delta events, matching how Thread.post() already handles streams.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(example): use thread.allMessages for DM history instead of adapter directly

The DM handler was calling channel.adapter.fetchMessages() which always
returns empty on WhatsApp (no native history API). Now uses
thread.allMessages which falls back to the persisted message history
cache, giving the AI conversation context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(chat): add message history support to Channel for DM platforms

Channel now falls back to the persisted message history cache when the
adapter lacks native message fetch (e.g. WhatsApp, Telegram). Incoming
messages are persisted under both thread and channel IDs. Outgoing
messages from channel.post() are also persisted.

The example DM handler now uses channel.messages instead of calling the
adapter directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(whatsapp): improve markdown rendering and remove broken typing indicator

- Convert headings to bold text, thematic breaks to text separators,
  and tables to code blocks (WhatsApp doesn't support these)
- Convert standard italic (*text*) to WhatsApp italic (_text_) since
  WhatsApp uses *text* for bold
- Make startTyping a no-op (Cloud API doesn't support typing indicators)
- Update channelIdFromThreadId test for channel===thread change

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(example): reverse channel.messages to chronological order for AI

channel.messages yields newest first but AI expects chronological order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(chat): auto-sort messages chronologically in toAiMessages

toAiMessages now sorts by dateSent (oldest first) so callers don't need
to worry about iteration order from channel.messages or thread.messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(chat): pass accumulated stream text as markdown in Channel.post()

Stream text was posted as a plain string, bypassing the adapter's format
converter. Now wraps it as { markdown: accumulated } so headings, bold,
italic etc. are properly converted for each platform.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(whatsapp): use stringifier options for emphasis and bullets

Use emphasis: '_' and bullet: '-' options in stringifyMarkdown so the
only * in output is **strong**, avoiding conflicts between list bullets
and italic markers. Simplifies toWhatsAppFormat to only convert
**bold** -> *bold* and ~~strike~~ -> ~strike~.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(whatsapp): flatten bold inside headings to avoid triple asterisks

When AI outputs headings with bold text like `## **Choose React if:**`,
the heading-to-bold conversion created nested strong nodes producing
`***text***`. Now flattens strong children in headings so they merge
into a single bold span.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(whatsapp): add full toBe assertion for complex markdown conversion

Also use ━━━ for thematic breaks instead of --- to avoid remark escaping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add WhatsApp replay tests from production recordings

Adds WhatsApp DM replay test infrastructure:
- Fixture from real webhook recordings (dm/whatsapp.json)
- WhatsApp test utilities with HMAC-signed request factory and
  Graph API fetch mock (whatsapp-utils.ts)
- 6 replay tests covering DM handling, thread/channel IDs, message
  sending, status update filtering, sequential messages, and
  message history persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(whatsapp): fix type narrowing in replay test after merge

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update WhatsApp logo

* Update adapters.json

* Update logos.tsx

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Chitru Shrestha <chitra.shrestha@akuru.com.au>
Co-authored-by: Malte Ubl <malte.ubl@gmail.com>
2026-03-10 15:16:10 -07:00
github-actions[bot] 7ba7465071 Version Packages (#200)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-09 23:10:49 -07:00
Ben Sabic dac88f8ad1 docs: mention PostgreSQL state adapter across remaining docs (#201)
Several docs pages only referenced Redis as the production state
adapter. Add PostgreSQL mentions to usage page, guide "Next steps"
sections, adapters overview, SKILL.md, and package README.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-03-08 12:43:33 -07:00
Ben Sabic 5b41f0869d feat: add scheduled message support via Slack's chat.scheduleMessage API (#202)
Add thread.schedule() and ScheduledMessage type for scheduling messages
to be sent at a future time. The Slack adapter implements scheduling
natively via chat.scheduleMessage with cancel() support via
chat.deleteScheduledMessage. Other adapters throw NotImplementedError.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-03-08 06:53:21 -07:00
Hayden Bleasel c4b0e69012 Misc improvements 2026-03-08 00:02:53 -08:00
Georgios Konstantopoulos eb49b2a9d1 feat: add forceReleaseLock + onLockConflict for interrupt/steerability (#193)
* feat: add forceReleaseLock and onLockConflict for steerability

Amp-Thread-ID: https://ampcode.com/threads/T-019cc675-20e8-73db-b852-5690bafe0008
Co-authored-by: Amp <amp@ampcode.com>

* fix: add forceReleaseLock to mock state adapter

Implements the new StateAdapter.forceReleaseLock method in the mock
adapter so tests using createMockState() don't break.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: support async onLockConflict callbacks

Allow the onLockConflict callback to return a Promise, enabling
users to check external state (e.g. DB queries) before deciding
whether to force-release or drop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add tests for forceReleaseLock and onLockConflict

Covers: default drop behavior, force mode, sync/async callbacks
returning force/drop, and forceReleaseLock on memory adapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add changeset for forceReleaseLock and onLockConflict

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: alphabetize interface members, add forceReleaseLock to ioredis adapter

Biome enforces alphabetical ordering on interface members. Also adds
the missing forceReleaseLock implementation to the ioredis state adapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing @chat-adapter/state-ioredis to changeset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add comment noting race window after force-release

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add forceReleaseLock tests for ioredis adapter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: assert lock re-acquisition after force-release

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add forceReleaseLock tests for state-redis adapter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: replace fragile acquireLock call count with last-call assertion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: document concurrent handler execution after force-release

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use .at(-1) and single-line format for biome compliance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add forceReleaseLock to state-pg adapter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: document onLockConflict and forceReleaseLock

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-07 23:43:40 -08:00
github-actions[bot] 28cbcb2994 Version Packages (#192)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-07 12:39:16 -08:00
Malte Ubl a3cfc1aee3 Support native tables in slack (#194)
* Support native tables in slack

Brand new per https://docs.slack.dev/reference/block-kit/blocks/table-block/
Only one per message.

* Record streams

* Error handling

* Debugging

* fix-delta

* fix-delta2

* fix-delta3

* fix-delta4

* fix-delta5

* show-table

* show-table2

* replay-test

* changeset

* lint-types
2026-03-07 12:31:18 -08:00
github-actions[bot] 3f881daf07 Version Packages (#186)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-06 00:01:23 -08:00
Hayden Bleasel cc65dc3111 fix: atomic message deduplication to prevent silent app_mention drops (#189)
* Add setIfNotExists

* Update chat.test.ts

* Improve tests

* Create bright-regions-dance.md
2026-03-05 16:16:03 -08:00
Hayden Bleasel 5cfd9be5fa Fix JSX issues (#161)
* Fix JSX issues

* fix: add null guards for event.thread in onAction handlers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 16:13:33 -08:00
github-actions[bot] e937e0d3dd Version Packages (#181)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-04 15:27:16 -08:00
zm2231 f27c89b424 feat(slack): streaming StreamChunk support + url_verification fix (#173)
* feat(slack): support streaming task_update, plan_update, markdown_text chunks

- Add StreamChunk type for structured streaming content
- Extend stream() to accept AsyncIterable<string | StreamChunk>
- Add taskDisplayMode option to StreamOptions
- Slack adapter passes structured chunks directly to Slack's streaming API
- Handle url_verification before signature check to prevent Slack from
  silently disabling event delivery when signing secret is misconfigured
- Add tests for StreamChunk handling in thread.post()

* review fixes: type safety, security, dedup

- Replace loose StreamChunk interface (index signature) with
  discriminated union (MarkdownTextChunk | TaskUpdateChunk | PlanUpdateChunk)
- Move url_verification back after signature check to prevent
  unauthenticated challenge responses
- Remove redundant AsyncIterable<string> from PostableMessage union
- Extract pushTextAndFlush helper to deduplicate markdown flush logic
- Remove unnecessary `as string` casts after type guards
- Export individual chunk types from package index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add changeset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 11:31:30 -08:00
John Pham 130e780418 fix openModal crash when action has no thread context (#174)
* fix openModal crash when action has no thread context

Home tab actions (e.g. Change Team) have no thread → openModal closure
crashed accessing thread.channel on null. Guard thread access in the
openModal closure and pass undefined to storeModalContext.

Add test for empty threadId action → openModal works with null thread.

* add changeset

* make ActionEvent.thread nullable, remove unsafe cast

- Change `ActionEvent.thread` type from `Thread` to `Thread | null`
- Remove `null as unknown as Thread` unsafe cast in handleActionEvent
- Update test files to use null guards and optional chaining

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 11:15:33 -08:00
Utopia f0dfa4d64d fix: preserve nesting depth when rendering lists in all chat adapters (#177)
* fix: preserve nesting depth when rendering lists in all chat adapters

All format converters (Slack, Discord, Teams, Google Chat) were flattening
nested lists during Markdown→platform conversion. When a listItem had a
paragraph followed by a child list, the child list output was concatenated
directly onto the parent item's text with no indentation or separating
newline, producing garbled output like "• parent• child 1\n• child 2".

Add an optional `depth` parameter to each adapter's private `nodeToX()`
method. The `isListNode` handler now builds lines explicitly: paragraph
content is prefixed with the bullet/number at the correct indent level
(`"  ".repeat(depth)`), and nested list nodes are rendered recursively
at `depth + 1`. Sibling items at the same depth are unaffected.

Add regression tests covering:
- nested unordered lists (2 levels)
- nested ordered lists (2 levels)
- deeply nested lists (3 levels)
- flat sibling items (no regressions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: extract renderList() helper to eliminate dead depth parameter

The previous fix added `depth = 0` to each adapter's general nodeToX()
method, but only the isListNode branch ever used it — a dead parameter
across ~10 other branches that made the signature misleading.

Extract a private renderList(node: List, depth: number) method in each
adapter. nodeToX() stays as a clean single-argument node converter;
all depth-aware list logic lives exclusively inside renderList(), which
recurses into itself for nested lists and calls nodeToX() for leaf nodes.

Import the List type from the chat package to give renderList() an
explicit, narrowed parameter type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Deduplicate renderList into BaseFormatConverter, fix start offset, add mixed list tests

- Extract shared renderList() into BaseFormatConverter with configurable bullet char
- Respect List.start property for ordered lists starting at non-1 values
- Remove dead isListItemNode branches from all adapters
- Add mixed ordered/unordered nesting test to all adapters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
2026-03-04 11:06:32 -08:00
Malte Ubl ff954f938b Correctly handle AI SDK stream concatenation (#170)
* Correctly handle AI SDK stream concatenation

* changset

* docs

---------

Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
2026-03-04 11:02:02 -08:00
github-actions[bot] 0ab742c62d Version Packages (#133)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-04 00:32:57 -08:00
Nicolás Montone f0c7050349 add member_joined_channel event support to Slack adapter (#167)
* Add member_joined_channel event support to Slack adapter

* changset

* Apply suggestion from @NicolasMontone

* fix
2026-03-03 16:24:40 -08:00
Malte Ubl 02e7ef605c Implement markdown and AST rendering for tables across adapters (#160)
* Implement markdown and AST rendering for tables across adapters

* Render the default instead

* Fix bot app

* lint

* streaming-rendering

* fix-streaming

* fix-streaming-agent

* fix-streaming-again2

* fix-streaming-again3

* prefix tests

* no final edit

* changeset

* feedback

* lint

* Copilot feedback
2026-03-03 09:43:18 -08:00
Hayden Bleasel 73de82dbef Add remend for streaming markdown healing (#163)
* Add remend

* Create sixty-wasps-hope.md

* feat: wrap message content in markdown objects (#164)

Update 'fallbackStream' and 'handleStream' to send '{ markdown: string }'. Adjust tests for '{ markdown: string }'.

Slack-Thread: https://vercel.slack.com/archives/C08077A6JDB/p1772491650055389?thread_ts=1772491650.055389&cid=C08077A6JDB

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>

---------

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
2026-03-03 07:59:58 -08:00
Hayden Bleasel 8a6f0ce8fa Improve test coverage 2026-03-02 14:42:32 -08:00
Hayden Bleasel d93d5470f6 Add unit tests for newline streaming issue 2026-03-02 14:17:34 -08:00
Hayden Bleasel b38fda49fa Fix formatting 2026-03-02 13:10:22 -08:00
handlebauer 9522b04243 feat(chat, adapter-gchat, adapter-discord): add disabled prop to Button (#150)
* feat(chat, adapter-gchat): add disabled prop to Button

Google Chat Cards v2 natively supports `disabled` on buttons, but
`ButtonOptions` / `ButtonElement` didn't expose it. Add the prop to
the core type and thread it through the gchat adapter serializer.

Closes #149

Made-with: Cursor

* feat(adapter-discord): thread disabled prop through Discord button serializer

DiscordButton already had `disabled` in its type but convertButtonElement
never set it from ButtonElement. Wire it through so disabled buttons
render correctly on Discord too.

Made-with: Cursor
2026-03-02 12:39:16 -08:00
Vincenzo Domina f5a75c9680 feat(chat): allow disabling fallback streaming placeholder (#139)
* feat(chat): allow disabling fallback streaming placeholder

Add ChatConfig options to delay initial post+edit placeholder and wait for real text before creating the streamed message. Improves ordering for Telegram and other fallback-streaming adapters.

Made-with: Cursor

* fix(chat): simplify fallback streaming placeholder

* Create fallback-streaming-placeholder.md

* Improve test

* Add new test

* Run fix

* Update streaming.mdx

---------

Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
2026-02-28 19:18:37 -08:00
Hayden Bleasel 8a9a6d2374 Upgrade vitest 2026-02-27 09:58:28 -08:00