codetether_agent/session/
listing.rs1use super::Session;
2use anyhow::Result;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use tokio::fs;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SessionSummary {
11 pub id: String,
12 pub title: Option<String>,
13 pub created_at: DateTime<Utc>,
14 pub updated_at: DateTime<Utc>,
15 pub message_count: usize,
16 pub agent: String,
17 #[serde(default)]
19 pub directory: Option<PathBuf>,
20}
21
22pub async fn list_sessions() -> Result<Vec<SessionSummary>> {
24 let sessions_dir = crate::config::Config::data_dir()
25 .map(|d| d.join("sessions"))
26 .ok_or_else(|| anyhow::anyhow!("Could not determine data directory"))?;
27
28 if !sessions_dir.exists() {
29 return Ok(Vec::new());
30 }
31
32 let mut summaries = Vec::new();
33 let mut entries = fs::read_dir(&sessions_dir).await?;
34
35 while let Some(entry) = entries.next_entry().await? {
36 let path = entry.path();
37 if path.extension().map(|e| e == "json").unwrap_or(false) {
38 let content = match fs::read_to_string(&path).await {
39 Ok(c) => c,
40 Err(err) => {
41 tracing::warn!(path = %path.display(), error = %err, "skipping unreadable session file");
42 continue;
43 }
44 };
45 let session = match serde_json::from_str::<Session>(&content) {
46 Ok(s) => s,
47 Err(err) => {
48 tracing::warn!(path = %path.display(), error = %err, "skipping malformed session file");
49 continue;
50 }
51 };
52 summaries.push(SessionSummary {
53 id: session.id,
54 title: session.title,
55 created_at: session.created_at,
56 updated_at: session.updated_at,
57 message_count: session.messages.len(),
58 agent: session.agent,
59 directory: session.metadata.directory,
60 });
61 }
62 }
63
64 summaries.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
65 Ok(summaries)
66}
67
68pub async fn list_sessions_for_directory(dir: &std::path::Path) -> Result<Vec<SessionSummary>> {
73 let all = list_sessions().await?;
74 let canonical = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
75 Ok(all
76 .into_iter()
77 .filter(|s| {
78 s.directory
79 .as_ref()
80 .map(|d| {
81 match d.canonicalize() {
82 Ok(c) => c == canonical,
83 Err(_) => {
84 d == dir || canonical.starts_with(d) || d.starts_with(&canonical)
86 }
87 }
88 })
89 .unwrap_or(false)
90 })
91 .collect())
92}
93
94pub async fn list_sessions_paged(
99 dir: &std::path::Path,
100 limit: usize,
101 offset: usize,
102) -> Result<Vec<SessionSummary>> {
103 let mut sessions = list_sessions_for_directory(dir).await?;
104 sessions.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
105 Ok(sessions.into_iter().skip(offset).take(limit).collect())
106}