From 0a67f401c4ca563263f7f707b78190f6b5a9201e Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Mon, 15 Jun 2026 16:32:25 +0700 Subject: [PATCH] Update version to 0.1.3 Add daemon version check and restart logic Add pane count to CenterToolbar Add minSlots filter to PresetPicker --- Cargo.lock | 10 +++---- Cargo.toml | 2 +- app/src-tauri/Cargo.lock | 2 +- app/src-tauri/src/bridge.rs | 52 +++++++++++++++++++++++++++++++++-- app/src-tauri/tauri.conf.json | 2 +- app/src/App.tsx | 2 +- app/src/CenterToolbar.tsx | 4 +-- app/src/PresetPicker.tsx | 6 ++-- 8 files changed, 65 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0bd3b75..0110835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -869,7 +869,7 @@ dependencies = [ [[package]] name = "spacesh-cli" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "clap", @@ -881,7 +881,7 @@ dependencies = [ [[package]] name = "spacesh-core" -version = "0.1.2" +version = "0.1.3" dependencies = [ "alacritty_terminal", "serde", @@ -891,7 +891,7 @@ dependencies = [ [[package]] name = "spacesh-proto" -version = "0.1.2" +version = "0.1.3" dependencies = [ "bytes", "serde", @@ -903,7 +903,7 @@ dependencies = [ [[package]] name = "spacesh-pty" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "bytes", @@ -913,7 +913,7 @@ dependencies = [ [[package]] name = "spaceshd" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 4dc131e..5cfa664 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ [workspace.package] edition = "2021" -version = "0.1.2" +version = "0.1.3" [workspace.dependencies] tokio = { version = "1", features = ["full"] } diff --git a/app/src-tauri/Cargo.lock b/app/src-tauri/Cargo.lock index 07fe3e3..6f38403 100644 --- a/app/src-tauri/Cargo.lock +++ b/app/src-tauri/Cargo.lock @@ -3440,7 +3440,7 @@ dependencies = [ [[package]] name = "spacesh-proto" -version = "0.1.2" +version = "0.1.3" dependencies = [ "bytes", "serde", diff --git a/app/src-tauri/src/bridge.rs b/app/src-tauri/src/bridge.rs index 244d052..d4bfe65 100644 --- a/app/src-tauri/src/bridge.rs +++ b/app/src-tauri/src/bridge.rs @@ -78,6 +78,13 @@ fn find_daemon() -> PathBuf { PathBuf::from("spaceshd") // last resort: rely on PATH } +/// The installed `spaceshd` binary's mtime as ms since the epoch (for staleness check). +fn daemon_bin_mtime_ms() -> Option { + let meta = std::fs::metadata(find_daemon()).ok()?; + let mtime = meta.modified().ok()?; + Some(mtime.duration_since(std::time::UNIX_EPOCH).ok()?.as_millis() as u64) +} + async fn ensure_daemon(sock: &PathBuf) -> Result { if let Ok(s) = UnixStream::connect(sock).await { return Ok(s); @@ -120,7 +127,7 @@ impl Bridge { let pending: Arc>>> = Arc::default(); let out_channels: Arc>>>> = Arc::default(); let (tx, reader) = spawn_connection(&sock, &app, pending.clone(), out_channels.clone()).await?; - Ok(Self { + let bridge = Self { next_id: AtomicU64::new(1), app, sock, @@ -130,7 +137,48 @@ impl Bridge { reader: Mutex::new(reader), pending, out_channels, - }) + }; + // The daemon outlives the GUI by design, so after an update the GUI may + // attach to a stale daemon — new features that need new daemon code then + // silently don't work. Restart it if it's out of date. + bridge.ensure_matching_daemon().await; + Ok(bridge) + } + + /// Restart the running daemon if it predates the installed `spaceshd` binary + /// (or was built from a different commit). The bundled binary's mtime vs the + /// daemon's `started_at_ms` is the reliable signal: it catches every reinstall + /// even while developing dirty, where the git build id doesn't change. + async fn ensure_matching_daemon(&self) { + let Ok(reply) = self.request(Cmd::Health).await else { return }; + let (daemon_build, started_at_ms) = match &reply { + Envelope::Res { data, .. } => ( + data.get("build").and_then(|v| v.as_str()).map(str::to_string), + data.get("started_at_ms").and_then(|v| v.as_u64()), + ), + _ => (None, None), + }; + let gui_build = option_env!("SPACESH_BUILD").unwrap_or("dev"); + let build_mismatch = gui_build != "dev" + && daemon_build.as_deref().map(|b| b != gui_build).unwrap_or(false); + let binary_newer = match (daemon_bin_mtime_ms(), started_at_ms) { + (Some(bin_ms), Some(start_ms)) => bin_ms > start_ms, + _ => false, + }; + if !build_mismatch && !binary_newer { + return; + } + // Ask the stale daemon to exit, wait for its socket to clear, then reconnect + // — which lazily spawns the fresh bundled daemon. + self.fire(Cmd::Shutdown).await; + for _ in 0..100 { + if UnixStream::connect(&self.sock).await.is_err() { + break; + } + tokio::time::sleep(Duration::from_millis(30)).await; + } + let seen = self.gen.load(Ordering::Acquire); + let _ = self.reconnect(seen).await; } /// Send a command without awaiting a reply or retrying. Used for Shutdown: diff --git a/app/src-tauri/tauri.conf.json b/app/src-tauri/tauri.conf.json index fab51a2..5a5b825 100644 --- a/app/src-tauri/tauri.conf.json +++ b/app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "spacesh", - "version": "0.1.2", + "version": "0.1.3", "identifier": "xyz.spacesh.app", "build": { "frontendDist": "../dist", diff --git a/app/src/App.tsx b/app/src/App.tsx index 663c00a..63d8176 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -183,7 +183,7 @@ export function App() { setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />
{active && ( - { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} /> + { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} /> )}
{active diff --git a/app/src/CenterToolbar.tsx b/app/src/CenterToolbar.tsx index f674cc6..edad99b 100644 --- a/app/src/CenterToolbar.tsx +++ b/app/src/CenterToolbar.tsx @@ -3,10 +3,10 @@ import { COLORS, FONT } from "./theme"; import { PresetPicker } from "./PresetPicker"; /** Top-of-grid toolbar: layout presets on the left, scrollback search on the right (search is a mock). */ -export function CenterToolbar({ selected, onSelect, onOpenSearch }: { selected: string; onSelect: (id: string) => void; onOpenSearch: () => void }) { +export function CenterToolbar({ selected, onSelect, onOpenSearch, paneCount }: { selected: string; onSelect: (id: string) => void; onOpenSearch: () => void; paneCount: number }) { return (
- +
void }) { +// `minSlots` hides presets smaller than the current pane count — applying a preset +// only ever ADDS panes (never destroys running ones); shrink by closing panels. +export function PresetPicker({ selected, onSelect, minSlots = 0 }: { selected: string; onSelect: (id: string) => void; minSlots?: number }) { return (
- {PRESETS.map((p) => { + {PRESETS.filter((p) => p.slots >= minSlots).map((p) => { const on = p.id === selected; return (