Iterating on the Voltius UI
Overview
Modify the app, then look at it and interact with it in the live dev build via the
tauri-docker MCP. Don't claim a UI change works because it compiled — drive it and read
the screenshot. A blank frame means it failed to launch.
The stack is compose.headless.yml: tauri-headless (debug app + Xvfb + tauri-driver on
4444, driven by the MCP) and ssh-host-1 (throwaway SSH host: user voltius / pass
voltius, port 2222, reachable by name on the shared network).
Bring-up (do this FIRST — the MCP fails to connect if the container is down)
docker compose -f compose.headless.yml up -d
docker compose -f compose.headless.yml logs tauri-headless # wait for the driver
Ready signals: log shows Joined session keyring, and claude mcp list shows
tauri-docker connected. First build from a cold cache takes minutes; a warm cache
(the voltius-target volume at /target) finishes in seconds. If the MCP is registered but failing,
the cause is almost always that the container isn't up yet — bring it up and retry.
Register the MCP if absent:
claude mcp add tauri-docker -- docker exec -i tauri-headless npx -y github:VoltiusApp/mcp-tauri-automation
Loop
launch_appappPath=/target/debug/voltius(checkget_app_statefirst).- Interact:
click_element,type_text(clear:trueto overwrite;\nsends Enter),press_key(Enter, arrows, chords like["Control","l"]),wait_for_element. capture_screenshotwithreturnBase64:false→ saves to/app/screenshots/<name>.png→ Read the host path./screenshots/<name>.pngto actually look at it.
The MCP is yours — extend it, don't work around it
tauri-docker runs our own MCP: VoltiusApp/mcp-tauri-automation, checked out at
../mcp-tauri-automation. It is not a fixed constraint. When a tool is missing or too
limited (e.g. type_text couldn't send Enter or Ctrl-keys) and you catch yourself
building a fragile workaround, stop and fix the MCP:
- Edit
../mcp-tauri-automation/src;npm install && npm run buildto typecheck. - Commit and push to
origin/main(the VoltiusApp fork) — that's what the container'snpxpulls on the next fresh session. - To use the new capability in this session: a
claude mcpadd/reload does NOT surface new or changed tools mid-session — the tool registry is fixed at startup. Insteaddocker cpthe built dir into the container and drive the builtTauriDriverdirectly from a throwawaynodescript (docker exec tauri-headless node x.mjs), or drivetauri-driverdirectly over WebDriver. - Resume the paused work.
Don't grep src-tauri for a backend command to fake a keystroke — improve the tool.
Selectors (the #1 time-sink — read source, don't guess)
click_element / type_text take CSS selectors only — no :contains, no XPath.
- Grep the component before guessing a selector.
- Prefer stable hooks already in the code:
[data-host-card="true"],button[title="…"],input[placeholder="…"]. - Tailwind v4 arbitrary classes contain literal parens. Match by substring to skip
escaping:
button[class*="bg-(--t-bg-elevated)"][class*="text-(--t-accent)"]. - Context-menu / dropdown items have no ids → positional. The menu portal is
z-100; the Nth item is[class*="z-100"] > div > div:nth-child(N) > button. button:"right"onclick_elementopens the context menu.
After editing code
- Frontend (
.tsx/.css): hot-reloads via Vite, no rebuild. Just re-screenshot. Confirm:docker exec tauri-headless tail /tmp/vite.logshowshmr update …. - Rust (
src-tauri/):docker exec tauri-headless cargo build --manifest-path src-tauri/Cargo.toml(debug + mold), thenclose_app+launch_app.
Verifying non-visual effects
A toast firing proves the handler ran; confirm the actual effect too.
- App's own UI: the notification bell (
button[title="Notifications"]) keeps a history. - Clipboard (needs the app's X auth, not just
DISPLAY):pid=$(docker exec tauri-headless pgrep -f target/debug/voltius | head -1) docker exec tauri-headless sh -c "export \$(tr '\0' '\n' </proc/$pid/environ | grep -E '^(DISPLAY|XAUTHORITY)='); xclip -o -selection clipboard" - SSH host reachability: the host card shows a green ping dot once added.
Common mistakes
| Mistake | Fix |
|---|---|
Calling the MCP before compose up |
Bring the stack up first; "Failed to connect" = container down |
| Guessing CSS selectors | Grep the component; use data-*/title/placeholder/class*= |
Using :contains/XPath |
Unsupported — CSS only |
returnBase64:true then not looking |
Save to file, Read it, inspect the pixels |
Rebuilding for a .tsx change |
Frontend hot-reloads; only Rust needs cargo build |
| "It compiled / toast showed" = done | Verify the real effect (clipboard, host state, DB) |
| App hangs on splash | Container needs seccomp=unconfined (keyutils keyring) |
| Hacking around a missing/limited MCP tool | The MCP is ours — add the tool, push, continue ("The MCP is yours") |