Skip to main content

magi_code/config/settings/
core.rs

1use crate::config::{CustomProviderConfig, HookSettings};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use super::{
7    agent::{
8        CompactionSettings, InstructionsSettings, IntegrationsSettings, ModelsSettings,
9        SessionTitleSettings, SkillsSettings, SubagentsSettings, TuiSettings,
10    },
11    helpers::is_false,
12    providers::{ProviderStreamSettings, SelectedModelSettings, TtsrSettings},
13    services::{LspSettings, McpServersSettings},
14    tools::ToolSettings,
15};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SettingsScope {
19    Global,
20    Project,
21}
22
23impl SettingsScope {
24    pub(crate) fn label(self) -> &'static str {
25        match self {
26            Self::Global => "Global",
27            Self::Project => "Project",
28        }
29    }
30
31    pub(crate) fn toggle(self) -> Self {
32        match self {
33            Self::Global => Self::Project,
34            Self::Project => Self::Global,
35        }
36    }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SettingsListKind {
41    Skills,
42    Tools,
43    Subagents,
44    Models,
45}
46
47#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
48#[serde(rename_all = "lowercase")]
49pub enum TextVerbosity {
50    #[default]
51    Low,
52    Medium,
53    High,
54}
55
56impl TextVerbosity {
57    pub(crate) fn as_api_str(self) -> &'static str {
58        match self {
59            Self::Low => "low",
60            Self::Medium => "medium",
61            Self::High => "high",
62        }
63    }
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
67pub struct FastSettings {
68    #[serde(default, skip_serializing_if = "is_false")]
69    #[schemars(!skip_serializing_if, default)]
70    pub enabled: bool,
71}
72
73impl FastSettings {
74    pub(crate) fn is_default(&self) -> bool {
75        self == &Self::default()
76    }
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
80pub struct OpenAiResponsesSettings {
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub text_verbosity: Option<TextVerbosity>,
83}
84
85impl OpenAiResponsesSettings {
86    pub(crate) fn is_default(&self) -> bool {
87        self == &Self::default()
88    }
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
92pub struct OpenAiCodexSettings {
93    #[serde(default)]
94    pub text_verbosity: TextVerbosity,
95    /// Unverified Codex support; disabled unless explicitly requested.
96    #[serde(default, skip_serializing_if = "is_false")]
97    pub experimental_reasoning_updates: bool,
98}
99
100impl OpenAiCodexSettings {
101    pub(crate) fn is_default(&self) -> bool {
102        self == &Self::default()
103    }
104}
105
106#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
107pub enum AnthropicCacheTtl {
108    #[default]
109    #[serde(rename = "5m")]
110    FiveMinutes,
111    #[serde(rename = "1h")]
112    OneHour,
113}
114
115pub const DEFAULT_SESSION_RETENTION_DAYS: u64 = 30;
116
117fn default_session_retention_days() -> u64 {
118    DEFAULT_SESSION_RETENTION_DAYS
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
122pub struct SessionSettings {
123    #[serde(default = "default_session_retention_days")]
124    pub retention_days: u64,
125}
126
127impl Default for SessionSettings {
128    fn default() -> Self {
129        Self {
130            retention_days: DEFAULT_SESSION_RETENTION_DAYS,
131        }
132    }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
136pub(crate) struct AppearanceSettings {
137    #[serde(
138        default = "default_appearance_theme",
139        skip_serializing_if = "is_default_appearance_theme"
140    )]
141    pub(crate) theme: String,
142    #[serde(default, skip_serializing_if = "is_false")]
143    pub(crate) reduced_motion: bool,
144}
145
146impl Default for AppearanceSettings {
147    fn default() -> Self {
148        Self {
149            theme: crate::appearance::DEFAULT_THEME_ID.to_string(),
150            reduced_motion: false,
151        }
152    }
153}
154
155fn default_appearance_theme() -> String {
156    crate::appearance::DEFAULT_THEME_ID.to_string()
157}
158
159fn is_default_appearance_theme(theme: &String) -> bool {
160    theme == crate::appearance::DEFAULT_THEME_ID
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct Settings {
165    pub selected_model: SelectedModelSettings,
166    pub fast: FastSettings,
167    pub openai_codex: OpenAiCodexSettings,
168    pub openai_responses: OpenAiResponsesSettings,
169    pub no_color: Option<bool>,
170    pub anthropic_cache_ttl: Option<AnthropicCacheTtl>,
171    pub file_autocomplete_respects_gitignore: bool,
172    pub context: Option<crate::context::ContextBudget>,
173    pub session_titles: SessionTitleSettings,
174    pub summarizer: super::agent::SummarizerSettings,
175    pub compaction: CompactionSettings,
176    pub tools: ToolSettings,
177    pub subagents: SubagentsSettings,
178    pub models: ModelsSettings,
179    pub hooks: HookSettings,
180    pub instructions: InstructionsSettings,
181    pub skills: SkillsSettings,
182    pub custom_providers: BTreeMap<String, CustomProviderConfig>,
183    pub mcp_servers: McpServersSettings,
184    /// Global-only approvals keyed by canonical .mcp.json source path, then server name.
185    pub mcp_approvals: BTreeMap<String, BTreeMap<String, bool>>,
186    pub lsp: LspSettings,
187    pub integrations: IntegrationsSettings,
188    pub tui: TuiSettings,
189    pub provider_stream: ProviderStreamSettings,
190    pub ttsr: TtsrSettings,
191    pub selected_primary_agent: Option<String>,
192}
193
194impl Default for Settings {
195    fn default() -> Self {
196        Self {
197            fast: FastSettings::default(),
198
199            openai_responses: OpenAiResponsesSettings::default(),
200            selected_model: SelectedModelSettings::default(),
201            openai_codex: OpenAiCodexSettings::default(),
202            no_color: None,
203            file_autocomplete_respects_gitignore: true,
204            anthropic_cache_ttl: None,
205            context: None,
206            session_titles: SessionTitleSettings::default(),
207            summarizer: super::agent::SummarizerSettings::default(),
208            compaction: CompactionSettings::default(),
209            tools: ToolSettings::default(),
210            subagents: SubagentsSettings::default(),
211            models: ModelsSettings::default(),
212            hooks: HookSettings::default(),
213            instructions: InstructionsSettings::default(),
214            skills: SkillsSettings::default(),
215            custom_providers: BTreeMap::new(),
216            mcp_servers: BTreeMap::new(),
217            mcp_approvals: BTreeMap::new(),
218            lsp: LspSettings::default(),
219            integrations: IntegrationsSettings::default(),
220            tui: TuiSettings::default(),
221            provider_stream: ProviderStreamSettings::default(),
222            ttsr: TtsrSettings::default(),
223            selected_primary_agent: None,
224        }
225    }
226}
227
228impl Settings {
229    pub(crate) fn text_verbosity_for(&self, provider: &str) -> Option<TextVerbosity> {
230        if provider == crate::providers::OPENAI_CODEX_PROVIDER {
231            return Some(
232                self.openai_responses
233                    .text_verbosity
234                    .unwrap_or(self.openai_codex.text_verbosity),
235            );
236        }
237        self.custom_providers.get(provider).and_then(|custom| {
238            (custom.use_responses_endpoint && custom.supports_text_verbosity)
239                .then_some(self.openai_responses.text_verbosity)
240                .flatten()
241        })
242    }
243}