Skip to content

feat: web UI for flows, runs and demos - #4

Merged
piekstra merged 5 commits into
mainfrom
piekstra/ui
Oct 7, 2026
Merged

piekstra merged 5 commits into
mainfrom
piekstra/ui

Conversation

@piekstra

@piekstra piekstra commented Oct 7, 2026

Copy link
Copy Markdown
Member

Summary

lest ui serves a web UI for flows, runs and demos from the binary. docs/ui.md describes the screens; docs/design.md explains why the UI is server-first.

  • Server (crates/lest-server):
    • Binds loopback only. Every request needs the per-launch token, as an HttpOnly SameSite=Strict cookie set from ?token= or as a bearer token, and a loopback Host header, which blocks DNS rebinding. Responses carry a strict CSP.
    • JSON API for state, flows, runs, starting and cancelling runs (two-stage cancel), and tool sign-in in a terminal.
    • One server-sent event stream for every run.
    • Run files with range requests, confined to the run directory, and reveal-in-file-manager.
    • The UI is embedded with rust-embed.
  • UI (ui/): React and TypeScript, with no runtime dependencies beyond React.
    • Flows explorer: cards grouped by folder, with search, tag filters and validation banners.
    • Flow view:
      • environment and inputs, Watch browser, Run and Cancel;
      • a live step tree with retry countdowns, and Output, Details and Artifacts tabs;
      • a failure card with the deterministic headline, Copy rerun command and Copy context;
      • a side panel with the live browser, artifacts, cleanups and warnings.
    • Runs: history and the run page.
    • Demos: gallery and record screen, with beats lighting up live. It plays the cut with chapters once the demos PR lands, and falls back to the raw recording until then.
    • Settings: theme.
  • Live view: the UI opens the browser's DevTools socket directly, which is allowed for the UI's origin only. It acknowledges each screencast frame before drawing and draws only the newest frame, through a ref, without React re-renders.
  • State: runs are keyed by runId in a pure reducer that ignores duplicate and stale events and refetches on a gap. A view opened mid-run hydrates from GET /api/runs/:id and continues from the stream.
  • Fixes from building it:
    • The harness no longer emits duplicate page events.
    • The runner follows browser events every 50ms, so short steps can be watched.
    • Running entries in /api/runs now include startedAt and environment.
    • The leak scan skips binary files (short patterns matched random bytes in PNGs); images are reviewed by eye.
  • CI: make lint runs tsc, make test runs vitest, and make build builds the UI before the binary so it is embedded.

Evidence

make check passes: Rust lint and tests, UI typecheck, 28 vitest tests (reducer fixtures are real captured event streams, including a cancelled run), and the UI and release builds. Screenshots come from npm run screenshots against lest ui on the examples project, at 1440x900 @2x.

Flows

Flows explorer

A run in progress, with the live browser showing the consent popup

Live run

A failed run, with the headline, copy actions and the screenshot at failure, in light and dark

Failed run

Failed run, dark

A passed run with step details

Passed run

Runs, demo page and settings

Runs

Demo page

Settings

@piekstra

piekstra commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

A security and correctness review found 10 issues. Dispositions:

  1. Artifacts could run scripts with the UI's access. Run files are now served with Content-Security-Policy: sandbox; default-src 'none' (images and media still load in the UI). HTML, SVG, XML and JS download as attachments instead of rendering. Verified with curl.
  2. CSRF from other local ports. A request that changes something and arrives with the cookie must carry the UI's own Origin, or Sec-Fetch-Site: same-origin. Bearer-token clients are not affected. Verified: a cancel with Origin: http://127.0.0.1:4796 gets 403, and the UI's own origin gets 200. Unit test: changes_need_the_ui_origin.
  3. A second lest ui logged out the first. The cookie is now lest_token_<port>, so two servers no longer collide, and the origin check above stops other local pages from using the cookie.
  4. Sign in from the UI dropped the tool's env pins. The tool's pinned env vars that need no run values are now prefixed to the command.
  5. Hydration dropped newer events. Events that arrive during a gap are held, then applied after the snapshot if they are newer. New vitest: "hydration during a gap".
  6. An SSE reconnect did not rehydrate. After a reconnect, every run still shown as live is refetched.
  7. The live view failed on localhost. --remote-allow-origins now lists both loopback names, and the CSP allows both.
  8. The 401 copy said "one-time token". It now reads "this server's token".
  9. The doc comment said the app shell needed the token. It now describes what actually needs the token (the API) and what does not (the app shell and assets, which carry no data).
  10. Smaller items:
    • Reveal uses open -R, so it no longer launches .app directories.
    • A run is marked done when its task ends, even without a runFinished event.
    • Markdown rejects protocol-relative //host links.

Not changed: GET /runs/:id returns 404 if the report write itself failed. That failure already appears as a run warning, and the run is gone from disk.

make check passes. 29 UI tests and the new server test are included.

@piekstra
piekstra marked this pull request as ready for review October 7, 2026 14:11
@piekstra
piekstra merged commit 516ffc2 into main Oct 7, 2026
5 checks passed
@piekstra
piekstra deleted the piekstra/ui branch October 7, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant