# Make a live wallpaper

> A scene behind your canvas whose creatures are your agents: build one, ship it in a package, publish it.

Source: https://hivemind.griiken.com/guide/live-wallpapers/

Make a scene of your own that runs behind your canvas, with one creature for each of your agents,
as Shoal's sharks are. You or your coding agent can take it from an idea to HiveHub with the
`hive` command.

<AgentPrompt description="Paste this into your coding agent to build a live wallpaper, use it as your background and publish it." page="live-wallpapers">{`
Build a Hivemind live wallpaper (a scene drawn behind the canvas in a sandboxed frame, one creature for each of my agents) and, if I want, publish it on HiveHub.

1. Ask me what the scene is: its world, what an agent's creature is, and what a creature does while its agent works, is ready, needs me, and rests. Ask its name (lowercase letters, digits and dashes).
2. Start it: \`hive plugins new wallpaper <name>\`. It writes a package of that name holding the wallpaper in \`wallpapers/<name>\`, with \`startup.wallpaper\` set. It takes the owner from the GitHub account \`gh\` is signed in as. If \`gh\` is not signed in, ask me for my GitHub login and pass \`--owner <login>\`. To add the wallpaper to a package I already have, pass \`--in <package>\` instead.
3. Read \`PROMPT.md\` in full, and the two skills it copied into the package's \`.claude/skills/\`: hivemind-live-wallpaper (the contract) and ascii-character-animation (the craft). Write the scene in \`src/main.ts\` from them. The wallpaper has no network and no permissions. It is told each agent's creature (never a name), where the tiles are, the colours, whether to run, and the pointer and clicks over empty canvas.
4. Where the starter's \`package.json\` is, run \`npm install\` once, then \`npm run build\` after each change, then \`hive plugins check <package>\`. It must say ok. It names the file and line of each problem, and loads the wallpaper in a headless browser to check it says ready and drawn and names only creatures it was sent.
5. Install it: \`hive plugins install <package>\`. Ask me before changing my background. On a yes, run the command the install prints for it (\`--json\` gives it as startWallpaper), or \`hive config set appearance.wallpaper.kind '"<the wallpaper's id>"'\`. Its id is \`@owner/package/<folder>\`.
6. Done means: it draws in my theme's two colours, light and dark; its creatures keep off my tiles; the creature of an agent that needs me goes to the open side of that agent's tile; it draws nothing on its own while held and one settled frame while still; and \`README.md\` says what it is.
7. Publish only if I say so. Commit the package with the wallpaper's \`dist/\`, push it to GitHub, and run \`hivehub publish <package>\`, as https://hivemind.griiken.com/guide/publish-to-hivehub.md says. If I must sign in, ask me to open the link and type the code myself.

At the end, tell me the wallpaper's id, the command others use to install it (\`hive plugins install @owner/package\`), and anything you could not check.
`}</AgentPrompt>

## What a live wallpaper is [#what-a-live-wallpaper-is]

A live wallpaper is a small web page that the app runs behind the canvas, in a sandboxed frame,
as it runs a [view](https://hivemind.griiken.com/guide/extension-authoring/). Shoal, Koi Pond, Jellyfish and Fireflies are the ones
that come with the app ([how to pick one](https://hivemind.griiken.com/guide/appearance/)). Yours ships in a
[package](https://hivemind.griiken.com/guide/packages/), and its id is `@owner/package/<folder>`.

What it is told is less than a view is:

- Each agent's creature: an id that means nothing outside the scene, the program (`claude`,
  `codex` …), a `kind` from 0 to 1 that is the same for every agent of one program, a `size`, and
  what the agent is doing: `working`, `ready`, `needs-you` or `resting`.
- Where the tiles are on the screen, and whether they are see-through.
- The colours to draw in, and whether to run, keep its last frame or show one settled frame.
- The pointer and clicks over empty canvas, and a turn an agent finished well.

It is never told an agent's name, a folder, a command, a tile's own id or anything a terminal
shows. It loads and fetches nothing from the network, has no permissions, never has the keyboard,
and cannot change the workspace. The two things it says back are that its first frame is up, and
which creature is under the pointer. While the pointer is over empty canvas, the app shows that
agent's name and state beside the pointer, and a click there goes to the agent's tile.

Like a view, its frame can still open a WebRTC connection, which no page policy in Chromium
stops, and so could send what it is told elsewhere. Install live wallpapers, like views, from
people you trust.

It is not drawn behind a view you installed, which lays its tiles out itself, nor on the
[phone page](https://hivemind.griiken.com/guide/phone/).

## Before you start [#before-you-start]

- Hivemind 2026.10.8 or newer, open on a project with a few agents.
- Node 22 or newer, for `npm`.
- You know a little TypeScript or JavaScript, or your coding agent does.

<Steps>
  <Step title="Start it">
    ```bash
    hive plugins new wallpaper pond
    ```

    It writes the package `pond/`. Its `hivemind-package.json` names the wallpaper in
    `wallpapers/pond` and offers it as the background once it is installed (`startup.wallpaper`).
    The wallpaper's folder holds its `hivemind-wallpaper.json` and a working scene in
    `src/main.ts`. A `PROMPT.md` and two skills in the package's `.claude/skills/`, one for the
    contract and one for drawing in characters, are there for your coding agent.

    The owner is the GitHub login `gh` is signed in as, or the one you pass with `--owner`. To
    add a wallpaper to a package you have, run `hive plugins new wallpaper pond --in kit`.

    Check: `hivemind-package.json` has `"startup": { "wallpaper": "@<your login>/pond/pond" }`.
  </Step>
  <Step title="Write the scene">
    The starter draws a working scene. The smallest one that runs on the app's clock looks like
    this:

    ```ts src/main.ts
    import { connectWallpaper } from "@hivemind/view-sdk";

    const wp = await connectWallpaper();
    const canvas = document.body.appendChild(document.createElement("canvas"));
    Object.assign(canvas.style, { position: "fixed", inset: "0" });
    const g = canvas.getContext("2d")!;
    const at = new Map<string, { x: number; y: number }>();

    // Sharp on any screen: the canvas is the viewport times its pixel ratio.
    wp.on("viewport", ({ w, h, dpr }) => {
      canvas.width = Math.round(w * dpr);
      canvas.height = Math.round(h * dpr);
      Object.assign(canvas.style, { width: `${w}px`, height: `${h}px` });
    });
    // An agent that closed: its creature goes.
    wp.on("creatures", (cs) => {
      for (const id of at.keys()) if (!cs.some((c) => c.id === id)) at.delete(id);
    });
    // Which creature is at a point: the app shows its agent's name, and a click opens its tile.
    wp.hitTest((x, y) => {
      for (const [id, p] of at) if (Math.hypot(p.x - x, p.y - y) < 12) return id;
      return null;
    });

    wp.animate({
      step(dt) {
        const { w, h } = wp.viewport;
        for (const c of wp.creatures) {
          const p = at.get(c.id) ?? { x: Math.random() * w, y: Math.random() * h };
          p.x = (p.x + (c.state === "working" ? 60 : 15) * dt) % w;
          at.set(c.id, p);
        }
      },
      draw() {
        const { w, h, dpr } = wp.viewport;
        const { page, text, attention, mono } = wp.look;
        g.setTransform(dpr, 0, 0, dpr, 0, 0);
        g.fillStyle = page;
        g.fillRect(0, 0, w, h);
        g.font = `16px ${mono}, monospace`;
        for (const c of wp.creatures) {
          const p = at.get(c.id);
          if (!p) continue;
          g.fillStyle = c.state === "needs-you" ? attention : text;
          g.fillText("@", p.x, p.y);
        }
      },
    });
    ```

    This one does not yet keep off the tiles or go to its agent's tile; the sections below say
    how. To have your coding agent write it, put your idea at the top of `PROMPT.md` and hand it
    the file.
  </Step>
  <Step title="Build and check it">
    Where the starter's `package.json` is, run `npm install` once. Then, after each change:

    ```bash
    npm run build              # writes dist/main.js, the entry its manifest names
    hive plugins check pond
    ```

    The check reads the package and every member as the install and HiveHub do, with the file and
    line of each problem. It also loads the wallpaper in a headless browser against a pretend app,
    and checks that it says it is ready, draws its first frame, and names only creatures it was
    sent.

    Check: it ends with `ok`. `--json` gives the same report to an agent.
  </Step>
  <Step title="Install it and use it">
    ```bash
    hive plugins install ./pond
    ```

    A package of live wallpapers, views, widgets and themes holds no code that runs outside a
    sandbox, so it arrives switched on. Because the package names its wallpaper in
    `startup.wallpaper`, the app offers it as your background once, and never switches without
    you. The install prints the command that switches to it; `--json` gives it as
    `startWallpaper`. To switch yourself:

    ```bash
    hive config set appearance.wallpaper.kind '"@<your login>/pond/pond"'
    ```

    Or open **Settings → Appearance** and pick it under **Background**, after the built-in
    scenes. Install again after each change.

    Check: your agents swim behind the canvas. Point at one and its agent's name shows.
  </Step>
  <Step title="Publish it">
    Commit the package with the wallpaper's `dist/`, push it to GitHub, and run
    `hivehub publish pond`. A package publishes as one listing: see
    [Publish a plugin](https://hivemind.griiken.com/guide/publish-to-hivehub/#publish-a-package). HiveHub lists a package that
    holds a live wallpaper as needing Hivemind 2026.10.8 or newer.
  </Step>
</Steps>

## What it is told, and what it says [#what-it-is-told-and-what-it-says]

The app and the wallpaper talk over one message port. Every message is a JSON object with a
`type`, checked on both sides. `@hivemind/view-sdk` speaks it for you: `connectWallpaper()`
waits for the app and gives you the values and their changes as `wp.on(…)`.

| The app sends | Holds | When |
|---|---|---|
| `hello` | everything below, at once | once, after it says `ready` |
| `viewport` | `{ w, h, dpr }`: its size in CSS pixels, and the screen's pixel ratio | the window is resized or moves to another screen |
| `look` | `{ page, text, attention, dark, colour, mono }`: the theme's page, text and needs-you colours as `#rrggbb`, whether the theme is dark, whether to draw in its own colours, and the terminals' font as a CSS font list | the look changes |
| `mode` | `run`, `hold` or `still` | see [the clock](#the-clock) |
| `creatures` | up to 8 of `{ id, agent, kind, size, state }` | an agent comes, goes or changes state |
| `tiles` | `{ tiles, seeThrough }`: each tile, and each panel that is not glass, as `{ x0, y0, x1, y1, id? }` in CSS pixels; `id` is the creature's id on its agent's tile | once a second while it runs, and at most once a frame while the canvas moves |
| `done` | `{ id }`: that creature's agent finished a turn well | a turn ends well |
| `pointer` | `{ x, y, over }`: the pointer over empty canvas, or gone from it | at most once a frame while it moves there |
| `click` | `{ x, y, id }`: a click on empty canvas, and the creature it opened, or `null` for open water | a click |

| It sends | Holds | The SDK sends it |
|---|---|---|
| `ready` | `{ protocol: 1 }` | in `connectWallpaper()` |
| `drawn` | nothing: the first frame is up, and the app shows the layer | after the first frame of `animate`, or with `wp.drawn()` |
| `hover` | `{ id }`, the creature under the pointer, or `null` | when what `hitTest` answers changes |

A creature's `size` is the same for its agent every time, from about 0.78 to 1. Pick what kind of
creature it is from `kind`, so all of one program's agents look alike.

## The clock [#the-clock]

`wp.animate({ step, draw, settle })` runs your scene on the clock the built-in scenes use:

- `step(dt)` moves the scene on by 1/60 of a second, as many times as the time that passed needs
  (at most four a frame; a busy computer drops the rest). The scene moves at the same speed
  whatever the frame rate.
- `draw(dt)` draws it as it is, at most 30 times a second. Characters read smooth at 30, and it
  costs half of 60. `animate(scene, { fps: 60 })` asks for more.
- `settle()`, if you give it, puts everything where it would be after a while. Without it, the
  clock runs 360 steps.

The mode decides what runs:

| Mode | When | What the clock does |
|---|---|---|
| `run` | the window is in use | steps and draws |
| `hold` | the window is hidden, unfocused, idle or covered | nothing. It draws one frame again only when something it shows changes: the size, the look, the creatures or the tiles |
| `still` | the person turned off **Animate the background**, or the computer asks for reduced motion | one settled frame: `settle()` and one draw. Again only when something it shows changes |

Its first frame is drawn at once, whatever the mode. Never start a timer or a
`requestAnimationFrame` loop of your own: while held, a wallpaper that keeps drawing spends the
battery on a window nobody sees.

## Keep off the tiles [#keep-off-the-tiles]

Your agents' tiles sit over the scene, and their text must stay readable. Use `tiles`:

- Pick where a creature goes in the open water between the tiles, and a way there that does not
  pass under one.
- Turn a creature away before it goes under a tile. One a tile was put down over goes out by the
  nearest side, quickly.
- Draw nothing of a creature under a tile. When `seeThrough` is true the tiles are frosted glass,
  and a creature under one may show through, soft and faint.

When an agent needs you, its creature goes to that agent's tile: the tile in `tiles` whose `id`
is the creature's. It holds by the open side, the side with more room clear of the other tiles,
else the other side, else above or below. When the tile is not on the screen, the creature stays
in the open water.

## Hover and click [#hover-and-click]

`wp.hitTest((x, y) => id)` answers which creature is at a point of the viewport, or `null`. While
the pointer is over empty canvas, the app shows that creature's agent's name and state beside it,
and a click there goes to the agent's tile. The wallpaper never learns the name, and the app
takes `hover` only for a creature it sent.

Every click on empty canvas comes as `click`. Its `id` is the creature the app opened, or `null`
for open water: answer a click on open water in your scene, with a ripple or a scatter.

The built-in scenes answer the pointer the same way.

## Colours [#colours]

- **Your theme's colours.** By default draw in `page` and `text` and the steps between them, so
  the scene follows every theme. `dark` says which way round: light creatures on a dark page, or
  ink on paper on a light one. Keep `attention`, the theme's colour for an agent that needs you,
  for that alone.
- **Its own colours.** With `"colour": true` in its manifest, Settings shows the **Colour** switch
  for it, and `look.colour` says when the person turned it on. Then draw in your scene's own
  colours: one set for a dark theme and one for a light one.
- **Characters.** `mono` is the terminals' font, as a CSS font list. It is the person's choice and
  may not be on this computer, so end it with a fallback: `` `${mono}, monospace` ``.
- **Panel text over it.** With glass on, the Layers panel and the toolbars are frosted glass, and
  their text sits over your scene. Keep the scene dark enough on a dark theme, and light enough on
  a light one, that the text reads over every part of it. Test it: the built-in scenes keep the
  Layers panel's text at 7:1 and its secondary text at 4.5:1 over every point of their water.

## hivemind-wallpaper.json [#hivemind-wallpaperjson]

| Field | Needed | Takes |
|---|---|---|
| `protocol` | yes | `1` |
| `name` | yes | up to 60 characters: the name in **Settings → Appearance** |
| `entry` | yes | an `.html` page or a `.js` script inside the folder, usually `dist/main.js`. A script is put in a page of the app's own |
| `description` | no | one sentence, up to 200 characters, shown under its name |
| `colour` | no | `true` when it draws in colours of its own with **Colour** on |
| `preview` | no | a `.png`, `.jpg` or `.webp` inside the folder, up to 512 KB, shown in the picker |

```json
{
  "protocol": 1,
  "name": "Pond",
  "entry": "dist/main.js",
  "description": "Koi under lily pads, one for each agent.",
  "colour": true,
  "preview": "preview.png"
}
```

The folder's name is the last part of its id: lowercase letters, digits and single dashes.

## Ship it in a package [#ship-it-in-a-package]

A live wallpaper always comes in a package, which names it in `hivemind-package.json`:

```json
"wallpapers": [{ "path": "wallpapers/pond" }],
"startup": { "wallpaper": "@you/pond/pond" }
```

`startup.wallpaper` names one of the package's own wallpapers. Once the package is installed and
on, the app offers it as the background, once per package, as `startup.view` offers a view. The
person chooses; nothing switches without them.

A package may hold a live wallpaper alone, or beside a view and a theme that go with it.
Archipelago (`@dip497/archipelago` on HiveHub) ships its view with its widgets and a theme, and
offers the view with `startup.view`; a wallpaper is offered the same way. Give `startup` both a
view and a wallpaper, and the app offers both. The rest of what a package holds, and how to
update and roll it back: [Packages](https://hivemind.griiken.com/guide/packages/).

## What turns it off [#what-turns-it-off]

The app turns a live wallpaper off for the session, shows a notice, and draws the plain
background instead, when it:

- sends 8 messages that are malformed, or that name a creature the app did not send;
- sends more than 120 messages a second;
- asks for a newer protocol than this Hivemind speaks;
- takes the keyboard (focuses anything in its frame);
- uses 60% of a CPU core for six seconds.

The scene runs in a process of its own, so it never slows your terminals, but a busy one is still
turned off. Measure what a frame costs: the hivemind-live-wallpaper skill in your package's
`.claude/skills/` says how.

## Limits [#limits]

| What | Most |
|---|---|
| Creatures | 8. With more agents, the ones that need you come first, then the ones at work |
| Tiles in one `tiles` message | 256 |
| Live wallpapers in a package | 8 |
| A preview picture | 512 KB |
| Frames a second | 30, or what `fps` asks, up to 60 |
| Waiting for the app | `connectWallpaper()` gives up after 10 seconds |

## Commands [#commands]

```bash
hive plugins new wallpaper <name>              # a package holding a working wallpaper, with PROMPT.md
hive plugins new wallpaper <name> --in <pkg>   # one more in a package you have
hive plugins check <package>                   # the manifests, and the wallpaper loaded once
hive plugins install <package>|@owner/package
hive config set appearance.wallpaper.kind '"@owner/package/name"'
hivehub publish <package>
```

The `hive plugins` commands are in the [CLI reference](https://hivemind.griiken.com/guide/cli/#plugins).

<LastValidated />
