world: solid team + project orbs, hide ROOT, per-topic repo landmarks
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 13s
ci / frontend (push) Successful in 28s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Three fixes to the Live viz per user feedback:

1) Hide the ROOT sentinel — it was rendering at the origin as a red disc
   with no label. It's a physics anchor, not a real orb. Skipping it in
   nodes/edges/labels loops removes the mystery red circle.

2) Emit `repo:<topic_id>` project orbs for active research topics from
   the world SSE loop. Labeled with the topic title, tier "repo" gets its
   own soft sky-blue palette and a landmark radius (r=11 between company
   and team). Assigned agents get a low-weight (0.15) convergence touch
   so pawns cluster around their project's orb even at rest — no wait
   for a file op to see the affiliation.

3) Solid at rest, bloom on interaction. Two engine changes:
   - Structural + repo orbs now have a soft 0.08 glow at heat=0 (down
     from 0.22 ambient) so the disc reads as solid until agents heat it.
   - `world.touch` heat is now weight-scaled (`+w*0.6`) instead of a
     flat `+0.5` regardless of intent. Soft convergence stays soft;
     file ops still explode.

Click a `repo:` orb → the existing Commit F focus mode already treats
that prefix as a subtree root, so users drop straight into the
Gource-style repo detail view with only the files their agents are
touching.

Follow-ups queued: teardown of repo orbs when a topic reaches 'published'
(currently they persist until the SSE loop's status filter drops them,
which is correct); loops equivalent (loop:<id> landmark orbs).
This commit is contained in:
Omar Sobh
2026-07-09 13:22:27 -07:00
parent f88e8642d9
commit 708f45d09b
3 changed files with 101 additions and 21 deletions
+52
View File
@@ -61,6 +61,37 @@ async fn active_runs(pool: &PgPool, ws: WorkspaceId) -> Vec<(String, String)> {
.collect()
}
/// 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.
async fn active_research_topics(
pool: &PgPool,
ws: WorkspaceId,
) -> Vec<(String, String, String)> {
let rows = sqlx::query(
"SELECT t.id::text AS topic_id, t.title AS title, 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
AND t.status IN ('processing', 'reviewing', 'publishing')",
)
.bind(ws.as_uuid())
.fetch_all(pool)
.await
.unwrap_or_default();
rows.into_iter()
.map(|r| {
(
r.get::<String, _>("topic_id"),
r.get::<String, _>("title"),
r.get::<String, _>("agent_id"),
)
})
.collect()
}
/// A short human label for a tool's input (for the tool-call target).
fn summarize_input(input: &Value) -> String {
for k in ["target", "path", "url", "query", "name", "file", "command"] {
@@ -361,6 +392,27 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
}
}
// Active research topics → landmark project orbs. One `repo:<id>`
// per topic, labeled with the topic title so users can click it
// and drop into the repo-focus (Gource) view before any files are
// touched. Assigned agents gently converge on their topic's orb
// 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 {
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 }),
);
}
yield sse(
"world.touch",
json!({ "agentId": agent_id, "nodeId": node_id, "kind": "service", "weight": 0.15 }),
);
}
// Real convergence: each running agent beams toward its active-run node.
for (run_id, agent_id) in &runs {
let node_id = format!("run:{}", &run_id[..run_id.len().min(8)]);