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