From 3705625b74710dcc6f1b57af9b5ddb660b5e0520 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:19:05 -0700 Subject: [PATCH] feat(gateway): render terminal commands as bare fenced code blocks in chat (#42576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Terminal tool progress on markdown-capable gateways (Telegram, Slack, Discord, WhatsApp, Matrix, Weixin, Feishu) renders the full command in a fenced code block again, in all/new AND verbose modes — gated on the adapter's supports_code_blocks capability. Plain-text platforms keep the short truncated preview. No language tag is emitted: Slack mrkdwn renders a '```bash' fence with 'bash' as a literal first code line, so a bare '```' fence is used, which renders correctly on every platform that supports blocks. This restores the #41215 feature (removed in #41950 due to the command showing in group chats) as the default. For a personal assistant the command display is desired; the group-chat concern is a preference, not a vulnerability. --- gateway/platforms/base.py | 7 ++--- gateway/run.py | 32 +++++++++++++++++++++-- tests/gateway/test_run_progress_topics.py | 20 +++++++------- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 36cc9c4495d..50ca91df263 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -1797,9 +1797,10 @@ class BasePlatformAdapter(ABC): # ``format_message`` translates/preserves markdown fences into a real code # block). Capability flag for markdown-aware presentation choices. # Default False (plain-text platforms); markdown-rendering adapters set True. - # Note: tool-progress deliberately does NOT use this to render a terminal - # command as a ```bash block — that exposed full commands in chat. Progress - # shows a short truncated preview only (see gateway/run.py progress_callback). + # Tool-progress uses this to render a terminal command as a bare fenced code + # block (no language tag — Slack mrkdwn would print the tag as a literal + # first code line). Plain-text platforms fall back to the short truncated + # preview (see gateway/run.py progress_callback). supports_code_blocks: bool = False def __init__(self, config: PlatformConfig, platform: Platform): diff --git a/gateway/run.py b/gateway/run.py index d445a8ee3fd..1a97457d9eb 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -12971,9 +12971,33 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # 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 fenced + # code block (full command, no truncation) instead of the compact + # `terminal: "cmd…"` preview. Gated on the adapter's + # ``supports_code_blocks`` capability so plain-text platforms keep + # the short line. No language tag is emitted — Slack mrkdwn renders + # the tag as a literal first code line ("bash"), and a bare fence + # renders correctly everywhere that supports blocks. + _code_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() + ): + _code_block = f"{emoji} {tool_name}\n```\n{args['command'].rstrip()}\n```" + # Verbose mode: show detailed arguments, respects tool_preview_length if progress_mode == "verbose": + if _code_block is not None: + progress_queue.put(_code_block) + return if args: from agent.display import get_tool_preview_max_len _pl = get_tool_preview_max_len() @@ -12994,7 +13018,11 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew # "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: + # Terminal commands on markdown platforms get the full fenced block + # built above instead of the truncated preview. + if _code_block is not None: + msg = _code_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/tests/gateway/test_run_progress_topics.py b/tests/gateway/test_run_progress_topics.py index 88dee23b3fe..4515d75a0dd 100644 --- a/tests/gateway/test_run_progress_topics.py +++ b/tests/gateway/test_run_progress_topics.py @@ -1295,10 +1295,10 @@ class TerminalCommandAgent: @pytest.mark.asyncio -async def test_terminal_progress_is_truncated_preview_not_bash_block(monkeypatch, tmp_path): - """Regression for #41215: terminal progress must render as a short truncated - preview, never the full command in a fenced ```bash block, even on a - markdown-capable (supports_code_blocks) gateway.""" +async def test_terminal_progress_renders_fenced_code_block(monkeypatch, tmp_path): + """Terminal progress on a markdown-capable (supports_code_blocks) gateway + renders the full command in a bare fenced code block — no language tag + (Slack mrkdwn would print 'bash' as a literal first code line).""" monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "all") fake_dotenv = types.ModuleType("dotenv") @@ -1328,18 +1328,20 @@ async def test_terminal_progress_is_truncated_preview_not_bash_block(monkeypatch context_prompt="", history=[], source=source, - session_id="sess-terminal-no-bash-block", + session_id="sess-terminal-code-block", session_key="agent:main:telegram:dm:12345", ) assert result["final_response"] == "done" all_content = " ".join(call["content"] for call in adapter.sent) all_content += " ".join(call["content"] for call in adapter.edits) - # Compact truncated preview, not a fenced bash block. + # Bare fenced block, no language tag (no '```bash'). + assert "```" in all_content assert "```bash" not in all_content - assert 'terminal: "' in all_content - # The full multi-line command body must not reach the chat. - assert "npm install -g hyperframes@latest" not in all_content + # The full multi-line command body IS present in the block. + assert "npm install -g hyperframes@latest" in all_content + # No truncated quoted preview for the terminal command. + assert 'terminal: "' not in all_content @pytest.mark.asyncio