fix(cli): keep trace output under assistant header
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
-4
@@ -176,13 +176,15 @@ def _print_agent_response(
|
||||
response: str,
|
||||
render_markdown: bool,
|
||||
metadata: dict | None = None,
|
||||
show_header: bool = True,
|
||||
) -> None:
|
||||
"""Render assistant response with consistent terminal styling."""
|
||||
console = _make_console()
|
||||
content = response or ""
|
||||
body = _response_renderable(content, render_markdown, metadata)
|
||||
console.print()
|
||||
console.print(f"[cyan]{__logo__} nanobot[/cyan]")
|
||||
if show_header:
|
||||
console.print()
|
||||
console.print(f"[cyan]{__logo__} nanobot[/cyan]")
|
||||
console.print(body)
|
||||
console.print()
|
||||
|
||||
@@ -235,6 +237,8 @@ def _print_cli_progress_line(text: str, thinking: ThinkingSpinner | None, render
|
||||
target = renderer.console if renderer else console
|
||||
pause = renderer.pause_spinner() if renderer else (thinking.pause() if thinking else nullcontext())
|
||||
with pause:
|
||||
if renderer:
|
||||
renderer.ensure_header()
|
||||
target.print(f" [dim]↳ {text}[/dim]")
|
||||
|
||||
|
||||
@@ -245,6 +249,8 @@ def _print_cli_reasoning(text: str, thinking: ThinkingSpinner | None, renderer:
|
||||
target = renderer.console if renderer else console
|
||||
pause = renderer.pause_spinner() if renderer else (thinking.pause() if thinking else nullcontext())
|
||||
with pause:
|
||||
if renderer:
|
||||
renderer.ensure_header()
|
||||
target.print(f"[dim italic]✻ {text}[/dim italic]")
|
||||
|
||||
|
||||
@@ -254,6 +260,7 @@ async def _print_interactive_progress_line(text: str, thinking: ThinkingSpinner
|
||||
return
|
||||
if renderer:
|
||||
with renderer.pause_spinner():
|
||||
renderer.ensure_header()
|
||||
renderer.console.print(f" [dim]↳ {text}[/dim]")
|
||||
else:
|
||||
with thinking.pause() if thinking else nullcontext():
|
||||
@@ -275,7 +282,7 @@ async def _maybe_print_interactive_progress(
|
||||
return False
|
||||
|
||||
is_tool_hint = metadata.get("_tool_hint", False)
|
||||
is_reasoning = metadata.get("_reasoning", False)
|
||||
is_reasoning = metadata.get("_reasoning", False) or metadata.get("_reasoning_delta", False)
|
||||
if is_reasoning:
|
||||
if channels_config and not channels_config.show_reasoning:
|
||||
return True
|
||||
@@ -1118,10 +1125,14 @@ def agent(
|
||||
)
|
||||
if not renderer.streamed:
|
||||
await renderer.close()
|
||||
print_kwargs: dict[str, Any] = {}
|
||||
if renderer.header_printed:
|
||||
print_kwargs["show_header"] = False
|
||||
_print_agent_response(
|
||||
response.content if response else "",
|
||||
render_markdown=markdown,
|
||||
metadata=response.metadata if response else None,
|
||||
**print_kwargs,
|
||||
)
|
||||
await agent_loop.close_mcp()
|
||||
|
||||
@@ -1246,8 +1257,14 @@ def agent(
|
||||
if content and not meta.get("_streamed"):
|
||||
if renderer:
|
||||
await renderer.close()
|
||||
print_kwargs: dict[str, Any] = {}
|
||||
if renderer and renderer.header_printed:
|
||||
print_kwargs["show_header"] = False
|
||||
_print_agent_response(
|
||||
content, render_markdown=markdown, metadata=meta,
|
||||
content,
|
||||
render_markdown=markdown,
|
||||
metadata=meta,
|
||||
**print_kwargs,
|
||||
)
|
||||
elif renderer and not renderer.streamed:
|
||||
await renderer.close()
|
||||
|
||||
+37
-10
@@ -10,6 +10,7 @@ that plagued earlier approaches.
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from contextlib import contextmanager, nullcontext
|
||||
|
||||
from rich.console import Console
|
||||
from rich.live import Live
|
||||
@@ -93,6 +94,7 @@ class StreamRenderer:
|
||||
self._console = _make_console()
|
||||
self._live: Live | None = None
|
||||
self._spinner: ThinkingSpinner | None = None
|
||||
self._header_printed = False
|
||||
self._start_spinner()
|
||||
|
||||
def _renderable(self):
|
||||
@@ -122,12 +124,41 @@ class StreamRenderer:
|
||||
"""Expose the Live's console so external print functions can use it."""
|
||||
return self._console
|
||||
|
||||
@property
|
||||
def header_printed(self) -> bool:
|
||||
"""Whether this turn has already opened the assistant output block."""
|
||||
return self._header_printed
|
||||
|
||||
def ensure_header(self) -> None:
|
||||
"""Print the assistant header once, before trace or answer content."""
|
||||
if self._header_printed:
|
||||
return
|
||||
self._stop_spinner()
|
||||
self._console.print()
|
||||
header = f"{self._bot_icon} {self._bot_name}" if self._bot_icon else self._bot_name
|
||||
self._console.print(f"[cyan]{header}[/cyan]")
|
||||
self._header_printed = True
|
||||
|
||||
def pause_spinner(self):
|
||||
"""Context manager: temporarily stop spinner for clean output."""
|
||||
if self._spinner:
|
||||
return self._spinner.pause()
|
||||
from contextlib import nullcontext
|
||||
return nullcontext()
|
||||
"""Context manager: temporarily stop transient output for clean trace lines."""
|
||||
@contextmanager
|
||||
def _pause():
|
||||
live_was_active = self._live is not None
|
||||
if self._live:
|
||||
# Trace/reasoning can arrive after answer streaming has started.
|
||||
# Stop the transient Live view first so it does not leak a raw
|
||||
# partial markdown frame before the trace line.
|
||||
self._live.stop()
|
||||
self._live = None
|
||||
with self._spinner.pause() if self._spinner else nullcontext():
|
||||
yield
|
||||
# If more answer deltas arrive after the trace, on_delta() will
|
||||
# create a fresh Live using the existing buffer. If no deltas arrive,
|
||||
# on_end() prints the final buffered answer once.
|
||||
if live_was_active:
|
||||
return
|
||||
|
||||
return _pause()
|
||||
|
||||
async def on_delta(self, delta: str) -> None:
|
||||
self.streamed = True
|
||||
@@ -135,10 +166,7 @@ class StreamRenderer:
|
||||
if self._live is None:
|
||||
if not self._buf.strip():
|
||||
return
|
||||
self._stop_spinner()
|
||||
self._console.print()
|
||||
header = f"{self._bot_icon} {self._bot_name}" if self._bot_icon else self._bot_name
|
||||
self._console.print(f"[cyan]{header}[/cyan]")
|
||||
self.ensure_header()
|
||||
self._live = Live(
|
||||
self._renderable(),
|
||||
console=self._console,
|
||||
@@ -174,7 +202,6 @@ class StreamRenderer:
|
||||
|
||||
def pause(self):
|
||||
"""Context manager: pause spinner for external output. No-op once streaming has started."""
|
||||
from contextlib import nullcontext
|
||||
if self._spinner:
|
||||
return self._spinner.pause()
|
||||
return nullcontext()
|
||||
|
||||
Reference in New Issue
Block a user