import { useEffect, useState, useCallback, useMemo, useRef } from "react"; import { LayoutEngine } from "./LayoutEngine"; import { Sidebar } from "./Sidebar"; import { TopBar } from "./TopBar"; import { CenterToolbar } from "./CenterToolbar"; import { Wizard } from "./Wizard"; import { EventCenter } from "./EventCenter"; import { maybeNotify } from "./notify"; import { COLORS } from "./theme"; import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, getHealth } from "./socketBridge"; import type { EventRecord, DaemonHealth } from "./socketBridge"; import { leafIds } from "./layoutTypes"; import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes"; /** Read a boolean UI flag from localStorage, falling back to `def`. */ function loadFlag(key: string, def: boolean): boolean { try { const v = localStorage.getItem(key); return v === null ? def : v === "1"; } catch { return def; } } function saveFlag(key: string, value: boolean): void { try { localStorage.setItem(key, value ? "1" : "0"); } catch { /* ignore */ } } export function App() { const [groups, setGroups] = useState([]); const [workspaces, setWorkspaces] = useState([]); const [activeId, setActiveId] = useState(null); const [running, setRunning] = useState>({}); const [states, setStates] = useState>({}); const [events, setEvents] = useState([]); const [wizard, setWizard] = useState(false); const [eventsOpen, setEventsOpen] = useState(() => loadFlag("spacesh.eventsOpen", true)); const [sidebarOpen, setSidebarOpen] = useState(() => loadFlag("spacesh.sidebarOpen", true)); const [health, setHealth] = useState(null); const [connected, setConnected] = useState(false); const [focusedId, setFocusedId] = useState(null); const [searchSurfaceId, setSearchSurfaceId] = useState(null); const [searchNonce, setSearchNonce] = useState(0); const activeRef = useRef(null); const effectiveFocusRef = useRef(null); const wsRef = useRef([]); activeRef.current = activeId; wsRef.current = workspaces; const seedEvents = useCallback(async () => { const log = await getEventLog(); setEvents((existing) => { const byId = new Map(); for (const e of log.events) byId.set(e.id, e); // daemon is authoritative for overlapping ids for (const e of existing) if (!byId.has(e.id)) byId.set(e.id, e); // keep live events not in the snapshot return [...byId.values()].sort((a, b) => b.id - a.id).slice(0, 1000); }); }, []); const refresh = useCallback(async () => { const st = await getStatusFull(); setGroups(st.groups); setWorkspaces(st.workspaces); const run: Record = {}; const stt: Record = {}; st.workspaces.forEach((w) => Object.entries(w.surfaces).forEach(([id, sv]) => { run[id] = sv.running; stt[id] = sv.state; })); setRunning(run); setStates(stt); if (!activeRef.current && st.workspaces.length) setActiveId(st.workspaces[0].id); }, []); const loadHealth = useCallback(async () => { try { setHealth(await getHealth()); setConnected(true); } catch { setConnected(false); } }, []); const wsOf = (surfaceId: string): WorkspaceView | undefined => wsRef.current.find((w) => surfaceId in w.surfaces); useEffect(() => { void refresh(); void seedEvents(); void loadHealth(); const unlisten = onDaemonEvent((evt) => { if (evt.evt === "event") { const rec = evt.data.record; setEvents((es) => [rec, ...es].slice(0, 1000)); const w = wsOf(rec.surface_id); if (w && w.id !== activeRef.current) void setWorkspaceMeta(w.id, { unread: true }); void maybeNotify(rec.surface_id, rec.agent_label ?? "shell", rec.workspace_name, rec.kind); } else if (evt.evt === "events_read") { const ids = new Set(evt.data.ids); setEvents((es) => es.map((e) => (ids.has(e.id) ? { ...e, read: true } : e))); } else if (evt.evt === "state") { setStates((m) => ({ ...m, [evt.data.surface_id]: evt.data.state })); void refresh(); } else if (evt.evt === "exit") { void refresh(); } else { void refresh(); } }); const reconnect = onDaemonRawEvent("spacesh:disconnected", () => { setConnected(false); void refresh(); void seedEvents(); void loadHealth(); }); return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); }; }, [refresh, seedEvents, loadHealth]); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "f") { if (activeRef.current && effectiveFocusRef.current) { e.preventDefault(); setSearchSurfaceId(effectiveFocusRef.current); // anchor to the focused panel setSearchNonce((n) => n + 1); } } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); useEffect(() => { saveFlag("spacesh.eventsOpen", eventsOpen); }, [eventsOpen]); useEffect(() => { saveFlag("spacesh.sidebarOpen", sidebarOpen); }, [sidebarOpen]); const unread = useMemo(() => events.filter((e) => !e.read).length, [events]); const active = workspaces.find((w) => w.id === activeId) ?? null; const leaves = active ? leafIds(active.layout) : []; const effectiveFocus = focusedId && leaves.includes(focusedId) ? focusedId : leaves[0] ?? null; effectiveFocusRef.current = effectiveFocus; function selectWorkspace(id: string) { setActiveId(id); setFocusedId(null); void setWorkspaceMeta(id, { unread: false }); } return (
setEventsOpen((v) => !v)} onShowEvents={() => setEventsOpen(true)} sidebarOpen={sidebarOpen} onToggleSidebar={() => setSidebarOpen((v) => !v)} unread={unread} />
{sidebarOpen && setWizard(true)} health={health} connected={connected} />}
{active && ( { if (active) void applyPreset(active.id, p, []); }} onOpenSearch={() => { if (effectiveFocus) { setSearchSurfaceId(effectiveFocus); setSearchNonce((n) => n + 1); } }} /> )}
{active ? setSearchSurfaceId(null)} /> :
No workspace — create one to begin.
}
{eventsOpen && ( { void markEventsRead({ target: "all" }); }} onSelect={(sid, id) => { void focusSurface(sid); void markEventsRead({ target: "ids", value: [id] }); }} /> )}
{wizard && { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
); }