feat(cli): display model reasoning content during streaming

Add show_reasoning config (default: False) to display model
thinking/reasoning content in the TUI during streaming.  Reasoning
is emitted via a new emit_reasoning hook on AgentHook, gated by the
channels config.  Display uses ✻ prefix with dim italic styling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Flinn Xie
2026-05-11 01:02:49 +08:00
co-authored by Claude Opus 4.7
parent d630ac90d1
commit 3a27af0018
9 changed files with 182 additions and 13 deletions
+24 -3
View File
@@ -237,6 +237,16 @@ def _print_cli_progress_line(text: str, thinking: ThinkingSpinner | None, render
target.print(f" [dim]↳ {text}[/dim]")
def _print_cli_reasoning(text: str, thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None:
"""Print reasoning/thinking content in a distinct style."""
if not text.strip():
return
target = renderer.console if renderer else console
pause = renderer.pause_spinner() if renderer else (thinking.pause() if thinking else nullcontext())
with pause:
target.print(f"[dim italic]✻ {text}[/dim italic]")
async def _print_interactive_progress_line(text: str, thinking: ThinkingSpinner | None, renderer: StreamRenderer | None = None) -> None:
"""Print an interactive progress line, pausing the spinner if needed."""
if not text.strip():
@@ -264,12 +274,18 @@ async def _maybe_print_interactive_progress(
return False
is_tool_hint = metadata.get("_tool_hint", False)
is_reasoning = metadata.get("_reasoning", False)
if channels_config and is_tool_hint and not channels_config.send_tool_hints:
return True
if channels_config and not is_tool_hint and not channels_config.send_progress:
return True
if is_reasoning and channels_config and not channels_config.show_reasoning:
return True
await _print_interactive_progress_line(msg.content, thinking, renderer)
if is_reasoning:
_print_cli_reasoning(msg.content, thinking, renderer)
else:
await _print_interactive_progress_line(msg.content, thinking, renderer)
return True
@@ -1129,13 +1145,18 @@ def agent(
_thinking: ThinkingSpinner | None = None
def _make_progress(renderer: StreamRenderer | None = None):
async def _cli_progress(content: str, *, tool_hint: bool = False, **_kwargs: Any) -> None:
async def _cli_progress(content: str, *, tool_hint: bool = False, reasoning: bool = False, **_kwargs: Any) -> None:
ch = agent_loop.channels_config
if ch and tool_hint and not ch.send_tool_hints:
return
if ch and not tool_hint and not ch.send_progress:
return
_print_cli_progress_line(content, _thinking, renderer)
if reasoning and ch and not ch.show_reasoning:
return
if reasoning:
_print_cli_reasoning(content, _thinking, renderer)
else:
_print_cli_progress_line(content, _thinking, renderer)
return _cli_progress
if message:
+5 -6
View File
@@ -84,7 +84,6 @@ class StreamRenderer:
self._show_spinner = show_spinner
self._buf = ""
self.streamed = False
self._header_printed = False
self._console = _make_console()
self._live: Live | None = None
self._spinner: ThinkingSpinner | None = None
@@ -127,12 +126,12 @@ class StreamRenderer:
async def on_delta(self, delta: str) -> None:
self.streamed = True
self._buf += delta
if not self._header_printed and self._buf.strip():
if self._live is None:
if not self._buf.strip():
return
self._stop_spinner()
self._console.print()
self._console.print(f"[cyan]{__logo__} nanobot[/cyan]")
self._header_printed = True
self._stop_spinner()
if not self._live:
self._live = Live(
self._renderable(),
console=self._console,
@@ -153,7 +152,7 @@ class StreamRenderer:
self._live.stop()
self._live = None
self._stop_spinner()
if self._header_printed and self._buf.strip():
if self._buf.strip():
# Print final rendered content (persists after Live is gone).
out = sys.stdout
out.write(self._render_str())