Plugin development Tutorial

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. The state you pass is persisted as the tab’s surfaceState and handed back to render(tab).
  • Everything you register through ctx is 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.