gestura_core_knowledge/
session_settings.rs1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::collections::HashSet;
8use std::fs;
9use std::path::PathBuf;
10use std::sync::RwLock;
11
12pub const DEFAULT_KNOWLEDGE_SETTINGS_SESSION_ID: &str = "__default__";
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SessionKnowledgeSettings {
21 pub session_id: String,
23 pub enabled_knowledge: HashSet<String>,
25}
26
27impl SessionKnowledgeSettings {
28 pub fn new(session_id: String) -> Self {
30 Self {
31 session_id,
32 enabled_knowledge: HashSet::new(),
33 }
34 }
35
36 pub fn enable(&mut self, knowledge_id: String) {
38 self.enabled_knowledge.insert(knowledge_id);
39 }
40
41 pub fn disable(&mut self, knowledge_id: &str) {
43 self.enabled_knowledge.remove(knowledge_id);
44 }
45
46 pub fn is_enabled(&self, knowledge_id: &str) -> bool {
48 self.enabled_knowledge.contains(knowledge_id)
49 }
50}
51
52pub struct KnowledgeSettingsManager {
54 base_dir: PathBuf,
56 cache: RwLock<HashMap<String, SessionKnowledgeSettings>>,
58}
59
60impl KnowledgeSettingsManager {
61 pub fn new(base_dir: PathBuf) -> Self {
63 Self {
64 base_dir,
65 cache: RwLock::new(HashMap::new()),
66 }
67 }
68
69 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 pub fn load(&self, session_id: &str) -> Result<SessionKnowledgeSettings, std::io::Error> {
79 {
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 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 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 {
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 pub fn save(&self, settings: &SessionKnowledgeSettings) -> Result<(), std::io::Error> {
128 let path = self.settings_path(&settings.session_id);
129
130 if let Some(parent) = path.parent() {
132 fs::create_dir_all(parent)?;
133 }
134
135 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 {
142 let mut cache = self.cache.write().unwrap();
143 cache.insert(settings.session_id.clone(), settings.clone());
144 }
145
146 Ok(())
147 }
148
149 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 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 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 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 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 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}