Skip to content

fix(slack): read mentions from message content - #947

Merged
bensabic merged 5 commits into
mainfrom
fix/slack-structured-mentions
Sep 17, 2026
Merged

bensabic merged 5 commits into
mainfrom
fix/slack-structured-mentions

Conversation

@privatenumber

@privatenumber privatenumber commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Code samples that contain the bot's id no longer start a conversation
  • Real mentions keep working, including beside a code sample
  • Depends on #946, which lets the adapter's non-mention decision stick

Problem

Chat SDK routes a message to onNewMention when the bot is mentioned, and the Slack adapter decided that from the raw text: it scanned for the bot's id and marked every app_mention event as a mention.

Slack models those two cases differently. A user mention "renders as a mention of a user", while a text element with style.code displays literal code. Both can carry the same id characters:

{ "type": "text", "text": "<@UBOT123>", "style": { "code": true } }
{ "type": "user", "user_id": "UBOT123" }

Only the second is a mention. Because the adapter also searched the text, a message that only documents the bot was treated as an invocation:

  1. A user posts a code sample that contains the bot's id.
  2. Slack delivers the event as app_mention.
  3. The adapter set isMention, and the mention handler ran.

Changes

The adapter now classifies the content the way Slack renders it, instead of scanning every string for the bot's id.

  • A user element for the bot outside code is a mention.
  • A <@U…> token in a text object explicitly typed mrkdwn is a mention.
  • Inline code (style.code) and preformatted elements are not.
  • A rich-text text element, a link label, and a raw_text table cell are display text. A token there stays literal, so it is not a mention.

When a message carries blocks, those blocks model its body and the flattened text field is not consulted, because it loses the code/literal distinction. Attachment blocks are authoritative for their attachment, so an attachment's legacy fallback cannot add mention evidence. Blocks and mrkdwn content are both traversed, so a section block mention is still found.

The unconditional app_mention override is gone. A fully inspected message with no mention reports isMention: false, even when the bot's display name appears as plain text, which #946 preserves. If the adapter cannot identify the bot (botUserId unresolved), an app_mention event is still trusted and other messages stay undetermined.

The routing test in packages/adapter-slack/src/index.test.ts sends a signed webhook through the real adapter and a real Chat instance. Against the pre-fix adapter it fails because the mention handler runs; with the fix it passes. Seven cases were confirmed to fail before the element-type fix, covering raw_text cells, rich-text text elements, link labels, plain_text blocks, and attachment fallbacks.

The Slack integration suite also delivered events as text: "@botname ...". Slack mentions are <@U…> tokens, so those fixtures were corrected to the documented syntax; the operations those tests own (subscriptions, edits, reactions, uploads) are unchanged.

Changed files:

Related PRs

  • Depends on #946: without it, Chat re-detects the mention from the flattened text and the false positive returns

@privatenumber
privatenumber requested a review from a team as a code owner September 16, 2026 10:07
@vercel

vercel Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
chat Ready Ready Preview, v0 Sep 17, 2026 9:34am UTC
chat-sdk-nextjs-chat Ready Ready Preview, v0 Sep 17, 2026 9:34am UTC

Comment thread packages/adapter-slack/src/index.ts Outdated
@privatenumber
privatenumber added this pull request to stack #948 September 16, 2026 11:41
@privatenumber
privatenumber force-pushed the fix/slack-structured-mentions branch from 559ad23 to b2d05aa Compare September 16, 2026 13:21
Base automatically changed from fix/chat-authoritative-mention to main September 17, 2026 08:08
bensabic added a commit that referenced this pull request Sep 17, 2026
## Problem

Chat SDK decides whether to call `onNewMention` from
`message.isMention`, and adapters are expected to set that flag from
platform-specific content. Core then overrides a negative result:

```ts
message.isMention =
  message.isMention || this.detectMention(adapter, message);
```

An adapter can inspect content that plain text cannot. Slack, for
example, distinguishes a [user
mention](https://docs.slack.dev/reference/block-kit/block-elements/user-element/)
from a [code-styled text
element](https://docs.slack.dev/reference/block-kit/block-elements/text-element/),
even when both contain the same characters.

With `||`, that distinction is lost. An adapter that reports `isMention:
false` for a code sample is overridden when text detection finds
`@username` in the flattened message, and the mention handler runs for a
message that mentions nobody.
[#947](#947) depends on this
behavior: without it, the Slack adapter cannot keep literal code out of
mention routing.

## Changes

`message.isMention` is now a three-state value.

| Adapter result | Core behavior |
| --- | --- |
| `true` | Keep the mention |
| `false` | Keep the non-mention |
| `undefined` | Match `@username` in the message text |

An adapter that reads structured content reports `true` or `false`; one
that needs text matching leaves the flag unset. The same rule applies to
messages skipped while draining a queue. Ordinary Linear comments now
leave the flag unset so `@displayName` detection in a comment body keeps
working.

Adapter authors that previously returned `false` to mean "not detected"
should return `undefined` instead. The adapter-building docs now state
this contract, which the previous docs left implicit by only showing the
`true` case.

Changed files:

-
[packages/chat/src/chat.ts](https://github.com/vercel/chat/blob/35c37505/packages/chat/src/chat.ts):
fall back to text detection only when the adapter reports nothing
-
[packages/chat/src/message.ts](https://github.com/vercel/chat/blob/35c37505/packages/chat/src/message.ts):
document the three states
-
[packages/adapter-linear/src/index.ts](https://github.com/vercel/chat/blob/35c37505/packages/adapter-linear/src/index.ts):
leave ordinary comments unset
-
[packages/chat/src/chat.test.ts](https://github.com/vercel/chat/blob/35c37505/packages/chat/src/chat.test.ts):
cover a definitive non-mention, a definitive mention, and a skipped
queued message that must stay a non-mention
-
[packages/adapter-linear/src/index.test.ts](https://github.com/vercel/chat/blob/35c37505/packages/adapter-linear/src/index.test.ts):
pin the Linear fallback contract, where an ordinary comment stays
undetermined and an agent session comment is a mention
-
[apps/docs/content/docs/contributing/building.mdx](https://github.com/vercel/chat/blob/35c37505/apps/docs/content/docs/contributing/building.mdx)
and
[threads-messages-channels.mdx](https://github.com/vercel/chat/blob/35c37505/apps/docs/content/docs/threads-messages-channels.mdx):
state the adapter contract

## Related PRs

- [#947](#947) builds on this
contract to keep literal Slack code references out of mention routing

---------

Signed-off-by: Hiroki Osame <hiroki.osame@gmail.com>
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
Signed-off-by: Hiroki Osame <hiroki.osame@gmail.com>
Signed-off-by: Hiroki Osame <hiroki.osame@gmail.com>
Signed-off-by: Hiroki Osame <hiroki.osame@gmail.com>
Signed-off-by: Hiroki Osame <hiroki.osame@gmail.com>
- Trust `app_mention` when the content never shows the known bot id, so an id Slack reports under another form (Enterprise Grid `W…`) still routes as a mention
- Distinguish a literal bot reference (code or display text) from no reference at all when classifying
- Render `raw_text` table cells literally, matching how Slack displays them and how they are classified
- Classify the bot's mention in the synchronous `parseMessage` path so pre-edit snapshots agree with edited messages
- Match structured `user` elements against the bot id case-insensitively, like the token pattern
- Compute attachment content once per message and share it between classification and rendering
- Add `maskCodeSpans` to `@chat-adapter/shared`, built on the existing code-span scanner, and use it in place of the Slack-local regex masker
- Add a captured `app_mention` fixture with a code-styled `user` element to `sample-messages.md`
- Add tests for the Enterprise Grid id case, code-styled `user` elements, sync classification, lowercase bot ids, and literal `raw_text` rendering
- Update the Slack changeset and add one for `@chat-adapter/shared`

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
@bensabic
bensabic merged commit 683eadc into main Sep 17, 2026
19 checks passed
@bensabic
bensabic deleted the fix/slack-structured-mentions branch September 17, 2026 09:42

This branch was successfully deployed

2 active deployments
Preview – chat — d597d1fe Deployed Sep 17, 2026 by vercel[bot]
Preview – chat-sdk-nextjs-chat — d597d1fe Deployed Sep 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants