use serde_json::Value;
use crate::config::{
McPaths,
settings_storage::{
insert_default_schema_ref_if_absent, merge_local_settings_json,
merge_local_settings_values, read_settings_json_or_empty, update_settings_json,
},
};
use super::{
Settings, SettingsScope,
json::set_path,
persistence::{parse_settings_document_with_session_settings, read_settings},
tools::{
validate_model_identifier, validate_view_image_identifier,
validate_view_image_max_image_bytes,
},
};
pub(crate) const EDITABLE_SETTINGS_PATHS: &[&str] = &[
"providers.catalog.disabled",
"agent.compaction.provider",
"agent.compaction.model",
"agent.compaction.auto.enabled",
"agent.compaction.auto.threshold_percent",
"agent.compaction.auto.threshold_tokens",
"agent.compaction.auto.max_compactions_per_run",
"agent.context.enabled",
"agent.context.max_tokens",
"agent.context.reserve_tokens",
"agent.context.keep_recent_tokens",
"agent.subagents.execution.absolute_paths",
"agent.subagents.execution.max_depth",
"agent.summarizer.auto_start",
"agent.summarizer.model",
"agent.summarizer.provider",
"agent.summarizer.reasoning",
"agent.side.provider",
"agent.side.model",
"automation.integrations.herdr.enabled",
"capabilities.tools.output_compression.enabled",
"capabilities.tools.view_image.absolute_paths",
"capabilities.tools.view_image.max_image_bytes",
"capabilities.tools.view_image.vision_model",
"capabilities.tools.view_image.vision_model.provider",
"capabilities.tools.view_image.vision_model.model",
"interface.tui.subagent_card_rows",
"interface.tui.autocomplete.respects_gitignore",
"knowledge.skills.additional_paths",
"providers.streaming.semantic_progress_timeout_seconds",
"providers.streaming.subagent_semantic_progress_timeout_seconds",
"sessions.titles.enabled",
"sessions.titles.model",
"sessions.titles.provider",
];
pub(crate) fn load_settings_editor(
paths: &McPaths,
scope: SettingsScope,
) -> anyhow::Result<Settings> {
match scope {
SettingsScope::Global => {
parse_editor_settings(read_settings_json_or_empty(&paths.settings_file)?)
}
SettingsScope::Project => {
parse_editor_settings(read_settings_json_or_empty(&paths.settings_file)?)?;
let settings = read_settings(paths)?;
validate_editor_settings(&settings)?;
Ok(settings)
}
}
}
pub(crate) fn save_settings_editor(
paths: &McPaths,
scope: SettingsScope,
changes: &[(&str, Value)],
) -> anyhow::Result<Settings> {
for (path, value) in changes {
anyhow::ensure!(
EDITABLE_SETTINGS_PATHS.contains(path),
"setting is not editable: {path}"
);
if *path == "capabilities.tools.view_image.vision_model" {
anyhow::ensure!(
value.is_null(),
"set image provider/model fields together, or use null to clear the selection"
);
}
}
if changes.is_empty() {
parse_editor_settings(read_settings_json_or_empty(&paths.settings_file)?)?;
let settings = read_settings(paths)?;
validate_editor_settings(&settings)?;
return Ok(settings);
}
let mut saved = None;
update_settings_json(paths, scope, |raw| {
for (path, value) in changes {
set_path(raw, &path.split('.').collect::<Vec<_>>(), value.clone())?;
}
if scope == SettingsScope::Global {
parse_editor_settings(raw.clone())?;
}
let effective = match scope {
SettingsScope::Global => {
insert_default_schema_ref_if_absent(raw);
merge_local_settings_json(paths, raw.clone())?
}
SettingsScope::Project => {
let global = read_settings_json_or_empty(&paths.settings_file)?;
parse_editor_settings(global.clone())?;
merge_local_settings_values(global, raw.clone())?
}
};
let settings = parse_editor_settings(effective)?;
settings
.session_titles
.eligible_config()
.map_err(anyhow::Error::msg)?;
saved = Some(settings);
Ok(())
})?;
saved.ok_or_else(|| anyhow::anyhow!("settings update produced no result"))
}
fn parse_editor_settings(raw: Value) -> anyhow::Result<Settings> {
let (document, _) = parse_settings_document_with_session_settings(raw)?;
validate_editor_settings(&document.settings)?;
Ok(document.settings)
}
fn validate_editor_settings(settings: &Settings) -> anyhow::Result<()> {
if settings.compaction.provider.is_some() || settings.compaction.model.is_some() {
settings
.compaction
.resolve_config("", "")
.map_err(anyhow::Error::msg)?;
}
let image = &settings.tools.view_image;
validate_view_image_max_image_bytes(image.max_image_bytes)?;
if let Some(model) = &image.vision_model {
validate_view_image_identifier("provider", &model.provider)?;
validate_view_image_identifier("model", &model.model)?;
}
anyhow::ensure!(
settings.tools.subagents.max_depth
== super::tools::clamp_subagent_max_depth(settings.tools.subagents.max_depth),
"agent.subagents.execution.max_depth must be between 1 and {}",
crate::subagents::MAX_SUBAGENT_MAX_DEPTH
);
for (field, value) in [
(
"sessions.titles.provider",
&settings.session_titles.provider,
),
("sessions.titles.model", &settings.session_titles.model),
] {
if let Some(value) = value {
validate_model_identifier(field, value)?;
}
}
Ok(())
}