use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri_plugin_store::StoreExt; const SESSIONS_STORE_KEY: &str = "sessions"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SavedSession { pub id: String, pub name: String, pub created_at: DateTime, pub last_activity_at: DateTime, pub working_directory: String, pub message_count: usize, pub preview: String, // First ~100 chars of conversation for preview pub messages: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SavedMessage { pub id: String, #[serde(rename = "type")] pub message_type: String, pub content: String, pub timestamp: DateTime, pub tool_name: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionListItem { pub id: String, pub name: String, pub created_at: DateTime, pub last_activity_at: DateTime, pub working_directory: String, pub message_count: usize, pub preview: String, } impl From<&SavedSession> for SessionListItem { fn from(session: &SavedSession) -> Self { SessionListItem { id: session.id.clone(), name: session.name.clone(), created_at: session.created_at, last_activity_at: session.last_activity_at, working_directory: session.working_directory.clone(), message_count: session.message_count, preview: session.preview.clone(), } } } fn load_all_sessions(app: &AppHandle) -> Result, String> { let store = app .store("hikari-sessions.json") .map_err(|e| e.to_string())?; match store.get(SESSIONS_STORE_KEY) { Some(value) => serde_json::from_value(value.clone()).map_err(|e| e.to_string()), None => Ok(Vec::new()), } } fn save_all_sessions(app: &AppHandle, sessions: &[SavedSession]) -> Result<(), String> { let store = app .store("hikari-sessions.json") .map_err(|e| e.to_string())?; let value = serde_json::to_value(sessions).map_err(|e| e.to_string())?; store.set(SESSIONS_STORE_KEY, value); store.save().map_err(|e| e.to_string())?; Ok(()) } #[tauri::command] pub async fn list_sessions(app: AppHandle) -> Result, String> { let sessions = load_all_sessions(&app)?; let mut items: Vec = sessions.iter().map(SessionListItem::from).collect(); // Sort by last activity, most recent first items.sort_by(|a, b| b.last_activity_at.cmp(&a.last_activity_at)); Ok(items) } #[tauri::command] pub async fn save_session(app: AppHandle, session: SavedSession) -> Result<(), String> { let mut sessions = load_all_sessions(&app)?; // Update existing or add new if let Some(existing) = sessions.iter_mut().find(|s| s.id == session.id) { *existing = session; } else { sessions.push(session); } save_all_sessions(&app, &sessions) } #[tauri::command] pub async fn load_session(app: AppHandle, session_id: String) -> Result, String> { let sessions = load_all_sessions(&app)?; Ok(sessions.into_iter().find(|s| s.id == session_id)) } #[tauri::command] pub async fn delete_session(app: AppHandle, session_id: String) -> Result<(), String> { let mut sessions = load_all_sessions(&app)?; sessions.retain(|s| s.id != session_id); save_all_sessions(&app, &sessions) } #[tauri::command] pub async fn search_sessions(app: AppHandle, query: String) -> Result, String> { let sessions = load_all_sessions(&app)?; let query_lower = query.to_lowercase(); let mut matching: Vec = sessions .iter() .filter(|s| { s.name.to_lowercase().contains(&query_lower) || s.preview.to_lowercase().contains(&query_lower) || s.working_directory.to_lowercase().contains(&query_lower) || s.messages .iter() .any(|m| m.content.to_lowercase().contains(&query_lower)) }) .map(SessionListItem::from) .collect(); // Sort by last activity, most recent first matching.sort_by(|a, b| b.last_activity_at.cmp(&a.last_activity_at)); Ok(matching) } #[tauri::command] pub async fn clear_all_sessions(app: AppHandle) -> Result<(), String> { save_all_sessions(&app, &[]) } #[cfg(test)] mod tests { use super::*; #[test] fn test_session_list_item_from_saved_session() { let session = SavedSession { id: "test-id".to_string(), name: "Test Session".to_string(), created_at: Utc::now(), last_activity_at: Utc::now(), working_directory: "/home/test".to_string(), message_count: 5, preview: "Hello world".to_string(), messages: vec![], }; let item = SessionListItem::from(&session); assert_eq!(item.id, "test-id"); assert_eq!(item.name, "Test Session"); assert_eq!(item.message_count, 5); } }