LavaUI agent control plane
Project map for coding agents (architecture, build, where to edit): see AGENTS.md at the repo root. This file is only the runtime agent TCP/MCP control plane inside a running LavaUI app.
An optional localhost TCP server lets an external agent (or CLI / MCP wrapper) inspect layout, capture screenshots, and inject input without OS-level accessibility hooks.
Enable
export LAVA_AGENT_PORT=9876 # any port > 0; unset = off
export CANVAS_VK_VALIDATION=1 # optional Vulkan validation layers
# Optional override for engine shaders (default: CanvasResources SPM bundle):
# export CANVAS_ASSETS_ROOT=...
swift run HelloWorld
# stderr: AgentServer: listening on 127.0.0.1:9876
Coordinates are layout / framebuffer pixels (top-left origin), matching Yoga and hit-testing. With the demo’s menu height of 0 they are also window pixels.
Captured drags use independent button phases with any number of moves between them:
python3 tools/lava_agent_cli.py pointer_down --x 300 --y 450
python3 tools/lava_agent_cli.py move --x 600 --y 450
python3 tools/lava_agent_cli.py move --x 900 --y 450
python3 tools/lava_agent_cli.py pointer_up --x 900 --y 450
pointer_down and pointer_up also accept --sid, --label, --id, or
--query and use the resolved node center, like click. The MCP server exposes
the same two operations.
Stable ids (sid)
Process-local NodeID (id in the tree) changes every launch. Agents could
use sid:
- Explicit —
.agentId("theme-toggle")on a view (preferred). - Structural path — e.g.
0:VStack/0:HStack/3:Text. ForEach rows usek:<element-id>so reordering does not rename other rows.
Layout nodes may also report agent_id (when tagged) and path (structural
path when an explicit id is set).
Text("[ Theme: Dark ]", onClick: { … })
.agentId("theme-toggle")
Per-widget paint profiling
export LAVAUI_PROFILE=1 # in addition to the env above
swift run HelloWorld
With this set, the profile command (also tools/lava_agent_cli.py profile)
settles a frame and returns the most recent frame's paint cost per widget —
[{"label", "ms", "count"}], worst first, label being a view's .agentId
if it has one, else its structural kind ("Canvas", "EditorView", …).
Timed per widget's whole paint, not per draw primitive, so it points at
which widget is expensive without the timing calls themselves swamping the
cost they're measuring. LAVAUI_DEBUG (off by default) also prints the top 5
each frame on stdout when this is set, prefixed top:.