Skip to main content

git_iris/companion/
session.rs

1//! Session state tracking for Iris Companion
2//!
3//! Tracks files touched, time elapsed, and commits made during a session.
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use uuid::Uuid;
10
11/// Activity tracking for a single file
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FileActivity {
14    /// Path to the file
15    pub path: PathBuf,
16    /// When this file was first touched in the session
17    pub first_touched: DateTime<Utc>,
18    /// When this file was last touched
19    pub last_touched: DateTime<Utc>,
20    /// Number of times this file was touched
21    pub touch_count: u32,
22}
23
24impl FileActivity {
25    /// Create a new file activity record
26    #[must_use]
27    pub fn new(path: PathBuf) -> Self {
28        let now = Utc::now();
29        Self {
30            path,
31            first_touched: now,
32            last_touched: now,
33            touch_count: 1,
34        }
35    }
36
37    /// Record another touch
38    pub fn touch(&mut self) {
39        self.last_touched = Utc::now();
40        self.touch_count += 1;
41    }
42}
43
44/// Session state for the current Studio session
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct SessionState {
47    /// Unique session identifier
48    pub session_id: Uuid,
49    /// Repository path
50    pub repo_path: PathBuf,
51    /// Current branch name
52    pub branch: String,
53    /// When the session started
54    pub started_at: DateTime<Utc>,
55    /// Last activity timestamp
56    pub last_activity: DateTime<Utc>,
57    /// Most recent commit timestamp
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub last_commit_at: Option<DateTime<Utc>>,
60    /// Files touched during this session
61    pub files_touched: HashMap<PathBuf, FileActivity>,
62    /// Commits made during this session (hashes)
63    pub commits_made: Vec<String>,
64}
65
66impl SessionState {
67    /// Create a new session
68    #[must_use]
69    pub fn new(repo_path: PathBuf, branch: String) -> Self {
70        let now = Utc::now();
71        Self {
72            session_id: Uuid::new_v4(),
73            repo_path,
74            branch,
75            started_at: now,
76            last_activity: now,
77            last_commit_at: None,
78            files_touched: HashMap::new(),
79            commits_made: Vec::new(),
80        }
81    }
82
83    /// Record a file touch
84    pub fn touch_file(&mut self, path: PathBuf) {
85        self.last_activity = Utc::now();
86        let normalized_path = self.normalize_path(path);
87        self.files_touched
88            .entry(normalized_path.clone())
89            .and_modify(FileActivity::touch)
90            .or_insert_with(|| FileActivity::new(normalized_path));
91    }
92
93    /// Record a commit
94    pub fn record_commit(&mut self, hash: String) {
95        let now = Utc::now();
96        self.last_activity = now;
97        self.last_commit_at = Some(now);
98        self.commits_made.push(hash);
99    }
100
101    /// Get session duration
102    #[must_use]
103    pub fn duration(&self) -> chrono::Duration {
104        Utc::now() - self.started_at
105    }
106
107    /// Get number of files touched
108    #[must_use]
109    pub fn files_count(&self) -> usize {
110        self.files_touched.len()
111    }
112
113    /// Get files ordered by most recently touched
114    #[must_use]
115    pub fn recent_files(&self) -> Vec<&FileActivity> {
116        let mut files: Vec<_> = self.files_touched.values().collect();
117        files.sort_by_key(|f| std::cmp::Reverse(f.last_touched));
118        files
119    }
120
121    /// Get time since last commit (if any)
122    #[must_use]
123    pub fn time_since_last_commit(&self) -> Option<chrono::Duration> {
124        self.last_commit_at
125            .map(|last_commit_at| Utc::now() - last_commit_at)
126    }
127
128    /// Update branch and start a fresh session for the new branch
129    pub fn set_branch(&mut self, branch: String) {
130        self.session_id = Uuid::new_v4();
131        self.started_at = Utc::now();
132        self.last_activity = self.started_at;
133        self.last_commit_at = None;
134        self.files_touched.clear();
135        self.commits_made.clear();
136        self.branch = branch;
137    }
138
139    /// Normalize a path so the same file is tracked consistently
140    fn normalize_path(&self, path: PathBuf) -> PathBuf {
141        if path.is_absolute()
142            && let Ok(relative) = path.strip_prefix(&self.repo_path)
143        {
144            return relative.to_path_buf();
145        }
146
147        path
148    }
149}