UI Schema

Declarative JSON tree produced by Django / host apps and consumed by agency-ui UiRenderer. Types → components: Frontend components (agency-ui).

Envelope

{
  "version": "1.0",
  "root": {
    "type": "desktop",
    "id": "home-desktop",
    "children": [ … ]
  }
}

Every node:

{ "type": "…", "id": "optional", "channel": "…", "uuid": "…", "name": "…",
  "props": { }, "children": [ ], "action": {…} }

id is chrome (window store). The address space is channel + uuid (+ optional name). TypeScript: agency-ui/src/schema/ui.ts and address.ts. The type field is a closed union (SCHEMA_TYPES); isValidNode rejects unknown names (including Vuetify). A new type is ui.ts + SCHEMA_TYPES + registry + Django KNOWN_UI_TYPES + Sphinx. Python: agency.ui.address, agency.ui.schema.

Address space

Like Home Assistant. A domain instance is channel + uuid:

{
  "type": "agency-mission",
  "channel": "agency.mission",
  "uuid": "00000000-0000-4000-8000-000000000001",
  "children": [
    { "type": "agency-button", "name": "execute", "props": { "label": "Execute" } }
  ]
}

entity_id is derived, never written as a composed path on the child:

  • parent → agency.mission.{uuid}

  • named control → agency.mission.{uuid}.execute

UiRenderer injects the prefix. Built-ins read it; they do not invent ids or compose REST / socket URLs from the tree. The Agency Bus topic is the entity_id. Reconnect rehydrates the document over REST — the WebSocket is not the source of truth.

Live overlay

Two layers, one mutation owner each:

  • Schema (shape): persisted document — type, children, label, icon.

  • Overlay (live): entity_id → attrs (disabled, hidden, label, value). Closed set; window chrome (x / y / open) and pose are windowsStore, not here.

A state envelope on the entity topic carries { "attrs": { "disabled": true } }. The overlay projector patches the map; it does not call replaceDocument / updateNode. Built-ins paint schema.props + overlay.

Reconnect: GET /api/ui/overlay/ (and bootstrap overlay) replaceAll. An attr that existed only on the socket evaporates — that is the test that it did not belong only on the WS. Django agency.ui.overlay.live_overlay is the place to derive attrs from domain facts (a running execution disables execute). Until a producer fills it, the snapshot is empty.

Bot panels

A bot composes a screen from the registered palette and persists a UIDocument (Django UiPanel). It does not send Vue, invent a type, or upload JavaScript.

  • Scope: session (owner), workspace (active workspace), or ephemeral (still in the DB so reconnect works; not a workspace catalog item).

  • Address: window chrome panel.{uuid}; nodes inside use channel agency.panel + the panel uuid (children declare name only). Same entity_id the operator and the bot talk to.

  • Write path validates with is_valid_node / KNOWN_UI_TYPES.

  • REST: GET/POST /api/ui/panels/, GET/PATCH /api/ui/panels/<uuid>/. Bootstrap includes panels. open_panel publishes desktop visibility; the shell openAddress loads the document from REST.

  • Tools: create_panel, patch_panel, open_panel.

Do not address nodes by children index. Do not put a composed channel on a child (that reintroduces a Vue↔path map).

Pinned fixture: agency-ui/src/fixtures/address-space.json (same bytes as agency-django/src/agency/data/address_space.json).

Important contracts

Split — props.direction is semantic:

  • horizontal → panes side-by-side (left/right)

  • vertical → panes stacked (top/bottom)

Window — type: window with title, icon, width, height, x, y, chrome flags (noResize, noFull, noMax, …). Maximize is on by default; noFull or noMax hides the button.

Pose

Structured transform. Numbers only — Vue writes one CSS transform. Never a rotateY(...) string on the wire.

{ "x": 0, "y": 0, "z": -80, "rx": 0, "ry": 12, "rz": 0, "scale": 1 }

Three mutation owners:

  1. Window chrome — windowsStore + desktop snapshot (prefs.desktop). x / y stay left / top. Depth, rotation, and scale are z / rx / ry / rz / scale. Not overlay.

  2. Content viewport — a later overlay bag (scale / pan). Leaflet keeps its own zoom. Do not reuse chrome pose keys here.

  3. Public scene (#22) — optional pose on the node. The kiosk host writes the matrix. UiRenderer does not wrap every semantic node in 3D.

DSM projection is desktop.projection: flat (today’s MDI) or perspective (stage). The toggle is in the drawer chrome. Open windows tilt toward the vanishing point from the window center; the focused window stays almost frontal and comes forward. Minimize in 2D shelves the window into a top-right quarter-width column (the stack scales if it does not fit; click to restore). On the 3D stage, minimize is a bottom carousel. Window body is transform-style: flat so grid / xterm / Leaflet still hit-test. The operator owns an FPS helm: triple glass, seated eye {0, -400, 72}, Space mouselook (no RMB, no pistol, no orbit). WASD walks; wheel is head height; Ctrl crouches from the current Y. Sit / Home resets the camera. The bot does not move it. Windows hop between panes via screen. See agency-ui#26.

Python: agency.ui.pose.parse_pose. TypeScript: agency-ui/src/schema/pose.ts.

Explorers — interactive CRUD surfaces (*-explorer): list left, detail right; mutate via REST; live rows via observe (Python: serving the SPA).

System — typically a log-stream node inside a System window. The Info tab is three sibling collapsibles: Versions, Inference, System.

Singletons — schema windows with a stable address, same as system: media (operator player, agency-media), map (operator chart). Veronica opens them with open_window, not a kiosk assign.

Schema events (ui.*)

The user WebSocket (/channel/) can emit schema mutations (ui.update, ui.append, …) and Agency Bus envelopes. open_window publishes desktop.*.visibility.changed (handled by the desktop projector + openAddress), not ui.open_window. Schema ui.* is still handled by uiStore — separate from domain message / monitor_event traffic.

See agency-ui/src/schema/ui.ts and renderer/registry.ts.

Legacy / ops logs

Standalone log stream outside the SPA: /logs/ (SSE via /stream/logs/, session required). System → Logs uses the same ops feed: errors, warnings, tracebacks, process restarts, GPU updates. It tails wsgi/asgi/work/beat system.log / app.log plus gunicorn — not docker/nginx/Ollama stdout.