refactor(cli): restore context manager pattern for spinner lifecycle
Replace manual _active_spinner + _pause_spinner/_resume_spinner with _ThinkingSpinner class that owns the spinner lifecycle via __enter__/ __exit__ and provides a pause() context manager for temporarily stopping the spinner during progress output. Benefits: - Restores Pythonic context manager pattern matching original code - Eliminates duplicated start/stop boilerplate between single-message and interactive modes - pause() context manager guarantees resume even if print raises - _active flag prevents post-teardown resume from async callbacks
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"""CLI commands for nanobot."""
|
"""CLI commands for nanobot."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from contextlib import contextmanager
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import signal
|
import signal
|
||||||
@@ -635,29 +636,40 @@ def agent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Show spinner when logs are off (no output to miss); skip when logs are on
|
# Show spinner when logs are off (no output to miss); skip when logs are on
|
||||||
def _make_spinner():
|
class _ThinkingSpinner:
|
||||||
if logs:
|
"""Context manager that owns spinner lifecycle with pause support."""
|
||||||
return None
|
|
||||||
return console.status("[dim]nanobot is thinking...[/dim]", spinner="dots")
|
|
||||||
|
|
||||||
# Shared reference so progress callbacks can pause/resume the spinner
|
def __init__(self):
|
||||||
_active_spinner = None
|
self._spinner = None if logs else console.status(
|
||||||
|
"[dim]nanobot is thinking...[/dim]", spinner="dots"
|
||||||
|
)
|
||||||
|
self._active = False
|
||||||
|
|
||||||
def _pause_spinner() -> None:
|
def __enter__(self):
|
||||||
"""Temporarily stop the spinner before printing progress."""
|
if self._spinner:
|
||||||
if _active_spinner is not None:
|
self._spinner.start()
|
||||||
|
self._active = True
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self._active = False
|
||||||
|
if self._spinner:
|
||||||
|
self._spinner.stop()
|
||||||
|
return False
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def pause(self):
|
||||||
|
"""Temporarily stop spinner for clean console output."""
|
||||||
|
if self._spinner and self._active:
|
||||||
|
self._spinner.stop()
|
||||||
try:
|
try:
|
||||||
_active_spinner.stop()
|
yield
|
||||||
except Exception:
|
finally:
|
||||||
pass
|
if self._spinner and self._active:
|
||||||
|
self._spinner.start()
|
||||||
|
|
||||||
def _resume_spinner() -> None:
|
# Shared reference for progress callbacks
|
||||||
"""Restart the spinner after printing progress."""
|
_thinking: _ThinkingSpinner | None = None
|
||||||
if _active_spinner is not None:
|
|
||||||
try:
|
|
||||||
_active_spinner.start()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _cli_progress(content: str, *, tool_hint: bool = False) -> None:
|
async def _cli_progress(content: str, *, tool_hint: bool = False) -> None:
|
||||||
ch = agent_loop.channels_config
|
ch = agent_loop.channels_config
|
||||||
@@ -665,26 +677,20 @@ def agent(
|
|||||||
return
|
return
|
||||||
if ch and not tool_hint and not ch.send_progress:
|
if ch and not tool_hint and not ch.send_progress:
|
||||||
return
|
return
|
||||||
_pause_spinner()
|
if _thinking:
|
||||||
try:
|
with _thinking.pause():
|
||||||
|
console.print(f" [dim]↳ {content}[/dim]")
|
||||||
|
else:
|
||||||
console.print(f" [dim]↳ {content}[/dim]")
|
console.print(f" [dim]↳ {content}[/dim]")
|
||||||
finally:
|
|
||||||
_resume_spinner()
|
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
# Single message mode — direct call, no bus needed
|
# Single message mode — direct call, no bus needed
|
||||||
async def run_once():
|
async def run_once():
|
||||||
nonlocal _active_spinner
|
nonlocal _thinking
|
||||||
spinner = _make_spinner()
|
_thinking = _ThinkingSpinner()
|
||||||
_active_spinner = spinner
|
with _thinking:
|
||||||
if spinner:
|
|
||||||
spinner.start()
|
|
||||||
try:
|
|
||||||
response = await agent_loop.process_direct(message, session_id, on_progress=_cli_progress)
|
response = await agent_loop.process_direct(message, session_id, on_progress=_cli_progress)
|
||||||
finally:
|
_thinking = None
|
||||||
if spinner:
|
|
||||||
spinner.stop()
|
|
||||||
_active_spinner = None
|
|
||||||
_print_agent_response(response, render_markdown=markdown)
|
_print_agent_response(response, render_markdown=markdown)
|
||||||
await agent_loop.close_mcp()
|
await agent_loop.close_mcp()
|
||||||
|
|
||||||
@@ -733,12 +739,11 @@ def agent(
|
|||||||
pass
|
pass
|
||||||
elif ch and not is_tool_hint and not ch.send_progress:
|
elif ch and not is_tool_hint and not ch.send_progress:
|
||||||
pass
|
pass
|
||||||
else:
|
elif _thinking:
|
||||||
_pause_spinner()
|
with _thinking.pause():
|
||||||
try:
|
await _print_interactive_line(msg.content)
|
||||||
|
else:
|
||||||
await _print_interactive_line(msg.content)
|
await _print_interactive_line(msg.content)
|
||||||
finally:
|
|
||||||
_resume_spinner()
|
|
||||||
|
|
||||||
elif not turn_done.is_set():
|
elif not turn_done.is_set():
|
||||||
if msg.content:
|
if msg.content:
|
||||||
@@ -778,17 +783,11 @@ def agent(
|
|||||||
content=user_input,
|
content=user_input,
|
||||||
))
|
))
|
||||||
|
|
||||||
nonlocal _active_spinner
|
nonlocal _thinking
|
||||||
spinner = _make_spinner()
|
_thinking = _ThinkingSpinner()
|
||||||
_active_spinner = spinner
|
with _thinking:
|
||||||
if spinner:
|
|
||||||
spinner.start()
|
|
||||||
try:
|
|
||||||
await turn_done.wait()
|
await turn_done.wait()
|
||||||
finally:
|
_thinking = None
|
||||||
if spinner:
|
|
||||||
spinner.stop()
|
|
||||||
_active_spinner = None
|
|
||||||
|
|
||||||
if turn_response:
|
if turn_response:
|
||||||
_print_agent_response(turn_response[0], render_markdown=markdown)
|
_print_agent_response(turn_response[0], render_markdown=markdown)
|
||||||
|
|||||||
Reference in New Issue
Block a user