research canvas: collapsible sidebar + live topology-run logs
ci / gates (pull_request) Successful in 6s
ci / frontend (pull_request) Failing after 39s
ci / rust (pull_request) Successful in 4m25s
ci / e2e (pull_request) Skipped
ci / publish (pull_request) Skipped

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:
Omar Sobh
2026-07-15 10:01:26 -07:00
parent 13ead5cb9f
commit 203c85d671
7 changed files with 433 additions and 2 deletions
+25
View File
@@ -167,6 +167,31 @@ pub async fn active_runs_for_research_topic(
Ok(row.n.unwrap_or(0))
}
/// Live-run panel companion to `active_runs_for_research_topic`: return
/// the actual run ids (queued + running) so the UI can subscribe to
/// their SSE event streams. Ordered newest first — the freshest run is
/// the one the user just kicked off.
pub async fn active_run_ids_for_research_topic(
pool: &PgPool,
research_topic_id: Uuid,
) -> Result<Vec<Uuid>, DbError> {
use sqlx::Row;
// Dynamic query (not `sqlx::query!`) so cm-db builds air-gapped
// without a fresh `cargo sqlx prepare` round-trip. Schema shape is
// identical to `active_runs_for_research_topic` above.
let rows: Vec<sqlx::postgres::PgRow> = sqlx::query(
"SELECT id
FROM topology_runs
WHERE research_topic_id = $1
AND status IN ('queued', 'running')
ORDER BY created_at DESC",
)
.bind(research_topic_id)
.fetch_all(pool)
.await?;
Ok(rows.into_iter().map(|r| r.get::<Uuid, _>("id")).collect())
}
/// The research topic this run belongs to, if any. Used by the topology
/// worker's `freeze_research_outcome` post-hook to snapshot the run's
/// final synthesis into `research_outcomes`.