1use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::path::PathBuf;
9use std::time::{Duration, Instant, SystemTime};
10
11use crate::{discover_session_files, parse_session_file};
12
13#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
15pub struct TokenUsage {
16 pub input_tokens: i64,
17 pub output_tokens: i64,
18 pub cache_creation_tokens: i64,
19 pub cache_read_tokens: i64,
20 pub total_tokens: i64,
21}
22
23impl TokenUsage {
24 pub(crate) fn add(
25 &mut self,
26 input: i64,
27 output: i64,
28 cache_creation: i64,
29 cache_read: i64,
30 total: i64,
31 ) {
32 self.input_tokens += input;
33 self.output_tokens += output;
34 self.cache_creation_tokens += cache_creation;
35 self.cache_read_tokens += cache_read;
36 self.total_tokens += if total > 0 {
37 total
38 } else {
39 input + output + cache_creation + cache_read
40 };
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct AgentSession {
47 pub agent_type: String,
48 pub session_id: String,
49 pub conversation_id: Option<String>,
50 pub display_id: String,
51 pub path: PathBuf,
52 pub updated: SystemTime,
53 pub start_timestamp_ms: Option<u64>,
54 pub end_timestamp_ms: Option<u64>,
55 pub model: Option<String>,
56 pub usage: TokenUsage,
57 pub model_usage: BTreeMap<String, TokenUsage>,
58 pub tools: BTreeMap<String, usize>,
59 pub files: BTreeMap<String, usize>,
60 pub prompt_preview: Option<String>,
61 pub duration_ms: u64,
62 pub cwd: Option<String>,
63 pub last_message_at: Option<String>,
64}
65
66#[derive(Debug, Clone)]
68pub struct SessionCandidate {
69 pub agent: &'static str,
70 pub path: PathBuf,
71 pub updated: SystemTime,
72}
73
74#[derive(Debug, Clone)]
76pub struct SessionDirStat {
77 pub agent: &'static str,
78 pub dir: PathBuf,
79 pub sessions: usize,
80 pub bytes: u64,
81}
82
83#[derive(Default)]
85pub struct SessionCache {
86 entries: HashMap<PathBuf, CacheEntry>,
87 cached_sessions: Vec<AgentSession>,
88 last_refresh: Option<Instant>,
89 last_limit: usize,
90}
91
92struct CacheEntry {
93 mtime: SystemTime,
94 session: Option<AgentSession>,
95}
96
97impl SessionCache {
98 pub fn new() -> Self {
99 Self::default()
100 }
101
102 pub fn discover_cached(&mut self, limit: usize, max_age: Duration) -> Vec<AgentSession> {
103 let target = limit.clamp(1, 25);
104 if self.last_limit < target
105 || self
106 .last_refresh
107 .is_none_or(|last| last.elapsed() >= max_age)
108 {
109 self.refresh(target);
110 }
111 self.cached_sessions.iter().take(target).cloned().collect()
112 }
113
114 fn refresh(&mut self, limit: usize) {
115 let mut candidates = discover_session_files();
116 candidates.sort_by_key(|candidate| std::cmp::Reverse(candidate.updated));
117 let target = limit.clamp(1, 25);
118 let mut live_paths = HashSet::new();
119 let mut sessions = Vec::new();
120 let mut seen = HashSet::new();
121
122 for candidate in candidates
123 .into_iter()
124 .take(target.saturating_mul(3).clamp(10, 75))
125 {
126 live_paths.insert(candidate.path.clone());
127 let session = match self.entries.get(&candidate.path) {
128 Some(entry) if entry.mtime == candidate.updated => entry.session.clone(),
129 _ => {
130 let parsed = parse_session_file(&candidate);
131 self.entries.insert(
132 candidate.path.clone(),
133 CacheEntry {
134 mtime: candidate.updated,
135 session: parsed.clone(),
136 },
137 );
138 parsed
139 }
140 };
141 if let Some(session) = session
142 && seen.insert(session.display_id.clone())
143 {
144 sessions.push(session);
145 if sessions.len() >= target {
146 break;
147 }
148 }
149 }
150 self.entries.retain(|path, _| live_paths.contains(path));
151 self.cached_sessions = sessions;
152 self.last_refresh = Some(Instant::now());
153 self.last_limit = target;
154 }
155}