world: always-on topology edges (V2)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 9s
ci / frontend (push) Successful in 3m25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

The parent → child hierarchy was already drawn between structural nodes,
but agent pawns had no visible tie to their team — you saw a swarm of
free-floating disks with no legible affiliation. Two new persistent
edge kinds:

- Pawn → home team: every agent gets a thin line to their team node,
  so the workspace skeleton reads visually. Line stays whether the
  pawn is idle or working.
- Pawn → active landmark: when a pawn is targeting a repo:<id> or
  loop:<id> orb (the soft-convergence state we shipped in V1 + the
  research pass), an edge appears too. Bridges the gap between
  world.touch bumps so the affinity stays visible during quiet periods.

Focus mode culling honored — pawn/target edges only draw if both ends
are in the visible subtree, so the Gource repo view still reads clean.

All three edge types share the same LineBasicMaterial + draw call, so
this is free at render time.
This commit is contained in:
Omar Sobh
2026-07-09 13:59:40 -07:00
parent 5a2340587e
commit 059873b59f
+31 -2
View File
@@ -759,8 +759,17 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
} }
} }
// edges — parent → child hierarchy. In focus mode only draw the // edges — the always-on topology skeleton, drawn in two passes so
// subtree so the repo detail view reads as a clean file tree. // the World reads as a system, not just a swarm:
//
// 1. Parent → child hierarchy (org → company → team → landmark).
// In focus mode we cull to the subtree so the repo detail view
// still reads as a clean file tree.
// 2. Pawn → home team (thin persistent line so every agent visibly
// "lives" in their team's neighborhood).
// 3. Pawn → its current landmark target (repo: or loop:) when the
// pawn is actively engaging one, so the agent↔project affinity
// stays visible between world.touch bumps.
const epos: number[] = []; const epos: number[] = [];
for (const n of engine.nodes.values()) { for (const n of engine.nodes.values()) {
if (!n.parentId) continue; if (!n.parentId) continue;
@@ -771,6 +780,26 @@ export function WorldCanvas({ roots, selectedId, onSelect, expanded, onToggleExp
if (visibleNodes && !visibleNodes.has(p.id)) continue; if (visibleNodes && !visibleNodes.has(p.id)) continue;
epos.push(n.x, n.y, 0, p.x, p.y, 0); epos.push(n.x, n.y, 0, p.x, p.y, 0);
} }
// Pawn → home + pawn → active landmark. Kept in the same buffer so
// the whole skeleton renders as one draw call with a single opacity.
for (const p of engine.pawns.values()) {
const home = p.homeId ? engine.nodes.get(p.homeId) : undefined;
if (home && home.tier !== "root") {
if (!visibleNodes || (visibleNodes.has(p.id) && visibleNodes.has(home.id))) {
epos.push(p.x, p.y, 0, home.x, home.y, 0);
}
}
if (p.targetId) {
const t = engine.nodes.get(p.targetId);
if (
t &&
(t.tier === "repo" || t.tier === "loop") &&
(!visibleNodes || (visibleNodes.has(p.id) && visibleNodes.has(t.id)))
) {
epos.push(p.x, p.y, 0, t.x, t.y, 0);
}
}
}
edgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(epos, 3)); edgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(epos, 3));
edgeGeom.attributes.position.needsUpdate = true; edgeGeom.attributes.position.needsUpdate = true;
edgeGeom.setDrawRange(0, epos.length / 3); edgeGeom.setDrawRange(0, epos.length / 3);