Newer
Older
navi-1 / clients / terminal / tui / renderers / base.py
"""Base class for content renderers used in the chat panel."""

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from rich.console import RenderableType


def _strip_panel(renderable: "RenderableType") -> "RenderableType":
    """Drop the bubble chrome for reading-mode (plain) rendering.

    Most chat items render a rich ``Panel`` (rounded border + title) or a
    ``Padding(Panel, ...)`` for sub-agent indent. In reading mode the goal is to
    show just the content so a raw terminal ``Shift+drag`` + ``Ctrl+Shift+C``
    copies clean text without the ``│``/``╭─╮`` borders — so unwrap the Panel (or
    padded Panel) to its body. Renderables that are already borderless (plain
    ``Text``/``Group``/status lines) pass through unchanged.
    """
    from rich.panel import Panel
    from rich.padding import Padding

    if isinstance(renderable, Panel):
        return renderable.renderable
    if isinstance(renderable, Padding):
        inner = renderable.renderable
        if isinstance(inner, Panel):
            return inner.renderable
        return inner
    return renderable


class ContentRenderer(ABC):
    """Render a single WebSocket event or chat message into a Rich renderable."""

    @abstractmethod
    def accepts(self, msg: dict) -> bool:
        """Return True if this renderer can handle the event/message."""
        raise NotImplementedError

    @abstractmethod
    def render(self, msg: dict) -> "RenderableType":
        """Return a Rich renderable."""
        raise NotImplementedError

    def render_plain(self, msg: dict) -> "RenderableType":
        """Return a borderless renderable for reading mode.

        The default unwraps the Panel/Padding that :meth:`render` produces so the
        body shows without bubble chrome. Renderers whose body is itself a styled
        markdown renderable (assistant answers, finalized plans) override this to
        return the raw markdown text instead — otherwise rich ``Markdown`` would
        re-wrap fenced code blocks in their own Panels and the borders would leak
        back into a raw-terminal copy.
        """
        return _strip_panel(self.render(msg))