Merge PR #360: improve agent CLI chat rendering and input experience

This commit is contained in:
Re-bin 2026-02-09 05:16:04 +00:00
commit 8fa52120b1
2 changed files with 65 additions and 6 deletions

View File

@ -459,11 +459,15 @@ That's it! Environment variables, model prefixing, config matching, and `nanobot
| `nanobot onboard` | Initialize config & workspace | | `nanobot onboard` | Initialize config & workspace |
| `nanobot agent -m "..."` | Chat with the agent | | `nanobot agent -m "..."` | Chat with the agent |
| `nanobot agent` | Interactive chat mode | | `nanobot agent` | Interactive chat mode |
| `nanobot agent --no-markdown` | Show plain-text replies |
| `nanobot agent --logs` | Show runtime logs during chat |
| `nanobot gateway` | Start the gateway | | `nanobot gateway` | Start the gateway |
| `nanobot status` | Show status | | `nanobot status` | Show status |
| `nanobot channels login` | Link WhatsApp (scan QR) | | `nanobot channels login` | Link WhatsApp (scan QR) |
| `nanobot channels status` | Show channel status | | `nanobot channels status` | Show channel status |
Interactive mode exits: `exit`, `quit`, `/exit`, `/quit`, `:q`, or `Ctrl+D`.
<details> <details>
<summary><b>Scheduled Tasks (Cron)</b></summary> <summary><b>Scheduled Tasks (Cron)</b></summary>

View File

@ -10,7 +10,10 @@ import sys
import typer import typer
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.table import Table from rich.table import Table
from rich.text import Text
from nanobot import __version__, __logo__ from nanobot import __version__, __logo__
@ -21,6 +24,7 @@ app = typer.Typer(
) )
console = Console() console = Console()
EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", ":q"}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Lightweight CLI input: readline for arrow keys / history, termios for flush # Lightweight CLI input: readline for arrow keys / history, termios for flush
@ -132,6 +136,28 @@ def _prompt_text() -> str:
return "\001\033[1;34m\002You:\001\033[0m\002 " return "\001\033[1;34m\002You:\001\033[0m\002 "
def _print_agent_response(response: str, render_markdown: bool) -> None:
"""Render assistant response with consistent terminal styling."""
content = response or ""
body = Markdown(content) if render_markdown else Text(content)
console.print()
console.print(
Panel(
body,
title=f"{__logo__} nanobot",
title_align="left",
border_style="cyan",
padding=(0, 1),
)
)
console.print()
def _is_exit_command(command: str) -> bool:
"""Return True when input should end interactive chat."""
return command.lower() in EXIT_COMMANDS
async def _read_interactive_input_async() -> str: async def _read_interactive_input_async() -> str:
"""Read user input with arrow keys and history (runs input() in a thread).""" """Read user input with arrow keys and history (runs input() in a thread)."""
try: try:
@ -410,17 +436,25 @@ def gateway(
def agent( def agent(
message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"), message: str = typer.Option(None, "--message", "-m", help="Message to send to the agent"),
session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"), session_id: str = typer.Option("cli:default", "--session", "-s", help="Session ID"),
markdown: bool = typer.Option(True, "--markdown/--no-markdown", help="Render assistant output as Markdown"),
logs: bool = typer.Option(False, "--logs/--no-logs", help="Show nanobot runtime logs during chat"),
): ):
"""Interact with the agent directly.""" """Interact with the agent directly."""
from nanobot.config.loader import load_config from nanobot.config.loader import load_config
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.agent.loop import AgentLoop from nanobot.agent.loop import AgentLoop
from loguru import logger
config = load_config() config = load_config()
bus = MessageBus() bus = MessageBus()
provider = _make_provider(config) provider = _make_provider(config)
if logs:
logger.enable("nanobot")
else:
logger.disable("nanobot")
agent_loop = AgentLoop( agent_loop = AgentLoop(
bus=bus, bus=bus,
provider=provider, provider=provider,
@ -430,17 +464,25 @@ def agent(
restrict_to_workspace=config.tools.restrict_to_workspace, restrict_to_workspace=config.tools.restrict_to_workspace,
) )
# Show spinner when logs are off (no output to miss); skip when logs are on
def _thinking_ctx():
if logs:
from contextlib import nullcontext
return nullcontext()
return console.status("[dim]nanobot is thinking...[/dim]", spinner="dots")
if message: if message:
# Single message mode # Single message mode
async def run_once(): async def run_once():
with _thinking_ctx():
response = await agent_loop.process_direct(message, session_id) response = await agent_loop.process_direct(message, session_id)
console.print(f"\n{__logo__} {response}") _print_agent_response(response, render_markdown=markdown)
asyncio.run(run_once()) asyncio.run(run_once())
else: else:
# Interactive mode # Interactive mode
_enable_line_editing() _enable_line_editing()
console.print(f"{__logo__} Interactive mode (Ctrl+C to exit)\n") console.print(f"{__logo__} Interactive mode (type [bold]exit[/bold] or [bold]Ctrl+C[/bold] to quit)\n")
# input() runs in a worker thread that can't be cancelled. # input() runs in a worker thread that can't be cancelled.
# Without this handler, asyncio.run() would hang waiting for it. # Without this handler, asyncio.run() would hang waiting for it.
@ -457,16 +499,29 @@ def agent(
try: try:
_flush_pending_tty_input() _flush_pending_tty_input()
user_input = await _read_interactive_input_async() user_input = await _read_interactive_input_async()
if not user_input.strip(): command = user_input.strip()
if not command:
continue continue
if _is_exit_command(command):
_save_history()
_restore_terminal()
console.print("\nGoodbye!")
break
with _thinking_ctx():
response = await agent_loop.process_direct(user_input, session_id) response = await agent_loop.process_direct(user_input, session_id)
console.print(f"\n{__logo__} {response}\n") _print_agent_response(response, render_markdown=markdown)
except KeyboardInterrupt: except KeyboardInterrupt:
_save_history() _save_history()
_restore_terminal() _restore_terminal()
console.print("\nGoodbye!") console.print("\nGoodbye!")
break break
except EOFError:
_save_history()
_restore_terminal()
console.print("\nGoodbye!")
break
asyncio.run(run_interactive()) asyncio.run(run_interactive())