feat: add interactive VT and workspace command plane
CI / rust (push) Successful in 3m48s

This commit is contained in:
2026-08-31 17:05:49 -07:00
parent 32d190c6c6
commit b838d000db
54 changed files with 3004 additions and 113 deletions
+9
View File
@@ -2,6 +2,15 @@
use std::fmt;
mod workspace;
pub use workspace::{
CommandOrigin, CommandRequestId, LayoutNode, PaneCloseDisposition, PaneDefinition, PaneId,
PaneLaunchIntent, PanePlacement, PaneSurface, SplitAxis, SplitRatio, WorkspaceApplyOutcome,
WorkspaceCapability, WorkspaceCommand, WorkspaceError, WorkspaceEvent, WorkspaceId,
WorkspaceRequest, WorkspaceState,
};
/// The transport Lumbridge uses to reach a user-owned remote machine.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RemoteTransport {
+861
View File
@@ -0,0 +1,861 @@
//! Deterministic workspace command plane shared by UI, CLI, and agents.
use crate::ExecutionTarget;
use std::collections::BTreeMap;
use std::fmt;
use thiserror::Error;
const MIN_SPLIT_PERMILLE: u16 = 100;
const MAX_SPLIT_PERMILLE: u16 = 900;
const MAX_IDENTITY_BYTES: usize = 256;
macro_rules! string_id {
($name:ident, $label:literal) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
/// Creates a stable, non-empty identifier.
///
/// # Errors
///
#[doc = concat!("Returns [`WorkspaceError::InvalidIdentifier`] for an empty ", $label, ".")]
pub fn new(value: impl Into<String>) -> Result<Self, WorkspaceError> {
let value = value.into();
if value.trim().is_empty() || value.chars().any(char::is_control) {
return Err(WorkspaceError::InvalidIdentifier($label));
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
};
}
string_id!(WorkspaceId, "workspace ID");
string_id!(PaneId, "pane ID");
string_id!(CommandRequestId, "command request ID");
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum WorkspaceCapability {
Observe,
Configure,
Execute,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CommandOrigin {
Human,
Agent { identity: String },
Automation { client: String },
}
impl CommandOrigin {
fn validate(&self) -> Result<(), WorkspaceError> {
let value = match self {
Self::Human => return Ok(()),
Self::Agent { identity } => identity,
Self::Automation { client } => client,
};
if value.trim().is_empty()
|| value.len() > MAX_IDENTITY_BYTES
|| value.chars().any(char::is_control)
{
return Err(WorkspaceError::InvalidOrigin);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PaneSurface {
Terminal,
Markdown,
Browser,
Tools,
Context,
Goal,
Review,
}
/// A credential-free launch reference. Executable arguments and secret values
/// are deliberately absent from the workspace plan.
#[derive(Clone, Eq, PartialEq)]
pub enum PaneLaunchIntent {
UserShell {
working_directory: Option<String>,
},
HarnessProfile {
profile_id: String,
working_directory: Option<String>,
},
}
/// Pane metadata suitable for persistence and automation. It intentionally has
/// no `Debug` implementation because titles and paths may be private.
#[derive(Clone, Eq, PartialEq)]
pub struct PaneDefinition {
pub id: PaneId,
pub title: String,
pub surface: PaneSurface,
pub execution_target: ExecutionTarget,
pub launch: Option<PaneLaunchIntent>,
}
impl PaneDefinition {
/// Validates a pane definition before it reaches the layout state.
///
/// # Errors
///
/// Returns [`WorkspaceError::InvalidPane`] for an empty title or for a
/// launch intent on a non-terminal pane.
pub fn validate(&self) -> Result<(), WorkspaceError> {
if self.title.trim().is_empty() || self.title.chars().any(char::is_control) {
return Err(WorkspaceError::InvalidPane("pane title"));
}
if self.launch.is_some() && self.surface != PaneSurface::Terminal {
return Err(WorkspaceError::InvalidPane(
"only terminal panes may carry a launch intent",
));
}
match &self.launch {
Some(PaneLaunchIntent::HarnessProfile { profile_id, .. })
if profile_id.trim().is_empty() || profile_id.chars().any(char::is_control) =>
{
Err(WorkspaceError::InvalidPane("harness profile ID"))
}
_ => Ok(()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SplitAxis {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PanePlacement {
Before,
After,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PaneCloseDisposition {
Detach,
Terminate,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SplitRatio(u16);
impl SplitRatio {
/// Creates the first child's share in permille.
///
/// # Errors
///
/// Returns [`WorkspaceError::InvalidSplitRatio`] outside 100..=900.
pub fn from_permille(value: u16) -> Result<Self, WorkspaceError> {
if !(MIN_SPLIT_PERMILLE..=MAX_SPLIT_PERMILLE).contains(&value) {
return Err(WorkspaceError::InvalidSplitRatio(value));
}
Ok(Self(value))
}
#[must_use]
pub const fn permille(self) -> u16 {
self.0
}
}
impl Default for SplitRatio {
fn default() -> Self {
Self(500)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LayoutNode {
Pane(PaneId),
Split {
axis: SplitAxis,
ratio: SplitRatio,
first: Box<Self>,
second: Box<Self>,
},
}
impl LayoutNode {
fn contains(&self, pane: &PaneId) -> bool {
match self {
Self::Pane(candidate) => candidate == pane,
Self::Split { first, second, .. } => first.contains(pane) || second.contains(pane),
}
}
fn split(
&mut self,
target: &PaneId,
new_pane: PaneId,
axis: SplitAxis,
ratio: SplitRatio,
placement: PanePlacement,
) -> bool {
match self {
Self::Pane(candidate) if candidate == target => {
let existing = Self::Pane(candidate.clone());
let inserted = Self::Pane(new_pane);
let (first, second) = match placement {
PanePlacement::Before => (inserted, existing),
PanePlacement::After => (existing, inserted),
};
*self = Self::Split {
axis,
ratio,
first: Box::new(first),
second: Box::new(second),
};
true
}
Self::Pane(_) => false,
Self::Split { first, second, .. } => {
first.split(target, new_pane.clone(), axis, ratio, placement)
|| second.split(target, new_pane, axis, ratio, placement)
}
}
}
fn remove(self, target: &PaneId) -> Option<Self> {
match self {
Self::Pane(pane) => (pane != *target).then_some(Self::Pane(pane)),
Self::Split {
axis,
ratio,
first,
second,
} => match (first.remove(target), second.remove(target)) {
(Some(first), Some(second)) => Some(Self::Split {
axis,
ratio,
first: Box::new(first),
second: Box::new(second),
}),
(Some(remaining), None) | (None, Some(remaining)) => Some(remaining),
(None, None) => None,
},
}
}
fn first_pane(&self) -> &PaneId {
match self {
Self::Pane(pane) => pane,
Self::Split { first, .. } => first.first_pane(),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub enum WorkspaceCommand {
SplitPane {
target: PaneId,
pane: PaneDefinition,
axis: SplitAxis,
ratio: SplitRatio,
placement: PanePlacement,
},
SelectPane {
pane: PaneId,
},
RenamePane {
pane: PaneId,
title: String,
},
ClosePane {
pane: PaneId,
disposition: PaneCloseDisposition,
},
}
impl WorkspaceCommand {
#[must_use]
pub const fn required_capability(&self) -> WorkspaceCapability {
match self {
Self::ClosePane {
disposition: PaneCloseDisposition::Terminate,
..
} => WorkspaceCapability::Execute,
Self::SplitPane { pane, .. } if pane.launch.is_some() => WorkspaceCapability::Execute,
Self::SplitPane { .. }
| Self::SelectPane { .. }
| Self::RenamePane { .. }
| Self::ClosePane {
disposition: PaneCloseDisposition::Detach,
..
} => WorkspaceCapability::Configure,
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct WorkspaceRequest {
pub request_id: CommandRequestId,
pub origin: CommandOrigin,
pub command: WorkspaceCommand,
}
/// State transition emitted after an accepted command. Transcript or input
/// content never appears in these events.
#[derive(Clone, Eq, PartialEq)]
pub enum WorkspaceEvent {
PaneSplit {
target: PaneId,
pane: PaneId,
},
PaneSelected {
pane: PaneId,
},
PaneRenamed {
pane: PaneId,
},
PaneClosed {
pane: PaneId,
disposition: PaneCloseDisposition,
},
}
pub struct WorkspaceApplyOutcome {
pub revision: u64,
pub duplicate: bool,
pub event: Option<WorkspaceEvent>,
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum WorkspaceError {
#[error("invalid {0}")]
InvalidIdentifier(&'static str),
#[error("invalid {0}")]
InvalidPane(&'static str),
#[error("split ratio {0} must be between 100 and 900 permille")]
InvalidSplitRatio(u16),
#[error("pane does not exist: {}", .0.as_str())]
PaneNotFound(PaneId),
#[error("pane already exists: {}", .0.as_str())]
PaneAlreadyExists(PaneId),
#[error("command origin identity is invalid")]
InvalidOrigin,
#[error("request ID was already used for a different request: {}", .0.as_str())]
RequestConflict(CommandRequestId),
#[error("the last pane cannot be closed")]
CannotCloseLastPane,
#[error("workspace command requires {required:?} capability but only {granted:?} was granted")]
CapabilityDenied {
required: WorkspaceCapability,
granted: WorkspaceCapability,
},
}
/// Deterministic layout state shared by all command-plane frontends. It has no
/// `Debug` implementation because pane metadata may contain local paths.
#[derive(Clone, Eq, PartialEq)]
pub struct WorkspaceState {
id: WorkspaceId,
name: String,
panes: BTreeMap<PaneId, PaneDefinition>,
layout: LayoutNode,
selected: PaneId,
applied_requests: BTreeMap<CommandRequestId, WorkspaceRequest>,
revision: u64,
}
impl WorkspaceState {
/// Creates a workspace with one root pane.
///
/// # Errors
///
/// Returns [`WorkspaceError::InvalidPane`] for invalid names or pane
/// definitions.
pub fn new(
id: WorkspaceId,
name: impl Into<String>,
root: PaneDefinition,
) -> Result<Self, WorkspaceError> {
let name = name.into();
if name.trim().is_empty() || name.chars().any(char::is_control) {
return Err(WorkspaceError::InvalidPane("workspace name"));
}
root.validate()?;
let selected = root.id.clone();
let layout = LayoutNode::Pane(root.id.clone());
let panes = BTreeMap::from([(root.id.clone(), root)]);
Ok(Self {
id,
name,
panes,
layout,
selected,
applied_requests: BTreeMap::new(),
revision: 0,
})
}
#[must_use]
pub const fn id(&self) -> &WorkspaceId {
&self.id
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn layout(&self) -> &LayoutNode {
&self.layout
}
#[must_use]
pub const fn selected(&self) -> &PaneId {
&self.selected
}
#[must_use]
pub const fn revision(&self) -> u64 {
self.revision
}
#[must_use]
pub fn pane(&self, id: &PaneId) -> Option<&PaneDefinition> {
self.panes.get(id)
}
#[must_use]
pub fn pane_count(&self) -> usize {
self.panes.len()
}
/// Applies one idempotent command after checking its required capability.
///
/// # Errors
///
/// Rejects insufficient authority and invalid state transitions without
/// changing the workspace.
pub fn apply(
&mut self,
request: WorkspaceRequest,
granted: WorkspaceCapability,
) -> Result<WorkspaceApplyOutcome, WorkspaceError> {
if let Some(applied) = self.applied_requests.get(&request.request_id) {
if applied == &request {
return Ok(WorkspaceApplyOutcome {
revision: self.revision,
duplicate: true,
event: None,
});
}
return Err(WorkspaceError::RequestConflict(request.request_id));
}
request.origin.validate()?;
let required = request.command.required_capability();
if granted < required {
return Err(WorkspaceError::CapabilityDenied { required, granted });
}
let event = self.apply_command(request.command.clone())?;
self.applied_requests
.insert(request.request_id.clone(), request);
self.revision = self.revision.saturating_add(1);
Ok(WorkspaceApplyOutcome {
revision: self.revision,
duplicate: false,
event: Some(event),
})
}
/// Applies a setup plan atomically. Any rejected command leaves the
/// original workspace unchanged.
///
/// # Errors
///
/// Returns the first command error without committing partial state.
pub fn apply_plan(
&mut self,
requests: impl IntoIterator<Item = WorkspaceRequest>,
granted: WorkspaceCapability,
) -> Result<Vec<WorkspaceApplyOutcome>, WorkspaceError> {
let mut candidate = self.clone();
let mut outcomes = Vec::new();
for request in requests {
outcomes.push(candidate.apply(request, granted)?);
}
*self = candidate;
Ok(outcomes)
}
fn apply_command(
&mut self,
command: WorkspaceCommand,
) -> Result<WorkspaceEvent, WorkspaceError> {
match command {
WorkspaceCommand::SplitPane {
target,
pane,
axis,
ratio,
placement,
} => {
if !self.layout.contains(&target) {
return Err(WorkspaceError::PaneNotFound(target));
}
if self.panes.contains_key(&pane.id) {
return Err(WorkspaceError::PaneAlreadyExists(pane.id));
}
pane.validate()?;
let pane_id = pane.id.clone();
let did_split = self
.layout
.split(&target, pane_id.clone(), axis, ratio, placement);
debug_assert!(did_split, "target presence was checked");
self.panes.insert(pane_id.clone(), pane);
self.selected = pane_id.clone();
Ok(WorkspaceEvent::PaneSplit {
target,
pane: pane_id,
})
}
WorkspaceCommand::SelectPane { pane } => {
if !self.panes.contains_key(&pane) {
return Err(WorkspaceError::PaneNotFound(pane));
}
self.selected = pane.clone();
Ok(WorkspaceEvent::PaneSelected { pane })
}
WorkspaceCommand::RenamePane { pane, title } => {
if title.trim().is_empty() || title.chars().any(char::is_control) {
return Err(WorkspaceError::InvalidPane("pane title"));
}
let definition = self
.panes
.get_mut(&pane)
.ok_or_else(|| WorkspaceError::PaneNotFound(pane.clone()))?;
definition.title = title;
Ok(WorkspaceEvent::PaneRenamed { pane })
}
WorkspaceCommand::ClosePane { pane, disposition } => {
if self.panes.len() == 1 && self.panes.contains_key(&pane) {
return Err(WorkspaceError::CannotCloseLastPane);
}
if !self.panes.contains_key(&pane) {
return Err(WorkspaceError::PaneNotFound(pane));
}
let layout = self
.layout
.clone()
.remove(&pane)
.ok_or(WorkspaceError::CannotCloseLastPane)?;
self.panes.remove(&pane);
self.layout = layout;
if self.selected == pane {
self.selected = self.layout.first_pane().clone();
}
Ok(WorkspaceEvent::PaneClosed { pane, disposition })
}
}
}
}
impl fmt::Debug for WorkspaceEvent {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PaneSplit { target, pane } => formatter
.debug_struct("PaneSplit")
.field("target", target)
.field("pane", pane)
.finish(),
Self::PaneSelected { pane } => formatter
.debug_struct("PaneSelected")
.field("pane", pane)
.finish(),
Self::PaneRenamed { pane } => formatter
.debug_struct("PaneRenamed")
.field("pane", pane)
.finish(),
Self::PaneClosed { pane, disposition } => formatter
.debug_struct("PaneClosed")
.field("pane", pane)
.field("disposition", disposition)
.finish(),
}
}
}
#[cfg(test)]
mod tests {
use super::{
CommandOrigin, CommandRequestId, LayoutNode, PaneCloseDisposition, PaneDefinition, PaneId,
PaneLaunchIntent, PanePlacement, PaneSurface, SplitAxis, SplitRatio, WorkspaceCapability,
WorkspaceCommand, WorkspaceError, WorkspaceId, WorkspaceRequest, WorkspaceState,
};
use crate::ExecutionTarget;
fn id<T>(constructor: impl FnOnce(String) -> Result<T, WorkspaceError>, value: &str) -> T {
constructor(value.to_owned()).unwrap()
}
fn pane(value: &str, launch: bool) -> PaneDefinition {
PaneDefinition {
id: id(PaneId::new, value),
title: value.to_owned(),
surface: PaneSurface::Terminal,
execution_target: ExecutionTarget::Local,
launch: launch.then_some(PaneLaunchIntent::UserShell {
working_directory: None,
}),
}
}
fn request(value: &str, command: WorkspaceCommand) -> WorkspaceRequest {
WorkspaceRequest {
request_id: id(CommandRequestId::new, value),
origin: CommandOrigin::Agent {
identity: "test-agent".into(),
},
command,
}
}
fn workspace() -> WorkspaceState {
WorkspaceState::new(id(WorkspaceId::new, "ws"), "Workspace", pane("root", false)).unwrap()
}
#[test]
fn agent_plan_builds_a_deterministic_split_tree() {
let mut state = workspace();
let requests = [
request(
"split-1",
WorkspaceCommand::SplitPane {
target: id(PaneId::new, "root"),
pane: pane("agent", false),
axis: SplitAxis::Horizontal,
ratio: SplitRatio::default(),
placement: PanePlacement::After,
},
),
request(
"split-2",
WorkspaceCommand::SplitPane {
target: id(PaneId::new, "agent"),
pane: pane("tests", false),
axis: SplitAxis::Vertical,
ratio: SplitRatio::from_permille(600).unwrap(),
placement: PanePlacement::Before,
},
),
];
let outcomes = state
.apply_plan(requests, WorkspaceCapability::Configure)
.unwrap();
assert_eq!(outcomes.len(), 2);
assert_eq!(state.pane_count(), 3);
assert_eq!(state.selected().as_str(), "tests");
assert_eq!(state.revision(), 2);
assert!(matches!(state.layout(), LayoutNode::Split { .. }));
}
#[test]
fn repeated_request_is_idempotent() {
let mut state = workspace();
let command = request(
"select-root",
WorkspaceCommand::SelectPane {
pane: id(PaneId::new, "root"),
},
);
let first = state
.apply(command.clone(), WorkspaceCapability::Configure)
.unwrap();
let second = state
.apply(command, WorkspaceCapability::Configure)
.unwrap();
assert!(!first.duplicate);
assert!(second.duplicate);
assert_eq!(state.revision(), 1);
}
#[test]
fn reused_request_id_must_match_origin_and_command() {
let mut state = workspace();
state
.apply(
request(
"shared-id",
WorkspaceCommand::SelectPane {
pane: id(PaneId::new, "root"),
},
),
WorkspaceCapability::Configure,
)
.unwrap();
let conflicting = request(
"shared-id",
WorkspaceCommand::RenamePane {
pane: id(PaneId::new, "root"),
title: "Different".into(),
},
);
assert!(matches!(
state.apply(conflicting, WorkspaceCapability::Configure),
Err(WorkspaceError::RequestConflict(_))
));
assert_eq!(state.pane(&id(PaneId::new, "root")).unwrap().title, "root");
assert_eq!(state.revision(), 1);
}
#[test]
fn rejects_unattributed_agent_requests() {
let mut state = workspace();
let request = WorkspaceRequest {
request_id: id(CommandRequestId::new, "origin"),
origin: CommandOrigin::Agent {
identity: "\n".into(),
},
command: WorkspaceCommand::SelectPane {
pane: id(PaneId::new, "root"),
},
};
assert!(matches!(
state.apply(request, WorkspaceCapability::Configure),
Err(WorkspaceError::InvalidOrigin)
));
assert_eq!(state.revision(), 0);
}
#[test]
fn launch_and_termination_require_execute_capability() {
let mut state = workspace();
let launch = request(
"launch",
WorkspaceCommand::SplitPane {
target: id(PaneId::new, "root"),
pane: pane("shell", true),
axis: SplitAxis::Horizontal,
ratio: SplitRatio::default(),
placement: PanePlacement::After,
},
);
assert!(matches!(
state.apply(launch.clone(), WorkspaceCapability::Configure),
Err(WorkspaceError::CapabilityDenied { .. })
));
state.apply(launch, WorkspaceCapability::Execute).unwrap();
let terminate = request(
"terminate",
WorkspaceCommand::ClosePane {
pane: id(PaneId::new, "shell"),
disposition: PaneCloseDisposition::Terminate,
},
);
assert!(matches!(
state.apply(terminate, WorkspaceCapability::Configure),
Err(WorkspaceError::CapabilityDenied { .. })
));
}
#[test]
fn failed_plan_is_atomic() {
let mut state = workspace();
let before = state.clone();
let requests = [
request(
"good",
WorkspaceCommand::SplitPane {
target: id(PaneId::new, "root"),
pane: pane("new", false),
axis: SplitAxis::Horizontal,
ratio: SplitRatio::default(),
placement: PanePlacement::After,
},
),
request(
"bad",
WorkspaceCommand::SelectPane {
pane: id(PaneId::new, "missing"),
},
),
];
assert!(
state
.apply_plan(requests, WorkspaceCapability::Configure)
.is_err()
);
assert!(state == before);
}
#[test]
fn close_promotes_sibling_and_never_closes_last_pane() {
let mut state = workspace();
state
.apply(
request(
"split",
WorkspaceCommand::SplitPane {
target: id(PaneId::new, "root"),
pane: pane("second", false),
axis: SplitAxis::Horizontal,
ratio: SplitRatio::default(),
placement: PanePlacement::After,
},
),
WorkspaceCapability::Configure,
)
.unwrap();
state
.apply(
request(
"close",
WorkspaceCommand::ClosePane {
pane: id(PaneId::new, "second"),
disposition: PaneCloseDisposition::Detach,
},
),
WorkspaceCapability::Configure,
)
.unwrap();
assert_eq!(state.selected().as_str(), "root");
assert_eq!(state.pane_count(), 1);
assert!(matches!(
state.apply(
request(
"last",
WorkspaceCommand::ClosePane {
pane: id(PaneId::new, "root"),
disposition: PaneCloseDisposition::Detach,
},
),
WorkspaceCapability::Configure
),
Err(WorkspaceError::CannotCloseLastPane)
));
}
#[test]
fn rejects_invalid_identifiers_ratios_and_launch_surfaces() {
assert!(PaneId::new(" ").is_err());
assert!(SplitRatio::from_permille(99).is_err());
let invalid = PaneDefinition {
id: id(PaneId::new, "browser"),
title: "Browser".into(),
surface: PaneSurface::Browser,
execution_target: ExecutionTarget::Local,
launch: Some(PaneLaunchIntent::UserShell {
working_directory: None,
}),
};
assert!(invalid.validate().is_err());
}
}