diff --git a/crates/spacesh-proto/src/lib.rs b/crates/spacesh-proto/src/lib.rs index cef3834..987b410 100644 --- a/crates/spacesh-proto/src/lib.rs +++ b/crates/spacesh-proto/src/lib.rs @@ -2,9 +2,11 @@ pub mod codec; pub mod ids; pub mod layout; pub mod message; +pub mod status; pub mod workspace; pub use ids::{GroupId, SurfaceId, WorkspaceId}; pub use layout::{LayoutNode, Orient}; pub use message::{Cmd, Envelope, ErrorBody, Evt}; +pub use status::SurfaceState; pub use workspace::{Group, SurfaceSpec, SurfaceView, Workspace, WorkspaceView}; diff --git a/crates/spacesh-proto/src/message.rs b/crates/spacesh-proto/src/message.rs index b8187a2..dc26265 100644 --- a/crates/spacesh-proto/src/message.rs +++ b/crates/spacesh-proto/src/message.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::ids::{GroupId, SurfaceId, WorkspaceId}; use crate::layout::LayoutNode; +use crate::status::SurfaceState; use crate::workspace::{Group, WorkspaceView}; /// Wire envelope. `kind` is the serde tag. @@ -114,6 +115,7 @@ pub enum Cmd { order: Option, }, DeleteGroup { group_id: GroupId }, + SetState { surface_id: SurfaceId, state: SurfaceState }, Status, Shutdown, } @@ -131,6 +133,7 @@ pub enum Evt { WorkspaceClosed { workspace_id: WorkspaceId }, GroupsChanged { groups: Vec }, SurfaceRestarted { surface_id: SurfaceId }, + State { surface_id: SurfaceId, state: SurfaceState }, } #[cfg(test)] @@ -240,4 +243,23 @@ mod tests { let back: Envelope = serde_json::from_str(&serde_json::to_string(&evt).unwrap()).unwrap(); assert_eq!(back, evt); } + + #[test] + fn set_state_round_trips() { + let env = Envelope::Req { + id: 1, + cmd: Cmd::SetState { surface_id: SurfaceId("s_1".into()), state: crate::status::SurfaceState::Done }, + }; + let back: Envelope = serde_json::from_str(&serde_json::to_string(&env).unwrap()).unwrap(); + assert_eq!(back, env); + } + + #[test] + fn state_event_round_trips() { + let evt = Envelope::Evt(Evt::State { surface_id: SurfaceId("s_1".into()), state: crate::status::SurfaceState::Wait }); + let j = serde_json::to_string(&evt).unwrap(); + assert!(j.contains(r#""evt":"state""#)); + let back: Envelope = serde_json::from_str(&j).unwrap(); + assert_eq!(back, evt); + } } diff --git a/crates/spacesh-proto/src/status.rs b/crates/spacesh-proto/src/status.rs new file mode 100644 index 0000000..ee14d64 --- /dev/null +++ b/crates/spacesh-proto/src/status.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +/// Ephemeral agent-activity status of a running surface (orthogonal to the +/// running/stopped process lifecycle). Defaults to `Idle`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SurfaceState { + Work, + Wait, + Done, + Error, + #[default] + Idle, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serializes_snake_case() { + assert_eq!(serde_json::to_string(&SurfaceState::Work).unwrap(), r#""work""#); + assert_eq!(serde_json::to_string(&SurfaceState::Idle).unwrap(), r#""idle""#); + } + + #[test] + fn default_is_idle() { + assert_eq!(SurfaceState::default(), SurfaceState::Idle); + } + + #[test] + fn round_trips() { + for s in [SurfaceState::Work, SurfaceState::Wait, SurfaceState::Done, SurfaceState::Error, SurfaceState::Idle] { + let j = serde_json::to_string(&s).unwrap(); + let back: SurfaceState = serde_json::from_str(&j).unwrap(); + assert_eq!(back, s); + } + } +} diff --git a/crates/spacesh-proto/src/workspace.rs b/crates/spacesh-proto/src/workspace.rs index 23ffea7..5a0c28a 100644 --- a/crates/spacesh-proto/src/workspace.rs +++ b/crates/spacesh-proto/src/workspace.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use crate::ids::{GroupId, SurfaceId, WorkspaceId}; use crate::layout::LayoutNode; +use crate::status::SurfaceState; /// Everything needed to (re)create a panel's process. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -50,6 +51,9 @@ pub struct SurfaceView { pub spec: SurfaceSpec, /// true = has a live actor/PTY; false = stopped (in tree, no process). pub running: bool, + /// Ephemeral agent-activity status (meaningful while running). + #[serde(default)] + pub state: SurfaceState, } /// Workspace view in `status` / `workspace_changed`: structure + per-surface state.