From dde9c0d19d1609cb4d70dadc89c76659a1004e08 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:29:55 -0700 Subject: [PATCH] feat(gateway): render terminal tool calls as native bash code blocks on markdown platforms (#41215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- gateway/platforms/base.py | 9 ++++++++- gateway/platforms/feishu.py | 2 ++ gateway/platforms/matrix.py | 2 ++ gateway/platforms/slack.py | 1 + gateway/platforms/telegram.py | 1 + gateway/platforms/weixin.py | 2 ++ gateway/platforms/whatsapp.py | 1 + gateway/run.py | 28 ++++++++++++++++++++++++++-- plugins/platforms/discord/adapter.py | 1 + 9 files changed, 44 insertions(+), 3 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 0ddcc1e8cb6..adac5fad2a7 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -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 diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index b361ebc8cfc..4814107bacd 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -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 diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index a649bb91e59..e885afc9337 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -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. diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 46068ca20ea..6754e21fb75 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -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) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index d2b425b52b9..ea19bba8016 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -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 diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index 73e9e68ea70..adb6d21a0e0 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -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 diff --git a/gateway/platforms/whatsapp.py b/gateway/platforms/whatsapp.py index 7ece37dbca5..59392201150 100644 --- a/gateway/platforms/whatsapp.py +++ b/gateway/platforms/whatsapp.py @@ -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 diff --git a/gateway/run.py b/gateway/run.py index 14dc362a4da..08c6a35cda5 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -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 diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 3d97274ea48..1cf33020e7b 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -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