use crate::config::{CustomProviderConfig, HookSettings};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::{
agent::{
CompactionSettings, InstructionsSettings, IntegrationsSettings, ModelsSettings,
SessionTitleSettings, SkillsSettings, SubagentsSettings, TuiSettings,
},
helpers::is_false,
providers::{ProviderStreamSettings, SelectedModelSettings},
services::{LspSettings, McpServersSettings},
tools::ToolSettings,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsScope {
Global,
Project,
}
impl SettingsScope {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Global => "Global",
Self::Project => "Project",
}
}
pub(crate) fn toggle(self) -> Self {
match self {
Self::Global => Self::Project,
Self::Project => Self::Global,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SettingsListKind {
Skills,
Tools,
Subagents,
Models,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TextVerbosity {
#[default]
Low,
Medium,
High,
}
impl TextVerbosity {
pub(crate) fn as_api_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct FastSettings {
#[serde(default, skip_serializing_if = "is_false")]
#[schemars(!skip_serializing_if, default)]
pub enabled: bool,
}
impl FastSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct OpenAiResponsesSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text_verbosity: Option<TextVerbosity>,
}
impl OpenAiResponsesSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct OpenAiCodexSettings {
#[serde(default)]
pub text_verbosity: TextVerbosity,
#[serde(default, skip_serializing_if = "is_false")]
pub experimental_reasoning_updates: bool,
}
impl OpenAiCodexSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub enum AnthropicCacheTtl {
#[default]
#[serde(rename = "5m")]
FiveMinutes,
#[serde(rename = "1h")]
OneHour,
}
pub const DEFAULT_SESSION_RETENTION_DAYS: u64 = 30;
fn default_session_retention_days() -> u64 {
DEFAULT_SESSION_RETENTION_DAYS
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct SessionSettings {
#[serde(default = "default_session_retention_days")]
pub retention_days: u64,
}
impl Default for SessionSettings {
fn default() -> Self {
Self {
retention_days: DEFAULT_SESSION_RETENTION_DAYS,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub(crate) struct AppearanceSettings {
#[serde(
default = "default_appearance_theme",
skip_serializing_if = "is_default_appearance_theme"
)]
pub(crate) theme: String,
#[serde(default, skip_serializing_if = "is_false")]
pub(crate) reduced_motion: bool,
}
impl Default for AppearanceSettings {
fn default() -> Self {
Self {
theme: crate::appearance::DEFAULT_THEME_ID.to_string(),
reduced_motion: false,
}
}
}
fn default_appearance_theme() -> String {
crate::appearance::DEFAULT_THEME_ID.to_string()
}
fn is_default_appearance_theme(theme: &String) -> bool {
theme == crate::appearance::DEFAULT_THEME_ID
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BashProtectionFailurePolicy {
#[default]
AutoApprove,
AutoDeny,
}
impl BashProtectionFailurePolicy {
pub(crate) fn allows_execution(self) -> bool {
matches!(self, Self::AutoApprove)
}
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::AutoApprove => "auto_approve",
Self::AutoDeny => "auto_deny",
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BashProtectionLevel {
Permissive,
#[default]
Relaxed,
Balanced,
Cautious,
Strict,
}
impl BashProtectionLevel {
pub(crate) fn thresholds(self) -> (f64, f64) {
match self {
Self::Permissive => (2.0, 0.0),
Self::Relaxed => (1.8, 0.2),
Self::Balanced => (1.5, 0.35),
Self::Cautious => (1.0, 0.5),
Self::Strict => (0.5, 0.7),
}
}
pub(crate) const fn as_str(self) -> &'static str {
match self {
Self::Permissive => "permissive",
Self::Relaxed => "relaxed",
Self::Balanced => "balanced",
Self::Cautious => "cautious",
Self::Strict => "strict",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct BashProtectionSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default)]
pub protection_level: BashProtectionLevel,
#[serde(default)]
pub failure_policy: BashProtectionFailurePolicy,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HumanizeProtectionFailurePolicy {
AutoApprove,
#[default]
AutoDeny,
}
impl HumanizeProtectionFailurePolicy {
pub(crate) fn allows_execution(self) -> bool {
matches!(self, Self::AutoApprove)
}
}
fn default_content_threshold() -> f64 {
0.7
}
fn default_humanized_threshold() -> f64 {
0.65
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct HumanizeProtectionSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default = "default_content_threshold")]
pub content_threshold: f64,
#[serde(default = "default_humanized_threshold")]
pub humanized_threshold: f64,
#[serde(default)]
pub failure_policy: HumanizeProtectionFailurePolicy,
}
impl Default for HumanizeProtectionSettings {
fn default() -> Self {
Self {
enabled: false,
content_threshold: default_content_threshold(),
humanized_threshold: default_humanized_threshold(),
failure_policy: HumanizeProtectionFailurePolicy::default(),
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PromptInjectionFailurePolicy {
Annotate,
#[default]
Quarantine,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PromptInjectionProtectionLevel {
Permissive,
Relaxed,
#[default]
Balanced,
Cautious,
Strict,
}
impl PromptInjectionProtectionLevel {
pub(crate) fn thresholds(self) -> (f64, f64, f64, f64) {
match self {
Self::Permissive => (0.60, 0.90, 1.80, 0.30),
Self::Relaxed => (0.50, 0.80, 1.70, 0.40),
Self::Balanced => (0.35, 0.70, 1.50, 0.55),
Self::Cautious => (0.25, 0.60, 1.20, 0.65),
Self::Strict => (0.15, 0.50, 0.80, 0.75),
}
}
}
fn default_prompt_injection_max_assessed_bytes() -> usize {
32 * 1024
}
fn default_prompt_injection_tools() -> Vec<String> {
vec!["web".to_string()]
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct PromptInjectionProtectionSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub enforcement_enabled: bool,
#[serde(default)]
pub protection_level: PromptInjectionProtectionLevel,
#[serde(default = "default_prompt_injection_max_assessed_bytes")]
pub max_assessed_bytes: usize,
#[serde(default)]
pub failure_policy: PromptInjectionFailurePolicy,
#[serde(default = "default_prompt_injection_tools")]
pub tools: Vec<String>,
}
impl Default for PromptInjectionProtectionSettings {
fn default() -> Self {
Self {
enabled: false,
enforcement_enabled: false,
protection_level: PromptInjectionProtectionLevel::default(),
max_assessed_bytes: default_prompt_injection_max_assessed_bytes(),
failure_policy: PromptInjectionFailurePolicy::default(),
tools: default_prompt_injection_tools(),
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CompletionVerificationFailurePolicy {
#[default]
AcceptWithWarning,
RejectWithWarning,
}
fn default_completion_verification_threshold() -> f64 {
0.7
}
fn default_completion_verification_max_state_bytes() -> usize {
32 * 1024
}
fn default_completion_verification_max_continuations() -> u8 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CompletionVerificationSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub enforcement_enabled: bool,
#[serde(default = "default_completion_verification_threshold")]
pub threshold: f64,
#[serde(default = "default_completion_verification_max_state_bytes")]
pub max_state_bytes: usize,
#[serde(default = "default_completion_verification_max_continuations")]
pub max_continuations: u8,
#[serde(default)]
pub failure_policy: CompletionVerificationFailurePolicy,
}
impl Default for CompletionVerificationSettings {
fn default() -> Self {
Self {
enabled: false,
enforcement_enabled: false,
threshold: default_completion_verification_threshold(),
max_state_bytes: default_completion_verification_max_state_bytes(),
max_continuations: default_completion_verification_max_continuations(),
failure_policy: CompletionVerificationFailurePolicy::default(),
}
}
}
fn default_skill_suggestion_gate_threshold() -> f64 {
0.3
}
fn default_skill_suggestion_fit_threshold() -> f64 {
0.3
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct SkillSuggestionSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default = "default_skill_suggestion_gate_threshold")]
pub gate_threshold: f64,
#[serde(default = "default_skill_suggestion_fit_threshold")]
pub fit_threshold: f64,
}
impl Default for SkillSuggestionSettings {
fn default() -> Self {
Self {
enabled: false,
gate_threshold: default_skill_suggestion_gate_threshold(),
fit_threshold: default_skill_suggestion_fit_threshold(),
}
}
}
fn default_compaction_timing_soft_threshold_percent() -> u8 {
60
}
fn default_compaction_timing_threshold() -> f64 {
0.7
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CompactionTimingSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default = "default_compaction_timing_soft_threshold_percent")]
pub soft_threshold_percent: u8,
#[serde(default = "default_compaction_timing_threshold")]
pub threshold: f64,
}
impl Default for CompactionTimingSettings {
fn default() -> Self {
Self {
enabled: false,
soft_threshold_percent: default_compaction_timing_soft_threshold_percent(),
threshold: default_compaction_timing_threshold(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct BashFileTrackingSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct JevSettings {
#[serde(default, skip_serializing_if = "is_false")]
pub enabled: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub show_debug_messages: bool,
#[serde(default)]
pub bash_protection: BashProtectionSettings,
#[serde(default)]
pub bash_file_tracking: BashFileTrackingSettings,
#[serde(default)]
pub humanize_protection: HumanizeProtectionSettings,
#[serde(default)]
pub prompt_injection_protection: PromptInjectionProtectionSettings,
#[serde(default)]
pub completion_verification: CompletionVerificationSettings,
#[serde(default)]
pub skill_suggestion: SkillSuggestionSettings,
#[serde(default)]
pub compaction_timing: CompactionTimingSettings,
}
impl JevSettings {
pub(crate) fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Settings {
pub selected_model: SelectedModelSettings,
pub fast: FastSettings,
pub openai_codex: OpenAiCodexSettings,
pub openai_responses: OpenAiResponsesSettings,
pub no_color: Option<bool>,
pub anthropic_cache_ttl: Option<AnthropicCacheTtl>,
pub file_autocomplete_respects_gitignore: bool,
pub context: Option<crate::context::ContextBudget>,
pub session_titles: SessionTitleSettings,
pub summarizer: super::agent::SummarizerSettings,
pub side: super::agent::SideAgentSettings,
pub compaction: CompactionSettings,
pub tools: ToolSettings,
pub subagents: SubagentsSettings,
pub models: ModelsSettings,
pub hooks: HookSettings,
pub instructions: InstructionsSettings,
pub skills: SkillsSettings,
pub custom_providers: BTreeMap<String, CustomProviderConfig>,
pub mcp_servers: McpServersSettings,
pub mcp_approvals: BTreeMap<String, BTreeMap<String, bool>>,
pub lsp: LspSettings,
pub integrations: IntegrationsSettings,
pub tui: TuiSettings,
pub provider_stream: ProviderStreamSettings,
pub selected_primary_agent: Option<String>,
pub jev: JevSettings,
}
impl Default for Settings {
fn default() -> Self {
Self {
fast: FastSettings::default(),
openai_responses: OpenAiResponsesSettings::default(),
selected_model: SelectedModelSettings::default(),
openai_codex: OpenAiCodexSettings::default(),
no_color: None,
file_autocomplete_respects_gitignore: true,
anthropic_cache_ttl: None,
context: None,
session_titles: SessionTitleSettings::default(),
summarizer: super::agent::SummarizerSettings::default(),
side: super::agent::SideAgentSettings::default(),
compaction: CompactionSettings::default(),
tools: ToolSettings::default(),
subagents: SubagentsSettings::default(),
models: ModelsSettings::default(),
hooks: HookSettings::default(),
instructions: InstructionsSettings::default(),
skills: SkillsSettings::default(),
custom_providers: BTreeMap::new(),
mcp_servers: BTreeMap::new(),
mcp_approvals: BTreeMap::new(),
lsp: LspSettings::default(),
integrations: IntegrationsSettings::default(),
tui: TuiSettings::default(),
provider_stream: ProviderStreamSettings::default(),
selected_primary_agent: None,
jev: JevSettings::default(),
}
}
}
impl Settings {
pub(crate) fn text_verbosity_for(&self, provider: &str) -> Option<TextVerbosity> {
if provider == crate::providers::OPENAI_CODEX_PROVIDER {
return Some(
self.openai_responses
.text_verbosity
.unwrap_or(self.openai_codex.text_verbosity),
);
}
self.custom_providers.get(provider).and_then(|custom| {
(custom.use_responses_endpoint && custom.supports_text_verbosity)
.then_some(self.openai_responses.text_verbosity)
.flatten()
})
}
}