Skip to main content

pawan_api/
sessions.rs

1use serde::Serialize;
2use std::fs;
3use std::path::PathBuf;
4
5#[derive(Debug, Serialize)]
6/// Summary information about a chat session
7///
8/// This struct represents basic metadata about a saved chat session,
9/// used for listing sessions in the UI.
10pub struct SessionSummary {
11    pub id: String,
12    pub created_at: String,
13    pub message_count: usize,
14    pub size_bytes: u64,
15}
16
17#[derive(Debug, Serialize)]
18/// Detailed information about a chat session
19///
20/// This struct contains the full content of a saved chat session,
21/// including all messages and their metadata.
22pub struct SessionDetail {
23    pub id: String,
24    pub messages: serde_json::Value,
25}
26
27fn sessions_dir() -> PathBuf {
28    let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into());
29    PathBuf::from(home).join(".pawan").join("sessions")
30}
31
32/// List all saved chat sessions
33///
34/// Returns a list of all saved chat sessions with their metadata.
35///
36/// # Returns
37/// * `Ok(Vec<SessionSummary>)` - List of session summaries sorted by creation date (newest first)
38/// * `Err(String)` - Error message if session directory cannot be read
39pub fn list_sessions() -> Result<Vec<SessionSummary>, String> {
40    let dir = sessions_dir();
41    if !dir.exists() {
42        return Ok(vec![]);
43    }
44
45    let mut sessions = Vec::new();
46    let entries = fs::read_dir(&dir).map_err(|e| e.to_string())?;
47
48    for entry in entries.flatten() {
49        let path = entry.path();
50        if path.extension().and_then(|e| e.to_str()) != Some("json") {
51            continue;
52        }
53
54        let id = path
55            .file_stem()
56            .and_then(|s| s.to_str())
57            .unwrap_or("unknown")
58            .to_string();
59
60        let metadata = fs::metadata(&path).map_err(|e| e.to_string())?;
61        let size_bytes = metadata.len();
62
63        let created_at = metadata
64            .created()
65            .or_else(|_| metadata.modified())
66            .map(|t| {
67                let dt: chrono::DateTime<chrono::Utc> = t.into();
68                dt.to_rfc3339()
69            })
70            .unwrap_or_default();
71
72        // Count messages by parsing
73        let message_count = fs::read_to_string(&path)
74            .ok()
75            .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
76            .and_then(|v| {
77                v.get("messages")
78                    .and_then(|m| m.as_array())
79                    .map(|a| a.len())
80            })
81            .unwrap_or(0);
82
83        sessions.push(SessionSummary {
84            id,
85            created_at,
86            message_count,
87            size_bytes,
88        });
89    }
90
91    sessions.sort_by(|a, b| b.created_at.cmp(&a.created_at));
92    Ok(sessions)
93}
94
95/// Get a specific chat session by ID
96///
97/// Retrieves the full content of a saved chat session.
98///
99/// # Arguments
100/// * `id` - The session ID to retrieve
101///
102/// # Returns
103/// * `Ok(SessionDetail)` - The session content
104/// * `Err(String)` - Error message if session is not found or cannot be read
105pub fn get_session(id: &str) -> Result<SessionDetail, String> {
106    let path = sessions_dir().join(format!("{}.json", id));
107    if !path.exists() {
108        return Err(format!("session '{}' not found", id));
109    }
110
111    let content = fs::read_to_string(&path).map_err(|e| e.to_string())?;
112    let messages: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
113
114    Ok(SessionDetail {
115        id: id.to_string(),
116        messages,
117    })
118}
119
120/// Delete a chat session by ID
121///
122/// Permanently deletes a saved chat session.
123///
124/// # Arguments
125/// * `id` - The session ID to delete
126///
127/// # Returns
128/// * `Ok(())` - Session successfully deleted
129/// * `Err(String)` - Error message if session is not found or cannot be deleted
130pub fn delete_session(id: &str) -> Result<(), String> {
131    let path = sessions_dir().join(format!("{}.json", id));
132    if !path.exists() {
133        return Err(format!("session '{}' not found", id));
134    }
135    fs::remove_file(&path).map_err(|e| e.to_string())
136}
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use std::fs;
141    use tempfile::TempDir;
142
143    #[test]
144    fn test_sessions_dir_uses_home_env() {
145        let dir = sessions_dir();
146        assert!(dir.ends_with(".pawan/sessions"));
147    }
148
149    #[test]
150    fn test_list_sessions_empty_directory() {
151        let tmp = TempDir::new().unwrap();
152        let sessions_path = tmp.path().join(".pawan").join("sessions");
153        fs::create_dir_all(&sessions_path).unwrap();
154
155        // Temporarily override HOME env var
156        std::env::set_var("HOME", tmp.path());
157        let sessions = list_sessions().unwrap();
158        std::env::remove_var("HOME");
159
160        assert!(sessions.is_empty());
161    }
162
163    #[test]
164    fn test_list_sessions_nonexistent_directory() {
165        // Temporarily override HOME to a non-existent path
166        std::env::set_var("HOME", "/nonexistent/path/that/does/not/exist");
167        let sessions = list_sessions().unwrap();
168        std::env::remove_var("HOME");
169
170        assert!(sessions.is_empty());
171    }
172
173    #[test]
174    fn test_get_session_not_found() {
175        let tmp = TempDir::new().unwrap();
176        std::env::set_var("HOME", tmp.path());
177
178        let result = get_session("nonexistent");
179        std::env::remove_var("HOME");
180
181        assert!(result.is_err());
182        assert!(result.unwrap_err().contains("not found"));
183    }
184
185    #[test]
186    fn test_delete_session_not_found() {
187        let tmp = TempDir::new().unwrap();
188        std::env::set_var("HOME", tmp.path());
189
190        let result = delete_session("nonexistent");
191        std::env::remove_var("HOME");
192
193        assert!(result.is_err());
194        assert!(result.unwrap_err().contains("not found"));
195    }
196
197    #[test]
198    fn test_delete_session_success() {
199        let tmp = TempDir::new().unwrap();
200        let sessions_path = tmp.path().join(".pawan").join("sessions");
201        fs::create_dir_all(&sessions_path).unwrap();
202
203        let session_path = sessions_path.join("test.json");
204        fs::write(&session_path, r#"{"messages":[]}"#).unwrap();
205
206        std::env::set_var("HOME", tmp.path());
207        let result = delete_session("test");
208        std::env::remove_var("HOME");
209
210        assert!(result.is_ok());
211        assert!(!session_path.exists());
212    }
213
214    #[test]
215    fn test_get_session_success() {
216        let tmp = TempDir::new().unwrap();
217        let sessions_path = tmp.path().join(".pawan").join("sessions");
218        fs::create_dir_all(&sessions_path).unwrap();
219
220        let session_path = sessions_path.join("test.json");
221        let content = r#"{"messages":[{"role":"user","content":"hello"}]}"#;
222        fs::write(&session_path, content).unwrap();
223
224        std::env::set_var("HOME", tmp.path());
225        let result = get_session("test");
226        std::env::remove_var("HOME");
227
228        assert!(result.is_ok());
229        let session = result.unwrap();
230        assert_eq!(session.id, "test");
231        assert!(session.messages.is_object());
232    }
233}