Skip to main content

tycode_core/settings/
config.rs

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    /// Agent can proceed with implementation directly without presenting a plan
57    FullyAutonomous,
58    /// Agent must present and get approval before implementing changes
59    #[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/// Configuration for the skills system.
140#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
141pub struct SkillsConfig {
142    /// Master switch to enable/disable skills
143    #[serde(default = "default_skills_enabled")]
144    pub enabled: bool,
145
146    /// Skills to disable by name
147    #[serde(default)]
148    pub disabled_skills: HashSet<String>,
149
150    /// Additional directories to search for skills
151    #[serde(default)]
152    pub additional_dirs: Vec<PathBuf>,
153
154    /// Load skills from ~/.claude/skills/ for Claude Code compatibility
155    #[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/// Core application settings.
179///
180/// # Maintainer Note
181///
182/// When adding new settings fields, you must also update the VSCode extension
183/// settings UI in:
184/// - `tycode-vscode/src/settingsProvider.ts` - HTML form elements
185/// - `tycode-vscode/src/webview/settings.js` - JavaScript state and handlers
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct Settings {
188    /// The name of the currently active provider
189    #[serde(default)]
190    pub active_provider: Option<String>,
191
192    /// Map of provider name to configuration
193    #[serde(default)]
194    pub providers: HashMap<String, ProviderConfig>,
195
196    /// Agent-specific model overrides
197    #[serde(default)]
198    pub agent_models: HashMap<String, ModelSettings>,
199
200    /// Default agent to use for new conversations
201    #[serde(default = "default_agent_name")]
202    pub default_agent: String,
203
204    /// Global maximum quality tier applied across agents
205    #[serde(default)]
206    pub model_quality: Option<ModelCost>,
207
208    /// Review level for messages
209    #[serde(default)]
210    pub review_level: ReviewLevel,
211
212    /// MCP server configurations
213    #[serde(default)]
214    pub mcp_servers: HashMap<String, McpServerConfig>,
215
216    /// Output mode for run_build_test tool
217    #[serde(default)]
218    pub run_build_test_output_mode: RunBuildTestOutputMode,
219
220    /// Enable type analyzer tools (search_types, get_type_docs)
221    #[serde(default)]
222    pub enable_type_analyzer: bool,
223
224    /// Controls how sub-agent context is initialized when spawning
225    #[serde(default)]
226    pub spawn_context_mode: SpawnContextMode,
227
228    /// Enable XML-based tool calling instead of native tool use
229    #[serde(default)]
230    pub xml_tool_mode: bool,
231
232    /// Disable custom steering documents (from .tycode and external agent configs)
233    #[serde(default)]
234    pub disable_custom_steering: bool,
235
236    /// Communication tone for agent responses
237    #[serde(default)]
238    pub communication_tone: CommunicationTone,
239
240    /// Controls whether agent must get plan approval before implementing
241    #[serde(default)]
242    pub autonomy_level: AutonomyLevel,
243
244    /// Voice/speech-to-text configuration
245    #[serde(default)]
246    pub voice: VoiceSettings,
247
248    /// Skills system configuration
249    #[serde(default)]
250    pub skills: SkillsConfig,
251
252    /// Global default reasoning effort applied to all agents unless overridden
253    #[serde(default)]
254    pub reasoning_effort: Option<ReasoningBudget>,
255
256    /// When true, AI responses arrive as a single complete message instead of streaming incrementally
257    #[serde(default)]
258    pub disable_streaming: bool,
259
260    /// Enables modules to own their configuration without modifying tycode-core,
261    /// supporting external/plugin modules that aren't known at compile time.
262    #[serde(default)]
263    pub modules: HashMap<String, serde_json::Value>,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct McpServerConfig {
268    /// Command to execute for the MCP server
269    pub command: String,
270
271    /// Arguments to pass to the command
272    #[serde(default)]
273    pub args: Vec<String>,
274
275    /// Environment variables to set for the server process
276    #[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    /// Get the active provider configuration
347    pub fn active_provider(&self) -> Option<&ProviderConfig> {
348        let provider = self.active_provider.as_ref()?;
349        self.providers.get(provider)
350    }
351
352    /// Set the active provider (returns error if provider doesn't exist)
353    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    /// Add or update a provider configuration
363    pub fn add_provider(&mut self, name: String, config: ProviderConfig) {
364        self.providers.insert(name, config);
365    }
366
367    /// Remove a provider configuration
368    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    /// List all provider names
381    pub fn list_providers(&self) -> Vec<String> {
382        self.providers.keys().cloned().collect()
383    }
384
385    /// Get module-specific configuration, deserializing from the modules map
386    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    /// Set module-specific configuration, serializing to the modules map
398    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    /// Get the model settings for a specific agent
405    pub fn get_agent_model(&self, agent_name: &str) -> Option<&ModelSettings> {
406        self.agent_models.get(agent_name)
407    }
408
409    /// Set the model settings for a specific agent
410    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    /// Get the AWS profile for Bedrock provider
417    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    /// Get the API key for OpenRouter provider
427    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}