Skip to content

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

Closed
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56090
Closed

feat(slack): opt-in Block Kit rendering for agent messages#171
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-56090

Conversation

@hashbender

Copy link
Copy Markdown
Owner

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 NousResearch#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 NousResearch#18918.


Mirror-of: NousResearch#56090
NousResearch#56090

@hashbender hashbender closed this Jul 1, 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.

Slack: render Markdown pipe tables as Block Kit tables

1 participant