agents sidebar: fold org/company/team/agent tree into one pane
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 10s
ci / frontend (push) Successful in 26s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

The Agents tier used to render a flat list of agent cards with an
"Add to teams" button pinned to the bottom. Swap that for the same
nested tree the Visualizations tier already uses so the whole
workforce reads from one collapsible view.

- treeRoots is now worldRoots on both tiers (no more clawNode flat
  fallback); the Agents tier's selectLevel stays "claw" so the
  Wrench → select → Delete flow keeps its agent-only scope.
- Drop the "Add to teams" footer button plus its addTeamOpen state,
  AddToTeamModal render, and import.
- Header on the Agents tier now shows the full breakdown:
  N ORGS · N CO · N TEAMS · N AGENTS.
This commit is contained in:
Omar Sobh
2026-07-08 05:30:34 -07:00
parent a6da19430f
commit 16fcf8ef96
@@ -22,7 +22,7 @@ import { panelParsers, type DeviceSize } from "@/lib/url/panel-params";
import { type FlowItem } from "./flow/TopologyFlow"; import { type FlowItem } from "./flow/TopologyFlow";
import { WorldCanvas } from "../world/WorldCanvas"; import { WorldCanvas } from "../world/WorldCanvas";
import { ObservePanel } from "../observe/ObservePanel"; import { ObservePanel } from "../observe/ObservePanel";
import { StructureTree, orgNode, clawNode, type TreeItem } from "./StructureTree"; import { StructureTree, orgNode, type TreeItem } from "./StructureTree";
import { ResearchList } from "./ResearchList"; import { ResearchList } from "./ResearchList";
import { ResearchCanvas } from "./ResearchCanvas"; import { ResearchCanvas } from "./ResearchCanvas";
import { LoopsList } from "./LoopsList"; import { LoopsList } from "./LoopsList";
@@ -43,7 +43,6 @@ import { BrainRegistryPanel } from "./BrainRegistryPanel";
import { BrainHistoryModal } from "./BrainHistoryModal"; import { BrainHistoryModal } from "./BrainHistoryModal";
import { TeamRunsModal } from "./TeamRunsModal"; import { TeamRunsModal } from "./TeamRunsModal";
import { ReapProgressModal, type ReapKind } from "./ReapProgressModal"; import { ReapProgressModal, type ReapKind } from "./ReapProgressModal";
import { AddToTeamModal } from "./AddToTeamModal";
import { AddToCompanyModal } from "./AddToCompanyModal"; import { AddToCompanyModal } from "./AddToCompanyModal";
import { AddToOrgModal } from "./AddToOrgModal"; import { AddToOrgModal } from "./AddToOrgModal";
import { DevicePanel } from "@/components/computer/DevicePanel"; import { DevicePanel } from "@/components/computer/DevicePanel";
@@ -280,7 +279,6 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
} }
} }
// Grouping modals: claws→team, teams→company, companies→org. // Grouping modals: claws→team, teams→company, companies→org.
const [addTeamOpen, setAddTeamOpen] = useState(false);
const [addCompanyOpen, setAddCompanyOpen] = useState(false); const [addCompanyOpen, setAddCompanyOpen] = useState(false);
const [addOrgOpen, setAddOrgOpen] = useState(false); const [addOrgOpen, setAddOrgOpen] = useState(false);
@@ -508,7 +506,11 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
return roots; return roots;
}; };
const worldCanvasRoots: TreeItem[] = narrowRoots(worldRoots, worldSel); const worldCanvasRoots: TreeItem[] = narrowRoots(worldRoots, worldSel);
const treeRoots: TreeItem[] = isClaw ? allAgents.map(clawNode) : worldRoots; // 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 treeActiveId = isClaw ? agentId : worldSel;
const treeAutoExpand: string[] = []; const treeAutoExpand: string[] = [];
@@ -607,7 +609,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
) : isInfra || isResearch || isLoops || isRepos ? null : ( ) : isInfra || isResearch || isLoops || isRepos ? null : (
<div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)", display: "flex", alignItems: "flex-start", gap: 8 }}> <div style={{ padding: "16px 16px 12px", borderBottom: "1px solid rgba(255,255,255,.06)", display: "flex", alignItems: "flex-start", gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 6 }}>{allAgents.length} AGENT{allAgents.length === 1 ? "" : "S"}</div> <div style={{ fontFamily: mono, fontSize: 10, letterSpacing: ".12em", color: "#5a5a62", marginBottom: 6 }}>{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"}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.01em" }}>Agents</div> <div style={{ fontSize: 18, fontWeight: 700, color: "#f3f3f5", letterSpacing: "-.01em" }}>Agents</div>
</div> </div>
<button type="button" onClick={() => { setSelectMode((v) => { if (v) setSelectedAgents(new Set()); return !v; }); }} title="Select agents to manage" aria-label="Select agents" style={{ flex: "none", width: 34, height: 34, borderRadius: 9, border: `1px solid ${selectMode ? "rgba(255,111,97,.5)" : "rgba(255,255,255,.12)"}`, background: selectMode ? "rgba(255,111,97,.12)" : "transparent", color: selectMode ? "#ff6f61" : "#9a9aa2", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}><Wrench aria-hidden size={16} /></button> <button type="button" onClick={() => { setSelectMode((v) => { if (v) setSelectedAgents(new Set()); return !v; }); }} title="Select agents to manage" aria-label="Select agents" style={{ flex: "none", width: 34, height: 34, borderRadius: 9, border: `1px solid ${selectMode ? "rgba(255,111,97,.5)" : "rgba(255,255,255,.12)"}`, background: selectMode ? "rgba(255,111,97,.12)" : "transparent", color: selectMode ? "#ff6f61" : "#9a9aa2", cursor: "pointer", display: "inline-flex", alignItems: "center", justifyContent: "center" }}><Wrench aria-hidden size={16} /></button>
@@ -663,10 +665,6 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<button type="button" onClick={() => { setSelectMode(false); setSelectedAgents(new Set()); }} style={{ flex: "none", padding: "9px 14px", borderRadius: 9, border: "1px solid rgba(255,255,255,.14)", background: "transparent", color: "#9a9aa2", fontSize: 12.5, fontWeight: 600, cursor: "pointer" }}>Cancel</button> <button type="button" onClick={() => { setSelectMode(false); setSelectedAgents(new Set()); }} style={{ flex: "none", padding: "9px 14px", borderRadius: 9, border: "1px solid rgba(255,255,255,.14)", background: "transparent", color: "#9a9aa2", fontSize: 12.5, fontWeight: 600, cursor: "pointer" }}>Cancel</button>
</div> </div>
</div> </div>
) : isClaw ? (
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px" }}>
<button type="button" onClick={() => setAddTeamOpen(true)} style={{ width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 9, border: "1px solid rgba(94,200,216,.35)", background: "rgba(94,200,216,.08)", color: "#5ec8d8", fontSize: 12.5, fontWeight: 600, cursor: "pointer" }}><Users aria-hidden size={14} /> Add to teams</button>
</div>
) : null} ) : null}
</> </>
)} )}
@@ -906,7 +904,6 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
{reapOpen ? <ReapProgressModal items={selectedItems} kind={reapKind} onClose={() => setReapOpen(false)} onDone={() => { setReapOpen(false); setSelectMode(false); setSelectedAgents(new Set()); router.refresh(); }} /> : null} {reapOpen ? <ReapProgressModal items={selectedItems} kind={reapKind} onClose={() => 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 claws into a new named team, then land on its team page. */}
{addTeamOpen ? <AddToTeamModal claws={claws} onClose={() => setAddTeamOpen(false)} /> : null}
{/* Group existing teams into a new named company, then land on its page. */} {/* Group existing teams into a new named company, then land on its page. */}
{addCompanyOpen ? ( {addCompanyOpen ? (