aico/commands/
session_fork.rs

1use 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    // 1. Create Forked View
34    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        // Truncate other metadata to match
48        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        // Standard fork: update pointer
58        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        // Execute in fork: use temp pointer
68        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}