Merge PR #1930: fix async interactive CLI formatting with prompt_toolkit

This commit is contained in:
Re-bin
2026-03-12 15:38:39 +00:00

View File

@@ -19,10 +19,12 @@ if sys.platform == "win32":
pass pass
import typer import typer
from prompt_toolkit import print_formatted_text
from prompt_toolkit import PromptSession from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import HTML from prompt_toolkit.formatted_text import ANSI, HTML
from prompt_toolkit.history import FileHistory from prompt_toolkit.history import FileHistory
from prompt_toolkit.patch_stdout import patch_stdout from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.application import run_in_terminal
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown from rich.markdown import Markdown
from rich.table import Table from rich.table import Table
@@ -111,8 +113,25 @@ def _init_prompt_session() -> None:
) )
def _make_console() -> Console:
return Console(file=sys.stdout)
def _render_interactive_ansi(render_fn) -> str:
"""Render Rich output to ANSI so prompt_toolkit can print it safely."""
ansi_console = Console(
force_terminal=True,
color_system=console.color_system or "standard",
width=console.width,
)
with ansi_console.capture() as capture:
render_fn(ansi_console)
return capture.get()
def _print_agent_response(response: str, render_markdown: bool) -> None: def _print_agent_response(response: str, render_markdown: bool) -> None:
"""Render assistant response with consistent terminal styling.""" """Render assistant response with consistent terminal styling."""
console = _make_console()
content = response or "" content = response or ""
body = Markdown(content) if render_markdown else Text(content) body = Markdown(content) if render_markdown else Text(content)
console.print() console.print()
@@ -121,6 +140,34 @@ def _print_agent_response(response: str, render_markdown: bool) -> None:
console.print() console.print()
async def _print_interactive_line(text: str) -> None:
"""Print async interactive updates with prompt_toolkit-safe Rich styling."""
def _write() -> None:
ansi = _render_interactive_ansi(
lambda c: c.print(f" [dim]↳ {text}[/dim]")
)
print_formatted_text(ANSI(ansi), end="")
await run_in_terminal(_write)
async def _print_interactive_response(response: str, render_markdown: bool) -> None:
"""Print async interactive replies with prompt_toolkit-safe Rich styling."""
def _write() -> None:
content = response or ""
ansi = _render_interactive_ansi(
lambda c: (
c.print(),
c.print(f"[cyan]{__logo__} nanobot[/cyan]"),
c.print(Markdown(content) if render_markdown else Text(content)),
c.print(),
)
)
print_formatted_text(ANSI(ansi), end="")
await run_in_terminal(_write)
def _is_exit_command(command: str) -> bool: def _is_exit_command(command: str) -> bool:
"""Return True when input should end interactive chat.""" """Return True when input should end interactive chat."""
return command.lower() in EXIT_COMMANDS return command.lower() in EXIT_COMMANDS
@@ -610,14 +657,15 @@ def agent(
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: else:
console.print(f" [dim]↳ {msg.content}[/dim]") await _print_interactive_line(msg.content)
elif not turn_done.is_set(): elif not turn_done.is_set():
if msg.content: if msg.content:
turn_response.append(msg.content) turn_response.append(msg.content)
turn_done.set() turn_done.set()
elif msg.content: elif msg.content:
console.print() await _print_interactive_response(msg.content, render_markdown=markdown)
_print_agent_response(msg.content, render_markdown=markdown)
except asyncio.TimeoutError: except asyncio.TimeoutError:
continue continue
except asyncio.CancelledError: except asyncio.CancelledError: