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