"""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))