diff --git a/app/src/LayoutEngine.tsx b/app/src/LayoutEngine.tsx
index 63b62f3..fa0870e 100644
--- a/app/src/LayoutEngine.tsx
+++ b/app/src/LayoutEngine.tsx
@@ -6,7 +6,7 @@ import { SearchBar } from "./SearchBar";
import { StatusRing } from "./StatusRing";
import { COLORS, FONT, STATE_COLOR } from "./theme";
import type { LayoutNode, SurfaceState, SurfaceView } from "./layoutTypes";
-import { setRatios, restartSurface, setZoom, moveSurface, attachSurface } from "./socketBridge";
+import { setRatios, restartSurface, setZoom, moveSurface, attachSurface, detachSurface } from "./socketBridge";
interface Props {
workspaceId: string;
@@ -149,7 +149,7 @@ function StoppedSnapshot({ surfaceId, font, palette }: { surfaceId: string; font
void attachSurface(surfaceId, () => {}).then((res) => {
if (!disposed && res.snapshot) term.write(res.snapshot);
});
- return () => { disposed = true; term.dispose(); };
+ return () => { disposed = true; term.dispose(); void detachSurface(surfaceId); };
}, [surfaceId, font, palette]); // eslint-disable-line react-hooks/exhaustive-deps
return
;
}
diff --git a/crates/spaceshd/src/server.rs b/crates/spaceshd/src/server.rs
index 148fa56..2307df5 100644
--- a/crates/spaceshd/src/server.rs
+++ b/crates/spaceshd/src/server.rs
@@ -453,17 +453,19 @@ async fn handle_request(
let Some(ws) = reg.workspace(&workspace_id).cloned() else {
let _ = out.send(err(id, "NOT_FOUND", "workspace")).await; return;
};
- // Kill current panels of this workspace.
- let existing: Vec = ws.surfaces.keys().cloned().collect();
- for sid in &existing {
- if let Some(h) = reg.live(sid) { let _ = h.tx.send(crate::surface::SurfaceMsg::Close).await; }
- reg.remove_surface(sid);
- subs.remove(sid);
- }
- // Spawn `count` panels (slots padded/truncated to count).
- let mut new_ids = Vec::new();
- for i in 0..count {
- let slot = slots.get(i);
+ // Additive: keep existing panels (and their live processes) in their
+ // current visual order, spawn only the delta needed to reach `count`,
+ // then rebuild the tree to the preset shape. Presets never destroy
+ // running panels — shrinking is done by closing panels via the X. The
+ // GUI only offers presets whose count >= the current pane count, so
+ // `count >= existing.len()` and `ids.len() == count` after the loop.
+ let existing: Vec = ws.layout.as_ref()
+ .map(spacesh_core::ops::leaves)
+ .unwrap_or_else(|| ws.surfaces.keys().cloned().collect());
+ let mut ids = existing.clone();
+ let to_spawn = count.saturating_sub(existing.len());
+ for j in 0..to_spawn {
+ let slot = slots.get(existing.len() + j);
let new_sid = reg.new_surface_id();
let command = slot.and_then(|s| s.command.clone());
let shell = command.clone().unwrap_or_else(|| config.resolved_shell());
@@ -476,20 +478,18 @@ async fn handle_request(
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);
+ broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceCreated { surface_id: new_sid.clone(), workspace_id: workspace_id.clone() }));
+ ids.push(new_sid);
}
Err(e) => { let _ = out.send(err(id, "SPAWN_FAILED", &e.to_string())).await; return; }
}
}
- if let Some(tree) = spacesh_core::presets::build(&preset_id, &new_ids) {
+ if let Some(tree) = spacesh_core::presets::build(&preset_id, &ids) {
if let Some(w) = reg.workspace_mut(&workspace_id) { w.layout = Some(tree); }
}
- for sid in &new_ids {
- broadcast_evt(clients, &Envelope::Evt(Evt::SurfaceCreated { surface_id: sid.clone(), workspace_id: workspace_id.clone() }));
- }
emit_layout(reg, &workspace_id, clients);
persister.mark_dirty(reg.persist_state());
- let _ = out.send(ok(id, serde_json::json!({ "surface_ids": new_ids.iter().map(|s| s.0.clone()).collect::>() }))).await;
+ let _ = out.send(ok(id, serde_json::json!({ "surface_ids": ids.iter().map(|s| s.0.clone()).collect::>() }))).await;
}
Cmd::RestartSurface { surface_id, resume } => {
@@ -732,6 +732,19 @@ async fn handle_request(
}
Cmd::Shutdown => {
+ // Final snapshot pass: capture each live surface's visible screen so a
+ // clean restart (e.g. Settings → Restart daemon) repaints last screens.
+ // Written synchronously through the store (the async writer task would
+ // not drain before process::exit).
+ for sid in reg.live_ids() {
+ let Some(handle) = reg.live(&sid) else { continue };
+ let (reply_tx, reply_rx) = oneshot::channel();
+ if handle.tx.send(SurfaceMsg::Snapshot { reply: reply_tx }).await.is_ok() {
+ if let Ok((snap, _dirty)) = reply_rx.await {
+ snapshot_store.save(&sid, &snap);
+ }
+ }
+ }
let _ = out.send(ok(id, serde_json::Value::Null)).await;
std::process::exit(0);
}