diff --git a/apps/src/app/platform-mode/use-platform-mode-state.ts b/apps/src/app/platform-mode/use-platform-mode-state.ts index 57d765d3d..8ea9761b7 100644 --- a/apps/src/app/platform-mode/use-platform-mode-state.ts +++ b/apps/src/app/platform-mode/use-platform-mode-state.ts @@ -28,14 +28,22 @@ const RELOAD_AFTER_SWITCH_STORAGE_KEY = "codexmanager.platform-mode.reload-after-switch"; const RELOAD_AFTER_SWITCH_EVENT = "codexmanager:platform-mode-reload-after-switch"; +const SAFE_RELOAD_DEFAULT_MIGRATION_KEY = + "codexmanager.platform-mode.reload-safe-default-v2"; -let reloadAfterSwitchMemoryValue = true; +let reloadAfterSwitchMemoryValue = false; function getReloadAfterSwitchPreference(): boolean { if (typeof window === "undefined") { return reloadAfterSwitchMemoryValue; } try { + if (window.localStorage.getItem(SAFE_RELOAD_DEFAULT_MIGRATION_KEY) !== "1") { + // Older versions enabled process termination by default. Reset that inherited preference + // once; users can explicitly opt in again after the migration. + window.localStorage.setItem(RELOAD_AFTER_SWITCH_STORAGE_KEY, "false"); + window.localStorage.setItem(SAFE_RELOAD_DEFAULT_MIGRATION_KEY, "1"); + } const stored = window.localStorage.getItem(RELOAD_AFTER_SWITCH_STORAGE_KEY); if (stored === "true" || stored === "false") { reloadAfterSwitchMemoryValue = stored === "true"; @@ -115,7 +123,7 @@ export function usePlatformModePageState( const reloadAfterSwitch = useSyncExternalStore( subscribeToReloadAfterSwitchPreference, getReloadAfterSwitchPreference, - () => true, + () => false, ); const browserOrigin = useSyncExternalStore( () => () => undefined, diff --git a/apps/tests/codex-profile-cache.test.mjs b/apps/tests/codex-profile-cache.test.mjs index 4aedaa0c5..21a337890 100644 --- a/apps/tests/codex-profile-cache.test.mjs +++ b/apps/tests/codex-profile-cache.test.mjs @@ -103,6 +103,8 @@ test("平台模式切换透传并持久化 Codex 后台重载开关", async () = const tauri = await readSource("src-tauri/src/commands/codex_profile.rs"); assert.match(state, /codexmanager\.platform-mode\.reload-after-switch/); + assert.match(state, /reloadAfterSwitchMemoryValue = false/); + assert.match(state, /reload-safe-default-v2/); assert.match(state, /reloadAfterSwitch,/); assert.match(sections, /切换后重载 Codex 后台/); assert.match(client, /reloadAfterSwitch: params\.reloadAfterSwitch/); diff --git a/crates/service/src/codex_profile.rs b/crates/service/src/codex_profile.rs index 929a95e75..f0e6ee543 100644 --- a/crates/service/src/codex_profile.rs +++ b/crates/service/src/codex_profile.rs @@ -353,17 +353,13 @@ pub(crate) fn apply_direct_account( account_id: Some(account.id.clone()), api_key_id: None, gateway_base_url: None, - provider_id: PROVIDER_ID.to_string(), + provider_id: DEFAULT_HISTORY_PROVIDER_ID.to_string(), previous_model_catalog_json: None, updated_at: now_ts(), }, )?; persist_codex_home(&profile_dir)?; - status_for_profile_after_apply( - &profile_dir, - DEFAULT_HISTORY_PROVIDER_ID, - reload_after_switch, - ) + status_for_profile_after_apply(&profile_dir, None, reload_after_switch) } pub(crate) fn apply_gateway( @@ -432,7 +428,7 @@ pub(crate) fn apply_gateway( }, )?; persist_codex_home(&profile_dir)?; - status_for_profile_after_apply(&profile_dir, PROVIDER_ID, reload_after_switch) + status_for_profile_after_apply(&profile_dir, Some(PROVIDER_ID), reload_after_switch) } pub(crate) fn restore(codex_home: Option<&str>) -> Result { @@ -814,10 +810,16 @@ fn status_for_profile_with_history_repair( fn status_for_profile_after_apply( profile_dir: &Path, - target_provider: &str, + history_provider: Option<&str>, reload_after_switch: bool, ) -> Result { - let mut status = status_for_profile_with_history_repair(profile_dir, target_provider)?; + // Direct mode keeps a secret-free `cm` compatibility provider, so existing conversations do + // not need a synchronous full-profile history rewrite. Gateway mode still repairs history so + // existing conversations are routed through the managed provider. + let mut status = match history_provider { + Some(provider) => status_for_profile_with_history_repair(profile_dir, provider)?, + None => status_for_profile(profile_dir)?, + }; status.runtime_reload = Some(if reload_after_switch { crate::codex_runtime::reload_codex_app_servers(profile_dir) } else { @@ -1971,23 +1973,22 @@ fn patch_config_for_direct( previous_model_catalog_json: Option<&str>, ) -> Result { let mut doc = parse_config(content.as_deref().unwrap_or(""))?; - if doc - .get("model_provider") - .and_then(Item::as_str) - .is_some_and(|provider| provider == PROVIDER_ID) - { - doc.as_table_mut().remove("model_provider"); + doc.as_table_mut() + .insert("model_provider", toml_value(DEFAULT_HISTORY_PROVIDER_ID)); + if doc.as_table().get("model_providers").is_none() { + doc.as_table_mut() + .insert("model_providers", Item::Table(Table::new())); } - if let Some(providers) = doc + let providers = doc .as_table_mut() .get_mut("model_providers") .and_then(Item::as_table_mut) - { - providers.remove(PROVIDER_ID); - if providers.is_empty() { - doc.as_table_mut().remove("model_providers"); - } - } + .ok_or_else(|| "config.toml model_providers is not a table".to_string())?; + let mut compatibility_provider = Table::new(); + compatibility_provider.insert("name", toml_value("OpenAI compatibility")); + compatibility_provider.insert("wire_api", toml_value("responses")); + compatibility_provider.insert("requires_openai_auth", toml_value(true)); + providers.insert(PROVIDER_ID, Item::Table(compatibility_provider)); let managed_catalog_path = managed_catalog_path.to_string_lossy(); let catalog_is_managed = doc .get("model_catalog_json") diff --git a/crates/service/src/codex_profile_tests.rs b/crates/service/src/codex_profile_tests.rs index 1a332b776..1677fc42c 100644 --- a/crates/service/src/codex_profile_tests.rs +++ b/crates/service/src/codex_profile_tests.rs @@ -119,7 +119,7 @@ fn sqlite_provider(dir: &Path, thread_id: &str) -> String { } #[test] -fn direct_config_removes_only_managed_provider() { +fn direct_config_uses_openai_and_keeps_secret_free_legacy_provider() { let input = r#" model_provider = "cm" model = "gpt-5.4" @@ -128,6 +128,8 @@ model = "gpt-5.4" name = "CodexManager" base_url = "http://localhost:48760/v1" wire_api = "responses" +experimental_bearer_token = "must-be-removed" +custom_header = "must-also-be-removed" [model_providers.other] name = "Other" @@ -138,8 +140,13 @@ base_url = "https://example.test/v1" let output = patch_config_for_direct(Some(input.to_string()), &managed_catalog, None) .expect("patch direct"); - assert!(!output.contains("model_provider = \"cm\"")); - assert!(!output.contains("[model_providers.cm]")); + assert!(output.contains("model_provider = \"openai\"")); + assert!(output.contains("[model_providers.cm]")); + assert!(output.contains("wire_api = \"responses\"")); + assert!(output.contains("requires_openai_auth = true")); + assert!(!output.contains("base_url = \"http://localhost:48760/v1\"")); + assert!(!output.contains("experimental_bearer_token")); + assert!(!output.contains("custom_header")); assert!(output.contains("[model_providers.other]")); assert!(output.contains("model = \"gpt-5.4\"")); }