Skip to main content

cove_cli/commands/
kill.rs

1use std::io::{self, BufRead};
2use std::process::{Command, Stdio};
3use std::thread;
4use std::time::{Duration, Instant};
5
6use crate::colors::*;
7use crate::events;
8use crate::tmux;
9
10const GRACEFUL_TIMEOUT: Duration = Duration::from_secs(15);
11
12/// Write an "end" event for a window's Claude pane before killing it.
13/// All errors are silently swallowed — kill must never fail because of event writing.
14fn write_end_event(window_name: &str) {
15    let pane_id = match tmux::get_claude_pane_id(window_name) {
16        Ok(id) => id,
17        Err(_) => return,
18    };
19    let session_id = match events::find_session_id(&pane_id) {
20        Some(id) => id,
21        None => return,
22    };
23    let cwd = tmux::list_windows()
24        .ok()
25        .and_then(|wins| {
26            wins.into_iter()
27                .find(|w| w.name == window_name)
28                .map(|w| w.pane_path)
29        })
30        .unwrap_or_default();
31    let _ = events::write_event(&session_id, &cwd, &pane_id, "end");
32}
33
34/// Send /exit to Claude and wait for it to stop.
35fn graceful_exit(window_name: &str) -> bool {
36    // Prevent pane-died hook from respawning Claude after exit
37    let _ = tmux::disable_respawn(window_name);
38
39    // Interrupt any in-progress work, then send /exit
40    let _ = tmux::send_keys(window_name, &["C-c"]);
41    thread::sleep(Duration::from_millis(500));
42    let _ = tmux::send_keys(window_name, &["/exit", "Enter"]);
43
44    // Poll until Claude exits (pane_current_command changes from "claude")
45    let start = Instant::now();
46    while start.elapsed() < GRACEFUL_TIMEOUT {
47        thread::sleep(Duration::from_secs(1));
48        match tmux::pane_command(window_name) {
49            Ok(cmd) if cmd != "claude" => return true,
50            Err(_) => return true,
51            _ => continue,
52        }
53    }
54    false
55}
56
57/// Run the brain-os capture script for a window, returning the Claude session_id if captured.
58fn run_capture(window_name: &str) -> Option<String> {
59    let pane_id = tmux::get_claude_pane_id(window_name).ok()?;
60    let session_id = events::find_session_id(&pane_id)?;
61    let cwd = tmux::list_windows()
62        .ok()
63        .and_then(|wins| {
64            wins.into_iter()
65                .find(|w| w.name == window_name)
66                .map(|w| w.pane_path)
67        })
68        .unwrap_or_default();
69
70    let home = std::env::var("HOME").unwrap_or_default();
71    let capture_script = std::path::PathBuf::from(home).join(".claude/hooks/brain-os-capture.py");
72
73    if !capture_script.exists() {
74        return None;
75    }
76
77    let status = Command::new("python3")
78        .arg(&capture_script)
79        .arg("--session-id")
80        .arg(&session_id)
81        .arg("--cwd")
82        .arg(&cwd)
83        .stdout(Stdio::inherit())
84        .stderr(Stdio::inherit())
85        .status();
86
87    match status {
88        Ok(s) if s.success() => {
89            // Write marker to prevent SessionEnd hook from double-capturing
90            let _ = std::fs::write(format!("/tmp/cove-captured-{session_id}"), "");
91            Some(session_id)
92        }
93        _ => None,
94    }
95}
96
97pub fn run(name: &str, force: bool) -> Result<(), String> {
98    if !tmux::has_session() {
99        println!("{ANSI_OVERLAY}No active cove session.{ANSI_RESET}");
100        return Err(String::new());
101    }
102
103    write_end_event(name);
104
105    if !force {
106        run_capture(name);
107
108        println!("Press Enter to close {ANSI_PEACH}{name}{ANSI_RESET}, or Ctrl-C to cancel.");
109        let _ = io::stdin().lock().read_line(&mut String::new());
110
111        println!("Shutting down {ANSI_PEACH}{name}{ANSI_RESET} gracefully...");
112        graceful_exit(name);
113    }
114    tmux::kill_window(name)?;
115    println!("Killed: {ANSI_PEACH}{name}{ANSI_RESET}");
116    Ok(())
117}
118
119pub fn run_all(force: bool) -> Result<(), String> {
120    if !tmux::has_session() {
121        println!("{ANSI_OVERLAY}No active cove session.{ANSI_RESET}");
122        return Err(String::new());
123    }
124
125    let windows = tmux::list_windows().unwrap_or_default();
126    for win in &windows {
127        write_end_event(&win.name);
128    }
129
130    if !force {
131        // Capture learnings from all sessions before exiting
132        for win in &windows {
133            run_capture(&win.name);
134        }
135
136        println!(
137            "\nPress Enter to close {} session(s), or Ctrl-C to cancel.",
138            windows.len()
139        );
140        let _ = io::stdin().lock().read_line(&mut String::new());
141
142        println!("Shutting down {} session(s) gracefully...", windows.len());
143        for win in &windows {
144            let exited = graceful_exit(&win.name);
145            let status = if exited { "exited" } else { "timed out" };
146            println!("  {}: {status}", win.name);
147        }
148    }
149
150    tmux::kill_session()?;
151    println!("Killed all sessions.");
152    Ok(())
153}