feat(app): daemon status with Stop/Restart in settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -346,3 +346,8 @@ pub async fn set_config(
|
|||||||
) -> Result<Value, String> {
|
) -> Result<Value, String> {
|
||||||
data_of(state.request(Cmd::SetConfig { default_shell, font_family, font_size, theme, accent }).await.map_err(|e| e.to_string())?)
|
data_of(state.request(Cmd::SetConfig { default_shell, font_family, font_size, theme, accent }).await.map_err(|e| e.to_string())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn shutdown_daemon(state: BridgeState<'_>) -> Result<Value, String> {
|
||||||
|
data_of(state.request(Cmd::Shutdown).await.map_err(|e| e.to_string())?)
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub fn run() {
|
|||||||
bridge::health,
|
bridge::health,
|
||||||
bridge::get_config,
|
bridge::get_config,
|
||||||
bridge::set_config,
|
bridge::set_config,
|
||||||
|
bridge::shutdown_daemon,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running spacesh");
|
.expect("error while running spacesh");
|
||||||
|
|||||||
+41
-4
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { COLORS, FONT } from "./theme";
|
import { COLORS, FONT } from "./theme";
|
||||||
import { setConfig } from "./socketBridge";
|
import { setConfig, shutdownDaemon, restartDaemon } from "./socketBridge";
|
||||||
import type { ConfigView, DaemonHealth } from "./socketBridge";
|
import type { ConfigView, DaemonHealth } from "./socketBridge";
|
||||||
|
|
||||||
const FONTS = ["JetBrains Mono", "Menlo", "Monaco", "SF Mono", "Fira Code", "Cascadia Code"];
|
const FONTS = ["JetBrains Mono", "Menlo", "Monaco", "SF Mono", "Fira Code", "Cascadia Code"];
|
||||||
@@ -56,5 +56,42 @@ export function Settings({ config, health, onClose }: { config: ConfigView; heal
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder — fleshed out in Task 11. Keep the signature stable.
|
function fmtUptime(ms: number): string {
|
||||||
function DaemonSection(_props: { health: DaemonHealth | null }) { return null; }
|
const s = Math.max(0, Math.floor((Date.now() - ms) / 1000));
|
||||||
|
if (s < 60) return `${s}s`;
|
||||||
|
if (s < 3600) return `${Math.floor(s / 60)}m`;
|
||||||
|
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DaemonSection({ health }: { health: DaemonHealth | null }) {
|
||||||
|
const [confirm, setConfirm] = useState<null | "stop" | "restart">(null);
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 8, paddingTop: 16, borderTop: `1px solid ${COLORS.borderSubtle}` }}>
|
||||||
|
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 8 }}>Daemon</div>
|
||||||
|
<div style={{ fontFamily: FONT.mono, fontSize: 12, color: COLORS.textSecondary, lineHeight: 1.7 }}>
|
||||||
|
{health ? (<>
|
||||||
|
<div>version {health.version} · pid {health.pid}</div>
|
||||||
|
<div>uptime {fmtUptime(health.started_at_ms)}</div>
|
||||||
|
</>) : <div>offline</div>}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 8, marginTop: 12 }}>
|
||||||
|
<button onClick={() => setConfirm("restart")} style={{ padding: "7px 14px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 7, fontSize: 13 }}>Restart</button>
|
||||||
|
<button onClick={() => setConfirm("stop")} style={{ padding: "7px 14px", background: "transparent", color: COLORS.stError, border: `1px solid ${COLORS.stError}`, borderRadius: 7, fontSize: 13 }}>Stop</button>
|
||||||
|
</div>
|
||||||
|
{confirm && (
|
||||||
|
<div style={{ marginTop: 10, padding: 10, borderRadius: 8, background: COLORS.bgPanel, border: `1px solid ${COLORS.borderStrong}` }}>
|
||||||
|
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 8 }}>
|
||||||
|
{confirm === "stop" ? "Stop the daemon? All sessions end." : "Restart the daemon? Sessions end and respawn."}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||||
|
<button onClick={() => setConfirm(null)} style={{ padding: "5px 12px", background: COLORS.bgElevated, color: COLORS.textPrimary, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 6, fontSize: 12 }}>Cancel</button>
|
||||||
|
<button onClick={() => { const c = confirm; setConfirm(null); void (c === "stop" ? shutdownDaemon() : restartDaemon()); }}
|
||||||
|
style={{ padding: "5px 12px", background: COLORS.stError, color: "#fff", border: "none", borderRadius: 6, fontSize: 12, fontWeight: 600 }}>
|
||||||
|
{confirm === "stop" ? "Stop" : "Restart"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -211,3 +211,15 @@ export async function setConfig(patch: Partial<Pick<ConfigView, "default_shell"
|
|||||||
accent: patch.accent ?? null,
|
accent: patch.accent ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function shutdownDaemon(): Promise<void> {
|
||||||
|
try { await invoke("shutdown_daemon"); } catch { /* connection drops as the daemon exits — expected */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartDaemon(): Promise<void> {
|
||||||
|
await shutdownDaemon();
|
||||||
|
// Let the old process exit; the next request triggers the bridge's
|
||||||
|
// ensure_daemon respawn (or launchd KeepAlive) and reconnects.
|
||||||
|
await new Promise((r) => setTimeout(r, 600));
|
||||||
|
try { await getHealth(); } catch { /* reconnect loop will retry */ }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user