1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SessionEntry {
9 pub id: Uuid,
10 #[serde(skip_serializing_if = "Option::is_none")]
12 pub parent_id: Option<Uuid>,
13 pub message: AgentMessage,
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub label: Option<String>,
17 pub timestamp: i64,
18}
19
20impl SessionEntry {
21 pub fn new(message: AgentMessage) -> Self {
23 Self {
24 id: Uuid::new_v4(),
25 parent_id: None,
26 message,
27 label: None,
28 timestamp: chrono::Utc::now().timestamp_millis(),
29 }
30 }
31
32 pub fn branched(message: AgentMessage, parent_id: Uuid) -> Self {
34 Self {
35 id: Uuid::new_v4(),
36 parent_id: Some(parent_id),
37 message,
38 label: None,
39 timestamp: chrono::Utc::now().timestamp_millis(),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(tag = "type")]
46pub enum AgentMessage {
47 User { content: String },
48 Assistant { content: String },
49 System { content: String },
50}
51
52impl AgentMessage {
53 pub fn content(&self) -> &str {
55 match self {
56 AgentMessage::User { content } => content,
57 AgentMessage::Assistant { content } => content,
58 AgentMessage::System { content } => content,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SessionMeta {
66 pub id: Uuid,
67 pub parent_id: Option<Uuid>, pub root_id: Option<Uuid>, pub branch_point: Option<Uuid>, pub created_at: i64,
71 pub updated_at: i64,
72 pub name: Option<String>,
73}
74
75impl SessionMeta {
76 pub fn new(id: Uuid) -> Self {
77 let now = chrono::Utc::now().timestamp_millis();
78 Self {
79 id,
80 parent_id: None,
81 root_id: None,
82 branch_point: None,
83 created_at: now,
84 updated_at: now,
85 name: None,
86 }
87 }
88
89 pub fn branched_from(parent_id: Uuid, root_id: Option<Uuid>, branch_point: Uuid) -> Self {
90 let now = chrono::Utc::now().timestamp_millis();
91 Self {
92 id: Uuid::new_v4(),
93 parent_id: Some(parent_id),
94 root_id: root_id.or(Some(parent_id)),
95 branch_point: Some(branch_point),
96 created_at: now,
97 updated_at: now,
98 name: None,
99 }
100 }
101}
102
103pub struct SessionManager {
104 sessions_dir: PathBuf,
105 meta_dir: PathBuf,
106}
107
108impl SessionManager {
109 pub async fn new() -> Result<Self> {
110 let home = dirs::home_dir().context("Cannot find home directory")?;
111 let base_dir = home.join(".oxi");
112 let sessions_dir = base_dir.join("sessions");
113 let meta_dir = base_dir.join("meta");
114 tokio::fs::create_dir_all(&sessions_dir).await?;
115 tokio::fs::create_dir_all(&meta_dir).await?;
116 Ok(Self { sessions_dir, meta_dir })
117 }
118
119 pub async fn save(&self, id: Uuid, entries: &[SessionEntry]) -> Result<()> {
120 let path = self.session_path(&id);
121 let json = serde_json::to_string_pretty(entries)?;
122 tokio::fs::write(&path, json).await?;
123 Ok(())
124 }
125
126 pub async fn load(&self, id: Uuid) -> Result<Vec<SessionEntry>> {
127 let path = self.session_path(&id);
128 if !path.exists() {
129 return Ok(Vec::new());
130 }
131 let contents = tokio::fs::read_to_string(&path).await?;
132 let entries: Vec<SessionEntry> = serde_json::from_str(&contents)?;
133 Ok(entries)
134 }
135
136 pub fn session_path(&self, id: &Uuid) -> PathBuf {
137 self.sessions_dir.join(format!("{}.json", id))
138 }
139
140 fn meta_path(&self, id: &Uuid) -> PathBuf {
142 self.meta_dir.join(format!("{}.json", id))
143 }
144
145 pub async fn list_sessions(&self) -> Result<Vec<SessionMeta>> {
147 let mut entries = tokio::fs::read_dir(&self.meta_dir).await?;
148 let mut metas = Vec::new();
149
150 while let Some(entry) = entries.next_entry().await? {
151 let path = entry.path();
152 if path.extension().and_then(|s| s.to_str()) == Some("json") {
153 if let Ok(contents) = tokio::fs::read_to_string(&path).await {
154 if let Ok(meta) = serde_json::from_str::<SessionMeta>(&contents) {
155 metas.push(meta);
156 }
157 }
158 }
159 }
160
161 metas.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
162 Ok(metas)
163 }
164
165 pub async fn save_meta(&self, meta: &SessionMeta) -> Result<()> {
167 let path = self.meta_path(&meta.id);
168 let json = serde_json::to_string_pretty(meta)?;
169 tokio::fs::write(&path, json).await?;
170 Ok(())
171 }
172
173 pub async fn load_meta(&self, id: Uuid) -> Result<Option<SessionMeta>> {
175 let path = self.meta_path(&id);
176 if !path.exists() {
177 return Ok(None);
178 }
179 let contents = tokio::fs::read_to_string(&path).await?;
180 let meta: SessionMeta = serde_json::from_str(&contents)?;
181 Ok(Some(meta))
182 }
183
184 pub async fn create(&self) -> Result<SessionMeta> {
186 let id = Uuid::new_v4();
187 let meta = SessionMeta::new(id);
188 self.save_meta(&meta).await?;
189 Ok(meta)
190 }
191
192 pub async fn branch_from(&self, parent_id: Uuid, entry_id: Uuid) -> Result<(Uuid, Vec<SessionEntry>)> {
194 let parent_entries = self.load(parent_id).await?;
196
197 let entry_idx = parent_entries
199 .iter()
200 .position(|e| e.id == entry_id)
201 .with_context(|| format!("Entry {} not found in session {}", entry_id, parent_id))?;
202
203 let parent_meta = self.load_meta(parent_id).await?
205 .with_context(|| format!("Parent session {} not found", parent_id))?;
206
207 let new_id = Uuid::new_v4();
209 let meta = SessionMeta::branched_from(parent_id, parent_meta.root_id.or(Some(parent_id)), entry_id);
210
211 let mut new_entries: Vec<SessionEntry> = parent_entries[..=entry_idx]
213 .iter()
214 .map(|e| {
215 let mut new_entry = e.clone();
216 new_entry.id = Uuid::new_v4();
217 new_entry
218 })
219 .collect();
220
221 if let Some(last) = new_entries.last_mut() {
223 last.parent_id = Some(entry_id);
224 }
225
226 self.save_meta(&meta).await?;
228 self.save(new_id, &new_entries).await?;
229
230 Ok((new_id, new_entries))
231 }
232
233 pub async fn get_entries(&self, session_id: Uuid) -> Result<Vec<SessionEntry>> {
235 self.load(session_id).await
236 }
237
238 pub async fn get_tree(&self, session_id: Uuid) -> Result<Vec<(Uuid, SessionEntry)>> {
240 let mut tree = Vec::new();
241 let mut current_id = Some(session_id);
242
243 while let Some(id) = current_id {
244 let meta = match self.load_meta(id).await? {
245 Some(m) => m,
246 None => break,
247 };
248
249 let entries = self.load(id).await?;
251 for entry in entries {
252 tree.push((id, entry));
253 }
254
255 current_id = meta.parent_id;
257 }
258
259 Ok(tree)
260 }
261
262 pub async fn get_branches_from_entry(&self, entry_id: Uuid) -> Result<Vec<(Uuid, SessionEntry)>> {
264 let mut branches = Vec::new();
265 let metas = self.list_sessions().await?;
266
267 for meta in metas {
268 if meta.branch_point == Some(entry_id) || meta.parent_id == Some(entry_id) {
270 let entries = self.load(meta.id).await?;
272 if let Some(first) = entries.first() {
273 branches.push((meta.id, first.clone()));
274 }
275 }
276 }
277
278 Ok(branches)
279 }
280
281 pub async fn get_branch_info(&self, session_id: Uuid) -> Result<Option<BranchInfo>> {
283 let meta = match self.load_meta(session_id).await? {
284 Some(m) => m,
285 None => return Ok(None),
286 };
287
288 if meta.parent_id.is_none() {
289 return Ok(None);
290 }
291
292 let parent_meta = self.load_meta(meta.parent_id.unwrap()).await?;
293 Ok(Some(BranchInfo {
294 session_id,
295 parent_session_id: meta.parent_id,
296 root_session_id: meta.root_id,
297 branch_point_entry_id: meta.branch_point,
298 parent_session_name: parent_meta.as_ref().and_then(|m| m.name.clone()),
299 }))
300 }
301
302 pub async fn delete(&self, id: Uuid) -> Result<()> {
304 tokio::fs::remove_file(self.session_path(&id)).await.ok();
305 tokio::fs::remove_file(self.meta_path(&id)).await.ok();
306 Ok(())
307 }
308}
309
310#[derive(Debug, Clone)]
312pub struct BranchInfo {
313 pub session_id: Uuid,
314 pub parent_session_id: Option<Uuid>,
315 pub root_session_id: Option<Uuid>,
316 pub branch_point_entry_id: Option<Uuid>,
317 pub parent_session_name: Option<String>,
318}