Skip to main content

opencode_provider_manager/app/
state.rs

1//! Application state management.
2
3use crate::config_core::{ConfigLayer, ConfigPaths, OpenCodeConfig, ProviderConfig};
4
5/// The current UI state.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum AppMode {
8    /// Viewing the merged config.
9    MergedView,
10    /// Viewing global and project side by side.
11    SplitView,
12    /// Adding a new provider (wizard).
13    AddProvider,
14    /// Editing an existing provider.
15    EditProvider { provider_id: String },
16    /// Selecting models for a provider.
17    ModelSelector { provider_id: String },
18    /// Viewing auth status.
19    AuthStatus,
20    /// Importing config.
21    Import,
22}
23
24/// The application state.
25#[derive(Debug)]
26pub struct AppState {
27    /// Loaded global config.
28    pub global_config: Option<OpenCodeConfig>,
29    /// Loaded project config.
30    pub project_config: Option<OpenCodeConfig>,
31    /// Loaded custom config (from --config / OPENCODE_CONFIG).
32    pub custom_config: Option<OpenCodeConfig>,
33    /// Merged config (global + custom + project, per documented precedence).
34    pub merged_config: OpenCodeConfig,
35    /// Resolved config paths.
36    pub paths: ConfigPaths,
37    /// Current UI state.
38    pub mode: AppMode,
39    /// Whether any config has unsaved changes.
40    pub dirty: bool,
41    /// Currently selected layer for edits.
42    pub edit_layer: ConfigLayer,
43}
44
45impl AppState {
46    /// Create a new app state by discovering config paths.
47    pub fn new() -> crate::config_core::Result<Self> {
48        let paths = ConfigPaths::discover()?;
49        Ok(Self {
50            global_config: None,
51            project_config: None,
52            custom_config: None,
53            merged_config: OpenCodeConfig::default(),
54            paths,
55            mode: AppMode::MergedView,
56            dirty: false,
57            edit_layer: ConfigLayer::Project,
58        })
59    }
60
61    /// Load all config layers and merge them.
62    ///
63    /// Merge order follows the documented OpenCode precedence:
64    /// global < custom (OPENCODE_CONFIG) < project.
65    pub fn load_configs(&mut self) -> crate::config_core::Result<()> {
66        // Load global config
67        self.global_config = if self.paths.global.exists() {
68            Some(crate::config_core::jsonc::read_config(&self.paths.global)?)
69        } else {
70            None
71        };
72
73        // Load custom config (from --config or OPENCODE_CONFIG)
74        self.custom_config = if let Some(ref custom_path) = self.paths.custom {
75            if custom_path.exists() {
76                Some(crate::config_core::jsonc::read_config(custom_path)?)
77            } else {
78                None
79            }
80        } else {
81            None
82        };
83
84        // Load project config
85        self.project_config = if let Some(ref project_path) = self.paths.project {
86            if project_path.exists() {
87                Some(crate::config_core::jsonc::read_config(project_path)?)
88            } else {
89                None
90            }
91        } else {
92            None
93        };
94
95        // Merge in documented order: global < custom < project
96        let mut configs_to_merge = Vec::new();
97        if let Some(global) = &self.global_config {
98            configs_to_merge.push(global.clone());
99        }
100        if let Some(custom) = &self.custom_config {
101            configs_to_merge.push(custom.clone());
102        }
103        if let Some(project) = &self.project_config {
104            configs_to_merge.push(project.clone());
105        }
106
107        self.merged_config = crate::config_core::merge_configs(&configs_to_merge);
108        self.dirty = false;
109
110        Ok(())
111    }
112
113    /// Get a provider from the merged config.
114    pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderConfig> {
115        self.merged_config.provider.as_ref()?.get(provider_id)
116    }
117
118    /// Get the list of configured provider IDs.
119    pub fn provider_ids(&self) -> Vec<String> {
120        self.merged_config
121            .provider
122            .as_ref()
123            .map(|p| p.keys().cloned().collect())
124            .unwrap_or_default()
125    }
126
127    /// Mark state as dirty (unsaved changes).
128    pub fn mark_dirty(&mut self) {
129        self.dirty = true;
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_app_state_creation() {
139        let state = AppState::new();
140        assert!(state.is_ok());
141        let state = state.unwrap();
142        assert_eq!(state.mode, AppMode::MergedView);
143        assert!(!state.dirty);
144    }
145
146    #[test]
147    fn test_app_state_default_mode() {
148        let state = AppState::new().unwrap();
149        assert!(matches!(state.mode, AppMode::MergedView));
150    }
151
152    #[test]
153    fn test_provider_ids_empty() {
154        let state = AppState::new().unwrap();
155        assert!(state.provider_ids().is_empty());
156    }
157}