Commit Graph

359 Commits

Author SHA1 Message Date
github-actions[bot] f55378a3d8 chore(release): version packages (#378)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-30 13:41:37 -07:00
josh ded6f78088 fix(slack): enrich link previews with unfurl metadata from attachments (#395)
* fix(slack): enrich link previews with unfurl metadata from attachments

* fix(slack): add trailing slash normalization for unfurl URL matching

* fix(slack): store unfurl metadata from message_changed and enrich subsequent messages

* fix(slack): poll for unfurls so link metadata survives the message_changed race
2026-04-30 13:19:22 -07:00
mdnanocom 8a0c7b308d [chat] fix Slack streaming team ID for interactive payloads (#330)
* [chat] fix Slack streaming team ID for interactive payloads

* chore: downgrade changeset to patch

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-29 08:17:05 -07:00
Aamir Jawaid ed46bae52e feat(adapter-teams): native streaming for DMs via emit (#416)
* feat(adapter-teams): use native Teams SDK streaming for DMs

Use ctx.stream.emit() from the Teams SDK for DM streaming instead of
manual post+edit. This sends proper typing activities with streamType
channelData, giving the native streaming UI in Teams.

- Capture IStreamer from activity context in handleMessageActivity
- Block handler with deferred promise so stream stays alive during processing
- streamViaEmit() for DMs: uses stream.emit() with incremental text deltas
- Group chats: accumulate full response and post as single message (no flicker)
- Handle StreamCancelledError and stream.canceled for graceful cancellation

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

* fix(tests): update Teams streaming assertions for accumulate-and-post

Group chats now accumulate streamed chunks and post as a single message
instead of post+edit, so assertions should check sentActivities not
updatedActivities.

* style: format replay-streaming test

* chore: add changeset for teams native streaming

---------
2026-04-29 08:08:29 -07:00
Aamir Jawaid 7a67ff5997 docs(adapter-teams): simplify bot setup using Teams CLI (#402)
* docs(adapter-teams): simplify bot setup using Teams CLI

Replace manual 6-step Azure portal walkthrough with Teams CLI commands.
`teams app create` handles AAD registration, secret generation, bot
registration, and channel setup in a single command. Also updates RSC
permission and troubleshooting sections to reference CLI equivalents.

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

* docs(adapter-teams): remove bot migration section

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

* docs(adapter-teams): clarify message history permissions by context

RSC permissions cover channels and group chats (no admin consent).
Azure AD Chat.Read.All is only needed for DM history. Add permission
table and az CLI commands for DM setup.

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

* docs(adapter-teams): simplify bot setup using Teams CLI

Replace manual 6-step Azure portal walkthrough with Teams CLI commands.
Correct message history permissions: RSC for channels/group chats,
Azure AD only for DM history. Add local dev tunnel tip.

* docs(adapter-teams): add teams status step after login

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 07:59:08 -07:00
Ben Sabic a520797922 feat: add chat.getUser() for cross-platform user lookups (#391)
* feat: add chat.getUser() for cross-platform user lookups

Add UserInfo type and optional getUser() method to the Adapter interface.
Implement on Slack (extends existing lookupUser with email/avatar),
Discord, Google Chat, GitHub, Linear, and Telegram adapters.

Add "Who Am I" button to the example app demonstrating the feature.
Update docs with getUser API reference and usage examples.

* fix(chat): improve getUser across slack and gchat adapters

- slack: return null from lookupUser on failure instead of fallback
  object, removing the isBot === undefined sentinel in getUser
- slack: use image_192 instead of image_72 for better avatar quality
- gchat: cache avatarUrl from webhook sender payload
- gchat: return avatarUrl in getUser response
- gchat: fix tests to use current cache format with isBot field
- docs: document null return, fix example to use message.author

* chore: fix lint

* docs(chat): include Microsoft Teams in getUser supported adapters list

* feat(adapter-teams): add getUser() support (#404)

* feat(adapter-teams): add getUser() via Microsoft Graph API

- Cache aadObjectId from activity.from during webhook handling
- Implement getUser() using Graph GET /users/{user-id} endpoint
- Requires User.Read.All application permission
- Returns null gracefully when user hasn't interacted or Graph call fails

* docs: add getUser() section to Teams adapter README

* chore: apply ultracite formatting to adapter-teams getUser

* fix(chat): cover all 7 adapters in getUser inference and document per-platform constraints

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-04-29 07:58:33 -07:00
josh 70281dc58f feat(chat): add initialOption and option_groups to ExternalSelect (#410)
* feat(chat): add initialOption and option_groups to ExternalSelect

* docs(modals): document ExternalSelect initialOption and option_groups, truncate group label to 75 chars
2026-04-28 09:32:15 +10:00
Ben Sabic e4908d8eb2 docs(state-pg): disclose chat_state_lists and chat_state_queues tables (#430)
* docs(state-pg): disclose chat_state_lists and chat_state_queues tables

The Data model section in the @chat-adapter/state-pg README listed only
three tables, but ensureSchema() also creates chat_state_lists and
chat_state_queues. Add them so the docs accurately describe what the
adapter writes to the database.

Closes #428

* docs(state-pg): document expired row cleanup for lists and queues

Extend the "Expired row cleanup" section to cover chat_state_lists and
chat_state_queues — both have expires_at columns. Note that queue
entries are purged opportunistically per-thread on enqueue/dequeue, and
list entries are filtered on read but never deleted, then add both
tables to the suggested periodic cleanup SQL.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-27 12:23:53 -07:00
Ben Sabic b0ab804f18 - Bundle guide markdown and a templates manifest with the chat package at resources/guides/*.md and resources/templates.json so AI agents can discover Chat SDK resources offline (#423)
- Add scripts/sync-resources.ts (run via pnpm sync-resources) that reads apps/docs/resources-edge-config.json, fetches each guide's .md version over https with a timeout and size cap, writes templates.json, and regenerates the Available resources block in skills/chat/SKILL.md
- Migrate the Slack Next.js, Discord Nuxt, and Hono code-review guides from on-site MDX to Vercel KB and register them in the resources edge-config JSON alongside the existing external guides
- Remove /docs/guides MDX content, sidebar entries, top-level Guides nav entry, getting-started cards, and the dead /guides/ branch in the sitemap route now that all guides live externally and are surfaced on /resources
- Replace the homepage Guides/Templates section and the standalone Adapters pill section with a single two-column Resources + Adapters section (icons, headings, descriptions, outline buttons, divider), and drop the URL footer from ResourceCard on the Resources page
- Update skills/chat/SKILL.md to point at resources/guides and resources/templates.json and list the available guides and templates between marker comments that sync-resources rewrites
- Add tsx to knip's ignoreBinaries so npx tsx in the new script does not fail lint

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-04-27 10:20:44 +10:00
Dima Voytenko 2531e9cff5 feat(slack): dynamic botToken resolver and custom webhookVerifier (#421)
* feat(slack): dynamic botToken resolver and custom webhookVerifier

Allow `botToken` to be a function returning `string | Promise<string>`
so apps can rotate or lazily fetch tokens; the resolver is invoked per
API call. Add `webhookVerifier: (request) => string | Promise<string>`
as an alternative to `signingSecret` for custom request verification —
returns the verified body text or throws to produce a 401.

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

* change verifier signature to make it compatible with  function

* make scheduleMessage cancel() rotation-safe and honor verifier body

- scheduleMessage cancel(): re-resolve token in single-workspace mode so
  rotation works. Slack rotated tokens have a 12h TTL and scheduled messages
  can outlive their schedule-time token, leaving cancel() with stale auth.
  Multi-workspace still snapshots ctx.token since cancel() runs outside
  the AsyncLocalStorage frame.
- webhookVerifier: when it returns a string, use it as the verified body
  for downstream parsing. JSDoc previously implied this contract; the code
  only checked truthiness.
- webhookVerifier JSDoc: explicit SECURITY note that timestamp/replay
  protection is the implementer's responsibility when bypassing signingSecret.
- Tests: cover Attachment.fetchData snapshot semantics — multi-workspace
  uses the ctx token captured at attachment creation; single-workspace
  re-resolves the default provider per fetch (rotation-safe).

* docs(slack): document botToken resolver and webhookVerifier in README

* opt out of SLACK_SIGNING_SECRET env fallback when webhookVerifier is set

A webhookVerifier passed in config was being silently shadowed by
SLACK_SIGNING_SECRET in the env (read by both createSlackAdapter and the
SlackAdapter constructor). An explicit verifier now opts out of that
fallback in both code paths. Added a regression test that stubs the env
var via vi.stubEnv.

* register handleReactionEvent's outer promise via waitUntil

handleReactionEvent does async work (conversations.replies, users.info)
before delegating to chat.processReaction, which is the only point that
registers a waitUntil task. The outer prep work was untracked, so callers
that drained waitUntil tasks could complete before the reaction handler
finished — flaky in CI under tight microtask scheduling. Track the outer
promise too so the full handler is awaited.

* fix(integration-tests): drain waitUntil cascade in test tracker

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-04-26 16:00:37 -07:00
Ben Sabic d630e6c8a7 fix(chat): honor concurrency.maxConcurrent in the concurrent strategy (#419)
Closes #417.

- handleConcurrent now acquires a per-thread semaphore slot when
  maxConcurrent is finite; fast path preserved for the default Infinity.
- Constructor throws on maxConcurrent < 1 (would deadlock) and warns
  when maxConcurrent is paired with a non-concurrent strategy (was
  previously ignored silently).

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-24 11:59:42 -07:00
Aamir Jawaid 885a4717c9 Bump Teams SDK to 2.0.8, use User-Agent header (#415)
* Bump Teams SDK to 2.0.8 and switch to User-Agent header

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

* fix(teams): remove unused ts-expect-error after SDK 2.0.8 upgrade

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-04-23 17:22:36 +01:00
Ben Sabic e8c4b1a6c4 docs: update adapter docs (#412)
* docs: update adapter docs

* docs: update adapter docs

* docs: update adapter docs

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-22 13:28:20 +10:00
Franz Rebaza a179b29141 Implement external_select block kit for Slack (#397)
* feat(slack): external_select's block kit implementation

- block_suggestion handler for slack webgook
- new <ExternalSelect/> to chat modal jsx
- new .onOptionsLoad()

* feat: add tests for new external_selec implementation

* feat(docs): Slack externa_select docs

* chore: changeset

* chore: remove docs from changeset

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-04-21 05:49:51 -07:00
Sergei Patrikeev b9a1961aa5 fix(telegram): MarkdownV2 rendering + telegram-chat reference example (#407)
* fix(telegram): switch parse_mode from legacy Markdown to MarkdownV2

The Telegram adapter hardcoded `parse_mode: "Markdown"` (legacy) but
rendered messages via the SDK's generic `stringifyMarkdown()`, which
emits standard markdown. Two incompatible dialects glued together:

- Standard markdown uses `**bold**`, Telegram legacy uses `*bold*`
- Legacy Markdown has no escape rules — any message with `.`, `!`,
  `(`, `)`, `-`, `_` in unexpected positions was rejected with
  `can't parse entities`, which is virtually every LLM-generated
  response
- Legacy Markdown is deprecated by Telegram and lacks support for
  underline, strikethrough, spoiler, and blockquote

This commit:

- Switches TELEGRAM_MARKDOWN_PARSE_MODE to "MarkdownV2"
- Replaces fromAst() with a proper AST → MarkdownV2 renderer:
  - Single `*bold*`, `_italic_`, `~strike~` markers
  - Context-aware escaping: 20-char matrix for normal text, only
    `` ` `` and `\` inside code blocks, only `)` and `\` inside link
    URLs
  - Headings rendered as bold (MarkdownV2 has no heading syntax)
  - Ordered/unordered lists with escaped dashes and periods
  - Blockquotes with per-line `>` prefix
  - Tables pre-empted and rendered as ASCII code blocks
  - Explicit handlers for reference-style links, images, HTML, and
    definitions so nothing is silently dropped
- Routes card fallback text through `fromMarkdown` (not raw escape)
  with `boldFormat: "**"` — @chat-adapter/shared's cardToFallbackText
  defaults `boldFormat` to "*" (Slack mrkdwn), which would render as
  italic on Telegram. Explicit "**" keeps the card title rendered as
  real MarkdownV2 bold.
- Fixes resolveParseMode so every message routed through the format
  converter (`{markdown}`, `{ast}`, cards, JSX) gets
  `parse_mode: "MarkdownV2"`. Previously only `{markdown}` and cards
  were covered, so `{ast}` messages shipped without parse_mode and
  rendered asterisks literally.
- Documents inbound vs outbound dialects on applyTelegramEntities /
  escapeMarkdownInEntity (inbound entities → standard markdown)
  versus the new outbound MarkdownV2 renderer, so future
  contributors don't confuse the two.

Tests: full 20-char MarkdownV2 escape matrix, context-escape tests
for code blocks and link URLs, nested-formatting tests, edge cases
(empty, whitespace-only, raw HTML), and an end-to-end LLM-output
corpus test that asserts MarkdownV2 validity (no unescaped special
chars outside entities or code blocks). Regression guards added in
index.test.ts for the AST / plain-string / raw parse_mode paths and
for card-title MarkdownV2 bold rendering.

Fixes #226

* feat(examples): add telegram-chat reference bot

Polling-mode Telegram bot that exercises the adapter end-to-end:
MarkdownV2 rendering, interactive cards with inline keyboards,
reactions, file uploads, and streaming edits. Runs with a single
`pnpm --filter example-telegram-chat start`; no webhook, no public
URL, no external API keys.

Menu structure — three categorized sub-menus reached from any DM text:

- Text & Markdown: plain, inline emphasis, code block, links, list+table,
  20-char torture string, LLM-style corpus, streaming editMessage loop
- Cards & Actions: interactive approval card (edits in-place on press),
  callback_data size probe demonstrating the 64-byte limit, LinkButton
- Media & Reactions: on-demand reaction one-shot (briefly subscribes),
  generated 1×1 PNG upload, generated minimal PDF upload

Zero new runtime deps. PNG/PDF are hand-rolled in memory
(lib/png.ts, lib/pdf.ts) rather than pulled from a binary-processing
library. Failure handling is consistent: every demo runner is
try/catch-wrapped and posts an inline ❌ line with the error message.

Excluded from npm release via .changeset/config.json.

* fix(telegram): produce valid MarkdownV2 when truncating long messages

The MarkdownV2 migration widened a latent truncation bug into a reliable
400. The previous truncator sliced at 4096/1024 chars and appended
literal "..." — but in MarkdownV2 `.` is a reserved character, the slice
can leave an orphan trailing `\`, and it can cut through a paired
entity (`*bold*`, `` `code` ``) leaving it unclosed.

Unify the two truncate methods into one `truncateForTelegram(text,
limit, parseMode)` that appends `\.\.\.` for MarkdownV2 and walks back
past unbalanced entity delimiters or orphan backslashes. Plain text
keeps literal `...`. Adds 8 length-limit tests.

Related cleanup:
- Move MarkdownV2 string utilities and Bot API limits to markdown.ts.
- Type renderMarkdownV2 exhaustively on mdast's `Nodes` union with a
  `never` assertion so new node kinds fail the build. Replaces the
  hand-rolled `AstNode` interface. Adds explicit cases for table /
  tableRow / tableCell (throw — preprocessed by fromAst),
  footnoteDefinition, footnoteReference, yaml.
- Introduce `TelegramParseMode = "MarkdownV2" | "plain"` replacing
  `string | undefined`. `toBotApiParseMode` handles the wire mapping.
- Re-export `Nodes` from the chat package; re-export
  `TelegramReactionType` from the adapter entry.

* feat(examples): add length-limit demos to telegram-chat reference bot

Three new menu entries exercise the MarkdownV2 truncation path that the
prior commit fixed:

- Long (5000 plain) — basic truncation, verifies escaped `\.\.\.` ellipsis
- Long (bold crosses 4096) — entity-balancing heuristic for unclosed `*`
- Long (code crosses 4096) — entity-balancing heuristic for unclosed `` ` ``

Each button posts a message whose rendered length exceeds Telegram's
4096-char limit and would have produced `can't parse entities` 400s
against the previous truncator. Serves as an interactive smoke test
alongside the unit tests in packages/adapter-telegram.

* test(telegram): add unit tests for truncation helpers and MarkdownV2 boundary trimming

* docs(telegram): update README to reflect MarkdownV2 parse mode

* chore: unexport trimToMarkdownV2SafeBoundary to fix knip

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-04-21 05:27:47 -07:00
Aamir Jawaid 4c24c94832 fix(adapter-teams): resolve DM conversation IDs for Graph API (#403)
* fix(adapter-teams): resolve DM conversation IDs for Graph API fetchMessages

DM conversation IDs from Bot Framework are opaque and don't work with
Graph's /chats/{chat-id}/messages endpoint. Cache the user's AAD object
ID from incoming activities and construct the correct Graph chat ID
(19:{aadId}_{botId}@unq.gbl.spaces) via a new TeamsGraphContext union.

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

* fix(adapter-teams): simplify graph context branching

Remove redundant type checks — DMs never have threadMessageId so the
channel guard doesn't need an explicit DM exclusion.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-21 01:12:47 +10:00
Hayden Bleasel 7e90d9c8fa Add Slack Socket Mode support (#162)
* Add slack/socket mode dependency

* Update config types and SlackAdapter class

* Add socket mode methods, extract interactive dispatch

* Update createSlackAdapter factory function

* Write tests for socket mode

* Create slack-socket-mode.md

* Run fix

* Fix polynomial regex issues

* Fix: Floating promises in `routeSocketEvent` for slash commands and interactive payloads can cause unhandled promise rejections that crash the Node.js process.


This commit fixes the issue reported at packages/adapter-slack/src/index.ts:1152

**Bug Analysis:**

In `routeSocketEvent` (line 1150), which is a synchronous `void` method, two async operations produce floating promises:

1.  `this.handleSlashCommand(params)` (line 1165) - `handleSlashCommand` is `async` and always returns a `Promise<Response>`. It calls `await this.lookupUser(userId)` which internally calls `await this.chat.getState().get()` (before the try/catch around the API call), and `this.chat.processSlashCommand()`. Any of these could throw.
    
2.  `this.dispatchInteractivePayload(payload)` (line 1172) - Returns `Response | Promise<Response>`. When the payload type is `view_submission`, it delegates to `async handleViewSubmission()`, which calls `await this.chat.processModalSubmit()` and accesses `payload.view.state.values` (which could throw on malformed payloads).
    

Since `routeSocketEvent` is synchronous (`void` return type) and called from a sync context within the socket mode event handler (after `await ack()` has already completed), these returned promises are fire-and-forget. If any reject, it triggers an unhandled promise rejection, which in Node.js 15+ terminates the process by default.

In contrast, in the webhook code path (`handleWebhook`), these same methods are always `return`-ed from async functions, so their promises are properly chained to the caller.

**Fix:**

Added `.catch()` handlers to both floating promises:

1.  For `handleSlashCommand`: Added `.catch()` that logs the error via `this.logger.error`.
2.  For `dispatchInteractivePayload`: Since it returns `Response | Promise<Response>` (only a Promise for `view_submission`), used `instanceof Promise` to conditionally attach a `.catch()` handler only when the result is a Promise.

This approach was chosen over making `routeSocketEvent` async because: (a) it doesn't change the method signature, (b) the caller doesn't need to await it (the ack has already been sent), and (c) errors are logged rather than silently swallowed.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: haydenbleasel <hello@haydenbleasel.com>

* Add socket mode forwarding support to Slack adapter

- Export SlackForwardedSocketEvent type
- Add x-slack-socket-token check at top of handleWebhook() for forwarded events
- Update routeSocketEvent() to accept WebhookOptions and use waitUntil
- Add startSocketModeListener(), runSocketModeListener(), forwardSocketEvent()

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

* Add tests for socket mode forwarding

- Forwarded event accepted/rejected based on appToken
- Bypasses signature verification for forwarded events
- Options passthrough to handlers
- startSocketModeListener returns 200/500 appropriately

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

* Add socket mode cron route and vercel config

- New /api/slack/socket-mode route using createPersistentListener
- Mirrors Discord gateway pattern (CRON_SECRET auth, Redis coordination)
- Cron runs every 9 min, listener duration 10 min

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

* Fix signingSecret defaulting to empty string in socket mode

Make signingSecret optional (string | undefined) instead of falling
back to "". verifySignature now returns false when no secret is
configured, preventing HMAC with an empty key from silently passing.

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

* Wrap event_callback in try-catch in routeSocketEvent

Sync errors from processEventPayload were silently dropped in
socket mode. Wrap with try-catch for parity with slash_commands
and interactive cases.

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

* Use dedicated socketForwardingSecret for forwarding auth

Stop using the Slack app-level token (xapp-...) as the bearer token
for HTTP forwarding. Adds socketForwardingSecret config option
(auto-detected from SLACK_SOCKET_FORWARDING_SECRET) with fallback
to appToken for backwards compatibility.

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

* Replace double cast with type guard for socket event body

Validate body.event exists and construct a properly typed
SlackWebhookPayload instead of using `as unknown as`.

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

* Internalize SlackForwardedSocketEvent type

Remove export — only used internally by the forwarding mechanism.

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

* Fix formatting in socketForwardingSecret check

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

* Add socket mode documentation to Slack adapter README

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

* fix(slack): use SDK envelope type for socket mode event routing

* fix(slack): pass interactive response through ack in socket mode

* feat(chat): add clear modal response action to close entire view stack

* chore: update changeset for clear modal action

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-04-20 08:09:26 -07:00
Hayden Bleasel 7e5b447e1d fix(discord): remove duplicate text when posting card messages (#256)
* Resolves #246

* Add card test coverage for postMessage/editMessage and changeset

* fix(discord): clear content on edit to prevent text persisting alongside card

* test(discord): add card content edge case tests

---------

Co-authored-by: dancer <josh@afterima.ge>
2026-04-17 08:04:14 -07:00
Kelvin Hsu 53c6b688ed fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts (#292)
* fix(slack): guard Slack API calls against empty threadTs to fix invalid_thread_ts

Preserve the intentional empty threadTs for top-level DMs (added in #39
for openDM() subscription matching) while preventing Slack API errors.

Instead of changing the threadTs logic, normalize empty threadTs to
undefined at the entry of each method that calls Slack APIs (postMessage,
postEphemeral, scheduleMessage, stream). This way:

- openDM() subscription matching continues to work (empty threadTs)
- Slack API calls receive undefined instead of "" (no invalid_thread_ts)

The stream method throws ValidationError on empty threadTs (matching
startTyping's early-return pattern) so TypeScript narrows correctly
without any `as string` casts.

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

* Add tests for empty threadTs normalization in stream, scheduleMessage, and postEphemeral

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-17 07:49:35 -07:00
Ben Sabic c26ee6c721 fix(slack): preserve email addresses in @mention regex (#394)
The mention rewrite regex only excluded `<` before `@`, so
`user@example.com` was converted to `user<@example>.com`, breaking
plain emails and `<mailto:…>` links. Extend the lookbehind to also
exclude word characters so emails pass through unchanged.

Fixes #392
2026-04-17 05:58:31 -07:00
josh 9093292ef4 feat(chat): add streaming options to thread.post() (#388)
* feat(chat): add streaming options to thread.post()

* test(chat): add comprehensive tests for PostStreamOptions

* feat(chat): add StreamMessage PostableObject for streaming with options

* refactor(chat): remove PostStreamOptions second param, keep only StreamMessage PostableObject

* refactor(chat): rename StreamMessage to StreamingPlan, fix post() return type

Rename per Malte's feedback - StreamingPlan better describes what the
options control (task grouping, stop blocks for streamed plans).

Fix type safety issue where post<T extends PostableObject>() returned
SentMessage at runtime instead of T. Now awaits handleStream() for
side effects and returns the original StreamingPlan instance.

* test(chat): cover updateIntervalMs-only and fallback paths for StreamingPlan

Remove a duplicated test block, drop a duplicate JSDoc line on Thread.post,
and add tests for posting a StreamingPlan with only updateIntervalMs and
for routing StreamingPlan through the fallback post+edit path when the
adapter has no native streaming.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-17 02:40:13 +10:00
mdnanocom 1c12d33629 wip (#383) 2026-04-16 13:42:22 +10:00
Ben Sabic 37dbb4ac8b feat(chat): add thread.getParticipants() (#386)
* feat(chat): add thread.getParticipants() method

Returns unique human participants in a thread by scanning message
history. Excludes the bot itself. Useful for subscribing only to
1:1 conversations and unsubscribing when others join.

* fix: filter all bots in getParticipants(), not just self

Third-party bots (e.g. Jira, GitHub) were included as participants
because only isMe was checked. Now filters on isBot as well.

* docs: add getParticipants() to Thread API reference and guides

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-15 10:51:20 +10:00
josh 47a9c04fca fix(docs): replace thread.stream() with adapter.stream() in streaming docs (#387) 2026-04-15 10:32:56 +10:00
Nate McGrady bca47924b7 feat: enhance task update structure with optional details field (#385)
- Added a `details` field to the `task_update` type for providing additional context in task updates.
- Updated relevant documentation and test cases to reflect the new field, improving clarity on task progress reporting.
2026-04-15 09:47:34 +10:00
Ben Sabic 608d5f00b0 feat(chat): add chat.thread() for creating thread handles (#380)
* feat(chat): add chat.thread() method for creating thread handles

Allows constructing a Thread handle from a thread ID outside of webhook
contexts, enabling proactive messaging to existing threads.

Closes #148

* docs: add chat.thread() to API reference

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-14 15:12:45 +10:00
josh 1e7c55150e feat: restore attachment fetchData after queue/debounce serialization (#338) 2026-04-13 21:25:21 -07:00
Samy Pessé bc94f0afc4 Multi-tenant support in Linear adapter (#344)
* Support passing scopes as a config to the linear adapter

* Changeset

* Add multi-tenant support for Linear

* Lint

* Cleanup

* Fix tests

* Make organizationId mandatory

* Fix

* Fix fetch of comments

* Start agent sessions support

* Continue cleanup

* Cleanup more

* Simplify more

* Fix

* Fix fetchMessages for agent sessions

* Improve streaming to linear

* Fix agent raw messages

* Fix tests

* Lint

* Simplify

* Work

* Remove debug logs and adapt tests

* Fix organizationId

* Cleanup types

* Cleanup and comment

* Fix webhook handling

* Expose prompt context

* First review pass

* Rework the changeset

* Improve getInstallation and expose refreshInstallation

* Fix error on agent sessions coming from issue delegation

---------

Co-authored-by:  <dancer@users.noreply.github.com>
2026-04-13 20:42:16 -07:00
mdnanocom 0f8b2b150b [slack] [bug] preserve self mentions in multi-workspace installs (#355)
* [slack] preserve self mentions in multi-workspace installs

* add changeset for slack multi-workspace self-mention fix

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-14 13:27:22 +10:00
mdnanocom 53ee151b80 fix(gchat): clear cards when editing a message to text (#372)
* fix(gchat): clear cards when editing a message to text

* add changeset for gchat clear cards fix

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-14 13:17:11 +10:00
Ben Sabic 6b17c603fb feat: add apiUrl config option to all platform adapters (#377)
* feat: add apiUrl config option to all platform adapters

Add a consistent `apiUrl` option across all 8 adapters for custom API
endpoint configuration (e.g. GovSlack, GitHub Enterprise, GCC-High
Teams). Includes env var fallbacks and unit tests for each adapter.

* add changeset for adapter apiUrl feature

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-14 13:01:57 +10:00
Vishal Yathish a8f2aab256 [chat] enable updating task blocks by ID, rather than only most recent (#375)
* [chat] enable updating task blocks by ID, rather than only most recent

* add changeset for updateTask by ID

* test(chat): add tests for updateTask by ID

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: dancer <josh@afterima.ge>
2026-04-14 12:59:49 +10:00
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
Matthew Lewis 691348b74a feat: add test for empty header cells and update markdown conversion logic (#373)
Slack-Thread: https://vercel.slack.com/archives/C08077A6JDB/p1776093728715659?thread_ts=1776093728.715659&cid=C08077A6JDB

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Matthew Lewis <29663600+malewis5@users.noreply.github.com>
2026-04-13 21:35:47 -04:00
Ben Sabic ccc042a608 docs: misc updates and fixes (#370)
* docs: misc updates and fixes

* docs: update feature matrix to show Teams modal support

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-13 20:17:41 +10: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
mdnanocom 4cc87dd888 [gchat] [bug] render markdown links with Google Chat custom link syntax (#356) 2026-04-12 22:31:59 +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
Aamir Jawaid ce7cd2f8f0 feat(adapter-teams): add Select and RadioSelect support (#341)
* feat(adapter-teams): add Select and RadioSelect support in card Actions

Map Select and RadioSelect card elements to Teams Adaptive Card
ChoiceSetInput (compact and expanded styles). Auto-inject a submit
button when inputs exist without explicit buttons, and fan out the
auto-submit payload into individual onAction calls per input.
2026-04-09 11:08:25 -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
Ben Sabic 1856198f6d fix(slack): OAuth redirect handling (#307)
* Fix Slack OAuth redirect handling

* fix slack check

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-09 00:15:37 +10:00
Ben Sabic 21498a4a7f feat(github): add support for issue comment threads (#342)
* feat(github): add support for issue comment threads

Handle issue_comment webhooks on plain issues, not just PRs.
Issue threads use the format github:owner/repo:issue:42.

* fix(github): propagate threadType in GitHubRawMessage for issue threads

parseMessage and other methods now correctly produce issue-format thread
IDs when the raw message originated from a plain issue, not a PR.

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-09 00:15:13 +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
Yejun Su e8dbef2bab fix(adapter-slack): replace empty table cells with single space (#327)
* fix(adapter-slack): replace empty table cells with single space

Slack's Block Kit API rejects cells with empty text fields. Fall back
to a single space in both mdastTableToSlackBlock (markdown.ts) and
convertTableToBlocks (cards.ts) to satisfy the API constraint.

* chore: add changeset for slack empty table cells fix

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-04-07 08:52:23 +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
josh cb1deebd67 fix(github): handle webhook events without repository in multi-tenant mode (#335) 2026-04-06 11:39:54 -07:00