research canvas: collapsible sidebar + live topology-run logs
Two operator-visibility improvements on the research canvas: **A. Collapsible sidebar (RosterColumn)** - Adds a PanelLeftClose/Open chevron toggle on the roster column header. When collapsed the 252px column shrinks to a 32px handle, giving the main canvas the extra real estate. - Persists the choice in localStorage (cm.sidebar.rosterCollapsed) so it sticks across nav and reloads. Client-only hydration keeps SSR + client render in sync. **B. Live run logs panel** - New LiveRunLogs component under ResearchCanvas that renders when the topic has at least one active topology_run. Reuses the existing SSE endpoint /api/topology-runs/:id/events (Last-Event-ID resume already supported), so no backend streaming work needed. - Multi-run support (rare — usually one, but topology fan-out can produce N): tab strip picks which run's stream to view; one EventSource at a time. - Terminal-style panel with timestamped lines, auto-scroll pinned to the tail unless the user scrolls up, 'Jump to latest' button when unpinned. Header pill mirrors stream state (connecting/streaming/done/error). Small backend addition: - cm_db::repo::topology_runs::active_run_ids_for_research_topic — companion to active_runs_for_research_topic that returns ids. Uses dynamic sqlx::query() so cm-db builds air-gapped without a fresh cargo sqlx prepare. - GET /api/research/:id/active-runs — workspace-scoped list feeder for the new panel.
This commit is contained in:
@@ -454,6 +454,10 @@ pub fn router(state: AppState) -> Router {
|
||||
"/api/research/{id}/pipeline-state",
|
||||
get(routes::research_pipeline::pipeline_state),
|
||||
)
|
||||
.route(
|
||||
"/api/research/{id}/active-runs",
|
||||
get(routes::research_pipeline::active_runs),
|
||||
)
|
||||
.route("/api/research/probe", post(routes::probe::probe))
|
||||
.route(
|
||||
"/api/loops",
|
||||
|
||||
@@ -39,6 +39,38 @@ pub struct PipelineState {
|
||||
pub stages: Vec<PipelineStage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ActiveRun {
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ActiveRuns {
|
||||
pub topic_id: Uuid,
|
||||
pub runs: Vec<ActiveRun>,
|
||||
}
|
||||
|
||||
/// `GET /api/research/:id/active-runs` — queued + running topology_run
|
||||
/// ids for this topic, newest first. Feeds the wizard's live-log panel
|
||||
/// (SSE per run at `/api/topology-runs/:id/events`).
|
||||
pub async fn active_runs(
|
||||
State(state): State<AppState>,
|
||||
Authed(user): Authed,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<Json<ActiveRuns>, ApiError> {
|
||||
// Workspace-scope: 404 rather than leak run ids for a topic the
|
||||
// caller can't see.
|
||||
let _topic = cm_db::repo::research_topics::get(&state.pool, id, user.workspace_id.as_uuid())
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
let ids =
|
||||
cm_db::repo::topology_runs::active_run_ids_for_research_topic(&state.pool, id).await?;
|
||||
Ok(Json(ActiveRuns {
|
||||
topic_id: id,
|
||||
runs: ids.into_iter().map(|id| ActiveRun { id }).collect(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// `GET /api/research/:id/pipeline-state`.
|
||||
pub async fn pipeline_state(
|
||||
State(state): State<AppState>,
|
||||
|
||||
Reference in New Issue
Block a user