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.ts—movePanelToGrid(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). AFeaturePanelis{ panelId, surfaceType, title }.src/renderer/plugins/panel-move-buttons.tsx—MoveToGridButton/MoveToDockButton(drop intodock.panelsheaderActionsand the grid surface’sSurfaceFrameactions, 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.ts—usePanelPinStore+visibleDockState/isPanelVisibleForWorkspace. A pinned panel shows in the dock + rail only while its Group is active (persisted undermuxie:panelPinskeyed 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.