magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
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, TtsrSettings},
    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,
}

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, PartialEq, Eq)]
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 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 lsp: LspSettings,
    pub integrations: IntegrationsSettings,
    pub tui: TuiSettings,
    pub provider_stream: ProviderStreamSettings,
    pub ttsr: TtsrSettings,
    pub selected_primary_agent: Option<String>,
}

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(),
            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(),
            lsp: LspSettings::default(),
            integrations: IntegrationsSettings::default(),
            tui: TuiSettings::default(),
            provider_stream: ProviderStreamSettings::default(),
            ttsr: TtsrSettings::default(),
            selected_primary_agent: None,
        }
    }
}

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()
        })
    }
}