feat(app): daemon status with Stop/Restart in settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 18:57:53 +07:00
parent a2087a0de5
commit 9ca1ff3bc5
4 changed files with 59 additions and 4 deletions
+41 -4
View File
@@ -1,6 +1,6 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { COLORS, FONT } from "./theme";
import { setConfig } from "./socketBridge";
import { setConfig, shutdownDaemon, restartDaemon } from "./socketBridge";
import type { ConfigView, DaemonHealth } from "./socketBridge";
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 DaemonSection(_props: { health: DaemonHealth | null }) { return null; }
function fmtUptime(ms: number): string {
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>
);
}
+12
View File
@@ -211,3 +211,15 @@ export async function setConfig(patch: Partial<Pick<ConfigView, "default_shell"
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 */ }
}