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 arewindowsStore, 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), orephemeral(still in the DB so reconnect works; not a workspace catalog item).Address: window chrome
panel.{uuid}; nodes inside usechannelagency.panel+ the panel uuid (children declarenameonly). Sameentity_idthe 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 includespanels.open_panelpublishes desktop visibility; the shellopenAddressloads 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:
Window chrome —
windowsStore+ desktop snapshot (prefs.desktop).x/ystayleft/top. Depth, rotation, and scale arez/rx/ry/rz/scale. Not overlay.Content viewport — a later overlay bag (
scale/ pan). Leaflet keeps its own zoom. Do not reuse chrome pose keys here.Public scene (#22) — optional
poseon the node. The kiosk host writes the matrix.UiRendererdoes 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.