Generate a charm icon

Painter — LLM-driven charm-icon generation with a session cost cap.

Why a Painter?

Every charm on Charmhub ships an icon.svg at the project root. It's what the Charmhub catalogue lists, what the Web UI shows beside each application in juju status, and what people see on the charm's page. Authors today either hand-roll one in Inkscape, hire a designer, or ship the default placeholder — most charms do the third.

The Painter is Cantrip's charm_icon_generate tool (plus the /icon slash for interactive use). It routes a structured prompt to an image-generation provider (Imagen by default), gets back a square PNG, and writes it into icon.svg as an embedded image so the file is one self-contained artefact ready to commit.

The honest disclaimer up-front: reliable SVG output from image models is still weak, so the Painter rasterises and embeds rather than vectorising. Charmhub accepts the result and so does juju show-charm, but a designer-polish pass before release is recommended — replace icon.svg with a true vector once the visual language is settled.

What's available

Surface Use it for
/icon <description> Interactive: paint a fresh icon for the active charm.
charm_icon_generate agent tool Programmatic: the agent calls it during a BUILD phase, or you can /run charm_icon_generate ....
generate_icon (placeholder) Deterministic placeholder — coloured circle with the charm's initial. Shipped before Painter; still the right call when no image-provider API key is configured.

The Painter and the placeholder coexist intentionally: the deterministic placeholder is fast, free, and offline-friendly; the Painter is more interesting but costs money and needs an API key.

Use it from the chat

In an active session with a charm path set:

cantrip> /icon a Postgres database operator

The Painter prefixes a Charmhub style block to your description (square, flat, simple, legible at 32×32, no embedded text), calls the image provider, and writes the result. A typical reply:

Generated icon.svg for 'myapp' at /home/me/charms/myapp/icon.svg.
- model: gemini/imagen-3.0-generate-002
- cost: ≈ $0.0400 (session ≈ $0.0400 of $1.00 cap)
- format: PNG embedded in SVG; designer-polish recommended before release.

Cost cap

Iterating on an icon is fun but pricey: at $0.04 per Imagen call, ten attempts is forty cents. The Painter enforces a per-session cap via state.icon_max_session_cost_usd (default $1.00 — about 25 attempts). When the cap trips, further calls return a tool error naming the spent amount and the cap; raise the cap by setting state.icon_max_session_cost_usd from a slash command or in code.

There is no per-turn cap (icons aren't easy to spam from one user message); the session cap alone is enough.

Refusal to overwrite real artwork

The tool refuses to overwrite an existing icon.svg unless one of:

Anything else — your own SVG, a designer's work, an existing Inkscape file — is treated as expensive human output and left alone.

Switching providers

The defaults (gemini / imagen-3.0-generate-002) are settable per session via:

API key resolution mirrors the text Gemini provider: GEMINI_API_KEY or GOOGLE_API_KEY. Without one of those set, the Painter returns a clean "image provider not configured" error rather than crashing.

What the SVG actually contains

A Painter-generated icon.svg is a small XML document wrapping a base64-encoded PNG:

<?xml version="1.0" encoding="UTF-8"?>
<!-- cantrip-icon-generated: charm=myapp; raster — designer-polish recommended before release -->
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
  <image x="0" y="0" width="256" height="256" href="data:image/png;base64,iVBORw0KGgo..." />
</svg>

Charmhub accepts this format; so does any SVG-aware viewer. When you move on to a true vector icon, drop your replacement into the same path and the Painter's refusal logic protects it from the next iteration.

Known gaps

The MVP deliberately defers a few things: