world: pre-seed repo tree on clone (V3)
ci / gates (push) Successful in 6s
ci / rust (push) Failing after 11s
ci / frontend (push) Successful in 25s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Repo focus mode used to open into an empty tree — the file/dir nodes
only synthesized as agents touched files via tool calls. Cold-start
users saw a lonely repo:<id> orb with nothing under it. Now the tree
pre-seeds from the actual cloned repo the moment a client subscribes.

Backend:
- active_research_topics also returns repo_workspace_path.
- preseed_repo_paths(clone_path) runs `git ls-files` (bounded to top 200)
  against the on-disk clone. Silently returns empty on any failure so a
  missing clone / git-off-PATH / empty repo just degrades to the pre-V3
  behavior (tree still builds on touch).
- SSE loop, on first sight of a topic per client, emits one
  node.activity per pre-seeded path (label = leaf name, heat 0) so the
  tree is quiet-solid at rest.

Frontend:
- engine.onNodeActivity now synthesizes the dir:<partial> chain for
  file: nodeIds the same way onTouch does — otherwise the pre-seed
  would render as flat leaves under ROOT.
- Same 5-line synthesis extracted from onTouch; both paths now agree on
  the layout.

Cap of 200 keeps the SSE payload bounded on huge repos; the tail fills
in as agents actually touch files. When we later add per-file heat map
(V4), the 200 already-known files get first-class treatment out of the
gate.
This commit is contained in:
Omar Sobh
2026-07-09 15:49:43 -07:00
parent 9e5034cb53
commit a49cf86623
2 changed files with 92 additions and 8 deletions
+70 -7
View File
@@ -62,16 +62,21 @@ async fn active_runs(pool: &PgPool, ws: WorkspaceId) -> Vec<(String, String)> {
}
/// Active research topics + their assigned agents. Each returned row is a
/// `(topic_id, title, agent_id)` — one row per (topic, agent) pair. Emitted
/// from the SSE loop as `repo:<topic_id>` project orbs so the World shows a
/// clickable, labeled landmark for every in-flight R&D initiative — no need
/// for a file touch to land first.
/// `(topic_id, title, agent_id, repo_workspace_path)` — one row per
/// (topic, agent) pair. Emitted from the SSE loop as `repo:<topic_id>`
/// project orbs so the World shows a clickable, labeled landmark for
/// every in-flight R&D initiative — no need for a file touch to land
/// first. `repo_workspace_path` (when non-null) is the on-disk clone
/// location; the SSE loop uses it to pre-seed the repo tree.
async fn active_research_topics(
pool: &PgPool,
ws: WorkspaceId,
) -> Vec<(String, String, String)> {
) -> Vec<(String, String, String, Option<String>)> {
let rows = sqlx::query(
"SELECT t.id::text AS topic_id, t.title AS title, ra.agent_id::text AS agent_id
"SELECT t.id::text AS topic_id,
t.title AS title,
t.repo_workspace_path AS repo_path,
ra.agent_id::text AS agent_id
FROM research_topics t
JOIN research_topic_agents ra ON ra.topic_id = t.id
WHERE t.workspace_id = $1
@@ -87,11 +92,46 @@ async fn active_research_topics(
r.get::<String, _>("topic_id"),
r.get::<String, _>("title"),
r.get::<String, _>("agent_id"),
r.try_get::<Option<String>, _>("repo_path").unwrap_or(None),
)
})
.collect()
}
/// Cap on pre-seeded file entries per repo. Large repos surface only the
/// top N so the SSE payload stays bounded — a subsequent tool call
/// exercising a specific path will fill in additional nodes on demand.
const REPO_PRESEED_CAP: usize = 200;
/// Read the top-level file list of a topic's cloned repo via `git ls-files`
/// so the SSE loop can pre-seed dir:/file: nodes in the client engine.
/// Bounded by `REPO_PRESEED_CAP`. Returns an empty vec on any failure
/// (missing clone, git not on PATH, empty repo) — a missing pre-seed
/// degrades gracefully to the pre-V3 behavior (tree builds as agents
/// touch files).
async fn preseed_repo_paths(clone_path: &str) -> Vec<String> {
let path = std::path::Path::new(clone_path);
if !path.join(".git").exists() {
return Vec::new();
}
let out = tokio::process::Command::new("git")
.arg("-C")
.arg(path)
.arg("ls-files")
.output()
.await;
let Ok(out) = out else { return Vec::new() };
if !out.status.success() {
return Vec::new();
}
String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| !l.trim().is_empty())
.take(REPO_PRESEED_CAP)
.map(|s| s.to_string())
.collect()
}
/// Enabled scheduled loops + their assigned agents. Same shape as
/// `active_research_topics` — `(loop_id, title, agent_id)` per (loop, agent).
/// Emitted as `loop:<loop_id>` landmark orbs so recurring/scheduled work is
@@ -428,13 +468,36 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
// so the affinity is visible even in idle windows.
let research = active_research_topics(&pool, ws).await;
let mut seen_topics = std::collections::HashSet::new();
for (topic_id, title, agent_id) in &research {
for (topic_id, title, agent_id, repo_path) in &research {
let node_id = format!("repo:{topic_id}");
if seen_topics.insert(topic_id.clone()) {
yield sse(
"node.activity",
json!({ "nodeId": node_id, "label": title, "kind": "service", "heat": 0.0 }),
);
// Pre-seed the repo tree (V3). One-shot on first sight
// of the topic per SSE client. Each file emits with
// heat=0 so the tree is quiet-solid at rest — activity
// still hot-swaps as agents touch files. Bounded to
// REPO_PRESEED_CAP so payload stays reasonable.
if let Some(clone_path) = repo_path {
for p in preseed_repo_paths(clone_path).await {
let leaf = std::path::Path::new(&p)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(&p)
.to_string();
yield sse(
"node.activity",
json!({
"nodeId": format!("file:{p}"),
"label": leaf,
"kind": "service",
"heat": 0.0,
}),
);
}
}
}
yield sse(
"world.touch",