feat(gateway): render terminal commands as bare fenced code blocks in chat (#42576)

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.
This commit is contained in:
Teknium 2026-06-08 21:19:05 -07:00 committed by GitHub
parent 3dcfbbfc49
commit 3705625b74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 45 additions and 14 deletions

View file

@ -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):

View file

@ -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

View file

@ -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