fix(cli): clear thinking spinner before trace output

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren 2026-05-13 09:15:53 +00:00
parent 3fab736262
commit 53831e1611
2 changed files with 31 additions and 0 deletions

View File

@ -18,6 +18,16 @@ from rich.markdown import Markdown
from rich.text import Text from rich.text import Text
def _clear_current_line(console: Console) -> None:
"""Erase a transient status line before printing persistent output."""
file = console.file
isatty = getattr(file, "isatty", lambda: False)
if not isatty():
return
file.write("\r\x1b[2K")
file.flush()
def _make_console() -> Console: def _make_console() -> Console:
"""Create a Console that emits plain text when stdout is not a TTY. """Create a Console that emits plain text when stdout is not a TTY.
@ -37,6 +47,7 @@ class ThinkingSpinner:
def __init__(self, console: Console | None = None, bot_name: str = "nanobot"): def __init__(self, console: Console | None = None, bot_name: str = "nanobot"):
c = console or _make_console() c = console or _make_console()
self._console = c
self._spinner = c.status(f"[dim]{bot_name} is thinking...[/dim]", spinner="dots") self._spinner = c.status(f"[dim]{bot_name} is thinking...[/dim]", spinner="dots")
self._active = False self._active = False
@ -48,6 +59,7 @@ class ThinkingSpinner:
def __exit__(self, *exc): def __exit__(self, *exc):
self._active = False self._active = False
self._spinner.stop() self._spinner.stop()
_clear_current_line(self._console)
return False return False
def pause(self): def pause(self):
@ -58,6 +70,7 @@ class ThinkingSpinner:
def _ctx(): def _ctx():
if self._spinner and self._active: if self._spinner and self._active:
self._spinner.stop() self._spinner.stop()
_clear_current_line(self._console)
try: try:
yield yield
finally: finally:

View File

@ -1,5 +1,6 @@
import asyncio import asyncio
from contextlib import nullcontext from contextlib import nullcontext
from io import StringIO
from unittest.mock import AsyncMock, MagicMock, call, patch from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest import pytest
@ -97,6 +98,23 @@ def test_print_cli_progress_line_pauses_spinner_before_printing():
assert order == ["start", "stop", "print", "start", "stop"] assert order == ["start", "stop", "print", "start", "stop"]
def test_thinking_spinner_clears_status_line_when_paused():
"""Stopping the spinner should erase its transient line before output."""
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
thinking = stream_mod.ThinkingSpinner(console=mock_console)
with thinking:
with thinking.pause():
pass
assert "\r\x1b[2K" in stream.getvalue()
def test_print_cli_progress_line_opens_renderer_header_before_trace(): def test_print_cli_progress_line_opens_renderer_header_before_trace():
"""Trace lines should appear under the assistant header, not under You.""" """Trace lines should appear under the assistant header, not under You."""
order: list[str] = [] order: list[str] = []