Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<codex_plus_core::launcher::CodexLaunch> {
self.core
.launch_codex(app_dir, debug_port, extra_args)
.launch_codex(app_dir, debug_port, extra_args, windows_codex_launch_mode)
.await
}

Expand Down
24 changes: 24 additions & 0 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type OverviewResult = CommandResult<{
type BackendSettings = {
codexAppPath: string;
codexExtraArgs: string[];
windowsCodexLaunchMode: WindowsCodexLaunchMode;
providerSyncEnabled: boolean;
enhancementsEnabled: boolean;
codexGoalsEnabled: boolean;
Expand All @@ -112,6 +113,7 @@ type BackendSettings = {
};

type LaunchMode = "patch" | "relay";
type WindowsCodexLaunchMode = "packagedActivation" | "directProcess";

type RelayProfile = {
id: string;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -2061,6 +2064,22 @@ function SettingsScreen({
/>
</Field>
<p className="field-hint">每行一个参数,例如 --force_high_performance_gpu。不需要填写 open 或 --args。</p>
<label className="check-row">
<input
checked={form.windowsCodexLaunchMode === "directProcess"}
onChange={(event) =>
onFormChange({
...form,
windowsCodexLaunchMode: event.currentTarget.checked ? "directProcess" : "packagedActivation",
})
}
type="checkbox"
/>
<span>Windows 使用管理员直启官方 Codex</span>
</label>
<p className="field-hint">
开启后,WindowsApps 版 Codex 会跳过系统应用激活,先关闭旧 Codex 进程,再直接启动官方 Codex.exe。
</p>
<Toolbar>
<Button onClick={() => void actions.saveSettings()}>保存设置</Button>
</Toolbar>
Expand Down Expand Up @@ -3826,6 +3845,7 @@ function normalizeSettings(settings: BackendSettings): BackendSettings {
return syncLegacyRelayFields({
...defaultSettings,
...settings,
windowsCodexLaunchMode: normalizeWindowsCodexLaunchMode(settings.windowsCodexLaunchMode),
relayCommonConfigContents,
relayContextConfigContents,
relayProfiles: profiles,
Expand All @@ -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 = {
Expand Down
42 changes: 39 additions & 3 deletions crates/codex-plus-core/src/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<CodexLaunch>;
async fn bridge_context(
&self,
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -399,9 +407,21 @@ impl LaunchHooks for DefaultLaunchHooks {
app_dir: &Path,
debug_port: u16,
extra_args: &[String],
windows_codex_launch_mode: WindowsCodexLaunchMode,
) -> anyhow::Result<CodexLaunch> {
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,
Expand Down Expand Up @@ -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<String, String> {
let env = std::env::vars().collect::<HashMap<_, _>>();
codex_process_environment_from(&env, crate::proxy::detect_system_proxy)
Expand Down
53 changes: 48 additions & 5 deletions crates/codex-plus-core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -132,6 +140,8 @@ pub struct BackendSettings {
pub codex_app_path: String,
#[serde(rename = "codexExtraArgs", default)]
pub codex_extra_args: Vec<String>,
#[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")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -418,20 +429,28 @@ fn merge_known_setting_fields(target: &mut Map<String, Value>, source: &Map<Stri
),
);
}
if let Some(value) = source.get("windowsCodexLaunchMode").and_then(Value::as_str)
&& matches!(value, "packagedActivation" | "directProcess")
{
target.insert(
"windowsCodexLaunchMode".to_string(),
Value::String(value.to_string()),
);
}
if let Some(value) = source.get("providerSyncEnabled").and_then(Value::as_bool) {
target.insert("providerSyncEnabled".to_string(), Value::Bool(value));
}
if let Some(value) = source.get("enhancementsEnabled").and_then(Value::as_bool) {
target.insert("enhancementsEnabled".to_string(), Value::Bool(value));
}
if let Some(value) = source.get("launchMode").and_then(Value::as_str)
&& matches!(value, "patch" | "relay")
{
target.insert("launchMode".to_string(), Value::String(value.to_string()));
}
if let Some(value) = source.get("codexGoalsEnabled").and_then(Value::as_bool) {
target.insert("codexGoalsEnabled".to_string(), Value::Bool(value));
}
if let Some(value) = source.get("launchMode").and_then(Value::as_str) {
if matches!(value, "patch" | "relay") {
target.insert("launchMode".to_string(), Value::String(value.to_string()));
}
}
if let Some(value) = source.get("relayBaseUrl").and_then(Value::as_str) {
target.insert("relayBaseUrl".to_string(), Value::String(value.to_string()));
}
Expand Down Expand Up @@ -632,6 +651,10 @@ mod tests {
assert!(!settings.codex_goals_enabled);
assert!(settings.codex_app_path.is_empty());
assert!(settings.codex_extra_args.is_empty());
assert_eq!(
settings.windows_codex_launch_mode,
WindowsCodexLaunchMode::PackagedActivation
);
assert_eq!(settings.launch_mode, LaunchMode::Patch);
assert_eq!(settings.relay_base_url, default_relay_base_url());
assert!(settings.relay_api_key.is_empty());
Expand All @@ -657,6 +680,10 @@ mod tests {
assert_eq!(settings.cli_wrapper_api_key_env, "CUSTOM_OPENAI_API_KEY");
assert_eq!(settings.relay_base_url, default_relay_base_url());
assert!(settings.codex_extra_args.is_empty());
assert_eq!(
settings.windows_codex_launch_mode,
WindowsCodexLaunchMode::PackagedActivation
);
}

#[test]
Expand All @@ -675,6 +702,17 @@ mod tests {
);
}

#[test]
fn settings_deserialize_reads_windows_codex_launch_mode() {
let settings: BackendSettings =
serde_json::from_str(r#"{"windowsCodexLaunchMode":"directProcess"}"#).unwrap();

assert_eq!(
settings.windows_codex_launch_mode,
WindowsCodexLaunchMode::DirectProcess
);
}

#[test]
fn relay_profile_official_mix_api_key_defaults_to_false() {
let profile: RelayProfile =
Expand Down Expand Up @@ -905,6 +943,7 @@ requires_openai_auth = true
"providerSyncEnabled": true,
"codexAppPath": "C:\\Portable\\Codex\\Codex.exe",
"enhancementsEnabled": false,
"windowsCodexLaunchMode": "directProcess",
"codexGoalsEnabled": true,
"relayBaseUrl": "https://relay.example.test/v1",
"relayApiKey": "sk-relay",
Expand All @@ -927,6 +966,10 @@ requires_openai_auth = true
"--enable-gpu".to_string(),
]
);
assert_eq!(
updated.windows_codex_launch_mode,
WindowsCodexLaunchMode::DirectProcess
);
assert!(updated.cli_wrapper_enabled);
assert_eq!(updated.cli_wrapper_base_url, "https://old.test");
assert_eq!(updated.cli_wrapper_api_key, "old-key");
Expand Down
3 changes: 2 additions & 1 deletion crates/codex-plus-core/tests/bridge_routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use codex_plus_core::routes::{
BridgeContext, BridgeDataService, BridgeRuntimeService, BridgeSettingsService,
CoreRuntimeService, handle_bridge_request,
};
use codex_plus_core::settings::BackendSettings;
use codex_plus_core::settings::{BackendSettings, WindowsCodexLaunchMode};
use codex_plus_core::status::StatusStore;
use codex_plus_core::user_scripts::UserScriptManager;
use serde_json::{Value, json};
Expand Down Expand Up @@ -1108,6 +1108,7 @@ impl LaunchHooks for ContextHooks {
_app_dir: &std::path::Path,
_debug_port: u16,
_extra_args: &[String],
_windows_codex_launch_mode: WindowsCodexLaunchMode,
) -> anyhow::Result<CodexLaunch> {
Ok(CodexLaunch::Process {
command: vec!["codex".to_string()],
Expand Down
50 changes: 48 additions & 2 deletions crates/codex-plus-core/tests/launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -1115,6 +1160,7 @@ impl LaunchHooks for FakeHooks {
app_dir: &Path,
debug_port: u16,
extra_args: &[String],
_windows_codex_launch_mode: WindowsCodexLaunchMode,
) -> anyhow::Result<CodexLaunch> {
assert!(app_dir.ends_with("Codex.app"));
if extra_args.is_empty() {
Expand Down