-
Notifications
You must be signed in to change notification settings - Fork 11
Creating a Plugin
The supported Rust starter lives in examples/extensions. Copy the sdk directory and one plugin directory into your own repository. The SDK is intentionally small and exposes Serein ABI version 1.
Start with this layout:
my-serein-plugin/
├── Cargo.toml
├── Cargo.lock
├── LICENSE
├── sdk/
└── hello-serein/
├── Cargo.toml
├── manifest.json
└── src/lib.rs
Use the starter workspace configuration and keep the plugin crate as a WebAssembly library:
[package]
name = "hello-serein"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
serein-extension-sdk.workspace = trueEvery package uses api_version: 1, a stable lowercase ID, a version, license, public source URL, and kind: "plugin".
{
"api_version": 1,
"id": "hello-serein",
"name": "Hello Serein",
"version": "1.0.0",
"author": "Your name",
"license": "MIT",
"source": "https://github.com/your-name/hello-serein",
"kind": "plugin",
"capabilities": [],
"actions": [
{
"id": "open",
"label": "Open hello panel",
"surface": "panel"
}
]
}IDs may contain lowercase ASCII letters, digits, and hyphens, must start with a letter or digit, and may be at most 64 bytes. Action labels may be at most 128 UTF-8 bytes. A plugin may declare at most 16 actions and one activation action.
The SDK turns a Rust function into the required serein_alloc and serein_invoke exports:
use serein_extension_sdk::{Element, Invocation, Output};
fn handle(input: Invocation) -> Output {
let panel = if input.action == "open" {
vec![
Element::Heading { text: "Hello Serein".into() },
Element::Text { text: "My first sandboxed plugin is running.".into() },
]
} else {
Vec::new()
};
Output { panel, ..Default::default() }
}
serein_extension_sdk::export!(handle);Each invocation receives a fresh WebAssembly instance. Return native panel elements rather than drawing UI directly.
Supported panel elements are heading, text, separator, row, button, text_input, checkbox, select, and integer slider. Button IDs must point to actions whose surface is panel.
| Surface | Where the action appears | Context it may receive |
|---|---|---|
message |
Selected message context menu | Selected message, with consent |
composer |
Composer tools | Current draft, with consent |
panel |
Extensions page or Tools menu | Native form values and granted storage |
activation |
Enable and account load | Granted storage; no conversation text |
| Capability | Allows |
|---|---|
selected_message |
The selected message text for a message action |
composer |
The current composer draft and an explicit replacement proposal |
storage |
One bounded local UTF-8 value for plugin settings |
appearance |
App-wide colors and supported native control metrics |
deleted_messages |
An activation action may request bounded, session-only retention of already loaded deleted messages |
Request only what the plugin uses. Users review capabilities before enabling a package and again when an update changes them. Composer replacements always require the user to press Apply; plugins cannot send messages.
From your workspace root:
rustup target add wasm32-unknown-unknown
cargo build --locked --release --target wasm32-unknown-unknown
python pack.py hello-serein/manifest.json target/wasm32-unknown-unknown/release/hello_serein.wasm packages/hello-serein.serein-extensionCopy pack.py from the starter. It combines the manifest and Wasm bytes without downloading or executing creator code, then prints the package size and SHA-256 digest.
Continue with Testing and Packaging.