standard-plugin-sdk 0.1.2

Write Standard Code plugins in Rust: wasm components against standard:plugin@2.0.0
Documentation
# The manifest

Every plugin carries `standard-plugin.json` beside its component. The CLI
keeps it at `bundle/standard-plugin.json` (or `standard-plugin.json` beside
`Cargo.toml`, copied into `bundle/` by `build`). The viewer's host, the CLI
and the SDK's test host all parse it with `standard-plugin-manifest`, so a
manifest `standard-plugin check` accepts is one the host accepts.

```json
{
  "apiVersion": 2,
  "kind": "ui",
  "id": "git-status",
  "version": "0.3.1",
  "module": "git_status.wasm",
  "surfaces": [
    { "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 3 },
    { "id": "footer", "anchor": "pane.footer", "model": "cells" }
  ],
  "grants": {
    "account.read": "Shows which pane each branch belongs to",
    "values.read:git.*": "Reads the branch state the git companion records"
  },
  "memoryMib": 16
}
```

## Fields

| Field | Required | Meaning |
| --- | --- | --- |
| `apiVersion` | yes | `2`: this runtime. Anything else is rejected. |
| `kind` | yes | `ui`, `daemon` or `companion` (below). A viewer loads `ui` only. |
| `id` | yes | 1 to 64 lowercase letters, digits and dashes, not starting with a dash. The plugin's namespace: its own keys, live keys and events are `<id>.<rest>`. |
| `name` | no | The name the viewer shows for the plugin: card titles, the plugin store and the Plugin settings view. 1 to 64 characters, no control characters. Absent: the `id`. The halves of a companion pair agree or one leaves it out; `pack` records it in the package. |
| `version` | yes | The plugin's version, shown to the user. |
| `module` | yes | The component's file name inside the bundle (no directories). |
| `surfaces` | no | Where a `ui` plugin paints (below). Other kinds declare none. |
| `grants` | no | Grant to reason: every capability beyond the plugin's own namespace, each with one sentence for the install screen. See [grants](grants.md). |
| `memoryMib` | no | Linear memory the plugin may use; the host caps it (64 MiB today). |
| `config` | no | The configuration's JSON Schema (below): its settings show in the Plugin settings view, and both halves receive their configuration with its defaults filled in. |
| `daemon` | no | Where a `daemon` or `companion` runs: `{ "placement": "fleet" \| "singleton", "failover": bool }` (below). Not allowed on a `ui` plugin. |

Unknown fields are rejected, in the manifest and in each surface.

Limits: the manifest is at most 64 KiB and the component at most 8 MiB.

## Kinds

| Kind | World | Runs |
| --- | --- | --- |
| `ui` | `ui-plugin` | In every viewer signed in to the account, sandboxed; paints its surfaces; side-effect free. |
| `daemon` | `daemon-plugin` | Inside `standardd` on the machines it is enabled on; never renders. |
| `companion` | `daemon-plugin` | A daemon plugin with the same `id` as a `ui` plugin, answering its calls (see [working together](working-together.md)). |

A companion pair is two bundles with one id: the UI half (kind `ui`) and
the daemon half (kind `companion`). `standard-plugin new --kind companion`
scaffolds both.

## Where a daemon runs

```json
"daemon": { "placement": "singleton", "failover": true }
```

| Field | Meaning |
| --- | --- |
| `placement` | `fleet` (the default): on every daemon of the account. `singleton`: on one machine, the one holding the plugin's lease, pinned when it is installed (`standard plugin install --machine`, or the Plugin settings view). |
| `failover` | A singleton only: when its machine goes offline the account moves it to another eligible daemon, and back when the pinned machine returns. |

`pack` writes both into the account manifest's `daemon` entry
(`daemon.placement`, `daemon.failover`), which is what the account places
by; a daemon half without the section packs as `fleet`.

## Surfaces

```json
{ "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 2 }
```

| Field | Meaning |
| --- | --- |
| `id` | Lowercase letters, digits and dashes; unique in the manifest. The plugin opens it with `Surface::new(id)`. |
| `anchor` | Where it renders (below). The viewer decides size and placement. |
| `model` | `cells` (graphemes, colours, attributes per cell) or `pixels` (RGBA8), or both in preference order: `["pixels", "cells"]` paints pixels where the viewer has real graphics and cells where it has not ([`AnySurface`](rendering.md#two-models)). See [rendering](rendering.md). |
| `input` | `true`: a click gives the surface input focus; keys and pastes go to it until Escape or a click elsewhere ([input](input.md)). A `stage` and a `panel.popover` always take input. |
| `height` | Rows a `sidebar.card`, `machine.after`, `project.after`, `project.before` or `panel.popover` asks for. A machine or project row may say 0: it starts with no rows (below). Ignored (and warned about by `check`) on other anchors. |
| `width` | Columns a `panel.popover` or `column` asks for. Ignored on other anchors. |
| `title` | The title the viewer shows on the surface's chrome (a card's top border or title row), at most 64 characters. Absent: the plugin's `name`, else its `id`. `""` shows no title; a plugin that is not running still shows its name and status there. |
| `border` | `sidebar.card` only. `true` (the default) draws a box; `false` draws the card without one: its title row, when it has a title or a label, then its rows, at the columns a boxed card's rows take. `"hover"` keeps the box's rows but shows its lines only while the pointer is over the card or it has focus. |
| `hovers` | `panel.hover` only, and required there: the id of the surface it shows beside. |
| `over` | `pane` only: another `pane` surface of the plugin that this one slides up over, as a child pane slides over its parent, instead of standing in the pane strip. Opening it opens that pane first; opening it again, or its Close, slides it back down. |

A surface may also ask for a size at run time, `cx.request_size(id, cols,
rows)` (or `Surface::request_size`; 0 in a dimension keeps the viewer's
choice): it overrides `height`/`width`, the viewer clamps it to the anchor
and to the space where it shows the surface, and the plugin hears a
`Resize` when its size changes. A card grows to its content; a popover fits
what it shows.

A `machine.after`, `project.after` or `project.before` instance may take no rows at all:
`request_size(0, 0)` (or `"height": 0` in the manifest) and the viewer draws
nothing and reserves no row for it, so a machine or project with nothing to
show keeps no empty line. (Elsewhere 0 rows still means the viewer's
choice.) An instance at zero rows is hidden and hears no resize; the plugin
asks by its id, `row@<machine>` or `row@<project>`, for rows again
(`Instances::instance(owner)` gives the surface before the viewer has shown
it), and the viewer shows it with a `Resize` and `Visibility`.

| Anchor | Where | Size |
| --- | --- | --- |
| `sidebar.card` | A card in the sidebar's plugin region, boxed unless `border` is `false`. | The sidebar's width; `height` rows (64 at most). |
| `machine.after` | Rows at the foot of every machine's card in the sidebar, on that machine's tint: one instance per machine, `<surface>@<machine>` (below). | The card's width; 1 row, or `height`, 4 at most. |
| `project.after` | Rows under every project in the sidebar, after its panes (also while the project is collapsed): one instance per project, `<surface>@<project>` (below). | The card's inner width; 1 row, or `height`, 4 at most. |
| `project.before` | Rows right under every project's name in the sidebar, above its panes (also while the project is collapsed): one instance per project, `<surface>@<project>`, as for `project.after`. | As `project.after`. |
| `pane.footer` | A footer row in every pane the viewer shows: one instance per pane (below). | The pane's width; 1 row unless requested, 8 at most. |
| `pane.header` | A header row in every pane the viewer shows, the same way. | As a footer. |
| `stage` | The plugin stage: the workspace slides away and the surface fills it. It opens from one of the plugin's `commands` or from `surface.open` during a user gesture; Escape closes it. | The workspace. |
| `panel.hover` | A tooltip: a box beside the surface named by `hovers` (every instance of it), shown while the pointer is over that surface and gone when it leaves. It takes no input; the plugin paints it from the `enter` and `leave` its target hears. | `width` by `height` (or requested), 80 by 16 at most. |
| `pane` | A workspace pane beside terminal panes: tabs, focus, moves and width work as theirs, and Close or the pane close key closes it (it falls out, as a closed terminal pane does). It opens like a stage (a command's `opens`, or `surface.open` during a gesture): first in the strip, with focus, growing out of the plugin's card; opening it again hides it back into the card when it shows whole in the workspace, and scrolls it into view with focus when it is open but out of view. Opened by the viewer that holds the controller lease (outside priority mode), it is part of the account's workspace group and shows in every viewer; opened by any other viewer, it stays in that viewer. | The pane's body: its slot less the frame. |
| `column` | A full-height column attached to the sidebar's edge; the workspace narrows beside it. It opens like a stage (a command's `opens` toggles it) with input focus, belongs to the viewer, and closes on Escape while it has focus or `surface.close`. It slides out of the sidebar and back under it (at once with animations off). Like the stage, one column shows at a time across every plugin: opening another slides the open one shut first. Opened from a press on a project row, a pane's footer or header, or a machine row, it takes that owner's identity colour, with an arrow from that row into it; opening it again from another row moves it there. | `width` (or requested) columns, 48 by default, beside its gutter and rule, leaving the workspace a full pane slot or at most 60% of it; the body's height less a title row. |
| `pane.overlay` | A see-through surface over one pane's body, opened by a `pane` command (its `opens`, or `surface.open` of `<surface>@<pane>` during a gesture): the pane recedes beneath it, and a cell with no grapheme and a default background shows the pane. It takes keys until Escape and the pointer over it; it goes when the pane closes. One instance per pane, `<surface>@<pane>`. | The pane's body. |
| `panel.popover` | A box beside the sidebar card or row whose press or key opened it (over an open column, whose width never moves it), below or above the pressed cell when a press elsewhere (a column's button) opened it, else centred over the workspace. It opens like a stage (a command's `opens`, or `surface.open` during a gesture) and takes keys and the pointer; Escape or a click outside it closes it. | `width` by `height` (or requested), 120 by 40 at most, within the workspace. |

### Pane, machine and project instances

A `machine.after` surface has one instance per machine the sidebar shows,
named `<surface-id>@<machine-id>`; `view::surface_machine(id)` names its
machine and `view::surface_tint(id)` its tint. A machine that leaves the
sidebar resizes its instance to nothing. A `project.after` surface works the
same way per project, `<surface-id>@<project-id>`:
`view::surface_project(id)` names the project (the id `account::state()`
lists) and `view::surface_tint(id)` its tint. Pane instances work the same
way:

A `pane.footer` or `pane.header` surface has one instance per pane the
viewer shows, named `<surface-id>@<pane-id>`. Each instance arrives as its
own `Event::Resize` and `Event::Visibility`; `view::surface_pane(id)`
names its pane and the pane's generation now (`PaneRef { id, generation }`).
A restarted pane keeps its id and advances its generation, so state kept
for one run of a pane is keyed `PaneRef::key()` (`<pane>@<generation>`, as
in `git-status.<machine>.<pane>@<generation>`) and is not shown for the
next. `account::Pane` carries the same `generation` (0 while unknown). A
pane that closes resizes its instance to nothing.
`Instances<Cells>` follows all of this for the plugin:

```rust,ignore
let mut footers = Instances::<Cells>::new("status"); // in activate
footers.event(&event);                               // in event
for (pane, footer) in footers.iter_mut() { /* paint */ }
```

## Commands

```json
"commands": [{ "id": "play", "title": "Play pong", "opens": "court" }]
```

Each command shows in the command palette (grouped under Plugins). `id` is
lowercase letters, digits and dashes and unique; `title` is 1 to 64
characters; `opens`, when given, names a `stage` or `panel.popover` surface the viewer opens
before the plugin receives `Event::Command(id)` (a `panel.popover` opens
the same way). Running a command is a
user gesture: the plugin may call `cx.open(surface)` while handling it.

`"context": "pane"` makes a command act on one pane. The palette offers it
when a pane is selected, and every pane's menu lists it after the
viewer's own items. The viewer shows and focuses the pane before the
plugin hears the command, so `view::focused_pane()` names it. Its `opens`
may name a `pane.overlay` surface, which opens over that pane
(`<surface>@<pane>`); a stage or popover opens as for any command.

## Configuration

```json
{
  "apiVersion": 2,
  "kind": "ui",
  "id": "quota-meters",
  "version": "2.0.0",
  "module": "quota_meters_ui.wasm",
  "surfaces": [{ "id": "card", "anchor": "sidebar.card", "model": "cells", "height": 3 }],
  "config": {
    "type": "object",
    "properties": {
      "refreshSeconds": { "type": "integer", "title": "Refresh every (seconds)",
                          "minimum": 300, "maximum": 1800, "default": 300 },
      "enabledProviders": { "type": "array", "items": { "type": "string", "enum": ["claude", "codex"] },
                            "default": ["claude", "codex"] }
    },
    "additionalProperties": false
  }
}
```

`config` is a JSON Schema in a subset every tool agrees on
(`standard_plugin_manifest::config_schema`): the root is an `object` with
`properties` (and optionally `required` and `additionalProperties`); each
property has a `type` (`string`, `number`, `integer`, `boolean`, `array`,
`object`) and may carry `title`, `description`, `default`, `enum`,
`minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `items` and
nested `properties`, four levels deep at most. An unknown keyword or a
`default` the schema itself refuses is an error, so `check` finds a typo
before a user does. Both halves of a companion carry the same schema
(`pack` refuses halves that differ). The configuration document a plugin
receives (`activate`'s argument, `config::get()`) is the account's with
every top-level `default` filled in; the Plugin settings view lists each setting
with the value in effect. The schema replaces a sidecar
`config.schema.json`.

The Plugin settings view edits a plugin's settings (`S`): Enter switches a
`boolean`, steps a setting with an `enum` to its next value, and opens any
other for typing (an array of strings as `a, b`, an `object` as JSON); R
returns a setting to its default. The viewer checks the whole document
against the schema before it sends it, and the account stores it as the
plugin's configuration: every half starts again with it. `pack` copies
the schema into the package's `manifest.json` (`config`), and the account
records it with the install, so a viewer lists the
settings of a daemon-only plugin too.

## What `check` adds

Beyond what the host enforces, `standard-plugin check` reports every bad
grant at once, checks the built component against its kind's world
(imports and exports), and warns about a UI plugin with no surfaces,
`height` on an anchor that ignores it, grants nothing uses, interfaces no
grant covers, grants on the plugin's own namespace, and the double-play
pattern ([grants](grants.md#what-check-warns-about)).