Install

Most people don’t need this page — the home page covers the one-step paths for agents, VS Code, and the CLI. This page is the full reference: every install surface, requirements, and CI recipes.

CLI

The compose-preview binary works against any Compose project with no build edits — it injects the preview plugin at runtime via a bundled Gradle init script.

The default route installs the agent skills with the skills CLI, then runs the stub bundled with the compose-preview skill once:

npx skills add yschimke/skills --global --yes --skill compose-preview --skill compose-ui-builder
~/.agents/skills/compose-preview/scripts/compose-preview --version   # first run installs the CLI and puts it on PATH

npx skills add installs the skill content only, into ~/.agents/skills/ with per-agent links. The stub’s first run executes the canonical installer with --cli-only: it downloads the CLI, links ~/.local/bin/compose-preview, adds ~/.local/bin to your bash/zsh/fish startup files, and re-execs. Open a new terminal afterwards. To update: compose-preview update updates the CLI (and PATH), npx skills update updates the skills.

That installs compose-preview and compose-ui-builder. The other skills (compose-preview-review, compose-preview-ci, compose-preview-design-board, compose-design-catalog, figma-catalog-import, design-parity-review) are opt-in: add another --skill <name>; the curl installer takes --skills a,b or --all-skills.

Fallback: the installer. When there’s no Node, or you want the CLI and skills in one step:

curl -fsSL https://raw.githubusercontent.com/yschimke/skills/main/scripts/install.sh | bash

The installer drops the CLI on your $PATH and (unless you pass --cli-only) the agent skills into the Claude / Codex / Gemini skill directories. Useful flags (pass them as … | bash -s -- <flags>):

Flag Effect
--cli-only Install the CLI only, skip the skill bundles.
--skills a,b Install these skills in addition to the default two.
--all-skills Install every skill in the repo.
--with-skills Refresh skill content even when npx or a plugin installed it.
--no-modify-path Don’t add ~/.local/bin to shell startup files.
--android-sdk Also install the Android cmdline-tools + platform + build-tools.
--jdk 17,21 Install the listed JDK majors.
VERSION Install a specific release instead of latest.

Then:

compose-preview doctor    # check Java + project compatibility
compose-preview list      # scan @Preview annotations
compose-preview render    # render every @Preview to PNG
compose-preview browse    # Storybook-like local component browser
compose-preview render-matrix --id com.example.MyPreview \
    --ui-mode light,dark --font-scale 1.0,2.0   # one preview across a grid

browse scans every valid preview module. When a conventional executable CMP Wasm browser project owns the previews or depends on their module, its distribution is built and attached automatically:

compose-preview browse

Java/Android-only composables remain available as rendered snapshots with their sample source. --module :shared:ui is an optional narrowing control, not required setup.

Because the CLI auto-injects, projects that already apply com.android.application / com.android.library / org.jetbrains.compose work without touching build.gradle.kts. Projects that already declare the plugin are detected and left alone, so mixed setups don’t conflict.

Pinning one version for every entrypoint

compose-preview reaches a project through several doors — the CLI, the VS Code extension, the CI actions, an explicitly applied Gradle plugin — and by default each one uses whatever version it was installed at. On a project with more than one door in use that drifts, and a CLI newer than the plugin it’s driving fails in a way that reads like a project misconfiguration (“no modules have the compose-preview plugin applied”) rather than the version skew it is.

Pin the project once:

compose-preview pin --cli      # pin to the CLI you just installed
compose-preview pin 1.1.0      # …or to a specific release
compose-preview pin            # show the pin, and whether your CLI matches

That writes a single line to your gradle.properties:

composePreview.version=1.1.0

From then on the CLI auto-injects that plugin version, the VS Code extension applies it too, and CI installs it with version: pin on the install action (the apply action’s default cli-version: auto picks it up with no workflow change). If a project already pins composePreviewCli in gradle/libs.versions.toml, that’s read as a pin too — nothing to add.

compose-preview doctor reports the pin, its source, and whether the CLI on your $PATH matches it. Unpinned projects keep working exactly as before; the pin is opt-in.

The pin governs the auto-injected plugin — the zero-config path above. A module that declares id("ee.schimke.composeai.preview") version "…" itself keeps its own version, and compose-preview pin never edits build scripts or version catalogs: auto-inject already leaves such a module alone, so its declaration is the single source of truth there. Full model: VERSION_PIN.md.

Builds that apply AGP via a convention plugin

If your build supplies the Android Gradle Plugin through an included build’s convention plugin — the build-logic / gradle/conventions pattern used by Now in Android, AndroidX, and similar repos — auto-inject cannot apply the preview plugin, and discovery will report 0 modules with failures like:

NoClassDefFoundError: com/android/build/api/variant/AndroidComponentsExtension

This is a classloader limitation, not a bug. With the convention-plugin layout AGP is applied by the included build, so AGP’s classes live on the convention plugin’s classloader. Auto-inject can only add the preview plugin to each project’s own buildscript classpath — a sibling classloader that can’t see AGP — so the plugin throws the moment it touches AndroidComponentsExtension. There is no Gradle init-script API to contribute a dependency to an included build’s classpath, so auto-inject genuinely can’t reach AGP here.

Apply the plugin from your convention plugin instead. That puts it on the same classloader as AGP, where it renders correctly. The CLI then detects the included-build apply and skips auto-inject automatically (no --no-auto-inject needed).

  1. Add the preview plugin’s marker artifact to your build-logic build’s dependencies. This is the key step: it puts the plugin on the convention build’s runtime classpath, so the compiled convention plugin can apply it by id. (Declaring it in build-logic’s plugins {} block with apply false only resolves it for that build script — not for the convention plugin applied to your app modules, which fails with Plugin with id … not found.) The marker coordinate is <id>:<id>.gradle.plugin:<version>:

    // build-logic/.../build.gradle.kts (the convention build)
    plugins {
        `kotlin-dsl`
    }
    dependencies {
        implementation(
            "ee.schimke.composeai.preview:ee.schimke.composeai.preview.gradle.plugin:<latest>"
        )
    }
    

    Make sure build-logic’s repositories include where the plugin is published (mavenCentral() / gradlePluginPortal()).

  2. Apply it from your convention plugin alongside AGP:

    // build-logic/.../YourAndroidConventionPlugin.kt
    override fun apply(target: Project) = with(target) {
        pluginManager.apply("com.android.library") // or the AGP plugin you use
        pluginManager.apply("ee.schimke.composeai.preview")
        // …rest of your convention…
    }
    
  3. Run the CLI as usual — it sees the included build provides the plugin and leaves your build alone:

    compose-preview render
    

This is the explicit “apply manually via your convention plugin” integration mode: auto-inject is the zero-config convenience for the common plugins { id("com.android.application") } layout; the convention-plugin layout opts out of it by design.

Gradle plugin (version-pinned)

If you’d rather wire it into your build explicitly, the plugin is published to Maven Central — no auth, no PAT.

// <module>/build.gradle.kts
plugins {
    id("ee.schimke.composeai.preview") version "<latest>"
}

Pin the version shown on Maven Central. (The in-repo samples/ apply it without a version because they resolve it from this repository’s own included build — that’s not drop-in for an external project.)

./gradlew :app:composePreviewDiscover    # scan @Preview annotations
./gradlew :app:composePreviewRenderAll   # render every @Preview to PNG

For direct ./gradlew use with the auto-inject script (e.g. a CI step that needs extra Gradle flags), materialise the init script once and thread its path through each invocation:

INIT_SCRIPT="$(compose-preview init-script --path)"
./gradlew --init-script "$INIT_SCRIPT" :app:composePreviewRenderAll

VS Code extension

Published to the VS Code Marketplace and Open VSX (VSCodium / Cursor / Windsurf). Open the Extensions view (⇧⌘X / Ctrl+Shift+X), search Compose Preview, click Install. It auto-injects on every Gradle invocation it makes — no project changes needed.

Agent skill

Point any agent that can fetch a URL at the compose-preview skill — a complete install-and-iterate playbook. The skill checks whether the CLI is present and bootstraps it (via its bundled stub and the installer above) if not, so “point the agent at the skill” and “run the installer” converge on the same place. See Agents & MCP for the agent loop the skill drives.

Per-harness plugins

For agent setup, follow the Compose Agent Plugins quick start. The repository is now yschimke/compose-agent-plugins; existing installs using the old marketplace name should follow its migration steps.

  • compose-skills supplies the canonical workflows from yschimke/skills.
  • compose-preview supplies local MCP wiring and hooks. Local rendering needs the CLI on PATH, Java 17 or newer, and compose-preview mcp install run once from each Compose project’s root.
  • compose-catalogs connects to hosted catalogs and the UI Builder without a local toolchain.

The quick start has the commands for Antigravity, Claude Code, Codex and OpenCode. Use one copy of each skill bundle and MCP registration; the single marketplace also installs compose-skills, so a separate skills marketplace is unnecessary.

When Antigravity, Claude Code, or Codex is detected, mcp install prints the matching wiring-plugin commands as a reminder; pass --no-plugin-hint for config-only automation.

Once the Antigravity compose-preview plugin is installed (~/.gemini/config/plugins/compose-preview), mcp install and mcp register leave Antigravity’s global config alone: the plugin already provides the server, and a global entry would be a second copy of every tool. If an older global compose-preview-mcp entry exists in ~/.gemini/antigravity/mcp_config.json or ~/.gemini/config/mcp_config.json, both commands and mcp doctor name the file to remove it from. Pass --antigravity to write the global entry anyway.

Host registration is global and one-time: each entry is <stable launcher> mcp serve, with no --project. mcp install in a project only adds a missing entry or repairs a broken one. compose-preview mcp register does the registration without touching Gradle, and compose-preview mcp repair (run by compose-preview update) points existing entries at the stable launcher and drops a global --project, for example after an upgrade removed an old compose-preview-<version> directory.

CI / GitHub Actions

Composite actions for pipelines:

  • install — pin the CLI on $PATH. version: pin reads the project’s own version pin; version: catalog tracks a specific catalog key (Renovate recipe included).
  • apply — unified pipeline: baselines on push, before/after PR comments, a11y + notification surfaces.

There’s also a reusable, preview-gated AI PR-review workflow (Codex / Claude / Gemini): see PR review workflow.

Speeding up preview CI

Rendering is the long pole. Two levers, both about parallelism:

  • Sharding (on by default). composePreview { shards } fans a module’s previews across parallel JVM forks. The default is 0 (auto): the plugin sizes the fork count from the discovered preview cost and the runner’s cores
    • memory, and only shards when the predicted saving is worth the extra JVM cold-starts — so small modules stay single-fork and heavy ones (many previews, GIF/animated captures) fan out automatically. Set shards = 1 to force it off, or a fixed ≥ 2 to pin it. No workflow changes needed; auto sizing engages on the first CI run because the pipeline discovers previews in a separate Gradle pass before rendering.
  • Bigger runners. Auto sharding is capped at cores − 1 forks (leaving one core for the Gradle daemon), so it can only go as wide as the runner is. The default runs-on: ubuntu-latest is 2 vCPU on private repos (no sharding) and 4 vCPU on public ones (up to 3 forks). For a large preview suite, point the preview job at a 4- or 8-vCPU runner — that’s where the two levers compound into real wall-clock savings. RAM scales with cores on GitHub-hosted runners, which keeps the per-fork memory bound out of the way.

Render classpath conflicts

The render JVM classpath is built from a single resolved dependency graph: the plugin’s renderer configuration extendsFrom your module’s unit-test runtime classpath (and its screenshotTest runtime classpath, when Google’s screenshot plugin is applied), so Gradle picks one version per module across the renderer’s dependencies and your own. Only entries that exist nowhere else — the unit-test merged R.jar, generated class dirs — are appended from AGP’s test task on top.

Before this, those graphs were resolved separately and concatenated, which put two versions of the same module in front of one classloader. Java loads the first match per class, so a class from the winning jar could link against a sibling only the other version defined, and you’d get a NoSuchFieldError or NoSuchMethodError in an unrelated <clinit> — e.g. two bcprov-jdk18on jars failing every accessibility preview inside BouncyCastle’s post-quantum KeyFactorySpi.

A guard runs before each render and reports any module still present at more than one version, naming each version and the jar that wins:

Property Default Effect
composePreview.classpathDuplicates warn fail turns a duplicate into a build error (good for CI); off silences the check.
composePreview.legacyClasspathUnion false true restores the old concatenated classpath. Escape hatch only — set it if a render suddenly can’t find a class that lives solely on your unit-test classpath, and please file an issue.

If the guard reports a duplicate module, align it in your own build with a version force, or exclude it from whichever graph shouldn’t carry it.

Split families

A second check covers libraries published as several coordinates with no BOM — org.bouncycastle:bcprov/bcutil/bcpkix is the usual offender. Gradle aligns a module against itself and nothing further, so the graph can settle on bcprov-jdk18on:1.85 while leaving bcutil-jdk18on:1.84: one version per coordinate, no conflict to resolve, and classes that still don’t agree at runtime. That’s what failed every accessibility preview in homeassistant-remotecompose#495.

The fix is a virtual platform, which states that the coordinates move together and lets normal conflict resolution pick the highest version any member asks for:

abstract class BouncyCastleAlignmentRule : ComponentMetadataRule {
  override fun execute(context: ComponentMetadataContext) {
    val id = context.details.id
    if (id.group == "org.bouncycastle") {
      context.details.belongsTo("org.bouncycastle:bouncycastle-virtual-platform:${id.version}")
    }
  }
}

dependencies { components.all(BouncyCastleAlignmentRule::class.java) }

Prefer that over useVersion("1.84"): a force goes stale, and silently becomes a downgrade the moment one member’s floor moves (Robolectric’s did).

The plugin reports this rather than applying it. ComponentMetadataRules register on DependencyHandler, so they apply to every configuration in the module — including your releaseRuntimeClasspath. Aligning a family upward is usually right for a render classpath and isn’t a preview tool’s call to make for a shipped app, so the decision stays yours.

Keeping a module off the render graph

Sharing one graph with your unit-test classpath is what keeps a single coherent version of each module in front of the render classloader — but it also means the render configuration inherits your constraints. If your build applies a platform whose constraints are strictly(v) + reject("(v,"), every renderer dependency above one of those pins becomes a conflict Gradle cannot solve, and the render configuration fails to resolve before a single preview renders.

renderGraph removes a module from the configurations the plugin owns, leaving your own build exactly as it was:

composePreview {
  renderGraph {
    exclude(group = "com.example", module = "version-constraints")
  }
}

In a Groovy build script, the usual map form:

composePreview {
  renderGraph {
    exclude group: 'com.example', module: 'version-constraints'
  }
}

Either half may be omitted, exactly as in Gradle’s own Configuration.exclude(group:, module:): group alone drops the whole group, module alone drops that name wherever it comes from.

Property Default Effect
composePreview.renderGraphExcludes (none) Comma-separated group:module coordinates, for a build script you can’t edit — a CLI-driven render where the plugin is auto-injected, or one CI job. Additive with the DSL.
./gradlew composePreviewRender -PcomposePreview.renderGraphExcludes=com.example:version-constraints

The exclusions land on the configurations the plugin creates — the Android render config, its daemon superset, and the two desktop equivalents — and never on the configurations your build declares, so a normal build and everything your platform guarantees it are untouched. The Kotlin-build-tools configurations are deliberately not covered: they are standalone compiler classpaths that never inherit your graph.

This is a scalpel, not a switch. Excluding something the renderer genuinely needs trades an unresolvable configuration for a ClassNotFoundException on the render JVM; --info prints one line per configuration naming what was excluded.

Requirements

Java 17+, Gradle 8.13+, AGP 8.13.0+ (Android), Kotlin 2.0.21+, Compose Multiplatform 1.10.3+ (Desktop). The bottom edge of this envelope is exercised on every push by the agp8-min job.

Running an agent in a cloud sandbox (Claude Code on the web, etc.)? See the cloud sandbox setup for the network allowlist and install.sh --android-sdk Setup recipe. </content>


Apache 2.0 licensed. Source on GitHub.

This site uses Just the Docs, a documentation theme for Jekyll.