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},
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 #[serde(default, skip_serializing_if = "is_false")]
72 #[schemars(!skip_serializing_if, default)]
73 pub ultrafast: bool,
74}
75
76impl FastSettings {
77 pub(crate) fn mode_name(&self) -> &'static str {
78 if self.ultrafast { "Ultrafast" } else { "Fast" }
79 }
80
81 pub(crate) fn is_default(&self) -> bool {
82 self == &Self::default()
83 }
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
87pub struct OpenAiResponsesSettings {
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub text_verbosity: Option<TextVerbosity>,
90}
91
92impl OpenAiResponsesSettings {
93 pub(crate) fn is_default(&self) -> bool {
94 self == &Self::default()
95 }
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
99pub struct OpenAiCodexSettings {
100 #[serde(default)]
101 pub text_verbosity: TextVerbosity,
102 #[serde(default, skip_serializing_if = "is_false")]
104 pub experimental_reasoning_updates: bool,
105}
106
107impl OpenAiCodexSettings {
108 pub(crate) fn is_default(&self) -> bool {
109 self == &Self::default()
110 }
111}
112
113#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
114pub enum AnthropicCacheTtl {
115 #[default]
116 #[serde(rename = "5m")]
117 FiveMinutes,
118 #[serde(rename = "1h")]
119 OneHour,
120}
121
122pub const DEFAULT_SESSION_RETENTION_DAYS: u64 = 30;
123
124fn default_session_retention_days() -> u64 {
125 DEFAULT_SESSION_RETENTION_DAYS
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
129pub struct SessionSettings {
130 #[serde(default = "default_session_retention_days")]
131 pub retention_days: u64,
132}
133
134impl Default for SessionSettings {
135 fn default() -> Self {
136 Self {
137 retention_days: DEFAULT_SESSION_RETENTION_DAYS,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
143pub(crate) struct AppearanceSettings {
144 #[serde(
145 default = "default_appearance_theme",
146 skip_serializing_if = "is_default_appearance_theme"
147 )]
148 pub(crate) theme: String,
149 #[serde(default, skip_serializing_if = "is_false")]
150 pub(crate) reduced_motion: bool,
151}
152
153impl Default for AppearanceSettings {
154 fn default() -> Self {
155 Self {
156 theme: crate::appearance::DEFAULT_THEME_ID.to_string(),
157 reduced_motion: false,
158 }
159 }
160}
161
162fn default_appearance_theme() -> String {
163 crate::appearance::DEFAULT_THEME_ID.to_string()
164}
165
166fn is_default_appearance_theme(theme: &String) -> bool {
167 theme == crate::appearance::DEFAULT_THEME_ID
168}
169
170#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
171#[serde(rename_all = "snake_case")]
172pub enum BashProtectionFailurePolicy {
173 #[default]
174 AutoApprove,
175 AutoDeny,
176}
177
178impl BashProtectionFailurePolicy {
179 pub(crate) fn allows_execution(self) -> bool {
180 matches!(self, Self::AutoApprove)
181 }
182
183 pub(crate) const fn as_str(self) -> &'static str {
184 match self {
185 Self::AutoApprove => "auto_approve",
186 Self::AutoDeny => "auto_deny",
187 }
188 }
189}
190#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
191#[serde(rename_all = "snake_case")]
192pub enum BashProtectionLevel {
193 Permissive,
194 #[default]
195 Relaxed,
196 Balanced,
197 Cautious,
198 Strict,
199}
200
201impl BashProtectionLevel {
202 pub(crate) fn thresholds(self) -> (f64, f64) {
203 match self {
204 Self::Permissive => (2.0, 0.0),
205 Self::Relaxed => (1.8, 0.2),
206 Self::Balanced => (1.5, 0.35),
207 Self::Cautious => (1.0, 0.5),
208 Self::Strict => (0.5, 0.7),
209 }
210 }
211
212 pub(crate) const fn as_str(self) -> &'static str {
213 match self {
214 Self::Permissive => "permissive",
215 Self::Relaxed => "relaxed",
216 Self::Balanced => "balanced",
217 Self::Cautious => "cautious",
218 Self::Strict => "strict",
219 }
220 }
221}
222
223#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
224pub struct BashProtectionSettings {
225 #[serde(default, skip_serializing_if = "is_false")]
226 pub enabled: bool,
227 #[serde(default)]
228 pub protection_level: BashProtectionLevel,
229 #[serde(default)]
230 pub failure_policy: BashProtectionFailurePolicy,
231}
232
233#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
234#[serde(rename_all = "snake_case")]
235pub enum HumanizeProtectionFailurePolicy {
236 AutoApprove,
237 #[default]
238 AutoDeny,
239}
240
241impl HumanizeProtectionFailurePolicy {
242 pub(crate) fn allows_execution(self) -> bool {
243 matches!(self, Self::AutoApprove)
244 }
245}
246
247fn default_content_threshold() -> f64 {
248 0.7
249}
250
251fn default_humanized_threshold() -> f64 {
252 0.65
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
256pub struct HumanizeProtectionSettings {
257 #[serde(default, skip_serializing_if = "is_false")]
258 pub enabled: bool,
259 #[serde(default = "default_content_threshold")]
260 pub content_threshold: f64,
261 #[serde(default = "default_humanized_threshold")]
262 pub humanized_threshold: f64,
263 #[serde(default)]
264 pub failure_policy: HumanizeProtectionFailurePolicy,
265}
266
267impl Default for HumanizeProtectionSettings {
268 fn default() -> Self {
269 Self {
270 enabled: false,
271 content_threshold: default_content_threshold(),
272 humanized_threshold: default_humanized_threshold(),
273 failure_policy: HumanizeProtectionFailurePolicy::default(),
274 }
275 }
276}
277
278#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
279#[serde(rename_all = "snake_case")]
280pub enum PromptInjectionFailurePolicy {
281 Annotate,
282 #[default]
283 Quarantine,
284}
285
286#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
287#[serde(rename_all = "snake_case")]
288pub enum PromptInjectionProtectionLevel {
289 Permissive,
290 Relaxed,
291 #[default]
292 Balanced,
293 Cautious,
294 Strict,
295}
296
297impl PromptInjectionProtectionLevel {
298 pub(crate) fn thresholds(self) -> (f64, f64, f64, f64) {
299 match self {
300 Self::Permissive => (0.60, 0.90, 1.80, 0.30),
301 Self::Relaxed => (0.50, 0.80, 1.70, 0.40),
302 Self::Balanced => (0.35, 0.70, 1.50, 0.55),
303 Self::Cautious => (0.25, 0.60, 1.20, 0.65),
304 Self::Strict => (0.15, 0.50, 0.80, 0.75),
305 }
306 }
307}
308fn default_prompt_injection_max_assessed_bytes() -> usize {
309 32 * 1024
310}
311
312fn default_prompt_injection_tools() -> Vec<String> {
313 vec!["web".to_string()]
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
317pub struct PromptInjectionProtectionSettings {
318 #[serde(default, skip_serializing_if = "is_false")]
319 pub enabled: bool,
320 #[serde(default, skip_serializing_if = "is_false")]
321 pub enforcement_enabled: bool,
322 #[serde(default)]
323 pub protection_level: PromptInjectionProtectionLevel,
324 #[serde(default = "default_prompt_injection_max_assessed_bytes")]
325 pub max_assessed_bytes: usize,
326 #[serde(default)]
327 pub failure_policy: PromptInjectionFailurePolicy,
328 #[serde(default = "default_prompt_injection_tools")]
329 pub tools: Vec<String>,
330}
331
332impl Default for PromptInjectionProtectionSettings {
333 fn default() -> Self {
334 Self {
335 enabled: false,
336 enforcement_enabled: false,
337 protection_level: PromptInjectionProtectionLevel::default(),
338 max_assessed_bytes: default_prompt_injection_max_assessed_bytes(),
339 failure_policy: PromptInjectionFailurePolicy::default(),
340 tools: default_prompt_injection_tools(),
341 }
342 }
343}
344
345#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
346#[serde(rename_all = "snake_case")]
347pub enum CompletionVerificationFailurePolicy {
348 #[default]
349 AcceptWithWarning,
350 RejectWithWarning,
351}
352
353fn default_completion_verification_threshold() -> f64 {
354 0.7
355}
356fn default_completion_verification_max_state_bytes() -> usize {
357 32 * 1024
358}
359fn default_completion_verification_max_continuations() -> u8 {
360 1
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
364pub struct CompletionVerificationSettings {
365 #[serde(default, skip_serializing_if = "is_false")]
366 pub enabled: bool,
367 #[serde(default, skip_serializing_if = "is_false")]
368 pub enforcement_enabled: bool,
369 #[serde(default = "default_completion_verification_threshold")]
370 pub threshold: f64,
371 #[serde(default = "default_completion_verification_max_state_bytes")]
372 pub max_state_bytes: usize,
373 #[serde(default = "default_completion_verification_max_continuations")]
374 pub max_continuations: u8,
375 #[serde(default)]
376 pub failure_policy: CompletionVerificationFailurePolicy,
377}
378
379impl Default for CompletionVerificationSettings {
380 fn default() -> Self {
381 Self {
382 enabled: false,
383 enforcement_enabled: false,
384 threshold: default_completion_verification_threshold(),
385 max_state_bytes: default_completion_verification_max_state_bytes(),
386 max_continuations: default_completion_verification_max_continuations(),
387 failure_policy: CompletionVerificationFailurePolicy::default(),
388 }
389 }
390}
391
392fn default_skill_suggestion_gate_threshold() -> f64 {
393 0.3
394}
395fn default_skill_suggestion_fit_threshold() -> f64 {
396 0.3
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
400pub struct SkillSuggestionSettings {
401 #[serde(default, skip_serializing_if = "is_false")]
402 pub enabled: bool,
403 #[serde(default = "default_skill_suggestion_gate_threshold")]
404 pub gate_threshold: f64,
405 #[serde(default = "default_skill_suggestion_fit_threshold")]
406 pub fit_threshold: f64,
407}
408
409impl Default for SkillSuggestionSettings {
410 fn default() -> Self {
411 Self {
412 enabled: false,
413 gate_threshold: default_skill_suggestion_gate_threshold(),
414 fit_threshold: default_skill_suggestion_fit_threshold(),
415 }
416 }
417}
418
419fn default_compaction_timing_soft_threshold_percent() -> u8 {
420 60
421}
422fn default_compaction_timing_threshold() -> f64 {
423 0.7
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
427pub struct CompactionTimingSettings {
428 #[serde(default, skip_serializing_if = "is_false")]
429 pub enabled: bool,
430 #[serde(default = "default_compaction_timing_soft_threshold_percent")]
431 pub soft_threshold_percent: u8,
432 #[serde(default = "default_compaction_timing_threshold")]
433 pub threshold: f64,
434}
435
436impl Default for CompactionTimingSettings {
437 fn default() -> Self {
438 Self {
439 enabled: false,
440 soft_threshold_percent: default_compaction_timing_soft_threshold_percent(),
441 threshold: default_compaction_timing_threshold(),
442 }
443 }
444}
445
446#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
447pub struct BashFileTrackingSettings {
448 #[serde(default, skip_serializing_if = "is_false")]
449 pub enabled: bool,
450}
451
452#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
453pub struct JevSettings {
454 #[serde(default, skip_serializing_if = "is_false")]
455 pub enabled: bool,
456 #[serde(default, skip_serializing_if = "is_false")]
457 pub show_debug_messages: bool,
458 #[serde(default)]
459 pub bash_protection: BashProtectionSettings,
460 #[serde(default)]
461 pub bash_file_tracking: BashFileTrackingSettings,
462 #[serde(default)]
463 pub humanize_protection: HumanizeProtectionSettings,
464 #[serde(default)]
465 pub prompt_injection_protection: PromptInjectionProtectionSettings,
466 #[serde(default)]
467 pub completion_verification: CompletionVerificationSettings,
468 #[serde(default)]
469 pub skill_suggestion: SkillSuggestionSettings,
470 #[serde(default)]
471 pub compaction_timing: CompactionTimingSettings,
472}
473
474impl JevSettings {
475 pub(crate) fn is_default(&self) -> bool {
476 self == &Self::default()
477 }
478}
479
480#[derive(Debug, Clone, PartialEq)]
481pub struct Settings {
482 pub selected_model: SelectedModelSettings,
483 pub fast: FastSettings,
484 pub openai_codex: OpenAiCodexSettings,
485 pub openai_responses: OpenAiResponsesSettings,
486 pub no_color: Option<bool>,
487 pub anthropic_cache_ttl: Option<AnthropicCacheTtl>,
488 pub file_autocomplete_respects_gitignore: bool,
489 pub context: Option<crate::context::ContextBudget>,
490 pub session_titles: SessionTitleSettings,
491 pub summarizer: super::agent::SummarizerSettings,
492 pub side: super::agent::SideAgentSettings,
493 pub compaction: CompactionSettings,
494 pub tools: ToolSettings,
495 pub subagents: SubagentsSettings,
496 pub models: ModelsSettings,
497 pub hooks: HookSettings,
498 pub instructions: InstructionsSettings,
499 pub skills: SkillsSettings,
500 pub custom_providers: BTreeMap<String, CustomProviderConfig>,
501 pub mcp_servers: McpServersSettings,
502 pub mcp_approvals: BTreeMap<String, BTreeMap<String, bool>>,
504 pub lsp: LspSettings,
505 pub integrations: IntegrationsSettings,
506 pub tui: TuiSettings,
507 pub provider_stream: ProviderStreamSettings,
508 pub selected_primary_agent: Option<String>,
509 pub jev: JevSettings,
510}
511
512impl Default for Settings {
513 fn default() -> Self {
514 Self {
515 fast: FastSettings::default(),
516
517 openai_responses: OpenAiResponsesSettings::default(),
518 selected_model: SelectedModelSettings::default(),
519 openai_codex: OpenAiCodexSettings::default(),
520 no_color: None,
521 file_autocomplete_respects_gitignore: true,
522 anthropic_cache_ttl: None,
523 context: None,
524 session_titles: SessionTitleSettings::default(),
525 summarizer: super::agent::SummarizerSettings::default(),
526 side: super::agent::SideAgentSettings::default(),
527 compaction: CompactionSettings::default(),
528 tools: ToolSettings::default(),
529 subagents: SubagentsSettings::default(),
530 models: ModelsSettings::default(),
531 hooks: HookSettings::default(),
532 instructions: InstructionsSettings::default(),
533 skills: SkillsSettings::default(),
534 custom_providers: BTreeMap::new(),
535 mcp_servers: BTreeMap::new(),
536 mcp_approvals: BTreeMap::new(),
537 lsp: LspSettings::default(),
538 integrations: IntegrationsSettings::default(),
539 tui: TuiSettings::default(),
540 provider_stream: ProviderStreamSettings::default(),
541 selected_primary_agent: None,
542 jev: JevSettings::default(),
543 }
544 }
545}
546
547impl Settings {
548 pub(crate) fn text_verbosity_for(&self, provider: &str) -> Option<TextVerbosity> {
549 if provider == crate::providers::OPENAI_CODEX_PROVIDER {
550 return Some(
551 self.openai_responses
552 .text_verbosity
553 .unwrap_or(self.openai_codex.text_verbosity),
554 );
555 }
556 self.custom_providers.get(provider).and_then(|custom| {
557 (custom.use_responses_endpoint && custom.supports_text_verbosity)
558 .then_some(self.openai_responses.text_verbosity)
559 .flatten()
560 })
561 }
562}