mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-19 16:12:30 +00:00
fix(cli): clear thinking spinner before trace output
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3fab736262
commit
53831e1611
@ -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:
|
||||||
|
|||||||
@ -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] = []
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user