Verifying this app's UI through the Tauri MCP bridge
This app ships an optional MCP bridge that lets an agent screenshot the window, read/execute JS in the webview, and invoke real Tauri commands. It is the only way to check "does this actually render / actually work" without asking the user.
The single most common failure: launching with the wrong command. The bridge
is behind a non-default Cargo feature. npm run tauri dev compiles without
it and every mcp___hypothesi_tauri-mcp-server__* tool will then fail to connect,
with nothing in the log explaining why.
1. Launch
# Correct — bridge compiled in, listening on 127.0.0.1:9223
nohup npx tauri dev --features dev-mcp > /tmp/tauri-dev.log 2>&1 &
# Wait for the window instead of guessing a sleep duration
until pgrep -f "target/debug/cc-sessions-viewer" >/dev/null 2>&1; do sleep 2; done
sleep 5 # let the webview finish loading
First build takes minutes; incremental rebuilds are seconds. Do not run this in the foreground — it never exits.
Port 1420 is already in use
Vite is locked to port 1420 (strictPort, hardcoded in tauri.conf.json). A
stale Vite from a previous run survives pkill -f "tauri dev", and the next
launch dies with Error: Port 1420 is already in use followed by
The "beforeDevCommand" terminated with a non-zero status code.
lsof -ti :1420 | xargs kill # then relaunch
2. Connect
driver_session { action: "start" }
Required before any webview_* / ipc_* tool. A warning about the plugin not
reporting its version is expected and harmless — this repo pins
tauri-plugin-mcp-bridge 0.2, and tools needing ≥0.13 (such as
manage_window { action: "focus" }) will refuse; see §4 for the workaround.
Re-run driver_session start after any location.reload() — the reload drops
the injected bridge globals.
3. Drive the UI
Prefer webview_execute_js over coordinate clicking: it is deterministic, and it
returns data you can assert on in the same call.
// Open Settings → a specific tab, then read back what rendered
(async () => {
const wait = (ms) => new Promise(r => setTimeout(r, ms))
const btn = [...document.querySelectorAll('button')]
.find(b => /Settings|设置/.test(b.textContent || ''))
btn && btn.click()
await wait(300)
const tab = [...document.querySelectorAll('.set-nav-item')]
.find(n => /Storage|存储/.test(n.textContent || ''))
tab && tab.click()
await wait(500)
return { rows: [...document.querySelectorAll('.set-store-name')].map(e => e.textContent.trim()) }
})()
Calling real backend commands
Go through the webview's own bridge — ipc_execute_command has an allowlist and
will reject most commands:
(async () => {
const invoke = window.__TAURI__.core.invoke
return await invoke('storage_usage')
})()
This runs the real command against real data. For anything destructive, seed disposable fixtures first and restore afterwards (see §5).
4. Screenshots: focus the window first
If the window is not frontmost, document.visibilityState === 'hidden' and
requestAnimationFrame never fires. Vue <Transition> then freezes at
*-leave-from — a dialog you just closed stays in the DOM forever, and
screenshots come back faded, stale, or showing a modal that is logically gone.
This looks exactly like a product bug and is not one.
osascript -e 'tell application "System Events" to set frontmost of \
(first process whose unix id is (do shell script \
"pgrep -f \"target/debug/cc-sessions-viewer\" | head -1") as integer) to true'
Confirm before trusting a screenshot:
({ hidden: document.hidden, visibility: document.visibilityState })
manage_window { action: "focus" } would be the clean way, but it needs plugin
≥0.13 and this app is on 0.2 — use the osascript above.
5. Hazards worth planning around
- HMR reloads the webview. Editing locale files or several files at once remounts the app: open modals close and your carefully staged state is gone. Re-open and re-assert rather than trusting a stale handle.
- Disabled buttons swallow clicks silently. A clear/delete button gated on
:disabled="!bytes"does nothing when the target is empty, and the absence of a dialog reads as "my code is broken". Checkbutton.disabledbefore concluding anything. - Two copies of the app may be running. The user's installed build
(
/Applications/Sessions Viewer.app) shares on-disk state with the dev build. Never kill it, and remember it can be the one holding a file you are watching. - Seed, then restore. When verifying deletion/retention paths, write throwaway
fixtures (a
TEST-name prefix makes cleanup greppable), back up any real config you perturb, and put it back when finished.
6. Shut down
Kill the wrapper, the app, and Vite — the first two leave Vite holding 1420:
pkill -f "tauri dev"
pkill -f "target/debug/cc-sessions-viewer"
pkill -f "node_modules/.bin/vite"
lsof -ti :1420 # must print nothing
pgrep -fl "Applications/Sessions Viewer.app" # the user's app — must survive
Then driver_session { action: "stop" }.
Leave the machine as you found it: dev processes stopped, port free, fixtures deleted, backed-up config restored.