feat(gateway): render terminal tool calls as native bash code blocks on markdown platforms (#41215)

Tool-progress now shows a terminal command in a ```bash fenced block —
full command, no surrounding quotes, no label, no 40-char truncation —
instead of the noisy `terminal: "cmd…"` line, on every platform that
renders markdown code blocks (Telegram, Slack, Matrix, WhatsApp, Feishu,
Weixin, Discord). Plain-text platforms keep the compact preview line.

Gated on a new `BasePlatformAdapter.supports_code_blocks` capability
(default False) rather than a hardcoded platform list, so plugin adapters
(Discord lives in plugins/platforms/) opt in by setting the flag. Applies
to both all/new and verbose progress modes, with a safe fallback when the
command arg is missing or blank.
This commit is contained in:
Teknium 2026-06-07 17:29:55 -07:00 committed by GitHub
parent e029b7597b
commit dde9c0d19d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 44 additions and 3 deletions

View file

@ -1792,7 +1792,14 @@ class BasePlatformAdapter(ABC):
- Sending messages/responses
- Handling media
"""
# Whether this platform renders triple-backtick fenced code blocks (i.e.
# ``format_message`` translates/preserves markdown fences into a real code
# block). Drives presentation choices like rendering a ``terminal`` tool
# call's command as a ```bash block instead of a flat preview line.
# Default False (plain-text platforms); markdown-rendering adapters set True.
supports_code_blocks: bool = False
def __init__(self, config: PlatformConfig, platform: Platform):
self.config = config
self.platform = platform

View file

@ -1409,6 +1409,8 @@ def check_feishu_requirements() -> bool:
class FeishuAdapter(BasePlatformAdapter):
"""Feishu/Lark bot adapter."""
supports_code_blocks = True # Feishu renders fenced code blocks
MAX_MESSAGE_LENGTH = 8000
# Max distinct chat IDs retained in _chat_locks before LRU eviction kicks in.
CHAT_LOCK_MAX_SIZE: int = 1000

View file

@ -420,6 +420,8 @@ class _CryptoStateStore:
class MatrixAdapter(BasePlatformAdapter):
"""Gateway adapter for Matrix (any homeserver)."""
supports_code_blocks = True # Matrix renders fenced code blocks (HTML/markdown)
# Threshold for detecting Matrix client-side message splits.
# When a chunk is near the ~4000-char practical limit, a continuation
# is almost certain.

View file

@ -317,6 +317,7 @@ class SlackAdapter(BasePlatformAdapter):
"""
MAX_MESSAGE_LENGTH = 39000 # Slack API allows 40,000 chars; leave margin
supports_code_blocks = True # Slack mrkdwn renders fenced code blocks
def __init__(self, config: PlatformConfig):
super().__init__(config, Platform.SLACK)

View file

@ -344,6 +344,7 @@ class TelegramAdapter(BasePlatformAdapter):
# Telegram message limits
MAX_MESSAGE_LENGTH = 4096
supports_code_blocks = True # Telegram MarkdownV2 renders fenced code blocks
# Threshold for detecting Telegram client-side message splits.
# When a chunk is near this limit, a continuation is almost certain.
_SPLIT_THRESHOLD = 4000

View file

@ -1138,6 +1138,8 @@ async def qr_login(
class WeixinAdapter(BasePlatformAdapter):
"""Native Hermes adapter for Weixin personal accounts."""
supports_code_blocks = True # Weixin renders fenced code blocks
MAX_MESSAGE_LENGTH = 2000
# WeChat does not support editing sent messages — streaming must use the

View file

@ -242,6 +242,7 @@ class WhatsAppAdapter(BasePlatformAdapter):
# WhatsApp message limits — practical UX limit, not protocol max.
# WhatsApp allows ~65K but long messages are unreadable on mobile.
MAX_MESSAGE_LENGTH = 4096
supports_code_blocks = True # WhatsApp renders fenced code blocks (monospace)
DEFAULT_REPLY_PREFIX = "⚕ *Hermes Agent*\n────────────\n"
# Default bridge location relative to the hermes-agent install

View file

@ -17339,10 +17339,32 @@ class GatewayRunner:
# Build progress message with primary argument preview
from agent.display import get_tool_emoji
emoji = get_tool_emoji(tool_name, default="⚙️")
# Markdown-capable platforms render a terminal command as a native
# ```bash fenced block (full command, no quotes, no label, no
# truncation) instead of the noisy `terminal: "cmd…"` line. Gated
# on the adapter's ``supports_code_blocks`` capability so every
# markdown-rendering platform (and plugin adapters that opt in) gets
# it, while plain-text platforms keep the compact line.
_bash_block = None
try:
_progress_adapter = self.adapters.get(source.platform)
except Exception:
_progress_adapter = None
if (
getattr(_progress_adapter, "supports_code_blocks", False)
and tool_name == "terminal"
and isinstance(args, dict)
and isinstance(args.get("command"), str)
and args["command"].strip()
):
_bash_block = f"```bash\n{args['command'].rstrip()}\n```"
# Verbose mode: show detailed arguments, respects tool_preview_length
if progress_mode == "verbose":
if args:
if _bash_block is not None:
msg = _bash_block
elif args:
from agent.display import get_tool_preview_max_len
_pl = get_tool_preview_max_len()
args_str = json.dumps(args, ensure_ascii=False, default=str)
@ -17362,7 +17384,9 @@ class GatewayRunner:
# "all" / "new" modes: short preview, respects tool_preview_length
# config (defaults to 40 chars when unset to keep gateway messages
# compact — unlike CLI spinners, these persist as permanent messages).
if preview:
if _bash_block is not None:
msg = _bash_block
elif preview:
from agent.display import get_tool_preview_max_len
_pl = get_tool_preview_max_len()
_cap = _pl if _pl > 0 else 40

View file

@ -573,6 +573,7 @@ class DiscordAdapter(BasePlatformAdapter):
# Discord message limits
MAX_MESSAGE_LENGTH = 2000
_SPLIT_THRESHOLD = 1900 # near the 2000-char split point
supports_code_blocks = True # Discord markdown renders fenced code blocks natively
# Auto-disconnect from voice channel after this many seconds of inactivity
VOICE_TIMEOUT = 300