feat(app): Event Center, native notifications, auto-unread, state wiring in App
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+50
-9
@@ -1,40 +1,80 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { LayoutEngine } from "./LayoutEngine";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
import { PresetPicker } from "./PresetPicker";
|
||||
import { Wizard } from "./Wizard";
|
||||
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent } from "./socketBridge";
|
||||
import type { Group, WorkspaceView } from "./layoutTypes";
|
||||
import { EventCenter, type FeedEntry } from "./EventCenter";
|
||||
import { maybeNotify } from "./notify";
|
||||
import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface } from "./socketBridge";
|
||||
import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes";
|
||||
|
||||
export function App() {
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [workspaces, setWorkspaces] = useState<WorkspaceView[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [running, setRunning] = useState<Record<string, boolean>>({});
|
||||
const [states, setStates] = useState<Record<string, SurfaceState>>({});
|
||||
const [feed, setFeed] = useState<FeedEntry[]>([]);
|
||||
const [wizard, setWizard] = useState(false);
|
||||
const feedId = useRef(0);
|
||||
const activeRef = useRef<string | null>(null);
|
||||
const wsRef = useRef<WorkspaceView[]>([]);
|
||||
activeRef.current = activeId;
|
||||
wsRef.current = workspaces;
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const st = await getStatusFull();
|
||||
setGroups(st.groups);
|
||||
setWorkspaces(st.workspaces);
|
||||
const run: Record<string, boolean> = {};
|
||||
st.workspaces.forEach((w) => Object.entries(w.surfaces).forEach(([id, sv]) => { run[id] = sv.running; }));
|
||||
const stt: Record<string, SurfaceState> = {};
|
||||
st.workspaces.forEach((w) => Object.entries(w.surfaces).forEach(([id, sv]) => { run[id] = sv.running; stt[id] = sv.state; }));
|
||||
setRunning(run);
|
||||
if (!activeId && st.workspaces.length) setActiveId(st.workspaces[0].id);
|
||||
}, [activeId]);
|
||||
setStates(stt);
|
||||
if (!activeRef.current && st.workspaces.length) setActiveId(st.workspaces[0].id);
|
||||
}, []);
|
||||
|
||||
const wsOf = (surfaceId: string): WorkspaceView | undefined =>
|
||||
wsRef.current.find((w) => surfaceId in w.surfaces);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
const unlisten = onDaemonEvent(() => { void refresh(); });
|
||||
const unlisten = onDaemonEvent((evt) => {
|
||||
if (evt.evt === "state") {
|
||||
const { surface_id, state } = evt.data;
|
||||
setStates((m) => ({ ...m, [surface_id]: state }));
|
||||
const w = wsOf(surface_id);
|
||||
const agent = w?.surfaces[surface_id]?.spec.agent_label ?? "shell";
|
||||
if (["done", "wait", "error"].includes(state)) {
|
||||
const entry: FeedEntry = { id: feedId.current++, surfaceId: surface_id, workspace: w?.name ?? "?", agent, kind: state, time: "now" };
|
||||
setFeed((f) => [entry, ...f].slice(0, 200));
|
||||
if (w && w.id !== activeRef.current) void setWorkspaceMeta(w.id, { unread: true });
|
||||
void maybeNotify(surface_id, agent, w?.name ?? "?", state);
|
||||
}
|
||||
void refresh();
|
||||
} else if (evt.evt === "exit") {
|
||||
const w = wsOf(evt.data.surface_id);
|
||||
const exitEntry: FeedEntry = { id: feedId.current++, surfaceId: evt.data.surface_id, workspace: w?.name ?? "?", agent: w?.surfaces[evt.data.surface_id]?.spec.agent_label ?? "shell", kind: "exit", time: "now" };
|
||||
setFeed((f) => [exitEntry, ...f].slice(0, 200));
|
||||
void refresh();
|
||||
} else {
|
||||
void refresh();
|
||||
}
|
||||
});
|
||||
const reconnect = onDaemonRawEvent("spacesh:disconnected", () => { void refresh(); });
|
||||
return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); };
|
||||
}, [refresh]);
|
||||
|
||||
const active = workspaces.find((w) => w.id === activeId) ?? null;
|
||||
|
||||
function selectWorkspace(id: string) {
|
||||
setActiveId(id);
|
||||
void setWorkspaceMeta(id, { unread: false });
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: "100vh", background: "#0E1116" }}>
|
||||
<Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={setActiveId} onNew={() => setWizard(true)} />
|
||||
<Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={selectWorkspace} onNew={() => setWizard(true)} />
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
{active && (
|
||||
<div style={{ padding: 8, borderBottom: "1px solid #232A33" }}>
|
||||
@@ -43,10 +83,11 @@ export function App() {
|
||||
)}
|
||||
<div style={{ flex: 1, minHeight: 0 }}>
|
||||
{active
|
||||
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} />
|
||||
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} states={states} />
|
||||
: <div style={{ color: "#666", padding: 24 }}>No workspace — create one to begin.</div>}
|
||||
</div>
|
||||
</div>
|
||||
<EventCenter feed={feed} onMarkRead={() => setFeed([])} onSelect={(sid) => { void focusSurface(sid); }} />
|
||||
{wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user