diff --git a/src/cleveragents/tui/materializer.py b/src/cleveragents/tui/materializer.py new file mode 100644 index 000000000..e4973128e --- /dev/null +++ b/src/cleveragents/tui/materializer.py @@ -0,0 +1,862 @@ +"""TuiMaterializer -- bridges A2A event queue to Output Rendering Framework. + +The TuiMaterializer is a :class:`MaterializationStrategy` implementation that +maps ``OutputSession`` / ``ElementHandle`` events (created, updated, closed, +session-end) into live Textual widget operations for the CleverAgents TUI. + +It also subscribes to the :class:`A2aEventQueue` so that plan progress events +(TaskStatusUpdateEvent, TaskArtifactUpdateEvent) arrive in real time and are +routed to the appropriate TUI widgets. Producer code is completely unaware of +whether it is driving a CLI Rich terminal, a plain-text pipe, or a Textual +widget tree - it writes to handles identically in all cases. + +Widget mapping per ADR-044 :: + + +---------------------+----------------------------------+ + | ElementHandle Type | Textual Widget | + +---------------------+----------------------------------+ + | PanelHandle | Static container + Collapsible | + | TableHandle | DataTable | + | TreeHandle | Tree | + | ProgressHandle | ProgressBar / Throbber | + | StatusHandle | Label (semantic CSS class) | + | CodeHandle | Read-only TextArea | + | DiffHandle | Custom DiffView widget | + | SeparatorHandle | Rule | + | ActionHintHandle | Static (muted commands) | + +---------------------+----------------------------------+ + +Real-time A2A event subscription :: + + A2aEventQueue -> EventBusBridge -> TuiMaterializer widget routes + +The materialiser handles all ElementHandle types from day one - partial +implementation would cause silent failures when CLI commands run in a TUI +context (ADR-044 requirement). + +Based on the Output Rendering Framework specification and ADR-021 / ADR-044. +""" + +from __future__ import annotations + +import threading +import time +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from cleveragents.a2a.events import A2aEventQueue + from cleveragents.cli.output.handles._models import ElementSnapshot +else: + # When Textual is not installed we fall back to minimal stubs so the TUI can + # still be imported without pulling in the full framework. The materializer + # methods are no-op in that case but do not crash. + from textual.widgets import Static + + class ElementSnapshot: # type: ignore[no-redef] + pass + + +_A2A_AVAILABLE = isinstance(Static, type) + +from cleveragents.cli.output.handles._models import ( + ActionHint, + CodeBlock, + DiffBlock, + DiffHunk, + DiffLine, + ElementClosed, + ElementCreated, + ElementUpdated, + Panel, + ProgressIndicator, + Separator, + SessionEnd, + StatusMessage, + Table, + TextBlock, + Tree, +) + + +# --------------------------------------------------------------------------- +# Reactive element mapping -- each handle kind produces a TUI widget tree +# --------------------------------------------------------------------------- + + +class _TuiWidget: + """Lightweight descriptor tracking one TUI widget instance and its + associated element handle. + + The materializer maintains a dict of _TuiWidget instances keyed by + ``handle_id`` so it can dispatch incremental updates to the right widget + without reconstructing the entire tree on every event. + """ + + __slots__ = ("widget", "handle_id", "element_type", "declared_at", "closed") + + def __init__( + self, + widget: Any, + handle_id: str, + element_type: str, + declared_at: float, + ) -> None: + self.widget = widget + self.handle_id = handle_id + self.element_type = element_type + self.declared_at = declared_at + self.closed = False + + +# --------------------------------------------------------------------------- +# Reactive panel builder -- maps Panel/PanelEntry -> TUI widgets +# --------------------------------------------------------------------------- + + +def _build_panel_display(widget: Any, panel: Panel) -> None: + """Render a Panel into an existing TUI widget.""" + if not _A2A_AVAILABLE: # pragma: no cover - Textual fallback path + return + + entries_text: list[str] = [] + for entry in panel.entries: + icon_str = f"{entry.icon} " if entry.icon else "" + key_label = ( + f" {icon_str}{entry.key}: {entry.value}" + if entry.style_hint != "error" + else f" :material-check_circle: {icon_str}{entry.key}: {entry.value}" + ) + entries_text.append(key_label) + + rendered = f"[title]{panel.title}[/title]\n" + "\n".join(entries_text) + + if hasattr(widget, "label"): + widget.label = panel.title + if hasattr(widget, "_static"): + widget._static.update(rendered) + + +# --------------------------------------------------------------------------- +# Reactive table builder -- maps Table rows -> DataTable widgets +# --------------------------------------------------------------------------- + + +def _init_table_display(widget: Any, table: Table) -> None: + """Populate a DataTable widget headers and initial rows.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + + for col in table.columns: + try: + widget.add_column(col.name) + except Exception: + pass + + for row in table.rows: + try: + widget.add_row(*[str(v) for v in row.values()]) + except TypeError: + pass + + +def _add_table_row(widget: Any, row_dict: dict[str, Any]) -> None: + """Append a single row to the DataTable.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + try: + widget.add_row(*[str(v) for v in row_dict.values()]) + except TypeError: + pass + + +# --------------------------------------------------------------------------- +# Reactive tree builder -- maps Tree -> textual.widgets.Tree widgets +# --------------------------------------------------------------------------- + + +def _populate_tree(widget: Any, tree: Tree) -> None: + """Add a TreeNode hierarchy to a Textual Tree widget.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + + def _add_node(parent_label: str, children_list: list[Tree]) -> None: + if not children_list: + return + for child in children_list: + try: + node = widget.add_node( + parent_label, label=child.label, collapsed=child.collapsed + ) + _add_node(node, child.children) + except Exception: + pass + + if not hasattr(widget, "_added"): + _add_node("root", [tree.root]) + + +# --------------------------------------------------------------------------- +# Reactive progress indicator -- maps ProgressIndicator -> ProgressBar/Throbber +# --------------------------------------------------------------------------- + + +def _update_progress(widget: Any, status_msg: StatusMessage) -> None: + """Write a status message to the progress widget.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + try: + widget.update_label(status_msg.message) + except AttributeError: + pass + + +# --------------------------------------------------------------------------- +# Status message display -- maps StatusMessage -> Label with CSS class +# --------------------------------------------------------------------------- + + +def _display_status(widget: Any, status_message: StatusMessage) -> None: + """Render a StatusMessage as a styled label.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + + widget.update(status_message.message) + + css_classes = { + "ok": "status--ok", + "info": "status--info", + "warn": "status--warn", + "error": "status--error", + } + cls = css_classes.get(status_message.level, "") + if cls: + widget.add_class(cls) + + +# --------------------------------------------------------------------------- +# Code block display -- maps CodeBlock -> read-only TextArea +# --------------------------------------------------------------------------- + + +def _render_code(widget: Any, code_block: CodeBlock) -> None: + """Set the content of a read-only TextArea.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + + try: + widget.read_only = True + if hasattr(widget, "text"): + widget.text = code_block.content + elif hasattr(widget, "update"): + widget.update(code_block.content) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Diff display -- maps DiffBlock -> custom DiffView widgets +# --------------------------------------------------------------------------- + + +class _DiffViewStub: + """Stubs for the DiffView widget when Textual is not fully available.""" + + def __init__(self, file_a: str | None = None, file_b: str | None = None) -> None: + self.file_a = file_a + self.file_b = file_b + self._hunks: list[str] = [] + + def add_hunk(self, hunk: DiffHunk) -> None: + lines = [hunk.header] + for line in hunk.lines: + prefix = "+" if line.line_type == "add" else "-" if line.line_type == "remove" else " " + lines.append(f"{prefix} {line.content}") + self._hunks.append("\n".join(lines)) + + @property + def rendered_text(self) -> str: + return "\n\n".join(self._hunks) + + +def _render_diff(widget: Any, diff_block: DiffBlock) -> None: + """Render a DiffBlock into its Display widget.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + + for hunk in diff_block.hunks: + if hasattr(widget, "add_hunk"): + widget.add_hunk(hunk) + + +# --------------------------------------------------------------------------- +# Separator display -- maps Separator -> Rule widgets +# --------------------------------------------------------------------------- + + +def _render_separator(widget: Any, sep: Separator) -> None: + """Apply the rule style.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + rule_styles = {"line": "=", "blank": "", "double": "="} + char = rule_styles.get(sep.style, "-") + if hasattr(widget, "label"): + widget.label = char * 30 + + +# --------------------------------------------------------------------------- +# Action hint display -- maps ActionHint -> Static with muted commands +# --------------------------------------------------------------------------- + + +def _render_action_hint(widget: Any, hint: ActionHint) -> None: + """Render command suggestions in a muted format.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + cmd_text = ", ".join(f"`{cmd}`" for cmd in hint.commands[:5]) + prefix = f"{hint.description}: " if hint.description else "" + widget.update(f"{prefix}{cmd_text}") + + +# --------------------------------------------------------------------------- +# Text block display -- maps TextBlock -> Static text +# --------------------------------------------------------------------------- + + +def _render_text(widget: Any, text_block: TextBlock) -> None: + """Set the text content.""" + if not _A2A_AVAILABLE: # pragma: no cover + return + + leading = " " * text_block.indent if text_block.indent else "" + rendered = f"{leading}{text_block.content}" + try: + widget.update(rendered) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Reactive A2A event subscription helper +# --------------------------------------------------------------------------- + + +class _A2aEventSubscriber: + """Subscribes to an :class:`A2aEventQueue` and calls a callback on each + incoming :class:`A2aEvent`. + + Usage:: + + subscriber = _A2aEventSubscriber(event_queue) + subscriber.subscribe(lambda event: materializer._handle_a2a_event(event)) + """ + + def __init__(self, event_queue: Any) -> None: + self._event_queue = event_queue + self._sub_id: str | None = None + + def subscribe(self, callback: Any) -> str | None: + """Register callback with the event queue. Returns subscription ID.""" + if self._event_queue is None: + return None + self._sub_id = self._event_queue.subscribe_local(callback) + return self._sub_id + + def unsubscribe(self) -> None: + """Remove our subscription.""" + if self._event_queue is not None and self._sub_id is not None: + try: + self._event_queue.unsubscribe(self._sub_id) + except Exception: # pylint: disable=broad-except + pass + self._sub_id = None + + +# ============================================================================ +# TuiMaterializer -- the main class +# ============================================================================ + + +class TuiMaterializer: + """OutputRendering framework materialization strategy for the Textual TUI. + + The TuiMaterializer implements :class:`MaterializationStrategy` protocol and + bridges ``ElementCreated``, ``ElementUpdated``, ``ElementClosed``, and + ``SessionEnd`` events from an ``OutputSession`` into live Textual widget + operations. + + It also supports direct A2A event subscription for real-time plan progress + visibility. + + Thread Safety + ------------- + Internal state is protected by ``_lock``. The public event-handling + methods acquire the lock to prevent race conditions between concurrent + producers writing to the same handle. + + Parameters + ---------- + event_queue: + An :class:`A2aEventQueue` instance for real-time A2A event + subscription. When None the materializer functions as a pure + ``MaterializationStrategy`` with no A2A integration. + callback_registry: + Optional list of callbacks invoked when session begins / ends. + """ + + strategy_name: str = "tui" + supports_incremental_updates: bool = True + + # Mapping from element_kind strings to widget builder and updater names. + _KIND_WIDGET_MAP: dict[str, tuple[str, str | None]] = { + "panel": ("_build_panel", "_update_panel"), + "table": ("_build_table", "_add_table_row"), + "tree": ("_build_tree", None), + "status": ("_build_status", None), + "progress": ("_build_progress", "_update_progress"), + "code": ("_build_code_block", "_render_code"), + "text": ("_build_text_block", None), + "diff": ("_build_diff", None), + "separator": ("_build_separator", None), + "action_hint": ("_build_action_hint", None), + } + + def __init__( + self, + event_queue: Any | None = None, + callback_registry: list[Any] | None = None, + ) -> None: + self._event_queue = event_queue + self._callbacks = callback_registry or [] + # Widget registry -- handle_id -> _TuiWidget. + self._widgets: dict[str, _TuiWidget] = {} + # Order-preserving index to handle_id mapping. + self._index_map: dict[int, str] = {} + self._next_index: int = 0 + # Thread safety lock. + self._lock = threading.Lock() + # Reactive A2A event subscriber (lazy). + self._a2a_subscriber: _A2aEventSubscriber | None = None + + def on_session_begin(self, session: Any) -> None: + """Called when a new OutputSession begins. + + Registers the A2A event subscriber if an event queue was provided. + Invokes all registered callbacks. + """ + with self._lock: + if self._event_queue is not None and self._a2a_subscriber is None: + self._a2a_subscriber = _A2aEventSubscriber(self._event_queue) + + def _on_a2a_event(event: Any) -> None: + """Callback invoked on each A2A event from the queue.""" + self._handle_a2a_event(event) + + self._a2a_subscriber.subscribe(_on_a2a_event) + + # Fire begin callbacks. + for cb in (self._callbacks or []): + try: + if callable(cb): + cb("session_begin", session) + except Exception: # pylint: disable=broad-except + pass + + def on_element_created(self, event: ElementCreated) -> None: + """Handle an element created event. + + Creates the corresponding Textual widget and registers it in the + widget dict keyed by handle_id. + """ + with self._lock: + kind = event.element_kind + handle_id = event.handle_id + + idx = getattr(event, "declaration_index", 0) + self._index_map[idx] = handle_id + self._next_index += 1 + + widget = self._make_widget(kind, event.initial_state) + tui_widget = _TuiWidget( + widget=widget, + handle_id=handle_id, + element_type=kind, + declared_at=time.monotonic(), + ) + self._widgets[handle_id] = tui_widget + + # Fire element-created callbacks. + for cb in (self._callbacks or []): + try: + if callable(cb): + cb("element_created", event) + except Exception: # pylint: disable=broad-except + pass + + def on_element_updated(self, event: ElementUpdated) -> None: + """Handle an incremental element update. + + Looks up the existing widget by handle_id and applies the update + using a kind-specific updater function. + """ + with self._lock: + widget_tui = self._widgets.get(event.handle_id) + if widget_tui is None or widget_tui.closed: + return + + kind = event.element_kind + snapshot = getattr(event, "element_snapshot", None) + if snapshot is None: + return + + self._apply_update(widget_tui.widget, kind, snapshot) + + def on_element_closed(self, event: ElementClosed) -> None: + """Handle element closing. + + Marks the associated widget as closed and fires a close callback. + The widget is retained in the registry for post-close inspection. + """ + with self._lock: + widget_tui = self._widgets.get(event.handle_id) + if widget_tui is None: + return + + widget_tui.closed = True + + # Fire close callbacks. + for cb in (self._callbacks or []): + try: + if callable(cb): + cb("element_closed", event) + except Exception: # pylint: disable=broad-except + pass + + def on_session_end(self, event: SessionEnd) -> None: + """Handle session end. + + Unsubscribes from A2A events and invokes all session-end callbacks. + """ + with self._lock: + if self._a2a_subscriber is not None: + self._a2a_subscriber.unsubscribe() + self._a2a_subscriber = None + + for cb in (self._callbacks or []): + try: + if callable(cb): + cb("session_end", event) + except Exception: # pylint: disable=broad-except + pass + + # ------------------------------------------------------------------ + # Internal widget factory and updater dispatch + # ------------------------------------------------------------------ + + def _make_widget(self, kind: str, element: Any = None) -> Any: + """Create a new Textual widget for the given element kind.""" + factory_name = self._KIND_WIDGET_MAP.get(kind, ("", ""))[0] + builder = getattr(self, factory_name, None) + if builder is not None and element is not None: + return builder(element) + return type("TuiWidget", (), {"_kind": kind, "label": ""}) + + # -- Element kind-specific builders --- + + def _build_panel(self, panel: Panel) -> Any: + try: + from textual.widgets import Static as TwStatic + + widget = TwStatic(panel.title) + _build_panel_display(widget, panel) + return widget + except ImportError: # pragma: no cover + return type("FallbackPanel", (), {"label": panel.title}) + + def _build_table(self, table: Table) -> Any: + try: + from textual.widgets import DataTable + + widget = DataTable(title=table.title or "") + _init_table_display(widget, table) + return widget + except ImportError: # pragma: no cover + return type("FallbackTable", (), {"title": str(table.title)}) + + def _build_tree(self, tree: Tree) -> Any: + try: + from textual.widgets import Tree as TwTree + + widget = TwTree(f"[b]{tree.root.label}[/b]") + _populate_tree(widget, tree) + return widget + except ImportError as exc: # pylint: disable=broad-exception-caught + return type("FallbackTree", (), {"root_label": tree.root.label}) + + def _build_status(self, status_msg: StatusMessage) -> Any: + try: + from textual.widgets import Label as TwLabel + + widget = TwLabel(status_msg.message) + if status_msg.level == "error": + widget.add_class("tui-status--red") + elif status_msg.level == "warn": + widget.add_class("tui-status--yellow") + elif status_msg.level == "ok": + widget.add_class("tui-status--green") + else: + widget.add_class("tui-status--info") + return widget + except ImportError: # pragma: no cover + return type("FallbackStatus", (), {"message": status_msg.message}) + + def _build_progress(self, progress: ProgressIndicator) -> Any: + try: + from textual.widgets import Throbber as TwThrobber + from textual.widgets import ProgressBar as TwProgressBar + + if progress.indeterminate or progress.total is None: + widget = TwThrobber(start=False) + else: + widget = TwProgressBar(total=progress.total, finished="idle") + return widget + except ImportError: # pragma: no cover + return type("FallbackProgress", (), {"label": progress.label}) + + def _build_code_block(self, code: CodeBlock) -> Any: + try: + from textual.widgets import TextArea as TwTextArea + + widget = TwTextArea(code.content, read_only=True) + return widget + except ImportError: # pragma: no cover + return type("FallbackCode", (), {"_text": code.content}) + + def _build_text_block(self, text: TextBlock) -> Any: + try: + from textual.widgets import Static as TwStatic + + widget = TwStatic(text.content) + return widget + except ImportError: # pragma: no cover + return type("FallbackText", (), {"_text": text.content}) + + def _build_diff(self, diff: DiffBlock) -> Any: + try: + widget = _DiffViewStub(diff.file_a, diff.file_b) + for hunk in diff.hunks: + widget.add_hunk(hunk) + return widget + except ImportError: # pragma: no cover + return _DiffViewStub(diff.file_a, diff.file_b) + + def _build_separator(self, sep: Separator) -> Any: + try: + from textual.widgets import Rule as TwRule + + widget = TwRule() + return widget + except ImportError: # pragma: no cover + return type("FbSep", (), {"_text": "\u2500" * 30}) + + def _build_action_hint(self, hint: ActionHint) -> Any: + try: + from textual.widgets import Static as TwStatic + + widget = TwStatic(f"[em]{', '.join(hint.commands)}[/]") + return widget + except ImportError: # pragma: no cover + return type("FbHint", (), {}) + + def _apply_update(self, widget: Any, kind: str, snapshot: Any) -> None: + """Dispatch an incremental update to the correct renderer function.""" + + updater_map: dict[str, Any] = { + "panel": lambda w, s: (_build_panel_display(w, s) if isinstance(s, Panel) else None), + "table": self._apply_table_update, + "tree": self._apply_tree_update, + "status": _display_status, + "progress": self._apply_progress_update, + "code": _render_code, + "text": _render_text, + "diff": _render_diff, + "separator": _render_separator, + "action_hint": _render_action_hint, + } + + fn = updater_map.get(kind) + if fn is not None: + try: + fn(widget, snapshot) # type: ignore[arg-type] + except Exception: # pylint: disable=broad-except + pass + + def _apply_panel_update(self, widget: Any, snapshot: Any) -> None: + if isinstance(snapshot, Panel): + _build_panel_display(widget, snapshot) + + def _apply_table_update(self, widget: Any, snapshot: Any) -> None: + if isinstance(snapshot, Table): + for row in snapshot.rows: + _add_table_row(widget, row) + + def _apply_tree_update(self, widget: Any, snapshot: Any) -> None: + if isinstance(snapshot, Tree) and snapshot.root.children: + _populate_tree(widget, snapshot) + + def _apply_progress_update(self, widget: Any, snapshot: Any) -> None: + if isinstance(snapshot, StatusMessage): + _update_progress(widget, snapshot) + + # ------------------------------------------------------------------ + # A2A event handling -- real-time plan progress to TUI widgets + # ------------------------------------------------------------------ + + def _handle_a2a_event(self, event: Any) -> None: + """Route an incoming A2A event to the appropriate TUI widget. + + This method is invoked as a callback on every A2aEvent published to + the subscribed A2aEventQueue. + """ + with self._lock: + event_type = getattr(event, "event_type", "") + data = getattr(event, "data", None) or {} + + if event_type == "TaskStatusUpdateEvent": + self._render_status_event(data) + elif event_type == "TaskArtifactUpdateEvent": + self._render_artifact_event(data) + + def _render_status_event(self, data: dict[str, Any]) -> None: + """Render a status update event into the TUI's conversation widget.""" + phase = str(data.get("phase", "") or "") + state = str(data.get("state", "") or "") + plan_id = str(data.get("plan_id", "") or "") + + clean_plan = plan_id.replace("-", "") + for handle_id, tui_widget in self._widgets.items(): + if tui_widget.element_type == "progress": + if not clean_plan or clean_plan in handle_id.replace("-", ""): + try: + sm = StatusMessage(message=f"Phase: phase={phase}, state={state}") + _update_progress(tui_widget.widget, sm) + except Exception: # pylint: disable=broad-except + pass + + def _render_artifact_event(self, data: dict[str, Any]) -> None: + """Render an artifact update event (code, diffs, status).""" + kind = str(data.get("artifact_type", "") or "") + artifact_data = data.get("data", data) + + if kind == "code" or data.get("type") == "code": + content = str(artifact_data.get("content", "")) + for handle_id, tui_widget in self._widgets.items(): + if tui_widget.element_type == "code": + try: + widget = tui_widget.widget + if hasattr(widget, "text"): + widget.text = content + except Exception: # pylint: disable=broad-except + pass + + elif kind == "diff" or data.get("type") == "diff": + hunks = artifact_data.get("hunks", []) + for handle_id, tui_widget in self._widgets.items(): + if tui_widget.element_type == "diff": + try: + widget = tui_widget.widget + for hunk in hunks: + if hasattr(widget, "add_hunk"): + dh = DiffHunk(header=str(hunk.get("header", ""))) + for ln in hunk.get("lines", []): + dh.lines.append( + DiffLine(line_type=ln.get("type", "context"), content=ln.get("content", "")) # type: ignore[arg-type] + ) + widget.add_hunk(dh) + except Exception: # pylint: disable=broad-except + pass + + elif kind == "status" or data.get("type") == "status": + status_msg = StatusMessage( + message=str(artifact_data.get("message", "")), + level=str(artifact_data.get("level", "info")), + detail=str(artifact_data.get("detail", "")) or None, + ) + for handle_id, tui_widget in self._widgets.items(): + if tui_widget.element_type == "status": + try: + _display_status(tui_widget.widget, status_msg) + except Exception: # pylint: disable=broad-except + pass + + def get_widget_registry(self) -> dict[str, _TuiWidget]: + """Return a copy of the widget registry for inspection.""" + with self._lock: + return dict(self._widgets) + + def clear(self) -> None: + """Clear all registered widgets and flush state. + + Used when the TUI switches sessions or closes a plan. + """ + with self._lock: + if self._a2a_subscriber is not None: + self._a2a_subscriber.unsubscribe() + self._a2a_subscriber = None + + self._widgets.clear() + self._index_map.clear() + self._next_index = 0 + + +# ============================================================================ +# Convenience factory +# ============================================================================ + + +def create_tui_materializer( + event_queue: Any | None = None, + callbacks: list[Any] | None = None, +) -> TuiMaterializer: + """Factory function to create a TuiMaterializer with standard wiring. + + Parameters + ---------- + event_queue: + The shared ``A2aEventQueue`` for A2A protocol events. When None the + materializer still works as a pure MaterializationStrategy. + callbacks: + Optional callback list for session-level hooks. + + Returns + ------- + TuiMaterializer + An initialized materializer ready to receive OutputSession events. + """ + return TuiMaterializer( + event_queue=event_queue, + callback_registry=callbacks, + ) + + +# ============================================================================ +# Public exports +# ============================================================================ + + +__all__ = [ + "TuiMaterializer", + "create_tui_materializer", + "_DiffViewStub", + "_A2aEventSubscriber", + "_build_panel_display", + "_init_table_display", + "_add_table_row", + "_populate_tree", + "_display_status", + "_render_code", + "_render_diff", + "_render_separator", + "_render_action_hint", + "_render_text", +]