fix(slack): restore table rendering via markdown block#2
Conversation
The top-level `markdown_text` parameter on `chat.postMessage` runs through
a stripped-down renderer that does not support GFM tables — they appear as
raw `|`-delimited text. Emit a `{ type: "markdown" }` Block Kit block
instead, which restores native table rendering while keeping native
rendering of bold/italic, lists, headings, code fences, and blockquotes.
For streamed messages, Slack's streaming API only accepts `markdown_text`
for incremental chunks, so tables briefly appear raw during streaming.
After `streamer.stop()` completes, the adapter now rewrites the finalized
message via `chat.update` with a `markdown` block when a table is
detected (unless the caller supplied explicit `stopBlocks`).
Regression: vercel#440
|
Closing — this is not actually a chat-sdk bug. After more testing I found that Slack's top-level So PR vercel#440 did not regress table rendering — the regression was on our side. Apologies for the noise. Leaving the branch in place for reference if anyone wants the streaming |
Summary
Restores native rendering of GFM tables in Slack messages, which regressed in vercel#440.
vercel#440 switched outgoing Slack messages from a custom Block Kit renderer to the top-level
markdown_textparameter onchat.postMessage, claiming "tables render natively." That claim is wrong for tables: Slack'smarkdown_textparameter goes through a stripped-down renderer that does not support GFM tables — they show up as raw| col | col |text.The Slack feature that does render tables natively is the
{ type: "markdown" }Block Kit block (docs), not the top-level parameter. Empirically:{ markdown_text: "...table..." }→ raw pipes{ blocks: [{ type: "markdown", text: "...table..." }] }→ real columnsThis PR switches
{ markdown }/{ ast }messages to emit amarkdownBlock Kit block (with the original markdown also placed intextas the notification/accessibility fallback). All other behavior — bold, italic, lists, headings, code fences, blockquotes — is preserved, because themarkdownblock supports everythingmarkdown_textdid, plus tables.What changed
postMessage,postEphemeral,chat.update,scheduleMessage):toSlackPayloadnow returns{ text, blocks: [{ type: "markdown", text }] }for{ markdown }/{ ast }. Plain-string and{ raw }branches are unchanged.stream): the streaming API only acceptsmarkdown_textfor incremental chunks — there is no streaming equivalent of the markdown block, so streamed tables briefly appear raw during streaming. Afterstreamer.stop()completes, when no explicitstopBlockswas supplied and the accumulated text contains a markdown table (header row + delimiter row), the adapter now callschat.updateto rewrite the finalized message as amarkdownblock. The rewrite is best-effort and wrapped in try/catch — streaming itself already succeeded.response_url(toResponseUrlText): unchanged. Slack rejectsmarkdown_textonresponse_urland doesn't acceptblocksthe same way, so the existing mrkdwn + ASCII-table fallback stays.payloadKeyupdated from"markdown_text" | "text"to"blocks" | "text"to reflect the new shape.api/markdown.mdx,posting-messages.mdx) updated to describe themarkdownblock route and the brief streaming flash.@chat-adapter/slack).Known limitation
For streamed messages, table content briefly appears as raw pipes during streaming and "snaps" into rendered form when
chat.updaterewrites the message at the end. This is unavoidable: Slack's streaming API simply does not provide a way to streammarkdownblocks today.Test plan
packages/adapter-slack/src/markdown.test.ts—toSlackPayloadreturns a markdown block (with text fallback) for{ markdown }/{ ast }, including a regression test that a GFM-table input produces a markdown block (notmarkdown_text).packages/adapter-slack/src/index.test.ts— newstream: GFM table post-rewritesuite covering: rewrites on table content, no rewrite without a table, no rewrite whenstopBlocksis supplied, errors in the rewrite are swallowed.packages/integration-tests/src/slack.test.ts— updated markdown/file-upload assertions; new regression test asserts table content arrives atchat.postMessageas amarkdownblock.packages/integration-tests/src/emulator/slack/post-message.test.ts— round-trips a{ markdown }post through the Slack-shaped emulator.pnpm test,pnpm typecheck,pnpm checkall green.Regression: vercel#440