## Context Every planning step already computes a fused `Scene` via `perception/scene_builder.py::build_scene()`. Confirmed by reading the fusion logic: every UI-tree element passed in as `ui_elements` survives into `Scene.elements` with `source == "ui"` unchanged (whether or not it matched an OCR box — matching only overwrites `text`/`confidence`, never `source` or drops the element). Only OCR elements that never matched a UI-tree element are additionally kept, separately, in `Scene.ocr_elements`. `runtime/task.py::_append_timeline()` today only ever extracts `source == "ocr"` elements (via `scene.ocr_results_to_dict()`, with a fallback scan of `scene.elements`) into the Timeline. No equivalent extraction of `source == "ui"` elements exists, and `TimelineRecord` has no field to hold them even if extracted. `host_agent/web/app.py::_ui_tree_nodes()` was written against a different, incorrect assumption: that UI-tree data only exists when a step's tool call is `get_ui_tree`/`ui_tree`. That action has never existed in `runtime/tool_specs.py::ALL_TOOL_SPECS` (`tap`, `swipe`, `input_text`, `launch_app`, `terminate_app`, `finish_task` only) — so `_ui_tree_nodes()` returns `[]` for every real task. `task_detail.html` already has working Jinja2 markup for `step.ui_tree_nodes` (a `
` block, same shape as the OCR one), so once the data is wired through it needs no template rewrite. `tap`/`swipe` tool arguments (`runtime/tool_specs.py`) are specified in "Scene pixel coordinates" — the same coordinate space as `SceneElement.bounds` and `Scene.width`/`Scene.height`. This is what makes drawing both the perception boxes and the action-effect marker from the same coordinate space consistent. ## Goals / Non-Goals **Goals:** - Make UI-tree elements flow from the fused `Scene` into the Timeline and render as a list on the task-detail page, replacing dead code. - Let an operator toggle an overlay of OCR/UI-tree bounding boxes directly on the screenshot that scene data was actually captured from. - Visualize the actual spatial effect of `tap`/`swipe` actions on that same screenshot. - Do this without any new backend endpoint or additional persisted fields beyond one new Timeline field (`ui_tree_results`), since bounds/text/tool args are already computed and already serialized to the page today. **Non-Goals:** - Not changing `perception/scene_builder.py`'s fusion algorithm. - Not adding overlay/animation to the after-screenshot — the persisted `Scene` and its bounds describe the state the action was planned against (the before-screenshot), not the resulting state. Overlaying boxes on the after-image would misleadingly imply they describe post-action element positions. - Not persisting rendered overlay images; overlay/animation are computed client-side from data already in the page. - Not adding overlay/animation for non-spatial actions (`input_text`, `launch_app`, `terminate_app`, `finish_task`). ## Decisions ### D1: UI-tree evidence sourced from `Scene.elements` where `source == "ui"`, not a new Scene field **Decision**: `_append_timeline()` computes `ui_tree_results = [element.to_dict() for element in scene.elements if element.source == "ui"]` and passes it to `Timeline.append(..., ui_tree_results=ui_tree_results)`. `TimelineRecord` gains `ui_tree_results: list[dict[str, Any]] = field(default_factory=list)`, mirroring the existing `ocr_results` field exactly. **Why not add a raw `ui_elements` field to `Scene`**: `scene_builder.build_scene()` already preserves every UI-tree element in `Scene.elements` untouched aside from an OCR-provided `text`/`confidence` merge; filtering by `source` at the Timeline layer needs no change to the perception layer, and matches the existing pattern for how `ocr_results` gets a fallback scan of `scene.elements` by `source`. **Why not reuse the old `get_ui_tree`/`ui_tree` tool-name check**: There is no such tool; it was never callable. Removing the dead branch entirely rather than keeping it alongside the new path avoids two divergent, only one of which is reachable, code paths for the same concept. ### D2: `host_agent/web/app.py::_ui_tree_nodes()` reads the new field directly **Decision**: `_ui_tree_nodes(record)` becomes a direct, unconditional read of `record.get("ui_tree_results", [])` filtered to dicts — the same shape as the existing `_ocr_results(record)`. The tool-name filter is removed. **Why**: `task_detail.html` already renders `step.ui_tree_nodes` as a collapsible list identical in structure to `step.ocr_results`; no template change is needed once the data source is correct. ### D3: Overlay is rendered entirely client-side from data already in the page **Decision**: The task-detail page already embeds each step's OCR/UI-tree results and screenshot as inline data (base64 image `src`, and the per-step context dict rendered into the template). Add a `