use crate::console::CliConsole;
use sage_core::agent::UnifiedExecutor;
use sage_core::config::Config;
use sage_core::error::{SageError, SageResult};
use sage_core::session::JsonlSessionStorage;
use sage_core::trajectory::SessionRecorder;
use sage_core::types::TaskMetadata;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use super::args::UnifiedArgs;
use super::input::handle_user_input;
use super::outcome::display_outcome;
use super::slash_commands::{SlashCommandAction, process_slash_command};
use sage_core::input::InputChannel;
pub async fn execute_single_task(
executor: &mut UnifiedExecutor,
console: &CliConsole,
working_dir: &std::path::Path,
_jsonl_storage: &Arc<JsonlSessionStorage>,
session_recorder: &Option<Arc<Mutex<SessionRecorder>>>,
task_description: &str,
config_file: &str,
) -> SageResult<()> {
let action = process_slash_command(task_description, console, working_dir).await?;
let task_description = match action {
SlashCommandAction::Prompt(desc) => desc,
SlashCommandAction::Handled => return Ok(()),
SlashCommandAction::HandledWithOutput(output) => {
println!("{}", output);
return Ok(());
}
SlashCommandAction::SetOutputMode(mode) => {
executor.set_output_mode(mode);
console.info("Output mode updated.");
return Ok(());
}
SlashCommandAction::Resume { session_id } => {
let resume_result: SageResult<String> = if let Some(id) = session_id {
executor
.restore_session(&id)
.await
.map(|msgs| format!("Session {} restored ({} messages)", id, msgs.len()))
} else {
match executor.get_most_recent_session().await? {
Some(metadata) => {
let id = metadata.id;
let msgs = executor.restore_session(&id).await?;
Ok(format!("Session {} restored ({} messages)", id, msgs.len()))
}
None => Err(SageError::config(
"No previous sessions found. Start a new session first.",
)),
}
};
match resume_result {
Ok(msg) => console.success(&msg),
Err(e) => console.error(&format!("Resume failed: {}", e)),
}
return Ok(());
}
SlashCommandAction::SwitchModel { model } => {
match executor.switch_model(&model) {
Ok(_) => console.success(&format!("Switched to model: {}", model)),
Err(e) => console.error(&format!("Failed to switch model: {}", e)),
}
return Ok(());
}
SlashCommandAction::ModelSelect { models } => {
let mut output = "Available models:\n".to_string();
for model in &models {
output.push_str(&format!(" - {}\n", model));
}
output.push_str("\nUse /model <name> to switch.");
println!("{}", output);
return Ok(());
}
SlashCommandAction::Doctor => {
crate::commands::diagnostics::doctor(config_file).await?;
return Ok(());
}
SlashCommandAction::Exit => {
console.info("Exiting...");
return Ok(());
}
};
let task = TaskMetadata::new(&task_description, &working_dir.display().to_string());
let start_time = std::time::Instant::now();
let outcome = executor.execute(task).await?;
let duration = start_time.elapsed();
console.print_separator();
let session_path = if let Some(recorder) = session_recorder {
Some(recorder.lock().await.file_path().to_path_buf())
} else {
None
};
display_outcome(console, &outcome, duration, session_path.as_ref());
Ok(())
}
pub async fn execute_session_resume(
args: UnifiedArgs,
mut executor: UnifiedExecutor,
console: CliConsole,
config: Config,
working_dir: PathBuf,
config_file: String,
) -> SageResult<()> {
struct InputTaskGuard(Option<JoinHandle<()>>);
impl Drop for InputTaskGuard {
fn drop(&mut self) {
if let Some(handle) = self.0.take() {
handle.abort();
}
}
}
let mut input_task = InputTaskGuard(None);
let session_id = if let Some(id) = args.resume_session_id {
id
} else {
match executor.get_most_recent_session().await? {
Some(metadata) => {
console.info(&format!(
"Resuming most recent session: {} ({})",
metadata.id,
metadata.resume_title()
));
metadata.id
}
None => {
return Err(SageError::config(
"No previous sessions found in this directory. Start a new session first.",
));
}
}
};
console.print_header("Session Resume");
console.info(&format!("Resuming session: {}", session_id));
let _restored_messages = executor.restore_session(&session_id).await?;
console.success("Session restored successfully");
let session_recorder = if config.trajectory.is_enabled() {
let recorder = sage_core::trajectory::init_session_recorder(&working_dir);
if let Some(ref r) = recorder {
executor.set_session_recorder(r.clone());
}
recorder
} else {
None
};
console.info(&format!("Provider: {}", config.get_default_provider()));
let max_steps_display = match executor.options().max_steps {
Some(n) => n.to_string(),
None => "unlimited".to_string(),
};
console.info(&format!("Max Steps: {}", max_steps_display));
console.print_separator();
console.info("Enter your next message to continue the conversation (press Enter to submit):");
print!("> ");
let _ = std::io::Write::flush(&mut std::io::stdout());
let next_message = tokio::task::spawn_blocking(|| {
let mut input = String::new();
match std::io::stdin().read_line(&mut input) {
Ok(0) => String::new(), Ok(_) => input.trim().to_string(),
Err(_) => String::new(),
}
})
.await
.unwrap_or_default();
if next_message.is_empty() {
console.info("No input provided. Session ready for future continuation.");
return Ok(());
}
let verbose = args.verbose;
if !args.non_interactive {
let (input_channel, input_handle) = InputChannel::new(16);
executor.set_input_channel(input_channel);
input_task.0 = Some(tokio::spawn(handle_user_input(input_handle, verbose)));
}
let working_dir_path = std::path::Path::new(&working_dir);
let action = process_slash_command(&next_message, &console, working_dir_path).await?;
let task_description = match action {
SlashCommandAction::Prompt(desc) => desc,
SlashCommandAction::Handled => return Ok(()),
SlashCommandAction::HandledWithOutput(output) => {
println!("{}", output);
return Ok(());
}
SlashCommandAction::SetOutputMode(mode) => {
executor.set_output_mode(mode);
console.info("Output mode updated.");
return Ok(());
}
SlashCommandAction::Resume { session_id } => {
let resume_result = if let Some(id) = session_id {
executor
.restore_session(&id)
.await
.map(|_| format!("Session {} restored", id))
} else {
match executor.get_most_recent_session().await? {
Some(metadata) => {
let id = metadata.id;
executor.restore_session(&id).await?;
Ok(format!("Session {} restored", id))
}
None => Err(SageError::config(
"No previous sessions found. Start a new session first.",
)),
}
};
match resume_result {
Ok(msg) => console.success(&msg),
Err(e) => console.error(&format!("Resume failed: {}", e)),
}
return Ok(());
}
SlashCommandAction::SwitchModel { model } => {
match executor.switch_model(&model) {
Ok(_) => console.success(&format!("Switched to model: {}", model)),
Err(e) => console.error(&format!("Failed to switch model: {}", e)),
}
return Ok(());
}
SlashCommandAction::ModelSelect { models } => {
let mut output = "Available models:\n".to_string();
for model in &models {
output.push_str(&format!(" - {}\n", model));
}
output.push_str("\nUse /model <name> to switch.");
println!("{}", output);
return Ok(());
}
SlashCommandAction::Doctor => {
crate::commands::diagnostics::doctor(&config_file).await?;
return Ok(());
}
SlashCommandAction::Exit => {
console.info("Exiting...");
return Ok(());
}
};
let task = TaskMetadata::new(&task_description, &working_dir.display().to_string());
let start_time = std::time::Instant::now();
let outcome = executor.execute(task).await?;
let duration = start_time.elapsed();
console.print_separator();
let session_path = if let Some(recorder) = &session_recorder {
Some(recorder.lock().await.file_path().to_path_buf())
} else {
None
};
display_outcome(&console, &outcome, duration, session_path.as_ref());
Ok(())
}