Repository navigation
fix(slack): parse composer lists from rich_text blocks - #1002
Conversation
Slack flattens composer lists in event.text to lines starting with bullet characters, which are not markdown list syntax. Build the message body from the rich_text blocks when they contain a list so message.formatted gets list nodes, including nested items. Fixes #956.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
## 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`](https://docs.slack.dev/reference/methods/chat.postMessage#text-blocks-attachments)). 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: ```json { "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](#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`](https://github.com/vercel/chat/blob/9a7cfb78/packages/adapter-slack/src/index.ts): `layoutBlockContent`, rendered through the existing attachment-content path - [`packages/adapter-slack/src/index.test.ts`](https://github.com/vercel/chat/blob/9a7cfb78/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`](https://github.com/vercel/chat/blob/9a7cfb78/packages/adapter-slack/sample-messages.md): a captured `section` mention payload, anonymized --------- 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>
main has since moved the Slack message parsing this change edited, so the original diff conflicts. Back it out so main merges cleanly; a later commit reapplies the fix on top of the current parsing code.
Pairs with the previous commit so main merges cleanly; the reworked fix brings its own tests.
Slack flattens composer lists in event.text to lines starting with bullet characters, so they never became mdast list nodes. When a message's rich_text blocks contain a list, render the body from the blocks instead. Inline styles share markers across elements, and a style change mrkdwn cannot express mid-word keeps the text and drops only that formatting. Trailing newlines are trimmed with a loop instead of a quadratic regex. Link labels in rich text blocks, including table cells, are escaped so a > cannot end the token early.
Run the issue's payload through parseMessage and the async parser, check mention resolution and bot-mention detection inside list items, tables next to a list, the event.text fallback, and a > in a table cell's link label.
|
CI on fdb41d9 failed on one test from this PR: 0d627ed switches the test input to a |
Signed-off-by: Ben Sabic <bensabic@users.noreply.github.com>
Summary
Fixes #956. Bulleted and numbered lists typed in Slack's message composer now parse to mdast
listnodes, 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 inevent.textto 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:Approach
The list structure exists only in
event.blocks, asrich_text_listelements withstyle,indent, andoffset. When a message's blocks contain a list and every non-table block isrich_text, the newrichTextBody(src/rich-text.ts) renders the blocks to the same mrkdwn dialectevent.textuses, with markdown list markers.content()andresolvedContent()use that body instead ofevent.texton 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.rich_text_listelements with a higherindent. Each item is indented to its parent's content column, so-and10.parents both nest correctly, and ordered lists continue fromoffsetacross a nested list.make, bold italicthis, boldworkrenders 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.@hereinside inline code stays literal, matching howevent.textparses.<!subteam^…|@handle>and<#C…|name>are copied fromevent.text./\n+$/regex flagged by CodeQL took about 39 seconds on 200,000 newlines in the new test.>in a label (including in table cells, which shareblocktext) no longer ends the link early.SlackMessageBlock["style"]gainsbold,italic, andstrike. It stays an object type, so existing reads ofblock.style?.codestill compile; a type test guards this.message.textfollows the coretoPlainTextoutput for lists, so it now readsone\ntwoinstead 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: thestyletype stays an object, andrichTextBody's signature.src/index.test.ts: the issue's payload throughparseMessageand the async parser, user mention resolution and bot-mention detection inside list items, a table next to a list, theevent.textfallback, and a>in a table cell link label. The list tests fail onmain.vitest runinpackages/adapter-slack(963 tests, no type errors),tsc --noEmit,ultracite check,pnpm knip, andpnpm konsistentpass. The fullpackages/integration-testssuite passes (63 files, 1,425 tests).event_callbackrequests throughchat.webhooks.slackwith a memory state reachonDirectMessageandonNewMentionwith the expected list markdown, and a message without a list keepsevent.text.The branch merged
mainto resolve conflicts with #1062. The first implementation is backed out in two commits before the merge and reapplied after it.Checklist
git commit -s)pnpm validatepasses