use crate::SessionError;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub working_directory: Option<String>,
}
impl SessionMetadata {
pub(crate) fn is_empty(&self) -> bool {
self.provider.is_none()
&& self.model.is_none()
&& self.token_count.is_none()
&& self.working_directory.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionEntry {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
pub timestamp: DateTime<Utc>,
pub role: String,
pub content: String,
#[serde(default, skip_serializing_if = "SessionMetadata::is_empty")]
pub metadata: SessionMetadata,
}
#[derive(Debug, Clone)]
pub struct SessionBranch {
pub root_id: String,
pub entries: Vec<SessionEntry>,
}
#[derive(Debug, Clone)]
pub struct SessionInfo {
pub id: Uuid,
pub project: String,
pub workspace_root: String,
pub last_message_preview: String,
pub timestamp: DateTime<Utc>,
pub message_count: usize,
}
#[derive(Debug, Clone)]
pub struct Session {
pub id: Uuid,
pub project: String,
pub workspace_root: String,
pub created_at: DateTime<Utc>,
pub file_path: PathBuf,
pub current_branch: String,
pub branches: HashMap<String, SessionBranch>,
pub persisted: bool,
pub(crate) last_entry_id: Arc<Mutex<Option<String>>>,
pub(crate) write_lock: Arc<Mutex<()>>,
}
impl Session {
pub fn new(id: Uuid, project: String, workspace_root: String, file_path: PathBuf) -> Self {
let root_id = Uuid::new_v4().to_string();
let mut branches = HashMap::new();
branches.insert(
root_id.clone(),
SessionBranch {
root_id: root_id.clone(),
entries: Vec::new(),
},
);
Self {
id,
project,
workspace_root,
created_at: Utc::now(),
file_path,
current_branch: root_id,
branches,
persisted: true,
last_entry_id: Arc::new(Mutex::new(None)),
write_lock: Arc::new(Mutex::new(())),
}
}
pub fn new_deferred(
id: Uuid,
project: String,
workspace_root: String,
file_path: PathBuf,
) -> Self {
let mut session = Self::new(id, project, workspace_root, file_path);
session.persisted = false;
session
}
pub fn ensure_persisted(&mut self) -> Result<(), SessionError> {
if self.persisted {
return Ok(());
}
if let Some(parent) = self.file_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::File::create(&self.file_path)?;
self.persisted = true;
Ok(())
}
pub fn fork(&mut self, from_entry_id: &str) -> Result<String, SessionError> {
let all_entries = self.read_entries()?;
let pos = all_entries
.iter()
.position(|e| e.id == from_entry_id)
.ok_or_else(|| SessionError::EntryNotFound(from_entry_id.to_string()))?;
let entries_up_to_fork: Vec<SessionEntry> = all_entries[..=pos].to_vec();
let new_branch_id = Uuid::new_v4().to_string();
let new_branch = SessionBranch {
root_id: from_entry_id.to_string(),
entries: entries_up_to_fork,
};
self.branches.insert(new_branch_id.clone(), new_branch);
self.current_branch = new_branch_id.clone();
Ok(new_branch_id)
}
pub fn with_fork_identity(&mut self, new_id: Uuid, new_file_path: PathBuf, branch_id: String) {
self.id = new_id;
self.file_path = new_file_path;
self.current_branch = branch_id;
}
pub fn get_branch(&self, branch_id: &str) -> Option<&SessionBranch> {
self.branches.get(branch_id)
}
pub fn snapshot_bytes(&self) -> Result<Vec<u8>, SessionError> {
let _guard = self
.write_lock
.lock()
.map_err(|_| SessionError::LockPoisoned)?;
std::fs::read(&self.file_path).map_err(SessionError::IoError)
}
pub fn list_branches(&self) -> Vec<String> {
let mut ids: Vec<String> = self.branches.keys().cloned().collect();
ids.sort();
ids
}
}