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    pub fn new(repo_path: PathBuf, branch: &str) -> Result<Self> {
40        let (event_tx, event_rx) = mpsc::unbounded_channel();
41
42        // Initialize storage
43        let storage = CompanionStorage::new(&repo_path)?;
44
45        // Try to load existing session or create new one
46        let session = storage
47            .load_session()?
48            .filter(|s| s.branch == branch) // Only restore if same branch
49            .unwrap_or_else(|| SessionState::new(repo_path.clone(), branch.to_owned()));
50
51        let session = Arc::new(parking_lot::RwLock::new(session));
52
53        // Try to start file watcher (non-fatal if it fails)
54        let watcher = match FileWatcherService::new(&repo_path, event_tx.clone()) {
55            Ok(w) => {
56                tracing::info!("Companion file watcher started");
57                Some(w)
58            }
59            Err(e) => {
60                tracing::warn!(
61                    "Failed to start file watcher: {}. Companion will run without live updates.",
62                    e
63                );
64                None
65            }
66        };
67
68        Ok(Self {
69            repo_path,
70            session,
71            storage,
72            watcher,
73            event_rx,
74            _event_tx: event_tx,
75        })
76    }
77
78    /// Get the current session state
79    pub fn session(&self) -> &Arc<parking_lot::RwLock<SessionState>> {
80        &self.session
81    }
82
83    /// Load branch memory for the given branch
84    pub fn load_branch_memory(&self, branch: &str) -> Result<Option<BranchMemory>> {
85        self.storage.load_branch_memory(branch)
86    }
87
88    /// Save branch memory
89    pub fn save_branch_memory(&self, memory: &BranchMemory) -> Result<()> {
90        self.storage.save_branch_memory(memory)
91    }
92
93    /// Save current session state
94    pub fn save_session(&self) -> Result<()> {
95        let session = self.session.read();
96        self.storage.save_session(&session)
97    }
98
99    /// Record a file touch (opened/modified)
100    pub fn touch_file(&self, path: PathBuf) {
101        let mut session = self.session.write();
102        session.touch_file(path);
103    }
104
105    /// Record a commit was made
106    pub fn record_commit(&self, hash: String) {
107        let mut session = self.session.write();
108        session.record_commit(hash);
109    }
110
111    /// Try to receive the next companion event (non-blocking)
112    pub fn try_recv_event(&mut self) -> Option<CompanionEvent> {
113        self.event_rx.try_recv().ok()
114    }
115
116    /// Check if file watcher is active
117    pub fn has_watcher(&self) -> bool {
118        self.watcher.is_some()
119    }
120
121    /// Get repository path
122    pub fn repo_path(&self) -> &PathBuf {
123        &self.repo_path
124    }
125}
126
127impl Drop for CompanionService {
128    fn drop(&mut self) {
129        // Try to save session on shutdown
130        if let Err(e) = self.save_session() {
131            tracing::warn!("Failed to save session on shutdown: {}", e);
132        }
133    }
134}