use super::super::*;
use super::MissionControlApp;
use crate::config::{Settings, SettingsListKind, SettingsScope};
#[derive(Debug, Clone)]
pub(super) enum SettingsChange {
Toggle {
kind: SettingsListKind,
scope: SettingsScope,
name: String,
enabled: bool,
},
Mcp {
name: String,
enabled: bool,
},
Thinking(crate::thinking::ThinkingLevel),
PrimaryAgent(Option<String>),
}
impl SettingsChange {
fn persist(&self, paths: &crate::config::McPaths) -> anyhow::Result<Settings> {
match self {
Self::Toggle {
kind,
scope,
name,
enabled,
} => {
match kind {
SettingsListKind::Skills => {
crate::config::set_skill_disabled_for_scope(paths, *scope, name, !enabled)?
}
SettingsListKind::Tools => {
crate::config::set_tool_disabled(paths, *scope, name, !enabled)?
}
SettingsListKind::Subagents => {
crate::config::set_subagent_profile_disabled(paths, *scope, name, !enabled)?
}
SettingsListKind::Models => {
crate::config::set_model_disabled_for_scope(paths, *scope, name, !enabled)?
}
};
}
Self::Mcp { name, enabled } => {
crate::config::set_mcp_server_enabled(paths, name, *enabled)?
}
Self::Thinking(level) => {
crate::config::set_thinking_level(paths, *level)?;
}
Self::PrimaryAgent(id) => {
crate::config::set_selected_primary_agent(paths, id.as_deref())?;
}
}
crate::config::read_settings(paths)
}
fn set_row(&self, ui: &mut state::MissionControlState, saved: bool) {
match self {
Self::Toggle {
kind,
scope,
name,
enabled,
} => {
let value = if saved { *enabled } else { !enabled };
match kind {
SettingsListKind::Skills if ui.skills_modal_scope() == Some(*scope) => {
ui.set_skill_row_enabled(name, value)
}
SettingsListKind::Tools if ui.tools_modal_scope() == Some(*scope) => {
ui.set_tool_row_enabled(name, value)
}
SettingsListKind::Subagents if ui.subagents_modal_scope() == Some(*scope) => {
ui.set_subagent_profile_row_enabled(name, value)
}
SettingsListKind::Models if ui.models_modal_scope() == Some(*scope) => {
ui.set_model_row_enabled(name, value)
}
_ => {}
}
}
Self::Mcp { name, enabled } => {
ui.set_mcp_server_row_enabled(name, if saved { *enabled } else { !enabled })
}
_ => {}
}
}
}
struct SettingsWorker {
request_id: u64,
change: SettingsChange,
handle: super::completion_worker::CompletionWorker<Settings>,
}
#[derive(Default)]
pub(super) struct SettingsPersistenceRuntime {
next_request_id: u64,
worker: Option<SettingsWorker>,
retained_failure: Option<String>,
}
impl SettingsPersistenceRuntime {
pub(super) fn is_pending(&self) -> bool {
self.worker.is_some()
}
pub(super) fn cleanup(&mut self) -> Vec<String> {
let mut errors = self
.retained_failure
.clone()
.into_iter()
.collect::<Vec<_>>();
if let Some(worker) = self.worker.take() {
let deadline = Instant::now() + WORKER_EXIT_JOIN_TIMEOUT;
while !worker.handle.is_finished() && Instant::now() < deadline {
thread::sleep(Duration::from_millis(5));
}
if !worker.handle.is_finished() {
errors.push(format!(
"settings persistence request {} did not finish before shutdown",
worker.request_id
));
} else if let Err(error) = worker.handle.join() {
errors.push(error);
}
}
errors
}
}
impl MissionControlApp {
pub(super) fn start_settings_persistence(
&mut self,
change: SettingsChange,
ui: &mut state::MissionControlState,
) {
change.set_row(ui, false);
if matches!(change, SettingsChange::Thinking(_)) {
ui.refresh_thinking_levels(self.config.thinking_level, ui.thinking_levels.clone());
}
if matches!(change, SettingsChange::PrimaryAgent(_)) {
ui.set_selected_primary_agent_id(self.settings.selected_primary_agent.as_deref());
}
if self.pending_model_selection.is_some() {
ui.status = "model selection pending; settings action blocked".into();
return;
}
if self.settings_persistence.is_pending() {
ui.status = "settings persistence already in progress".into();
return;
}
if self.settings_persistence.retained_failure.is_some() {
ui.status =
"settings persistence failed; restart Mission Control before retrying".into();
return;
}
let paths = self.config.paths.clone();
let task = change.clone();
let disabled_tools = Arc::clone(&self.disabled_tools);
let disabled_subagents = Arc::clone(&self.disabled_subagent_profiles);
let handle = super::completion_worker::CompletionWorker::spawn(
"magi-settings-persistence",
self.events.clone(),
move || {
panic::catch_unwind(panic::AssertUnwindSafe(|| -> anyhow::Result<Settings> {
let settings = task.persist(&paths)?;
if matches!(
task,
SettingsChange::Toggle {
kind: SettingsListKind::Skills
| SettingsListKind::Tools
| SettingsListKind::Subagents,
..
}
) {
let mut tools = disabled_tools
.lock()
.map_err(|_| anyhow::anyhow!("disabled tools lock poisoned"))?;
let mut subagents = disabled_subagents.lock().map_err(|_| {
anyhow::anyhow!("disabled subagent profiles lock poisoned")
})?;
*tools = crate::config::disabled_tool_names_from_settings(&settings)
.into_iter()
.collect();
*subagents =
crate::config::disabled_subagent_profile_names_from_settings(&settings)
.into_iter()
.collect();
}
Ok(settings)
}))
.map_err(|_| "settings persistence worker panicked".to_string())
.and_then(|r| r.map_err(|e| e.to_string()))
},
);
match handle {
Ok(handle) => {
self.settings_persistence.next_request_id += 1;
self.settings_persistence.worker = Some(SettingsWorker {
request_id: self.settings_persistence.next_request_id,
change,
handle,
});
ui.status = "saving settingsā¦".into();
}
Err(error) => ui.status = format!("failed to start settings persistence: {error}"),
}
}
pub(in crate::tui) fn reap_settings_persistence_worker(
&mut self,
ui: &mut state::MissionControlState,
) -> bool {
let Some(worker) = self.settings_persistence.worker.as_mut() else {
return false;
};
let result = worker.handle.take_result();
let change = worker.change.clone();
if worker.handle.ready_to_reap()
&& let Some(worker) = self.settings_persistence.worker.take()
{
let _ = worker.handle.join();
}
let Some(result) = result else {
return false;
};
match result {
Ok(settings) => {
change.set_row(ui, true);
match &change {
SettingsChange::Toggle { kind, .. } => match kind {
SettingsListKind::Models => self.settings.models = settings.models,
_ => {
self.skills = filter_enabled_skills(
&self.discovered_skills,
&crate::config::disabled_skill_names_from_settings(&settings),
)
}
},
SettingsChange::Thinking(level) => {
self.config.thinking_level = *level;
self.summarizer.refresh_config(&self.config, &self.settings);
if let Some(config) = &mut self.state.config {
config.thinking_level = *level;
}
ui.refresh_thinking_levels(*level, ui.thinking_levels.clone());
}
SettingsChange::PrimaryAgent(_) => {
self.settings.selected_primary_agent = settings.selected_primary_agent;
ui.set_selected_primary_agent_id(
self.settings.selected_primary_agent.as_deref(),
);
}
_ => {}
}
ui.status = match change {
SettingsChange::Mcp { .. } => {
"MCP server setting saved; takes effect next magi-code launch".into()
}
SettingsChange::Thinking(_) => {
format!("thinking level selected: {}", ui.thinking_label())
}
SettingsChange::PrimaryAgent(Some(id)) => {
format!("primary agent selected: {id}")
}
SettingsChange::PrimaryAgent(None) => "primary agent selection cleared".into(),
SettingsChange::Toggle {
kind,
scope,
name,
enabled,
} => format!(
"{} {} in {} settings: {name}",
match kind {
SettingsListKind::Skills => "skill",
SettingsListKind::Tools => "tool",
SettingsListKind::Subagents => "subagent",
SettingsListKind::Models => "model",
},
if enabled { "enabled" } else { "disabled" },
scope.label().to_ascii_lowercase(),
),
};
}
Err(error) => {
ui.status = format!(
"failed to save setting: {error}; restart Mission Control before retrying"
);
self.settings_persistence.retained_failure = Some(error);
}
}
true
}
}