use bmux_attach_layout_protocol::{
PaneFocusDirection, PaneLaunchCommand, PaneSelector, PaneSplitDirection,
};
use bmux_pane_runtime_plugin_api::pane_runtime_commands::{
FloatingPaneAck, PaneAck, PaneCommandError, SessionAck, SessionRuntimeCommandError,
};
use bmux_pane_runtime_plugin_api::pane_runtime_events::{self, AttachViewComponent, PaneEvent};
use bmux_pane_runtime_state::{
FloatingPaneLayer, FloatingPaneScope, LayoutRect, PaneResizeDirection,
};
use bmux_presentation_state::{
PresentationEntityRef, PresentationFact, PresentationFactRole, PresentationFactSnapshot,
global_presentation_fact_host_service,
};
use bmux_session_models::{ClientId, SessionId};
use bmux_sessions_plugin_api::sessions_events::{self, SessionEvent};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
use tracing::warn;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SplitPaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
pub direction: String,
pub ratio_percent: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LaunchPaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
pub direction: String,
pub ratio_percent: u8,
#[serde(default)]
pub name: Option<String>,
pub program: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FocusPaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
pub direction: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResizePaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
pub direction: String,
#[serde(default = "default_resize_cells")]
pub cells: u16,
}
const fn default_resize_cells() -> u16 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClosePaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionPaneInputArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionPaneTargetArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnsureExecutionPaneArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
#[serde(default)]
pub name: Option<String>,
pub program: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub env: Vec<bmux_pane_runtime_plugin_api::pane_runtime_commands::EnvironmentEntry>,
pub rows: u16,
pub cols: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaneSetPtySizeArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub rows: u16,
pub cols: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaneSignalArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub signal: bmux_pane_runtime_plugin_api::pane_runtime_commands::PaneProcessSignal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestartPaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZoomPaneArgs {
pub session_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateFloatingPaneArgs {
pub session_id: Uuid,
#[serde(default)]
pub target: Option<Uuid>,
#[serde(default)]
pub anchor_pane_id: Option<Uuid>,
#[serde(default)]
pub context_id: Option<Uuid>,
#[serde(default)]
pub client_id: Option<Uuid>,
#[serde(default)]
pub x: Option<u16>,
#[serde(default)]
pub y: Option<u16>,
#[serde(default)]
pub w: Option<u16>,
#[serde(default)]
pub h: Option<u16>,
#[serde(default)]
pub z: Option<i32>,
#[serde(default)]
pub layer: Option<String>,
#[serde(default)]
pub scope: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub program: Option<String>,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub cwd: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FloatingPaneTargetArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MoveFloatingPaneArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResizeFloatingPaneArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub w: u16,
pub h: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetFloatingPaneZArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub z: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetFloatingPaneLayerArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
pub layer: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaneDirectInputArgs {
pub session_id: Uuid,
pub pane_id: Uuid,
#[serde(with = "bmux_plugin_sdk::codec::serde_bytes_vec")]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SetClientWritePermissionArgs {
pub session_id: Uuid,
pub client_id: Uuid,
pub allowed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NewSessionArgs {
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KillSessionArgs {
pub session_id: Uuid,
pub force_local: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreSessionArgs {
pub session_id: Uuid,
pub snapshot_payload: Vec<u8>,
}
fn failed_command(reason: impl Into<String>) -> PaneCommandError {
PaneCommandError::Failed {
reason: reason.into(),
}
}
fn failed_session(reason: impl Into<String>) -> SessionRuntimeCommandError {
SessionRuntimeCommandError::Failed {
reason: reason.into(),
}
}
fn ensure_session_mutation_allowed(
ctx: &bmux_plugin_sdk::NativeServiceContext,
session_id: SessionId,
action: &str,
) -> Result<(), PaneCommandError> {
super::spawn::authorize_session(ctx, session_id.0, action).map_err(|error| {
let reason = match error {
bmux_pane_runtime_plugin_api::pane_runtime_spawn_v1::SpawnError::Denied { reason } => {
reason
}
other => format!("session policy check failed: {other:?}"),
};
PaneCommandError::Denied { reason }
})
}
fn parse_split_direction(raw: &str) -> Result<PaneSplitDirection, PaneCommandError> {
match raw {
"horizontal" => Ok(PaneSplitDirection::Horizontal),
"vertical" => Ok(PaneSplitDirection::Vertical),
other => Err(failed_command(format!(
"invalid split direction: '{other}' (expected horizontal|vertical)"
))),
}
}
fn parse_focus_direction(raw: &str) -> Result<Option<PaneFocusDirection>, PaneCommandError> {
match raw {
"next" => Ok(Some(PaneFocusDirection::Next)),
"prev" | "previous" => Ok(Some(PaneFocusDirection::Prev)),
"" => Ok(None),
other => Err(failed_command(format!(
"invalid focus direction: '{other}'"
))),
}
}
fn parse_resize_direction(raw: &str) -> Result<PaneResizeDirection, PaneCommandError> {
match raw {
"increase" => Ok(PaneResizeDirection::Increase),
"decrease" => Ok(PaneResizeDirection::Decrease),
"left" => Ok(PaneResizeDirection::Left),
"right" => Ok(PaneResizeDirection::Right),
"up" => Ok(PaneResizeDirection::Up),
"down" => Ok(PaneResizeDirection::Down),
other => Err(failed_command(format!(
"invalid resize direction: '{other}' (expected increase|decrease|left|right|up|down)"
))),
}
}
fn parse_floating_scope(raw: Option<&str>) -> Result<FloatingPaneScope, PaneCommandError> {
match raw.unwrap_or("per_window") {
"per_pane" | "pane" => Ok(FloatingPaneScope::PerPane),
"per_window" | "per_tab" | "window" | "tab" => Ok(FloatingPaneScope::PerWindow),
"per_session" | "session" => Ok(FloatingPaneScope::PerSession),
"client_global" | "client" => Ok(FloatingPaneScope::ClientGlobal),
"server_global" | "global" | "server" => Ok(FloatingPaneScope::ServerGlobal),
other => Err(failed_command(format!(
"invalid floating pane scope: '{other}'"
))),
}
}
fn parse_floating_layer(raw: Option<&str>) -> Result<FloatingPaneLayer, PaneCommandError> {
match raw.unwrap_or("floating_pane") {
"pane" => Ok(FloatingPaneLayer::Pane),
"overlay" => Ok(FloatingPaneLayer::Overlay),
"floating" | "floating_pane" => Ok(FloatingPaneLayer::FloatingPane),
"tooltip" | "top" => Ok(FloatingPaneLayer::Tooltip),
other => Err(failed_command(format!(
"invalid floating pane layer: '{other}'"
))),
}
}
fn floating_ack(
session_id: SessionId,
summary: bmux_pane_runtime_state::FloatingPaneRuntimeSummary,
) -> FloatingPaneAck {
FloatingPaneAck {
session_id: session_id.0,
pane_id: summary.pane_id,
surface_id: summary.id,
}
}
fn target_selector(target: Option<Uuid>) -> Option<PaneSelector> {
target.map(PaneSelector::ById)
}
fn publish_session_removed_event(session_id: SessionId) {
let _ = bmux_plugin::global_event_bus().emit(
&sessions_events::EVENT_KIND,
SessionEvent::Removed {
session_id: session_id.0,
},
);
}
fn publish_pane_event(event: PaneEvent) {
let _ = bmux_plugin::global_event_bus().emit(&pane_runtime_events::EVENT_KIND, event);
}
static PANE_FACT_REVISION: AtomicU64 = AtomicU64::new(0);
pub(crate) fn publish_pane_lifecycle_facts() {
let Some(handle) = super::session_runtime_handle() else {
return;
};
let facts = handle
.0
.active_session_ids()
.into_iter()
.flat_map(|session_id| handle.0.list_panes(session_id).unwrap_or_default())
.map(|pane| PresentationFact {
entity: PresentationEntityRef::new("bmux.panes", pane.id.to_string()),
key: "lifecycle".to_string(),
role: if matches!(pane.state, bmux_attach_layout_protocol::PaneState::Exited) {
PresentationFactRole::Error
} else {
PresentationFactRole::Active
},
short_text: if matches!(pane.state, bmux_attach_layout_protocol::PaneState::Exited) {
"exited".to_string()
} else {
"running".to_string()
},
detail_text: pane.state_reason,
icon_id: Some(
if matches!(pane.state, bmux_attach_layout_protocol::PaneState::Exited) {
"pane.exited"
} else {
"pane.running"
}
.to_string(),
),
priority: 100,
})
.collect();
let _ = global_presentation_fact_host_service()
.registry()
.publish_authorized(
"bmux.pane_runtime",
"bmux.pane_runtime",
PresentationFactSnapshot {
revision: PANE_FACT_REVISION
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1),
facts,
},
);
}
fn emit_attach_view_changed_scene(session_id: SessionId) {
let Some(handle) = super::session_runtime_handle() else {
return;
};
let Some(revision) = handle.0.bump_attach_view_revision(session_id) else {
return;
};
publish_pane_event(PaneEvent::AttachViewChanged {
context_id: None,
session_id: session_id.0,
revision,
components: vec![AttachViewComponent::Scene],
});
super::publish_focus_state_snapshot();
publish_pane_lifecycle_facts();
}
fn emit_attach_view_changed_scene_all() {
let Some(handle) = super::session_runtime_handle() else {
return;
};
for session_id in handle.0.active_session_ids() {
emit_attach_view_changed_scene(session_id);
}
}
pub fn split_pane(
req: &SplitPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let direction = parse_split_direction(&req.direction)?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.split")?;
let pane_id = handle
.0
.split_pane(session_id, target_selector(req.target), direction)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene(session_id);
Ok(PaneAck {
session_id: req.session_id,
pane_id,
})
}
pub fn launch_pane(
req: LaunchPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let direction = parse_split_direction(&req.direction)?;
let command = PaneLaunchCommand {
program: req.program,
args: req.args,
cwd: req.cwd,
env: std::collections::BTreeMap::new(),
};
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.launch")?;
let pane_id = handle
.0
.launch_pane(
session_id,
target_selector(req.target),
direction,
req.name,
command,
)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene(session_id);
Ok(PaneAck {
session_id: req.session_id,
pane_id,
})
}
pub fn focus_pane(
req: &FocusPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.focus")?;
let pane_id = match (req.target, parse_focus_direction(&req.direction)?) {
(Some(t), None) => handle
.0
.focus_pane_target(session_id, &PaneSelector::ById(t)),
(None, Some(dir)) => handle.0.focus_pane(session_id, dir),
(None, None) => handle
.0
.focus_pane_target(session_id, &PaneSelector::Active),
(Some(_), Some(_)) => {
return Err(failed_command(
"focus-pane cannot use target and direction together",
));
}
}
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene(session_id);
Ok(PaneAck {
session_id: req.session_id,
pane_id,
})
}
pub fn resize_pane(
req: &ResizePaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<SessionAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.resize")?;
let direction = parse_resize_direction(&req.direction)?;
handle
.0
.resize_pane(
session_id,
target_selector(req.target),
direction,
req.cells.max(1),
)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene(session_id);
Ok(SessionAck {
session_id: req.session_id,
})
}
pub fn close_pane(
req: &ClosePaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
use bmux_plugin::global_plugin_state_registry;
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.close")?;
if handle
.0
.list_panes(session_id)
.map_err(|e| failed_command(e.to_string()))?
.len()
<= 1
{
return Err(PaneCommandError::Denied {
reason: "cannot close the final pane without choosing a new target or quitting"
.to_string(),
});
}
let (pane_id, removed_runtime) = handle
.0
.close_pane(session_id, target_selector(req.target))
.map_err(|e| failed_command(e.to_string()))?;
let session_closed = removed_runtime.is_some();
if let Some(removed) = removed_runtime {
let had_attached_clients = !removed.attached_clients.is_empty();
handle.0.shutdown_removed_runtime(removed);
if let Some(session_handle) = global_plugin_state_registry()
.get::<bmux_session_state::SessionManagerHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
{
let _ = session_handle.0.remove_session(session_id);
}
if let Some(context_handle) = global_plugin_state_registry()
.get::<bmux_context_state::ContextStateHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
{
let _ = context_handle.0.remove_contexts_for_session(session_id);
}
if let Some(follow_handle) = global_plugin_state_registry()
.get::<bmux_client_state::FollowStateHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
{
follow_handle.0.clear_selections_for_session(session_id);
}
if let Some(attach_tokens) = global_plugin_state_registry()
.get::<bmux_attach_token_state::AttachTokenManagerHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
{
attach_tokens.0.remove_for_session(session_id);
}
if had_attached_clients {
publish_pane_event(PaneEvent::ClientDetached {
session_id: session_id.0,
});
}
publish_session_removed_event(session_id);
}
if !session_closed {
emit_attach_view_changed_scene(session_id);
}
Ok(PaneAck {
session_id: req.session_id,
pane_id,
})
}
pub fn restart_pane(
req: &RestartPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.restart")?;
let pane_id = handle
.0
.restart_pane(session_id, target_selector(req.target))
.map_err(|e| failed_command(e.to_string()))?;
publish_pane_event(PaneEvent::Restarted {
session_id: session_id.0,
pane_id,
});
emit_attach_view_changed_scene(session_id);
Ok(PaneAck {
session_id: req.session_id,
pane_id,
})
}
pub fn zoom_pane(
req: &ZoomPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.zoom")?;
let (pane_id, _zoomed) = handle
.0
.toggle_zoom(session_id)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene(session_id);
Ok(PaneAck {
session_id: req.session_id,
pane_id,
})
}
pub fn create_floating_pane(
req: CreateFloatingPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.floating.create")?;
let command = req.program.map(|program| PaneLaunchCommand {
program,
args: req.args,
cwd: req.cwd,
env: std::collections::BTreeMap::new(),
});
let summary = handle
.0
.create_floating_pane(
session_id,
target_selector(req.target),
LayoutRect {
x: req.x.unwrap_or(2),
y: req.y.unwrap_or(2),
w: req.w.unwrap_or(80).max(1),
h: req.h.unwrap_or(24).max(1),
},
parse_floating_scope(req.scope.as_deref())?,
parse_floating_layer(req.layer.as_deref())?,
req.z.unwrap_or(0),
req.name,
command,
req.anchor_pane_id,
req.context_id,
req.client_id.or(ctx.caller_client_id).map(ClientId),
)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene_all();
Ok(floating_ack(session_id, summary))
}
pub fn move_floating_pane(
req: &MoveFloatingPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.floating.move")?;
let summary = handle
.0
.move_floating_pane(session_id, req.pane_id, req.x, req.y)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene_all();
Ok(floating_ack(session_id, summary))
}
pub fn resize_floating_pane(
req: &ResizeFloatingPaneArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.floating.resize")?;
let summary = handle
.0
.resize_floating_pane(session_id, req.pane_id, req.w.max(1), req.h.max(1))
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene_all();
Ok(floating_ack(session_id, summary))
}
fn mutate_floating_target(
req: &FloatingPaneTargetArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
action: &str,
mutate: impl FnOnce(
&bmux_pane_runtime_state::SessionRuntimeManagerHandle,
SessionId,
Uuid,
) -> anyhow::Result<bmux_pane_runtime_state::FloatingPaneRuntimeSummary>,
) -> Result<FloatingPaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, action)?;
let summary =
mutate(&handle, session_id, req.pane_id).map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene_all();
Ok(floating_ack(session_id, summary))
}
pub fn focus_floating_pane(
req: &FloatingPaneTargetArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
mutate_floating_target(
req,
ctx,
"pane.floating.focus",
|handle, session_id, pane_id| handle.0.focus_floating_pane(session_id, pane_id),
)
}
pub fn raise_floating_pane(
req: &FloatingPaneTargetArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
mutate_floating_target(
req,
ctx,
"pane.floating.raise",
|handle, session_id, pane_id| handle.0.raise_floating_pane(session_id, pane_id),
)
}
pub fn lower_floating_pane(
req: &FloatingPaneTargetArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
mutate_floating_target(
req,
ctx,
"pane.floating.lower",
|handle, session_id, pane_id| handle.0.lower_floating_pane(session_id, pane_id),
)
}
pub fn set_floating_pane_z(
req: &SetFloatingPaneZArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.floating.set_z")?;
let summary = handle
.0
.set_floating_pane_z(session_id, req.pane_id, req.z)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene_all();
Ok(floating_ack(session_id, summary))
}
pub fn set_floating_pane_layer(
req: &SetFloatingPaneLayerArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
ensure_session_mutation_allowed(ctx, session_id, "pane.floating.set_layer")?;
let summary = handle
.0
.set_floating_pane_layer(
session_id,
req.pane_id,
parse_floating_layer(Some(&req.layer))?,
)
.map_err(|e| failed_command(e.to_string()))?;
emit_attach_view_changed_scene_all();
Ok(floating_ack(session_id, summary))
}
pub fn close_floating_pane(
req: &FloatingPaneTargetArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<FloatingPaneAck, PaneCommandError> {
let ack = close_pane(
&ClosePaneArgs {
session_id: req.session_id,
target: Some(req.pane_id),
},
ctx,
)?;
emit_attach_view_changed_scene_all();
Ok(FloatingPaneAck {
session_id: ack.session_id,
pane_id: ack.pane_id,
surface_id: Uuid::nil(),
})
}
pub fn pane_direct_input(
req: PaneDirectInputArgs,
ctx: &bmux_plugin_sdk::NativeServiceContext,
) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
let client_id = ctx
.caller_client_id
.map(bmux_session_models::ClientId)
.ok_or_else(|| {
warn!(session_id = %req.session_id, pane_id = %req.pane_id, "pane direct input rejected: missing caller client id");
failed_command("pane direct input requires a caller client id")
})?;
if !handle.0.client_can_write(session_id, client_id) {
warn!(session_id = %req.session_id, pane_id = %req.pane_id, client_id = %client_id.0, "pane direct input rejected: client lacks write permission");
return Err(PaneCommandError::Denied {
reason: "client does not have write permission for this attach stream".to_string(),
});
}
match handle
.0
.write_input_to_pane(session_id, client_id, req.pane_id, req.data)
{
Ok(_) => {}
Err(error) => {
let reason = handle
.0
.pane_state_reason(session_id, req.pane_id)
.unwrap_or_else(|| error.to_string());
warn!(session_id = %req.session_id, pane_id = %req.pane_id, client_id = %client_id.0, %error, %reason, "pane direct input failed");
return Err(failed_command(reason));
}
}
Ok(PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
}
pub fn execution_pane_input(req: ExecutionPaneInputArgs) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
handle
.0
.write_input_to_pane(
SessionId(req.session_id),
ClientId(Uuid::nil()),
req.pane_id,
req.data,
)
.map_err(|error| failed_command(error.to_string()))?;
Ok(PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
}
pub fn destroy_execution_pane(req: &ExecutionPaneTargetArgs) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let session_id = SessionId(req.session_id);
if handle
.0
.pane_process_identity(session_id, req.pane_id)
.is_none()
{
return Err(failed_command("execution pane is missing"));
}
let removed = handle
.0
.remove_runtime(session_id)
.ok_or_else(|| failed_command("execution runtime is missing"))?;
handle.0.shutdown_removed_runtime(removed);
Ok(PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
}
pub fn ensure_execution_pane(req: EnsureExecutionPaneArgs) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
if req.rows == 0 || req.cols == 0 || req.program.trim().is_empty() {
return Err(failed_command(
"execution pane requires a program and positive dimensions",
));
}
let session_id = SessionId(req.session_id);
if handle.0.session_exists(session_id) {
return handle
.0
.pane_process_identity(session_id, req.pane_id)
.map(|_| PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
.ok_or_else(|| {
failed_command("execution session exists without its deterministic pane")
});
}
let pane = bmux_pane_runtime_state::PaneRuntimeMeta {
id: req.pane_id,
name: req.name,
shell: req.program.clone(),
launch: Some(bmux_pane_runtime_state::PaneLaunchSpec {
program: req.program,
args: req.args,
cwd: req.cwd,
env: req
.env
.into_iter()
.map(|entry| (entry.key, entry.value))
.collect(),
}),
resurrection: bmux_pane_runtime_state::PaneResurrectionSnapshot::default(),
};
handle
.0
.restore_runtime(
session_id,
&[pane],
bmux_pane_runtime_state::RestoreRuntimeRequest::with_focus(req.pane_id).layout_root(
Some(bmux_pane_runtime_state::PaneLayoutNode::Leaf {
pane_id: req.pane_id,
}),
),
)
.map_err(|error| failed_command(error.to_string()))?;
handle
.0
.set_pane_pty_size(session_id, req.pane_id, req.rows, req.cols)
.map_err(|error| failed_command(error.to_string()))?;
Ok(PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
}
pub fn pane_set_pty_size(req: &PaneSetPtySizeArgs) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
handle
.0
.set_pane_pty_size(SessionId(req.session_id), req.pane_id, req.rows, req.cols)
.map_err(|error| failed_command(error.to_string()))?;
Ok(PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
}
pub fn pane_signal(req: &PaneSignalArgs) -> Result<PaneAck, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
let signal = match req.signal {
bmux_pane_runtime_plugin_api::pane_runtime_commands::PaneProcessSignal::Interrupt => {
bmux_pane_runtime_state::PaneProcessSignal::Interrupt
}
bmux_pane_runtime_plugin_api::pane_runtime_commands::PaneProcessSignal::Terminate => {
bmux_pane_runtime_state::PaneProcessSignal::Terminate
}
bmux_pane_runtime_plugin_api::pane_runtime_commands::PaneProcessSignal::Kill => {
bmux_pane_runtime_state::PaneProcessSignal::Kill
}
bmux_pane_runtime_plugin_api::pane_runtime_commands::PaneProcessSignal::Hangup => {
bmux_pane_runtime_state::PaneProcessSignal::Hangup
}
};
handle
.0
.signal_pane_process(SessionId(req.session_id), req.pane_id, signal)
.map_err(|error| failed_command(error.to_string()))?;
Ok(PaneAck {
session_id: req.session_id,
pane_id: req.pane_id,
})
}
pub fn set_client_write_permission(
req: &SetClientWritePermissionArgs,
) -> Result<u8, PaneCommandError> {
let handle = super::session_runtime_handle()
.ok_or_else(|| failed_command("pane-runtime manager handle not registered"))?;
handle.0.set_client_write_permission(
SessionId(req.session_id),
bmux_session_models::ClientId(req.client_id),
req.allowed,
);
Ok(u8::from(req.allowed))
}
pub fn new_session_with_runtime(
req: &NewSessionArgs,
) -> Result<SessionAck, SessionRuntimeCommandError> {
use bmux_plugin::global_plugin_state_registry;
let runtime_handle = super::session_runtime_handle()
.ok_or_else(|| failed_session("pane-runtime manager handle not registered"))?;
let session_handle = global_plugin_state_registry()
.get::<bmux_session_state::SessionManagerHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
.ok_or_else(|| failed_session("session manager handle not registered"))?;
let session_id = session_handle
.0
.create_session(req.name.clone())
.map_err(|error| failed_session(format!("failed creating session: {error:#}")))?;
if let Err(error) = runtime_handle.0.start_runtime(session_id) {
return Err(runtime_creation_failure(
session_id,
&error,
session_handle.0.remove_session(session_id),
));
}
Ok(SessionAck {
session_id: session_id.0,
})
}
fn runtime_creation_failure(
session_id: SessionId,
startup_error: &anyhow::Error,
rollback: Result<(), impl std::fmt::Display>,
) -> SessionRuntimeCommandError {
let reason = match rollback {
Ok(()) => format!("failed creating session runtime: {startup_error:#}"),
Err(cleanup_error) => format!(
"failed creating session runtime: {startup_error:#}; \
failed rolling back newly created session {}: {cleanup_error}",
session_id.0
),
};
failed_session(reason)
}
pub fn kill_session_runtime(
req: &KillSessionArgs,
) -> Result<SessionAck, SessionRuntimeCommandError> {
use bmux_plugin::global_plugin_state_registry;
let runtime_handle = super::session_runtime_handle()
.ok_or_else(|| failed_session("pane-runtime manager handle not registered"))?;
let session_handle = global_plugin_state_registry()
.get::<bmux_session_state::SessionManagerHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
.ok_or_else(|| failed_session("session manager handle not registered"))?;
let context_handle = global_plugin_state_registry()
.get::<bmux_context_state::ContextStateHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
.ok_or_else(|| failed_session("context state handle not registered"))?;
let attach_token_handle = global_plugin_state_registry()
.get::<bmux_attach_token_state::AttachTokenManagerHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
.ok_or_else(|| failed_session("attach-token manager handle not registered"))?;
let follow_handle = global_plugin_state_registry()
.get::<bmux_client_state::FollowStateHandle>()
.and_then(|arc| arc.read().ok().map(|g| (*g).clone()))
.ok_or_else(|| failed_session("follow state handle not registered"))?;
let session_id = SessionId(req.session_id);
let _ = req.force_local;
if session_handle.0.remove_session(session_id).is_err() {
return Err(SessionRuntimeCommandError::SessionNotFound);
}
let _removed_contexts = context_handle.0.remove_contexts_for_session(session_id);
follow_handle.0.clear_selections_for_session(session_id);
let Some(removed_runtime) = runtime_handle.0.remove_runtime(session_id) else {
return Err(failed_session(format!(
"failed stopping session runtime: session {} not found",
session_id.0
)));
};
let had_attached_clients = !removed_runtime.attached_clients.is_empty();
runtime_handle.0.shutdown_removed_runtime(removed_runtime);
attach_token_handle.0.remove_for_session(session_id);
if had_attached_clients {
publish_pane_event(PaneEvent::ClientDetached {
session_id: session_id.0,
});
}
publish_session_removed_event(session_id);
Ok(SessionAck {
session_id: req.session_id,
})
}
pub fn restore_session_runtime() -> Result<SessionAck, SessionRuntimeCommandError> {
Err(failed_session(
"restore-session-runtime is driven by the snapshot orchestrator on startup; \
clients should not invoke it directly",
))
}
#[cfg(test)]
mod creation_failure_tests {
use super::*;
#[test]
fn rollback_failure_preserves_both_errors_and_resource_identity() {
let session_id = SessionId(Uuid::new_v4());
let error = runtime_creation_failure(
session_id,
&anyhow::anyhow!("runtime unavailable"),
Err("storage unavailable"),
);
let SessionRuntimeCommandError::Failed { reason } = error else {
panic!("expected explicit failure");
};
assert!(reason.contains("runtime unavailable"));
assert!(reason.contains("storage unavailable"));
assert!(reason.contains(&session_id.0.to_string()));
}
#[test]
fn successful_rollback_does_not_claim_cleanup_failed() {
let error = runtime_creation_failure(
SessionId(Uuid::new_v4()),
&anyhow::anyhow!("runtime unavailable"),
Ok::<(), String>(()),
);
let SessionRuntimeCommandError::Failed { reason } = error else {
panic!("expected startup failure");
};
assert_eq!(
reason,
"failed creating session runtime: runtime unavailable"
);
}
}