"""Click CLI for the Navi Code terminal client."""
from __future__ import annotations
import asyncio
import click
from clients.terminal import api
from clients.terminal.config import settings
from clients.terminal.render import Renderer
from clients.terminal.state import StateManager
from clients.terminal.ws_client import NaviWebSocketClient
@click.command(context_settings={"ignore_unknown_options": True, "allow_extra_args": True})
@click.argument("prompt", required=False)
@click.option("--base-url", default=None, help="Navi API base URL.")
@click.option("--ws-url", default=None, help="Navi WebSocket URL (defaults to base-url).")
@click.option("--profile-id", default=None, help="Profile to use for new sessions.")
@click.option("--new-session", is_flag=True, help="Create a new session even if state exists.")
@click.option("--show-thinking", is_flag=True, help="Show model reasoning blocks.")
@click.option("--show-events/--no-events", default=True, help="Show tool call events.")
@click.option("--raw", is_flag=True, help="Use the plain CLI instead of the TUI.")
@click.version_option(version="0.1.0", prog_name="navi-code")
def main(
prompt: str | None,
base_url: str | None,
ws_url: str | None,
profile_id: str | None,
new_session: bool,
show_thinking: bool,
show_events: bool,
raw: bool,
) -> None:
"""Navi Code — terminal client for Navi.
Without PROMPT, runs the TUI. With PROMPT, sends one message and exits.
Use --raw to run the plain CLI instead of the TUI.
"""
if base_url:
settings.base_url = base_url
if ws_url:
settings.ws_url = ws_url
if show_thinking:
settings.show_thinking = True
settings.show_events = show_events
if raw or prompt:
_run_raw(prompt, new_session, profile_id)
return
_run_tui(profile_id, new_session)
def _run_raw(prompt: str | None, new_session: bool, profile_id: str | None) -> None:
state = StateManager()
session_id = _resolve_session_id(state, new_session, profile_id)
if not session_id:
raise click.ClickException("Failed to create or resume a session.")
renderer = Renderer(show_thinking=settings.show_thinking, show_events=settings.show_events)
client = NaviWebSocketClient(session_id, renderer=renderer)
if prompt:
asyncio.run(_run_one_shot(client, prompt))
return
asyncio.run(_run_interactive(client, state))
def _run_tui(profile_id: str | None, new_session: bool) -> None:
from clients.terminal.tui.tui_app import NaviCodeTui
app = NaviCodeTui(profile_id=profile_id, new_session=new_session)
app.run()
def _resolve_session_id(state: StateManager, force_new: bool, profile_id: str | None) -> str | None:
if not force_new:
saved = state.get_session_id()
if saved:
try:
session = api.get_session(saved)
click.secho(
f"Resumed session {session['id'][:8]} (profile {session['profile_id']})",
fg="bright_black",
)
return session["id"]
except Exception:
state.clear_session_id()
profile = profile_id or settings.default_profile_id
try:
session = api.create_session(profile)
except Exception as exc:
click.secho(f"Failed to create session: {exc}", fg="red", err=True)
return None
state.set_session_id(session["id"])
click.secho(
f"Created session {session['id'][:8]} (profile {session['profile_id']})",
fg="green",
)
return session["id"]
async def _run_one_shot(client: NaviWebSocketClient, prompt: str) -> None:
await client.run_one_shot(prompt)
async def _run_interactive(client: NaviWebSocketClient, state: StateManager) -> None:
click.secho("Navi Code interactive mode. Type /quit to exit, /help for commands.", fg="cyan")
await client.connect()
receive_task = asyncio.create_task(client.receive_loop())
try:
while True:
try:
user_input = await asyncio.get_event_loop().run_in_executor(
None, lambda: input(click.style("You: ", fg="blue", bold=True)),
)
except EOFError:
break
user_input = user_input.strip()
if not user_input:
continue
if user_input.startswith("/"):
handled = await _handle_command(user_input, state, client)
if handled:
continue
client.enqueue(user_input)
finally:
client.stop_input()
receive_task.cancel()
try:
await receive_task
except asyncio.CancelledError:
pass
await client.close()
async def _handle_command(cmd: str, state: StateManager, client: NaviWebSocketClient) -> bool:
parts = cmd.split()
head = parts[0].lower()
if head == "/quit":
raise click.exceptions.Exit(0)
if head == "/help":
click.echo("Commands:")
click.echo(" /new create a new session")
click.echo(" /sessions list server sessions")
click.echo(" /switch <id> switch to another session")
click.echo(" /profile show current session profile")
click.echo(" /clear clear local session state")
click.echo(" /quit exit")
return True
if head == "/new":
try:
session = api.create_session(settings.default_profile_id)
except Exception as exc:
click.secho(f"Failed to create session: {exc}", fg="red", err=True)
return True
state.set_session_id(session["id"])
click.secho(
f"Switched to new session {session['id'][:8]} (profile {session['profile_id']})",
fg="green",
)
return True
if head == "/sessions":
try:
sessions = api.list_sessions()
except Exception as exc:
click.secho(f"Failed to list sessions: {exc}", fg="red", err=True)
return True
for s in sessions:
click.echo(f" {s['id'][:8]} {s.get('profile_id', 'unknown')} {s.get('title', '')}")
return True
if head == "/switch" and len(parts) == 2:
target = parts[1]
try:
session = api.get_session(target)
except Exception as exc:
# Try to find by prefix
try:
sessions = api.list_sessions()
matches = [s for s in sessions if s["id"].startswith(target)]
if len(matches) == 1:
session = matches[0]
else:
raise exc
except Exception:
click.secho(f"Session not found: {target}", fg="red", err=True)
return True
state.set_session_id(session["id"])
click.secho(
f"Switched to session {session['id'][:8]} (profile {session['profile_id']})",
fg="green",
)
return True
if head == "/profile":
try:
current = api.get_session(state.get_session_id() or "")
click.echo(f"Profile: {current.get('profile_id', 'unknown')}")
click.echo(f"Session: {current['id']}")
except Exception as exc:
click.secho(f"Failed to get session: {exc}", fg="red", err=True)
return True
if head == "/clear":
state.clear_session_id()
click.secho("Local session state cleared.", fg="green")
return True
return False
if __name__ == "__main__":
main()