Skip to main content

opencode_provider_manager/app/
actions.rs

1//! User actions that transform app state.
2
3use super::error::Result;
4use super::state::AppState;
5use crate::config_core::{ConfigLayer, OpenCodeConfig, ProviderConfig};
6use std::collections::HashMap;
7
8/// Actions the user can perform on the app state.
9impl AppState {
10    /// Add a new provider to the config at the specified layer.
11    pub fn add_provider(
12        &mut self,
13        provider_id: String,
14        config: ProviderConfig,
15        layer: ConfigLayer,
16    ) -> Result<()> {
17        let target_config = self.config_for_layer_mut(layer)?;
18        target_config
19            .provider
20            .get_or_insert_with(HashMap::new)
21            .insert(provider_id.clone(), config);
22        self.recompute_merged();
23        self.mark_dirty();
24        Ok(())
25    }
26
27    /// Remove a provider from the config at the specified layer.
28    pub fn remove_provider(&mut self, provider_id: &str, layer: ConfigLayer) -> Result<()> {
29        let target_config = self.config_for_layer_mut(layer)?;
30        if let Some(ref mut providers) = target_config.provider {
31            providers.remove(provider_id);
32        }
33        self.recompute_merged();
34        self.mark_dirty();
35        Ok(())
36    }
37
38    /// Edit a provider field in the config at the specified layer.
39    pub fn edit_provider_field(
40        &mut self,
41        provider_id: &str,
42        field: &str,
43        value: serde_json::Value,
44        layer: ConfigLayer,
45    ) -> Result<()> {
46        let target_config = self.config_for_layer_mut(layer)?;
47        if let Some(ref mut providers) = target_config.provider {
48            if let Some(provider) = providers.get_mut(provider_id) {
49                match field {
50                    "name" => {
51                        if let serde_json::Value::String(s) = value {
52                            provider.name = Some(s);
53                        }
54                    }
55                    "npm" => {
56                        if let serde_json::Value::String(s) = value {
57                            provider.npm = Some(s);
58                        }
59                    }
60                    _ => {
61                        // Store as an option
62                        provider
63                            .options
64                            .get_or_insert_with(HashMap::new)
65                            .insert(field.to_string(), value);
66                    }
67                }
68            }
69        }
70        self.recompute_merged();
71        self.mark_dirty();
72        Ok(())
73    }
74
75    /// Add a model to a provider.
76    pub fn add_model(
77        &mut self,
78        provider_id: &str,
79        model_id: String,
80        model_config: crate::config_core::ModelConfig,
81        layer: ConfigLayer,
82    ) -> Result<()> {
83        let target_config = self.config_for_layer_mut(layer)?;
84        if let Some(ref mut providers) = target_config.provider {
85            if let Some(provider) = providers.get_mut(provider_id) {
86                provider
87                    .models
88                    .get_or_insert_with(HashMap::new)
89                    .insert(model_id, model_config);
90            }
91        }
92        self.recompute_merged();
93        self.mark_dirty();
94        Ok(())
95    }
96
97    /// Remove a model from a provider.
98    pub fn remove_model(
99        &mut self,
100        provider_id: &str,
101        model_id: &str,
102        layer: ConfigLayer,
103    ) -> Result<()> {
104        let target_config = self.config_for_layer_mut(layer)?;
105        if let Some(ref mut providers) = target_config.provider {
106            if let Some(provider) = providers.get_mut(provider_id) {
107                if let Some(ref mut models) = provider.models {
108                    models.remove(model_id);
109                }
110            }
111        }
112        self.recompute_merged();
113        self.mark_dirty();
114        Ok(())
115    }
116
117    /// Save the config at the specified layer to disk.
118    ///
119    /// For the Project layer, falls back to `./opencode.json` in the current
120    /// directory when no project file was discovered, so that new project
121    /// configs can be created.
122    pub fn save(&mut self, layer: ConfigLayer) -> Result<()> {
123        // Resolve save path. For Project, fall back to ./opencode.json if none
124        // was discovered so first-time project config creation works.
125        let path_buf = match layer {
126            ConfigLayer::Project => match self.paths.project.clone() {
127                Some(p) => p,
128                None => {
129                    let cwd = std::env::current_dir().map_err(|e| {
130                        super::error::AppError::State(format!("Cannot read cwd: {e}"))
131                    })?;
132                    let fallback = cwd.join("opencode.json");
133                    self.paths.project = Some(fallback.clone());
134                    fallback
135                }
136            },
137            other => self.paths.path_for_layer(other).cloned().ok_or_else(|| {
138                super::error::AppError::State(format!("No config path for layer {other:?}"))
139            })?,
140        };
141
142        let config = self.config_for_layer(layer)?;
143        crate::config_core::jsonc::write_config(config, &path_buf)?;
144        self.dirty = false;
145        Ok(())
146    }
147
148    // --- Private helpers ---
149
150    fn config_for_layer(&self, layer: ConfigLayer) -> Result<&OpenCodeConfig> {
151        match layer {
152            ConfigLayer::Global => self.global_config.as_ref().ok_or_else(|| {
153                super::error::AppError::State("No global config loaded".to_string())
154            }),
155            ConfigLayer::Project => self.project_config.as_ref().ok_or_else(|| {
156                super::error::AppError::State("No project config loaded".to_string())
157            }),
158            ConfigLayer::Custom => self.custom_config.as_ref().ok_or_else(|| {
159                super::error::AppError::State("No custom config loaded".to_string())
160            }),
161        }
162    }
163
164    fn config_for_layer_mut(&mut self, layer: ConfigLayer) -> Result<&mut OpenCodeConfig> {
165        match layer {
166            ConfigLayer::Global => {
167                if self.global_config.is_none() {
168                    self.global_config = Some(OpenCodeConfig::default());
169                }
170                Ok(self.global_config.as_mut().unwrap())
171            }
172            ConfigLayer::Project => {
173                if self.project_config.is_none() {
174                    self.project_config = Some(OpenCodeConfig::default());
175                }
176                Ok(self.project_config.as_mut().unwrap())
177            }
178            ConfigLayer::Custom => {
179                if self.custom_config.is_none() {
180                    self.custom_config = Some(OpenCodeConfig::default());
181                }
182                Ok(self.custom_config.as_mut().unwrap())
183            }
184        }
185    }
186
187    pub fn recompute_merged(&mut self) {
188        let mut configs = Vec::new();
189        if let Some(global) = &self.global_config {
190            configs.push(global.clone());
191        }
192        if let Some(custom) = &self.custom_config {
193            configs.push(custom.clone());
194        }
195        if let Some(project) = &self.project_config {
196            configs.push(project.clone());
197        }
198        self.merged_config = crate::config_core::merge_configs(&configs);
199    }
200}