Skip to content

feat(slack): render markdown tables as column-padded monospace blocks (#26947) - #26950

Closed
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:feat/slack-block-kit-tables-26947
Closed

feat(slack): render markdown tables as column-padded monospace blocks (#26947)#26950
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:feat/slack-block-kit-tables-26947

Conversation

@xxxigm

@xxxigm xxxigm commented May 16, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Teaches the Slack adapter to render markdown tables as actual aligned tables instead of leaving raw pipe characters as plain text.

Issue #26947 reported that when Hermes sends a markdown table to Slack (e.g. | col1 | col2 |), the pipes and dashed separator survive as literal characters and the columns smear together. The root cause is that:

  1. Slack's mrkdwn has no native renderer for GitHub-flavored markdown tables.
  2. Slack's public Block Kit API has no table block either (the issue's "Block Kit table" is a misnomer — even Slack's own docs only suggest preformatted text as the workaround).

The universally-compatible fix used by every other agent that handles this is to detect markdown tables on the way out and convert them to a column-padded triple-backtick block. Triple-backtick text is rendered as fixed-width preformatted code by Slack, so the columns line up the way the user intended.

This PR implements that, inline in format_message:

| Header 1 | Header 2 |        ```
| -------- | -------- |        | Header 1 | Header 2 |
| Cell 1   | Cell 2   |   →    |----------|----------|
| Cell 3   | Cell 4   |        | Cell 1   | Cell 2   |
                              | Cell 3   | Cell 4   |
                              ```

The conversion runs as step 0 of format_messagebefore the existing fenced-code-block protection pass — so the generated code fence is then treated like any other code block and survives later mrkdwn rewrites (bold, italics, link conversion, header conversion, etc.) intact.

Related Issue

Closes #26947

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • gateway/platforms/slack.py — add module-level _convert_markdown_tables_to_monospace (plus helpers _split_md_table_row, _parse_md_alignments, _pad_cell, _render_md_table_as_monospace) and an env-driven toggle _markdown_tables_enabled. Wire the converter into format_message as step 0 so the generated code fences are then protected by the existing fenced-block placeholder pass. No existing send path is touched; tables that don't match the strict separator-row regex are passed through byte-identical.
  • tests/gateway/test_slack_markdown_tables.py26 new regression tests covering basic shape, column padding, alignment hints (:---: / ---:), headerless/no-leading-pipe form, escaped pipes, ragged rows, empty-body tables, prose with pipes, single-column rejection, no-pipe short-circuit, empty/None inputs, fenced-block exclusion (we never reformat a user's code sample), pipe rows without a valid separator, two consecutive tables, in-place conversion inside a larger message, integration with the rest of format_message (bold/italics outside the table still convert, fence inside the table protects literals), no-table content byte-identity, and the off-switch (case-insensitive false / 0 / no / off; default / empty / garbage value → on).
  • website/docs/reference/environment-variables.md — document SLACK_RENDER_MARKDOWN_TABLES alongside the other Slack toggles.

Detection rules (deliberately strict)

To avoid false positives on prose that happens to contain pipes:

  • A "table" needs at least two rows.
  • The second row must be a separator row of dashes (with optional : alignment markers) and >=3 dashes per cell.
  • The header must have >=2 columns (single-column "tables" are almost always bulleted lists with piped items, not real tables).
  • Tables inside an existing fenced code block are left byte-identical — we never reformat a user's code sample.

Alignment hints (:---, ---:, :---:) are honoured by left-/right-/center-padding the generated cells. Ragged rows are padded to the column count. Backslash-escaped pipes inside cells survive as literal pipes.

Opt-out

Operators who already post-process agent output for Slack can disable the feature with SLACK_RENDER_MARKDOWN_TABLES=false (also accepts 0 / no / off, case-insensitive). Default is on — most users want their tables to actually render.

How to Test

# 1. New regression suite passes
python -m pytest tests/gateway/test_slack_markdown_tables.py -q
# expected: 26 passed

# 2. Existing Slack tests still pass (no regression)
python -m pytest tests/gateway/test_slack.py -q
# expected: 186 passed

# 3. Manual smoke — eyeball the before/after for the issue's repro
python - <<'PY'
from gateway.platforms.slack import SlackAdapter
from gateway.platforms.base import PlatformConfig
a = SlackAdapter(PlatformConfig(enabled=True, token="xoxb-fake"))
md = (
    "**Status**\n\n"
    "| Service | State | Notes |\n"
    "|---------|-------|-------|\n"
    "| api     | up    | stable |\n"
    "| db      | down  | restart needed |\n"
)
print(a.format_message(md))
PY

Expected output of step 3:

*Status*

```
| Service | State | Notes          |
|---------|-------|----------------|
| api     | up    | stable         |
| db      | down  | restart needed |
```

Posted into Slack with mrkdwn=True (the existing send path), the bold heading renders as bold and the code block renders as an aligned monospace table — exactly the "real Slack table" the issue asked for.

xxxigm added 3 commits May 16, 2026 20:44
Slack's mrkdwn has no native renderer for GitHub-flavored markdown
tables, and Block Kit has no public ``table`` block, so the pipe
characters and dashed separators that Hermes emits today survive as
literal text — columns smear together and the message is unreadable.

This patch teaches the Slack adapter to detect GFM tables in outbound
content and convert each one into a column-padded triple-backtick
block before the message reaches ``chat.postMessage``.  Triple-backtick
text is rendered as fixed-width preformatted code by Slack, so the
columns line up the way the user intended.  The conversion runs as
``step 0`` of ``format_message`` (before existing fenced-code
protection) so the generated code fence is then treated like any other
code block and survives the later mrkdwn passes intact.

Detection requirements (deliberately strict, to avoid false positives
on prose that happens to contain pipes):

* A "table" needs at least two rows.
* The second row must be a separator row of dashes with optional
  ``:`` alignment markers and ``>=3`` dashes per cell.
* The header must have ``>=2`` columns (single-column "tables" are
  almost always bulleted lists with piped items, not real tables).
* Tables that fall inside an existing fenced code block are left
  untouched — we never reformat a user's code sample.

Alignment hints (``:---``, ``---:``, ``:---:``) are honoured by
left- / right- / center-padding the generated cells.  Ragged rows
are padded to the column count.  Backslash-escaped pipes inside
cells round-trip correctly.

Operators who already post-process agent output for Slack can opt
out via ``SLACK_RENDER_MARKDOWN_TABLES=false`` without redeploying
code.

Closes NousResearch#26947
26 focused tests against the new converter and its integration into
``format_message``:

* Basic shape — minimal repro from NousResearch#26947 round-trips with a code
  fence, correct separator row, preserved body.
* Column padding — cells are padded to the widest column.
* Alignment hints — ``:---:`` centers, ``---:`` right-aligns.
* Headerless / no-leading-pipe form is supported (valid GFM variant).
* Backslash-escaped pipes inside cells survive as literal pipes.
* Ragged rows are padded to the header's column count.
* Empty-body tables (header + separator only) still render.
* Prose with pipes is left untouched.
* One-column ``tables`` are rejected (almost always false positives).
* Plain text with no pipes short-circuits.
* Empty / ``None`` inputs pass through.
* Tables inside an existing fenced code block come back byte-identical.
* A row of pipes without a proper dash separator is not a table.
* Two consecutive tables are each converted independently.
* Larger messages convert in place without disturbing surrounding text.
* ``format_message`` invokes the converter and leaves the generated
  fence un-mangled by later mrkdwn passes (bold inside a cell stays
  literal because the code fence protects it).
* Non-table content is byte-identical to the pre-fix path.
* Off-switch via ``SLACK_RENDER_MARKDOWN_TABLES`` accepts
  ``false`` / ``0`` / ``no`` / ``off`` (case-insensitive); default,
  empty, and garbage values all leave the feature on.
Reference table for the new opt-out env var introduced alongside
the markdown-table → monospace conversion for Slack (NousResearch#26947).
@alt-glitch alt-glitch added type/feature New feature or request comp/gateway Gateway runner, session dispatch, delivery platform/slack Slack app adapter P3 Low — cosmetic, nice to have labels May 16, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #16648 (which supersedes #15854) — same monospace code block approach for Slack markdown tables. Also note that the issue this fixes (#26947) was already triaged as a duplicate of #18918.

Related PRs in this space: #15854 (original monospace approach), #16648 (monospace + CJK alignment, supersedes #15854), #24267 (closed as dup of #15854), #18920 (Block Kit alternative), #8554 (broader Block Kit migration).

@cardtest15-coder

This comment was marked as spam.

@cardtest15-coder

This comment was marked as spam.

@donbowman

donbowman commented May 16, 2026

Copy link
Copy Markdown

this would be really nice with google chat as well.
suggest making it a generic filter, and, marking individual channels that don't support tables as going through it.

@teknium1

teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Superseded by #56102 (#56102), a broader opt-in Slack Block Kit renderer that includes native table blocks (per-column alignment, inline-formatted cells, monospace fallback over Slack's limits) and closes #18918. This was one of several independent fixes for the same area — thanks for contributing it.

@teknium1 teknium1 closed this Jul 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/slack Slack app adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Support Slack Block Kit table rendering for markdown tables

5 participants