Newer
Older
navi-1 / clients / terminal / tui / widgets / chat_panel.py
"""Chat panel widget for the TUI."""

from __future__ import annotations

from rich.console import Group
from textual.app import ComposeResult
from textual.containers import ScrollableContainer
from textual.widgets import Static

from clients.terminal.tui.chat_model import ChatModel
from clients.terminal.tui.renderers import default_registry


class ChatPanel(ScrollableContainer):
    """Scrollable conversation panel."""

    DEFAULT_CSS = """
    ChatPanel {
        border: solid $tui-border;
        background: $tui-surface;
        color: $tui-text;
        padding: 0 1;
        height: 1fr;
        width: 2fr;
    }
    ChatPanel Static {
        color: $tui-text;
    }
    """

    def __init__(self) -> None:
        super().__init__()
        self._model = ChatModel()
        self._registry = default_registry()
        self._items_container = Static("")

    def compose(self) -> ComposeResult:
        yield self._items_container

    def on_mount(self) -> None:
        self._items_container.styles.height = "auto"

    def add_user_message(self, text: str) -> None:
        self._model.add_user_message(text)
        self._refresh()

    def handle_ws_event(self, msg: dict) -> None:
        self._model.handle_ws_event(msg)
        self._refresh()

    def _refresh(self) -> None:
        renderables = []
        for item in self._model.items:
            if item.kind == "user_message":
                renderables.append(self._registry.render({"type": "user_message", "content": item.content}))
            elif item.kind == "assistant_message":
                renderables.append(self._registry.render({"type": "assistant_message", "content": item.content}))
            elif item.kind == "thinking_block":
                renderables.append(self._registry.render({"type": "thinking_block", "content": item.content}))
            elif item.kind == "tool_started":
                renderables.append(self._registry.render({"type": "tool_started", **item.meta}))
            elif item.kind == "tool_call":
                renderables.append(self._registry.render({"type": "tool_call", **item.meta}))
            elif item.kind == "error":
                renderables.append(self._registry.render({"type": "error", "message": item.content}))
            else:
                renderables.append(self._registry.render({"type": "plain", "content": item.content}))

        self._items_container.update(Group(*renderables))
        self.scroll_end(animate=False)