Skip to main content

tycode_core/persistence/
storage.rs

1use crate::persistence::session::SessionData;
2use anyhow::{Context, Result};
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SessionMetadata {
10    pub id: String,
11    pub created_at: u64,
12    pub last_modified: u64,
13    pub task_list_title: String,
14    pub preview: String,
15}
16
17fn get_sessions_dir(override_dir: Option<&PathBuf>) -> Result<PathBuf> {
18    let sessions_dir = if let Some(dir) = override_dir {
19        dir.clone()
20    } else {
21        let home = dirs::home_dir().context("failed to get home directory")?;
22        home.join(".tycode").join("sessions")
23    };
24    fs::create_dir_all(&sessions_dir).context("failed to create sessions directory")?;
25    Ok(sessions_dir)
26}
27
28pub fn save_session(session: &SessionData, sessions_dir: Option<&PathBuf>) -> Result<()> {
29    let sessions_dir = get_sessions_dir(sessions_dir)?;
30    let file_path = sessions_dir.join(format!("{}.json", session.id));
31
32    let mut session_to_save = session.clone();
33    session_to_save.last_modified = Utc::now().timestamp_millis() as u64;
34
35    let json =
36        serde_json::to_string_pretty(&session_to_save).context("failed to serialize session")?;
37    fs::write(&file_path, json).context("failed to write session file")?;
38
39    Ok(())
40}
41
42pub fn load_session(id: &str, sessions_dir: Option<&PathBuf>) -> Result<SessionData> {
43    let sessions_dir = get_sessions_dir(sessions_dir)?;
44    let file_path = sessions_dir.join(format!("{}.json", id));
45
46    let json = fs::read_to_string(&file_path).context("failed to read session file")?;
47    let session: SessionData =
48        serde_json::from_str(&json).context("failed to deserialize session")?;
49
50    Ok(session)
51}
52
53pub fn list_sessions(sessions_dir: Option<&PathBuf>) -> Result<Vec<SessionMetadata>> {
54    let sessions_dir = get_sessions_dir(sessions_dir)?;
55    let mut sessions = Vec::new();
56
57    let entries = fs::read_dir(&sessions_dir).context("failed to read sessions directory")?;
58
59    for entry in entries {
60        let entry = entry.context("failed to read directory entry")?;
61        let path = entry.path();
62
63        if path.extension().and_then(|s| s.to_str()) != Some("json") {
64            continue;
65        }
66
67        let json = match fs::read_to_string(&path) {
68            Ok(j) => j,
69            Err(e) => {
70                tracing::warn!("Skipping unreadable session file {:?}: {}", path, e);
71                continue;
72            }
73        };
74
75        let session: SessionData = match serde_json::from_str(&json) {
76            Ok(s) => s,
77            Err(e) => {
78                tracing::warn!("Skipping unparseable session file {:?}: {}", path, e);
79                continue;
80            }
81        };
82
83        let mut preview_text = String::new();
84        for msg in session.messages.iter() {
85            if msg.role == crate::ai::types::MessageRole::User {
86                if !preview_text.is_empty() {
87                    preview_text.push_str(" | ");
88                }
89                preview_text.push_str(&msg.content.text());
90                if preview_text.len() >= 100 {
91                    break;
92                }
93            }
94        }
95
96        if preview_text.is_empty() {
97            preview_text = "New Session".to_string();
98        }
99
100        let preview_text = preview_text
101            .replace("\r\n", " ")
102            .replace('\r', " ")
103            .replace('\n', " ");
104        let truncated: String = preview_text.chars().take(100).collect();
105        let preview = if preview_text.chars().count() > 100 {
106            format!("{}...", truncated)
107        } else {
108            truncated
109        };
110
111        let task_list_title = session
112            .module_state
113            .get("task_list")
114            .and_then(|v| v.get("title"))
115            .and_then(|v| v.as_str())
116            .unwrap_or("")
117            .to_string();
118
119        sessions.push(SessionMetadata {
120            id: session.id,
121            created_at: session.created_at,
122            last_modified: session.last_modified,
123            task_list_title,
124            preview,
125        });
126    }
127
128    sessions.sort_by(|a, b| a.last_modified.cmp(&b.last_modified));
129
130    Ok(sessions)
131}
132
133pub fn delete_session(id: &str, sessions_dir: Option<&PathBuf>) -> Result<()> {
134    let sessions_dir = get_sessions_dir(sessions_dir)?;
135    let file_path = sessions_dir.join(format!("{}.json", id));
136
137    fs::remove_file(&file_path).context("failed to delete session file")?;
138
139    Ok(())
140}
141
142pub fn list_session_metadata(
143    sessions_dir: &Path,
144) -> Result<Vec<crate::persistence::session::SessionMetadata>, std::io::Error> {
145    let mut metadata_list = Vec::new();
146
147    let entries = fs::read_dir(sessions_dir)?;
148
149    for entry in entries {
150        let entry = entry?;
151        let path = entry.path();
152
153        if path.extension().and_then(|s| s.to_str()) != Some("json") {
154            continue;
155        }
156
157        let Some(id) = path
158            .file_stem()
159            .and_then(|s| s.to_str())
160            .map(|s| s.to_string())
161        else {
162            continue;
163        };
164
165        let sessions_dir_buf = sessions_dir.to_path_buf();
166        let session_data = match load_session(&id, Some(&sessions_dir_buf)) {
167            Ok(data) => data,
168            Err(e) => {
169                tracing::warn!("Skipping unparseable session {:?}: {}", id, e);
170                continue;
171            }
172        };
173
174        let metadata =
175            crate::persistence::session::SessionMetadata::from_session_data(&session_data);
176        metadata_list.push(metadata);
177    }
178
179    metadata_list.sort_by(|a, b| b.last_modified.cmp(&a.last_modified));
180
181    Ok(metadata_list)
182}