//! Local sessions (~/.local/share/codecraft/sessions/.json). use anyhow::Result; use codecraft_core::types::{Message, Part, Usage, new_id, now_ms}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocalSession { pub id: String, pub title: String, pub cwd: String, pub model: String, pub mode: String, pub permission_mode: String, pub created_at: i64, pub updated_at: i64, pub messages: Vec, #[serde(default)] pub usage: Usage, #[serde(default)] pub cost: f64, #[serde(default)] pub todos: Value, /// Tokens of the last request (context fill). #[serde(default)] pub context_tokens: u64, } fn dir() -> PathBuf { crate::conf::data_dir().join("sessions") } impl LocalSession { pub fn new(cwd: &str, model: &str, mode: &str, permission_mode: &str) -> Self { let now = now_ms(); Self { id: new_id(), title: String::new(), cwd: cwd.into(), model: model.into(), mode: mode.into(), permission_mode: permission_mode.into(), created_at: now, updated_at: now, messages: vec![], usage: Usage::default(), cost: 0.0, todos: Value::Null, context_tokens: 0, } } pub fn save(&mut self) -> Result<()> { if self.messages.is_empty() { return Ok(()); } self.updated_at = now_ms(); std::fs::create_dir_all(dir())?; let tmp = dir().join(format!("{}.json.tmp", self.id)); std::fs::write(&tmp, serde_json::to_vec(self)?)?; std::fs::rename(tmp, dir().join(format!("{}.json", self.id)))?; Ok(()) } pub fn load(id: &str) -> Result { Ok(serde_json::from_slice(&std::fs::read(dir().join(format!("{id}.json")))?)?) } /// Messages the model sees: everything from the last summary on. pub fn active(&self) -> Vec { let start = self.messages.iter().rposition(|m| m.parts.iter().any(|p| matches!(p, Part::Summary { .. }))).unwrap_or(0); self.messages[start..].to_vec() } } /// Sessions (newest first), optionally only those of one folder. pub fn list(cwd: Option<&str>) -> Vec { let mut out: Vec = std::fs::read_dir(dir()) .into_iter() .flatten() .flatten() .filter(|e| e.path().extension().is_some_and(|x| x == "json")) .filter_map(|e| serde_json::from_slice::(&std::fs::read(e.path()).ok()?).ok()) .filter(|s| cwd.is_none_or(|c| s.cwd == c)) .collect(); out.sort_by_key(|s| std::cmp::Reverse(s.updated_at)); out }