diff --git a/nanobot/cli/stream.py b/nanobot/cli/stream.py index 899950fb6..24a141cdd 100644 --- a/nanobot/cli/stream.py +++ b/nanobot/cli/stream.py @@ -143,10 +143,13 @@ class StreamRenderer: return self._header_printed def ensure_header(self) -> None: - """Print the assistant header once, before trace or answer content.""" + """Stop transient status and print the assistant header once.""" + # A turn can print trace rows before the final answer, then restart the + # spinner while tools run. The next answer delta still needs to stop + # that spinner even though the header was already printed. + self._stop_spinner() if self._header_printed: return - self._stop_spinner() self._console.print() header = f"{self._bot_icon} {self._bot_name}" if self._bot_icon else self._bot_name self._console.print(f"[cyan]{header}[/cyan]") diff --git a/tests/cli/test_cli_input.py b/tests/cli/test_cli_input.py index 3f5619c4f..34046e8d4 100644 --- a/tests/cli/test_cli_input.py +++ b/tests/cli/test_cli_input.py @@ -115,6 +115,24 @@ def test_thinking_spinner_clears_status_line_when_paused(): assert "\r\x1b[2K" in stream.getvalue() +def test_stream_renderer_stops_spinner_even_after_header_printed(): + """A later answer delta must stop the spinner even when header already exists.""" + stream = StringIO() + stream.isatty = lambda: True # type: ignore[method-assign] + mock_console = MagicMock() + mock_console.file = stream + spinner = MagicMock() + mock_console.status.return_value = spinner + + with patch.object(stream_mod, "_make_console", return_value=mock_console): + renderer = stream_mod.StreamRenderer(show_spinner=True) + renderer._header_printed = True + renderer.ensure_header() + + spinner.stop.assert_called_once() + assert "\r\x1b[2K" in stream.getvalue() + + def test_print_cli_progress_line_opens_renderer_header_before_trace(): """Trace lines should appear under the assistant header, not under You.""" order: list[str] = []