mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-30 06:45:55 +00:00
fix(cli): prevent spinner ANSI escape codes from being printed verbatim
Fixes #2591 The "nanobot is thinking..." spinner was printing ANSI escape codes literally in some terminals, causing garbled output like: ?[2K?[32m⠧?[0m ?[2mnanobot is thinking...?[0m Root causes: 1. Console created without force_terminal=True, so Rich couldn't reliably detect terminal capabilities 2. Spinner continued running during user input prompt, conflicting with prompt_toolkit Changes: - Set force_terminal=True in _make_console() for proper ANSI handling - Add stop_for_input() method to StreamRenderer - Call stop_for_input() before reading user input in interactive mode - Add tests for the new functionality
This commit is contained in:
parent
5da86258cc
commit
0d6deb9197
@ -984,6 +984,9 @@ def agent(
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
_flush_pending_tty_input()
|
_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()
|
user_input = await _read_interactive_input_async()
|
||||||
command = user_input.strip()
|
command = user_input.strip()
|
||||||
if not command:
|
if not command:
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from nanobot import __logo__
|
|||||||
|
|
||||||
|
|
||||||
def _make_console() -> Console:
|
def _make_console() -> Console:
|
||||||
return Console(file=sys.stdout)
|
return Console(file=sys.stdout, force_terminal=True)
|
||||||
|
|
||||||
|
|
||||||
class ThinkingSpinner:
|
class ThinkingSpinner:
|
||||||
@ -120,6 +120,10 @@ class StreamRenderer:
|
|||||||
else:
|
else:
|
||||||
_make_console().print()
|
_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:
|
async def close(self) -> None:
|
||||||
"""Stop spinner/live without rendering a final streamed round."""
|
"""Stop spinner/live without rendering a final streamed round."""
|
||||||
if self._live:
|
if self._live:
|
||||||
|
|||||||
@ -145,3 +145,29 @@ def test_response_renderable_without_metadata_keeps_markdown_path():
|
|||||||
renderable = commands._response_renderable(help_text, render_markdown=True)
|
renderable = commands._response_renderable(help_text, render_markdown=True)
|
||||||
|
|
||||||
assert renderable.__class__.__name__ == "Markdown"
|
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user