magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{
    config::{AuthState, EffectiveConfig, McPaths, ProviderCredential},
    shell::ShellState,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ModelSwitchResult {
    pub(crate) provider: String,
    pub(crate) model: String,
    pub(crate) notice: Option<String>,
}

pub(crate) fn switch_model_selection(
    paths: &McPaths,
    auth_state: &AuthState,
    model_id: &str,
) -> anyhow::Result<ModelSwitchResult> {
    let (provider, model, notice) =
        crate::model_catalog::switch_model(paths, auth_state, model_id)?;
    Ok(ModelSwitchResult {
        provider,
        model,
        notice,
    })
}

pub(crate) fn switch_shell_model(
    state: &mut ShellState,
    model_id: &str,
) -> anyhow::Result<ModelSwitchResult> {
    let config = state
        .config
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("/setmodel requires loaded runtime config"))?;
    let result = switch_model_selection(&config.paths, &state.auth_state, model_id)?;
    refresh_runtime_provider_selection(state, &result.provider, &result.model)?;
    Ok(result)
}

pub(crate) fn refresh_runtime_provider_selection(
    state: &mut ShellState,
    provider: &str,
    model: &str,
) -> anyhow::Result<()> {
    let Some(config) = state.config.as_ref() else {
        state.model = model.to_string();
        return Ok(());
    };
    let refreshed =
        crate::config::load_effective_provider_selection(&config.paths, provider, model)?;
    state.model = model.to_string();
    state.auth_state = refreshed.auth_state();
    state.config = Some(refreshed);
    Ok(())
}

pub(crate) fn fast_mode_status(
    enabled: bool,
    provider: &str,
    model: &str,
    effective: bool,
) -> String {
    if !enabled {
        return "Fast mode off. Magi-code will not request Fast processing.".to_string();
    }
    if effective {
        format!(
            "Fast mode on. {provider}/{model} will request Fast processing.\nEligible requests may have higher provider pricing."
        )
    } else {
        format!("Fast remains on, but {provider}/{model} has no configured Fast support.")
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum LogoutRemoval {
    CustomRemoved { provider_id: String },
    CustomMissing { provider_id: String },
    ProviderRemoved { label: String, id: String },
    ProviderMissing { label: String, id: String },
}

impl LogoutRemoval {
    pub(crate) fn message(&self) -> String {
        match self {
            Self::CustomRemoved { provider_id } => format!("removed custom provider {provider_id}"),
            Self::CustomMissing { provider_id } => {
                format!("custom provider {provider_id} is not configured")
            }
            Self::ProviderRemoved { label, id } => {
                format!("removed local auth for {label} ({id})")
            }
            Self::ProviderMissing { label, id } => {
                format!("{label} ({id}) is not configured; credentials unchanged")
            }
        }
    }

    pub(crate) fn provider_id(&self) -> &str {
        match self {
            Self::CustomRemoved { provider_id } | Self::CustomMissing { provider_id } => {
                provider_id
            }
            Self::ProviderRemoved { id, .. } | Self::ProviderMissing { id, .. } => id,
        }
    }

    pub(crate) fn removed(&self) -> bool {
        matches!(
            self,
            Self::CustomRemoved { .. } | Self::ProviderRemoved { .. }
        )
    }

    pub(crate) fn removed_custom_provider(&self) -> Option<&str> {
        match self {
            Self::CustomRemoved { provider_id } => Some(provider_id),
            Self::CustomMissing { .. }
            | Self::ProviderRemoved { .. }
            | Self::ProviderMissing { .. } => None,
        }
    }
}

pub(crate) fn remove_logout_target(
    paths: &McPaths,
    provider_id: &str,
) -> anyhow::Result<LogoutRemoval> {
    if crate::config::read_settings(paths)?
        .custom_providers
        .contains_key(provider_id)
    {
        return remove_custom_logout_target(paths, provider_id);
    }
    remove_provider_logout_target(paths, provider_id)
}

fn remove_custom_logout_target(
    paths: &McPaths,
    provider_id: &str,
) -> anyhow::Result<LogoutRemoval> {
    Ok(
        if crate::config::remove_custom_provider(paths, provider_id)? {
            LogoutRemoval::CustomRemoved {
                provider_id: provider_id.to_string(),
            }
        } else {
            LogoutRemoval::CustomMissing {
                provider_id: provider_id.to_string(),
            }
        },
    )
}

fn remove_provider_logout_target(
    paths: &McPaths,
    provider_id: &str,
) -> anyhow::Result<LogoutRemoval> {
    let provider = crate::login::validate_logout_provider(provider_id)?;
    let removal = crate::config::remove_provider_auth(paths, provider.id)?;
    Ok(if removal.removed {
        LogoutRemoval::ProviderRemoved {
            label: provider.label.to_string(),
            id: provider.id.to_string(),
        }
    } else {
        LogoutRemoval::ProviderMissing {
            label: provider.label.to_string(),
            id: provider.id.to_string(),
        }
    })
}

fn auth_after_local_logout(provider_id: &str) -> Option<ProviderCredential> {
    if provider_id != crate::providers::ANTHROPIC_PROVIDER {
        return None;
    }
    std::env::var("ANTHROPIC_API_KEY")
        .ok()
        .filter(|key| !key.is_empty())
        .map(|key| ProviderCredential::ApiKey { key })
}

fn set_effective_config_auth(
    config: &mut EffectiveConfig,
    provider_id: &str,
    credential: Option<&ProviderCredential>,
) {
    if provider_id == crate::providers::ANTHROPIC_PROVIDER {
        config.auth = credential.cloned();
    } else {
        config.auth = None;
    }
}

fn sync_effective_config_after_logout(
    config: &mut EffectiveConfig,
    removal: &LogoutRemoval,
    credential: Option<&ProviderCredential>,
) {
    if let Some(provider_id) = removal.removed_custom_provider() {
        config.custom_providers.remove(provider_id);
    }
    if removal.removed() && config.provider_id() == removal.provider_id() {
        set_effective_config_auth(config, removal.provider_id(), credential);
    }
}

pub(crate) fn reconcile_runtime_after_logout(
    state: &mut ShellState,
    app_config: Option<&mut EffectiveConfig>,
    removal: &LogoutRemoval,
) {
    if !removal.removed() {
        return;
    }

    let credential = auth_after_local_logout(removal.provider_id());
    let state_auth_state = state.config.as_mut().and_then(|config| {
        sync_effective_config_after_logout(config, removal, credential.as_ref());
        (config.provider_id() == removal.provider_id()).then(|| config.auth_state())
    });
    if let Some(config) = app_config {
        sync_effective_config_after_logout(config, removal, credential.as_ref());
    }
    if state.auth_state.provider() == removal.provider_id() {
        state.auth_state = state_auth_state.unwrap_or_else(|| AuthState::Missing {
            provider: removal.provider_id().to_string(),
        });
    }
}