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

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.wasm

Then 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.