From cd57c17d8e5b54b3766ea2058272b02edd69aa73 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 7 Apr 2026 12:15:45 -0700 Subject: [PATCH] feat: wire ecc2 session handoffs into spawn flows --- ecc2/src/main.rs | 72 +++++++++++++++++++++++++++++++++++++++ ecc2/src/tui/dashboard.rs | 33 ++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/ecc2/src/main.rs b/ecc2/src/main.rs index 89e57eeb..89f6c0f7 100644 --- a/ecc2/src/main.rs +++ b/ecc2/src/main.rs @@ -32,6 +32,9 @@ enum Commands { /// Create a dedicated worktree for this session #[arg(short, long)] worktree: bool, + /// Source session to delegate from + #[arg(long)] + from_session: Option, }, /// List active sessions Sessions, @@ -123,9 +126,14 @@ async fn main() -> Result<()> { task, agent, worktree: use_worktree, + from_session, }) => { let session_id = session::manager::create_session(&db, &cfg, &task, &agent, use_worktree).await?; + if let Some(from_session) = from_session { + let from_id = resolve_session_id(&db, &from_session)?; + send_handoff_message(&db, &from_id, &session_id)?; + } println!("Session started: {session_id}"); } Some(Commands::Sessions) => { @@ -254,6 +262,41 @@ fn short_session(session_id: &str) -> String { session_id.chars().take(8).collect() } +fn send_handoff_message( + db: &session::store::StateStore, + from_id: &str, + to_id: &str, +) -> Result<()> { + let from_session = db + .get_session(from_id)? + .ok_or_else(|| anyhow::anyhow!("Session not found: {from_id}"))?; + let context = format!( + "Delegated from {} [{}] | cwd {}{}", + short_session(&from_session.id), + from_session.agent_type, + from_session.working_dir.display(), + from_session + .worktree + .as_ref() + .map(|worktree| format!( + " | worktree {} ({})", + worktree.branch, + worktree.path.display() + )) + .unwrap_or_default() + ); + + comms::send( + db, + &from_session.id, + to_id, + &comms::MessageType::TaskHandoff { + task: from_session.task, + context, + }, + ) +} + #[cfg(test)] mod tests { use super::*; @@ -305,4 +348,33 @@ mod tests { _ => panic!("expected messages send subcommand"), } } + + #[test] + fn cli_parses_start_with_handoff_source() { + let cli = Cli::try_parse_from([ + "ecc", + "start", + "--task", + "Follow up", + "--agent", + "claude", + "--from-session", + "planner", + ]) + .expect("start with handoff source should parse"); + + match cli.command { + Some(Commands::Start { + from_session, + task, + agent, + .. + }) => { + assert_eq!(task, "Follow up"); + assert_eq!(agent, "claude"); + assert_eq!(from_session.as_deref(), Some("planner")); + } + _ => panic!("expected start subcommand"), + } + } } diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index 6afcea4b..c7b97e50 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -545,6 +545,39 @@ impl Dashboard { } }; + if let Some(source_session) = self.sessions.get(self.selected_session) { + let context = format!( + "Dashboard handoff from {} [{}] | cwd {}{}", + format_session_id(&source_session.id), + source_session.agent_type, + source_session.working_dir.display(), + source_session + .worktree + .as_ref() + .map(|worktree| format!( + " | worktree {} ({})", + worktree.branch, + worktree.path.display() + )) + .unwrap_or_default() + ); + if let Err(error) = comms::send( + &self.db, + &source_session.id, + &session_id, + &comms::MessageType::TaskHandoff { + task: source_session.task.clone(), + context, + }, + ) { + tracing::warn!( + "Failed to send handoff from session {} to {}: {error}", + source_session.id, + session_id + ); + } + } + self.refresh(); self.sync_selection_by_id(Some(&session_id)); self.reset_output_view();