world: pre-seed repo tree on clone (V3)
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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user