Expand description
§Getting started
This page takes a UI plugin and a companion daemon from nothing to a packed
bundle. Every source file it refers to is a compiled example in the SDK’s
examples/ directory; the scaffolds standard-plugin new writes are those
examples with your plugin’s id.
§Tools
- Rust stable with the
wasm32-wasip2target:rustup target add wasm32-wasip2. - The CLI:
cargo install standard-plugin-cli --locked. It installsstandard-pluginandcargo-standard-plugin, socargo standard-plugin ...works too. - The SDK: every scaffold depends on
standard-plugin-sdk = "0.1"; a daemon that uses WASI files, sockets or HTTP writes{ version = "0.1", features = ["wasi"] }(daemon plugins).
Install the CLI from the same release line as the SDK a plugin depends
on, so its WIT package matches the SDK’s. Never commit a Cargo.lock
resolved with a local path patch
(agent guide).
- Optional:
jco(npm install -g @bytecodealliance/jco) to inspect the component’s JavaScript form. Viewers do not run it. Without itbuildsays it skipped that step.
standard-plugin new --sdk-path <dir> points a scaffold at a local SDK
checkout instead of the published crate.
§A UI plugin
standard-plugin new my-card --kind ui
cd my-cardThe scaffold is one crate:
| File | What it is |
|---|---|
Cargo.toml | A cdylib depending on standard-plugin-sdk, with size-tuned release settings |
src/lib.rs | The plugin: examples/clock_card.rs |
bundle/standard-plugin.json | The manifest: one sidebar.card surface in the cell model, no grants |
.gitignore | Ignores target/ and what build writes into bundle/ |
AGENTS.md, CLAUDE.md | Where a coding agent finds these guides for the SDK version in use |
Read src/lib.rs top to bottom; it is short:
- The crate is
#![no_std]. A UI plugin runs inside every viewer with no WASI, andstdwould import it;alloc(String,Vec,format!) is available throughstandard_plugin::prelude. #[standard_plugin::ui]on the type exports it as the component’sui-pluginworld. The type implementsUiPlugin:activatecreates theSurface<Cells>named in the manifest,framepaints and commits, andeventhears about resizes.framerepaints only when the second changes and asks for its next frame at the next second withframe.wake_at(...). The viewer callsframeonly when a frame is due and the card is visible, so the card costs nothing while hidden. See rendering.
Build, check and pack:
standard-plugin build # cargo build --release --target wasm32-wasip2, into bundle/, then check
standard-plugin check # again, without building
standard-plugin pack # target/standard-plugin/my-card-0.1.0.tar and its sha256
standard-plugin install # the same, then `standard plugin install` on your accountcheck parses the manifest with the same code the viewer’s host uses,
checks every grant has a reason, and checks the component imports nothing
outside the ui-plugin world; see grants for its warnings.
install uploads the package to your account’s plugin store, lists its
grants with their reasons and asks before it installs; every viewer signed
in to the account then runs the card (publishing). To try
a build in one development viewer without installing it:
STANDARD_PLUGIN_DIRS=$PWD/bundle standardTo test it without a viewer, drive it with standard_plugin::testing from
an ordinary #[test]; tests/testing_harness.rs
is a complete example.
§A companion daemon
A UI plugin is side-effect free: it runs once per open viewer, so anything it did would happen once per viewer. Work that must happen once belongs to a companion: a daemon plugin with the same id that the UI half asks through calls and that publishes results the UI half reads. Working together explains the pattern.
standard-plugin new builds --kind companion
cd buildsThe scaffold is a Cargo workspace with two crates, one plugin id:
| Path | What it is |
|---|---|
ui/src/lib.rs | The UI half: examples/together_ui.rs |
ui/bundle/standard-plugin.json | Kind ui, grant call:builds (with its reason) |
companion/src/lib.rs | The daemon half: examples/together_companion.rs |
companion/bundle/standard-plugin.json | Kind companion, grant process.exec:make |
How the halves cooperate:
- The companion publishes the build status as a durable value,
builds.status, and announces each change with the eventbuilds.changed. Both are in the plugin’s own namespace, so neither needs a grant. - The UI half reads
builds.statusinactivate, listens tobuilds.*, and repaints its card whenbuilds.changedarrives. - A click on the card is an effect, so the UI half acts only when
view::is_driving()says the user is controlling this viewer, and then insideclaims().once(...)so exactly one request goes out. It calls the companion’srebuildmethod (calls().call(..., Target::Singleton), grantcall:builds). - The companion’s
callhandler answers at once and spawns the build as a task on its executor, which runs when the host next drives the plugin: it runsmake(grantprocess.exec:make) and publishes the new status, which step 2 shows in every viewer.
Build, check and pack from the workspace root; the CLI finds both halves:
standard-plugin build
standard-plugin packBoth halves build, check, pack and install as one package, and both run
under standard_plugin::testing. Once installed, standardd runs the
companion on the machines its placement names
(daemon plugins), and the UI half’s
calls reach it there.
§Next
- The manifest and grants: what a plugin asks for.
- Rendering: cells, pixels, the shared buffer, the helpers.
- Working together: values, events, calls, live; many viewers; companions.