Skip to content

fix(slack): restore table rendering via markdown block#2

Closed
mdnanocom wants to merge 1 commit into
mainfrom
fix/slack-markdown-table-rendering
Closed

fix(slack): restore table rendering via markdown block#2
mdnanocom wants to merge 1 commit into
mainfrom
fix/slack-markdown-table-rendering

Conversation

@mdnanocom
Copy link
Copy Markdown
Owner

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_text parameter on chat.postMessage, claiming "tables render natively." That claim is wrong for tables: Slack's markdown_text parameter 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 columns

This PR switches { markdown } / { ast } messages to emit a markdown Block Kit block (with the original markdown also placed in text as the notification/accessibility fallback). All other behavior — bold, italic, lists, headings, code fences, blockquotes — is preserved, because the markdown block supports everything markdown_text did, plus tables.

What changed

  • Non-streaming (postMessage, postEphemeral, chat.update, scheduleMessage): toSlackPayload now returns { text, blocks: [{ type: "markdown", text }] } for { markdown } / { ast }. Plain-string and { raw } branches are unchanged.
  • Streaming (stream): the streaming API only accepts markdown_text for incremental chunks — there is no streaming equivalent of the markdown block, so streamed tables briefly appear raw during streaming. After streamer.stop() completes, when no explicit stopBlocks was supplied and the accumulated text contains a markdown table (header row + delimiter row), the adapter now calls chat.update to rewrite the finalized message as a markdown block. The rewrite is best-effort and wrapped in try/catch — streaming itself already succeeded.
  • response_url (toResponseUrlText): unchanged. Slack rejects markdown_text on response_url and doesn't accept blocks the same way, so the existing mrkdwn + ASCII-table fallback stays.
  • Debug log payloadKey updated from "markdown_text" | "text" to "blocks" | "text" to reflect the new shape.
  • Docs (api/markdown.mdx, posting-messages.mdx) updated to describe the markdown block route and the brief streaming flash.
  • Changeset added (patch on @chat-adapter/slack).

Known limitation

For streamed messages, table content briefly appears as raw pipes during streaming and "snaps" into rendered form when chat.update rewrites the message at the end. This is unavoidable: Slack's streaming API simply does not provide a way to stream markdown blocks today.

Test plan

  • Unit: packages/adapter-slack/src/markdown.test.tstoSlackPayload returns a markdown block (with text fallback) for { markdown } / { ast }, including a regression test that a GFM-table input produces a markdown block (not markdown_text).
  • Unit: packages/adapter-slack/src/index.test.ts — new stream: GFM table post-rewrite suite covering: rewrites on table content, no rewrite without a table, no rewrite when stopBlocks is supplied, errors in the rewrite are swallowed.
  • Integration: packages/integration-tests/src/slack.test.ts — updated markdown/file-upload assertions; new regression test asserts table content arrives at chat.postMessage as a markdown block.
  • Integration (emulator): 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 check all green.

Regression: vercel#440

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
@mdnanocom
Copy link
Copy Markdown
Owner Author

Closing — this is not actually a chat-sdk bug.

After more testing I found that Slack's top-level markdown_text parameter on chat.postMessage does render GFM tables correctly, as long as the input is well-formed GFM (blank line before the delimiter row). The "raw pipes" output I was seeing originated downstream in our own code, where title/description were joined to the table with a single \n instead of \n\n, producing non-GFM markdown that no GFM parser (including Slack's) recognizes as a table.

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 chat.update rewrite as a separate enhancement, but no fix is required for this issue.

@mdnanocom mdnanocom closed this May 18, 2026
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.

1 participant