diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 5046f1c9..098ee1ac 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -5363,33 +5363,45 @@ async fn switch_provider( }) .await; - app.add_message(HistoryCell::System { - content: format!( - "Provider switched: {} → {}\nModel: {} → {}\nEndpoint: {}", - previous_provider.as_str(), - target.as_str(), - previous_model, - new_model, - new_endpoint - ), - }); - app.status_message = Some(format!( - "Provider: {} via {}", - target.as_str(), - new_endpoint - )); + let persist_warning = (|| -> anyhow::Result<()> { + commands::persist_root_string_key(app.config_path.as_deref(), "provider", target.as_str())?; - // Persist the provider choice so it survives restarts. - if let Ok(mut settings) = crate::settings::Settings::load() { + let mut settings = crate::settings::Settings::load()?; settings.default_provider = Some(target.as_str().to_string()); if model_override.is_some() { settings.set_model_for_provider(target.as_str(), &new_model); if matches!(target, ApiProvider::Deepseek | ApiProvider::DeepseekCN) { - let _ = settings.set("default_model", &new_model); + settings.set("default_model", &new_model)?; } } - let _ = settings.save(); + settings.save()?; + Ok(()) + })() + .err() + .map(|err| format!("Provider selection was not fully persisted: {err}")); + + let mut switch_summary = format!( + "Provider switched: {} → {}", + previous_provider.as_str(), + target.as_str(), + ); + switch_summary.push(char::from(10)); + switch_summary.push_str(&format!("Model: {} → {}", previous_model, new_model)); + switch_summary.push(char::from(10)); + switch_summary.push_str(&format!("Endpoint: {}", new_endpoint)); + if let Some(ref warning) = persist_warning { + switch_summary.push(char::from(10)); + switch_summary.push_str(warning); } + app.add_message(HistoryCell::System { + content: switch_summary, + }); + + let mut status_message = format!("Provider: {} via {}", target.as_str(), new_endpoint); + if persist_warning.is_some() { + status_message.push_str(" (not fully persisted)"); + } + app.status_message = Some(status_message); } fn root_base_url_belongs_to_non_deepseek_provider(base_url: &str) -> bool { diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index c16dde91..5c916fdd 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -2237,6 +2237,57 @@ async fn provider_switch_to_deepseek_drops_stale_xiaomi_root_base_url() { assert_eq!(config.base_url, None); } +#[tokio::test] +async fn provider_switch_persists_provider_to_config_for_restart() { + let _home = SettingsHomeGuard::new(); + let tmp = TempDir::new().expect("config tempdir"); + let config_path = tmp.path().join("config.toml"); + std::fs::write( + &config_path, + r#"provider = "arcee" + +[providers.xiaomi_mimo] +base_url = "https://token-plan-sgp.xiaomimimo.com/v1" +model = "mimo-v2.5-pro" +api_key = "mimo-key" + +[providers.arcee] +api_key = "arcee-key" +"#, + ) + .expect("write config"); + + let mut app = create_test_app(); + app.api_provider = ApiProvider::Arcee; + app.model = "auto".to_string(); + app.config_path = Some(config_path.clone()); + + let mut engine = mock_engine_handle(); + let mut config = Config::load(Some(config_path.clone()), None).expect("load config"); + + switch_provider( + &mut app, + &mut engine.handle, + &mut config, + ApiProvider::XiaomiMimo, + None, + ) + .await; + + assert_eq!(app.api_provider, ApiProvider::XiaomiMimo); + assert_eq!(config.provider.as_deref(), Some("xiaomi-mimo")); + + let reloaded = Config::load(Some(config_path.clone()), None).expect("reload config"); + assert_eq!(reloaded.api_provider(), ApiProvider::XiaomiMimo); + assert_eq!( + reloaded.deepseek_base_url(), + "https://token-plan-sgp.xiaomimimo.com/v1" + ); + + let settings = crate::settings::Settings::load().expect("load settings"); + assert_eq!(settings.default_provider.as_deref(), Some("xiaomi-mimo")); +} + #[tokio::test] async fn provider_switch_model_override_updates_target_provider_model_slot() { let _home = SettingsHomeGuard::new();