"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 { Activity, Bot, Brain, Camera, Cpu, Database, Drama, History, MessageSquare, Monitor, PanelRight, ScrollText, ShieldCheck, Trash2, Users, Wrench, X, Zap } 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 { WorldFlow } from "./flow/WorldFlow"; import { StructureTree, orgNode, clawNode, type TreeItem } from "./StructureTree"; import { UserMenu } from "./UserMenu"; import { ToolPanel, type ToolKey } from "./ToolPanel"; import { ClawChatSection } from "./ClawChatSection"; import { ResizableSplit } from "./ResizableSplit"; import { VitalsCard } from "./VitalsCard"; import { AvatarModal } from "./AvatarModal"; import { AddToolModal } from "./AddToolModal"; import { MasterPlannerModal } from "./MasterPlannerModal"; import { BrainRegistryPanel } from "./BrainRegistryPanel"; import { BrainHistoryModal } from "./BrainHistoryModal"; import { TeamRunsModal } from "./TeamRunsModal"; import { ReapProgressModal, type ReapKind } from "./ReapProgressModal"; import { AddToTeamModal } from "./AddToTeamModal"; 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" | "claw"; 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: (), claw: (), }; const TIER_TABS: { key: Tier; label: string }[] = [ { key: "world", label: "WORLD" }, { key: "claw", label: "AGENT" }, ]; 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; personality: 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"); 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 [addTeamOpen, setAddTeamOpen] = useState(false); 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 }); // 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" && 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", isClaw = tier === "claw"; 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); const treeRoots: TreeItem[] = isClaw ? allAgents.map(clawNode) : 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")}>Large World / setTier("claw")}>Agents
{/* 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
Large World
) : (
{allAgents.length} AGENT{allAgents.length === 1 ? "" : "S"}
Agents
{clawAgent ? ( ) : null}
)} {/* 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}` : ""}
) : isClaw ? (
) : null}
) : null} {/* CANVAS */}
{isWorld ? ( <> {/* Graph stage — condensed by the right slide-out's width. */}
{(() => { const sel = allNodes.find((n) => n.id === worldSel); // Agents get the rich side-panel summary instead of this inspector. if (!sel || sel.level === "claw") return null; const btn = (color: string): CSSProperties => ({ display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 6, width: "100%", padding: "8px 0", borderRadius: 9, border: `1px solid ${color}55`, background: `${color}14`, color, fontSize: 12.5, fontWeight: 600, cursor: "pointer" }); return (
{sel.level.toUpperCase()}
{sel.label}
{sel.level === "team" ? ( ) : null}
); })()} {/* 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 (
Click an agent node to see its summary.
); 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} ) : 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: anatomy profile + native chat, condensed by the computer's width (and the registry rail). The two are a resizable split — drag the divider to grow the chat. */}
{chatMin ? ( // Chat minimized → anatomy takes the whole left region. setEnrichBump((b) => b + 1)} /> ) : ( setEnrichBump((b) => b + 1)} />} bottom={ setParams({ app: a })} onMinimize={() => setChatMin(true)} />} /> )}
{/* Right: the original device computer (phone / tablet / desktop). */} {/* Top-right launchers: chat (left) + computer (right), each a coral icon when collapsed; controls show when the computer is open. */}
{chatMin ? ( ) : null} {computerOpen ? ( <> setParams({ device: d })} /> ) : ( )}
) : ( )}
{/* 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} {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. */} {addTeamOpen ? setAddTeamOpen(false)} /> : null} {/* 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}
); } // ── Deploy wizard modal ───────────────────────────────────────────────────── // ── Claw tier: the full-profile anatomy stage (sidebar is the all-claws tree) ── // Inline markdown: **bold** and `code` → styled spans (everything else verbatim). function renderInline(text: string, kp: string): React.ReactNode[] { return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).filter(Boolean).map((p, i) => { if (p.startsWith("**") && p.endsWith("**")) return {p.slice(2, -2)}; if (p.startsWith("`") && p.endsWith("`")) return {p.slice(1, -1)}; return {p}; }); } // A tiny, dependency-free Markdown renderer: headings (#/##/###), bullet lists // (- / *), bold/code, and paragraphs. Turns the raw prompt into readable prose // instead of showing literal `##`. function MarkdownText({ text, baseSize = 14 }: { text: string; baseSize?: number }) { const lines = text.replace(/\r\n/g, "\n").split("\n"); const blocks: React.ReactNode[] = []; let para: string[] = []; let list: string[] = []; const flushPara = () => { if (para.length) { const k = `p${blocks.length}`; blocks.push(

{renderInline(para.join(" "), k)}

); para = []; } }; const flushList = () => { if (list.length) { const k = `u${blocks.length}`; blocks.push(
    {list.map((li, i) =>
  • {renderInline(li, `${k}-${i}`)}
  • )}
); list = []; } }; for (const raw of lines) { const line = raw.replace(/\s+$/, ""); const h = line.match(/^(#{1,6})\s+(.*)$/); const li = line.match(/^\s*[-*]\s+(.*)$/); if (h) { flushPara(); flushList(); const lvl = h[1].length; const size = lvl <= 1 ? baseSize + 5 : lvl === 2 ? baseSize + 2 : baseSize + 1; const k = `h${blocks.length}`; blocks.push(
{renderInline(h[2], k)}
); } else if (li) { flushPara(); list.push(li[1]); } else if (line.trim() === "") { flushPara(); flushList(); } else { flushList(); para.push(line.replace(/^\s*>\s?/, "")); } } flushPara(); flushList(); return
{blocks}
; } function SystemPromptCard({ prompt }: { prompt: string }) { const [open, setOpen] = useState(true); return (
SYSTEM PROMPT setOpen((o) => !o)} />
{open ? (prompt?.trim() ? :

No system prompt set.

) : null}
); } // A same-colored triangle toggle: points down when open, left when closed. function CollapseTick({ open, color, onClick }: { open: boolean; color: string; onClick: () => void }) { return ( ); } // Personality is often stored as JSON (traits/tone/…). Render it as labeled // sections instead of dumping raw JSON; fall back to tags / text otherwise. function PersonalityBody({ raw, fallback }: { raw: string | null | undefined; fallback: string[] }) { const tags = (arr: string[]) =>
{arr.map((p, i) => {p})}
; const trimmed = (raw ?? "").trim(); let parsed: unknown; if (trimmed.startsWith("{") || trimmed.startsWith("[")) { try { parsed = JSON.parse(trimmed); } catch { parsed = undefined; } } if (Array.isArray(parsed)) return tags(parsed.map((x) => String(x))); if (parsed && typeof parsed === "object") { return (
{Object.entries(parsed as Record).map(([k, v]) => (
{k.replace(/_/g, " ")}
{Array.isArray(v) ? tags(v.map((x) => String(x))) : v && typeof v === "object" ?
{Object.entries(v as Record).map(([kk, vv]) =>
{kk}: {String(vv)}
)}
:
{String(v)}
}
))}
); } if (fallback.length) return tags(fallback); if (trimmed) return
{trimmed}
; return ; } function AnatomyCard({ tint, label, count, icon, collapsible, children }: { tint: string; label: string; count?: string; icon?: React.ReactNode; collapsible?: boolean; children: React.ReactNode }) { const [open, setOpen] = useState(true); const collapsed = !!collapsible && !open; return (
{icon} {label} {count || collapsible ? : null} {count ? {count} : null} {collapsible ? setOpen((o) => !o)} /> : null}
{!collapsed ?
{children}
: null}
); } const tag = (dim?: boolean): CSSProperties => ({ fontFamily: mono, fontSize: 10, color: dim ? "#8a8a92" : "#cfcfd5", padding: "3px 7px", borderRadius: 5, background: dim ? "rgba(255,255,255,.03)" : "rgba(255,255,255,.05)" }); function ClawAnatomyCanvas({ agent, teamName, avatarUrl, onToolsChanged, brain }: { agent: DemoAgent; teamName: string; avatarUrl?: string; onToolsChanged?: () => void; brain?: RawBrain }) { const c = agent.compartments; const row = (a: string, b: string, bc = "#cfcfd5") => (
{a}{b}
); const skillsShown = c.skills.slice(0, 3); const skillsRest = c.skills.length - skillsShown.length; // Avatar: click opens the image modal (upload / generate / save). `imgUrl` // holds a just-saved image for instant feedback; otherwise the persisted // `avatarUrl` (agent.avatar) is shown. Both reset when the claw changes. const [imgUrl, setImgUrl] = useState(null); const [seenImgId, setSeenImgId] = useState(agent.id); if (seenImgId !== agent.id) { setSeenImgId(agent.id); setImgUrl(null); } const [avatarOpen, setAvatarOpen] = useState(false); const [addToolOpen, setAddToolOpen] = useState(false); const shownAvatar = imgUrl ?? avatarUrl ?? null; const router = useRouter(); return (
{/* Name (left) · avatar (center) · title (right), above the activity card. */}
{agent.name}
{/* Avatar — centered between the name and title. Click to edit. */}
{agent.role}
{/* Card data mappings (intended real sources for live wiring): NOW RUNNING = every active task this agent is currently working on. SKILLS = the skills assigned to this agent (installed_skills). PERSONALITY = the agent's persona.md / soul.md from the backend. TOOLS = all tools granted to this agent; the "+ Add tool" button installs more from the catalog (/api/skills). */} {/* 3×3 grid: row1 Personality/Skills/Tools, row2 Memory/Capabilities/ Safety, row3 the full-width NOW RUNNING live log. */} {/* PERSONALITY as its own full-width card under the system prompt. */}
} collapsible>
{/* Skills · Tools · Memory across; Capabilities · Safety side by side; then the full-width live log. */}
}>
{skillsShown.map((s) => ({s}))}{skillsRest > 0 ? +{skillsRest} : null}
}>
{c.tools.map((t) => row(t.name, t.state === "gated" ? "gated ✓" : "blocked ⨯", t.state === "gated" ? "#5fd08a" : "#e8b465"))}
}> {brain && brain.memory.length ? (
{brain.memory.slice(0, 6).map((m, i) => (
{m}
))}
) : (
{brain ? no memories yet — chat with this agent and they appear here. : (<>{row("Long-term", c.memory.long)}{row("Recent ctx", c.memory.recent)})}
)}
}>
{c.capabilities.map((p) => ({p}))}
}>
{row("Sandbox", c.safety.sandbox, "#6fd0c0")}{row("Network", c.safety.network, "#6fd0c0")}
{/* Full-width live work log. */}
}>
{agent.nowRunning.length === 0 ? ( idle — no active tasks. The agent's live work log will stream here as it runs. ) : (
{agent.nowRunning.map((t, i) => (
{t.name} {t.detail}
{t.progress != null ? (
) : null}
))}
)}
part of {teamName} · the computer panel runs this agent's apps & routines
{avatarOpen ? ( setAvatarOpen(false)} onSaved={(url) => { setImgUrl(url); router.refresh(); }} /> ) : null} {addToolOpen ? ( setAddToolOpen(false)} onAdded={() => onToolsChanged?.()} /> ) : null}
); }