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