Skip to main content

git_iris/companion/
mod.rs

1//! Iris Companion - Ambient awareness for Git workflows
2//!
3//! Provides session tracking, branch memory, and live file watching
4//! to transform Studio into an always-aware development companion.
5
6mod branch_memory;
7mod session;
8mod storage;
9mod watcher;
10
11pub use branch_memory::{BranchMemory, FileFocus};
12pub use session::{FileActivity, SessionState};
13pub use storage::CompanionStorage;
14pub use watcher::{CompanionEvent, FileWatcherService};
15
16use anyhow::Result;
17use std::path::PathBuf;
18use std::sync::Arc;
19use tokio::sync::mpsc;
20
21/// Main companion service that coordinates all subsystems
22pub struct CompanionService {
23    /// Repository path being watched
24    repo_path: PathBuf,
25    /// Current session state
26    session: Arc<parking_lot::RwLock<SessionState>>,
27    /// Storage backend for persistence
28    storage: CompanionStorage,
29    /// File watcher service (optional - may fail to start)
30    watcher: Option<FileWatcherService>,
31    /// Channel for receiving companion events
32    event_rx: mpsc::UnboundedReceiver<CompanionEvent>,
33    /// Channel sender (held to keep channel alive)
34    _event_tx: mpsc::UnboundedSender<CompanionEvent>,
35}
36
37impl CompanionService {
38    /// Create a new companion service for the given repository
39    ///
40    /// # Errors
41    ///
42    /// Returns an error when companion storage cannot be initialized.
43    pub fn new(repo_path: PathBuf, branch: &str) -> Result<Self> {
44        let (event_tx, event_rx) = mpsc::unbounded_channel();
45
46        // Initialize storage
47        let storage = CompanionStorage::new(&repo_path)?;
48
49        // Try to load existing session or create new one.
50        // Session data should never block companion startup.
51        let session = match storage.load_session() {
52            Ok(Some(mut session)) if session.branch == branch => {
53                repo_path.clone_into(&mut session.repo_path);
54                session
55            }
56            Ok(Some(session)) => {
57                tracing::info!(
58                    "Ignoring session data for branch {} while starting on {}",
59                    session.branch,
60                    branch
61                );
62                SessionState::new(repo_path.clone(), branch.to_owned())
63            }
64            Ok(None) => SessionState::new(repo_path.clone(), branch.to_owned()),
65            Err(e) => {
66                tracing::warn!("Failed to load companion session; starting fresh: {}", e);
67                SessionState::new(repo_path.clone(), branch.to_owned())
68            }
69        };
70
71        let session = Arc::new(parking_lot::RwLock::new(session));
72
73        // Try to start file watcher (non-fatal if it fails)
74        let watcher = match FileWatcherService::new(&repo_path, event_tx.clone()) {
75            Ok(w) => {
76                tracing::info!("Companion file watcher started");
77                Some(w)
78            }
79            Err(e) => {
80                tracing::warn!(
81                    "Failed to start file watcher: {}. Companion will run without live updates.",
82                    e
83                );
84                None
85            }
86        };
87
88        Ok(Self {
89            repo_path,
90            session,
91            storage,
92            watcher,
93            event_rx,
94            _event_tx: event_tx,
95        })
96    }
97
98    /// Get the current session state
99    #[must_use]
100    pub fn session(&self) -> &Arc<parking_lot::RwLock<SessionState>> {
101        &self.session
102    }
103
104    /// Load branch memory for the given branch
105    ///
106    /// # Errors
107    ///
108    /// Returns an error when the branch memory cannot be read or parsed.
109    pub fn load_branch_memory(&self, branch: &str) -> Result<Option<BranchMemory>> {
110        self.storage.load_branch_memory(branch)
111    }
112
113    /// Save branch memory
114    ///
115    /// # Errors
116    ///
117    /// Returns an error when the branch memory cannot be serialized or written.
118    pub fn save_branch_memory(&self, memory: &BranchMemory) -> Result<()> {
119        self.storage.save_branch_memory(memory)
120    }
121
122    /// Save current session state
123    ///
124    /// # Errors
125    ///
126    /// Returns an error when the session cannot be serialized or written.
127    pub fn save_session(&self) -> Result<()> {
128        let session = self.session.read();
129        self.storage.save_session(&session)
130    }
131
132    /// Record a file touch (opened/modified)
133    pub fn touch_file(&self, path: PathBuf) {
134        let mut session = self.session.write();
135        session.touch_file(path);
136    }
137
138    /// Record a commit was made
139    pub fn record_commit(&self, hash: String) {
140        let mut session = self.session.write();
141        session.record_commit(hash);
142    }
143
144    /// Try to receive the next companion event (non-blocking)
145    pub fn try_recv_event(&mut self) -> Option<CompanionEvent> {
146        self.event_rx.try_recv().ok()
147    }
148
149    /// Check if file watcher is active
150    #[must_use]
151    pub fn has_watcher(&self) -> bool {
152        self.watcher.is_some()
153    }
154
155    /// Get repository path
156    #[must_use]
157    pub fn repo_path(&self) -> &PathBuf {
158        &self.repo_path
159    }
160}
161
162impl Drop for CompanionService {
163    fn drop(&mut self) {
164        // Try to save session on shutdown
165        if let Err(e) = self.save_session() {
166            tracing::warn!("Failed to save session on shutdown: {}", e);
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests;