1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ActivityType {
11 Message,
12 ToolUse,
13 Command,
14 FileAccess,
15 SessionStart,
16 SessionEnd,
17 Error,
18 UserInput,
19 Compact,
20 MemoryUpdate,
21}
22
23impl std::fmt::Display for ActivityType {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 ActivityType::Message => write!(f, "message"),
27 ActivityType::ToolUse => write!(f, "tool_use"),
28 ActivityType::Command => write!(f, "command"),
29 ActivityType::FileAccess => write!(f, "file_access"),
30 ActivityType::SessionStart => write!(f, "session_start"),
31 ActivityType::SessionEnd => write!(f, "session_end"),
32 ActivityType::Error => write!(f, "error"),
33 ActivityType::UserInput => write!(f, "user_input"),
34 ActivityType::Compact => write!(f, "compact"),
35 ActivityType::MemoryUpdate => write!(f, "memory_update"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SessionActivity {
43 pub activity_type: ActivityType,
44 pub details: Option<String>,
45 pub timestamp: DateTime<Utc>,
46 pub metadata: Option<serde_json::Value>,
47}
48
49impl SessionActivity {
50 pub fn new(activity_type: ActivityType) -> Self {
51 Self {
52 activity_type,
53 details: None,
54 timestamp: Utc::now(),
55 metadata: None,
56 }
57 }
58
59 pub fn with_details(mut self, details: String) -> Self {
60 self.details = Some(details);
61 self
62 }
63
64 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
65 self.metadata = Some(metadata);
66 self
67 }
68
69 pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
70 self.timestamp = timestamp;
71 self
72 }
73
74 pub fn age(&self) -> Duration {
76 let now = Utc::now();
77 (now - self.timestamp).to_std().unwrap_or(Duration::ZERO)
78 }
79}
80
81pub struct SessionActivityTracker {
83 activities: Vec<SessionActivity>,
84 max_capacity: usize,
85}
86
87impl SessionActivityTracker {
88 pub fn new() -> Self {
89 Self {
90 activities: Vec::new(),
91 max_capacity: 10_000,
92 }
93 }
94
95 pub fn with_capacity(max_capacity: usize) -> Self {
97 Self {
98 activities: Vec::new(),
99 max_capacity,
100 }
101 }
102
103 pub fn record(&mut self, activity: SessionActivity) {
105 if self.activities.len() >= self.max_capacity {
107 let remove_count = self.max_capacity / 4; self.activities.drain(..remove_count);
109 }
110 self.activities.push(activity);
111 }
112
113 pub fn record_type(&mut self, activity_type: ActivityType) {
115 self.record(SessionActivity::new(activity_type));
116 }
117
118 pub fn record_type_with_details(&mut self, activity_type: ActivityType, details: String) {
120 self.record(SessionActivity::new(activity_type).with_details(details));
121 }
122
123 pub fn get_activities(&self) -> &[SessionActivity] {
125 &self.activities
126 }
127
128 pub fn get_recent(&self, duration: Duration) -> Vec<&SessionActivity> {
130 let now = Utc::now();
131 let cutoff = now - chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::zero());
132
133 self.activities
134 .iter()
135 .filter(|a| a.timestamp >= cutoff)
136 .collect()
137 }
138
139 pub fn get_recent_count(&self, duration: Duration) -> usize {
141 self.get_recent(duration).len()
142 }
143
144 pub fn get_activities_by_type(&self, activity_type: ActivityType) -> Vec<&SessionActivity> {
146 self.activities
147 .iter()
148 .filter(|a| a.activity_type == activity_type)
149 .collect()
150 }
151
152 pub fn count_by_type(&self, activity_type: ActivityType) -> usize {
154 self.get_activities_by_type(activity_type).len()
155 }
156
157 pub fn get_last_activity(&self) -> Option<&SessionActivity> {
159 self.activities.last()
160 }
161
162 pub fn get_last_activity_of_type(
164 &self,
165 activity_type: ActivityType,
166 ) -> Option<&SessionActivity> {
167 self.activities
168 .iter()
169 .rev()
170 .find(|a| a.activity_type == activity_type)
171 }
172
173 pub fn time_since_last_activity(&self) -> Option<Duration> {
175 self.activities.last().map(|a| a.age())
176 }
177
178 pub fn time_since_last_activity_of_type(
180 &self,
181 activity_type: ActivityType,
182 ) -> Option<Duration> {
183 self.get_last_activity_of_type(activity_type)
184 .map(|a| a.age())
185 }
186
187 pub fn get_activity_rate(&self, duration: Duration) -> f64 {
189 let count = self.get_recent_count(duration);
190 let secs = duration.as_secs_f64();
191 if secs > 0.0 { count as f64 / secs } else { 0.0 }
192 }
193
194 pub fn has_recent_activity(&self, duration: Duration) -> bool {
196 self.get_recent_count(duration) > 0
197 }
198
199 pub fn total_count(&self) -> usize {
201 self.activities.len()
202 }
203
204 pub fn clear(&mut self) {
206 self.activities.clear();
207 }
208
209 pub fn export_activities(&self) -> Vec<SessionActivity> {
211 self.activities.clone()
212 }
213
214 pub fn import_activities(&mut self, activities: Vec<SessionActivity>) {
216 self.activities = activities;
217 }
218
219 pub fn get_activity_summary(&self) -> serde_json::Value {
221 let mut summary = serde_json::Map::new();
222 for activity_type in &[
223 ActivityType::Message,
224 ActivityType::ToolUse,
225 ActivityType::Command,
226 ActivityType::FileAccess,
227 ActivityType::SessionStart,
228 ActivityType::SessionEnd,
229 ActivityType::Error,
230 ActivityType::UserInput,
231 ActivityType::Compact,
232 ActivityType::MemoryUpdate,
233 ] {
234 let count = self.count_by_type(*activity_type);
235 summary.insert(
236 activity_type.to_string(),
237 serde_json::Value::Number(count.into()),
238 );
239 }
240 serde_json::Value::Object(summary)
241 }
242}
243
244impl Default for SessionActivityTracker {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_new_tracker() {
256 let tracker = SessionActivityTracker::new();
257 assert_eq!(tracker.total_count(), 0);
258 }
259
260 #[test]
261 fn test_record_activity() {
262 let mut tracker = SessionActivityTracker::new();
263 tracker.record_type(ActivityType::Message);
264 assert_eq!(tracker.total_count(), 1);
265 }
266
267 #[test]
268 fn test_record_with_details() {
269 let mut tracker = SessionActivityTracker::new();
270 tracker.record_type_with_details(ActivityType::ToolUse, "ReadFile".to_string());
271 assert_eq!(tracker.count_by_type(ActivityType::ToolUse), 1);
272 }
273
274 #[test]
275 fn test_get_recent_count() {
276 let mut tracker = SessionActivityTracker::new();
277 tracker.record_type(ActivityType::Message);
278 assert!(tracker.get_recent_count(Duration::from_secs(60)) > 0);
280 }
281
282 #[test]
283 fn test_clear() {
284 let mut tracker = SessionActivityTracker::new();
285 tracker.record_type(ActivityType::Message);
286 tracker.record_type(ActivityType::ToolUse);
287 tracker.clear();
288 assert_eq!(tracker.total_count(), 0);
289 }
290
291 #[test]
292 fn test_activity_type_display() {
293 assert_eq!(ActivityType::Message.to_string(), "message");
294 assert_eq!(ActivityType::ToolUse.to_string(), "tool_use");
295 assert_eq!(ActivityType::Command.to_string(), "command");
296 }
297
298 #[test]
299 fn test_last_activity() {
300 let mut tracker = SessionActivityTracker::new();
301 assert!(tracker.get_last_activity().is_none());
302
303 tracker.record_type(ActivityType::Message);
304 assert!(tracker.get_last_activity().is_some());
305 }
306
307 #[test]
308 fn test_time_since_last_activity() {
309 let mut tracker = SessionActivityTracker::new();
310 assert!(tracker.time_since_last_activity().is_none());
311
312 tracker.record_type(ActivityType::Message);
313 assert!(tracker.time_since_last_activity().is_some());
314 }
315
316 #[test]
317 fn test_activity_summary() {
318 let mut tracker = SessionActivityTracker::new();
319 tracker.record_type(ActivityType::Message);
320 tracker.record_type(ActivityType::Message);
321 tracker.record_type(ActivityType::ToolUse);
322
323 let summary = tracker.get_activity_summary();
324 assert!(summary.is_object());
325 }
326
327 #[test]
328 fn test_activity_rate() {
329 let mut tracker = SessionActivityTracker::new();
330 for _ in 0..5 {
331 tracker.record_type(ActivityType::Message);
332 }
333 let rate = tracker.get_activity_rate(Duration::from_secs(60));
334 assert!(rate > 0.0);
335 }
336
337 #[test]
338 fn test_has_recent_activity() {
339 let mut tracker = SessionActivityTracker::new();
340 assert!(!tracker.has_recent_activity(Duration::from_secs(60)));
341
342 tracker.record_type(ActivityType::Message);
343 assert!(tracker.has_recent_activity(Duration::from_secs(60)));
344 }
345
346 #[test]
347 fn test_export_import() {
348 let mut tracker = SessionActivityTracker::new();
349 tracker.record_type(ActivityType::Message);
350 tracker.record_type(ActivityType::ToolUse);
351
352 let exported = tracker.export_activities();
353 let mut tracker2 = SessionActivityTracker::new();
354 tracker2.import_activities(exported);
355
356 assert_eq!(tracker2.total_count(), 2);
357 }
358}