From 1ec6b5684862aa9d98fe5ae41c89bcc3b0b3e3ba Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 7 Apr 2026 11:34:46 -0700 Subject: [PATCH] feat: wire real stop and resume controls into ecc2 tui --- ecc2/src/tui/app.rs | 3 +- ecc2/src/tui/dashboard.rs | 108 +++++++++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/ecc2/src/tui/app.rs b/ecc2/src/tui/app.rs index ae8142eb..7155b43a 100644 --- a/ecc2/src/tui/app.rs +++ b/ecc2/src/tui/app.rs @@ -39,7 +39,8 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> { (_, KeyCode::Char('j')) | (_, KeyCode::Down) => dashboard.scroll_down(), (_, KeyCode::Char('k')) | (_, KeyCode::Up) => dashboard.scroll_up(), (_, KeyCode::Char('n')) => dashboard.new_session(), - (_, KeyCode::Char('s')) => dashboard.stop_selected(), + (_, KeyCode::Char('s')) => dashboard.stop_selected().await, + (_, KeyCode::Char('u')) => dashboard.resume_selected().await, (_, KeyCode::Char('r')) => dashboard.refresh(), (_, KeyCode::Char('?')) => dashboard.toggle_help(), _ => {} diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index a86f052f..8208adf9 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::path::{Path, PathBuf}; +use std::path::Path; use ratatui::{ prelude::*, @@ -14,6 +14,7 @@ use crate::config::{Config, PaneLayout}; use crate::observability::ToolLogEntry; use crate::session::output::{OutputEvent, OutputLine, SessionOutputStore, OutputStream, OUTPUT_BUFFER_LIMIT}; use crate::session::store::StateStore; +use crate::session::manager; use crate::session::{Session, SessionMetrics, SessionState, WorktreeInfo}; const DEFAULT_PANE_SIZE_PERCENT: u16 = 35; @@ -341,7 +342,7 @@ impl Dashboard { fn render_status_bar(&self, frame: &mut Frame, area: Rect) { let text = format!( - " [n]ew session [s]top [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ", + " [n]ew session [s]top [u]resume [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ", self.layout_label() ); let aggregate = self.aggregate_usage(); @@ -382,6 +383,7 @@ impl Dashboard { "", " n New session", " s Stop selected session", + " u Resume selected session", " Tab Next pane", " S-Tab Previous pane", " j/↓ Scroll down", @@ -496,17 +498,30 @@ impl Dashboard { tracing::info!("New session dialog requested"); } - pub fn stop_selected(&mut self) { - if let Some(session) = self.sessions.get(self.selected_session) { - if let Err(error) = - self.db - .update_state_and_pid(&session.id, &SessionState::Stopped, None) - { - tracing::warn!("Failed to stop session {}: {error}", session.id); - return; - } - self.refresh(); + pub async fn stop_selected(&mut self) { + let Some(session) = self.sessions.get(self.selected_session) else { + return; + }; + + if let Err(error) = manager::stop_session(&self.db, &session.id).await { + tracing::warn!("Failed to stop session {}: {error}", session.id); + return; } + + self.refresh(); + } + + pub async fn resume_selected(&mut self) { + let Some(session) = self.sessions.get(self.selected_session) else { + return; + }; + + if let Err(error) = manager::resume_session(&self.db, &session.id).await { + tracing::warn!("Failed to resume session {}: {error}", session.id); + return; + } + + self.refresh(); } pub fn refresh(&mut self) { @@ -954,6 +969,7 @@ mod tests { use anyhow::Result; use chrono::Utc; use ratatui::{backend::TestBackend, Terminal}; + use std::path::PathBuf; use uuid::Uuid; use super::*; @@ -1123,6 +1139,74 @@ mod tests { Ok(()) } + #[tokio::test] + async fn stop_selected_uses_session_manager_transition() -> Result<()> { + let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4())); + let db = StateStore::open(&db_path)?; + let now = Utc::now(); + + db.insert_session(&Session { + id: "running-1".to_string(), + task: "stop me".to_string(), + agent_type: "claude".to_string(), + state: SessionState::Running, + pid: Some(999_999), + worktree: None, + created_at: now, + updated_at: now, + metrics: SessionMetrics::default(), + })?; + + let dashboard_store = StateStore::open(&db_path)?; + let mut dashboard = Dashboard::new(dashboard_store, Config::default()); + dashboard.stop_selected().await; + + let session = db + .get_session("running-1")? + .expect("session should exist after stop"); + assert_eq!(session.state, SessionState::Stopped); + assert_eq!(session.pid, None); + + let _ = std::fs::remove_file(db_path); + Ok(()) + } + + #[tokio::test] + async fn resume_selected_requeues_failed_session() -> Result<()> { + let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4())); + let db = StateStore::open(&db_path)?; + let now = Utc::now(); + + db.insert_session(&Session { + id: "failed-1".to_string(), + task: "resume me".to_string(), + agent_type: "claude".to_string(), + state: SessionState::Failed, + pid: None, + worktree: Some(WorktreeInfo { + path: PathBuf::from("/tmp/ecc2-resume"), + branch: "ecc/failed-1".to_string(), + base_branch: "main".to_string(), + }), + created_at: now, + updated_at: now, + metrics: SessionMetrics::default(), + })?; + + let dashboard_store = StateStore::open(&db_path)?; + let mut dashboard = Dashboard::new(dashboard_store, Config::default()); + dashboard.resume_selected().await; + + let session = db + .get_session("failed-1")? + .expect("session should exist after resume"); + assert_eq!(session.state, SessionState::Pending); + assert_eq!(session.pid, None); + + let _ = std::fs::remove_file(db_path); + Ok(()) + } + #[test] fn grid_layout_renders_four_panes() { let mut dashboard = test_dashboard(vec![sample_session("grid-1", "claude", SessionState::Running, None, 1, 1)], 0);