loops: per-iteration live logs (Steps + Container tabs), collapse graph JSON
The Loops canvas' IterationsTimeline was a flat status-pill list —
no way to see WHAT an iteration was actually doing. Meanwhile the
Research canvas had full LiveRunLogs with Steps + Container tabs
against the same underlying topology_run SSE endpoints. This
factors LiveRunLogs so both canvases share it.
Frontend
- LiveRunLogs gains a directRunId?: string prop. In this mode it
skips the topic-scoped active-runs + pipeline-state polls and
pins activeRun to the given id. topicId stays optional (topic
mode unchanged from ResearchCanvas' perspective).
- LoopsCanvas IterationsTimeline: each row is now a click-to-
expand card. On expand, renders <LiveRunLogs directRunId={...}/>
right below the header — Steps + Container tabs, full SSE tail,
same 320px terminal.
- Auto-opens the newest running/queued iteration so a click on
'Run now' immediately exposes the live pane.
- New CollapsibleSection helper wraps the graph JSON block so it
starts closed. Reference material stays one click away without
cluttering the canvas.
Backend
- run_container_log_sse now resolves the tail target via
loop.source_research_topic_id when the run itself has no
research_topic_id. Paired coding loops (kind=exec with a
source_research_topic_id) reuse the paired topic's team
container, so we tail its docker logs. Pure loop runs still
error with a clearer message.
This commit is contained in:
@@ -482,22 +482,47 @@ pub async fn run_container_log_sse(
|
||||
let ws = user.workspace_id;
|
||||
let stream = async_stream::stream! {
|
||||
use futures::StreamExt;
|
||||
// 1) Workspace scope + topic id.
|
||||
let topic_id = match cm_db::repo::topology_runs::status(&pool, id, ws).await {
|
||||
Ok(_) => match cm_db::repo::topology_runs::research_topic_id(&pool, id).await {
|
||||
Ok(Some(t)) => t,
|
||||
_ => {
|
||||
yield Ok::<Event, Infallible>(
|
||||
Event::default().event("error").data(
|
||||
"run has no bound research topic; container log unavailable",
|
||||
),
|
||||
);
|
||||
return;
|
||||
// 1) Workspace scope + resolve the topic id whose container we'll
|
||||
// tail. Two paths:
|
||||
// a) run.research_topic_id set → research pipeline; use it
|
||||
// directly (existing behavior).
|
||||
// b) research_topic_id NULL + run belongs to a loop whose
|
||||
// source_research_topic_id is set → paired coding loop;
|
||||
// the loop reuses the research topic's team container.
|
||||
// Anything else (raw topology runs, pure loop with no paired
|
||||
// topic) errors out with a clear message.
|
||||
if cm_db::repo::topology_runs::status(&pool, id, ws).await.is_err() {
|
||||
yield Ok::<Event, Infallible>(
|
||||
Event::default().event("error").data("run not found"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let direct = cm_db::repo::topology_runs::research_topic_id(&pool, id).await.ok().flatten();
|
||||
let via_loop = if direct.is_none() {
|
||||
let loop_id = cm_db::repo::topology_runs::loop_id_for_run(&pool, id).await.ok().flatten();
|
||||
match loop_id {
|
||||
Some(lid) => {
|
||||
use sqlx::Row;
|
||||
sqlx::query(
|
||||
"SELECT source_research_topic_id FROM loops WHERE id = $1"
|
||||
)
|
||||
.bind(lid)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|r| r.try_get::<Option<Uuid>, _>("source_research_topic_id").ok().flatten())
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
None => None,
|
||||
}
|
||||
} else { None };
|
||||
let topic_id = match direct.or(via_loop) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
yield Ok::<Event, Infallible>(
|
||||
Event::default().event("error").data("run not found"),
|
||||
Event::default().event("error").data(
|
||||
"run has no bound research topic or paired-loop topic; container log unavailable",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user