diff --git a/apps/codex-plus-launcher/src/main.rs b/apps/codex-plus-launcher/src/main.rs index c5ceda66..08ce1531 100644 --- a/apps/codex-plus-launcher/src/main.rs +++ b/apps/codex-plus-launcher/src/main.rs @@ -179,9 +179,10 @@ impl LaunchHooks for LauncherHooks { app_dir: &Path, debug_port: u16, extra_args: &[String], + windows_codex_launch_mode: codex_plus_core::settings::WindowsCodexLaunchMode, ) -> anyhow::Result { self.core - .launch_codex(app_dir, debug_port, extra_args) + .launch_codex(app_dir, debug_port, extra_args, windows_codex_launch_mode) .await } diff --git a/apps/codex-plus-manager/src/App.tsx b/apps/codex-plus-manager/src/App.tsx index 475f7df1..8cd4c08a 100644 --- a/apps/codex-plus-manager/src/App.tsx +++ b/apps/codex-plus-manager/src/App.tsx @@ -94,6 +94,7 @@ type OverviewResult = CommandResult<{ type BackendSettings = { codexAppPath: string; codexExtraArgs: string[]; + windowsCodexLaunchMode: WindowsCodexLaunchMode; providerSyncEnabled: boolean; enhancementsEnabled: boolean; codexGoalsEnabled: boolean; @@ -112,6 +113,7 @@ type BackendSettings = { }; type LaunchMode = "patch" | "relay"; +type WindowsCodexLaunchMode = "packagedActivation" | "directProcess"; type RelayProfile = { id: string; @@ -374,6 +376,7 @@ const routes: Array<{ id: Route; label: string; icon: LucideIcon }> = [ const defaultSettings: BackendSettings = { codexAppPath: "", codexExtraArgs: [], + windowsCodexLaunchMode: "packagedActivation", providerSyncEnabled: false, enhancementsEnabled: true, codexGoalsEnabled: false, @@ -2061,6 +2064,22 @@ function SettingsScreen({ />

每行一个参数,例如 --force_high_performance_gpu。不需要填写 open 或 --args。

+ +

+ 开启后,WindowsApps 版 Codex 会跳过系统应用激活,先关闭旧 Codex 进程,再直接启动官方 Codex.exe。 +

@@ -3826,6 +3845,7 @@ function normalizeSettings(settings: BackendSettings): BackendSettings { return syncLegacyRelayFields({ ...defaultSettings, ...settings, + windowsCodexLaunchMode: normalizeWindowsCodexLaunchMode(settings.windowsCodexLaunchMode), relayCommonConfigContents, relayContextConfigContents, relayProfiles: profiles, @@ -3841,6 +3861,10 @@ function inputToCodexExtraArgs(value: string) { return value === "" ? [] : value.split(/\r?\n/); } +function normalizeWindowsCodexLaunchMode(value: string | undefined): WindowsCodexLaunchMode { + return value === "directProcess" ? "directProcess" : "packagedActivation"; +} + function normalizeRelayProfile(profile: RelayProfile, defaultContextSelection = emptyContextSelection()): RelayProfile { const legacyMixedApi = profile.relayMode === "mixedApi"; let normalized: RelayProfile = { diff --git a/crates/codex-plus-core/src/launcher.rs b/crates/codex-plus-core/src/launcher.rs index 93974823..178e3f7f 100644 --- a/crates/codex-plus-core/src/launcher.rs +++ b/crates/codex-plus-core/src/launcher.rs @@ -13,7 +13,9 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::process::{Child, Command}; use tokio::sync::Mutex; -use crate::settings::{BackendSettings, SettingsStore, normalize_codex_extra_args}; +use crate::settings::{ + BackendSettings, SettingsStore, WindowsCodexLaunchMode, normalize_codex_extra_args, +}; use crate::status::{LaunchStatus, StatusStore}; #[derive(Debug, Clone, PartialEq, Eq)] @@ -134,6 +136,7 @@ pub trait LaunchHooks: Send + Sync { app_dir: &Path, debug_port: u16, extra_args: &[String], + windows_codex_launch_mode: WindowsCodexLaunchMode, ) -> anyhow::Result; async fn bridge_context( &self, @@ -216,7 +219,12 @@ where } let launch = hooks - .launch_codex(&app_dir, debug_port, &settings.codex_extra_args) + .launch_codex( + &app_dir, + debug_port, + &settings.codex_extra_args, + settings.windows_codex_launch_mode, + ) .await?; launched = Some(launch.clone()); @@ -399,9 +407,21 @@ impl LaunchHooks for DefaultLaunchHooks { app_dir: &Path, debug_port: u16, extra_args: &[String], + windows_codex_launch_mode: WindowsCodexLaunchMode, ) -> anyhow::Result { if cfg!(windows) { - if let Some(activation) = build_packaged_activation(app_dir, debug_port, extra_args) { + if should_stop_existing_codex_before_direct_windows_launch( + app_dir, + windows_codex_launch_mode, + ) { + crate::watcher::stop_codex_processes(); + } + + if should_use_packaged_activation(app_dir, windows_codex_launch_mode) { + let Some(activation) = build_packaged_activation(app_dir, debug_port, extra_args) + else { + unreachable!("packaged activation precondition was checked"); + }; let CodexLaunch::PackagedActivation { app_user_model_id, arguments, @@ -1024,6 +1044,22 @@ pub fn build_packaged_activation( }) } +pub fn should_use_packaged_activation( + app_dir: &Path, + windows_codex_launch_mode: WindowsCodexLaunchMode, +) -> bool { + windows_codex_launch_mode == WindowsCodexLaunchMode::PackagedActivation + && crate::app_paths::packaged_app_user_model_id(app_dir).is_some() +} + +pub fn should_stop_existing_codex_before_direct_windows_launch( + app_dir: &Path, + windows_codex_launch_mode: WindowsCodexLaunchMode, +) -> bool { + windows_codex_launch_mode == WindowsCodexLaunchMode::DirectProcess + && crate::app_paths::packaged_app_user_model_id(app_dir).is_some() +} + pub fn codex_process_environment() -> HashMap { let env = std::env::vars().collect::>(); codex_process_environment_from(&env, crate::proxy::detect_system_proxy) diff --git a/crates/codex-plus-core/src/settings.rs b/crates/codex-plus-core/src/settings.rs index cadd17c5..fbc3adde 100644 --- a/crates/codex-plus-core/src/settings.rs +++ b/crates/codex-plus-core/src/settings.rs @@ -13,6 +13,14 @@ pub enum LaunchMode { Relay, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub enum WindowsCodexLaunchMode { + #[default] + PackagedActivation, + DirectProcess, +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct RelayContextSelection { @@ -132,6 +140,8 @@ pub struct BackendSettings { pub codex_app_path: String, #[serde(rename = "codexExtraArgs", default)] pub codex_extra_args: Vec, + #[serde(rename = "windowsCodexLaunchMode", default)] + pub windows_codex_launch_mode: WindowsCodexLaunchMode, #[serde(rename = "providerSyncEnabled", default)] pub provider_sync_enabled: bool, #[serde(rename = "enhancementsEnabled", default = "default_true")] @@ -173,6 +183,7 @@ impl Default for BackendSettings { Self { codex_app_path: String::new(), codex_extra_args: Vec::new(), + windows_codex_launch_mode: WindowsCodexLaunchMode::PackagedActivation, provider_sync_enabled: false, enhancements_enabled: true, codex_goals_enabled: false, @@ -418,20 +429,28 @@ fn merge_known_setting_fields(target: &mut Map, source: &Map anyhow::Result { Ok(CodexLaunch::Process { command: vec!["codex".to_string()], diff --git a/crates/codex-plus-core/tests/launcher.rs b/crates/codex-plus-core/tests/launcher.rs index 72dfd33a..43c14409 100644 --- a/crates/codex-plus-core/tests/launcher.rs +++ b/crates/codex-plus-core/tests/launcher.rs @@ -11,13 +11,16 @@ use codex_plus_core::launcher::{ CodexLaunch, DefaultLaunchHooks, LaunchHooks, LaunchOptions, MacosCleanupPolicy, build_codex_arguments, build_codex_command, build_macos_cleanup_command, build_macos_open_command, build_packaged_activation, codex_process_environment_from, - launch_and_inject_with_hooks, with_temporary_proxy_environment, + launch_and_inject_with_hooks, should_stop_existing_codex_before_direct_windows_launch, + should_use_packaged_activation, with_temporary_proxy_environment, }; #[cfg(windows)] use codex_plus_core::launcher::{WindowsProcessControlStrategy, windows_process_control_strategy}; use codex_plus_core::ports::select_platform_loopback_port_with; use codex_plus_core::proxy::has_proxy_environment; -use codex_plus_core::settings::{BackendSettings, RelayProfile, RelayProtocol}; +use codex_plus_core::settings::{ + BackendSettings, RelayProfile, RelayProtocol, WindowsCodexLaunchMode, +}; use codex_plus_core::status::StatusStore; #[test] @@ -259,6 +262,48 @@ fn launcher_packaged_activation_can_preserve_process_id() { assert_eq!(launch.process_id(), Some(4242)); } +#[test] +fn launcher_uses_packaged_activation_by_default_for_windows_packages() { + let app_dir = PathBuf::from( + r"C:\Program Files\WindowsApps\OpenAI.Codex_26.506.2212.0_x64__2p2nqsd0c76g0\app", + ); + + assert!(should_use_packaged_activation( + &app_dir, + WindowsCodexLaunchMode::PackagedActivation + )); + assert!(!should_stop_existing_codex_before_direct_windows_launch( + &app_dir, + WindowsCodexLaunchMode::PackagedActivation + )); +} + +#[test] +fn launcher_direct_windows_mode_skips_packaged_activation_and_requires_clean_slate() { + let app_dir = PathBuf::from( + r"C:\Program Files\WindowsApps\OpenAI.Codex_26.506.2212.0_x64__2p2nqsd0c76g0\app", + ); + + assert!(!should_use_packaged_activation( + &app_dir, + WindowsCodexLaunchMode::DirectProcess + )); + assert!(should_stop_existing_codex_before_direct_windows_launch( + &app_dir, + WindowsCodexLaunchMode::DirectProcess + )); +} + +#[test] +fn launcher_direct_windows_mode_does_not_stop_portable_codex_instances() { + let app_dir = PathBuf::from(r"C:\Portable\Codex\app"); + + assert!(!should_stop_existing_codex_before_direct_windows_launch( + &app_dir, + WindowsCodexLaunchMode::DirectProcess + )); +} + #[cfg(windows)] #[test] fn launcher_windows_packaged_process_management_uses_native_api() { @@ -1115,6 +1160,7 @@ impl LaunchHooks for FakeHooks { app_dir: &Path, debug_port: u16, extra_args: &[String], + _windows_codex_launch_mode: WindowsCodexLaunchMode, ) -> anyhow::Result { assert!(app_dir.ends_with("Codex.app")); if extra_args.is_empty() {