world viz: repo focus mode — click a file/dir/run to enter the tree
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 41s
ci / rust (push) Successful in 4m1s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 12m18s

MVP of the repo-detail sub-view. Click any file/dir/run/repo node
in Live and the viz culls to just that node's subtree — you're
now watching agents crawl the repo instead of the whole workspace.
Esc (or the pill button that appears) exits.

Backend
- routes/world.rs: file-op tool events now emit a `file:<path>`
  world.touch IN ADDITION to the existing `tool:<name>` touch.
  The path is pulled from the tool input's path / target / file
  / filename / url keys (same lookup summarize_input uses, but we
  keep the full string so the client can build a real hierarchy).
  Non-file tools are unchanged — they still hit tool:<name> nodes
  as before.

Engine
- onTouch synthesizes a directory hierarchy when the id starts
  with `file:`. Each intermediate path segment gets a `dir:<acc>`
  node (label = the segment), parented at the previous dir; the
  file itself parents at the innermost dir. Ensures the layout
  spring-simulates as a tree naturally, no separate render mode
  needed.
- New pawn fireColor for file: touches: coral #ff8a7a. Reads as
  "file work" vs #5ec8d8 (tool convergence) vs #5fd08a (run
  activity).

WorldCanvas
- Render pass now takes a `visibleNodes: Set<string> | null`.
  When the selected id starts with file: / dir: / run: / repo:,
  we BFS descendants via parentId and hide every non-descendant
  node. Node meshes, glow sprites, hierarchy edges, and labels
  all gate on the set. Pawns stay visible (agents still dart to
  the focused files).
- ESC handler on window: clears the selection by calling
  onSelect("") when a repo-focus id is set.
- Small "REPO FOCUS · <path>" pill lands at top-center with an
  Esc button so the exit is discoverable at a glance without
  learning the shortcut.
- Dashboard.onWorldSelect now treats empty string as "clear
  focus" (setWorldSel(null)) so the same callback handles ESC.

Not yet: the always-on repo:<topic_id> node emitted at run
start when a research topic has a repo bound. Today the focus
works off run:<id> nodes; a repo:<id> anchor would let users
click without waiting for a first file touch. Also skipped:
per-file heat map / call-count visualization tied to touch
weight over time. Both are natural follow-ups on this bones.
This commit is contained in:
Omar Sobh
2026-07-09 12:16:05 -07:00
parent bce370978d
commit f88e8642d9
4 changed files with 168 additions and 4 deletions
+27
View File
@@ -114,6 +114,33 @@ fn normalize_run_event(
out.push(("node.activity", json!({ "nodeId": node_id, "label": tool, "kind": "service", "heat": if file_op { 1.0 } else { 0.9 } })));
// the agent converges on the tool it's using (the Gource beam)
out.push(("world.touch", json!({ "agentId": agent_id, "nodeId": node_id, "kind": "service", "weight": weight })));
// File-op tools ALSO emit a `file:<path>` touch so the repo
// detail view can build the tree from real events. The path
// comes from the input's `path` / `target` / `file` / `url`
// keys — same lookup summarize_input does but we keep the
// full string so the client can build the dir hierarchy.
if file_op {
if let Some(input) = payload.get("input") {
for k in ["path", "target", "file", "filename", "url"] {
if let Some(p) = input.get(k).and_then(|v| v.as_str()) {
let cleaned = p.trim().trim_start_matches("./");
if !cleaned.is_empty() {
let file_node = format!("file:{cleaned}");
out.push((
"node.activity",
json!({ "nodeId": file_node, "label": cleaned, "kind": "service", "heat": 1.0 }),
));
out.push((
"world.touch",
json!({ "agentId": agent_id, "nodeId": file_node, "kind": "service", "weight": 1.0 }),
));
break;
}
}
}
}
}
}
"approval_required" => {
let action = payload