Escapement can serve a browser app over the same read-only EQL/Pathom surface the CLI exposes
with --api-server <port>. The app is a statechart-driven RAD explorer: a Sessions report that
drills into a session's events and artifacts, plus a live Debugger screen with an embedded
statechart visualizer. The remote and Fulcro Inspect are fully wired.
It is optional: the /api surface works with or without it, and a normal escapement run never
loads any of the UI code.
The explorer's RAD reports/model are shared CLJC and render two ways from one definition: in the
browser with a Semantic-UI adapter, and in a terminal with a fulcro-tui target (see
RAD explorer below). Adding --debug turns
the surface into a live single-stepping control plane (see
Live single-stepping).
| Piece | File | Role |
|---|---|---|
| SPA entry | src/escapement/ui/client.cljs | Fulcro app. :remote POSTs transit EQL to /api. Installs the Semantic-UI RAD adapter, mounts the explorer Root/routing, installs Fulcro Inspect via ido (elided from release builds). |
| Explorer model | src/escapement/ui/model/*.cljc | Shared CLJC RAD attributes (session / transcript-event / artifact) — all virtual, backed by the Pathom resolvers (no storage adapter). |
| Explorer screens | src/escapement/ui/screens/*.cljc | Shared CLJC statechart defsc-report/screens: Sessions → Events → Artifacts, session detail, plus the live debugger + chart-view. |
| Render adapters | src/escapement/ui/rendering/{semantic-ui,tui}/*.cljc | Two render targets for the same reports: Semantic-UI DOM (browser) and fulcro-tui terminal nodes. |
| HTTP surface | src/escapement/ui/server.clj | http-kit server. POST /api runs EQL (reads and control mutations); GET serves the SPA (same origin, so the relative /api remote just works). |
| Resolvers | src/escapement/ui/resolvers.cljc | Read resolvers + the live control plane — host-agnostic, shared with the CLI/TUI. |
| Build | shadow-cljs.edn :main build | :target :browser → resources/public/js/main/main.js. Now includes the RAD + Semantic-UI + elkjs stack. |
| Page | resources/public/index.html | Committed template; __MAIN_JS__ is replaced at serve time with the content-addressed bundle URL. |
| Fetch manifest | resources/escapement-ui.edn | Committed. {:version :sha256 :asset} — pins which release asset to fetch and verifies it. |
The CLJS deps (Fulcro, fulcro-rad, fulcro-rad-statecharts, fulcro-rad-semantic-ui, Fulcro Inspect,
devtools-remote, transit-cljs, shadow) live only in the :cljs alias in deps.edn; they are
never on the bb runtime classpath.
The explorer is built with fulcro-rad-statecharts (RAD reports whose state machine is a statechart). Its model and screens are CLJC and shared, so one report definition drives both the browser and a terminal:
escapement.ui.model.{session,transcript-event,artifact}) — RAD defattrs that mirror
the read-surface resolver output. They are virtual attributes (no ao/schema): the values come
from the existing Pathom 2 resolvers (escapement.ui.resolvers), not from a RAD storage adapter.escapement.ui.screens.*) — defsc-reports and routing: SessionsReport (landing,
:escapement/all-sessions) → row-action drill into EventsReport (::sc/session-id route param)
→ ArtifactsReport, plus session detail, the Debugger, and the ChartView visualizer.escapement.ui.rendering.semantic-ui.* — a hybrid adapter (defmethods for the
statechart report/form/field render multimethods + a controls map) emitting Semantic-UI-classed
DOM. Installed by client.cljs.escapement.ui.rendering.tui.* — a fulcro-tui plugin whose leaves return terminal
element nodes instead of DOM. The same RAD reports/model run headless under the JVM.Because the report logic, query, and model are shared CLJC, there is no second copy of the explorer: the only per-target code is the thin render adapter.
The compiled bundle (~1.2 MB) is not committed to git — it would bloat history, since it changes on every build. Instead the server resolves it on demand, cheapest-first:
public/js/main/main.js — present in the release jar and after a local bb build-ui.~/.cache/escapement/ui/<version>/main.js (honours $XDG_CACHE_HOME).https://github.com/fulcrologic/escapement/releases/download/escapement-<version>/main.js,
verified against the SHA-256 in resources/escapement-ui.edn, then cached.So a bbin install (which builds its classpath from the git tree, with no bundle in it) fetches the
bundle from the matching GitHub release the first time you use --api-server, and caches it. With no
bundle and no network, / still loads and shows a notice; /api always works.
The bundle is served content-addressed at /js/main/<sha>.js with
Cache-Control: public, max-age=31536000, immutable, and GET / injects that SHA path into
index.html. A freshly built/published bundle has a new SHA, so the browser never serves a stale
bundle from cache. (/js/main/main.js remains as a no-cache alias.)
/api)With --api-server <port> --debug, a run starts headless and auto-paused, and the web Debugger
drives it event-by-event. The mechanism, edge to core:
escapement.engine.instrumented-queue). The debug pause gate has been
relocated out of the runner loop and into the event queue. InstrumentedQueue wraps the plain
InProcessQueue; in receive-events! it applies the gate per deliverable event before the
handler runs (arm-from-external → park on paused → consume a step budget), then delivers. The gate is
a no-op unless a escapement.debug.controller is attached, so a normal run pays nothing.
send!/cancel! delegate straight through and stay non-blocking — invocation worker threads
call send! while the single draining thread is parked, and that must never deadlock. bb + JVM safe
(atoms + promises only). It also exposes pending-events / last-delivered for the live resolvers.escapement.debug.controller). A plain atom holding {:mode :step-budget :gate :pause-on-next-external?}. pause!/step!/continue!/arm-pause-on-next-external! mutate it and
release the gate promise.escapement.debug.control-handle). The api-server starts before the runner
builds its env, so it cannot be handed the env directly. cmd-run creates a shared handle (an atom)
and passes it to both the server ctx (:escapement/live) and run!'s on-env-ready, which fill!s
it with {:controller :env :session-id :queue} the moment the live env exists. Live
resolvers/mutations deref the handle each request and degrade to nil before it is filled./api (escapement.ui.resolvers). The Pathom parser now sets
::p/mutate pc/mutate, so a POST /api can dispatch a mutation as well as a read:
escapement.control/{pause,step,continue,arm-pause-on-next-external} →
{:debug/paused? :debug/step-budget}.:session/paused?, :session/step-budget, :session/live-configuration
(the active state set, via runtime/current-configuration over the live working-memory store) and
:session/pending-events (queued-but-undelivered events from the instrumented queue).#?(:clj …) (the controller/handle/runtime are JVM/bb-only) so resolvers.cljc
stays cljs-safe; under cljs live-resolvers is empty.escapement.ui.screens.debugger renders Pause/Step/Continue/Arm + the live snapshot;
escapement.ui.screens.chart-view embeds the statechart visualizer (ELK layout) and highlights the
live configuration. Both read/refresh the single shared [:component/id ::control/live] entity, so a
step in the Debugger updates the chart highlight and vice-versa.Usage:
escapement run escapement.examples.hello/agent --api-server 8920 --debug
# open http://localhost:8920/ → Debugger → Pause / Step / Continue / Arm; Chart → live highlight
--debug is independent of the TUI. The full POST /api transit → handler → parser → control path is
proved deterministically (no socket, no engine thread) by
test/escapement/ui/live_control_http_test.clj, which runs under bb test.
bb test vs clojure -M:api:ui-testThe UI work spans two classpaths that cannot coexist, because of a guardrails version conflict:
bb test — the engine, the instrumented queue, the control plane, the resolvers, and the
live_control_http_test integration proof. These run under babashka, where Pathom 2.4.0 only loads
with guardrails pinned to 1.2.16 (Pathom's transitive guardrails uses a timbre macro newer SCI
builds reject). These test namespaces stay bb-compatible (no Fulcro RAD).clojure -M:api:ui-test — the RAD/TUI UI tests (e.g. escapement.ui.{screens-load,tui-render}-test).
These load the Fulcro RAD stack, which wants guardrails 1.3.2, and run under the JVM (where
Pathom has no SCI constraint, so 1.3.2 and Pathom coexist). The :ui-test alias overrides the
guardrails pin for that classpath only.So the RAD/TUI namespaces are excluded from bb test via the jvm-only-namespaces skip-set in
bb_test/escapement/test/runner.clj. Run both:
bb test # engine + queue + control plane + live-control HTTP proof (bb, guardrails 1.2.16)
clojure -M:api:ui-test # RAD/TUI explorer screens (JVM, guardrails 1.3.2)
CLJC gotcha:
::fully.qualified.ns/kw(double-colon + full ns) resolves in CLJS when that ns is required, but is an invalid token in CLJ — so a.cljcfile using it compiles under shadow yet fails to load under:ui-test(JVM). In CLJC use the single-colon fully-qualified form:fully.qualified.ns/kw, or a real::alias/kwagainst an established alias.
# Terminal 1 — recompile + hot-reload on every save (dev build, with shadow's reload runtime).
bb watch-ui # = npx shadow-cljs watch main (output to resources/public/js/main, gitignored)
# Terminal 2 — serve the app + the live API from one origin so the relative `/api` remote works.
escapement run escapement.examples.ask/agent --no-tui --api-server 8920
Open http://localhost:8920/. The page loads, the remote hits /api on the same origin, and the
active session id renders.
watch process regardless
of who served the HTML); :after-load escapement.ui.client/refresh re-renders. You normally do not
refresh the page.com.fulcrologic.devtools.chrome-preload. (Open a new tab / restart Chrome after first installing
the extension. See the fulcro-inspect README.)--api-server run to re-hash after a rebuild. (Hot reload does
not need this.)/api, so it must be served from the same origin as the API
— i.e. by the escapement server, as above. Serving the HTML from shadow's own dev http server would
put /api on shadow's origin (404). Keep the escapement-served loop unless you add a dev proxy.The bundle ships two ways — bundled in the clojars jar and as a GitHub release asset for bbin.
Two tasks cover it:
bb build-ui — shadow-cljs release main compiles the optimized bundle to
resources/public/js/main/main.js and rewrites resources/escapement-ui.edn with the bundle's
:version (read from pom.xml, minus -SNAPSHOT) and :sha256. This is the only task that
produces the bundle and the manifest.bb release-ui — verify-not-rebuild: it never compiles. It asserts the on-disk main.js
SHA-256 equals the committed escapement-ui.edn (so the published asset is byte-identical to what
every bbin install verifies against), that the manifest is committed, and that the tag is on
origin; then it uses gh to publish main.js on the release escapement-<version>, creating
the release if needed (the maven-release-plugin makes the git tag but not a GitHub release)
and --clobbering an existing asset. Needs gh authenticated (gh auth login).The ordering matters — the git tag must capture the same manifest whose SHA the asset will match, and the bundle must not be recompiled after the manifest is committed:
bb build-ui — compile + write the manifest.resources/escapement-ui.edn.maven-release-plugin), which bumps the pom to the release version, commits,
and tags escapement-<version> at the commit that includes the manifest from step 2, then pushes.
release:perform builds/deploys the jar — and because bb build-ui already populated the working
tree, the pom.xml <resource> bundles main.js into the jar, so jar users serve it from the
classpath and never fetch.bb release-ui — publishes that exact main.js to the GitHub release.Do not run bb watch-ui between steps 1 and 4 — it overwrites main.js with a dev build, and
release-ui will refuse to publish (SHA mismatch). If you do, just re-run bb build-ui.
bbin install io.github.fulcrologic/escapement resolves to the latest git tag and pins the
install to that tag's exact commit SHA (it only falls back to latest-sha if the repo has no tags).
So:
bbin install resolves.escapement-ui.edn manifest; the server fetches escapement-<version>/main.js and verifies it
against that manifest's SHA-256, failing closed on any mismatch. A user can never receive a
bundle that doesn't match the hash committed at the ref they installed.release-ui guarantees published-asset == manifest. Because it verifies rather than recompiles,
the asset on the GitHub release is byte-identical to the manifest SHA at the tag. No reproducible-
build assumption, and no possibility of a checksum failure for end users.main.js is baked into each clojars jar version, so source
and UI are inherently consistent for that artifact.The one residual is process discipline: if you change client.cljs but forget step 1 (rebuild) before
tagging, the tag's manifest still points at the previous bundle, so bbin users get the older UI JS
with newer server source. Because the SPA talks to the server only through the read-only, additive EQL
API, that is a "stale UI" (missing the newest UI features), not a broken one — and release-ui's SHA
check still prevents any mismatch between the manifest and the published asset.
A dev install (
bbin install .or--latest-sha) can reference a manifest whoseescapement-<version>release doesn't exist yet (the version is the next release). The fetch then 404s and the UI degrades to its offline notice;/apiis unaffected. Use a tagged install for a guaranteed bundle.
bb watch-ui — dev compile + hot reload.bb build-ui — release compile + refresh escapement-ui.edn.bb release-ui — publish the already-built main.js to the GitHub release escapement-<version>
via gh (verify-not-rebuild; run bb build-ui + commit the manifest first).escapement run … --api-server <port> — serve /api + the SPA on <port>.escapement run … --api-server <port> --debug — same, but headless + auto-paused for live stepping.bb test — engine + queue + control plane + the live_control_http_test HTTP proof (bb).clojure -M:api:ui-test — the RAD/TUI explorer screen tests (JVM, guardrails 1.3.2).resources/public/index.html, resources/escapement-ui.edn, all src/escapement/ui/*.resources/public/js/.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |