gestura_core_knowledge/
session_settings.rs

1//! Session-scoped knowledge settings
2//!
3//! Manages which knowledge items are enabled for each session.
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::fs;
9use std::path::PathBuf;
10use std::sync::RwLock;
11
12/// Pseudo-session ID used to store *default* knowledge enablement.
13///
14/// When a real session has no settings file yet, its enabled knowledge set will
15/// fall back to this default settings file.
16pub const DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID: &str = "__default__";
17
18/// Session-scoped knowledge settings
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SessionKnowledgeSettings {
21    /// Session ID
22    pub session_id: String,
23    /// Set of enabled knowledge item IDs
24    pub enabled_knowledge: HashSet<String>,
25}
26
27impl SessionKnowledgeSettings {
28    /// Create new settings for a session
29    pub fn new(session_id: String) -> Self {
30        Self {
31            session_id,
32            enabled_knowledge: HashSet::new(),
33        }
34    }
35
36    /// Enable a knowledge item
37    pub fn enable(&mut self, knowledge_id: String) {
38        self.enabled_knowledge.insert(knowledge_id);
39    }
40
41    /// Disable a knowledge item
42    pub fn disable(&mut self, knowledge_id: &str) {
43        self.enabled_knowledge.remove(knowledge_id);
44    }
45
46    /// Check if a knowledge item is enabled
47    pub fn is_enabled(&self, knowledge_id: &str) -> bool {
48        self.enabled_knowledge.contains(knowledge_id)
49    }
50}
51
52/// Manager for session-scoped knowledge settings
53pub struct KnowledgeSettingsManager {
54    /// Base directory for settings files
55    base_dir: PathBuf,
56    /// In-memory cache of settings
57    cache: RwLock<HashMap<String, SessionKnowledgeSettings>>,
58}
59
60impl KnowledgeSettingsManager {
61    /// Create a new manager with the given base directory
62    pub fn new(base_dir: PathBuf) -> Self {
63        Self {
64            base_dir,
65            cache: RwLock::new(HashMap::new()),
66        }
67    }
68
69    /// Get the settings file path for a session
70    fn settings_path(&self, session_id: &str) -> PathBuf {
71        self.base_dir
72            .join(".gestura")
73            .join("knowledge_settings")
74            .join(format!("{}.json", session_id))
75    }
76
77    /// Load settings for a session
78    pub fn load(&self, session_id: &str) -> Result<SessionKnowledgeSettings, std::io::Error> {
79        // Check cache first
80        {
81            let cache = self.cache.read().unwrap();
82            if let Some(settings) = cache.get(session_id) {
83                return Ok(settings.clone());
84            }
85        }
86
87        // Load from file (or synthesize from defaults if missing)
88        let path = self.settings_path(session_id);
89
90        let settings = if path.exists() {
91            let content = fs::read_to_string(&path)?;
92            serde_json::from_str::<SessionKnowledgeSettings>(&content)
93                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?
94        } else if session_id == DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID {
95            SessionKnowledgeSettings::new(session_id.to_string())
96        } else {
97            // Fall back to defaults for brand-new sessions.
98            let defaults = self.load_defaults_no_cache()?;
99            let mut s = SessionKnowledgeSettings::new(session_id.to_string());
100            s.enabled_knowledge = defaults.enabled_knowledge;
101            s
102        };
103
104        // Update cache
105        {
106            let mut cache = self.cache.write().unwrap();
107            cache.insert(session_id.to_string(), settings.clone());
108        }
109
110        Ok(settings)
111    }
112
113    fn load_defaults_no_cache(&self) -> Result<SessionKnowledgeSettings, std::io::Error> {
114        let path = self.settings_path(DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID);
115        if !path.exists() {
116            return Ok(SessionKnowledgeSettings::new(
117                DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID.to_string(),
118            ));
119        }
120
121        let content = fs::read_to_string(&path)?;
122        serde_json::from_str::<SessionKnowledgeSettings>(&content)
123            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
124    }
125
126    /// Save settings for a session
127    pub fn save(&self, settings: &SessionKnowledgeSettings) -> Result<(), std::io::Error> {
128        let path = self.settings_path(&settings.session_id);
129
130        // Ensure directory exists
131        if let Some(parent) = path.parent() {
132            fs::create_dir_all(parent)?;
133        }
134
135        // Save to file
136        let content = serde_json::to_string_pretty(settings)
137            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
138        fs::write(&path, content)?;
139
140        // Update cache
141        {
142            let mut cache = self.cache.write().unwrap();
143            cache.insert(settings.session_id.clone(), settings.clone());
144        }
145
146        Ok(())
147    }
148
149    /// Set knowledge enabled/disabled for a session
150    pub fn set_knowledge_enabled(
151        &self,
152        session_id: &str,
153        knowledge_id: &str,
154        enabled: bool,
155    ) -> Result<(), std::io::Error> {
156        let mut settings = self.load(session_id)?;
157
158        if enabled {
159            settings.enable(knowledge_id.to_string());
160        } else {
161            settings.disable(knowledge_id);
162        }
163
164        self.save(&settings)
165    }
166
167    /// Get list of enabled knowledge IDs for a session
168    pub fn get_enabled_knowledge(&self, session_id: &str) -> Result<Vec<String>, std::io::Error> {
169        let settings = self.load(session_id)?;
170        let mut out: Vec<String> = settings.enabled_knowledge.into_iter().collect();
171        out.sort();
172        Ok(out)
173    }
174
175    /// Check if a knowledge item is enabled for a session
176    pub fn is_enabled(&self, session_id: &str, knowledge_id: &str) -> Result<bool, std::io::Error> {
177        let settings = self.load(session_id)?;
178        Ok(settings.is_enabled(knowledge_id))
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use tempfile::tempdir;
186
187    #[test]
188    fn load_missing_session_falls_back_to_defaults() {
189        let tmp = tempdir().unwrap();
190        let mgr = KnowledgeSettingsManager::new(tmp.path().to_path_buf());
191
192        // Save defaults
193        let mut defaults =
194            SessionKnowledgeSettings::new(DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID.to_string());
195        defaults.enable("rust-expert".to_string());
196        defaults.enable("tauri-expert".to_string());
197        mgr.save(&defaults).unwrap();
198
199        // Missing session should inherit defaults
200        let s1 = mgr.load("session-1").unwrap();
201        assert!(s1.enabled_knowledge.contains("rust-expert"));
202        assert!(s1.enabled_knowledge.contains("tauri-expert"));
203    }
204
205    #[test]
206    fn load_existing_session_does_not_merge_defaults() {
207        let tmp = tempdir().unwrap();
208        let mgr = KnowledgeSettingsManager::new(tmp.path().to_path_buf());
209
210        let mut defaults =
211            SessionKnowledgeSettings::new(DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID.to_string());
212        defaults.enable("rust-expert".to_string());
213        mgr.save(&defaults).unwrap();
214
215        // Explicit session settings should be authoritative.
216        let mut s1 = SessionKnowledgeSettings::new("session-1".to_string());
217        s1.enable("mcp-expert".to_string());
218        mgr.save(&s1).unwrap();
219
220        let loaded = mgr.load("session-1").unwrap();
221        assert!(loaded.enabled_knowledge.contains("mcp-expert"));
222        assert!(!loaded.enabled_knowledge.contains("rust-expert"));
223    }
224}