// Public, typed contract of the tabs plugin. Other plugins that
// depend on `bmux_tabs_plugin_api` import these interfaces rather
// than the tabs plugin implementation itself.
//
// Four interfaces:
// - `tabs-state` — read-only queries about panes and tabs.
// - `tabs-commands` — mutating operations on panes and tabs.
// - `tabs-events` — event stream of tab/pane lifecycle changes.
// - `tabs-list` — reactive state channel carrying the ordered
// tab list for tab-bar rendering + positional navigation.
plugin bmux.tabs version 1;
capability TABS_READ = bmux.tabs.read;
capability TABS_WRITE = bmux.tabs.write;
@capability(TABS_WRITE)
interface tabs-placement-v1 {
// Append a live tab to the destination arrangement before changing its
// workspace attribute. Source references are retained, never destroyed.
// Retrying an interrupted transfer is idempotent.
command place-tab(context-id: uuid, workspace-id: uuid) -> result<unit, string>;
}
@capability(TABS_READ)
interface tabs-state {
// A pane's current lifecycle status.
variant pane-status {
@default running,
exited { exit-code: i32 },
}
// Snapshot of a single pane's state.
record pane-state {
id: uuid,
session-id: uuid,
focused: bool,
zoomed: bool,
name: string?,
status: pane-status,
}
// Snapshot of a floating pane surface exposed through the tabs UX facade.
record floating-pane-state {
id: uuid,
pane-id: uuid,
session-id: uuid,
anchor-pane-id: uuid?,
context-id: uuid?,
client-id: uuid?,
x: u16,
y: u16,
w: u16,
h: u16,
z: i32,
layer: string,
scope: string,
visible: bool,
}
// Snapshot of a tab. Tabs are a plugin-layered view over
// sessions/contexts; the id is the underlying context id.
record tab-entry {
id: string,
name: string,
active: bool,
workspace: string,
workspace-id: uuid,
}
// Pane-level queries.
query pane-state(id: uuid) -> pane-state?;
query focused-pane(session: uuid) -> uuid?;
query zoomed-pane(session: uuid) -> uuid?;
query list-panes(session: uuid) -> list<pane-state>;
query list-floating-panes(session: uuid?) -> list<floating-pane-state>;
record active-tab-pane-set {
tab-id: uuid,
session-id: uuid,
pane-ids: list<uuid>,
}
variant active-tab-pane-query-error {
no-active-tab,
no-selected-session,
failed { reason: string },
}
// Tab-level queries.
query list-tabs(session: string?) -> list<tab-entry>;
query active-tab-panes()
-> result<active-tab-pane-set, active-tab-pane-query-error>;
}
@capability(TABS_READ)
interface tabs-catalog-v1 {
// Complete discovery in workspace order, then each workspace's tab arrangement.
// Reading this catalog never initializes or rewrites arrangement storage.
query list-tabs() -> result<list<tabs-state.tab-entry>, string>;
}
@capability(TABS_WRITE)
interface tabs-commands {
// Errors a focus command can produce.
variant focus-error {
pane-not-found,
focus-denied { reason: string },
}
// Errors a close command can produce.
variant close-error {
pane-not-found,
close-denied { reason: string },
}
// Errors a tab-level command can produce.
variant tab-error {
not-found,
denied { reason: string },
failed { reason: string },
}
// Errors a pane mutation command (split/launch/resize/zoom/
// restart) can produce.
variant pane-mutation-error {
pane-not-found,
session-not-found,
invalid-argument { reason: string },
denied { reason: string },
failed { reason: string },
}
// Acknowledgement returned by mutating tab commands. When a new
// tab is created, `id` carries the new context id; other
// commands may leave it empty.
record tab-ack {
ok: bool,
id: string?,
}
// Acknowledgement returned by pane-mutation commands. `pane-id`
// carries the freshly-created pane id for split/launch; other
// commands leave it empty.
record pane-ack {
ok: bool,
pane-id: uuid?,
}
// Acknowledgement returned by `zoom-pane`. Carries the pane that
// was toggled plus the post-toggle zoom state so callers can
// render an accurate status without a follow-up query.
record pane-zoom-ack {
pane-id: uuid,
zoomed: bool,
}
// Direction in which a pane split/focus action moves.
enum pane-direction {
@default horizontal,
vertical,
left,
right,
up,
down,
}
// Directional intent for resizing a pane. Increase/decrease grow or
// shrink the focused pane automatically; physical directions move
// the nearest boundary on that side.
enum pane-resize-direction {
@default increase,
decrease,
left,
right,
up,
down,
}
// Directional intent for moving a floating pane.
enum floating-pane-move-direction {
@default right,
left,
up,
down,
}
// Placement for moving a tab relative to another tab.
enum tab-move-placement {
@default before,
after,
}
// Selector identifying a session or pane. Exactly one of `id`,
// `name`, or `index` should be populated. `id` wins when multiple
// are set. `index` is only meaningful for pane selectors scoped
// to a session.
record selector {
id: uuid?,
name: string?,
index: u32?,
}
// Pane-level commands.
command focus-pane(id: uuid) -> result<unit, focus-error>;
command close-pane(id: uuid) -> result<unit, close-error>;
// Selector-targeted variants. These accept the full `selector`
// record (id / name / index / active-by-empty) so callers that
// address panes by position within a session don't have to resolve
// an id first.
command focus-pane-by-selector(session: selector?, target: selector)
-> result<pane-ack, pane-mutation-error>;
command close-pane-by-selector(session: selector?, target: selector)
-> result<pane-ack, pane-mutation-error>;
// Close the currently-active pane in the targeted session.
// Equivalent to `close-pane-by-selector` with an empty target, but
// gives consumers a dedicated name that documents the "close active
// pane" semantic.
command close-active-pane(session: selector?)
-> result<pane-ack, pane-mutation-error>;
// Focus the next/previous pane in the layout relative to the
// currently-active pane. The direction is interpreted by the host
// today; once the tabs plugin owns focus ordering internally,
// this shifts to a plugin-local lookup.
command focus-pane-in-direction(session: selector?, direction: pane-direction)
-> result<pane-ack, pane-mutation-error>;
// Split / launch / resize / zoom / restart.
// Each targets a session (by uuid or name) and an existing pane
// (likewise), plus operation-specific extras.
command split-pane(
session: selector?,
target: selector?,
direction: pane-direction,
ratio-pct: u32?,
) -> result<pane-ack, pane-mutation-error>;
command launch-pane(
session: selector?,
target: selector?,
direction: pane-direction,
name: string?,
program: string,
args: list<string>,
) -> result<pane-ack, pane-mutation-error>;
command resize-pane(
session: selector?,
target: selector?,
direction: pane-resize-direction,
cells: u16,
) -> result<pane-ack, pane-mutation-error>;
command zoom-pane(
session: selector?,
) -> result<pane-zoom-ack, pane-mutation-error>;
command restart-pane(
session: selector?,
target: selector?,
) -> result<pane-ack, pane-mutation-error>;
command create-floating-pane(
session: selector?,
target: selector?,
x: u16?,
y: u16?,
w: u16?,
h: u16?,
z: i32?,
layer: string?,
scope: string?,
program: string?,
args: list<string>,
) -> result<pane-ack, pane-mutation-error>;
command move-floating-pane(
session: selector?,
target: selector,
x: u16,
y: u16,
) -> result<pane-ack, pane-mutation-error>;
command resize-floating-pane(
session: selector?,
target: selector,
w: u16,
h: u16,
) -> result<pane-ack, pane-mutation-error>;
command move-active-floating-pane(
session: selector?,
direction: floating-pane-move-direction,
cells: u16,
) -> result<pane-ack, pane-mutation-error>;
command resize-active-floating-pane(
session: selector?,
direction: pane-resize-direction,
cells: u16,
) -> result<pane-ack, pane-mutation-error>;
command focus-floating-pane(session: selector?, target: selector)
-> result<pane-ack, pane-mutation-error>;
command raise-floating-pane(session: selector?, target: selector)
-> result<pane-ack, pane-mutation-error>;
command lower-floating-pane(session: selector?, target: selector)
-> result<pane-ack, pane-mutation-error>;
command close-floating-pane(session: selector?, target: selector)
-> result<pane-ack, pane-mutation-error>;
command focus-next-floating-pane(session: selector?)
-> result<pane-ack, pane-mutation-error>;
command raise-active-floating-pane(session: selector?)
-> result<pane-ack, pane-mutation-error>;
command lower-active-floating-pane(session: selector?)
-> result<pane-ack, pane-mutation-error>;
command close-active-floating-pane(session: selector?)
-> result<pane-ack, pane-mutation-error>;
command set-floating-pane-z(
session: selector?,
target: selector,
z: i32,
) -> result<pane-ack, pane-mutation-error>;
command set-floating-pane-layer(
session: selector?,
target: selector,
layer: string,
) -> result<pane-ack, pane-mutation-error>;
// Tab-level commands.
command new-tab(name: string?) -> result<tab-ack, tab-error>;
command rename-tab(name: string) -> result<tab-ack, tab-error>;
// Rename a specific tab by id, independent of which tab is current.
command rename-tab-by-id(id: uuid, name: string)
-> result<tab-ack, tab-error>;
command kill-tab(target: string, force-local: bool) -> result<tab-ack, tab-error>;
command kill-all-tabs(force-local: bool) -> result<tab-ack, tab-error>;
command switch-tab(target: string) -> result<tab-ack, tab-error>;
command move-tab(source: uuid, target: uuid, placement: tab-move-placement)
-> result<tab-ack, tab-error>;
}
interface tabs-events {
// A single event describing a pane-level state change.
variant pane-event {
// A pane gained focus.
focused { pane-id: uuid },
// A pane lost focus.
unfocused { pane-id: uuid },
// A pane was zoomed to fill its session viewport.
zoomed { pane-id: uuid },
// A zoomed pane was restored.
unzoomed { pane-id: uuid },
// A new pane was opened.
opened { pane-id: uuid, session-id: uuid },
// A pane was closed.
closed { pane-id: uuid },
// The pane's lifecycle status changed (e.g., running -> exited).
status-changed { pane-id: uuid },
}
events pane-event;
}
// Reactive state channel carrying the ordered list of tabs in the
// order consumers should render them (and navigate through). The
// tabs plugin owns the authoritative tab order (persisted in its
// plugin-local storage under `tabs.order`) and publishes a fresh
// snapshot on every mutation: new-tab, rename-tab,
// switch-tab, move-tab, kill-tab, kill-all-tabs,
// goto-tab, close-current-tab, next/prev/last tab.
//
// Subscribers (notably the attach tab bar) consume this via
// `EventBus::subscribe_state::<TabListSnapshot>` to observe the
// current ordered list synchronously plus a live-update receiver.
// This lets the tab bar render the exact order the plugin maintains
// and lets next/prev navigation walk the same order without a
// round-trip query per frame.
//
// Payload semantics:
// - `tabs` is the ordered list of live tabs; consumers render
// positional indices (`1:`, `2:`, …) from this order.
// - `active` on each entry is `true` for the current active tab
// (as resolved by the plugin's effective-current-context logic) and
// `false` otherwise.
// - `revision` is a monotonic counter advancing on every publish;
// consumers can use it to deduplicate or order updates.
// Client-local selection, published by attach rather than the shared catalog.
interface tabs-local-view {
record tab-selection {
context-id: uuid?,
}
@state events tab-selection;
}
interface tabs-list {
record tab-list-snapshot {
tabs: list<tab-list-entry>,
revision: u64,
}
record tab-list-entry {
id: uuid,
name: string,
active: bool,
workspace: string,
workspace-id: uuid,
}
@state events tab-list-snapshot;
}