1use crate::ai::{
2 model::ModelCost,
3 types::{ModelSettings, ReasoningBudget},
4};
5use crate::modules::execution::config::RunBuildTestOutputMode;
6use schemars::JsonSchema;
7use serde::de::DeserializeOwned;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, JsonSchema)]
13pub enum FileModificationApi {
14 #[default]
15 Default,
16 Patch,
17 FindReplace,
18 ClineSearchReplace,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
22pub enum ReviewLevel {
23 #[default]
24 None,
25 Task,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
29pub enum SpawnContextMode {
30 #[default]
31 Fork,
32 Fresh,
33}
34
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
36#[serde(rename_all = "snake_case")]
37pub enum ToolCallStyle {
38 Xml,
39 #[default]
40 Json,
41}
42
43#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
44#[serde(rename_all = "snake_case")]
45pub enum CommunicationTone {
46 #[default]
47 ConciseAndLogical,
48 WarmAndFlowy,
49 Cat,
50 Meme,
51}
52
53#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
54#[serde(rename_all = "snake_case")]
55pub enum AutonomyLevel {
56 FullyAutonomous,
58 #[default]
60 PlanApprovalRequired,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type")]
65pub enum TtsProviderConfig {
66 #[serde(rename = "aws_polly")]
67 AwsPolly {
68 #[serde(default)]
69 profile: Option<String>,
70 #[serde(default = "default_region")]
71 region: String,
72 },
73 #[serde(rename = "elevenlabs")]
74 ElevenLabs {
75 api_key: String,
76 #[serde(default)]
77 voice_id: Option<String>,
78 #[serde(default)]
79 model_id: Option<String>,
80 },
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(tag = "type")]
85pub enum SttProviderConfig {
86 #[serde(rename = "aws_transcribe")]
87 AwsTranscribe {
88 #[serde(default)]
89 profile: Option<String>,
90 #[serde(default = "default_region")]
91 region: String,
92 },
93 #[serde(rename = "elevenlabs")]
94 ElevenLabs {
95 api_key: String,
96 #[serde(default)]
97 model_id: Option<String>,
98 },
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct VoiceSettings {
103 #[serde(default)]
104 pub default_tts: Option<String>,
105
106 #[serde(default)]
107 pub default_stt: Option<String>,
108
109 #[serde(default)]
110 pub tts_providers: HashMap<String, TtsProviderConfig>,
111
112 #[serde(default)]
113 pub stt_providers: HashMap<String, SttProviderConfig>,
114}
115
116impl Default for VoiceSettings {
117 fn default() -> Self {
118 Self {
119 default_tts: None,
120 default_stt: None,
121 tts_providers: HashMap::new(),
122 stt_providers: HashMap::new(),
123 }
124 }
125}
126
127impl VoiceSettings {
128 pub fn active_tts(&self) -> Option<&TtsProviderConfig> {
129 let name = self.default_tts.as_ref()?;
130 self.tts_providers.get(name)
131 }
132
133 pub fn active_stt(&self) -> Option<&SttProviderConfig> {
134 let name = self.default_stt.as_ref()?;
135 self.stt_providers.get(name)
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
141pub struct SkillsConfig {
142 #[serde(default = "default_skills_enabled")]
144 pub enabled: bool,
145
146 #[serde(default)]
148 pub disabled_skills: HashSet<String>,
149
150 #[serde(default)]
152 pub additional_dirs: Vec<PathBuf>,
153
154 #[serde(default = "default_claude_code_compat")]
156 pub enable_claude_code_compat: bool,
157}
158
159fn default_skills_enabled() -> bool {
160 true
161}
162
163fn default_claude_code_compat() -> bool {
164 true
165}
166
167impl Default for SkillsConfig {
168 fn default() -> Self {
169 Self {
170 enabled: default_skills_enabled(),
171 disabled_skills: HashSet::new(),
172 additional_dirs: Vec::new(),
173 enable_claude_code_compat: default_claude_code_compat(),
174 }
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct Settings {
188 #[serde(default)]
190 pub active_provider: Option<String>,
191
192 #[serde(default)]
194 pub providers: HashMap<String, ProviderConfig>,
195
196 #[serde(default)]
198 pub agent_models: HashMap<String, ModelSettings>,
199
200 #[serde(default = "default_agent_name")]
202 pub default_agent: String,
203
204 #[serde(default)]
206 pub model_quality: Option<ModelCost>,
207
208 #[serde(default)]
210 pub review_level: ReviewLevel,
211
212 #[serde(default)]
214 pub mcp_servers: HashMap<String, McpServerConfig>,
215
216 #[serde(default)]
218 pub run_build_test_output_mode: RunBuildTestOutputMode,
219
220 #[serde(default)]
222 pub enable_type_analyzer: bool,
223
224 #[serde(default)]
226 pub spawn_context_mode: SpawnContextMode,
227
228 #[serde(default)]
230 pub xml_tool_mode: bool,
231
232 #[serde(default)]
234 pub disable_custom_steering: bool,
235
236 #[serde(default)]
238 pub communication_tone: CommunicationTone,
239
240 #[serde(default)]
242 pub autonomy_level: AutonomyLevel,
243
244 #[serde(default)]
246 pub voice: VoiceSettings,
247
248 #[serde(default)]
250 pub skills: SkillsConfig,
251
252 #[serde(default)]
254 pub reasoning_effort: Option<ReasoningBudget>,
255
256 #[serde(default)]
258 pub disable_streaming: bool,
259
260 #[serde(default)]
263 pub modules: HashMap<String, serde_json::Value>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct McpServerConfig {
268 pub command: String,
270
271 #[serde(default)]
273 pub args: Vec<String>,
274
275 #[serde(default)]
277 pub env: HashMap<String, String>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(tag = "type")]
282pub enum ProviderConfig {
283 #[serde(rename = "bedrock")]
284 Bedrock {
285 profile: String,
286 #[serde(default = "default_region")]
287 region: String,
288 },
289 #[serde(rename = "mock")]
290 Mock {
291 #[serde(default)]
292 behavior: crate::ai::mock::MockBehavior,
293 },
294 #[serde(rename = "openrouter")]
295 OpenRouter { api_key: String },
296 #[serde(rename = "claude_code")]
297 ClaudeCode {
298 #[serde(default = "default_claude_command")]
299 command: String,
300 #[serde(default)]
301 extra_args: Vec<String>,
302 #[serde(default)]
303 env: HashMap<String, String>,
304 },
305}
306
307fn default_region() -> String {
308 "us-west-2".to_string()
309}
310
311fn default_claude_command() -> String {
312 "claude".to_string()
313}
314
315fn default_agent_name() -> String {
316 "one_shot".to_string()
317}
318
319impl Default for Settings {
320 fn default() -> Self {
321 Self {
322 active_provider: None,
323 providers: HashMap::new(),
324 agent_models: HashMap::new(),
325 default_agent: default_agent_name(),
326 model_quality: None,
327 review_level: ReviewLevel::None,
328 mcp_servers: HashMap::new(),
329 run_build_test_output_mode: RunBuildTestOutputMode::default(),
330 enable_type_analyzer: false,
331 spawn_context_mode: SpawnContextMode::default(),
332 xml_tool_mode: false,
333 disable_custom_steering: false,
334 communication_tone: CommunicationTone::default(),
335 autonomy_level: AutonomyLevel::default(),
336 reasoning_effort: None,
337 disable_streaming: false,
338 voice: VoiceSettings::default(),
339 skills: SkillsConfig::default(),
340 modules: HashMap::new(),
341 }
342 }
343}
344
345impl Settings {
346 pub fn active_provider(&self) -> Option<&ProviderConfig> {
348 let provider = self.active_provider.as_ref()?;
349 self.providers.get(provider)
350 }
351
352 pub fn set_active_provider(&mut self, name: &str) -> Result<(), String> {
354 if self.providers.contains_key(name) {
355 self.active_provider = Some(name.to_string());
356 Ok(())
357 } else {
358 Err(format!("Provider '{name}' not found"))
359 }
360 }
361
362 pub fn add_provider(&mut self, name: String, config: ProviderConfig) {
364 self.providers.insert(name, config);
365 }
366
367 pub fn remove_provider(&mut self, name: &str) -> Result<(), String> {
369 if Some(name) == self.active_provider.as_deref() {
370 return Err("Cannot remove the active provider".to_string());
371 }
372
373 if self.providers.remove(name).is_some() {
374 Ok(())
375 } else {
376 Err(format!("Provider '{name}' not found"))
377 }
378 }
379
380 pub fn list_providers(&self) -> Vec<String> {
382 self.providers.keys().cloned().collect()
383 }
384
385 pub fn get_module_config<T: Default + DeserializeOwned>(&self, namespace: &str) -> T {
387 self.modules
388 .get(namespace)
389 .and_then(|v| {
390 serde_json::from_value(v.clone())
391 .map_err(|e| tracing::warn!("Failed to parse module config '{namespace}': {e}"))
392 .ok()
393 })
394 .unwrap_or_default()
395 }
396
397 pub fn set_module_config<T: serde::Serialize>(&mut self, namespace: &str, config: T) {
399 if let Ok(value) = serde_json::to_value(&config) {
400 self.modules.insert(namespace.to_string(), value);
401 }
402 }
403
404 pub fn get_agent_model(&self, agent_name: &str) -> Option<&ModelSettings> {
406 self.agent_models.get(agent_name)
407 }
408
409 pub fn set_agent_model(&mut self, agent_name: String, model: ModelSettings) {
411 self.agent_models.insert(agent_name, model);
412 }
413}
414
415impl ProviderConfig {
416 pub fn bedrock_profile(&self) -> Option<&str> {
418 match self {
419 ProviderConfig::Bedrock { profile, .. } => Some(profile.as_str()),
420 ProviderConfig::Mock { .. } => None,
421 ProviderConfig::OpenRouter { .. } => None,
422 ProviderConfig::ClaudeCode { .. } => None,
423 }
424 }
425
426 pub fn openrouter_api_key(&self) -> Option<&str> {
428 match self {
429 ProviderConfig::OpenRouter { api_key } => Some(api_key.as_str()),
430 ProviderConfig::Bedrock { .. } => None,
431 ProviderConfig::Mock { .. } => None,
432 ProviderConfig::ClaudeCode { .. } => None,
433 }
434 }
435}