<< All versions

Skill v1.0.0

currentAutomated scan100/100
kvdm-co-pilot/create-cmp/cmp-preview
──Details
PublishedSeptember 28, 2026 at 11:23 AM
Content Hashsha256:4caf05eb619eabfa...
Git SHA
──Files
Files (1 file, 9.7 KB)
SKILL.md9.7 KBactive
SKILL.md · 151 lines · 9.7 KB

version: "1.0.0" name: cmp-preview description: >- 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". The cmp-inspector MCP's preview service serves a self-updating gallery (pixels, wireframe and a11y per screen) that re-renders on every save — pixels flow to the human, structure flows to the AI.


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

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

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

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

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

  1. `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.

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

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

All versions