Skip to content

feat(slack): opt-in Block Kit rendering for agent messages - #56090

Closed
benbarclay wants to merge 2 commits into
mainfrom
feat/slack-block-kit
Closed

feat(slack): opt-in Block Kit rendering for agent messages#56090
benbarclay wants to merge 2 commits into
mainfrom
feat/slack-block-kit

Conversation

@benbarclay

@benbarclay benbarclay commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Slack Block Kit for Hermes

What

Opt-in Slack Block Kit rendering for agent messages. Today every Slack reply goes out as flat mrkdwn text. Teams that use Slack blocks heavily in their workflows want the richer structure — section headers, dividers, true nested lists, and native tables. This adds that behind a config flag.

Renders the primary agent message as structured blocks (headers, dividers, rich_text nested lists, blockquotes, code, and native table blocks). Also fixes the long-standing #18918 (render Markdown pipe tables as native Slack tables — 13 👍).

Enable it: platforms.slack.extra.rich_blocks: true (default off — zero behavior change unless flipped).

platforms:
  slack:
    extra:
      rich_blocks: true

How

  • New plugins/platforms/slack/block_kit.py — a pure, self-contained render_blocks(markdown) function (no adapter state, no Slack client). Renders:
    • # headers → header blocks
    • ---divider blocks
    • bullet/ordered lists → rich_text_list with true nesting (indent levels)
    • blockquotes → rich_text_quote
    • fenced code → rich_text_preformatted
    • pipe tables → native table blocks (see below)
  • adapter.send() renders blocks on the single-chunk primary message; a text= fallback is always sent alongside (notifications / accessibility / old clients).
  • adapter.edit_message() renders blocks only on finalize=True, so intermediate streaming edits stay plain mrkdwn — no per-flush block re-derivation, no streaming jank.
  • Renderer enforces Slack's 50-block / 3000-char section limits and returns None (→ plain-text fallback) on empty / oversized / unexpected input. It never raises — a rendering bug can never drop a message.

Tables (native table block)

Markdown pipe tables render as Slack's native table block — real grid cells, not monospace text:

  • Cells are rich_text, so inline formatting works inside a cell (bold, links, code, emoji).
  • Column alignment is parsed from the markdown separator row (:--- / :-: / --:) into column_settings (left is the default and emitted as null to skip; center/right emitted explicitly).
  • Escaped pipes (\|) are kept as literal cell content, not column separators.
  • Slack's table limits are respected — max 100 rows, 20 columns, 10,000 aggregate cell characters. A table that exceeds any of these (or won't parse) gracefully falls back to aligned monospace (rich_text_preformatted), so a large table never breaks the message.

Note: an earlier revision of this PR shipped tables as monospace-only, on the (mistaken) assumption that Slack's table block was too limited. It isn't — the native block supports per-column alignment/wrapping and rich_text cells, so this revision uses it, with monospace kept only as the over-limit fallback.

Scope / caveats

  • Send-side only — no app reinstall / no manifest or scope change required. Contrast with scope/event changes, which are inert until reinstall.
  • Ephemeral / slash-command / private-notice paths intentionally left as plain text (short system messages); this targets the primary agent reply path.
  • Streaming: off by default, so replies go through the single-shot send() path and render fully. With streaming on, blocks apply on the final edit only.

Follow-up (optional)

Slack also has a newer markdown block that renders GFM directly (including tables). Not used here — the explicit table-block path gives deterministic control over alignment, cell formatting, and the over-limit fallback — but it's a viable alternative worth a look if we ever want Slack to own more of the markdown rendering.

Tests

  • tests/gateway/test_slack_block_kit.py — pure-renderer unit suite (headers, dividers, nested-list indent contract, ordered/bullet distinction, inline styling; native table shape, alignment→column_settings, inline-formatted cells, oversized/too-wide → monospace fallback, escaped-pipe cell; 50-block/3000-char limits; never-raises-on-garbage).
  • tests/gateway/test_slack_block_kit_adapter.py — adapter integration: blocks present when on, no blocks + plain text when off, text fallback always set, finalize gating on edits, multi-chunk fallback, string-"true" coercion.
  • Prove-failed against stubbed renderers (both render_blocks and _table_block): the relevant assertions fail without the real code; fallback-path tests correctly stay green. All 216 existing Slack tests still pass.
  • Live-tested against a real workspace via Socket Mode: headers/dividers/nested lists render, and native tables render as real grids with alignment + in-cell bold/links; toggling rich_blocks off cleanly reverts to plain mrkdwn.

Docs

website/docs/user-guide/messaging/slack.md + zh-Hans i18n: config example + key-table row, describing native tables and the monospace fallback.

Closes #18918.

Add platforms.slack.extra.rich_blocks (default off). When enabled, the
final agent message is sent as Slack Block Kit blocks — section headers,
dividers, and true nested lists via rich_text — instead of flat mrkdwn.

- New plugins/platforms/slack/block_kit.py: pure markdown->blocks renderer
  (headers, dividers, nested ordered/bullet lists, blockquotes, fenced code;
  pipe-tables as aligned monospace since Block Kit has no robust table block).
  Enforces Slack's 50-block / 3000-char section limits and returns None to
  fall back to plain text on empty/oversized/unexpected input. Never raises.
- adapter.send(): render blocks on the single-chunk primary message; a
  text= fallback is ALWAYS sent alongside (notifications/accessibility).
- adapter.edit_message(): blocks only on finalize=True, so intermediate
  streaming edits stay plain mrkdwn (no per-flush block re-derivation).
- Docs (EN + zh-Hans) + config example. Send-side only: no app reinstall.

Tests: pure-renderer unit suite + adapter integration suite (blocks present
when on, plain text when off, text fallback always set, finalize gating,
multi-chunk fallback). Prove-failed against a stubbed renderer.
@benbarclay
benbarclay requested a review from teknium1 July 1, 2026 05:57
@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 Jul 1, 2026
Replace the interim monospace table fallback with Slack's native `table`
block (rows of rich_text cells). Addresses the core ask in #18918.

- _table_block(): builds type:"table" with rich_text cells, so inline
  formatting (bold, links, code) renders inside cells.
- Column alignment parsed from the markdown separator row (:---, :-:, --:)
  into column_settings (left = default/null-skip, center/right emitted).
- Escaped pipes (\\|) are not treated as column separators.
- Respects Slack's table limits (100 rows / 20 cols / 10k aggregate chars);
  oversized or unparseable tables gracefully fall back to aligned monospace
  (rich_text_preformatted), so a big table never breaks the message.

Docs (EN + zh-Hans) updated to describe native tables + the fallback.
Tests: native table shape, alignment->column_settings, inline-formatted
cells, oversized/too-wide monospace fallback, escaped-pipe cell. Prove-
failed against a stubbed _table_block (native-table tests fail, fallback
tests stay green). All existing Slack tests still pass.
@teknium1

teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Merged via #56102 — your commits were cherry-picked onto current main with your authorship preserved in git log (b080b93, 7c7b489). Added a small follow-up fixing the module docstrings that still described the earlier monospace-only table approach. Thanks Ben!

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.

Slack: render Markdown pipe tables as Block Kit tables

3 participants