feat(viz): the World shows one mission, not every mission at once

The visualization page carried the org -> company -> team -> agent forest in its
sidebar — the hierarchy the agents page stopped rendering — so the two pages
disagreed about the shape of the workspace, and there was no way to ask the
World to show a single mission. Everything ran together in one clump.

Same sidebar as the agents page now: My Workforce, missions under it, agents
under those. Selecting a mission scopes the scene to that mission's crew.

Scoping had to happen at the FEED, not the seed. `WorldEngine.ensurePawn`
materialises a pawn for any agentId an event mentions, so seeding the engine
with one crew would have left every other mission's agents streaming in
anyway — the view would have looked filtered for a frame and then re-clumped.
`focusAgents` gates every agent-bearing event, `focusMissionId` keeps other
missions' landmark orbs out, and comm beams require BOTH ends in focus or a
delegation would drag an outside agent onto the stage.

The engine also re-seeds when the focus changes. It was seeded once on mount,
which was right when the World only ever showed everything; now a stale engine
would keep the previous mission's pawns on stage, and the feed filter cannot
remove what is already there. Keyed on focusMissionId rather than on `roots`
identity — `roots` is rebuilt every Dashboard render, so depending on it would
throw the scene away continuously.

The HUD says which mission is being shown when scoped. A filtered world and an
idle world look identical otherwise, and that difference is the whole question
a viewer is asking.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-10 16:17:28 -07:00
co-authored by Claude Opus 5
parent d3a398716b
commit 44079eb8b4
2 changed files with 150 additions and 22 deletions
@@ -21,6 +21,7 @@ import { topologyById } from "@/lib/topologies";
import { panelParsers, type DeviceSize } from "@/lib/url/panel-params"; 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 type { WorldSeed } from "../world/engine";
import { ObservePanel } from "../observe/ObservePanel"; import { ObservePanel } from "../observe/ObservePanel";
import { StructureTree, orgNode, type TreeItem, clawNode } from "./StructureTree"; import { StructureTree, orgNode, type TreeItem, clawNode } from "./StructureTree";
import { MissionsList } from "./MissionsList"; import { MissionsList } from "./MissionsList";
@@ -471,13 +472,20 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
const onWorldSelect = (id: string) => { const onWorldSelect = (id: string) => {
if (!id) { setWorldSel(null); return; } if (!id) { setWorldSel(null); return; }
setWorldSel(id); setWorldSel(id);
// A mission grouping row carries no level and is not a row in any of the
// org tables — it only sets the focus, which `focusedMissionId` derives
// from `worldSel`.
if (isGroupingRow(id)) return;
const lv = nodeLevel.get(id); const lv = nodeLevel.get(id);
if (lv === "claw") { if (lv === "claw") {
// Agents open a summary in the side panel (no redirect — the panel's robot // 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. // button drills in). Keep the path ids coherent for the summary + drill.
const loc = locateAgent(id); // The tree id may be `<missionId>:<clawId>`; every consumer below wants
// the claw id alone.
const clawId = clawIdOf(id);
const loc = locateAgent(clawId);
if (loc) { setOrgId(loc.org.id); setCompanyId(loc.company.id); setTeamId(loc.team.id); } if (loc) { setOrgId(loc.org.id); setCompanyId(loc.company.id); setTeamId(loc.team.id); }
setAgentId(id); setAgentId(clawId);
setWorldPanelOpen(true); setWorldPanelOpen(true);
} else if (lv === "org") selectOrg(id); } else if (lv === "org") selectOrg(id);
else if (lv === "company") selectCompany(id); else if (lv === "company") selectCompany(id);
@@ -500,8 +508,12 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
// is nothing else to do with it. // is nothing else to do with it.
if (ORPHAN_CONTAINER_IDS.has(item.id)) { setOrphanDialogOpen(true); return; } if (ORPHAN_CONTAINER_IDS.has(item.id)) { setOrphanDialogOpen(true); return; }
if (SYNTHETIC_TREE_IDS.has(item.id)) return; if (SYNTHETIC_TREE_IDS.has(item.id)) return;
// A mission grouping row is a heading, like the workforce root: the row // On the World tier a mission grouping row is the whole point: selecting it
// click has already toggled the branch. Falling through would hand a // focuses the scene on that mission. `worldSel` is the single source of
// that focus, so it is set here rather than in a second state.
if (isWorld && isGroupingRow(item.id)) { onWorldSelect(item.id); return; }
// Everywhere else a grouping row is a heading, like the workforce root: the
// row click has already toggled the branch. Falling through would hand a
// MISSION uuid to selectTeam, which is a real-looking id for the wrong // MISSION uuid to selectTeam, which is a real-looking id for the wrong
// table. // table.
if (isGroupingRow(item.id)) return; if (isGroupingRow(item.id)) return;
@@ -718,11 +730,55 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
meta: `${distinctAgentCount} agent${distinctAgentCount === 1 ? "" : "s"}`, meta: `${distinctAgentCount} agent${distinctAgentCount === 1 ? "" : "s"}`,
children: useGrouped ? groupedChildren : allAgents.map(clawNode), children: useGrouped ? groupedChildren : allAgents.map(clawNode),
}; };
const treeRoots: TreeItem[] = isWorld ? worldRoots : [workforceRoot]; // The World tier gets the SAME workforce sidebar as the agents page.
//
// It used to show the org → company → team → agent forest, which is the
// hierarchy the agents page stopped rendering — so the two pages disagreed
// about what the workspace looks like, and the World offered no way to ask
// "show me just this mission". Missions are the unit people think in, so the
// sidebar is missions here too, and picking one scopes the scene.
const treeRoots: TreeItem[] = [workforceRoot];
const treeActiveId = isClaw ? agentId : worldSel; const treeActiveId = isClaw ? agentId : worldSel;
// Open by default. A workforce collapsed behind one disclosure is a workforce // Open by default. A workforce collapsed behind one disclosure is a workforce
// the user has to discover they own. // the user has to discover they own.
const treeAutoExpand: string[] = isWorld ? [] : ["my-workforce"]; const treeAutoExpand: string[] = ["my-workforce"];
// Which mission the World is focused on, derived from the tree selection so
// there is ONE selection state rather than a second one to keep in sync.
// Selecting a mission group focuses it; selecting an agent inside a group
// keeps that mission focused, which is what makes clicking around inside a
// mission feel stable.
const focusedMissionId: string | null = (() => {
if (!isWorld || !worldSel) return null;
if (worldSel.startsWith("mission-group:")) return worldSel.slice("mission-group:".length);
const grouped = (workforce?.missions ?? []).find((m) =>
m.agents.some((a) => `${m.mission_id}:${a.id}` === worldSel),
);
return grouped?.mission_id ?? null;
})();
const focusedMission = (workforce?.missions ?? []).find((m) => m.mission_id === focusedMissionId) ?? null;
const focusAgentIds: Set<string> | null = focusedMission
? new Set(focusedMission.agents.map((a) => a.id))
: null;
// Seed the scene with just this mission's crew when one is focused. The seed
// alone does not scope the view (the engine materialises any agent an event
// mentions) — WorldCanvas filters the feed — but it means the mission's own
// people are on stage immediately rather than fading in with their first
// event.
const worldSeedRoots: WorldSeed[] = focusedMission
? [{
id: `mission:${focusedMission.mission_id}`,
level: "team",
label: focusedMission.title?.trim() || "Untitled mission",
status: focusedMission.status,
children: focusedMission.agents.map((a) => ({
id: a.id,
level: "claw",
label: a.name,
status: a.status,
})),
}]
: worldCanvasRoots;
// Every node in the active tree (flattened) — the wrench multi-select looks up // 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 nodes here regardless of depth, and derives the delete kind from the
@@ -1009,7 +1065,17 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<> <>
{/* Graph stage — condensed by the right slide-out's width. */} {/* Graph stage — condensed by the right slide-out's width. */}
<div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}> <div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}>
<WorldCanvas roots={worldCanvasRoots} expanded={expanded} onToggleExpand={toggleExpand} selectedId={worldSel} onSelect={onWorldSelect} onOpenRuns={() => setRunsOpen(true)} /> <WorldCanvas
roots={worldSeedRoots}
expanded={expanded}
onToggleExpand={toggleExpand}
selectedId={worldSel}
onSelect={onWorldSelect}
onOpenRuns={() => setRunsOpen(true)}
focusAgents={focusAgentIds}
focusMissionId={focusedMissionId}
focusLabel={focusedMission?.title ?? null}
/>
{/* Open the slide-out (top-right) when it's closed. */} {/* Open the slide-out (top-right) when it's closed. */}
{!worldPanelOpen ? ( {!worldPanelOpen ? (
<button type="button" aria-label="Open panel" title="Panel" onClick={() => setWorldPanelOpen(true)} style={{ position: "absolute", top: 14, right: 16, zIndex: 50, width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><PanelRight aria-hidden size={19} /></button> <button type="button" aria-label="Open panel" title="Panel" onClick={() => setWorldPanelOpen(true)} style={{ position: "absolute", top: 14, right: 16, zIndex: 50, width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><PanelRight aria-hidden size={19} /></button>
@@ -1026,8 +1092,12 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<button type="button" aria-label="Close panel" onClick={() => setWorldPanelOpen(false)} style={{ width: 30, height: 30, flex: "none", borderRadius: 8, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#cfcfd5", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><X aria-hidden size={16} /></button> <button type="button" aria-label="Close panel" onClick={() => setWorldPanelOpen(false)} style={{ width: 30, height: 30, flex: "none", borderRadius: 8, border: "1px solid rgba(255,255,255,.12)", background: "transparent", color: "#cfcfd5", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><X aria-hidden size={16} /></button>
</div> </div>
{(() => { {(() => {
const loc = locateAgent(worldSel); // `worldSel` may be a mission-group id or a composite
if (!loc) return <ObservePanel focusAgent={worldSel} onClearFocus={() => setWorldSel(null)} />; // `<missionId>:<clawId>`; the panel wants a claw id, and a
// grouping row has none.
const selClawId = worldSel && !isGroupingRow(worldSel) ? clawIdOf(worldSel) : null;
const loc = locateAgent(selClawId);
if (!loc) return <ObservePanel focusAgent={selClawId} onClearFocus={() => setWorldSel(null)} />;
const { org: o, company: co, team: t, agent: a } = loc; const { org: o, company: co, team: t, agent: a } = loc;
const statusCol = a.status === "running" ? "#5ec8d8" : a.status === "online" ? "#5fd08a" : "#5a5a62"; const statusCol = a.status === "running" ? "#5ec8d8" : a.status === "online" ? "#5fd08a" : "#5a5a62";
const chip = (text: string, col = "#9a9aa2") => <span style={{ fontFamily: mono, fontSize: 10, color: col, padding: "3px 9px", borderRadius: 999, background: `${col}1a`, border: `1px solid ${col}33` }}>{text}</span>; const chip = (text: string, col = "#9a9aa2") => <span style={{ fontFamily: mono, fontSize: 10, color: col, padding: "3px 9px", borderRadius: 999, background: `${col}1a`, border: `1px solid ${col}33` }}>{text}</span>;
+71 -13
View File
@@ -77,6 +77,15 @@ interface WorldCanvasProps {
expanded?: Set<string>; expanded?: Set<string>;
onToggleExpand?: (id: string) => void; onToggleExpand?: (id: string) => void;
onOpenRuns?: () => void; onOpenRuns?: () => void;
/// Agent ids to render. Empty or absent means the whole workspace, which is
/// what the World showed before missions could be focused.
focusAgents?: Set<string> | null;
/// The mission being focused, used to keep OTHER missions' landmark orbs out
/// of the scene. Separate from `focusAgents` because a mission node carries
/// no agent id.
focusMissionId?: string | null;
/// Shown in the HUD so a filtered view never looks like an empty world.
focusLabel?: string | null;
} }
const FORMATIONS: { id: Formation; label: string }[] = [ const FORMATIONS: { id: Formation; label: string }[] = [
@@ -98,7 +107,7 @@ const VIEW_LEGEND: Record<Formation, string> = {
hierarchy: "agents are neurons in a mesh; pathways fire as they work", hierarchy: "agents are neurons in a mesh; pathways fire as they work",
}; };
export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExpand, onOpenRuns }: WorldCanvasProps) { export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExpand, onOpenRuns, focusAgents, focusMissionId, focusLabel }: WorldCanvasProps) {
const mountRef = useRef<HTMLDivElement>(null); const mountRef = useRef<HTMLDivElement>(null);
const labelLayerRef = useRef<HTMLDivElement>(null); const labelLayerRef = useRef<HTMLDivElement>(null);
const live = useClawmatesLive(); const live = useClawmatesLive();
@@ -132,13 +141,25 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
speedRef.current = speedMult; speedRef.current = speedMult;
}); });
// seed the engine once from the structure roots // Seed the engine from the structure roots, and RE-seed when the focus
// changes.
//
// This used to run once on mount, which was right when the World only ever
// showed the whole workspace. Now that a mission can be focused, keeping the
// old engine would leave the previous mission's pawns on stage — the live
// filter stops NEW events for them but cannot remove what is already there.
// A fresh engine is the honest reset, and it is cheap: the scene rebuilds
// from the feed within a frame or two.
//
// Keyed on the focus rather than on `roots` identity: `roots` is rebuilt on
// every Dashboard render, so depending on it directly would throw the scene
// away continuously.
useEffect(() => { useEffect(() => {
const engine = new WorldEngine(); const engine = new WorldEngine();
engine.seed(roots); engine.seed(roots);
engineRef.current = engine; engineRef.current = engine;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [focusMissionId]);
// poll the engine for the HUD counts (agents + currently working) // poll the engine for the HUD counts (agents + currently working)
useEffect(() => { useEffect(() => {
@@ -156,33 +177,58 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
useEffect(() => { useEffect(() => {
const e = engineRef.current; const e = engineRef.current;
if (!e || mode !== "live") return; if (!e || mode !== "live") return;
// Scope the feed to one mission's crew.
//
// Seeding the engine with only that crew is NOT enough: `ensurePawn`
// creates a pawn for any agentId an event mentions, so every other
// mission's agents would materialise anyway and the view would be the
// same clump it was before. The filter has to sit here, between the feed
// and the engine.
//
// `focusAgents` empty/absent means the whole world, which is the previous
// behaviour and the default.
const inFocus = (agentId?: string | null) =>
!focusAgents || focusAgents.size === 0 || (!!agentId && focusAgents.has(agentId));
// A node is in focus when it is THIS mission's landmark, or when it is not
// a mission landmark at all (files, runs and tools are reached via a
// touch, whose agent has already been filtered).
const nodeInFocus = (nodeId?: string | null) => {
if (!focusMissionId || !nodeId) return true;
if (!nodeId.startsWith("mission:")) return true;
return nodeId === `mission:${focusMissionId}`;
};
const offs = [ const offs = [
live.on("topology.update", (d) => e.onTopology(d)), live.on("topology.update", (d) => e.onTopology(d)),
live.on("agent.status", (d) => e.onStatus(d)), live.on("agent.status", (d) => { if (inFocus(d.agentId)) e.onStatus(d); }),
live.on("world.touch", (d) => e.onTouch(d)), live.on("world.touch", (d) => { if (inFocus(d.agentId) && nodeInFocus(d.nodeId)) e.onTouch(d); }),
live.on("node.activity", (d) => e.onNodeActivity(d)), live.on("node.activity", (d) => { if (nodeInFocus(d.nodeId)) e.onNodeActivity(d); }),
live.on("agent.reasoning.delta", (d) => e.onReasoning(d.agentId)), live.on("agent.reasoning.delta", (d) => { if (inFocus(d.agentId)) e.onReasoning(d.agentId); }),
live.on("agent.memory", (d) => e.onMemory(d)), live.on("agent.memory", (d) => { if (inFocus(d.agentId)) e.onMemory(d); }),
// Comm beams — magenta for direct messages, coral for delegations. // Comm beams — magenta for direct messages, coral for delegations.
// The lifecycle "recent-comm web" also builds from these calls. // The lifecycle "recent-comm web" also builds from these calls.
// Both ends must be in focus, or a beam would drag an outside agent in.
live.on("agent.message", (d) => { live.on("agent.message", (d) => {
if (d.fromAgentId && d.toAgentId) e.onAgentComm(d.fromAgentId, d.toAgentId, "#ff5eae"); if (d.fromAgentId && d.toAgentId && inFocus(d.fromAgentId) && inFocus(d.toAgentId)) {
e.onAgentComm(d.fromAgentId, d.toAgentId, "#ff5eae");
}
}), }),
live.on("agent.delegate", (d) => { live.on("agent.delegate", (d) => {
if (d.fromAgentId && d.toAgentId) e.onAgentComm(d.fromAgentId, d.toAgentId, "#ff8a7a"); if (d.fromAgentId && d.toAgentId && inFocus(d.fromAgentId) && inFocus(d.toAgentId)) {
e.onAgentComm(d.fromAgentId, d.toAgentId, "#ff8a7a");
}
}), }),
// Room chat: fan out from the poster to every other participant. // Room chat: fan out from the poster to every other participant.
// Same edge palette so participants see themselves reciprocally // Same edge palette so participants see themselves reciprocally
// connected in the emergent shape. // connected in the emergent shape.
live.on("room.message", (d) => { live.on("room.message", (d) => {
if (!d.fromAgentId) return; if (!d.fromAgentId || !inFocus(d.fromAgentId)) return;
for (const pid of d.participantIds ?? []) { for (const pid of d.participantIds ?? []) {
if (pid && pid !== d.fromAgentId) e.onAgentComm(d.fromAgentId, pid, "#c98af0"); if (pid && pid !== d.fromAgentId && inFocus(pid)) e.onAgentComm(d.fromAgentId, pid, "#c98af0");
} }
}), }),
]; ];
return () => offs.forEach((off) => off()); return () => offs.forEach((off) => off());
}, [live, mode]); }, [live, mode, focusAgents, focusMissionId]);
// replay mode: fetch the run-history timeline and feed it back through the engine // replay mode: fetch the run-history timeline and feed it back through the engine
useEffect(() => { useEffect(() => {
@@ -1156,6 +1202,18 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
> >
<div style={{ fontSize: 11, letterSpacing: ".08em", color: "#ff8a7a", fontWeight: 700 }}>{VIEW_TITLE[formation]}</div> <div style={{ fontSize: 11, letterSpacing: ".08em", color: "#ff8a7a", fontWeight: 700 }}>{VIEW_TITLE[formation]}</div>
<div style={{ fontSize: 10, color: "#8a8a92", marginTop: 3, lineHeight: 1.5 }}>{VIEW_LEGEND[formation]}</div> <div style={{ fontSize: 10, color: "#8a8a92", marginTop: 3, lineHeight: 1.5 }}>{VIEW_LEGEND[formation]}</div>
{/* Say what is being shown when the scene is scoped. A filtered world
and a quiet world look identical, and the difference matters: one
is "this mission is idle", the other is "you are watching one
mission". */}
{focusMissionId ? (
<div style={{ fontSize: 10, color: "#6fd0c0", marginTop: 5, display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#6fd0c0", flex: "none" }} />
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 230 }}>
{focusLabel?.trim() || "this mission"} only
</span>
</div>
) : null}
<div style={{ fontSize: 10.5, color: "#cfcfd5", marginTop: 6, display: "flex", gap: 10, flexWrap: "wrap" }}> <div style={{ fontSize: 10.5, color: "#cfcfd5", marginTop: 6, display: "flex", gap: 10, flexWrap: "wrap" }}>
<span> <span>
{hud.agents} agent{hud.agents === 1 ? "" : "s"} {hud.agents} agent{hud.agents === 1 ? "" : "s"}