# Write a view

> Build your own way of drawing the workspace: a small web page that lists, groups or charts your agents.

Source: https://hivemind.griiken.com/guide/extension-authoring/

Build a <Tooltip tip="A way of drawing the workspace: Canvas, Windows, or one you install." cta="Glossary" href="https://hivemind.griiken.com/guide/glossary/#view">view</Tooltip> that lists every tile with a dot for its status, then change it. It takes about fifteen minutes.

<AgentPrompt description="Paste this into your coding agent to build a view, switch Hivemind to it and publish it." page="extension-authoring">{`
Build a Hivemind view (a sandboxed web page that draws the whole workspace its own way) and, if I want, publish it on HiveHub.

1. Ask me what the view should show and do, and its name (lowercase letters, digits and dashes).
2. Start it: \`hive plugins new view <name>\`. 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>\`. Then run \`npm install\` in the new folder (it needs Node).
3. Read \`PROMPT.md\` in full: it is the whole contract (the SDK, permissions, the sandbox). Write what I asked for under its "What I want", and build only from it. The view has no network, reads only its own files and has no \`window.hive\`. It uses \`@hivemind/view-sdk\`, which the app serves, so do not bundle it.
4. Check it, then build and install it, again after each change: \`hive plugins check .\` must say ok (it names the file and line of each problem), then \`npm run dev\`. Check it prints "installed @owner/name".
5. Show it: \`hive ctl plugin open view:@owner/name\`. If \`hive plugins list\` shows it off, run \`hive plugins enable view:@owner/name\` first.
6. Declare one command in \`hivemind-view.json\` under "commands", answered with \`hm.onCommand\`, that reports what the view shows. Check it with \`hive ctl plugin call @owner/name <command>\`.
7. Done means: \`hive plugins list\` shows it on with no problem, it follows the light and the dark theme (\`applyThemeVars\`), the manifest has a one-line description, a "category" and only the permissions it uses, and \`README.md\` says what it does.
8. Publish only if I say so. Commit \`dist/\` with the source, because HiveHub publishes the pushed commit. Follow the "Publish a plugin" page (https://hivemind.griiken.com/guide/publish-to-hivehub.md) and run \`hivehub publish dist\`. If I must sign in, ask me to open the link and type the code myself.

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

## Views others have built [#views-others-have-built]

Each is on HiveHub as `@dip497/<name>`.

<CardGroup cols={2}>
  <Card title="constellation" img="https://hivemind.griiken.com/guide/images/shots/showcase/constellation.webp">Your agents as stars, grouped by project.</Card>
  <Card title="timeline" img="https://hivemind.griiken.com/guide/images/shots/showcase/timeline.webp">Today's work, one row per agent.</Card>
  <Card title="agents-by-state" img="https://hivemind.griiken.com/guide/images/shots/showcase/agents-by-state.webp">A board with one column per state.</Card>
  <Card title="workspace-map" img="https://hivemind.griiken.com/guide/images/shots/showcase/workspace-map.webp">Frames, worktrees, agents and issues on one map.</Card>
</CardGroup>

## What you'll learn [#what-youll-learn]

- How to start a view from the starter and switch the window to it.
- How a view hears the workspace change and shows each agent's status.
- How to rebuild it and see the change.

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

- Hivemind is installed and open on a project with a few tiles. See [Install and run your first agent](https://hivemind.griiken.com/guide/getting-started/).
- Node 22 or newer, for `npm`.
- You know a little TypeScript or JavaScript.

<Steps>
  <Step title="Start the view">
    ```bash
    hive plugins new view pulse
    ```

    Check: it prints `created @<your login>/pulse`. If it asks for an owner, add
    `--owner <your GitHub login>`.

    ```text
    pulse/
      hivemind-view.json   the view's name, id, version and permissions
      index.html           the page, which loads ./main.js
      src/main.ts          your code
      PROMPT.md            the whole contract, for a coding agent
      dist/                what you install and publish, once built
    ```
  </Step>
  <Step title="Build and install it">
    ```bash
    cd pulse && npm install
    npm run dev
    ```

    Check: the last line says `installed @<your login>/pulse`. `npm run dev` builds `dist/` and
    installs it in one go.
  </Step>
  <Step title="Switch to it">
    Open the view menu in the toolbar (it shows **Canvas**) and pick **Pulse**.

    Check: the window shows **Pulse** and one row per tile, each with a dot for its status. Press a
    row and Hivemind selects that tile.
  </Step>
  <Step title="Read the code">
    `src/main.ts` connects, follows your theme, and redraws when tiles change:

    ```ts src/main.ts
    import { applyThemeVars, connect, statusTone } from "@hivemind/view-sdk";

    const hm = await connect();
    applyThemeVars(hm);

    const list = document.querySelector("ul")!;
    let unsubscribe: (() => void)[] = [];

    hm.on("structure", ({ tiles }) => {
      for (const off of unsubscribe) off();
      list.replaceChildren();
      unsubscribe = tiles.map((tile) => {
        const dot = Object.assign(document.createElement("span"), { className: "dot" });
        const button = document.createElement("button");
        button.append(dot, tile.name);
        button.onclick = () => hm.commands.selectTile(tile.id);
        const item = document.createElement("li");
        item.append(button);
        list.append(item);
        return hm.subscribeStatus(tile.id, (s) => { dot.dataset.tone = statusTone(s); });
      });
    });
    ```

    `structure` comes again whenever frames or tiles change. The app serves `@hivemind/view-sdk`
    to every view, so your bundle leaves it out. To draw a state the way the app does, a shape
    per state in its colour (only "needs you" pulses), put `statusMarkSvg(statusTone(s))` in the row
    instead of the dot; `STATUS_MARKS` holds each state's word and shape.
  </Step>
  <Step title="Change it">
    Add this line inside the `structure` handler, after `list.replaceChildren();`:

    ```ts
    document.querySelector("h1")!.textContent = `${tiles.length} tiles`;
    ```

    Run `npm run dev` again.

    Check: the heading now counts your tiles. Open a terminal and the count goes up.
  </Step>
</Steps>

## What you did [#what-you-did]

- You started a view with `hive plugins new view` and installed it with `npm run dev`.
- You switched the window to it from the toolbar's view menu.
- You read the workspace with `hm.on("structure")` and each agent's status with `hm.subscribeStatus`.

## Next [#next]

- Want an agent to build the rest? Write your idea at the top of `PROMPT.md` and hand it the file.
- Share it: [Publish a plugin](https://hivemind.griiken.com/guide/publish-to-hivehub/) on HiveHub.
- Ship it with your other tools, widgets and views as one: [Packages](https://hivemind.griiken.com/guide/packages/).
- A real example with a saved layout and agent commands:
  [`views/agent-board`](https://github.com/dip497/hivemind-plugins/tree/main/views/agent-board).
- Something wrong? See [Troubleshooting](https://hivemind.griiken.com/guide/troubleshooting/).

## View reference [#view-reference]

### What a view can use [#what-a-view-can-use]

Check `hm.supports("<feature>")` before using a newer feature.

| Need | Call |
|---|---|
| Put live terminals in your layout | `hm.setSurfaceRects(...)` (`surfaces`) |
| Show what a tile last said, or what its agent waits on, with Review | `hm.setLineRects([{ tileId, x, y, w, h }])` (`lineSlots`): the app draws it in the room you leave, at least 20px high, and its own **Review** for a permission (never Allow in your room); your view never gets the text |
| Save your layout (64 KB, per workspace) | `hm.setLayout(blob)` |
| An agent's status as it changes | `hm.subscribeStatus(tileId, (status, info) => …)` |
| Show a tile when someone asks to see it: a widget's button, a notification's **View**, `hive ctl focus` | `hm.onReveal((tileId) => …)`: move your camera to it and answer where it is drawn (`{ x, y, w, h }`), or `null` for a tile you do not draw |
| What happens: replies, waits, closes | `hm.onEvents(["turn", "needsInput"], cb)` |
| How busy a terminal is (0 to 3) | `hm.activity(tileId, cb)` |
| A day's history per tile | `await hm.history("2026-09-23")` |
| Installed agents, past sessions | `await hm.agents()`, `await hm.sessions("claude", frameId)` |
| Start an agent | `hm.commands.spawnAgent(agent, frameId, { prompt, resume })` |
| Suggest a prompt (the person confirms) | `await hm.prompt(tileId, "run the tests")` |
| Who else is here | `hm.onParticipants(cb)` (`participants`) |
| The canvas's widgets | `hm.onWidgets(cb)`, `hm.pressWidget(id, action)` (`widgets`) |
| Keep clear of the widget cards over your view | `hm.onWidgetBand(({ edge, width }) => …)` (`widgetBand`): how far the cards reach in from your widget edge, the gap beside them included, or 0 when there are none. It comes at once, then each time cards come or go, start a second column, or the window is resized |
| Issues | `hm.onIssues(cb)`, `hm.issues.read(id)`, `create`, `update`, … (`issues`) |
| Its settings | `hm.settings`, `hm.on("settings", cb)` (`settings`) |
| Draw your own toolbar | `"toolbar": "off"`, then `hm.commands.openHostMenu("views", rect)`, `"add"`, `"appearance"` or `"settings"` opens the app's own (`hostMenu`) |
| Messages from your scripts | `hm.onCustom("ci.*", cb)`, sent by `hive ctl view emit ci.build '{…}'` |

Each tile in `structure` carries its `icon` (an SVG in `currentColor`) and its `agent`. Each frame
carries its `folder`, `branch`, `parentId` (a worktree under its project) and `machine` when it
runs on another computer.

A view never reads an agent's replies or terminal text, and never answers its permission
questions. Every prompt a view writes opens a dialog showing the full text, with **Cancel** focused.

### Colours [#colours]

`applyThemeVars(hm)` sets the app's theme on your page as CSS variables, and again each time it
changes. 15 colours come from the app and the phone page, each as `--hm-color-<name>`; what each
is for is in [the app's colours](https://hivemind.griiken.com/guide/plugin-settings/#the-apps-colours). A release that removes or
renames one says so with **Breaking:** in the CHANGELOG. A colour added later reaches only newer
apps, so give it a fallback: `var(--hm-color-new, #888)`.

### Permissions [#permissions]

A view declares its permissions in `hivemind-view.json`. An unknown one is refused at install.
Selection and focus need none.

| Permission | Allows |
|---|---|
| `workspace:spawn` | start tiles, agents and frames |
| `workspace:close` | close tiles |
| `workspace:edit` | rename tiles, pick a frame's folder |
| `workspace:prompt` | `prompt`, and `spawnAgent` with a prompt |
| `workspace:sessions` | `sessions`, and `spawnAgent` with `resume` |
| `workspace:widgets` | press and click widgets |
| `workspace:issues` | read and change issues |

### Manifest fields [#manifest-fields]

| Field | Does |
|---|---|
| `id`, `name`, `version` | its id in your GitHub scope (`@you/name`), its name, its version |
| `"description"` | one line under its name wherever views are listed, at most 200 characters |
| `"icon"` | an `.svg` in the view's folder (`"icon.svg"`), drawn in one colour like the app's icons, at most 32 KB |
| `entry` | the page to load, usually `index.html` |
| `protocol` | the view protocol it speaks: `1` |
| `permissions` | what it may do (above) |
| `"settings"` | fields the app draws on the view's Settings page: see [Plugin settings](https://hivemind.griiken.com/guide/plugin-settings/) |
| `"tokens"` | colours of its own that follow the theme and the person can change: see [A view's own colours](https://hivemind.griiken.com/guide/plugin-settings/#a-views-own-colours) |
| `"files"` | `true` when it `fetch`es its own files; without it every `fetch` is refused |
| `"commands"` | what an agent can run in it (`hm.onCommand(name, handler)`), optionally with `"keys": "mod+alt+k"` |
| `"toolbar"` | `"top"`, `"bottom"` (the default), `"hidden"` or `"off"` |
| `"widgets": "left"` | start widget cards down the left edge over the view instead of the right, for a view that keeps its own panels on the right; a card the person moved stays put. `hm.onWidgetBand` says how much room they take |
| `"layout": "personal"` | keep the layout per person instead of shared |
| `"layers": true` | show the app's Layers panel beside the view |
| `"phone": true` | offer it on the phone page; use `hm.device.compact` and `hm.device.touch` |
| `"category"` | where it is grouped in the Add menu and on HiveHub |
| `"description"` | what it shows, in one line: the line on its card on HiveHub |

### The sandbox [#the-sandbox]

The page runs on its own `hm-view://<id>` origin with a strict content policy: no `window.hive`,
no Node, no network, no inline scripts, and no notifications, camera, microphone or location.
It loads its scripts, styles, pictures and fonts from its own files only, and may `fetch` them
when its `hivemind-view.json` says `"files": true`: `fetch("./levels.json")` works, and another
view's files or any other address are refused.
Malformed messages, floods or runaway CPU switch it off for the session and return you to the
canvas.

- Name your files relatively (`./main.js`, `fetch("./data.json")`): in a browser tab the app
  serves views under a path.
- Fetching its own files needs Hivemind 2026.10.6 or newer, and works in the app and a browser
  tab. Check `hm.supports("ownFiles")` before you fetch: on the phone page, and in a window that
  shows another computer's view, it is refused, so bundle what those must show.
- Draw on change, and stop work while `document.hidden`.
- A form's `submit` event fires, in the app and on the phone page, and the form goes nowhere:
  handle the event and call `preventDefault()`.
- Test without the app with `fakeHost()` from `@hivemind/view-sdk/testing`.

### Commands [#commands]

```bash
hive plugins new view <name> [--dir folder]   # a starter, with PROMPT.md
hive plugins check <folder>                   # its types, its manifest, and its page built and loaded once, with file:line
hive plugins install <folder>|@owner/name
hive plugins list --kind view                 # installed views, and why any fails to load
hive plugins remove <id>
```

<LastValidated />
