// Public, typed contract of the pane-runtime plugin.
//
// The pane-runtime plugin owns every PTY handle + layout tree in the
// bmux process; core server code dispatches through the typed
// services declared here.
//
// Interfaces:
// - `pane-runtime-state` — read queries over pane + session runtime.
// - `pane-runtime-commands` — mutating commands (split / launch /
// focus / resize / close / restart / zoom / direct-input / create /
// destroy sessions with runtime, restore from snapshot).
// - `attach-runtime-commands` — per-client attach lifecycle (attach
// session, attach context, open, send input, request output batch,
// set viewport, detach).
// - `attach-runtime-state` — attach-view queries (layout, snapshot,
// pane-snapshot, pane-images, pane-output-batch).
// - `pane-runtime-events` — pane + attach lifecycle event stream.
plugin bmux.pane_runtime version 1;
capability PANE_RUNTIME_READ = bmux.pane_runtime.read;
capability PANE_RUNTIME_WRITE = bmux.pane_runtime.write;
capability ATTACH_RUNTIME_WRITE = bmux.attach_runtime.write;
capability ATTACH_RUNTIME_READ = bmux.attach_runtime.read;
@capability(PANE_RUNTIME_READ)
interface pane-runtime-state {
record pane-summary {
id: uuid,
name: string?,
shell: string,
focused: bool,
}
record session-pane-list {
session_id: uuid,
panes: list<pane-summary>,
}
record floating-pane-summary {
id: uuid,
pane_id: uuid,
anchor_pane_id: uuid?,
context_id: uuid?,
client_id: uuid?,
x: u16,
y: u16,
w: u16,
h: u16,
scope: string,
layer: string,
z: i32,
visible: bool,
opaque: bool,
accepts_input: bool,
cursor_owner: bool,
}
record floating-pane-list {
session_id: uuid,
panes: list<floating-pane-summary>,
}
record pane-process-identity {
session_id: uuid,
pane_id: uuid,
pid: u32?,
process_group_id: i32?,
}
record pane-process-list {
panes: list<pane-process-identity>,
}
variant pane-state-error {
session-not-found,
pane-not-found,
}
query list-panes(session_id: uuid?) -> result<session-pane-list, pane-state-error>;
query get-pane(session_id: uuid, pane_id: uuid)
-> result<pane-summary, pane-state-error>;
query list-floating-panes(session_id: uuid)
-> result<floating-pane-list, pane-state-error>;
query list-pane-processes() -> result<pane-process-list, pane-state-error>;
query get-pane-process(session_id: uuid, pane_id: uuid)
-> result<pane-process-identity, pane-state-error>;
}
@capability(PANE_RUNTIME_WRITE)
interface pane-runtime-commands {
// Shared acknowledgement carrying the target pane + session id
// after a mutating command completes. All mutation commands return
// a result of this or a command-specific error.
record pane-ack {
session_id: uuid,
pane_id: uuid,
}
record session-ack {
session_id: uuid,
}
record floating-pane-ack {
session_id: uuid,
pane_id: uuid,
surface_id: uuid,
}
variant pane-command-error {
session-not-found,
pane-not-found,
denied { reason: string },
failed { reason: string },
}
// Errors the session-runtime commands can produce. Names are
// display hints, not identity: session creation does not reject
// duplicate names. Identity is the UUID assigned at creation.
variant session-runtime-command-error {
session-not-found,
failed { reason: string },
}
// Split an existing pane along the given direction at the given
// ratio (0.1..=0.9, clamped). Creates a new pane beside the target.
command split-pane(
session_id: uuid,
target: uuid?,
direction: string, // "horizontal" | "vertical"
ratio_percent: u8, // 10..=90
) -> result<pane-ack, pane-command-error>;
// Launch a new pane with an explicit program + args + cwd + env,
// splitting from `target` along `direction`.
command launch-pane(
session_id: uuid,
target: uuid?,
direction: string, // "horizontal" | "vertical"
ratio_percent: u8,
name: string?,
program: string,
args: list<string>,
cwd: string?,
) -> result<pane-ack, pane-command-error>;
// Focus a pane. `target` may be a pane id; when `target` is none,
// focus moves in `direction` (up/down/left/right/next/prev).
command focus-pane(
session_id: uuid,
target: uuid?,
direction: string,
) -> result<pane-ack, pane-command-error>;
command resize-pane(
session_id: uuid,
target: uuid?,
direction: string, // "left" | "right" | "up" | "down"
cells: u16,
) -> result<session-ack, pane-command-error>;
command close-pane(
session_id: uuid,
target: uuid?,
) -> result<pane-ack, pane-command-error>;
command restart-pane(
session_id: uuid,
target: uuid?,
) -> result<pane-ack, pane-command-error>;
command zoom-pane(
session_id: uuid,
) -> result<pane-ack, pane-command-error>;
command create-floating-pane(
session_id: uuid,
target: 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?,
name: string?,
program: string?,
args: list<string>,
cwd: string?,
) -> result<floating-pane-ack, pane-command-error>;
command move-floating-pane(
session_id: uuid,
pane_id: uuid,
x: u16,
y: u16,
) -> result<floating-pane-ack, pane-command-error>;
command resize-floating-pane(
session_id: uuid,
pane_id: uuid,
w: u16,
h: u16,
) -> result<floating-pane-ack, pane-command-error>;
command focus-floating-pane(
session_id: uuid,
pane_id: uuid,
) -> result<floating-pane-ack, pane-command-error>;
command raise-floating-pane(
session_id: uuid,
pane_id: uuid,
) -> result<floating-pane-ack, pane-command-error>;
command lower-floating-pane(
session_id: uuid,
pane_id: uuid,
) -> result<floating-pane-ack, pane-command-error>;
command set-floating-pane-z(
session_id: uuid,
pane_id: uuid,
z: i32,
) -> result<floating-pane-ack, pane-command-error>;
command set-floating-pane-layer(
session_id: uuid,
pane_id: uuid,
layer: string,
) -> result<floating-pane-ack, pane-command-error>;
command close-floating-pane(
session_id: uuid,
pane_id: uuid,
) -> result<floating-pane-ack, pane-command-error>;
// Send raw input bytes directly to a pane. Used for unattached
// programmatic input (e.g. playbook-driven tests).
command pane-direct-input(
session_id: uuid,
pane_id: uuid,
data: list<u8>,
) -> result<pane-ack, pane-command-error>;
// Update the cached write decision for an already-attached client.
// The permissions plugin calls this when a role changes so pane input
// stays hot-path local without becoming stale.
command set-client-write-permission(
session_id: uuid,
client_id: uuid,
allowed: bool,
) -> result<u8, pane-command-error>;
// Spin up a brand-new session runtime (session manager entry +
// initial pane PTY). Sessions-plugin bridges `new-session` into
// this operation.
command new-session-with-runtime(
name: string?,
) -> result<session-ack, session-runtime-command-error>;
// Tear down a session's runtime (shut down all pane PTYs, flush
// recording sinks, release attach tokens). Sessions-plugin bridges
// `kill-session` into this operation.
command kill-session-runtime(
session_id: uuid,
force_local: bool,
) -> result<session-ack, session-runtime-command-error>;
// Restore a session runtime from a snapshot payload. Invoked by
// the snapshot orchestrator on server startup. Payload is the
// opaque bytes from `PaneRuntimeSnapshotV1` (deserialized inside
// the plugin).
command restore-session-runtime(
session_id: uuid,
snapshot_payload: list<u8>,
) -> result<session-ack, session-runtime-command-error>;
}
@capability(ATTACH_RUNTIME_WRITE)
interface attach-runtime-commands {
// Response from `attach-session` / `attach-context`. Carries the
// attach grant issued by the AttachTokenManager; client re-presents
// it on `attach-open`.
record attach-grant {
token: uuid,
session_id: uuid,
context_id: uuid?,
expires_epoch_ms: u64,
}
// Response from `attach-open`. Indicates the open succeeded and
// describes the stream's terminal capabilities.
record attach-ready {
session_id: uuid,
context_id: uuid?,
can_write: bool,
}
record attach-output {
data: list<u8>,
}
record attach-viewport-set {
session_id: uuid,
cols: u16,
rows: u16,
status_top_inset: u16,
status_bottom_inset: u16,
context_id: uuid?,
}
record attach-retarget-ready {
session_id: uuid,
context_id: uuid?,
can_write: bool,
cols: u16,
rows: u16,
status_top_inset: u16,
status_bottom_inset: u16,
}
record attach-input-accepted {
bytes: u32,
}
// Selector identifying a session. Exactly one of `id` or `name`
// should be populated; `id` wins when both are set.
record session-selector {
id: uuid?,
name: string?,
}
// Selector identifying a context. Exactly one of `id` or `name`
// should be populated; `id` wins when both are set.
record context-selector {
id: uuid?,
name: string?,
}
variant attach-command-error {
session-not-found,
invalid-grant,
expired-grant,
denied { reason: string },
failed { reason: string },
}
command attach-session(
selector: session-selector,
can_write: bool,
) -> result<attach-grant, attach-command-error>;
command attach-context(
selector: context-selector,
can_write: bool,
) -> result<attach-grant, attach-command-error>;
command attach-open(
session_id: uuid,
attach_token: uuid,
) -> result<attach-ready, attach-command-error>;
command attach-input(
session_id: uuid,
data: list<u8>,
) -> result<attach-input-accepted, attach-command-error>;
command attach-output(
session_id: uuid,
max_bytes: u32,
) -> result<attach-output, attach-command-error>;
command attach-set-viewport(
session_id: uuid,
cols: u16,
rows: u16,
status_top_inset: u16,
status_bottom_inset: u16,
cell_pixel_w: u16,
cell_pixel_h: u16,
) -> result<attach-viewport-set, attach-command-error>;
command attach-retarget-context(
context_id: uuid,
can_write: bool,
cols: u16,
rows: u16,
status_top_inset: u16,
status_bottom_inset: u16,
cell_pixel_w: u16,
cell_pixel_h: u16,
) -> result<attach-retarget-ready, attach-command-error>;
command set-client-attach-policy(
allow_detach: bool,
) -> result<u8, attach-command-error>;
command detach() -> result<u8, attach-command-error>;
}
@capability(ATTACH_RUNTIME_READ)
interface attach-runtime-state {
record pane-chunk {
pane_id: uuid,
data: list<u8>,
stream_start: u64,
stream_end: u64,
stream_gap: bool,
sync_update_active: bool,
}
record pane-mouse-protocol {
pane_id: uuid,
// JSON-encoded `AttachMouseProtocolState` (schema is server-internal).
encoded: list<u8>,
}
record pane-input-mode {
pane_id: uuid,
// JSON-encoded `AttachInputModeState`.
encoded: list<u8>,
}
record attach-layout {
session_id: uuid,
context_id: uuid?,
focused_pane_id: uuid,
// JSON-encoded layout payload (panes + layout_root + scene + zoomed).
encoded: list<u8>,
}
record attach-snapshot {
session_id: uuid,
context_id: uuid?,
focused_pane_id: uuid,
zoomed: bool,
// JSON-encoded layout payload (panes + layout_root + scene).
layout_encoded: list<u8>,
chunks: list<pane-chunk>,
pane_mouse_protocols: list<pane-mouse-protocol>,
pane_input_modes: list<pane-input-mode>,
}
record attach-pane-snapshot {
chunks: list<pane-chunk>,
pane_mouse_protocols: list<pane-mouse-protocol>,
pane_input_modes: list<pane-input-mode>,
}
record attach-pane-output-batch {
chunks: list<pane-chunk>,
output_still_pending: bool,
}
record pane-grid-snapshot {
pane_id: uuid,
stream_end: u64,
encoded: list<u8>,
}
record attach-pane-grid-snapshot {
snapshots: list<pane-grid-snapshot>,
}
record pane-grid-window-request {
pane_id: uuid,
scrollback_offset: u32,
rows: u32,
anchor_total_scrolled_rows: u64?,
}
record pane-grid-window {
pane_id: uuid,
scrollback_offset: u32,
max_scrollback_offset: u32,
total_scrolled_rows: u64,
anchor_delta_rows: u32,
anchor_clamped: bool,
stream_end: u64,
encoded: list<u8>,
}
record attach-pane-grid-window {
windows: list<pane-grid-window>,
}
record pane-grid-delta {
pane_id: uuid,
base_revision: u64,
revision: u64,
desynced: bool,
encoded: list<u8>,
}
record attach-pane-grid-delta {
deltas: list<pane-grid-delta>,
}
record attach-pane-images {
// JSON-encoded `Vec<AttachPaneImageDelta>`.
encoded: list<u8>,
}
variant attach-state-error {
not-attached,
session-not-found,
failed { reason: string },
}
query attach-layout-state(
session_id: uuid,
) -> result<attach-layout, attach-state-error>;
query attach-snapshot-state(
session_id: uuid,
max_bytes_per_pane: u32,
) -> result<attach-snapshot, attach-state-error>;
query attach-pane-snapshot-state(
session_id: uuid,
pane_ids: list<uuid>,
max_bytes_per_pane: u32,
) -> result<attach-pane-snapshot, attach-state-error>;
query attach-pane-output-batch(
session_id: uuid,
pane_ids: list<uuid>,
max_bytes: u32,
) -> result<attach-pane-output-batch, attach-state-error>;
query attach-pane-grid-snapshot-state(
session_id: uuid,
pane_ids: list<uuid>,
max_rows_per_pane: u32,
) -> result<attach-pane-grid-snapshot, attach-state-error>;
query attach-pane-grid-window-state(
session_id: uuid,
windows: list<pane-grid-window-request>,
) -> result<attach-pane-grid-window, attach-state-error>;
query attach-pane-grid-delta-state(
session_id: uuid,
pane_ids: list<uuid>,
base_revisions: list<u64>,
max_batches_per_pane: u32,
) -> result<attach-pane-grid-delta, attach-state-error>;
query attach-pane-images(
session_id: uuid,
pane_ids: list<uuid>,
since_sequences: list<u64>,
) -> result<attach-pane-images, attach-state-error>;
}
interface pane-runtime-events {
enum attach-view-component {
scene,
surface-content,
layout,
status,
}
variant pane-event {
client-attached { session_id: uuid },
client-detached { session_id: uuid },
exited { session_id: uuid, pane_id: uuid, reason: string? },
restarted { session_id: uuid, pane_id: uuid },
output-available { session_id: uuid, pane_id: uuid },
image-available { session_id: uuid, pane_id: uuid },
attach-view-changed {
context_id: uuid?,
session_id: uuid,
revision: u64,
components: list<attach-view-component>,
},
}
events pane-event;
}
// Reactive state channel announcing the currently-focused pane (and
// the zoomed pane, if any) for every live session. Pane-runtime is
// the authoritative owner of this state; it publishes a fresh snapshot
// whenever any session's focus/zoom transitions, and also at attach
// time so newly-attached consumers observe the current state without
// an extra query round-trip.
//
// Payload semantics:
// - `entries` maps session-id to a per-session `focus-snapshot`. A
// session with no entry means "no focus state observed yet"; most
// consumers can ignore such gaps.
// - `focused_pane_id` is the pane currently receiving input focus.
// - `zoomed_pane_id` is the pane that has taken over the viewport
// via zoom mode; `None` means not zoomed.
// - `revision` is a monotonic counter that advances on every publish;
// consumers can use it to deduplicate or order updates.
//
// Subscribers use `EventBus::subscribe_state::<SessionFocusStateMap>`
// to obtain the current value synchronously plus a live-update
// receiver.
interface pane-runtime-focus {
record focus-snapshot {
focused_pane_id: uuid,
zoomed_pane_id: uuid?,
}
record session-focus-state-map {
entries: map<uuid, focus-snapshot>,
revision: u64,
}
@state events session-focus-state-map;
}