"use client"; // The integrated dashboard. Org → company → team → claw drill-down over the // example workspace (src/lib/dashboard-demo.ts), with left↔main highlight sync: // clicking a row in the left list HIGHLIGHTS the matching node in the main // canvas; clicking the node itself DRILLS one tier deeper. The org/company/team // canvases are React Flow topology graphs (src/components/dashboard/flow) driven // by the full categorized topology taxonomy (src/lib/topologies.ts); the claw // tier shows the agent's anatomy. The right slide-out is per-agent (the claw's // own computer) at claw tier, and the team's shared apps at team tier. import { useEffect, useRef, useState, type CSSProperties } from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useQueryStates } from "nuqs"; import { Bot, Brain, History, MessageSquare, Monitor, PanelRight, Trash2, Users, Wrench, X } from "lucide-react"; import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo"; import type { Agent } from "@/lib/api/schemas"; import { topologyById } from "@/lib/topologies"; import { panelParsers, type DeviceSize } from "@/lib/url/panel-params"; import { type FlowItem } from "./flow/TopologyFlow"; import { WorldCanvas } from "../world/WorldCanvas"; import { ObservePanel } from "../observe/ObservePanel"; import { StructureTree, orgNode, type TreeItem } from "./StructureTree"; import { ResearchList } from "./ResearchList"; import { ResearchCanvas } from "./ResearchCanvas"; import { LoopsList } from "./LoopsList"; import { LoopsCanvas } from "./LoopsCanvas"; import { RepoList } from "./RepoList"; import { RepoCanvas } from "./RepoCanvas"; import { RepoConnectionWizardStub } from "./RepoConnectionWizardStub"; import { RepoConnectionEditModal } from "./RepoConnectionEditModal"; import { UserMenu } from "./UserMenu"; import { ToolPanel, type ToolKey } from "./ToolPanel"; import { InfraNav, FleetConsole, FleetStatusBar, FleetPill } from "./fleet/FleetConsole"; import { NodeMonitor } from "./fleet/NodeMonitor"; import { ConnectHostWizard } from "./ConnectHostWizard"; import { INFRA_CATALOG } from "@/components/computer/catalogs/infra"; import { ClawCommandCenter } from "./ClawCommandCenter"; import { MasterPlannerModal } from "./MasterPlannerModal"; import { BrainRegistryPanel } from "./BrainRegistryPanel"; import { BrainHistoryModal } from "./BrainHistoryModal"; import { TeamRunsModal } from "./TeamRunsModal"; import { ReapProgressModal, type ReapKind } from "./ReapProgressModal"; import { AddToCompanyModal } from "./AddToCompanyModal"; import { AddToOrgModal } from "./AddToOrgModal"; import { DevicePanel } from "@/components/computer/DevicePanel"; import { DeviceSizeToggle } from "@/components/computer/DeviceSizeToggle"; const COMPUTER_WIDTH: Record = { phone: "448px", tablet: "67%", full: "100%" }; type Tier = "world" | "research" | "loops" | "claw" | "repos" | "infra"; const mono = "'JetBrains Mono', ui-monospace, monospace"; // Gradient palette for structure nodes that don't carry their own (companies, // teams). Agents bring their own grad/ink. const NODE_GRADS: [string, string][] = [ ["linear-gradient(135deg,#ff9a6a,#ff6f4a)", "#2a0d05"], ["linear-gradient(135deg,#6fd0c0,#4aa3b8)", "#06201f"], ["linear-gradient(135deg,#8a9af0,#5a6ad8)", "#0a0e2a"], ["linear-gradient(135deg,#e8c46a,#d89a3a)", "#2a1d05"], ["linear-gradient(135deg,#c98af0,#9a5ad8)", "#1a0a2a"], ]; // A few starter templates surfaced in the Templates tab (deploy + visualize). interface Template { id: string; name: string; topo: string; blurb: string; roles: string[]; } const TEAM_TEMPLATES: Template[] = [ { id: "t-research", name: "Research Pod", topo: "blackboard", blurb: "A lead curates a shared blackboard while researchers and a critic read/write findings in parallel.", roles: ["lead", "researcher", "researcher", "critic", "writer"] }, { id: "t-growth", name: "Growth Squad", topo: "hub_spoke", blurb: "A coordinator routes work to specialists and aggregates their output back.", roles: ["lead", "researcher", "writer", "analyst", "critic"] }, { id: "t-pipeline", name: "Content Pipeline", topo: "pipeline", blurb: "Linear stages: intake → draft → edit → publish, each agent feeding the next.", roles: ["intake", "drafter", "editor", "publisher"] }, { id: "t-debate", name: "Debate Room", topo: "debate", blurb: "A proposer and a critic argue; a judge resolves. Good for high-stakes decisions.", roles: ["proposer", "critic", "judge"] }, { id: "t-swarm", name: "Swarm Recon", topo: "swarm", blurb: "Many autonomous peers attack a problem in parallel; consensus emerges.", roles: ["scout", "scout", "scout", "scout", "synthesizer"] }, ]; const COMPANY_TEMPLATES: Template[] = [ { id: "c-pipeline", name: "Pipeline Co", topo: "pipeline", blurb: "Teams arranged as a value chain — intake feeds growth feeds research feeds ops.", roles: ["Intake", "Growth", "Research", "Ops"] }, { id: "c-federated", name: "Federated Co", topo: "federated", blurb: "Semi-autonomous teams with a light coordination layer between them.", roles: ["Team A", "Team B", "Team C"] }, { id: "c-holacratic", name: "Holacratic Co", topo: "holacratic", blurb: "Self-organizing circles with distributed authority and no fixed hierarchy.", roles: ["Circle 1", "Circle 2", "Circle 3"] }, ]; const railIcon: Record = { world: (), // Book with a magnifying-lens tucked into the lower-right — research. research: (), // Circular arrow ⟳ with an inner dot — loops. loops: (), claw: (), // Branching lines forking off a trunk — repositories. repos: (), infra: (), }; const TIER_TABS: { key: Tier; label: string }[] = [ { key: "world", label: "VIZ" }, { key: "research", label: "RESEARCH" }, { key: "loops", label: "LOOPS" }, { key: "claw", label: "AGENT" }, { key: "repos", label: "REPOS" }, { key: "infra", label: "INFRA" }, ]; const companyIcon = (size: number) => ( ); // ── Per-claw live enrichment (fetched on selection) ───────────────────────── // The tree/list carries a claw's name/role/status/system-prompt; its model, // installed skills, tools/doors, memory, capabilities, safety + apps come from // the per-claw endpoints, fetched lazily when a claw is opened. interface Enrich { model: string; apps: DemoAgent["apps"]; compartments: DemoAgent["compartments"]; brain: RawBrain } type RawCompartment = { key: string; label: string; items: string[]; count?: number }; type RawRuntime = { model?: string | null; provider_alias?: string; sandbox_enabled?: boolean; network_allowed?: boolean } | null; type RawApp = { id: string; name: string; category?: string }; // `GET /api/claws/{id}/brain` — the claw's .brain (cm-brain / ClawhDF5). type RawBrain = { exists: boolean; system_prompt: string | null; agent_md: string | null; personality: string | null; skills_md: string | null; skills: { name: string; body: string }[]; tools: { name: string; state: string }[]; memory: string[]; stats: { skills: number; tools: number; memories: number }; } | null; function appKind(a: RawApp): DemoAgent["apps"][number]["kind"] { const s = `${a.name} ${a.category ?? ""}`.toLowerCase(); if (s.includes("slack")) return "slack"; if (s.includes("browser") || s.includes("web")) return "browser"; if (s.includes("chat") || s.includes("message")) return "chat"; if (s.includes("wiki") || s.includes("notion") || s.includes("doc")) return "wiki"; if (s.includes("voice") || s.includes("voip") || s.includes("call")) return "voip"; if (s.includes("cluster") || s.includes("k8s") || s.includes("kube")) return "cluster"; return "files"; } function buildEnrich(rc: RawRuntime, comps: RawCompartment[], apps: RawApp[], brain: RawBrain): Enrich { const items = (k: string) => comps.find((c) => c.key === k)?.items ?? []; const mem = items("memory"); // Prefer the .brain as the source for the cards when it carries data; fall // back to the DB-derived compartments otherwise. const b = brain && (brain.skills?.length || brain.tools?.length || brain.system_prompt || brain.memory?.length) ? brain : null; const brainSkills = b?.skills.map((s) => s.name) ?? []; const brainTools = b?.tools.map((t) => ({ name: t.name, state: /block/i.test(t.state) ? ("blocked" as const) : ("gated" as const) })) ?? []; const brainPersona = b?.personality ? b.personality.split(/[,;\n·]| - /).map((s) => s.trim()).filter(Boolean) : []; return { model: rc?.model || rc?.provider_alias || "", apps: apps.map((a) => ({ id: a.id, name: a.name, kind: appKind(a) })), brain, compartments: { skills: brainSkills.length ? brainSkills : items("skills"), personality: brainPersona.length ? brainPersona : items("personality"), // With a brain, Capabilities visualizes its contents as a quick stat line. capabilities: b ? [`${b.stats.skills} skills`, `${b.stats.tools} tools`, `${b.stats.memories} memories`] : items("capabilities"), tools: brainTools.length ? brainTools : items("tools").map((s) => ({ name: String(s).split(/[·:|]/)[0].trim(), state: /block/i.test(s) ? "blocked" : "gated" })), memory: { long: mem[0] ?? "—", recent: mem[1] ?? "—" }, safety: { sandbox: rc?.sandbox_enabled === false ? "open" : "isolated", network: rc?.network_allowed ? "allowed" : "none" }, }, }; } const okJson = (r: Response) => (r.ok ? r.json() : null); // Keeps org/company/team non-null for a brand-new account with no claws yet. const EMPTY_ORG: DemoOrg = { id: "", name: "My Workspace", topology: "flat", companies: [{ id: "", name: "Direct", topology: "flat", meta: "0 agents", teams: [{ id: "", name: "My Agents", topology: "flat", status: "idle", dot: "#3a3a40", agents: [], groupApps: [] }] }], }; export function Dashboard({ user, orgs, claws }: { user?: { display_name?: string; email?: string }; orgs: DemoOrg[]; claws: Agent[] }) { const agentById = new Map(claws.map((a) => [a.id, a])); // Live-data lookups over the workspace passed from the server (real claws + // structure). All scoped to `orgs` so the dashboard reflects the real account. const allCompanies = orgs.flatMap((o) => o.companies.map((c) => ({ org: o, company: c }))); const findOrg = (id: string | null) => orgs.find((o) => o.id === id); const findCompany = (id: string | null) => allCompanies.find((e) => e.company.id === id)?.company; const orgOf = (id: string | null) => allCompanies.find((e) => e.company.id === id)?.org; const findTeam = (cid: string | null, tid: string | null) => findCompany(cid)?.teams.find((t) => t.id === tid); const findAgent = (cid: string | null, tid: string | null, aid: string | null) => findTeam(cid, tid)?.agents.find((a) => a.id === aid); const locateAgent = (aid: string | null) => { for (const o of orgs) for (const c of o.companies) for (const t of c.teams) { const a = t.agents.find((x) => x.id === aid); if (a) return { org: o, company: c, team: t, agent: a }; } return undefined; }; const locateTeam = (tid: string | null) => { for (const o of orgs) for (const c of o.companies) { const t = c.teams.find((x) => x.id === tid); if (t) return { org: o, company: c, team: t }; } return undefined; }; const fallbackOrg = orgs[0] ?? EMPTY_ORG; const [tier, setTier] = useState("claw"); const [orgId, setOrgId] = useState(fallbackOrg.id); const [companyId, setCompanyId] = useState(fallbackOrg.companies[0]?.id ?? ""); const [teamId, setTeamId] = useState(fallbackOrg.companies[0]?.teams[0]?.id ?? ""); const [agentId, setAgentId] = useState(fallbackOrg.companies[0]?.teams[0]?.agents[0]?.id ?? ""); // Large World: the selected node + which nodes are expanded in the graph. const [worldSel, setWorldSel] = useState(null); const [expanded, setExpanded] = useState>(() => new Set(orgs.map((o) => o.id))); // Large World right slide-out (sized like the agent computer: phone/tablet/full). const [worldPanelOpen, setWorldPanelOpen] = useState(false); const [worldSize, setWorldSize] = useState("phone"); // Research tier: currently-selected topic + a refresh key mutations bump so // list + canvas re-fetch after start/submit/publish etc. const [researchSel, setResearchSel] = useState(null); const [researchRefresh, setResearchRefresh] = useState(0); // Loops tier: same pattern. const [loopsSel, setLoopsSel] = useState(null); const [loopsRefresh, setLoopsRefresh] = useState(0); // Repos tier: selected repo + refresh key + wizard modal flag. const [repoSel, setRepoSel] = useState(null); const [repoRefresh, setRepoRefresh] = useState(0); const [repoWizardOpen, setRepoWizardOpen] = useState(false); const [repoEditId, setRepoEditId] = useState(null); // Infrastructure tier: which nav view (local / cloud) + the connect-host wizard. const [infraSel, setInfraSel] = useState("local"); const [infraConnectOpen, setInfraConnectOpen] = useState(false); // When set, the infra center shows the full-page monitor for this node. const [monitorNode, setMonitorNode] = useState<{ id: string; name: string } | null>(null); const org: DemoOrg = findOrg(orgId) ?? fallbackOrg; const company: DemoCompany = findCompany(companyId) ?? org.companies[0] ?? EMPTY_ORG.companies[0]; const team: DemoTeam = findTeam(companyId, teamId) ?? company.teams[0] ?? EMPTY_ORG.companies[0].teams[0]; const agent: DemoAgent | undefined = findAgent(companyId, teamId, agentId) ?? team.agents[0]; // After creating a claw the wizard sends us to /?claw=; open it here so a // freshly-created claw lands selected on the claw page in the new interface. const params = useSearchParams(); const clawParam = params.get("claw"); const [seenClaw, setSeenClaw] = useState(null); if (clawParam && clawParam !== seenClaw) { setSeenClaw(clawParam); const loc = locateAgent(clawParam); if (loc) { setOrgId(loc.org.id); setCompanyId(loc.company.id); setTeamId(loc.team.id); setAgentId(clawParam); setTier("claw"); } } // After creating a team (Add to teams), we land on /?team=; select it and // open the team page once the refreshed workspace data includes it. const teamParam = params.get("team"); const [seenTeam, setSeenTeam] = useState(null); if (teamParam && teamParam !== seenTeam) { const loc = locateTeam(teamParam); if (loc) { setSeenTeam(teamParam); setOrgId(loc.org.id); setCompanyId(loc.company.id); setTeamId(teamParam); setAgentId(loc.team.agents[0]?.id ?? ""); setTier("world"); setWorldSel(teamParam); setExpanded((p) => new Set([...p, loc.org.id, loc.company.id, teamParam])); } } // After creating a company (Add to company), land on /?company=. const companyParam = params.get("company"); const [seenCompany, setSeenCompany] = useState(null); if (companyParam && companyParam !== seenCompany) { const e = allCompanies.find((x) => x.company.id === companyParam); if (e) { setSeenCompany(companyParam); setOrgId(e.org.id); setCompanyId(companyParam); setTeamId(e.company.teams[0]?.id ?? ""); setTier("world"); setWorldSel(companyParam); setExpanded((p) => new Set([...p, e.org.id, companyParam])); } } // After creating an org (Add to organization), land on /?org=. const orgParam = params.get("org"); const [seenOrg, setSeenOrg] = useState(null); if (orgParam && orgParam !== seenOrg) { const o = findOrg(orgParam); if (o) { setSeenOrg(orgParam); setOrgId(orgParam); setCompanyId(o.companies[0]?.id ?? ""); setTier("world"); setWorldSel(orgParam); setExpanded((p) => new Set([...p, orgParam])); } } // Grouping modals: claws→team, teams→company, companies→org. const [addCompanyOpen, setAddCompanyOpen] = useState(false); const [addOrgOpen, setAddOrgOpen] = useState(false); // Lazily enrich the open claw with its real model / skills / tools / apps. const [enrichById, setEnrichById] = useState>({}); // Bump to force a re-fetch (e.g. after adding a tool/skill to the claw). const [enrichBump, setEnrichBump] = useState(0); const enrichId = tier === "claw" && agent ? agent.id : null; useEffect(() => { if (!enrichId) return; let alive = true; Promise.all([ fetch(`/api/claws/${enrichId}/runtime-config`, { cache: "no-store" }).then(okJson).catch(() => null), fetch(`/api/claws/${enrichId}/compartments`, { cache: "no-store" }).then(okJson).catch(() => null), fetch(`/api/apps?clawId=${enrichId}`, { cache: "no-store" }).then(okJson).catch(() => null), fetch(`/api/claws/${enrichId}/brain`, { cache: "no-store" }).then(okJson).catch(() => null), ]).then(([rc, comps, apps, brain]) => { if (alive) setEnrichById((prev) => ({ ...prev, [enrichId]: buildEnrich(rc as RawRuntime, (comps as RawCompartment[]) ?? [], (apps as RawApp[]) ?? [], brain as RawBrain) })); }); return () => { alive = false; }; }, [enrichId, enrichBump]); // The claw shown at the claw tier, merged with any fetched enrichment. const richAgent: DemoAgent | undefined = agent ? (() => { const e = enrichById[agent.id]; return e ? { ...agent, model: e.model || agent.model, apps: e.apps, compartments: e.compartments, systemPrompt: e.brain?.system_prompt || agent.systemPrompt } : agent; })() : undefined; // The selected claw's .brain payload (stats + recent memory) for the cards. const richBrain: RawBrain = agent ? (enrichById[agent.id]?.brain ?? null) : null; // The REAL backend Agent for the selected claw — drives the computer + chat. const clawAgent: Agent | undefined = (agent ? agentById.get(agent.id) : undefined) ?? (agent ? { id: agent.id, workspace_id: "", name: agent.name, job_title: agent.role, system_prompt: agent.systemPrompt, avatar: "", accent: "#ff6f61", wallpaper: "", managed_by: "", status: "online" } : undefined); // Computer panel state (size + open app), shared with DevicePanel via nuqs. const [{ app, device }, setParams] = useQueryStates(panelParsers, { shallow: true }); // Custom computer width (px) from dragging the panel's left edge; null = use the // preset (phone/tablet/full). A size-toggle click clears it back to the preset. const [customWidth, setCustomWidth] = useState(null); const [resizing, setResizing] = useState(false); const canvasRef = useRef(null); // Claw-page UI preferences persist across navigation AND reloads (localStorage): // the chat's collapsed state and the computer's open/size. The dashboard stays // mounted while you move between tiers, so in-memory state already survives // navigation; localStorage extends that across reloads and is the source of // truth applied on first entry to the claw tier. const [chatMin, setChatMin] = useState(false); const prefsLoadedRef = useRef(false); const computerInitRef = useRef(false); useEffect(() => { // One-time sync from the persisted store (render default first, then adopt // the saved value after mount — avoids an SSR/hydration mismatch). let v: string | null = null; try { v = localStorage.getItem("cm.claw.chatMin"); } catch { /* no storage */ } prefsLoadedRef.current = true; if (v != null) { // eslint-disable-next-line react-hooks/set-state-in-effect setChatMin(v === "1"); } }, []); // First time we land on the claw tier, apply the saved computer open/size. useEffect(() => { if (tier !== "claw" || computerInitRef.current) return; computerInitRef.current = true; let open = true; let dev: DeviceSize = "phone"; try { const o = localStorage.getItem("cm.claw.computerOpen"); if (o != null) open = o === "1"; const d = localStorage.getItem("cm.claw.device"); if (d === "phone" || d === "tablet" || d === "full") dev = d; } catch { /* no storage */ } setParams({ app: open ? "home" : null, device: dev }); }, [tier, setParams]); // Persist on change. useEffect(() => { if (prefsLoadedRef.current) try { localStorage.setItem("cm.claw.chatMin", chatMin ? "1" : "0"); } catch { /* no storage */ } }, [chatMin]); useEffect(() => { if (tier !== "claw" || !computerInitRef.current) return; try { localStorage.setItem("cm.claw.computerOpen", app !== null ? "1" : "0"); localStorage.setItem("cm.claw.device", device); } catch { /* no storage */ } }, [tier, app, device]); const computerOpen = app !== null; const clawFull = (tier === "claw" || tier === "infra") && computerOpen && device === "full"; const [orgTopo, setOrgTopo] = useState(org.topology); const [topoOrg, setTopoOrg] = useState(orgId); if (topoOrg !== orgId) { setTopoOrg(orgId); setOrgTopo((findOrg(orgId) ?? org).topology); } // Selecting anywhere in the tree keeps the whole org→company→team→claw path // coherent (breadcrumb, graphs, and the drilled views all point at real // ancestors), no matter which tier the tree is rooted at. const selectOrg = (id: string) => { const o = findOrg(id) ?? org; const c = o.companies[0]; setOrgId(id); setCompanyId(c?.id ?? ""); setTeamId(c?.teams[0]?.id ?? ""); setAgentId(c?.teams[0]?.agents[0]?.id ?? ""); }; const selectCompany = (id: string) => { const co = findCompany(id) ?? company; setOrgId(orgOf(id)?.id ?? orgId); setCompanyId(id); setTeamId(co.teams[0]?.id ?? ""); setAgentId(co.teams[0]?.agents[0]?.id ?? ""); }; const selectTeam = (id: string) => { const loc = locateTeam(id); if (loc) { setOrgId(loc.org.id); setCompanyId(loc.company.id); } setTeamId(id); setAgentId((loc?.team ?? team).agents[0]?.id ?? ""); }; // Open a claw from anywhere in the tree: resolve + set its full path, drill in. const openClaw = (id: string) => { const loc = locateAgent(id); if (loc) { setOrgId(loc.org.id); setCompanyId(loc.company.id); setTeamId(loc.team.id); } setAgentId(id); setTier("claw"); }; // Large World graph helpers. const toggleExpand = (id: string) => setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); // Expand a node + all its ancestors so it's visible in the graph. const expandPathTo = (id: string) => { const path: string[] = []; const find = (n: TreeItem, trail: string[]): boolean => { const t = [...trail, n.id]; if (n.id === id) { path.push(...t); return true; } return (n.children ?? []).some((c) => find(c, t)); }; worldRoots.some((r) => find(r, [])); if (path.length) setExpanded((prev) => new Set([...prev, ...path])); }; // Selecting a non-agent node in the graph: highlight + keep the path ids coherent. const onWorldSelect = (id: string) => { setWorldSel(id); const lv = nodeLevel.get(id); if (lv === "claw") { // Agents open a summary in the side panel (no redirect — the panel's robot // button drills in). Keep the path ids coherent for the summary + drill. const loc = locateAgent(id); if (loc) { setOrgId(loc.org.id); setCompanyId(loc.company.id); setTeamId(loc.team.id); } setAgentId(id); setWorldPanelOpen(true); } else if (lv === "org") selectOrg(id); else if (lv === "company") selectCompany(id); else if (lv === "team") selectTeam(id); }; // The tree's unified node handler (world tree + the flat agents list). On the // flat agents page a claw click opens the agent; in the World tree it selects. const onTreeSelect = (item: TreeItem) => { if (isClaw && item.level === "claw") { openClaw(item.id); return; } onWorldSelect(item.id); expandPathTo(item.id); }; // Per-tier topology overrides, reset when the drilled entity changes. const [companyTopo, setCompanyTopo] = useState(company.topology); const [topoCo, setTopoCo] = useState(companyId); if (topoCo !== companyId) { setTopoCo(companyId); setCompanyTopo((findCompany(companyId) ?? company).topology); } const router = useRouter(); // The team page's selector edits the SELECTED team's OWN topology (the value // shown on its node), persisted to the backend. Local overrides reflect it // instantly; falls back to the team's stored kind. const [teamTopo, setTeamTopo] = useState>({}); const teamTopoOf = (t: DemoTeam) => teamTopo[t.id] ?? t.topology; async function changeTeamTopo(kind: string) { if (!teamId) return; setTeamTopo((m) => ({ ...m, [teamId]: kind })); try { await fetch(`/api/teams/${encodeURIComponent(teamId)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind }) }); } catch { /* keep the optimistic label */ } } async function deleteTeam() { if (!teamId || !window.confirm(`Delete team "${team.name}"? Its agents stay in your workspace.`)) return; try { const res = await fetch(`/api/teams/${encodeURIComponent(teamId)}`, { method: "DELETE" }); if (res.ok) { router.push("/"); router.refresh(); } } catch { /* no-op */ } } // The company page's selector edits the SELECTED company's own topology // (persisted); delete removes the company (its teams stay). async function changeCompanyTopoPersist(kind: string) { setCompanyTopo(kind); if (!companyId) return; try { await fetch(`/api/companies/${encodeURIComponent(companyId)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ kind }) }); } catch { /* keep the optimistic label */ } } async function deleteCompany() { if (!companyId || !window.confirm(`Delete company "${company.name}"? Its teams stay in your workspace.`)) return; try { const res = await fetch(`/api/companies/${encodeURIComponent(companyId)}`, { method: "DELETE" }); if (res.ok) { router.push("/"); router.refresh(); } } catch { /* no-op */ } } // Left tab (items vs templates) + selected template, reset on tier change. const [tab, setTab] = useState<"items" | "templates">("items"); const [tabTier, setTabTier] = useState(tier); const [templateId, setTemplateId] = useState(null); if (tabTier !== tier) { setTabTier(tier); setTab("items"); setTemplateId(null); } const [teamDrawer, setTeamDrawer] = useState(false); const [deployOpen, setDeployOpen] = useState(false); // Brain registry rail (claw page) — collapsed by default, toggled by the brain // icon in the Agents sidebar header. const [registryOpen, setRegistryOpen] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [runsOpen, setRunsOpen] = useState(false); const [selectMode, setSelectMode] = useState(false); const [selectedAgents, setSelectedAgents] = useState>(new Set()); const [reapOpen, setReapOpen] = useState(false); const [toolOpen, setToolOpen] = useState(null); const isWorld = tier === "world", isResearch = tier === "research", isLoops = tier === "loops", isClaw = tier === "claw", isRepos = tier === "repos", isInfra = tier === "infra"; const allAgents = orgs.flatMap((o) => o.companies.flatMap((c) => c.teams.flatMap((t) => t.agents))); // World: the full expandable org→company→team→agent forest. Agents page: a flat list. const worldRoots: TreeItem[] = orgs.map(orgNode); // World canvas: when a node is selected, prune to just the branch that // contains it — sibling orgs/companies/teams disappear so the visualization // frames the selection alone. The left tree keeps the full forest. const narrowRoots = (roots: TreeItem[], sel: string | null): TreeItem[] => { if (!sel) return roots; const prune = (n: TreeItem): TreeItem | null => { if (n.id === sel) return n; for (const c of n.children ?? []) { const found = prune(c); if (found) return { ...n, children: [found] }; } return null; }; for (const r of roots) { const found = prune(r); if (found) return [found]; } return roots; }; const worldCanvasRoots: TreeItem[] = narrowRoots(worldRoots, worldSel); // Both the World and Agents tiers now share the same collapsible org → // company → team → agent tree — a single pane onto the whole workforce. // `selectLevel` below still constrains selection to agents on the Agents // tier so bulk-delete stays claw-scoped. const treeRoots: TreeItem[] = worldRoots; const treeActiveId = isClaw ? agentId : worldSel; const treeAutoExpand: string[] = []; // Every node in the active tree (flattened) — the wrench multi-select looks up // selected nodes here regardless of depth, and derives the delete kind from the // selected level (selection is kept homogeneous on toggle). const allNodes: { id: string; label: string; level: string }[] = (() => { const out: { id: string; label: string; level: string }[] = []; const walk = (n: TreeItem) => { out.push({ id: n.id, label: n.label, level: n.level }); (n.children ?? []).forEach(walk); }; treeRoots.forEach(walk); return out; })(); const nodeLevel = new Map(allNodes.map((n) => [n.id, n.level] as const)); const levelToKind = (lv?: string): ReapKind => (lv === "team" ? "teams" : lv === "company" ? "companies" : lv === "org" ? "orgs" : "agents"); const selectedItems = allNodes.filter((n) => selectedAgents.has(n.id)).map((n) => ({ id: n.id, name: n.label })); const reapKind: ReapKind = selectedItems.length ? levelToKind(nodeLevel.get(selectedItems[0].id)) : "agents"; const crumbStyle = (on: boolean): CSSProperties => on ? { color: "#fff", background: "rgba(255,111,97,.14)", border: "1px solid rgba(255,111,97,.3)", padding: "3px 8px", borderRadius: 6, cursor: "pointer" } : { color: "#6a6a72", padding: "3px 5px", cursor: "pointer" }; // Flow items per tier. const companyItems: FlowItem[] = org.companies.map((c, i) => ({ id: c.id, label: c.name, role: c.meta.split(" · ")[0], grad: NODE_GRADS[i % NODE_GRADS.length][0], ink: NODE_GRADS[i % NODE_GRADS.length][1], status: c.teams.some((t) => t.status === "running") ? "running" : "online", })); const teamItems: FlowItem[] = company.teams.map((t, i) => ({ id: t.id, label: t.name, role: `${topologyById(teamTopoOf(t))?.label ?? teamTopoOf(t)} · ${t.agents.length}`, grad: NODE_GRADS[i % NODE_GRADS.length][0], ink: NODE_GRADS[i % NODE_GRADS.length][1], status: t.status, })); return (
{/* TOP BAR */}
Clawmates
setTier("world")}>Visualizations / setTier("research")}>Research / setTier("loops")}>Loops / setTier("claw")}>Agents / setTier("infra")}>Infrastructure
{isInfra ? : null}
{/* BODY */}
{/* Rail + context column hide while the computer is fullscreen so it truly fills the screen (nothing peeks out to the left). */} {!clawFull ? ( <> {/* STRUCTURE RAIL — tiers, then a divider, then the "+" create affordance */}
{TIER_TABS.map((t) => { const on = tier === t.key; return (
setTier(t.key)} style={{ position: "relative", width: 42, height: 48, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 4, borderRadius: 10, cursor: "pointer", color: on ? "#ff6f61" : "#5a5a62", background: on ? "rgba(255,111,97,.1)" : "transparent" }}> {on ? : null} {railIcon[t.key]} {t.label}
); })}
{/* CONTEXT LIST */}
{/* Header: tiers get a Structure/Templates toggle; the claw page gets a plain "Claws" header — its sidebar is a flat list of agents. */} {isWorld ? (
{orgs.length} ORG{orgs.length === 1 ? "" : "S"} · {allAgents.length} AGENTS
Visualizations
) : isInfra || isResearch || isLoops || isRepos ? null : (
{orgs.length} ORG{orgs.length === 1 ? "" : "S"} · {allCompanies.length} CO · {orgs.flatMap((o) => o.companies).reduce((n, c) => n + c.teams.length, 0)} TEAM{orgs.flatMap((o) => o.companies).reduce((n, c) => n + c.teams.length, 0) === 1 ? "" : "S"} · {allAgents.length} AGENT{allAgents.length === 1 ? "" : "S"}
Agents
{clawAgent ? ( ) : null}
)} {isResearch ? ( { setResearchSel(id); setResearchRefresh((n) => n + 1); }} /> ) : isLoops ? ( { setLoopsSel(id); setLoopsRefresh((n) => n + 1); }} /> ) : isRepos ? ( setRepoWizardOpen(true)} onEdit={(id) => setRepoEditId(id)} refreshKey={repoRefresh} onRefresh={() => setRepoRefresh((n) => n + 1)} /> ) : isInfra ? ( setInfraConnectOpen(true)} /> ) : ( <> {/* The collapsible org → company → team → agent tree (World) or the flat agents list. */} setSelectedAgents((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); return next; } const lv = nodeLevel.get(id); const curLv = prev.size ? nodeLevel.get([...prev][0]) : lv; if (lv !== curLv) return new Set([id]); next.add(id); return next; })} /> {selectMode ? (
{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}
) : null} )}
) : null} {/* CANVAS */}
{isResearch ? ( setResearchRefresh((n) => n + 1)} /> ) : isLoops ? ( setLoopsRefresh((n) => n + 1)} onDeleted={() => { setLoopsSel(null); setLoopsRefresh((n) => n + 1); }} /> ) : isRepos ? ( ) : isWorld ? ( <> {/* Graph stage — condensed by the right slide-out's width. */}
setRunsOpen(true)} /> {/* Open the slide-out (top-right) when it's closed. */} {!worldPanelOpen ? ( ) : null}
{/* Right slide-out — sized like the agent computer (phone/tablet/full). Content TBD; for now the sized shell + the size toggle. */} {worldPanelOpen ? (
PANEL setWorldSize(d)} />
{(() => { const loc = locateAgent(worldSel); if (!loc) return setWorldSel(null)} />; const { org: o, company: co, team: t, agent: a } = loc; const statusCol = a.status === "running" ? "#5ec8d8" : a.status === "online" ? "#5fd08a" : "#5a5a62"; const chip = (text: string, col = "#9a9aa2") => {text}; const hierRow = (col: string, k: string, v: string) => (
{k.toUpperCase()} {v}
); const stat = (n: number, l: string) => (
{n}
{l.toUpperCase()}
); return (
{a.avatar ? null : a.initial}
{a.name}
{a.role}
{chip(a.status, statusCol)} {a.model ? chip(a.model) : null}
BELONGS TO {hierRow("#6fd0c0", "Team", t.name)} {hierRow("#8a9af0", "Company", co.name)} {hierRow("#c98af0", "Org", o.name)}
{stat(a.compartments.skills.length, "Skills")} {stat(a.compartments.tools.length, "Tools")} {stat(a.nowRunning.length, "Running")}
); })()}
) : null} ) : isInfra ? ( <> {/* Center: the operator console (stats → Tailscale → host cards) + a thin status bar, condensed by the computer pull-out's width. */}
{monitorNode ? ( setMonitorNode(null)} /> ) : ( setInfraConnectOpen(true)} onMonitor={(id, name) => setMonitorNode({ id, name })} /> )}
{/* Right: the IDENTICAL computer chrome, with cloud-infra apps. */} {computerOpen ? (
{ e.currentTarget.setPointerCapture(e.pointerId); setResizing(true); }} onPointerMove={(e) => { if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; setCustomWidth(Math.round(Math.max(448, Math.min(rect.width, rect.right - e.clientX)))); }} onPointerUp={(e) => { e.currentTarget.releasePointerCapture(e.pointerId); setResizing(false); }} style={{ position: "absolute", top: 0, bottom: 0, right: "var(--computer-width)", width: 10, marginRight: -5, zIndex: 41, cursor: "col-resize", touchAction: "none" }} >
) : null}
{chatMin ? ( ) : null} {computerOpen ? ( <> { setParams({ device: d }); setCustomWidth(null); }} /> ) : ( )}
{infraConnectOpen ? setInfraConnectOpen(false)} /> : null} ) : richAgent && clawAgent ? ( <> {/* Far-left: Brain Registry — search panel + results panel + detail slide-out. Toggled by the brain icon in the Agents sidebar; hidden when the computer is fullscreen. */} {registryOpen && device !== "full" ? (
setEnrichBump((b) => b + 1)} onClose={() => setRegistryOpen(false)} />
) : null} {/* Left region: the full-height agent command center (identity strip → metrics band → LIVE/BRAIN/SURFACE columns). Chat lives in the computer's Chat app now. Condensed by the computer's width + the registry rail. */}
setEnrichBump((b) => b + 1)} />
{/* Right: the original device computer (phone / tablet / desktop). */} {/* Drag the panel's left edge to a custom width (the size toggle above snaps back to the phone/tablet/full presets). */} {computerOpen ? (
{ e.currentTarget.setPointerCapture(e.pointerId); setResizing(true); }} onPointerMove={(e) => { if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; const rect = canvasRef.current?.getBoundingClientRect(); if (!rect) return; // Don't allow narrower than the phone preset (448px). setCustomWidth(Math.round(Math.max(448, Math.min(rect.width, rect.right - e.clientX)))); }} onPointerUp={(e) => { e.currentTarget.releasePointerCapture(e.pointerId); setResizing(false); }} style={{ position: "absolute", top: 0, bottom: 0, right: "var(--computer-width)", width: 10, marginRight: -5, zIndex: 41, cursor: "col-resize", touchAction: "none" }} >
) : null} {/* Top-right launchers: chat shortcut (opens the computer's Chat app) + computer; controls show when the computer is open. */}
{computerOpen ? ( <> { setParams({ device: d }); setCustomWidth(null); }} /> ) : ( )}
) : ( )}
{/* RIGHT SLIDE-OUT: team group apps (the claw computer is in-canvas now). */}
{/* MASTER PLANNER — the "+" opens a chat with Opus 4.8 that proposes and scaffolds a whole team of agents. */} {deployOpen ? setDeployOpen(false)} /> : null} {repoWizardOpen ? ( setRepoWizardOpen(false)} onCreated={() => { setRepoWizardOpen(false); setRepoRefresh((n) => n + 1); }} /> ) : null} {repoEditId ? ( setRepoEditId(null)} onChanged={() => setRepoRefresh((n) => n + 1)} onDeleted={() => { setRepoSel(null); setRepoRefresh((n) => n + 1); }} /> ) : null} {historyOpen && clawAgent ? setHistoryOpen(false)} onRolledBack={() => setEnrichBump((b) => b + 1)} /> : null} {(() => { const sel = allNodes.find((n) => n.id === worldSel); return runsOpen && sel?.level === "team" ? setRunsOpen(false)} /> : null; })()} {reapOpen ? setReapOpen(false)} onDone={() => { setReapOpen(false); setSelectMode(false); setSelectedAgents(new Set()); router.refresh(); }} /> : null} {/* Group existing claws into a new named team, then land on its team page. */} {/* Group existing teams into a new named company, then land on its page. */} {addCompanyOpen ? ( o.companies.flatMap((c) => c.teams)).filter((t) => t.id !== "ungrouped-team").map((t) => ({ id: t.id, name: t.name, claws: t.agents.length, topology: t.topology }))} onClose={() => setAddCompanyOpen(false)} /> ) : null} {/* Group existing companies into a new named organization, then land on it. */} {addOrgOpen ? ( o.companies).filter((c) => c.id && c.id !== "ws-teams" && c.id !== "ungrouped-co").map((c) => ({ id: c.id, name: c.name, teams: c.teams.length }))} onClose={() => setAddOrgOpen(false)} /> ) : null} {/* In-dashboard tool panels (Skills/Apps/Topologies/Approvals/Team/Credits). */} {toolOpen ? setToolOpen(null)} /> : null} {/* STATUS BAR */}
● durable runner ok checkpoint 3s ago §15 sandbox: isolated
); } function EmptyStage({ label }: { label: string }) { return (
{label}
); }