Update version to 0.1.3
Add daemon version check and restart logic Add pane count to CenterToolbar Add minSlots filter to PresetPicker
This commit is contained in:
Generated
+1
-1
@@ -3440,7 +3440,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spacesh-proto"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"serde",
|
||||
|
||||
@@ -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<u64> {
|
||||
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<UnixStream> {
|
||||
if let Ok(s) = UnixStream::connect(sock).await {
|
||||
return Ok(s);
|
||||
@@ -120,7 +127,7 @@ impl Bridge {
|
||||
let pending: Arc<Mutex<HashMap<u64, oneshot::Sender<Envelope>>>> = Arc::default();
|
||||
let out_channels: Arc<Mutex<HashMap<String, Channel<Vec<u8>>>>> = 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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ export function App() {
|
||||
<Sidebar railMode={!sidebarOpen} groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} onDelete={setDeleteTarget} health={health} connected={connected} />
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
{active && (
|
||||
<CenterToolbar selected="" onSelect={(p) => { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} />
|
||||
<CenterToolbar selected="" paneCount={leaves.length} onSelect={(p) => { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} />
|
||||
)}
|
||||
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
|
||||
{active
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: "0 12px", height: 46, borderBottom: `1px solid ${COLORS.borderSubtle}` }}>
|
||||
<PresetPicker selected={selected} onSelect={onSelect} />
|
||||
<PresetPicker selected={selected} onSelect={onSelect} minSlots={paneCount} />
|
||||
<div style={{ flex: 1 }} />
|
||||
<div
|
||||
title="Search scrollback"
|
||||
|
||||
@@ -13,10 +13,12 @@ export const PRESETS: { id: string; label: string; slots: number }[] = [
|
||||
|
||||
import { COLORS, FONT } from "./theme";
|
||||
|
||||
export function PresetPicker({ selected, onSelect }: { selected: string; onSelect: (id: string) => 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 (
|
||||
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
|
||||
{PRESETS.map((p) => {
|
||||
{PRESETS.filter((p) => p.slots >= minSlots).map((p) => {
|
||||
const on = p.id === selected;
|
||||
return (
|
||||
<button key={p.id} onClick={() => onSelect(p.id)}
|
||||
|
||||
Reference in New Issue
Block a user