"""UI Node: pure renderer — converts ThoughtResult actions + data into controls.""" import json import logging import re from .base import Node from ..types import ThoughtResult log = logging.getLogger("runtime") class UINode(Node): name = "ui" # No model — pure code, no LLM calls def __init__(self, send_hud): super().__init__(send_hud) self.current_controls: list[dict] = [] def _extract_table(self, tool_output: str) -> dict | None: """Try to parse tabular data from tool output.""" if not tool_output: return None lines = [l.strip() for l in tool_output.strip().split("\n") if l.strip()] if len(lines) < 2: return None # Detect pipe-separated tables (e.g. "col1 | col2\nval1 | val2") if " | " in lines[0]: columns = [c.strip() for c in lines[0].split(" | ")] data = [] for line in lines[1:]: if line.startswith("-") or line.startswith("="): continue # separator line vals = [v.strip() for v in line.split(" | ")] if len(vals) == len(columns): data.append(dict(zip(columns, vals))) if data: return {"type": "table", "columns": columns, "data": data} # Detect "Table: X" header format from sqlite wrapper if lines[0].startswith("Table:"): table_name = lines[0].replace("Table:", "").strip() if len(lines) >= 2 and " | " in lines[1]: columns = [c.strip() for c in lines[1].split(" | ")] data = [] for line in lines[2:]: vals = [v.strip() for v in line.split(" | ")] if len(vals) == len(columns): data.append(dict(zip(columns, vals))) if data: return {"type": "table", "columns": columns, "data": data} return None async def process(self, thought: ThoughtResult, history: list[dict], memory_context: str = "") -> list[dict]: controls = [] # 1. Render actions from Thinker as buttons for action in thought.actions: controls.append({ "type": "button", "label": action.get("label", "Action"), "action": action.get("action", "unknown"), "payload": action.get("payload", {}), }) # 2. Extract tables from tool output if thought.tool_output: table = self._extract_table(thought.tool_output) if table: controls.append(table) # 3. Add labels for key tool results (single-value outputs) if thought.tool_used and thought.tool_output and not any(c["type"] == "table" for c in controls): output = thought.tool_output.strip() # Short single-line output → label if "\n" not in output and len(output) < 100: controls.append({ "type": "label", "id": "tool_result", "text": thought.tool_used, "value": output, }) if controls: self.current_controls = controls await self.hud("controls", controls=controls) log.info(f"[ui] emitting {len(controls)} controls") else: if self.current_controls: # Keep previous controls visible controls = self.current_controls await self.hud("decided", instruction="no new controls") return controls