cmp-preview — live previews of real screens, zero commands
Your job: give the human a live gallery URL of their app's real screens and keep it
current while they (or you) edit code — and use the structural side of the same render
to verify changes. Nobody runs Gradle by hand; the MCP service owns the loop.
Before anything: confirm the capability (fail loud)
The cmp-inspector MCP tools are a capability, not a given. Before your first call, confirm
they resolve (ToolSearch for "cmp-inspector"). If no tools match, STOP — do not fall back
to manual Gradle, screenshots, or raw adb silently. Diagnose in order and REPORT to the human:
- Plugin enabled? Check
enabledPlugins in ~/.claude/settings.json (or the project's
.claude/settings.json).
- Session older than the plugin's enablement? MCP servers attach at session START — a
session born without the plugin never gains its tools, and no amount of in-session
retrying will surface them. The fix is restarting the session.
- Plugin copy stale or server broken? Run cmp-doctor's inspector-MCP check group.
Only after reporting may the documented degraded path (./gradlew :composeApp:renderScreens
node qa/preview-gallery.mjs) be used — and the report must name what is lost: on-save
re-render, changed-screen attribution, and the preview_diff change proof.
The one-call flow
preview { projectDir } (cmp-inspector MCP) — starts (or reuses) the resident
service. It:
- renders every
inspector/PreviewRegistry.kt entry headlessly via the app's
generated :composeApp:renderScreens task (real Koin DI, real theme, real data);
- serves the live gallery at the returned
url (default http://127.0.0.1:9600/);
- watches
composeApp/src/** — every save re-renders (debounced, serialized) and the
page reloads itself via SSE, with changed screens flagged CHANGED.
1b. Phase 2 runs automatically (hot, default true): the service boots a resident
preview daemon under Compose Hot Reload (hotRunDesktop --mainClass=<pkg>.inspector.PreviewDaemonKt --auto, JBR auto-provisioned). Saving a
file recompiles incrementally and hot-swaps classes INTO the running JVM; renders go
through the daemon's loopback /render — measured on a real app: ~900ms for one
screen, ~7s for all seven, ~10s save→gallery-shows-the-change (vs 25–40s per change
on the task path). If the daemon can't boot (no dev-client feature/hot-reload plugin,
no JBR) the service transparently stays on the Gradle path — same gallery, slower loop.
- Hand the human the
url (open it for them if you can, e.g. open <url> on
macOS, or the host's browser surface). That's their whole workflow: edit → save →
watch the gallery update. First render includes a Gradle compile (tens of seconds);
warm saves re-render in a few seconds.
- You work structurally — after every edit, ONE call:
preview_status { waitForRender: true }. It blocks until the render (or the
recompile) finishes and returns changedLastRender (empty = your edit reached no
screen), lastError/lastErrorSource ("compile" = your edit didn't even build —
the daemon surfaces hot-recompile failures, you are NOT flying blind), and
screens: [{ id, nodes, tokenized, tagged, a11yPass, a11yViolations, lastChangedVersion, tree, png }]. No HTTP polling, no sleeps. Assert deeper on the
tree paths with inspect_tree (pass testTag for one subtree) — never read PNGs.
preview_stop {} when the session is done (the Gradle daemon stays warm — good).
The verified edit loop (with the gallery open)
preview { projectDir } → human watches the gallery.
- Edit code →
preview_status { waitForRender: true } → the result names the changed
screens or carries the compile/render error.
preview_diff { screen: <a changed id> } → a proven verdict (proven-clean /
changed-with-regressions / no-change) between the last two renders — the service
keeps the previous generation, so there is NO snapshot bookkeeping. The human sees
the same change land visually: CHANGED flag, hover-to-compare before/after, and a
persistent "changed #N" badge per card.
- Baselines that must survive sessions are the lane's golden trees (
qa/golden/,
the goldenTrees step) — re-bless intended changes with UPDATE_GOLDEN=1.
Single-screen warm renders: render_screen { projectDir, screen } goes through the
running daemon automatically (via: "daemon", ~1s) and falls back to the Gradle task.
State variants (the Storybook "story" analog)
A screen in a forced UI state is just another registry entry with a derived id:
ScreenPreview("home@empty", "Home — empty") { … } hosting the screen with that state
(a state-first overload, or preview-only fakes). Loading/empty/error states then render
side by side with the default seeded state — same gallery, same selectors, same goldens.
The console — three more tabs on the same gallery
The URL from preview { projectDir } is a console, not just a gallery: alongside Screens
(the tab you've been reading about above) it also serves Design System (the swatch/dimen
grid resolved from the app's token catalog), Approvals (every governed artifact — design
system, architecture, exemplar feature/spec, per-feature specs — with its live status and an
Approve button), and Specs (each specs/*.spec.md's clauses with coverage badges). All four
tabs share the same SSE self-reload as the gallery.
Approving in the console (POST /api/approve) writes the exact same qa/approvals.json the
node qa/approve.mjs <artifact> CLI writes — same library underneath, either front end works.
Your side of that loop is approval_status { waitForDecision: true }: identical blocking
pattern to preview_status { waitForRender: true } — propose a change, tell the human to look
at the Approvals tab, then block on the decision instead of polling. Without
waitForDecision it just returns the current snapshot ({available, statuses:[...]}). This is
onboarding territory (the cmp-new skill's final step walks the human through the ordered
approval list); reach for it here whenever a governed artifact changed mid-session and needs
re-review.
Troubleshooting
- "does not look like a create-cmp app" —
projectDir must contain composeApp/.
- renderScreens task missing — the app predates project previews (scaffolded before
create-cmp 0.6). Re-stamp, or port the harness:
inspector/PreviewRegistry.kt +
PreviewHarness.kt + PreviewSemanticsJson.kt (desktopMain) + the renderScreens
task — see the template or the cmp-upgrade skill.
- First render slow / red banner "render FAILED" — the banner carries the Gradle
error and the gallery keeps the last good state; fix the compile error, save, it
recovers on the next cycle.
- Broken edit in daemon mode — a failed hot recompile produces no render, but it is
NOT silent: when a save yields no reload within ~20s the service runs its own compile
check, so the compiler's
e: lines land in lastError with
lastErrorSource: "compile", any pending waitForRender resolves with them, and the
gallery pill shows "compile failed". Expect that verdict to take ~20–40s (watchdog +
compile); a successful edit settles much sooner. lastActivity tells you what the
service last saw (src-change / compile-failed / render-ok / render-stale) if you need
to distinguish "quiet" from "stuck".
- Daemon quirks — the daemon listens on 9601;
preview_stop shuts it down. Rarely, a
hot swap can't apply a structural change (Compose Hot Reload limitation) — the daemon
keeps serving pre-change renders; restart the preview to heal. DI-module edits
(appModules) need a daemon restart too (Koin is started once).
- Port busy — the service probes upward from 9600 (or pass
port). Calling
preview again for the same project returns the running service's URL unchanged;
a different projectDir stops the old service first.
- Screen missing from the gallery — it's not in
inspector/PreviewRegistry.kt;
add a ScreenPreview entry (tab entries are regenerated from --tabs at scaffold
time; hand-added screens are yours to register).
Related
- cmp-inspect — the full inspector: tier-1 live device (
connect_live,
navigate_and_inspect, /inspect/remote click-to-tap device view), token drift,
golden trees. cmp-preview is the tier-0 daily driver; reach for cmp-inspect when you
need the RUNNING app (real navigation state, on-device data).
- cmp-dev-client — the interactive hot-reload desktop window (
hotRunDesktop);
complementary: dev-client is one live clickable window, cmp-preview is stills of
every screen at once, auto-refreshed.
1---2name: cmp-preview3description: THE default UI feedback loop while building or editing ANY Compose Multiplatform screen — in a stamped app, use it DURING development, not only when asked: it renders the app's REAL screens headlessly in seconds (no device, no emulator, no manual Gradle) and tells you exactly what each edit changed, so you catch your own mistakes as you build. Use this whenever you are about to write or change CMP/KMP UI code, need quick build/visual feedback, want to verify a UI change landed, or the user asks "preview my app", "show me my screens", "open the preview gallery", "Android Studio previews without the IDE", "see my UI without running the app", "live preview my compose screens", or "storybook for compose". Starts the cmp-inspector MCP's resident preview service: it renders every screen in the app's inspector/PreviewRegistry.kt headlessly (real DI, theme, data), serves a self-updating gallery at a local URL (pixels + wireframe + a11y per screen), and watches composeApp/src so every save re-renders automatically. Th4---56# cmp-preview — live previews of real screens, zero commands78Your job: give the human a **live gallery URL** of their app's real screens and keep it9current while they (or you) edit code — and use the structural side of the same render10to verify changes. Nobody runs Gradle by hand; the MCP service owns the loop.1112## Before anything: confirm the capability (fail loud)1314The cmp-inspector MCP tools are a capability, not a given. Before your first call, confirm15they resolve (ToolSearch for "cmp-inspector"). If no tools match, **STOP — do not fall back16to manual Gradle, screenshots, or raw adb silently.** Diagnose in order and REPORT to the human:17181. **Plugin enabled?** Check `enabledPlugins` in `~/.claude/settings.json` (or the project's19 `.claude/settings.json`).202. **Session older than the plugin's enablement?** MCP servers attach at session START — a21 session born without the plugin never gains its tools, and no amount of in-session22 retrying will surface them. The fix is restarting the session.233. **Plugin copy stale or server broken?** Run cmp-doctor's inspector-MCP check group.2425Only after reporting may the documented degraded path (`./gradlew :composeApp:renderScreens`26+ `node qa/preview-gallery.mjs`) be used — and the report must name what is lost: on-save27re-render, changed-screen attribution, and the `preview_diff` change proof.2829## The one-call flow30311. **`preview { projectDir }`** (cmp-inspector MCP) — starts (or reuses) the resident32 service. It:33 - renders every `inspector/PreviewRegistry.kt` entry headlessly via the app's34 generated `:composeApp:renderScreens` task (real Koin DI, real theme, real data);35 - serves the live gallery at the returned `url` (default `http://127.0.0.1:9600/`);36 - watches `composeApp/src/**` — every save re-renders (debounced, serialized) and the37 page reloads itself via SSE, with changed screens flagged `CHANGED`.381b. **Phase 2 runs automatically (`hot`, default true):** the service boots a resident39 preview daemon under Compose Hot Reload (`hotRunDesktop40 --mainClass=<pkg>.inspector.PreviewDaemonKt --auto`, JBR auto-provisioned). Saving a41 file recompiles incrementally and hot-swaps classes INTO the running JVM; renders go42 through the daemon's loopback `/render` — measured on a real app: **~900ms for one43 screen, ~7s for all seven, ~10s save→gallery-shows-the-change** (vs 25–40s per change44 on the task path). If the daemon can't boot (no dev-client feature/hot-reload plugin,45 no JBR) the service transparently stays on the Gradle path — same gallery, slower loop.462. **Hand the human the `url`** (open it for them if you can, e.g. `open <url>` on47 macOS, or the host's browser surface). That's their whole workflow: edit → save →48 watch the gallery update. First render includes a Gradle compile (tens of seconds);49 warm saves re-render in a few seconds.503. **You work structurally** — after every edit, ONE call:51 `preview_status { waitForRender: true }`. It blocks until the render (or the52 recompile) finishes and returns `changedLastRender` (empty = your edit reached no53 screen), `lastError`/`lastErrorSource` (`"compile"` = your edit didn't even build —54 the daemon surfaces hot-recompile failures, you are NOT flying blind), and55 `screens: [{ id, nodes, tokenized, tagged, a11yPass, a11yViolations,56 lastChangedVersion, tree, png }]`. No HTTP polling, no sleeps. Assert deeper on the57 `tree` paths with `inspect_tree` (pass `testTag` for one subtree) — never read PNGs.584. **`preview_stop {}`** when the session is done (the Gradle daemon stays warm — good).5960## The verified edit loop (with the gallery open)61621. `preview { projectDir }` → human watches the gallery.632. Edit code → `preview_status { waitForRender: true }` → the result names the changed64 screens or carries the compile/render error.653. `preview_diff { screen: <a changed id> }` → a proven verdict (`proven-clean` /66 `changed-with-regressions` / `no-change`) between the last two renders — the service67 keeps the previous generation, so there is NO snapshot bookkeeping. The human sees68 the same change land visually: CHANGED flag, hover-to-compare before/after, and a69 persistent "changed #N" badge per card.704. Baselines that must survive sessions are the lane's golden trees (`qa/golden/`,71 the `goldenTrees` step) — re-bless intended changes with `UPDATE_GOLDEN=1`.7273Single-screen warm renders: `render_screen { projectDir, screen }` goes through the74running daemon automatically (`via: "daemon"`, ~1s) and falls back to the Gradle task.7576## State variants (the Storybook "story" analog)7778A screen in a forced UI state is just another registry entry with a derived id:79`ScreenPreview("home@empty", "Home — empty") { … }` hosting the screen with that state80(a state-first overload, or preview-only fakes). Loading/empty/error states then render81side by side with the default seeded state — same gallery, same selectors, same goldens.8283## The console — three more tabs on the same gallery8485The URL from `preview { projectDir }` is a console, not just a gallery: alongside **Screens**86(the tab you've been reading about above) it also serves **Design System** (the swatch/dimen87grid resolved from the app's token catalog), **Approvals** (every governed artifact — design88system, architecture, exemplar feature/spec, per-feature specs — with its live status and an89Approve button), and **Specs** (each `specs/*.spec.md`'s clauses with coverage badges). All four90tabs share the same SSE self-reload as the gallery.9192Approving in the console (`POST /api/approve`) writes the exact same `qa/approvals.json` the93`node qa/approve.mjs <artifact>` CLI writes — same library underneath, either front end works.94Your side of that loop is `approval_status { waitForDecision: true }`: identical blocking95pattern to `preview_status { waitForRender: true }` — propose a change, tell the human to look96at the Approvals tab, then block on the decision instead of polling. Without97`waitForDecision` it just returns the current snapshot (`{available, statuses:[...]}`). This is98onboarding territory (the **cmp-new** skill's final step walks the human through the ordered99approval list); reach for it here whenever a governed artifact changed mid-session and needs100re-review.101102## Troubleshooting103104- **"does not look like a create-cmp app"** — `projectDir` must contain `composeApp/`.105- **renderScreens task missing** — the app predates project previews (scaffolded before106 create-cmp 0.6). Re-stamp, or port the harness: `inspector/PreviewRegistry.kt` +107 `PreviewHarness.kt` + `PreviewSemanticsJson.kt` (desktopMain) + the `renderScreens`108 task — see the template or the cmp-upgrade skill.109- **First render slow / red banner "render FAILED"** — the banner carries the Gradle110 error and the gallery keeps the last good state; fix the compile error, save, it111 recovers on the next cycle.112- **Broken edit in daemon mode** — a failed hot recompile produces no render, but it is113 NOT silent: when a save yields no reload within ~20s the service runs its own compile114 check, so the compiler's `e:` lines land in `lastError` with115 `lastErrorSource: "compile"`, any pending `waitForRender` resolves with them, and the116 gallery pill shows "compile failed". Expect that verdict to take ~20–40s (watchdog +117 compile); a successful edit settles much sooner. `lastActivity` tells you what the118 service last saw (src-change / compile-failed / render-ok / render-stale) if you need119 to distinguish "quiet" from "stuck".120- **Daemon quirks** — the daemon listens on 9601; `preview_stop` shuts it down. Rarely, a121 hot swap can't apply a structural change (Compose Hot Reload limitation) — the daemon122 keeps serving pre-change renders; restart the preview to heal. DI-module edits123 (`appModules`) need a daemon restart too (Koin is started once).124- **Port busy** — the service probes upward from 9600 (or pass `port`). Calling125 `preview` again for the same project returns the running service's URL unchanged;126 a different `projectDir` stops the old service first.127- **Screen missing from the gallery** — it's not in `inspector/PreviewRegistry.kt`;128 add a `ScreenPreview` entry (tab entries are regenerated from `--tabs` at scaffold129 time; hand-added screens are yours to register).130131## Related132133- **cmp-inspect** — the full inspector: tier-1 live device (`connect_live`,134 `navigate_and_inspect`, `/inspect/remote` click-to-tap device view), token drift,135 golden trees. cmp-preview is the tier-0 daily driver; reach for cmp-inspect when you136 need the RUNNING app (real navigation state, on-device data).137- **cmp-dev-client** — the interactive hot-reload desktop window (`hotRunDesktop`);138 complementary: dev-client is one live clickable window, cmp-preview is stills of139 every screen at once, auto-refreshed.