feat(app): sidebar, preset picker, wizard, App rewired around workspaces + LayoutEngine

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 21:31:49 +07:00
parent 0320a2f313
commit 7ec0c84685
4 changed files with 162 additions and 57 deletions
+42 -57
View File
@@ -1,68 +1,53 @@
import { useEffect, useState } from "react";
import { TerminalView } from "./TerminalView";
import { SurfaceList } from "./SurfaceList";
import { openWorkspace, newSurface, getStatus, onDaemonEvent, onDaemonRawEvent } from "./socketBridge";
import { useEffect, useState, useCallback } 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";
export function App() {
const [surfaces, setSurfaces] = useState<string[]>([]);
const [active, setActive] = useState<string | null>(null);
const [workspaceId, setWorkspaceId] = useState<string | null>(null);
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 [wizard, setWizard] = useState(false);
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; }));
setRunning(run);
if (!activeId && st.workspaces.length) setActiveId(st.workspaces[0].id);
}, [activeId]);
useEffect(() => {
void (async () => {
const ws = await getStatus();
const flat = ws.flatMap((w) => w.surfaces);
setSurfaces(flat);
if (flat.length) setActive(flat[0]);
})();
void refresh();
const unlisten = onDaemonEvent(() => { void refresh(); });
const reconnect = onDaemonRawEvent("spacesh:disconnected", () => { void refresh(); });
return () => { void unlisten.then((f) => f()); void reconnect.then((f) => f()); };
}, [refresh]);
const unlisten = onDaemonEvent((evt) => {
if (evt.evt === "surface_created") {
setSurfaces((s) => [...s, evt.data.surface_id]);
} else if (evt.evt === "surface_closed" || evt.evt === "exit") {
// exit leaves the surface visible; surface_closed removes it.
if (evt.evt === "surface_closed") {
setSurfaces((s) => s.filter((id) => id !== evt.data.surface_id));
}
}
});
const reconnect = onDaemonRawEvent("spacesh:disconnected", () => {
// Force a remount of the active TerminalView by toggling the key.
setActive((cur) => cur);
void getStatus().then((ws) => {
const flat = ws.flatMap((w) => w.surfaces);
setSurfaces(flat);
});
});
return () => {
void unlisten.then((f) => f());
void reconnect.then((f) => f());
};
}, []);
async function handleNewSurface() {
let ws = workspaceId;
if (!ws) {
ws = await openWorkspace(".");
setWorkspaceId(ws);
}
const id = await newSurface(ws, 80, 24);
setActive(id);
}
const active = workspaces.find((w) => w.id === activeId) ?? null;
return (
<div style={{ display: "flex", height: "100vh", background: "#000" }}>
<div style={{ display: "flex", flexDirection: "column", width: 160 }}>
<button onClick={handleNewSurface} style={{ margin: 8 }}>
+ surface
</button>
<SurfaceList surfaces={surfaces} active={active} onSelect={setActive} />
</div>
<div style={{ flex: 1 }}>
{active ? <TerminalView key={active} surfaceId={active} /> : <div style={{ color: "#666", padding: 16 }}>no surface</div>}
<div style={{ display: "flex", height: "100vh", background: "#0E1116" }}>
<Sidebar groups={groups} workspaces={workspaces} activeId={activeId} onSelect={setActiveId} onNew={() => setWizard(true)} />
<div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
{active && (
<div style={{ padding: 8, borderBottom: "1px solid #232A33" }}>
<PresetPicker selected="" onSelect={(p) => { if (active) void applyPreset(active.id, p, []); }} />
</div>
)}
<div style={{ flex: 1, minHeight: 0 }}>
{active
? <LayoutEngine workspaceId={active.id} layout={active.layout} running={running} />
: <div style={{ color: "#666", padding: 24 }}>No workspace create one to begin.</div>}
</div>
</div>
{wizard && <Wizard onDone={(id) => { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
</div>
);
}