import { useState, useEffect } from "react"; import { Plus, ChevronDown, ChevronRight } from "lucide-react"; import { COLORS, FONT, STATE_COLOR } from "./theme"; import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes"; import type { DaemonHealth } from "./socketBridge"; function fmtUptime(startedMs: number): string { const s = Math.max(0, Math.floor((Date.now() - startedMs) / 1000)); if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s / 60)}m`; if (s < 86400) return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`; return `${Math.floor(s / 86400)}d ${Math.floor((s % 86400) / 3600)}h`; } function aggregate(w: WorkspaceView): SurfaceState | "stopped" { const order: SurfaceState[] = ["error", "wait", "work", "done", "idle"]; const running = Object.values(w.surfaces).filter((s) => s.running); if (running.length === 0) return "stopped"; for (const st of order) { if (running.some((s) => s.state === st)) return st; } return "idle"; } export function Sidebar({ groups, workspaces, activeId, onSelect, onNew, health, connected, }: { groups: Group[]; workspaces: WorkspaceView[]; activeId: string | null; onSelect: (id: string) => void; onNew: () => void; health: DaemonHealth | null; connected: boolean; }) { const [collapsed, setCollapsed] = useState>({}); const [, setTick] = useState(0); useEffect(() => { const t = setInterval(() => setTick((n) => n + 1), 30000); return () => clearInterval(t); }, []); 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) => { const isActive = w.id === activeId; return (
onSelect(w.id)} style={{ display: "flex", alignItems: "center", gap: 10, height: 34, padding: "0 8px", borderRadius: 6, cursor: "pointer", background: isActive ? COLORS.bgElevated : "transparent", fontFamily: FONT.ui, fontSize: 13, color: isActive ? COLORS.textPrimary : COLORS.textSecondary, }}> {w.name} {w.unread && } {Object.keys(w.surfaces).length}
); }; return (
{groups.sort((a, b) => a.order - b.order).map((g) => { const open = !collapsed[g.id]; return (
setCollapsed((c) => ({ ...c, [g.id]: open }))} style={{ display: "flex", alignItems: "center", gap: 7, height: 24, padding: "0 4px", cursor: "pointer" }}> {open ? : } {g.name.toUpperCase()}
{open && byGroup(g.id).map(row)}
); })} {ungrouped.length > 0 &&
{ungrouped.map(row)}
}
{connected ? "spaceshd · live" : "spaceshd · offline"} {health ? fmtUptime(health.started_at_ms) : ""}
); }