aico/commands/
session_fork.rs1use crate::exceptions::AicoError;
2use crate::fs::atomic_write_json;
3use crate::models::SessionPointer;
4use crate::session::Session;
5use std::fs;
6use std::io::Write;
7use std::path::Path;
8use std::process::Command;
9use tempfile::NamedTempFile;
10
11pub fn run(
12 new_name: String,
13 until_pair: Option<usize>,
14 ephemeral: bool,
15 exec_args: Vec<String>,
16) -> Result<(), AicoError> {
17 let session = Session::load_active()?;
18
19 if new_name.trim().is_empty() {
20 return Err(AicoError::InvalidInput(
21 "New session name is required.".into(),
22 ));
23 }
24
25 let new_view_path = session.get_view_path(&new_name);
26 if new_view_path.exists() {
27 return Err(AicoError::InvalidInput(format!(
28 "A session view named '{}' already exists.",
29 new_name
30 )));
31 }
32
33 let mut new_view = session.view.clone();
35 if let Some(limit) = until_pair {
36 let total_pairs = new_view.message_indices.len() / 2;
37 if limit >= total_pairs {
38 return Err(AicoError::InvalidInput(format!(
39 "Pair index {} is out of bounds. Session only has {} pairs.",
40 limit, total_pairs
41 )));
42 }
43
44 let truncation_len = (limit + 1) * 2;
45 new_view.message_indices.truncate(truncation_len);
46
47 if new_view.history_start_pair > limit {
49 new_view.history_start_pair = limit + 1;
50 }
51 new_view.excluded_pairs.retain(|&idx| idx <= limit);
52 }
53
54 atomic_write_json(&new_view_path, &new_view)?;
55
56 if exec_args.is_empty() {
57 session.switch_to_view(&new_view_path)?;
59 let truncated = until_pair
60 .map(|u| format!(" (truncated at pair {})", u))
61 .unwrap_or_default();
62 println!(
63 "Forked new session '{}'{} and switched to it.",
64 new_name, truncated
65 );
66 } else {
67 let mut temp_ptr = NamedTempFile::new_in(&session.root)?;
69 let ptr_path = temp_ptr.path().to_path_buf();
70
71 let rel_view_path = Path::new(".aico")
72 .join("sessions")
73 .join(format!("{}.json", new_name));
74 let pointer = SessionPointer {
75 pointer_type: "aico_session_pointer_v1".to_string(),
76 path: rel_view_path.to_string_lossy().replace('\\', "/"),
77 };
78 serde_json::to_writer(&mut temp_ptr, &pointer)?;
79 temp_ptr.flush()?;
80
81 let mut cmd = Command::new(&exec_args[0]);
82 cmd.args(&exec_args[1..]);
83 cmd.env("AICO_SESSION_FILE", &ptr_path);
84
85 let status = cmd.status().map_err(AicoError::Io)?;
86
87 if ephemeral {
88 let _ = fs::remove_file(&new_view_path);
89 }
90
91 if !status.success() {
92 std::process::exit(status.code().unwrap_or(1));
93 }
94 }
95
96 Ok(())
97}