Skip to main content

cove_cli/
events.rs

1// ── Shared event helpers ──
2//
3// Functions for reading and writing Cove session event files.
4// Used by hook handler (writing), sidebar (reading), and kill (writing end events).
5
6use std::fs::{self, OpenOptions};
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use crate::paths;
11
12/// Path to the Cove events directory (XDG_STATE_HOME/cove/events/).
13pub fn events_dir() -> PathBuf {
14    paths::events_dir()
15}
16
17/// Append a state event to the session's event file.
18pub fn write_event(session_id: &str, cwd: &str, pane_id: &str, state: &str) -> Result<(), String> {
19    write_event_to(&events_dir(), session_id, cwd, pane_id, state)
20}
21
22/// Append a state event to a session file in the given directory.
23pub fn write_event_to(
24    dir: &Path,
25    session_id: &str,
26    cwd: &str,
27    pane_id: &str,
28    state: &str,
29) -> Result<(), String> {
30    fs::create_dir_all(dir).map_err(|e| format!("create events dir: {e}"))?;
31
32    let path = dir.join(format!("{session_id}.jsonl"));
33    let mut file = OpenOptions::new()
34        .create(true)
35        .append(true)
36        .open(&path)
37        .map_err(|e| format!("open event file: {e}"))?;
38
39    let ts = std::time::SystemTime::now()
40        .duration_since(std::time::UNIX_EPOCH)
41        .unwrap_or_default()
42        .as_secs();
43
44    let line = format!(r#"{{"state":"{state}","cwd":"{cwd}","pane_id":"{pane_id}","ts":{ts}}}"#);
45    writeln!(file, "{line}").map_err(|e| format!("write event: {e}"))?;
46
47    Ok(())
48}
49
50/// Find the Claude session_id for a given tmux pane_id by scanning cove event files.
51/// Returns the session_id from the file whose last event matches the pane_id
52/// with the highest timestamp (handles pane_id recycling).
53pub fn find_session_id(pane_id: &str) -> Option<String> {
54    find_session_id_in(&events_dir(), pane_id)
55}
56
57/// Find the Claude session_id for a given pane_id by scanning event files in the given directory.
58pub fn find_session_id_in(dir: &Path, pane_id: &str) -> Option<String> {
59    let entries = fs::read_dir(dir).ok()?;
60
61    let mut best: Option<(String, u64)> = None;
62    for entry in entries.flatten() {
63        let path = entry.path();
64        if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
65            continue;
66        }
67        let content = fs::read_to_string(&path).unwrap_or_default();
68        let last_line = content.lines().rev().find(|l| !l.trim().is_empty());
69        if let Some(line) = last_line {
70            if let Ok(event) = serde_json::from_str::<serde_json::Value>(line) {
71                if event.get("pane_id").and_then(|v| v.as_str()) == Some(pane_id) {
72                    let ts = event.get("ts").and_then(|v| v.as_u64()).unwrap_or(0);
73                    if best.as_ref().is_none_or(|(_, prev_ts)| ts > *prev_ts) {
74                        if let Some(sid) = path.file_stem().and_then(|s| s.to_str()) {
75                            best = Some((sid.to_string(), ts));
76                        }
77                    }
78                }
79            }
80        }
81    }
82
83    best.map(|(sid, _)| sid)
84}