use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ActivityType {
Message,
ToolUse,
Command,
FileAccess,
SessionStart,
SessionEnd,
Error,
UserInput,
Compact,
MemoryUpdate,
}
impl std::fmt::Display for ActivityType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ActivityType::Message => write!(f, "message"),
ActivityType::ToolUse => write!(f, "tool_use"),
ActivityType::Command => write!(f, "command"),
ActivityType::FileAccess => write!(f, "file_access"),
ActivityType::SessionStart => write!(f, "session_start"),
ActivityType::SessionEnd => write!(f, "session_end"),
ActivityType::Error => write!(f, "error"),
ActivityType::UserInput => write!(f, "user_input"),
ActivityType::Compact => write!(f, "compact"),
ActivityType::MemoryUpdate => write!(f, "memory_update"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionActivity {
pub activity_type: ActivityType,
pub details: Option<String>,
pub timestamp: DateTime<Utc>,
pub metadata: Option<serde_json::Value>,
}
impl SessionActivity {
pub fn new(activity_type: ActivityType) -> Self {
Self {
activity_type,
details: None,
timestamp: Utc::now(),
metadata: None,
}
}
pub fn with_details(mut self, details: String) -> Self {
self.details = Some(details);
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = timestamp;
self
}
pub fn age(&self) -> Duration {
let now = Utc::now();
(now - self.timestamp).to_std().unwrap_or(Duration::ZERO)
}
}
pub struct SessionActivityTracker {
activities: Vec<SessionActivity>,
max_capacity: usize,
}
impl SessionActivityTracker {
pub fn new() -> Self {
Self {
activities: Vec::new(),
max_capacity: 10_000,
}
}
pub fn with_capacity(max_capacity: usize) -> Self {
Self {
activities: Vec::new(),
max_capacity,
}
}
pub fn record(&mut self, activity: SessionActivity) {
if self.activities.len() >= self.max_capacity {
let remove_count = self.max_capacity / 4; self.activities.drain(..remove_count);
}
self.activities.push(activity);
}
pub fn record_type(&mut self, activity_type: ActivityType) {
self.record(SessionActivity::new(activity_type));
}
pub fn record_type_with_details(&mut self, activity_type: ActivityType, details: String) {
self.record(SessionActivity::new(activity_type).with_details(details));
}
pub fn get_activities(&self) -> &[SessionActivity] {
&self.activities
}
pub fn get_recent(&self, duration: Duration) -> Vec<&SessionActivity> {
let now = Utc::now();
let cutoff = now - chrono::Duration::from_std(duration).unwrap_or(chrono::Duration::zero());
self.activities
.iter()
.filter(|a| a.timestamp >= cutoff)
.collect()
}
pub fn get_recent_count(&self, duration: Duration) -> usize {
self.get_recent(duration).len()
}
pub fn get_activities_by_type(&self, activity_type: ActivityType) -> Vec<&SessionActivity> {
self.activities
.iter()
.filter(|a| a.activity_type == activity_type)
.collect()
}
pub fn count_by_type(&self, activity_type: ActivityType) -> usize {
self.get_activities_by_type(activity_type).len()
}
pub fn get_last_activity(&self) -> Option<&SessionActivity> {
self.activities.last()
}
pub fn get_last_activity_of_type(
&self,
activity_type: ActivityType,
) -> Option<&SessionActivity> {
self.activities
.iter()
.rev()
.find(|a| a.activity_type == activity_type)
}
pub fn time_since_last_activity(&self) -> Option<Duration> {
self.activities.last().map(|a| a.age())
}
pub fn time_since_last_activity_of_type(
&self,
activity_type: ActivityType,
) -> Option<Duration> {
self.get_last_activity_of_type(activity_type)
.map(|a| a.age())
}
pub fn get_activity_rate(&self, duration: Duration) -> f64 {
let count = self.get_recent_count(duration);
let secs = duration.as_secs_f64();
if secs > 0.0 { count as f64 / secs } else { 0.0 }
}
pub fn has_recent_activity(&self, duration: Duration) -> bool {
self.get_recent_count(duration) > 0
}
pub fn total_count(&self) -> usize {
self.activities.len()
}
pub fn clear(&mut self) {
self.activities.clear();
}
pub fn export_activities(&self) -> Vec<SessionActivity> {
self.activities.clone()
}
pub fn import_activities(&mut self, activities: Vec<SessionActivity>) {
self.activities = activities;
}
pub fn get_activity_summary(&self) -> serde_json::Value {
let mut summary = serde_json::Map::new();
for activity_type in &[
ActivityType::Message,
ActivityType::ToolUse,
ActivityType::Command,
ActivityType::FileAccess,
ActivityType::SessionStart,
ActivityType::SessionEnd,
ActivityType::Error,
ActivityType::UserInput,
ActivityType::Compact,
ActivityType::MemoryUpdate,
] {
let count = self.count_by_type(*activity_type);
summary.insert(
activity_type.to_string(),
serde_json::Value::Number(count.into()),
);
}
serde_json::Value::Object(summary)
}
}
impl Default for SessionActivityTracker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_tracker() {
let tracker = SessionActivityTracker::new();
assert_eq!(tracker.total_count(), 0);
}
#[test]
fn test_record_activity() {
let mut tracker = SessionActivityTracker::new();
tracker.record_type(ActivityType::Message);
assert_eq!(tracker.total_count(), 1);
}
#[test]
fn test_record_with_details() {
let mut tracker = SessionActivityTracker::new();
tracker.record_type_with_details(ActivityType::ToolUse, "ReadFile".to_string());
assert_eq!(tracker.count_by_type(ActivityType::ToolUse), 1);
}
#[test]
fn test_get_recent_count() {
let mut tracker = SessionActivityTracker::new();
tracker.record_type(ActivityType::Message);
assert!(tracker.get_recent_count(Duration::from_secs(60)) > 0);
}
#[test]
fn test_clear() {
let mut tracker = SessionActivityTracker::new();
tracker.record_type(ActivityType::Message);
tracker.record_type(ActivityType::ToolUse);
tracker.clear();
assert_eq!(tracker.total_count(), 0);
}
#[test]
fn test_activity_type_display() {
assert_eq!(ActivityType::Message.to_string(), "message");
assert_eq!(ActivityType::ToolUse.to_string(), "tool_use");
assert_eq!(ActivityType::Command.to_string(), "command");
}
#[test]
fn test_last_activity() {
let mut tracker = SessionActivityTracker::new();
assert!(tracker.get_last_activity().is_none());
tracker.record_type(ActivityType::Message);
assert!(tracker.get_last_activity().is_some());
}
#[test]
fn test_time_since_last_activity() {
let mut tracker = SessionActivityTracker::new();
assert!(tracker.time_since_last_activity().is_none());
tracker.record_type(ActivityType::Message);
assert!(tracker.time_since_last_activity().is_some());
}
#[test]
fn test_activity_summary() {
let mut tracker = SessionActivityTracker::new();
tracker.record_type(ActivityType::Message);
tracker.record_type(ActivityType::Message);
tracker.record_type(ActivityType::ToolUse);
let summary = tracker.get_activity_summary();
assert!(summary.is_object());
}
#[test]
fn test_activity_rate() {
let mut tracker = SessionActivityTracker::new();
for _ in 0..5 {
tracker.record_type(ActivityType::Message);
}
let rate = tracker.get_activity_rate(Duration::from_secs(60));
assert!(rate > 0.0);
}
#[test]
fn test_has_recent_activity() {
let mut tracker = SessionActivityTracker::new();
assert!(!tracker.has_recent_activity(Duration::from_secs(60)));
tracker.record_type(ActivityType::Message);
assert!(tracker.has_recent_activity(Duration::from_secs(60)));
}
#[test]
fn test_export_import() {
let mut tracker = SessionActivityTracker::new();
tracker.record_type(ActivityType::Message);
tracker.record_type(ActivityType::ToolUse);
let exported = tracker.export_activities();
let mut tracker2 = SessionActivityTracker::new();
tracker2.import_activities(exported);
assert_eq!(tracker2.total_count(), 2);
}
}