Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13002,13 +13002,20 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non
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:
Expand All @@ -13020,12 +13027,25 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non
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
Expand All @@ -13047,10 +13067,10 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non
# "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()
Expand Down
61 changes: 57 additions & 4 deletions tests/gateway/test_run_progress_topics.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,8 +1297,10 @@ def run_conversation(self, message, conversation_history=None, task_id=None):
@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")
Expand Down Expand Up @@ -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
Expand Down
Loading