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.compaction.claude_subscription.allow_compaction",
"agent.compaction.claude_subscription.reserve_tokens",
"agent.compaction.claude_subscription.threshold_percent",
"agent.compaction.claude_subscription.threshold_tokens",
"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.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",
"capabilities.tools.magi_control.compact",
"capabilities.tools.code_mode.enabled",
"capabilities.tools.code_mode.compact_direct_tools",
"capabilities.tools.code_mode.nudges_enabled",
"capabilities.tools.code_mode.disabled",
"capabilities.tools.code_mode.limits.max_source_bytes",
"capabilities.tools.code_mode.limits.max_inner_calls",
"capabilities.tools.code_mode.limits.max_runtime_ms",
"capabilities.tools.code_mode.limits.max_memory_bytes",
"capabilities.tools.code_mode.limits.max_inner_result_bytes",
"capabilities.tools.code_mode.limits.max_total_result_bytes",
"capabilities.tools.code_mode.limits.max_return_bytes",
"internal_tooling.jev.enabled",
"internal_tooling.jev.show_debug_messages",
"internal_tooling.jev.completion_verification.enabled",
"internal_tooling.jev.completion_verification.enforcement_enabled",
"internal_tooling.jev.completion_verification.threshold",
"internal_tooling.jev.completion_verification.max_state_bytes",
"internal_tooling.jev.completion_verification.max_continuations",
"internal_tooling.jev.completion_verification.failure_policy",
"internal_tooling.jev.bash_protection.enabled",
"internal_tooling.jev.bash_protection.protection_level",
"internal_tooling.jev.bash_protection.failure_policy",
"internal_tooling.jev.bash_file_tracking.enabled",
"internal_tooling.jev.humanize_protection.enabled",
"internal_tooling.jev.humanize_protection.content_threshold",
"internal_tooling.jev.humanize_protection.humanized_threshold",
"internal_tooling.jev.humanize_protection.failure_policy",
"internal_tooling.jev.prompt_injection_protection.enabled",
"internal_tooling.jev.prompt_injection_protection.enforcement_enabled",
"internal_tooling.jev.prompt_injection_protection.protection_level",
"internal_tooling.jev.prompt_injection_protection.max_assessed_bytes",
"internal_tooling.jev.prompt_injection_protection.failure_policy",
"internal_tooling.jev.prompt_injection_protection.tools",
"internal_tooling.jev.skill_suggestion.enabled",
"internal_tooling.jev.skill_suggestion.gate_threshold",
"internal_tooling.jev.skill_suggestion.fit_threshold",
"internal_tooling.jev.compaction_timing.enabled",
"internal_tooling.jev.compaction_timing.soft_threshold_percent",
"internal_tooling.jev.compaction_timing.threshold",
"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);
}
update_editor_settings(paths, scope, |raw| {
for (path, value) in changes {
set_path(raw, &path.split('.').collect::<Vec<_>>(), value.clone())?;
}
Ok(())
})
}
pub(crate) fn set_code_mode_tool_enabled(
paths: &McPaths,
scope: SettingsScope,
name: &str,
enabled: bool,
) -> anyhow::Result<Settings> {
let canonical = crate::tools::ToolRuntime::canonical_disabled_key(name)?;
update_editor_settings(paths, scope, |raw| {
let effective = match scope {
SettingsScope::Global => raw.clone(),
SettingsScope::Project => merge_local_settings_values(
read_settings_json_or_empty(&paths.settings_file)?,
raw.clone(),
)?,
};
let mut disabled = parse_editor_settings(effective)?.tools.code_mode.disabled;
disabled.retain(|entry| {
crate::tools::ToolRuntime::canonical_disabled_key(entry)
.is_ok_and(|entry| entry != canonical)
});
if !enabled {
disabled.push(canonical);
}
set_path(
raw,
&["capabilities", "tools", "code_mode", "disabled"],
serde_json::json!(disabled),
)
})
}
fn update_editor_settings(
paths: &McPaths,
scope: SettingsScope,
mutate: impl FnOnce(&mut Value) -> anyhow::Result<()>,
) -> anyhow::Result<Settings> {
let mut saved = None;
update_settings_json(paths, scope, |raw| {
mutate(raw)?;
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<()> {
settings.tools.code_mode.validate()?;
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(())
}