ricecoder_storage/markdown_config/integration/
modes.rs

1//! Integration with ricecoder-modes for markdown-based mode configuration
2
3use crate::markdown_config::error::MarkdownConfigResult;
4use crate::markdown_config::loader::{ConfigFile, ConfigFileType, ConfigurationLoader};
5use crate::markdown_config::types::ModeConfig;
6use std::path::PathBuf;
7use std::sync::Arc;
8use tracing::{debug, info, warn};
9
10/// Type alias for registration results: (success_count, error_count, errors)
11pub type RegistrationResult = (usize, usize, Vec<(String, String)>);
12
13/// Trait for registering mode configurations
14///
15/// This trait allows ricecoder-storage to register mode configurations without
16/// directly depending on ricecoder-modes, avoiding circular dependencies.
17pub trait ModeRegistrar: Send + Sync {
18    /// Register a mode configuration
19    fn register_mode(&mut self, mode: ModeConfig) -> Result<(), String>;
20}
21
22/// Integration layer for mode configuration with ricecoder-modes
23///
24/// This struct provides methods to discover, load, and register mode configurations
25/// from markdown files with the ricecoder-modes subsystem.
26pub struct ModeConfigIntegration {
27    loader: Arc<ConfigurationLoader>,
28}
29
30impl ModeConfigIntegration {
31    /// Create a new mode configuration integration
32    pub fn new(loader: Arc<ConfigurationLoader>) -> Self {
33        Self { loader }
34    }
35
36    /// Discover mode configuration files in the given paths
37    ///
38    /// # Arguments
39    /// * `paths` - Directories to search for mode markdown files
40    ///
41    /// # Returns
42    /// A vector of discovered mode configuration files
43    pub fn discover_mode_configs(&self, paths: &[PathBuf]) -> MarkdownConfigResult<Vec<ConfigFile>> {
44        let all_files = self.loader.discover(paths)?;
45
46        // Filter to only mode configuration files
47        let mode_files: Vec<ConfigFile> = all_files
48            .into_iter()
49            .filter(|f| f.config_type == ConfigFileType::Mode)
50            .collect();
51
52        debug!("Discovered {} mode configuration files", mode_files.len());
53        Ok(mode_files)
54    }
55
56    /// Load mode configurations from markdown files
57    ///
58    /// # Arguments
59    /// * `paths` - Directories to search for mode markdown files
60    ///
61    /// # Returns
62    /// A tuple of (loaded_modes, errors)
63    pub async fn load_mode_configs(
64        &self,
65        paths: &[PathBuf],
66    ) -> MarkdownConfigResult<(Vec<ModeConfig>, Vec<(PathBuf, String)>)> {
67        let files = self.discover_mode_configs(paths)?;
68
69        let mut modes = Vec::new();
70        let mut errors = Vec::new();
71
72        for file in files {
73            match self.loader.load(&file).await {
74                Ok(config) => {
75                    match config {
76                        crate::markdown_config::loader::LoadedConfig::Mode(mode) => {
77                            debug!("Loaded mode configuration: {}", mode.name);
78                            modes.push(mode);
79                        }
80                        _ => {
81                            warn!("Expected mode configuration but got different type from {}", file.path.display());
82                            errors.push((
83                                file.path,
84                                "Expected mode configuration but got different type".to_string(),
85                            ));
86                        }
87                    }
88                }
89                Err(e) => {
90                    let error_msg = e.to_string();
91                    warn!("Failed to load mode configuration from {}: {}", file.path.display(), error_msg);
92                    errors.push((file.path, error_msg));
93                }
94            }
95        }
96
97        info!("Loaded {} mode configurations", modes.len());
98        Ok((modes, errors))
99    }
100
101    /// Register mode configurations with a registrar
102    ///
103    /// This method registers mode configurations using a generic registrar trait,
104    /// allowing integration with any mode manager implementation.
105    ///
106    /// # Arguments
107    /// * `modes` - Mode configurations to register
108    /// * `registrar` - The mode registrar to register with
109    ///
110    /// # Returns
111    /// A tuple of (successful_count, error_count, errors)
112    pub fn register_modes(
113        &self,
114        modes: Vec<ModeConfig>,
115        registrar: &mut dyn ModeRegistrar,
116    ) -> MarkdownConfigResult<RegistrationResult> {
117        let mut success_count = 0;
118        let mut error_count = 0;
119        let mut errors = Vec::new();
120
121        for mode in modes {
122            // Validate mode configuration
123            if let Err(e) = mode.validate() {
124                error_count += 1;
125                let error_msg = format!("Invalid mode configuration: {}", e);
126                warn!("Failed to register mode '{}': {}", mode.name, error_msg);
127                errors.push((mode.name.clone(), error_msg));
128                continue;
129            }
130
131            debug!("Registering mode: {}", mode.name);
132
133            // Register the mode using the registrar
134            match registrar.register_mode(mode.clone()) {
135                Ok(_) => {
136                    success_count += 1;
137                    info!("Registered mode: {}", mode.name);
138                }
139                Err(e) => {
140                    error_count += 1;
141                    warn!("Failed to register mode '{}': {}", mode.name, e);
142                    errors.push((mode.name.clone(), e));
143                }
144            }
145        }
146
147        debug!(
148            "Mode registration complete: {} successful, {} failed",
149            success_count, error_count
150        );
151
152        Ok((success_count, error_count, errors))
153    }
154
155    /// Load and register mode configurations in one operation
156    ///
157    /// # Arguments
158    /// * `paths` - Directories to search for mode markdown files
159    /// * `registrar` - The mode registrar to register with
160    ///
161    /// # Returns
162    /// A tuple of (successful_count, error_count, errors)
163    pub async fn load_and_register_modes(
164        &self,
165        paths: &[PathBuf],
166        registrar: &mut dyn ModeRegistrar,
167    ) -> MarkdownConfigResult<(usize, usize, Vec<(String, String)>)> {
168        let (modes, load_errors) = self.load_mode_configs(paths).await?;
169
170        let (success, errors, mut reg_errors) = self.register_modes(modes, registrar)?;
171
172        // Combine load and registration errors
173        for (path, msg) in load_errors {
174            reg_errors.push((path.display().to_string(), msg));
175        }
176
177        Ok((success, errors, reg_errors))
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::markdown_config::registry::ConfigRegistry;
185    use std::fs;
186    use tempfile::TempDir;
187
188    fn create_test_mode_file(dir: &PathBuf, name: &str, content: &str) -> PathBuf {
189        let path = dir.join(format!("{}.mode.md", name));
190        fs::write(&path, content).unwrap();
191        path
192    }
193
194    #[test]
195    fn test_discover_mode_configs() {
196        let temp_dir = TempDir::new().unwrap();
197        let dir_path = temp_dir.path().to_path_buf();
198
199        // Create test mode files
200        create_test_mode_file(&dir_path, "mode1", "---\nname: mode1\n---\nTest");
201        create_test_mode_file(&dir_path, "mode2", "---\nname: mode2\n---\nTest");
202
203        // Create a non-mode file
204        fs::write(dir_path.join("agent1.agent.md"), "---\nname: agent1\n---\nTest").unwrap();
205
206        let registry = Arc::new(ConfigRegistry::new());
207        let loader = Arc::new(ConfigurationLoader::new(registry));
208        let integration = ModeConfigIntegration::new(loader);
209
210        let discovered = integration.discover_mode_configs(&[dir_path]).unwrap();
211
212        assert_eq!(discovered.len(), 2);
213        assert!(discovered.iter().all(|f| f.config_type == ConfigFileType::Mode));
214    }
215
216    #[tokio::test]
217    async fn test_load_mode_configs() {
218        let temp_dir = TempDir::new().unwrap();
219        let dir_path = temp_dir.path().to_path_buf();
220
221        let mode_content = r#"---
222name: focus-mode
223description: Focus mode
224keybinding: C-f
225enabled: true
226---
227Focus on the task"#;
228
229        create_test_mode_file(&dir_path, "focus-mode", mode_content);
230
231        let registry = Arc::new(ConfigRegistry::new());
232        let loader = Arc::new(ConfigurationLoader::new(registry));
233        let integration = ModeConfigIntegration::new(loader);
234
235        let (modes, errors) = integration.load_mode_configs(&[dir_path]).await.unwrap();
236
237        assert_eq!(modes.len(), 1);
238        assert_eq!(errors.len(), 0);
239        assert_eq!(modes[0].name, "focus-mode");
240        assert_eq!(modes[0].keybinding, Some("C-f".to_string()));
241    }
242
243    #[tokio::test]
244    async fn test_load_mode_configs_with_errors() {
245        let temp_dir = TempDir::new().unwrap();
246        let dir_path = temp_dir.path().to_path_buf();
247
248        // Create a valid mode file
249        let valid_content = r#"---
250name: valid-mode
251---
252Valid mode"#;
253        create_test_mode_file(&dir_path, "valid-mode", valid_content);
254
255        // Create an invalid mode file (missing frontmatter)
256        fs::write(dir_path.join("invalid.mode.md"), "# No frontmatter\nJust markdown").unwrap();
257
258        let registry = Arc::new(ConfigRegistry::new());
259        let loader = Arc::new(ConfigurationLoader::new(registry));
260        let integration = ModeConfigIntegration::new(loader);
261
262        let (modes, errors) = integration.load_mode_configs(&[dir_path]).await.unwrap();
263
264        assert_eq!(modes.len(), 1);
265        assert_eq!(errors.len(), 1);
266        assert_eq!(modes[0].name, "valid-mode");
267    }
268
269    #[test]
270    fn test_register_with_mode_manager() {
271        let registry = Arc::new(ConfigRegistry::new());
272        let loader = Arc::new(ConfigurationLoader::new(registry));
273        let integration = ModeConfigIntegration::new(loader);
274
275        let modes = vec![
276            ModeConfig {
277                name: "mode1".to_string(),
278                description: Some("Test mode 1".to_string()),
279                prompt: "You are mode 1".to_string(),
280                keybinding: Some("C-m".to_string()),
281                enabled: true,
282            },
283            ModeConfig {
284                name: "mode2".to_string(),
285                description: Some("Test mode 2".to_string()),
286                prompt: "You are mode 2".to_string(),
287                keybinding: None,
288                enabled: false,
289            },
290        ];
291
292        struct MockRegistrar;
293        impl ModeRegistrar for MockRegistrar {
294            fn register_mode(&mut self, _mode: ModeConfig) -> Result<(), String> {
295                Ok(())
296            }
297        }
298
299        let mut registrar = MockRegistrar;
300        let (success, errors, error_list) = integration
301            .register_modes(modes, &mut registrar)
302            .unwrap();
303
304        assert_eq!(success, 2);
305        assert_eq!(errors, 0);
306        assert_eq!(error_list.len(), 0);
307    }
308
309    #[test]
310    fn test_register_invalid_mode() {
311        let registry = Arc::new(ConfigRegistry::new());
312        let loader = Arc::new(ConfigurationLoader::new(registry));
313        let integration = ModeConfigIntegration::new(loader);
314
315        let modes = vec![
316            ModeConfig {
317                name: String::new(), // Invalid: empty name
318                description: None,
319                prompt: "Test".to_string(),
320                keybinding: None,
321                enabled: true,
322            },
323        ];
324
325        struct MockRegistrar;
326        impl ModeRegistrar for MockRegistrar {
327            fn register_mode(&mut self, _mode: ModeConfig) -> Result<(), String> {
328                Ok(())
329            }
330        }
331
332        let mut registrar = MockRegistrar;
333        let (success, errors, error_list) = integration
334            .register_modes(modes, &mut registrar)
335            .unwrap();
336
337        assert_eq!(success, 0);
338        assert_eq!(errors, 1);
339        assert_eq!(error_list.len(), 1);
340    }
341}