# Cmp Preview

> 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. Th

- Skill: `kvdm-co-pilot/cmp-preview` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kvdm-co-pilot/cmp-preview`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kvdm-co-pilot/cmp-preview/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: kvdm-co-pilot (https://skillmd.com/u/kvdm-co-pilot)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kvdm-co-pilot/cmp-preview

---


# 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:

1. **Plugin enabled?** Check `enabledPlugins` in `~/.claude/settings.json` (or the project's
   `.claude/settings.json`).
2. **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.
3. **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

1. **`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.
2. **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.
3. **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.
4. **`preview_stop {}`** when the session is done (the Gradle daemon stays warm — good).

## The verified edit loop (with the gallery open)

1. `preview { projectDir }` → human watches the gallery.
2. Edit code → `preview_status { waitForRender: true }` → the result names the changed
   screens or carries the compile/render error.
3. `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.
4. 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.

