diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 8184b51b1..68d2f8506 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -984,6 +984,9 @@ def agent( while True: try: _flush_pending_tty_input() + # Stop spinner before user input to avoid prompt_toolkit conflicts + if renderer: + renderer.stop_for_input() user_input = await _read_interactive_input_async() command = user_input.strip() if not command: diff --git a/nanobot/cli/stream.py b/nanobot/cli/stream.py index 16586ecd0..8151e3ddc 100644 --- a/nanobot/cli/stream.py +++ b/nanobot/cli/stream.py @@ -18,7 +18,7 @@ from nanobot import __logo__ def _make_console() -> Console: - return Console(file=sys.stdout) + return Console(file=sys.stdout, force_terminal=True) class ThinkingSpinner: @@ -120,6 +120,10 @@ class StreamRenderer: else: _make_console().print() + def stop_for_input(self) -> None: + """Stop spinner before user input to avoid prompt_toolkit conflicts.""" + self._stop_spinner() + async def close(self) -> None: """Stop spinner/live without rendering a final streamed round.""" if self._live: diff --git a/tests/cli/test_cli_input.py b/tests/cli/test_cli_input.py index 142dc7260..b772293bc 100644 --- a/tests/cli/test_cli_input.py +++ b/tests/cli/test_cli_input.py @@ -145,3 +145,29 @@ def test_response_renderable_without_metadata_keeps_markdown_path(): renderable = commands._response_renderable(help_text, render_markdown=True) assert renderable.__class__.__name__ == "Markdown" + + +def test_stream_renderer_stop_for_input_stops_spinner(): + """stop_for_input should stop the active spinner to avoid prompt_toolkit conflicts.""" + spinner = MagicMock() + mock_console = MagicMock() + mock_console.status.return_value = spinner + + # Create renderer with mocked console + with patch.object(stream_mod, "_make_console", return_value=mock_console): + renderer = stream_mod.StreamRenderer(show_spinner=True) + + # Verify spinner started + spinner.start.assert_called_once() + + # Stop for input + renderer.stop_for_input() + + # Verify spinner stopped + spinner.stop.assert_called_once() + + +def test_make_console_uses_force_terminal(): + """Console should be created with force_terminal=True for proper ANSI handling.""" + console = stream_mod._make_console() + assert console._force_terminal is True