use crate::error::{Autom8Error, Result};
use crate::output::{print_header, print_sessions_status, BOLD, CYAN, GRAY, GREEN, RESET, YELLOW};
use crate::prompt;
use crate::state::{RunStatus, SessionStatus, StateManager};
use crate::worktree::is_in_worktree;
use crate::Runner;
use super::ensure_project_dir;
pub fn resume_command(session: Option<&str>, list: bool) -> Result<()> {
ensure_project_dir()?;
print_header();
let state_manager = StateManager::new()?;
if list {
return list_resumable_sessions(&state_manager);
}
if let Some(session_id) = session {
return resume_specific_session(&state_manager, session_id);
}
resume_auto_detect(&state_manager)
}
fn list_resumable_sessions(state_manager: &StateManager) -> Result<()> {
let sessions = state_manager.list_sessions_with_status()?;
let resumable: Vec<SessionStatus> = sessions.into_iter().filter(is_resumable_session).collect();
if resumable.is_empty() {
println!("{GRAY}No resumable sessions found.{RESET}");
println!();
println!("A session is resumable when it has an incomplete run (running or failed state).");
return Ok(());
}
println!("{BOLD}Resumable sessions:{RESET}");
println!();
print_sessions_status(&resumable);
println!();
println!(
"{GRAY}Use {CYAN}autom8 resume --session <id>{GRAY} to resume a specific session.{RESET}"
);
Ok(())
}
fn resume_specific_session(state_manager: &StateManager, session_id: &str) -> Result<()> {
let session_sm = state_manager
.get_session(session_id)
.ok_or_else(|| Autom8Error::StateError(format!("Session '{}' not found", session_id)))?;
let metadata = session_sm.load_metadata()?.ok_or_else(|| {
Autom8Error::StateError(format!("Session '{}' has no metadata", session_id))
})?;
if !metadata.worktree_path.exists() {
return Err(Autom8Error::StateError(format!(
"Session '{}' worktree was deleted: {}",
session_id,
metadata.worktree_path.display()
)));
}
let state = session_sm.load_current()?;
if state.is_none() {
return Err(Autom8Error::StateError(format!(
"Session '{}' has no active run",
session_id
)));
}
let state = state.unwrap();
if state.status != RunStatus::Running
&& state.status != RunStatus::Failed
&& state.status != RunStatus::Interrupted
{
return Err(Autom8Error::StateError(format!(
"Session '{}' has no resumable run (status: {:?})",
session_id, state.status
)));
}
let current_dir = std::env::current_dir()?;
if current_dir != metadata.worktree_path {
println!(
"{CYAN}Changing to worktree:{RESET} {}",
metadata.worktree_path.display()
);
std::env::set_current_dir(&metadata.worktree_path)?;
}
println!(
"{YELLOW}[resume]{RESET} Resuming session {BOLD}{}{RESET} on branch {}",
session_id, metadata.branch_name
);
println!();
let runner = Runner::new()?;
runner.resume()
}
fn resume_auto_detect(state_manager: &StateManager) -> Result<()> {
let sessions = state_manager.list_sessions_with_status()?;
let current_session_id = state_manager.session_id();
let in_worktree = is_in_worktree().unwrap_or(false);
let resumable: Vec<&SessionStatus> = sessions
.iter()
.filter(|&s| is_resumable_session(s))
.collect();
if resumable.is_empty() {
println!(
"{YELLOW}[resume]{RESET} No active sessions found, scanning for incomplete specs..."
);
println!();
let runner = Runner::new()?;
return runner.resume();
}
if in_worktree {
if let Some(current) = resumable.iter().find(|s| s.is_current) {
println!(
"{GREEN}[resume]{RESET} Resuming current worktree session: {}",
current.metadata.session_id
);
println!();
let runner = Runner::new()?;
return runner.resume();
}
}
if let Some(current) = resumable
.iter()
.find(|s| s.metadata.session_id == current_session_id)
{
if current.is_current {
println!(
"{GREEN}[resume]{RESET} Resuming current session: {}",
current.metadata.session_id
);
println!();
let runner = Runner::new()?;
return runner.resume();
}
}
if resumable.len() == 1 {
let session = resumable[0];
return resume_session_with_change(&session.metadata);
}
println!("{BOLD}Multiple resumable sessions found:{RESET}");
println!();
let options: Vec<String> = resumable
.iter()
.map(|s| {
let current_marker = if s.is_current { " (current)" } else { "" };
let stale_marker = if s.is_stale { " [stale]" } else { "" };
let state_str = s
.machine_state
.map(|st| format!(" - {:?}", st))
.unwrap_or_default();
format!(
"{}{}{} [{}]{}",
s.metadata.session_id,
current_marker,
stale_marker,
s.metadata.branch_name,
state_str
)
})
.chain(std::iter::once("Exit".to_string()))
.collect();
let option_refs: Vec<&str> = options.iter().map(|s| s.as_str()).collect();
let choice = prompt::select("Which session would you like to resume?", &option_refs, 0);
if choice >= resumable.len() {
println!();
println!("Exiting.");
return Ok(());
}
let selected = resumable[choice];
if selected.is_stale {
return Err(Autom8Error::StateError(format!(
"Session '{}' worktree was deleted: {}",
selected.metadata.session_id,
selected.metadata.worktree_path.display()
)));
}
resume_session_with_change(&selected.metadata)
}
fn resume_session_with_change(metadata: &crate::state::SessionMetadata) -> Result<()> {
if !metadata.worktree_path.exists() {
return Err(Autom8Error::StateError(format!(
"Session '{}' worktree was deleted: {}",
metadata.session_id,
metadata.worktree_path.display()
)));
}
let current_dir = std::env::current_dir()?;
if current_dir != metadata.worktree_path {
println!(
"{CYAN}Changing to worktree:{RESET} {}",
metadata.worktree_path.display()
);
std::env::set_current_dir(&metadata.worktree_path)?;
}
println!(
"{YELLOW}[resume]{RESET} Resuming session {BOLD}{}{RESET} on branch {}",
metadata.session_id, metadata.branch_name
);
println!();
let runner = Runner::new()?;
runner.resume()
}
fn is_resumable_session(session: &SessionStatus) -> bool {
if session.is_stale {
return false; }
if session.metadata.is_running {
return true;
}
if let Some(state) = &session.machine_state {
match state {
crate::state::MachineState::Completed => false,
crate::state::MachineState::Idle => false,
_ => true, }
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{MachineState, SessionMetadata, SessionStatus};
use chrono::Utc;
use std::path::PathBuf;
fn make_session(
session_id: &str,
is_running: bool,
machine_state: Option<MachineState>,
is_stale: bool,
) -> SessionStatus {
SessionStatus {
metadata: SessionMetadata {
session_id: session_id.to_string(),
worktree_path: PathBuf::from("/tmp/test"),
branch_name: "feature/test".to_string(),
created_at: Utc::now(),
last_active_at: Utc::now(),
is_running,
spec_json_path: None,
},
machine_state,
current_story: None,
is_current: false,
is_stale,
}
}
#[test]
fn test_us010_is_resumable_running_session() {
let session = make_session("test", true, Some(MachineState::RunningClaude), false);
assert!(is_resumable_session(&session));
}
#[test]
fn test_us010_is_resumable_failed_session() {
let session = make_session("test", false, Some(MachineState::Failed), false);
assert!(is_resumable_session(&session));
}
#[test]
fn test_us010_is_not_resumable_completed_session() {
let session = make_session("test", false, Some(MachineState::Completed), false);
assert!(!is_resumable_session(&session));
}
#[test]
fn test_us010_is_not_resumable_stale_session() {
let session = make_session("test", true, Some(MachineState::RunningClaude), true);
assert!(!is_resumable_session(&session));
}
#[test]
fn test_us010_is_not_resumable_idle_session() {
let session = make_session("test", false, Some(MachineState::Idle), false);
assert!(!is_resumable_session(&session));
}
#[test]
fn test_us010_is_resumable_reviewing_session() {
let session = make_session("test", false, Some(MachineState::Reviewing), false);
assert!(is_resumable_session(&session));
}
#[test]
fn test_us010_is_not_resumable_no_state() {
let session = make_session("test", false, None, false);
assert!(!is_resumable_session(&session));
}
}