tycode_core/persistence/
storage.rs1use 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}