Skip to content

fix(slack): read Block Kit message bodies from layout blocks - #1062

Merged
bensabic merged 3 commits into
vercel:mainfrom
privatenumber:fix/slack-layout-block-body
Oct 19, 2026
Merged

bensabic merged 3 commits into
vercel:mainfrom
privatenumber:fix/slack-layout-block-body

Conversation

@privatenumber

@privatenumber privatenumber commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A bot now receives the content of Block Kit messages that apps and workflows post, instead of only their notification fallback
  • When an app mentions the bot from a section block, the handler gets the request it was invoked for
  • When the bot reads thread history, messages from alerting and deploy apps include the details shown in Slack
  • Messages typed in a Slack client are unchanged

Problem

A Slack message can carry both text and blocks. When it carries blocks, Slack renders the blocks and uses text only as "a fallback string to display in notifications" (chat.postMessage). Apps and workflows rely on that: the fallback is often a one-line summary, and the content people read is in the blocks.

The adapter builds message.text and message.formatted from text. From the message's own blocks it reads only tables. Text in section, header, and context blocks never reaches the bot, so the bot works from the notification summary while people in the channel see the full message.

This affects a bot in two ways.

An app mentions the bot. An internal tool asks the bot to act:

{
  "type": "app_mention",
  "text": "New request",
  "blocks": [
    { "type": "section", "text": { "type": "mrkdwn", "text": "<@U_BOT> please check the deploy" } }
  ]
}
  1. Slack fires app_mention for the token in the section block, even though text has none.
  2. The adapter reads the mention from the block and reports isMention: true.
  3. It builds the body from text, so message.text is New request.
  4. onNewMention runs without the request it was invoked for, and the bot can only ask what to do.

The bot reads an app's message in history. A deploy app posts text: "Deployment failed" with the error, project, and links in section blocks. A user replies in the thread and asks the bot what went wrong. fetchMessages, fetchChannelMessages, and fetchMessage parse messages the same way, so the bot's view of the alert is Deployment failed, without the details the user is looking at.

Changes

When a message has no rich_text blocks, text from its layout blocks becomes the body, in block order:

  • section text and fields, header text, and context text elements
  • mrkdwn objects keep their markup and mentions, which resolve to display names like the rest of the body
  • plain_text objects render literally, so * and _ stay characters

The same message now gives message.text = @Test Bot please check the deploy.

These rules keep existing behavior:

  • Typed messages keep text. A Slack client sends rich_text blocks that text mirrors, so a message with any rich_text block keeps the text body. #1002 covers composer lists in that path.

  • Blocks without text keep text. When the layout blocks carry no text, as in a message with only buttons or images, text stays the body.

  • Table placement is unchanged. Tables from the message's blocks keep their position around the body.

  • Mention detection is unchanged. Only the message content changes.

  • packages/adapter-slack/src/index.ts: layoutBlockContent, rendered through the existing attachment-content path

  • packages/adapter-slack/src/index.test.ts: a layout message parsed by both parse paths, a webhook mention in a section block, and a buttons-only message that keeps its fallback

  • packages/adapter-slack/sample-messages.md: a captured section mention payload, anonymized

Signed-off-by: Hiroki Osame <hiroki.osame@gmail.com>
@privatenumber
privatenumber requested a review from a team October 16, 2026 12:49
@vercel

vercel Bot commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@privatenumber is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

…r renders

Read the body from blocks only when every block is a section, header,
context, image, divider, actions, or table block. Messages with any other
block type keep `text` as the body, so content the parser can't render
still reaches the message.

Tables now stay in block order within the body, each text object renders
as its own paragraph, and `message.links` reads URLs from the block mrkdwn
when it forms the body. `resolvedContent` resolves inline mentions in the
raw text itself.

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
…k-body

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>

# Conflicts:
#	packages/adapter-slack/sample-messages.md
@bensabic
bensabic enabled auto-merge (squash) October 19, 2026 01:32
@bensabic
bensabic merged commit 48bb905 into vercel:main Oct 19, 2026
20 of 21 checks passed
bensabic added a commit that referenced this pull request Oct 27, 2026
## Summary

Fixes #956. Bulleted and numbered lists typed in Slack's message
composer now parse to mdast `list` nodes, including nested items.

The special-mentions half of #956 was fixed in #960. This PR covers the
list half, which still reproduces on `main`: Slack flattens composer
lists in `event.text` to lines starting with `•` or `◦`, which are not
markdown list syntax, so the issue's payload parses to a single
paragraph. With this change it gives:

```
nodes   : ["paragraph","list","paragraph"]
markdown: "Steps:\n\n* one\n* two\n  * nested\n\n@here and @devs\n"
```

## Approach

The list structure exists only in `event.blocks`, as `rich_text_list`
elements with `style`, `indent`, and `offset`. When a message's blocks
contain a list and every non-table block is `rich_text`, the new
`richTextBody` (`src/rich-text.ts`) renders the blocks to the same
mrkdwn dialect `event.text` uses, with markdown list markers.
`content()` and `resolvedContent()` use that body instead of
`event.text` on the text path, so mention resolution, the format
converter, and table handling are unchanged. Every other message,
including the layout-block bodies from #1062, keeps its current path.

- Nested lists arrive as sibling `rich_text_list` elements with a higher
`indent`. Each item is indented to its parent's content column, so `- `
and `10. ` parents both nest correctly, and ordered lists continue from
`offset` across a nested list.
- Inline styles share markers across consecutive elements: bold `make `,
bold italic `this`, bold ` work` renders as `*make _this_ work*`. A
style change that mrkdwn cannot express (bold starting mid-word, or a
closing marker pressed against an opening one) keeps the text and drops
only that formatting, instead of leaving stray `*` in the output.
- Styles on links, mentions, and emoji are kept, and `@here` inside
inline code stays literal, matching how `event.text` parses.
- Block elements carry only IDs, so labels for `<!subteam^…|@handle>`
and `<#C…|name>` are copied from `event.text`.
- Trailing newlines are trimmed with a loop. The `/\n+$/` regex flagged
by CodeQL took about 39 seconds on 200,000 newlines in the new test.
- Link labels in rich text are now entity-escaped, so a `>` in a label
(including in table cells, which share `blocktext`) no longer ends the
link early.

`SlackMessageBlock["style"]` gains `bold`, `italic`, and `strike`. It
stays an object type, so existing reads of `block.style?.code` still
compile; a type test guards this.

`message.text` follows the core `toPlainText` output for lists, so it
now reads `one\ntwo` instead of `• one\n• two`.

## Test plan

- `src/rich-text.test.ts`: renderer logic, checked as mrkdwn and as
markdown after the adapter's format converter (nesting columns, offsets,
multi-line items, style nesting and mid-word changes, punctuation, line
breaks, code, links, mention labels, quotes and code blocks, gating, and
linear-time trimming). The trimming and link-label tests fail against
the previous implementation.
- `src/rich-text.test-d.ts`: the `style` type stays an object, and
`richTextBody`'s signature.
- `src/index.test.ts`: the issue's payload through `parseMessage` and
the async parser, user mention resolution and bot-mention detection
inside list items, a table next to a list, the `event.text` fallback,
and a `>` in a table cell link label. The list tests fail on `main`.
- `vitest run` in `packages/adapter-slack` (963 tests, no type errors),
`tsc --noEmit`, `ultracite check`, `pnpm knip`, and `pnpm konsistent`
pass. The full `packages/integration-tests` suite passes (63 files,
1,425 tests).
- Smoke test against the built packages: signed `event_callback`
requests through `chat.webhooks.slack` with a memory state reach
`onDirectMessage` and `onNewMention` with the expected list markdown,
and a message without a list keeps `event.text`.

The branch merged `main` to resolve conflicts with #1062. The first
implementation is backed out in two commits before the merge and
reapplied after it.

## Checklist

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

---------

Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Co-authored-by: roboeve[bot] <311457182+roboeve[bot]@users.noreply.github.com>
Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
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