"""Renders a job's trace events into an interactive HTML timeline. Events are simple dicts (see local_runner.py for the exact shapes emitted): {"ts": "...", "kind": "tool_call" | "tool_result" | "note" | "task_update" | "status", "label": "short one-liner", "detail": "optional longer text", "ok": true/false/None} Rendered with plain
/ so expand/collapse works with no JS, which keeps it safe to drop straight into a Gradio gr.HTML component. """ from __future__ import annotations import html as _html _ICONS = { "tool_call": "\U0001F527", # wrench "tool_result_ok": "✅", "tool_result_fail": "❌", "note": "\U0001F4AC", "task_update": "\U0001F4CB", "status_start": "▶️", "status_end": "\U0001F3C1", } _STYLE = """ """ def _icon_for(event: dict) -> str: kind = event.get("kind") if kind == "tool_result": return _ICONS["tool_result_ok"] if event.get("ok") else _ICONS["tool_result_fail"] if kind == "status": return _ICONS["status_end"] if event.get("ok") is not None else _ICONS["status_start"] return _ICONS.get(kind, "•") def _row_class(event: dict) -> str: if event.get("ok") is True: return "ok" if event.get("ok") is False: return "fail" return "" def render_trace(events: list[dict], running: bool = False) -> str: if not events: empty_msg = "Waiting for the local runner to pick this job up..." if running else "No trace recorded yet." return f'{_STYLE}
{_html.escape(empty_msg)}
' items = [] for event in events: icon = _icon_for(event) label = _html.escape(str(event.get("label", ""))) ts = _html.escape(str(event.get("ts", ""))) detail = event.get("detail") row_class = _row_class(event) detail_html = "" if detail: detail_html = f'
{_html.escape(str(detail))}
' items.append( f'
  • ' f'{icon}' f'{label}' f'{ts}' f"{detail_html}
  • " ) live_banner = '
    ⏳ Run in progress — refreshing automatically...
    ' if running else "" return f'{_STYLE}
    {live_banner}
      {"".join(items)}
    '