From 52fc93180bfbb7e00b9eb08edf80bcf57121fca7 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 7 Apr 2026 11:53:31 -0700 Subject: [PATCH] feat: add ecc2 dashboard quick-spawn action --- ecc2/src/tui/app.rs | 2 +- ecc2/src/tui/dashboard.rs | 90 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/ecc2/src/tui/app.rs b/ecc2/src/tui/app.rs index 92dc36d0..a809d131 100644 --- a/ecc2/src/tui/app.rs +++ b/ecc2/src/tui/app.rs @@ -38,7 +38,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> { (_, KeyCode::Char('-')) => dashboard.decrease_pane_size(), (_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(), (_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(), - (_, KeyCode::Char('n')) => dashboard.new_session(), + (_, KeyCode::Char('n')) => dashboard.new_session().await, (_, KeyCode::Char('s')) => dashboard.stop_selected().await, (_, KeyCode::Char('u')) => dashboard.resume_selected().await, (_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await, diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index 451f860c..3c868871 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -504,8 +504,32 @@ impl Dashboard { } } - pub fn new_session(&mut self) { - tracing::info!("New session dialog requested"); + pub async fn new_session(&mut self) { + if self.active_session_count() >= self.cfg.max_parallel_sessions { + tracing::warn!( + "Cannot queue new session: active session limit reached ({})", + self.cfg.max_parallel_sessions + ); + return; + } + + let task = self.new_session_task(); + let agent = self.cfg.default_agent.clone(); + + let session_id = match manager::create_session(&self.db, &self.cfg, &task, &agent, true).await { + Ok(session_id) => session_id, + Err(error) => { + tracing::warn!("Failed to create new session from dashboard: {error}"); + return; + } + }; + + self.refresh(); + self.sync_selection_by_id(Some(&session_id)); + self.reset_output_view(); + self.sync_selected_output(); + self.sync_selected_diff(); + self.refresh_logs(); } pub async fn stop_selected(&mut self) { @@ -814,6 +838,31 @@ impl Dashboard { .collect() } + fn active_session_count(&self) -> usize { + self.sessions + .iter() + .filter(|session| { + matches!( + session.state, + SessionState::Pending | SessionState::Running | SessionState::Idle + ) + }) + .count() + } + + fn new_session_task(&self) -> String { + self.sessions + .get(self.selected_session) + .map(|session| { + format!( + "Follow up on {}: {}", + format_session_id(&session.id), + truncate_for_dashboard(&session.task, 96) + ) + }) + .unwrap_or_else(|| "New ECC 2.0 session".to_string()) + } + fn pane_areas(&self, area: Rect) -> PaneAreas { match self.cfg.pane_layout { PaneLayout::Horizontal => { @@ -1199,6 +1248,43 @@ mod tests { ); } + #[test] + fn new_session_task_uses_selected_session_context() { + let dashboard = test_dashboard( + vec![sample_session( + "focus-12345678", + "planner", + SessionState::Running, + Some("ecc/focus"), + 512, + 42, + )], + 0, + ); + + assert_eq!( + dashboard.new_session_task(), + "Follow up on focus-12: Render dashboard rows" + ); + } + + #[test] + fn active_session_count_only_counts_live_queue_states() { + let dashboard = test_dashboard( + vec![ + sample_session("pending-1", "planner", SessionState::Pending, None, 1, 1), + sample_session("running-1", "planner", SessionState::Running, None, 1, 1), + sample_session("idle-1", "planner", SessionState::Idle, None, 1, 1), + sample_session("failed-1", "planner", SessionState::Failed, None, 1, 1), + sample_session("stopped-1", "planner", SessionState::Stopped, None, 1, 1), + sample_session("done-1", "planner", SessionState::Completed, None, 1, 1), + ], + 0, + ); + + assert_eq!(dashboard.active_session_count(), 3); + } + #[test] fn refresh_preserves_selected_session_by_id() -> Result<()> { let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));