use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionStatus {
Busy,
Idle,
Waiting,
}
#[derive(Debug, Clone)]
pub struct SessionMeta {
pub status: SessionStatus,
pub session_id: String,
pub updated_at_ms: i64,
}
#[derive(serde::Deserialize)]
struct Raw {
#[serde(default)]
status: String,
#[serde(default, rename = "sessionId")]
session_id: String,
#[serde(default, rename = "updatedAt")]
updated_at: i64,
}
pub fn session_path(pid: u32) -> Option<PathBuf> {
Some(dirs::home_dir()?.join(format!(".claude/sessions/{pid}.json")))
}
pub fn read(pid: u32) -> Option<SessionMeta> {
let path = session_path(pid)?;
let bytes = fs::read(&path).ok()?;
parse(&bytes)
}
fn parse(bytes: &[u8]) -> Option<SessionMeta> {
let raw: Raw = serde_json::from_slice(bytes).ok()?;
let status = parse_status(&raw.status)?;
Some(SessionMeta {
status,
session_id: raw.session_id,
updated_at_ms: raw.updated_at,
})
}
fn parse_status(s: &str) -> Option<SessionStatus> {
match s {
"busy" => Some(SessionStatus::Busy),
"idle" => Some(SessionStatus::Idle),
"waiting" => Some(SessionStatus::Waiting),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_known_busy() {
let json =
br#"{"pid":1,"sessionId":"s-1","cwd":"/x","status":"busy","updatedAt":1700000000000}"#;
let m = parse(json).expect("parses");
assert_eq!(m.status, SessionStatus::Busy);
assert_eq!(m.session_id, "s-1");
assert_eq!(m.updated_at_ms, 1700000000000);
}
#[test]
fn parse_known_idle() {
let json = br#"{"status":"idle","sessionId":"sid"}"#;
let m = parse(json).expect("parses");
assert_eq!(m.status, SessionStatus::Idle);
}
#[test]
fn parse_known_waiting() {
let json = br#"{"status":"waiting"}"#;
let m = parse(json).expect("parses");
assert_eq!(m.status, SessionStatus::Waiting);
}
#[test]
fn parse_unknown_status_returns_none() {
let json = br#"{"status":"thinking-hard"}"#;
assert!(parse(json).is_none());
}
#[test]
fn parse_malformed_json_returns_none() {
assert!(parse(b"not json").is_none());
assert!(parse(b"").is_none());
assert!(parse(br#"{"status":"bu"#).is_none());
}
#[test]
fn parse_missing_status_field_returns_none() {
let json = br#"{"sessionId":"s-1"}"#;
assert!(parse(json).is_none());
}
#[test]
fn parse_tolerates_extra_fields() {
let json = br#"{
"pid": 22650,
"sessionId": "31e571fa-2230-4854-908a-5d13e7a017b6",
"cwd": "/home/x/dev/tmx",
"startedAt": 1779149382456,
"procStart": "126214",
"version": "2.1.143",
"peerProtocol": 1,
"kind": "interactive",
"entrypoint": "cli",
"status": "busy",
"updatedAt": 1779165593130
}"#;
let m = parse(json).expect("parses real-shape file");
assert_eq!(m.status, SessionStatus::Busy);
assert_eq!(m.session_id, "31e571fa-2230-4854-908a-5d13e7a017b6");
assert_eq!(m.updated_at_ms, 1779165593130);
}
#[test]
fn session_path_uses_pid_filename() {
let p = session_path(42).expect("home dir present in test env");
assert!(p.ends_with(".claude/sessions/42.json"));
}
}