Plugin API reference
Everything a Zumba plugin can declare, export and call, for API
version 0.2. The interface is defined in zumba-plugin.wit; any language
that compiles to a WebAssembly component can implement it. In Rust, build from the
interface file with wit-bindgen. Inside the Zumba
repository there's also a Rust SDK (zumba-plugin-sdk) with
the same types under Rust names (run-command →
run_command, insert-text →
Action::InsertText), walked through in Creating a plugin;
installing and the bundled plugins are covered in Plugins.
How a plugin runs
- A plugin is a WebAssembly component (
wasm32-wasip2). Each plugin runs in its ownzumba-plugin-hostprocess, outside the editor, with no filesystem, network, environment or arguments of its own. It reaches the outside world only through the host functions, and only those its capabilities allow. activateruns once when the plugin loads: at startup, after installing, and after enabling it.- One instance serves every call afterwards, so state kept in statics
(e.g.
thread_local!) survives between calls. Calls to one plugin never overlap. - If a call traps (a panic, too much memory, out of fuel, over time),
that call fails with the reason in the status bar and the plugin
restarts with a fresh instance: its state is gone.
activateis not called again; the contributions from the first activation stay. - If the host process itself dies or stops answering, the editor stops it, shows the plugin as failed in Settings → Plugins, and starts it again the next time one of its commands runs.
Manifest:
zumba-plugin.toml
id = "word-count" # unique; also the install folder name
name = "Word Count" # shown in Settings and the palette
version = "0.1.0"
api = "0.2" # must equal the editor's API version
description = "Counts words in the selection or the active file."
capabilities = ["commands", "panels"]
wasm = "plugin.wasm" # optional; the default
[limits] # optional; per call
fuel = 2000000000
memory_mb = 64
timeout_ms = 2000| Field | Required | Meaning |
|---|---|---|
id |
yes | Unique id. Prefix your command and panel ids with it
(word-count.count). |
name |
yes | Display name. |
version |
yes | Your version; shown to the user. |
api |
yes | Must be "0.2". A plugin built for another version is
refused with a message saying which. |
description |
no | One line, shown in Settings → Plugins. |
capabilities |
no | What the plugin may do (below). Anything not listed is denied. |
wasm |
no | Path of the component next to the manifest. Default
plugin.wasm. |
limits |
no | Per-call budgets (below). |
Capabilities
| Capability | Allows |
|---|---|
commands |
Contributing commands to the palette |
panels |
Contributing panels |
themes |
Contributing color themes |
lsp |
Registering language servers; the editor runs their commands |
agents |
Registering ACP agents; the editor runs their commands |
fs:read |
read-file and list-files |
fs:write |
write-file |
editor:write |
The insert-text action |
clipboard:read |
context.clipboard is filled in |
http |
fetch |
The user sees this list before installing. Contributions and actions
that need a capability the manifest doesn't have are dropped; host
functions return
Err("capability denied: <name>").
Limits
| Limit | Default | Maximum | When exceeded |
|---|---|---|---|
fuel |
2 000 000 000 | 20 000 000 000 | ran out of fuel |
memory_mb |
64 | 512 | exceeded the memory limit of N MB |
timeout_ms |
2 000 | 10 000 | timed out after N ms |
They apply to every call separately. Fuel is roughly one unit per
WebAssembly instruction. Values above the maximum are lowered to it.
Raise timeout_ms if you call fetch (it can
take up to 8 s). The first call also waits while the host compiles your
component, which doesn't count against timeout_ms.
Exports
A plugin exports four functions. With the Rust SDK, implement the
Plugin trait (every method has a do-nothing default) and
call export_plugin!(YourType).
| Export | Called | Returns |
|---|---|---|
activate() -> contributions |
Once, when the plugin loads | Everything the plugin adds |
run-command(id, ctx) -> result<list<action>, string> |
When the user runs one of its commands | Actions to perform, or an error message for the status bar |
render-panel(id) -> list<node> |
When one of its panels is shown or refreshed | The panel's contents |
panel-event(id, event, ctx) -> list<action> |
On clicks, typing, Enter and list selection in its panel | Actions to perform |
Types
contributions
Returned by activate. Each list needs its capability;
without it the list is dropped.
| Field | Type | Capability |
|---|---|---|
commands |
list<command> |
commands |
panels |
list<panel> |
panels |
themes |
list<theme> |
themes |
lsp-servers |
list<lsp-server> |
lsp |
agents |
list<agent> |
agents |
command
| Field | Type | Meaning |
|---|---|---|
id |
string |
Passed back to run-command. |
title |
string |
Palette entry, e.g. "Word Count: Count Words". |
keybinding |
option<string> |
e.g. "cmd-alt-h"; chords are space-separated
("cmd-k w"). The user's own keymap wins on conflicts. |
panel
| Field | Type | Meaning |
|---|---|---|
id |
string |
Passed to render-panel and
panel-event. |
title |
string |
Panel header. |
theme
| Field | Type | Meaning |
|---|---|---|
name |
string |
Shown in Select Theme. |
json |
string |
A gpui-component theme file, the same format as
~/.config/zumba/themes/*.json. |
lsp-server
| Field | Type | Meaning |
|---|---|---|
language |
string |
Language id, e.g. "zig". |
extensions |
list<string> |
File extensions without the dot, e.g. ["zig"]. |
command, args |
string, list<string> |
How to start the server (stdio). The editor runs it, outside the sandbox. |
agent
An ACP agent (stdio, newline-delimited JSON-RPC) for the agent panel.
Its id in the editor becomes
plugin:<plugin id>/<agent id>.
| Field | Type | Meaning |
|---|---|---|
id |
string |
Unique within the plugin. |
name |
string |
Shown in the agent picker. |
command, args |
string, list<string> |
How to start it. The editor runs it, outside the sandbox. |
context
Passed to run-command and panel-event: the
editor's state at that moment.
| Field | Type | Meaning |
|---|---|---|
workspace |
string |
Absolute path of the project folder. |
file |
option<string> |
Workspace-relative path of the active file. |
selection |
string |
Selected text in the active editor; empty if none. |
language |
option<string> |
Language of the active file, e.g. "rust". |
text |
string |
The active editor's whole text, unsaved edits included. |
cursor |
u32 |
Cursor position as a byte offset into text. |
clipboard |
option<string> |
Clipboard text; only with clipboard:read, otherwise
none. |
action
What run-command and panel-event ask the
editor to do, in order.
| Variant | Effect |
|---|---|
notify(string) |
Shows the message in the status bar (prefixed with your plugin id). |
insert-text(string) |
Replaces the selection in the active editor, or inserts at the
cursor. Needs editor:write; without it the user sees a
"capability denied" notice instead. Undoable like any edit. |
open-file(string) |
Opens a workspace-relative file. "src/a.rs:12" opens it
at line 12. Paths outside the workspace are ignored. |
show-panel(string) |
Opens one of your panels and renders it. |
refresh-panel(string) |
Renders one of your panels again. Panels only redraw when you ask. |
node
A panel is a flat column of nodes, top to bottom.
| Variant | Shows |
|---|---|
heading(string) |
A section heading. |
text(string) |
A line of muted text. Text containing line breaks shows as a monospace block instead (for code, JSON, output). |
button(button) |
A button: { id, label }. Clicking sends
click(id). |
input(input) |
A text field: { id, placeholder, value }. Sends
change while typing and submit on Enter. |
list(list-view) |
A clickable list: { id, items, selected },
selected being the highlighted index. Clicking sends
select(id, index). |
separator |
A horizontal rule. |
event
| Variant | Sent when |
|---|---|
click(button id) |
A button is clicked. |
change((input id, value)) |
An input's text changes (every keystroke). |
submit((input id, value)) |
Enter is pressed in an input. |
select((list id, index)) |
A list item is clicked. |
Keep what the user typed in your own state when you get
change, and put it back as the input's value
in render-panel, or the field resets when the panel
redraws.
http-request /
http-response
http-request field |
Type | Meaning |
|---|---|---|
method |
string |
GET, POST, PUT,
PATCH, DELETE… |
url |
string |
http:// or https:// only. |
headers |
list<tuple<string, string>> |
Request headers. |
body |
option<string> |
Request body. |
http-response field |
Type | Meaning |
|---|---|---|
status |
u16 |
e.g. 200. |
status-line |
string |
e.g. "HTTP/1.1 200 OK". |
headers |
list<tuple<string, string>> |
Response headers. |
body |
string |
Text body, cut at 2 MB. |
elapsed-ms |
u32 |
How long the request took. |
Host functions
Imported from zumba:plugin/host. In the Rust SDK they're
plain functions: log, read_file,
write_file, list_files,
fetch.
| Function | Capability | Does |
|---|---|---|
log(msg) |
none | Adds a line to the plugin's log, shown under it in Settings → Plugins. |
read-file(path) -> result<string, string> |
fs:read |
Reads a UTF-8 text file. |
write-file(path, contents) -> result<_, string> |
fs:write |
Writes (creates or replaces) a text file. Creates missing folders on the way. |
list-files(dir) -> result<list<string>, string> |
fs:read |
Every file under dir, recursively, as
workspace-relative paths. Respects .gitignore; skips
.git. Use "" for the whole project. |
fetch(req) -> result<http-response, string> |
http |
One HTTP request. 8 s timeout; redirects are not followed; non-2xx statuses are returned, not errors. |
Paths are always relative to the workspace. Absolute
paths and .. are refused (path not allowed),
and a symlink that leads outside the workspace is refused too
(path escapes the workspace). Your plugin's stdout and
stderr also end up in its log.
Errors come back as Err(String) with a
message meant for people: pass it on in a notify or return
it from run-command.
Versioning
The API version is major.minor of the WIT package
(zumba:plugin@0.2.0 → "0.2"). The editor loads
only plugins whose manifest api matches its own, so rebuild
against the new SDK when the version changes.
Building a plugin from the interface file
You don't need the Zumba SDK: wit-bindgen
and the interface file are enough. Download zumba-plugin.wit into a
wit/ folder next to your Cargo.toml:
word-count/
Cargo.toml
zumba-plugin.toml
wit/zumba-plugin.wit
src/lib.rs
# Cargo.toml
[package]
name = "word-count"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.63"
[profile.release]
opt-level = "s"
strip = true// src/lib.rs
wit_bindgen::generate!({ world: "plugin", path: "wit" });
// The world already brings `Action`, `Context`, `Contributions`, `Event` and `Node` into scope.
use zumba::plugin::types::Command;
struct WordCount;
impl Guest for WordCount {
fn activate() -> Contributions {
Contributions {
commands: vec![Command { id: "word-count.count".into(), title: "Word Count: Count Words".into(), keybinding: None }],
themes: vec![],
lsp_servers: vec![],
agents: vec![],
panels: vec![],
}
}
fn run_command(id: String, ctx: Context) -> Result<Vec<Action>, String> {
match id.as_str() {
"word-count.count" => {
let text = if ctx.selection.is_empty() { &ctx.text } else { &ctx.selection };
Ok(vec![Action::Notify(format!("{} words", text.split_whitespace().count()))])
}
_ => Err(format!("unknown command {id}")),
}
}
fn render_panel(_id: String) -> Vec<Node> {
vec![]
}
fn panel_event(_id: String, _event: Event, _ctx: Context) -> Vec<Action> {
vec![]
}
}
export!(WordCount);# zumba-plugin.toml
id = "word-count"
name = "Word Count"
version = "0.1.0"
api = "0.2"
description = "Counts words in the selection or the active file."
capabilities = ["commands"]Build it and put the component next to the manifest:
rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2
cp target/wasm32-wasip2/release/word_count.wasm plugin.wasmThen Settings → Plugins → Install from folder, and run
Word Count: Count Words from the palette. Host functions are in
zumba::plugin::host
(host::read_file("src/main.rs")).
Packaging
A plugin package is a folder (or git repository) with
zumba-plugin.toml and the built component at its root.
Users install it from Settings → Plugins with Install from
folder or Install from git. Zumba never builds a plugin
from source when installing, because a build would run code outside the
sandbox: ship the built plugin.wasm.