Build your first plugin
Create a hello-world plugin with a manifest, a command, and a pane surface, then install and reload it live.
This walkthrough builds a small plugin from scratch — a manifest, a command-palette command, and a pane surface — then installs and reloads it live. By the end you’ll have a working plugin and a feel for the renderer context.
The examples here are plain JavaScript to match the entry files Muxie loads.
Tip: Prefer types? See Writing plugins in TypeScript for a fully typed setup — a declaration file,
tsconfig, and a build that emits these same files.
1. Create the folder
A plugin is a folder containing a manifest and one or two entry files:
hello-world/
muxie-plugin.json # manifest (required)
main.cjs # optional main-process entry (CommonJS)
renderer.js # optional renderer entry (plain ESM, no bundler)
For this tutorial we only need the manifest and a renderer entry.
2. Write the manifest
The manifest is muxie-plugin.json. Its id is kebab-case and must equal the
folder name. At least one of main / renderer is required.
{
"id": "hello-world",
"name": "Hello World",
"version": "1.0.0",
"description": "My first Muxie plugin.",
"author": "you",
"renderer": "renderer.js"
}
3. Write the renderer entry
The renderer entry is plain ESM with no imports — it’s served over the
muxie-plugin:// scheme and import()ed by the host. Use ctx.react for the
app’s React instance; never bundle your own (two Reacts break hooks).
Export an activate(ctx) function. Here we register a palette command and a pane
surface, and a sidebar button that opens the surface:
export function activate(ctx) {
const h = ctx.react.createElement;
// A command-palette command. Convention: prefix ids with your plugin id.
ctx.commands.register({
id: "hello-world.greet",
title: "Hello World: Greet",
keywords: "example demo plugin",
run: () => ctx.log("hello from a plugin command"),
});
// A pane SURFACE: a real workspace tab the plugin renders.
ctx.surfaces.register("hello-world.demo", {
title: () => "Hello World",
render: (tab) =>
h(
"div",
{ style: { padding: "16px", fontSize: "13px" } },
h("h2", { style: { margin: "0 0 8px" } }, "👋 Hello from a plugin surface"),
h(
"p",
{ style: { margin: 0, opacity: 0.7 } },
`Opened at: ${tab.surfaceState?.openedAt ?? "unknown"}`,
),
),
});
// A rail button that opens the surface as a pane-tab.
ctx.slots.contribute("sidebar.actions", {
title: "Hello",
icon: h("span", { style: { fontSize: "12px", lineHeight: 1 } }, "👋"),
onClick: () =>
ctx.workspace.openSurface("hello-world.demo", {
state: { openedAt: new Date().toISOString() },
title: "Hello World",
}),
});
return {
deactivate: () => ctx.log("hello-world deactivated"),
};
}
A few things worth noting:
ctx.commands.register(...)adds a palette command (also dispatchable by id).ctx.surfaces.register(type, def)registers a renderer for a whole pane-tab;ctx.workspace.openSurface(type, { state, title })opens one. Thestateyou pass is persisted as the tab’ssurfaceStateand handed back torender(tab).- Everything you register through
ctxis tracked, so it all vanishes when the plugin is disabled, uninstalled, or reloaded — that’s the hot-unload invariant.
4. Install and reload
Open Settings → Plugins → Install from folder… and pick your hello-world
folder. The plugin loads immediately — no restart. Run Hello World: Greet from
the command palette, or click the rail button to open the surface.
When you edit renderer.js, hit Reload on the Plugins page to load the new
code live.
A complete reference
Muxie ships a complete example at examples/plugins/hello-world/ that adds a
command, a sidebar button, a dock panel, a settings section, a pane surface, and
a main-process IPC handler. Read it alongside the
renderer context and
main-process context reference pages as
you build out your own plugin.