diff --git a/app/src/App.tsx b/app/src/App.tsx index 2a0bccc..8e1f0da 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -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([]); - const [active, setActive] = useState(null); - const [workspaceId, setWorkspaceId] = useState(null); + const [groups, setGroups] = useState([]); + const [workspaces, setWorkspaces] = useState([]); + const [activeId, setActiveId] = useState(null); + const [running, setRunning] = useState>({}); + const [wizard, setWizard] = useState(false); + + const refresh = useCallback(async () => { + const st = await getStatusFull(); + setGroups(st.groups); + setWorkspaces(st.workspaces); + const run: Record = {}; + 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 ( -
-
- - -
-
- {active ? :
no surface
} +
+ setWizard(true)} /> +
+ {active && ( +
+ { if (active) void applyPreset(active.id, p, []); }} /> +
+ )} +
+ {active + ? + :
No workspace — create one to begin.
} +
+ {wizard && { setWizard(false); setActiveId(id); void refresh(); }} onCancel={() => setWizard(false)} />}
); } diff --git a/app/src/PresetPicker.tsx b/app/src/PresetPicker.tsx new file mode 100644 index 0000000..5b25e13 --- /dev/null +++ b/app/src/PresetPicker.tsx @@ -0,0 +1,30 @@ +export const PRESETS: { id: string; label: string; slots: number }[] = [ + { id: "1", label: "1", slots: 1 }, + { id: "2lr", label: "2↔", slots: 2 }, + { id: "2tb", label: "2↕", slots: 2 }, + { id: "2+1", label: "2+1", slots: 3 }, + { id: "1+2", label: "1+2", slots: 3 }, + { id: "3", label: "3", slots: 3 }, + { id: "2x2", label: "2×2", slots: 4 }, + { id: "4", label: "4", slots: 4 }, + { id: "2x3", label: "2×3", slots: 6 }, + { id: "2x4", label: "2×4", slots: 8 }, +]; + +export function PresetPicker({ selected, onSelect }: { selected: string; onSelect: (id: string) => void }) { + return ( +
+ {PRESETS.map((p) => ( + + ))} +
+ ); +} diff --git a/app/src/Sidebar.tsx b/app/src/Sidebar.tsx new file mode 100644 index 0000000..d85d8fa --- /dev/null +++ b/app/src/Sidebar.tsx @@ -0,0 +1,44 @@ +import type { Group, WorkspaceView } from "./layoutTypes"; + +export function Sidebar({ + groups, workspaces, activeId, onSelect, onNew, +}: { + groups: Group[]; + workspaces: WorkspaceView[]; + activeId: string | null; + onSelect: (id: string) => void; + onNew: () => void; +}) { + const byGroup = (gid: string | null) => workspaces.filter((w) => (w.group_id ?? null) === gid).sort((a, b) => a.order - b.order); + const ungrouped = byGroup(null); + + const row = (w: WorkspaceView) => ( +
onSelect(w.id)} + style={{ + display: "flex", alignItems: "center", gap: 9, padding: "6px 8px", borderRadius: 6, cursor: "pointer", + background: w.id === activeId ? "#1A2029" : "transparent", fontFamily: "Inter", fontSize: 13, + color: w.id === activeId ? "#E6EDF3" : "#8B97A6", + }}> + + {w.name} + {w.unread && } + {Object.keys(w.surfaces).length} +
+ ); + + return ( +
+ + {groups.sort((a, b) => a.order - b.order).map((g) => ( +
+
+ + {g.name.toUpperCase()} +
+ {byGroup(g.id).map(row)} +
+ ))} + {ungrouped.length > 0 &&
{ungrouped.map(row)}
} +
+ ); +} diff --git a/app/src/Wizard.tsx b/app/src/Wizard.tsx new file mode 100644 index 0000000..fbee211 --- /dev/null +++ b/app/src/Wizard.tsx @@ -0,0 +1,46 @@ +import { useState } from "react"; +import { PresetPicker, PRESETS } from "./PresetPicker"; +import { openWorkspace, applyPreset } from "./socketBridge"; + +export function Wizard({ onDone, onCancel }: { onDone: (workspaceId: string) => void; onCancel: () => void }) { + const [path, setPath] = useState("."); + const [preset, setPreset] = useState("2x2"); + const [agents, setAgents] = useState([]); + const slots = PRESETS.find((p) => p.id === preset)?.slots ?? 1; + const agentChoices = ["shell", "claude", "codex", "gemini"]; + + async function create() { + const ws = await openWorkspace(path); + const slotSpecs = Array.from({ length: slots }, (_, i) => { + const a = agents[i] ?? "shell"; + return a === "shell" ? {} : { command: a }; + }); + await applyPreset(ws, preset, slotSpecs); + onDone(ws); + } + + return ( +
+
+
New workspace
+ + setPath(e.target.value)} style={{ width: "100%", margin: "6px 0 16px", padding: 8, background: "#0A0D12", color: "#E6EDF3", border: "1px solid #323C49", borderRadius: 8 }} /> + +
+ +
+ {Array.from({ length: slots }, (_, i) => ( + + ))} +
+
+ + +
+
+
+ ); +}