fix(gateway): cap terminal code-block preview in non-verbose mode (#42729)

The markdown code-block change rendered args['command'] in full in both
verbose AND non-verbose (all/new) modes, so a long or multi-line terminal
command bypassed the tool_preview_length cap (default 40) and rendered as
a huge block. Non-verbose now collapses to a single line capped at the
preview length while keeping the fence; verbose keeps the full command.
This commit is contained in:
Teknium 2026-06-09 02:28:47 -07:00 committed by GitHub
parent a38cc69bcc
commit 8d99b5bc4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 18 deletions

View file

@ -13002,13 +13002,20 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
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
# code block 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.
#
# Verbose mode shows the FULL command. Non-verbose ("all"/"new")
# modes still wrap in a fence but truncate to a single line capped
# at ``tool_preview_length`` (default 40) so a long or multi-line
# command doesn't render as a huge block — matching the budget the
# non-terminal preview path already applies (#42634).
_code_block_full = None
_code_block_short = None
try:
_progress_adapter = self.adapters.get(source.platform)
except Exception:
@ -13020,12 +13027,25 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
and isinstance(args.get("command"), str)
and args["command"].strip()
):
_code_block = f"{emoji} {tool_name}\n```\n{args['command'].rstrip()}\n```"
from agent.display import get_tool_preview_max_len
_cmd_full = args["command"].rstrip()
_code_block_full = f"{emoji} {tool_name}\n```\n{_cmd_full}\n```"
# Single-line, capped preview for non-verbose modes.
_pl = get_tool_preview_max_len()
_cap = _pl if _pl > 0 else 40
_lines = _cmd_full.splitlines()
_cmd_short = _lines[0] if _lines else _cmd_full
_multiline = len(_lines) > 1
if len(_cmd_short) > _cap:
_cmd_short = _cmd_short[:_cap - 3] + "..."
elif _multiline:
_cmd_short = _cmd_short + " ..."
_code_block_short = f"{emoji} {tool_name}\n```\n{_cmd_short}\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)
if _code_block_full is not None:
progress_queue.put(_code_block_full)
return
if args:
from agent.display import get_tool_preview_max_len
@ -13047,10 +13067,10 @@ 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).
# 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
# Terminal commands on markdown platforms get a single-line capped
# fenced block (built above) instead of the truncated preview.
if _code_block_short is not None:
msg = _code_block_short
elif preview:
from agent.display import get_tool_preview_max_len
_pl = get_tool_preview_max_len()

View file

@ -1297,8 +1297,10 @@ class TerminalCommandAgent:
@pytest.mark.asyncio
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)."""
renders a bare fenced code block no language tag (Slack mrkdwn would print
'bash' as a literal first code line). In non-verbose ("all"/"new") mode the
command is collapsed to a single line capped at tool_preview_length so a long
or multi-line command doesn't render as a huge block (#42634)."""
monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "all")
fake_dotenv = types.ModuleType("dotenv")
@ -1338,12 +1340,63 @@ async def test_terminal_progress_renders_fenced_code_block(monkeypatch, tmp_path
# Bare fenced block, no language tag (no '```bash').
assert "```" in all_content
assert "```bash" not in all_content
# The full multi-line command body IS present in the block.
assert "npm install -g hyperframes@latest" in all_content
# Non-verbose collapses to the first line + truncation marker — the later
# command lines must NOT appear (this was the "huge block" regression).
assert "set -euo pipefail" in all_content
assert "npm install -g hyperframes@latest" not in all_content
assert "node --version" not in all_content
# No truncated quoted preview for the terminal command.
assert 'terminal: "' not in all_content
@pytest.mark.asyncio
async def test_terminal_progress_verbose_shows_full_command(monkeypatch, tmp_path):
"""Verbose mode on a markdown-capable gateway renders the FULL multi-line
command in a bare fenced block (no truncation, no 'bash' tag). This is the
parity guarantee for #42634: verbose keeps full detail, non-verbose caps."""
monkeypatch.setenv("HERMES_TOOL_PROGRESS_MODE", "verbose")
fake_dotenv = types.ModuleType("dotenv")
fake_dotenv.load_dotenv = lambda *args, **kwargs: None
monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv)
fake_run_agent = types.ModuleType("run_agent")
fake_run_agent.AIAgent = TerminalCommandAgent
monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)
import tools.terminal_tool # noqa: F401 - register terminal emoji
adapter = CodeBlockProgressAdapter(platform=Platform.TELEGRAM)
runner = _make_runner(adapter)
gateway_run = importlib.import_module("gateway.run")
monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path)
monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"})
source = SessionSource(
platform=Platform.TELEGRAM,
chat_id="12345",
chat_type="dm",
thread_id=None,
)
result = await runner._run_agent(
message="hello",
context_prompt="",
history=[],
source=source,
session_id="sess-terminal-code-block-verbose",
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)
assert "```" in all_content
assert "```bash" not in all_content
# Full command body present — verbose is uncapped.
assert "npm install -g hyperframes@latest" in all_content
assert "node --version" in all_content
@pytest.mark.asyncio
async def test_terminal_progress_no_bash_block_in_verbose_mode(monkeypatch, tmp_path):
"""#41215 also rendered the bash block in verbose mode. The revert removed it