Plugin development Reference

Pane surfaces & movable panels

Render whole pane-tabs with surfaces, and build feature panels that move between the dock and the grid and pin to a workspace.

Beyond contributing to slots, a plugin can render an entire workspace pane-tab — a surface — and build a feature panel that lives in both the dock and the grid at once. This page covers both, plus the helper modules a panel plugin imports directly.

Pane surfaces

Register a surface renderer from the renderer context:

ctx.surfaces.register("my-plugin.demo", {
  render: (tab) => h("div", null, `state: ${JSON.stringify(tab.surfaceState)}`),
  title: (tab) => "My Surface",   // optional
  // icon: …                       // optional
});

Open one in the active pane:

ctx.workspace.openSurface("my-plugin.demo", {
  state: { openedAt: Date.now() },
  title: "My Surface",
});

A plugin pane-tab (kind: "plugin", with a surfaceType and persisted JSON surfaceState) lives in the normal workspace layout — it splits, moves, reorders, and persists across relaunch like any other tab.

While the owning plugin is disabled the tab shows a placeholder and the layout survives; re-enabling the plugin swaps the live surface back in. (See examples/plugins/hello-world — the 👋 button opens a demo surface.)

Prefix your surface type with your plugin id, e.g. my-plugin.demo.

Movable feature panels (dock ↔ grid, pin-to-workspace)

A feature panel can live in both hosts at once — stacked in the dock (via the dock.panels slot) and/or open as a grid pane-tab (register the same body as a surface). Because the bodies read shared zustand stores, the two viewports stay in sync. Keep view-state (search text, selected item, expand/collapse) in a store, not local useState, so a panel works in either host.

The helpers below are all context-free renderer modules a panel plugin imports directly:

  • src/renderer/plugins/panel-move.tsmovePanelToGrid(panel) / movePanelToDock(panel) move a panel between hosts (state preserved); pinPanelToActiveWorkspace(panelId) / unpinPanel(panelId) pin a docked panel to the active sidebar Group; isPanelDocked(panelId). A FeaturePanel is { panelId, surfaceType, title }.
  • src/renderer/plugins/panel-move-buttons.tsxMoveToGridButton / MoveToDockButton (drop into dock.panels headerActions and the grid surface’s SurfaceFrame actions, respectively).
  • src/renderer/plugins/SurfaceFrame.tsx — frame for a grid-hosted panel body with an optional top actions bar (the dock supplies its own chrome).
  • src/renderer/sidebar-dock/panel-pins.tsusePanelPinStore + visibleDockState / isPanelVisibleForWorkspace. A pinned panel shows in the dock + rail only while its Group is active (persisted under muxie:panelPins keyed on durable Group ids); a pin to a deleted Group falls back to global. The pin filter is applied at render only — dock mutations use the unfiltered state, so a hidden panel is never lost.

The Agents / Sprites / SSH core plugins register Move … / Pin … palette commands using these helpers.

See the Slot catalog for dock.panels and the other UI surfaces.