"""Built-in slash commands for Navi Code TUI."""
from __future__ import annotations
from clients.terminal import api
from clients.terminal.config import settings
from clients.terminal.tui.commands.base import BaseCommand, CommandMeta
from clients.terminal.tui.context import TuiContext
class HelpCommand(BaseCommand):
meta = CommandMeta(
name="help",
aliases=(),
description="Show available slash commands.",
keybind=None,
)
async def execute(self, ctx: TuiContext, args: str) -> None:
from clients.terminal.tui.commands.registry import get_registry
registry = get_registry()
lines = ["[b]Slash commands[/b]"]
for cmd in registry.all():
aliases = f" ({', '.join(cmd.meta.aliases)})" if cmd.meta.aliases else ""
key = f" [{cmd.meta.keybind}]" if cmd.meta.keybind else ""
lines.append(f" /{cmd.meta.name}{aliases}{key} — {cmd.meta.description}")
ctx.chat_panel.handle_ws_event({"type": "status", "content": "\n".join(lines)})
class NewCommand(BaseCommand):
meta = CommandMeta(
name="new",
aliases=("clear",),
description="Start a new session.",
keybind="ctrl+x n",
)
async def execute(self, ctx: TuiContext, args: str) -> None:
try:
session = api.create_session(settings.default_profile_id)
except Exception as exc:
ctx.chat_panel.handle_ws_event({"type": "error", "message": f"Failed to create session: {exc}"})
return
ctx.session_id = session["id"]
ctx.profile_id = session.get("profile_id")
ctx.state.set_session_id(session["id"])
ctx.status_panel.set_session(session["id"])
ctx.status_panel.set_profile(ctx.profile_id or settings.default_profile_id)
ctx.chat_panel.handle_ws_event({"type": "status", "content": f"Created session {session['id'][:8]}"})
await _reconnect_ws(ctx)
class SessionsCommand(BaseCommand):
meta = CommandMeta(
name="sessions",
aliases=("resume", "continue"),
description="List and switch between sessions.",
keybind="ctrl+x l",
)
async def execute(self, ctx: TuiContext, args: str) -> None:
try:
sessions = api.list_sessions()
except Exception as exc:
ctx.chat_panel.handle_ws_event({"type": "error", "message": f"Failed to list sessions: {exc}"})
return
lines = ["[b]Sessions[/b]"]
for s in sessions:
marker = "● " if s["id"] == ctx.session_id else " "
lines.append(f"{marker}{s['id'][:8]} {s.get('profile_id', 'unknown')} {s.get('title', '')}")
ctx.chat_panel.handle_ws_event({"type": "status", "content": "\n".join(lines)})
class SwitchCommand(BaseCommand):
meta = CommandMeta(
name="switch",
aliases=(),
description="Switch to another session by id or prefix.",
keybind=None,
)
async def execute(self, ctx: TuiContext, args: str) -> None:
target = args.strip()
if not target:
ctx.chat_panel.handle_ws_event({"type": "error", "message": "Usage: /switch <session_id_or_prefix>"})
return
try:
session = api.get_session(target)
except Exception:
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 Exception("no unique match")
except Exception as exc:
ctx.chat_panel.handle_ws_event({"type": "error", "message": f"Session not found: {target} ({exc})"})
return
ctx.session_id = session["id"]
ctx.profile_id = session.get("profile_id")
ctx.state.set_session_id(session["id"])
ctx.status_panel.set_session(session["id"])
ctx.status_panel.set_profile(ctx.profile_id or settings.default_profile_id)
ctx.chat_panel.handle_ws_event({"type": "status", "content": f"Switched to {session['id'][:8]}"})
await _reconnect_ws(ctx)
class ProfileCommand(BaseCommand):
meta = CommandMeta(
name="profile",
aliases=(),
description="Show current session profile.",
keybind=None,
)
async def execute(self, ctx: TuiContext, args: str) -> None:
if not ctx.session_id:
ctx.chat_panel.handle_ws_event({"type": "error", "message": "No active session"})
return
try:
session = api.get_session(ctx.session_id)
except Exception as exc:
ctx.chat_panel.handle_ws_event({"type": "error", "message": f"Failed to get session: {exc}"})
return
ctx.chat_panel.handle_ws_event(
{"type": "status", "content": f"Profile: {session.get('profile_id')}\nSession: {session['id']}"}
)
class QuitCommand(BaseCommand):
meta = CommandMeta(
name="quit",
aliases=("exit", "q"),
description="Exit Navi Code.",
keybind="ctrl+x q",
)
async def execute(self, ctx: TuiContext, args: str) -> None:
app = ctx.app()
app.exit()
class ThinkingCommand(BaseCommand):
meta = CommandMeta(
name="thinking",
aliases=(),
description="Toggle thinking block visibility.",
keybind=None,
)
async def execute(self, ctx: TuiContext, args: str) -> None:
settings.show_thinking = not settings.show_thinking
ctx.chat_panel.handle_ws_event(
{"type": "status", "content": f"Thinking blocks: {'on' if settings.show_thinking else 'off'}"}
)
class CompactCommand(BaseCommand):
meta = CommandMeta(
name="compact",
aliases=(),
description="Compact the current session (ask model to summarize context).",
keybind="ctrl+x c",
)
async def execute(self, ctx: TuiContext, args: str) -> None:
if ctx.ws_client:
ctx.ws_client.enqueue("Please summarize and compact our conversation so far.")
async def _reconnect_ws(ctx: TuiContext) -> None:
"""Close old WebSocket and open a new one for the current session."""
if ctx.ws_client:
await ctx.ws_client.close()
if not ctx.session_id:
return
from clients.terminal.tui.events import ConnectionStatusChanged
from clients.terminal.ws_client import NaviWebSocketClient
new_client = NaviWebSocketClient(ctx.session_id)
ctx.ws_client = new_client
try:
await new_client.connect()
app = ctx.app()
app.run_worker(new_client.receive_loop)
ctx.status_panel.set_connection(True, "")
app.post_message(ConnectionStatusChanged(True, ""))
except Exception as exc:
ctx.status_panel.set_connection(False, str(exc))
ctx.app().post_message(ConnectionStatusChanged(False, str(exc)))