fix(app): bridge auto-reconnect so daemon restart no longer bricks the GUI
The Tauri bridge connected to the daemon once at startup and held a single stream with no recovery: when the daemon exited (Restart/Stop, crash, or an update), the reader emitted spacesh:disconnected and died, and every later request went through the dead writer forever — the GUI was permanently stuck (settings frozen, offline). Since the bridge is Rust-side state that survives a webview reload, even Cmd+R didn't recover it. - bridge.rs: requests now reconnect-and-retry on failure with a single-flight guard (generation counter) so concurrent failures collapse into one reconnect and never open duplicate connections; a 5s reply timeout catches silently-dropped connections. ensure_daemon respawns the daemon if it exited. On success the bridge emits spacesh:reconnected. - App.tsx: on spacesh:reconnected, bump a connection epoch that keys LayoutEngine, remounting terminals so they re-attach (snapshot + live stream) to the restarted daemon; also reload health/config/status. - Settings: drop the Stop button — with lazy daemon spawn any GUI request resurrects the daemon, so an in-GUI "stop" is contradictory. Restart now works end to end (shutdown → reconnect respawns → panels re-attach). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -37,6 +37,9 @@ export function App() {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(() => loadFlag("spacesh.sidebarOpen", true));
|
||||
const [health, setHealth] = useState<DaemonHealth | null>(null);
|
||||
const [config, setConfigState] = useState<ConfigView | null>(null);
|
||||
// Bumped when the daemon connection is re-established; used to remount the
|
||||
// layout so terminals re-attach (snapshot + live stream) to the restarted daemon.
|
||||
const [connEpoch, setConnEpoch] = useState(0);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [focusedId, setFocusedId] = useState<string | null>(null);
|
||||
const [searchSurfaceId, setSearchSurfaceId] = useState<string | null>(null);
|
||||
@@ -112,7 +115,15 @@ export function App() {
|
||||
void loadHealth();
|
||||
void getConfig().then((c) => { setConfigState(c); applyTheme(c.theme, c.accent); }).catch(() => {});
|
||||
});
|
||||
return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); };
|
||||
const reconnected = onDaemonRawEvent("spacesh:reconnected", () => {
|
||||
setConnected(true);
|
||||
setConnEpoch((n) => n + 1); // remount layout → terminals re-attach to the new daemon
|
||||
void refresh();
|
||||
void seedEvents();
|
||||
void loadHealth();
|
||||
void getConfig().then((c) => { setConfigState(c); applyTheme(c.theme, c.accent); }).catch(() => {});
|
||||
});
|
||||
return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); void reconnected.then((f) => f()); };
|
||||
}, [refresh, seedEvents, loadHealth]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -158,7 +169,7 @@ export function App() {
|
||||
)}
|
||||
<div style={{ flex: 1, minHeight: 0, position: "relative" }}>
|
||||
{active
|
||||
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} zoomed={active.zoomed} searchSurfaceId={searchSurfaceId} searchNonce={searchNonce} onCloseSearch={() => setSearchSurfaceId(null)} font={termFont} palette={termPalette} />
|
||||
? <LayoutEngine key={connEpoch} workspaceId={active.id} layout={active.layout} running={running} states={states} surfaces={active.surfaces} focusedId={effectiveFocus} onFocus={setFocusedId} zoomed={active.zoomed} searchSurfaceId={searchSurfaceId} searchNonce={searchNonce} onCloseSearch={() => setSearchSurfaceId(null)} font={termFont} palette={termPalette} />
|
||||
: <div style={{ color: COLORS.textMuted, padding: 24 }}>No workspace — create one to begin.</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user