From d470ed0c4c4cb59e83ceb025609b6c4f2d0a614f Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 27 Jun 2026 12:29:55 -0700 Subject: [PATCH] fix(cli): commit tool scrollback lines in verbose mode (non-streaming/MoA) (#53785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the interactive CLI, the aggregator's tool calls under a MoA preset (or any non-streaming model call, e.g. copilot-acp) appeared to overwrite each other instead of building scrollable history. Each tool only updated the transient spinner line; no committed scrollback line was printed. Root cause: persistent tool lines in _on_tool_progress's tool.completed branch were gated on tool_progress_mode in {all, new}, omitting 'verbose'. Streaming models hid the bug because _on_tool_gen_start commits a 'preparing' line per tool during streaming; non-streaming calls (MoA forces _use_streaming=False) never emit that, so under 'verbose' there was no committed line at all — only the self-overwriting spinner. 'verbose' is strictly more than 'all', so it now commits the same scrollback line. Verified live via interactive PTY on the MoA opus-gpt preset: three terminal calls in turn 1 and two in turn 2 each render as separate persistent lines. --- cli.py | 12 +++++++-- tests/cli/test_tool_progress_scrollback.py | 31 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index 6328fe217bf..e62b6f2ef2b 100644 --- a/cli.py +++ b/cli.py @@ -10485,8 +10485,16 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): if event_type == "tool.completed": self._tool_start_time = 0.0 - # Print stacked scrollback line for "all" / "new" modes - if function_name and self.tool_progress_mode in {"all", "new"}: + # Print stacked scrollback line for "new" / "all" / "verbose" modes. + # "verbose" was previously omitted here, so non-streaming model + # calls (MoA aggregator, copilot-acp) rendered each tool only into + # the transient spinner line — which overwrites itself, so no + # scrollable tool history accumulated. Streaming models hid the bug + # because _on_tool_gen_start commits a "preparing" line per tool; + # non-streaming calls never emit that, leaving verbose mode with no + # committed line at all. "verbose" is strictly more than "all", so + # it must commit at least the same line. + if function_name and self.tool_progress_mode in {"new", "all", "verbose"}: duration = kwargs.get("duration", 0.0) is_error = kwargs.get("is_error", False) # Pop stored args from tool.started for this function diff --git a/tests/cli/test_tool_progress_scrollback.py b/tests/cli/test_tool_progress_scrollback.py index d6af08deab9..906cfaf7822 100644 --- a/tests/cli/test_tool_progress_scrollback.py +++ b/tests/cli/test_tool_progress_scrollback.py @@ -169,14 +169,39 @@ class TestToolProgressScrollback: assert mock_print.call_count == 2 - def test_verbose_mode_no_duplicate_scrollback(self): - """In 'verbose' mode, scrollback lines are NOT printed (run_agent handles verbose output).""" + def test_verbose_mode_commits_scrollback_line(self): + """In 'verbose' mode, tool.completed commits a persistent scrollback line. + + Regression: 'verbose' used to be omitted from the scrollback gate on + the premise that run_agent renders verbose output. That premise is + false in the interactive CLI — run_agent's verbose prints are gated on + ``not quiet_mode`` and the interactive CLI runs quiet_mode=True. So a + non-streaming model call (MoA aggregator, copilot-acp) under 'verbose' + rendered each tool only into the self-overwriting spinner, building no + scrollable history. 'verbose' is strictly more than 'all', so it must + commit at least the same line. + """ cli = _make_cli(tool_progress="verbose") with patch.object(_cli_mod, "_cprint") as mock_print: cli._on_tool_progress("tool.started", "terminal", "ls", {"command": "ls"}) cli._on_tool_progress("tool.completed", "terminal", None, None, duration=0.5, is_error=False) - mock_print.assert_not_called() + mock_print.assert_called_once() + + def test_verbose_mode_commits_every_call(self): + """In 'verbose' mode, consecutive same-tool calls each commit a line. + + Mirrors 'all' (no consecutive-repeat suppression — that is 'new'-only), + so a multi-step turn builds a full scrollable tool history. + """ + cli = _make_cli(tool_progress="verbose") + with patch.object(_cli_mod, "_cprint") as mock_print: + cli._on_tool_progress("tool.started", "terminal", "echo one", {"command": "echo one"}) + cli._on_tool_progress("tool.completed", "terminal", None, None, duration=0.1, is_error=False) + cli._on_tool_progress("tool.started", "terminal", "echo two", {"command": "echo two"}) + cli._on_tool_progress("tool.completed", "terminal", None, None, duration=0.1, is_error=False) + + assert mock_print.call_count == 2 def test_verbose_mode_config_does_not_enable_global_debug_logging(self): """display.tool_progress=verbose controls TOOL-CALL DISPLAY ONLY.