feat(daemon): per-surface status (set_state/state), idle-on-spawn, SPACESH_SOCK override

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 22:13:50 +07:00
parent 4bd4aa4a36
commit 635f9f4356
3 changed files with 114 additions and 2 deletions
+54 -1
View File
@@ -133,8 +133,8 @@ async fn router(
}
}
ServerMsg::Exit { surface_id, code } => {
// Transition running -> stopped; keep panel + tree.
reg.mark_stopped(&surface_id);
reg.drop_state(&surface_id);
let evt = Envelope::Evt(Evt::Exit { surface_id: surface_id.clone(), code });
broadcast_evt(&clients, &evt);
}
@@ -211,6 +211,7 @@ async fn handle_request(
Ok(handle) => {
spawn_output_bridge(sid.clone(), &handle, router_tx.clone());
reg.set_live(handle);
reg.set_state(&sid, spacesh_proto::SurfaceState::Idle);
reg.add_surface_spec(&workspace_id, sid.clone(), spec);
// First panel of an empty workspace becomes the root leaf.
if let Some(w) = reg.workspace_mut(&workspace_id) {
@@ -241,6 +242,7 @@ async fn handle_request(
Ok(handle) => {
spawn_output_bridge(new_sid.clone(), &handle, router_tx.clone());
reg.set_live(handle);
reg.set_state(&new_sid, spacesh_proto::SurfaceState::Idle);
reg.add_surface_spec(&ws_id, new_sid.clone(), spec);
let orient = match dir { SplitDir::Right => Orient::H, SplitDir::Down => Orient::V };
if let Some(w) = reg.workspace_mut(&ws_id) {
@@ -313,6 +315,7 @@ async fn handle_request(
Ok(handle) => {
spawn_output_bridge(new_sid.clone(), &handle, router_tx.clone());
reg.set_live(handle);
reg.set_state(&new_sid, spacesh_proto::SurfaceState::Idle);
reg.add_surface_spec(&workspace_id, new_sid.clone(), spec);
new_ids.push(new_sid);
}
@@ -342,6 +345,7 @@ async fn handle_request(
Ok(handle) => {
spawn_output_bridge(surface_id.clone(), &handle, router_tx.clone());
reg.set_live(handle);
reg.set_state(&surface_id, spacesh_proto::SurfaceState::Idle);
broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceRestarted { surface_id: surface_id.clone() }));
let _ = out.send(ok(id, serde_json::Value::Null)).await;
}
@@ -467,6 +471,17 @@ async fn handle_request(
}
}
Cmd::SetState { surface_id, state } => {
if reg.is_running(&surface_id) {
reg.set_state(&surface_id, state);
broadcast_evt(clients, &Envelope::Evt(Evt::State { surface_id: surface_id.clone(), state }));
let _ = out.send(ok(id, serde_json::Value::Null)).await;
} else {
// unknown or stopped surface — status is only meaningful while running.
let _ = out.send(err(id, "NOT_FOUND", "surface not running")).await;
}
}
Cmd::Status => {
let (groups, workspaces) = reg.status();
let _ = out.send(ok(id, serde_json::json!({ "groups": groups, "workspaces": workspaces }))).await;
@@ -677,6 +692,44 @@ mod tests {
assert!(w0["layout"].to_string().contains("split"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn set_state_updates_status_and_emits_event() {
let _serial = crate::test_support::serial();
let dir = tempdir_path();
let sock = dir.join("sock");
let store: std::sync::Arc<dyn crate::state_store::StateStore> =
std::sync::Arc::new(crate::state_store::JsonStateStore::new(dir.join("state.json")));
let sock2 = sock.clone();
tokio::spawn(async move { let _ = serve(&sock2, store).await; });
wait_for_socket(&sock).await;
let mut s = UnixStream::connect(&sock).await.unwrap();
let r = req(&mut s, 1, Cmd::Open { path: std::env::temp_dir().to_string_lossy().into() }).await;
let ws = res_data(&r)["workspace_id"].as_str().unwrap().to_string();
let r = req(&mut s, 2, Cmd::NewSurface {
workspace_id: spacesh_proto::WorkspaceId(ws.clone()),
command: Some("/bin/sh".into()),
args: vec!["-c".into(), "sleep 1".into()],
cols: 80, rows: 24,
}).await;
let sid = res_data(&r)["surface_id"].as_str().unwrap().to_string();
let surface_id = spacesh_proto::SurfaceId(sid.clone());
// set_state on the running surface
let r = req(&mut s, 3, Cmd::SetState { surface_id: surface_id.clone(), state: spacesh_proto::status::SurfaceState::Work }).await;
assert!(matches!(r, Envelope::Res { ok: true, .. }));
// status reflects it
let r = req(&mut s, 4, Cmd::Status).await;
let wss = res_data(&r)["workspaces"].as_array().unwrap();
let w0 = wss.iter().find(|w| w["id"] == ws).unwrap();
assert_eq!(w0["surfaces"][&sid]["state"], "work");
// unknown surface -> NOT_FOUND
let r = req(&mut s, 5, Cmd::SetState { surface_id: spacesh_proto::SurfaceId("s_nope".into()), state: spacesh_proto::status::SurfaceState::Done }).await;
match r { Envelope::Res { ok, error, .. } => { assert!(!ok); assert_eq!(error.unwrap().code, "NOT_FOUND"); }, _ => panic!() }
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cold_restart_restores_structure_stopped() {
let _serial = crate::test_support::serial();