| name | ui-dev-loop |
| description | Persistent agent dev-loop for UI work โ keep the dev server and browser session alive across edits, wait on an explicit HMR-ready signal instead of sleeping, and query only what changed. Use when making UI/behavior changes to any tool (demo, moviemaker, movieplayer, shapeeditor, screenmap) and you need to observe the running app, not just pass unit tests. |
Persistent UI dev loop
The old loop restarts the server and browser on every check: edit โ start
server โ launch browser โ navigate โ rebuild state โ run Playwright โ parse
a huge result โ diagnose โ repeat. This burns tokens on repeated startup
output and stale/duplicate logs, and loses app state (route, in-tool view)
on every check.
The loop this skill teaches: edit โ Vite HMR (or reload) applies it โ wait
on an explicit ready signal โ query only what changed โ next edit. One
server, one browser session, for the whole task.
Visible-browser requests (do this first)
When the user says "open/start/turn on the agent browser", says they
should see a browser window, or otherwise asks only for the local UI to pop
up, run this exact command:
npm run dev -- --open
That is the simple visible-window path. Do not substitute npm run dev:agent, agent-browser --headed, daemon restarts, port inspection, or
session recovery unless the user separately asks for automated browser
control. npm run dev:agent intentionally opens no window.
If the user asks for both a visible window and automated interaction, start
with npm run dev -- --open; only then attach automation if the task needs
it. Do not restart a working visible server merely to change automation
launch flags.
Persistence rules
- Start the dev server with one command:
npm run dev:agent
(scripts/dev-server.mjs), backgrounded via your harness's
background-task support. It handles the whole lifecycle itself โ checks
whether something's already serving port 8080 and reuses it if so
(printing DEV-SERVER-READY <url> and exiting immediately without
touching the existing server), or starts one in-process and blocks until
killed. Wait on the DEV-SERVER-READY <url> line instead of polling with
curl. Opens no browser tab (vite.config.js's server.open is off by
default). Never restart it between edits โ stopping it is just killing
that one backgrounded task; there's no separate server to clean up
afterward (in-process, so even an ungraceful kill frees the port
immediately).
- Use one named agent-browser session per task:
--session <task-name>
on every command. Reuse it โ do not close and reopen between edits.
- Navigate only if the target route isn't already open:
agent-browser --session <name> get url.
The edit cycle
-
Baseline, before editing:
agent-browser --session <name> console --clear
agent-browser --session <name> errors --clear
agent-browser --session <name> eval "window.__agentUi?.update"
Record the returned update number.
-
Edit the code.
-
Wait โ never sleep:
agent-browser --session <name> wait --fn "window.__agentUi?.phase === 'ready'"
Do not additionally require update > baseline โ see the full-reload
caveat below, where the counter resets rather than increments.
-
On phase === 'error', read the compile error before touching
anything else: agent-browser --session <name> eval "window.__agentUi?.error".
-
Query narrowly:
agent-browser --session <name> errors
agent-browser --session <name> snapshot -s "#the-changed-panel" -i -c --depth 4
agent-browser --session <name> network requests --type xhr,fetch
Prefer accessibility snapshots for structural questions (is the button
present/disabled/what's its label). Use agent-browser --session <name> screenshot <path> only for visual questions (spacing, color, overlap) โ
scope to a selector when possible, not --full.
ledmapper-specific: canvas/WebGL state (read this first)
Every tool renders to a <canvas> (Three.js and/or Canvas 2D). A DOM
snapshot of a canvas is empty โ it tells you nothing about LED count,
recording state, or playback. The primary state channel for this repo is
window.__lmDebug, not DOM snapshots:
agent-browser --session <name> eval "window.__lmDebug?.moviemaker?.getState()"
agent-browser --session <name> eval "window.__lmDebug?.movieplayer?.getState()"
agent-browser --session <name> eval "window.__lmDebug?.shapeeditor?.getState()"
Only moviemaker, movieplayer, and shapeeditor are registered (see
src/debug-registry.ts) โ demo and screenmap are not, as of this
writing. Also pull the event trail for a fuller picture:
agent-browser --session <name> eval "window.__lmLog?.dump()"
See .claude/skills/debugging/SKILL.md for what the event trail and
watchdog warnings mean. There is no React introspection here (vanilla TS
throughout) โ __lmDebug fills that role instead.
The HMR sentinel and its real behavior here (important)
src/agent-ui-sentinel.ts exposes window.__agentUi = {phase, update, lastUpdateAt, error}, driven by Vite's HMR event stream. Dev-only, absent
from production builds.
Verified live behavior, not assumed:
- CSS edits hot-patch in place.
phase goes ready โ updating โ ready, update increments, zero page reloads, app state (route,
in-tool view, loaded video/screenmap) is preserved.
- JS/TS edits currently trigger Vite's full-reload fallback, because no
module in this codebase calls
import.meta.hot.accept(). The page fully
reloads; window.__agentUi re-initializes fresh (phase: 'ready', update: 0) rather than incrementing. App state is NOT preserved
across a JS/TS edit today โ the router will re-run from whatever route
is in the URL, but in-tool state (loaded file, playback position, drag
selection, etc.) is lost.
- This is why step 3 above waits on
phase === 'ready' alone rather than
an incrementing counter: the counter comparison is only meaningful for
CSS-only changes.
Practical effect: this stack still removes the sleep-and-guess pattern and
the repeated server/browser relaunch overhead for every edit, but the
state-preservation benefit this stack was designed for currently applies
fully only to CSS changes. If a task does many iterations on one loaded-state
scenario (e.g. a specific loaded video + screenmap in moviemaker), expect
to re-establish that state after each JS/TS edit โ plan the edit-observe
loop accordingly, and prefer batching several related JS/TS changes before
re-checking rather than round-tripping per line.
Testing split
- Narrowest relevant unit test during iteration:
npm test -- <pattern>
where practical, or the full npm test (it's fast, ~2s for 591 tests).
- Playwright / GPU specs only at task completion or for broad-impact
changes โ see the main debugging skill for the
@gpu rules.
HMR-applied (or reload-applied) โ correct: an explicit ready signal only
means the new code is live, not that it's right.
- Always run Playwright via
npm run test:integration [-- <spec>]
(scripts/run-playwright.mjs) โ never playwright test or npx playwright test directly. A PreToolUse hook
(.claude/hooks/check-playwright.py) blocks direct invocations and
errors with this same instruction. The blessed runner already does what
used to be manual advice here: reuses an already-running dev server
(starts one only if needed, tears down only the one it started), never
sets CI=1 (which would force a from-scratch production rebuild +
single-worker run every invocation), caps --workers to a safe default
(an unconstrained local run was observed to silently die mid-run โ no
crash message, dev server and every Chrome process just gone), and tees
full output to a gitignored .temp/logs/playwright-*.log while printing
a compact tail instead of the full firehose.
- Do not run the full suite as your default check during iteration โ
it takes minutes even with the blessed runner. Pick the spec(s) that
actually cover the changed surface:
npm run test:integration -- console-errors.spec.ts for "does any tool page still load cleanly", or
the relevant tool's own spec for behavior changes. Save the full suite
for final confirmation.
- Before declaring a UI task done, validate from a clean state at least
once: a fresh
agent-browser --session <name> reload (or a brand-new
session) plus the relevant formal test. Persistent sessions optimize
iteration, not final verification โ a page that works after ten edits
may still fail from a cold load.
Cleanup
Browser session state can contain cookies/auth โ never commit it (see
.gitignore: .agent-ui/, .agent-browser/, .auth/,
*.browser-state.json). Close the session when the task is done:
agent-browser --session <name> close.