diff --git a/src-tauri/src/codex_desktop.rs b/src-tauri/src/codex_desktop.rs
index 8487235d..67e29904 100644
--- a/src-tauri/src/codex_desktop.rs
+++ b/src-tauri/src/codex_desktop.rs
@@ -10,6 +10,10 @@ use sha2::{Digest, Sha256};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
+#[cfg(target_os = "windows")]
+#[path = "codex_windows_launch.rs"]
+pub(crate) mod windows_launch;
+
pub(crate) const DEFAULT_CODEX_DEBUG_PORT: u16 = 9229;
pub(crate) const CDP_HTTP_TIMEOUT: Duration = Duration::from_secs(2);
const CDP_CONNECT_TIMEOUT: Duration = Duration::from_secs(4);
@@ -1776,6 +1780,10 @@ pub(crate) fn launch_codex_with_debug_port(
);
}
}
+ #[cfg(target_os = "windows")]
+ if let Some(app_id) = windows_launch::resolve_app_id(executable)? {
+ return launch_windows_app(&app_id, debug_port);
+ }
let mut command = Command::new(executable);
append_codex_debug_args(&mut command, debug_port);
apply_codex_launch_timezone(&mut command, launch_timezone.as_deref());
@@ -1802,13 +1810,25 @@ fn apply_codex_launch_timezone(command: &mut Command, timezone: Option<&str>) {
}
}
+#[cfg(target_os = "windows")]
+pub(crate) fn launch_windows_app(app_id: &str, debug_port: u16) -> Result<(), String> {
+ windows_launch::activate(app_id, debug_port)?;
+ // The activation broker does not inherit our per-child environment. Renderer
+ // timezone emulation is applied separately by install_script over CDP.
+ crate::codex_egress_timezone::mark_codex_timezone_not_inherited();
+ Ok(())
+}
+
+fn codex_debug_args(debug_port: u16) -> [String; 2] {
+ [
+ format!("--remote-debugging-port={debug_port}"),
+ format!("--remote-allow-origins=http://127.0.0.1:{debug_port}"),
+ ]
+}
+
/// 为 Desktop 启动命令追加 Chromium remote-debugging 参数。
fn append_codex_debug_args(command: &mut Command, debug_port: u16) {
- command
- .arg(format!("--remote-debugging-port={debug_port}"))
- .arg(format!(
- "--remote-allow-origins=http://127.0.0.1:{debug_port}"
- ));
+ command.args(codex_debug_args(debug_port));
}
/// Windows 下查找 Codex App 主进程的脚本。
@@ -2665,6 +2685,21 @@ fn version_tuple_from_package_name(name: &str) -> Vec {
mod tests {
use super::*;
+ #[test]
+ fn desktop_launch_transports_share_the_requested_debug_port() {
+ let mut command = Command::new("codex-desktop-placeholder");
+ append_codex_debug_args(&mut command, 9231);
+ let arguments: Vec<_> = command
+ .get_args()
+ .map(|arg| arg.to_string_lossy())
+ .collect();
+ assert_eq!(arguments, codex_debug_args(9231));
+ assert_eq!(
+ codex_debug_args(9231).join(" "),
+ "--remote-debugging-port=9231 --remote-allow-origins=http://127.0.0.1:9231"
+ );
+ }
+
/// 返回当前测试平台的 Desktop 主程序文件名。
fn desktop_test_executable_name() -> &'static str {
if cfg!(target_os = "windows") {
diff --git a/src-tauri/src/codex_egress_timezone.rs b/src-tauri/src/codex_egress_timezone.rs
index c40d912e..2352fd43 100644
--- a/src-tauri/src/codex_egress_timezone.rs
+++ b/src-tauri/src/codex_egress_timezone.rs
@@ -57,6 +57,7 @@ pub enum CodexEgressMonitorState {
Checking,
Ready,
RestartRequired,
+ RendererOnly,
Error,
}
@@ -67,6 +68,7 @@ pub(crate) struct CodexEgressMonitorRuntime {
pub last_trigger: Option,
pub last_error: Option,
pub restart_required: bool,
+ pub process_timezone_unavailable: bool,
pub consecutive_failures: u32,
}
@@ -98,11 +100,16 @@ pub(crate) fn monitor_status_from_parts(
runtime: &CodexEgressMonitorRuntime,
_now: i64,
) -> CodexEgressMonitorStatus {
- let state = if settings.mode != CodexEgressTimezoneMode::Auto {
+ let process_timezone_unavailable =
+ runtime.process_timezone_unavailable && settings.mode != CodexEgressTimezoneMode::Off;
+ let restart_required = runtime.restart_required && !process_timezone_unavailable;
+ let state = if process_timezone_unavailable {
+ CodexEgressMonitorState::RendererOnly
+ } else if settings.mode != CodexEgressTimezoneMode::Auto {
CodexEgressMonitorState::Disabled
} else if runtime.running {
CodexEgressMonitorState::Checking
- } else if runtime.restart_required {
+ } else if restart_required {
CodexEgressMonitorState::RestartRequired
} else if runtime.last_error.is_some() {
CodexEgressMonitorState::Error
@@ -133,7 +140,7 @@ pub(crate) fn monitor_status_from_parts(
.map(|at| at.saturating_add(i64::from(interval) * 60))
},
monitor_interval_minutes: interval,
- restart_required: runtime.restart_required,
+ restart_required,
}
}
@@ -816,6 +823,22 @@ pub(crate) fn notify_proxy_failure(app_type: &str, error: &ProxyError) {
pub(crate) fn mark_codex_timezone_applied() {
let applied_timezone = resolve_launch_timezone(&crate::settings::get_settings());
+ record_codex_launch_timezone(applied_timezone, false);
+}
+
+#[cfg(target_os = "windows")]
+pub(crate) fn mark_codex_timezone_not_inherited() {
+ let configured = resolve_launch_timezone(&crate::settings::get_settings());
+ if configured.is_some() {
+ log::warn!("Codex MSIX activation cannot inherit TZ; process timezone remains unapplied. Renderer timezone emulation will be attempted over CDP.");
+ }
+ record_codex_launch_timezone(None, true);
+}
+
+fn record_codex_launch_timezone(
+ applied_timezone: Option,
+ process_timezone_unavailable: bool,
+) {
if let Err(error) = crate::settings::mutate_codex_egress_timezone(|settings| {
settings.last_applied_timezone = applied_timezone;
settings.last_applied_at = Some(Utc::now().timestamp());
@@ -826,6 +849,7 @@ pub(crate) fn mark_codex_timezone_applied() {
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
runtime.restart_required = false;
+ runtime.process_timezone_unavailable = process_timezone_unavailable;
drop(runtime);
emit_monitor_status(None);
}
@@ -836,16 +860,21 @@ pub(crate) fn start_automatic_monitor(app_handle: AppHandle) {
return;
}
let settings = crate::settings::get_settings().codex_egress_timezone;
- if settings.mode == CodexEgressTimezoneMode::Auto
- && settings.detected_timezone.is_some()
- && settings.detected_timezone != settings.last_applied_timezone
- && crate::codex_desktop::is_codex_desktop_running()
- {
- monitor_runtime()
+ let running = crate::codex_desktop::detect_running_codex_main_process();
+ #[cfg(target_os = "windows")]
+ let packaged = running
+ .as_deref()
+ .is_some_and(crate::codex_desktop::windows_launch::is_packaged_codex);
+ #[cfg(not(target_os = "windows"))]
+ let packaged = false;
+ initialize_monitor_launch_state(
+ &mut monitor_runtime()
.lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner())
- .restart_required = true;
- }
+ .unwrap_or_else(|poisoned| poisoned.into_inner()),
+ &settings,
+ running.is_some(),
+ packaged,
+ );
tauri::async_runtime::spawn(async move {
let mut last_tick = Instant::now();
loop {
@@ -864,6 +893,20 @@ pub(crate) fn start_automatic_monitor(app_handle: AppHandle) {
});
}
+pub(crate) fn initialize_monitor_launch_state(
+ runtime: &mut CodexEgressMonitorRuntime,
+ settings: &CodexEgressTimezoneSettings,
+ codex_running: bool,
+ packaged: bool,
+) {
+ runtime.process_timezone_unavailable = codex_running && packaged;
+ runtime.restart_required = !runtime.process_timezone_unavailable
+ && codex_running
+ && settings.mode == CodexEgressTimezoneMode::Auto
+ && settings.detected_timezone.is_some()
+ && settings.detected_timezone != settings.last_applied_timezone;
+}
+
#[tauri::command]
pub fn get_codex_egress_timezone_monitor_status() -> CodexEgressMonitorStatus {
current_monitor_status()
diff --git a/src-tauri/src/codex_egress_timezone_tests.rs b/src-tauri/src/codex_egress_timezone_tests.rs
index fbb56517..2104c329 100644
--- a/src-tauri/src/codex_egress_timezone_tests.rs
+++ b/src-tauri/src/codex_egress_timezone_tests.rs
@@ -368,6 +368,58 @@ fn monitor_status_exposes_failure_backoff_as_the_next_check() {
assert_eq!(status.state, CodexEgressMonitorState::Error);
assert_eq!(status.next_check_at, Some(2_180));
}
+
+#[test]
+fn package_activation_reports_renderer_only_without_a_refresh_loop() {
+ let runtime = CodexEgressMonitorRuntime {
+ process_timezone_unavailable: true,
+ restart_required: true,
+ ..CodexEgressMonitorRuntime::default()
+ };
+ for mode in [
+ CodexEgressTimezoneMode::Auto,
+ CodexEgressTimezoneMode::Manual,
+ ] {
+ let settings = CodexEgressTimezoneSettings {
+ mode,
+ ..CodexEgressTimezoneSettings::default()
+ };
+ let status = monitor_status_from_parts(&settings, &runtime, 2_010);
+ assert_eq!(status.state, CodexEgressMonitorState::RendererOnly);
+ assert!(!status.restart_required);
+ }
+ let status =
+ monitor_status_from_parts(&CodexEgressTimezoneSettings::default(), &runtime, 2_010);
+ assert_eq!(status.state, CodexEgressMonitorState::Disabled);
+}
+
+#[test]
+fn restarting_ccsm_preserves_the_running_package_timezone_limitation() {
+ let settings = CodexEgressTimezoneSettings {
+ mode: CodexEgressTimezoneMode::Auto,
+ detected_timezone: Some("Asia/Taipei".into()),
+ ..CodexEgressTimezoneSettings::default()
+ };
+ let mut runtime = CodexEgressMonitorRuntime::default();
+ super::codex_egress_timezone::initialize_monitor_launch_state(
+ &mut runtime,
+ &settings,
+ true,
+ true,
+ );
+ assert_eq!(
+ monitor_status_from_parts(&settings, &runtime, 0).state,
+ CodexEgressMonitorState::RendererOnly
+ );
+ assert!(!runtime.restart_required);
+ super::codex_egress_timezone::initialize_monitor_launch_state(
+ &mut runtime,
+ &settings,
+ true,
+ false,
+ );
+ assert!(runtime.restart_required);
+}
#[tokio::test]
#[ignore = "Explicit network diagnostic; never run as a regular test gate"]
async fn live_trace_transport_diagnostic() {
diff --git a/src-tauri/src/codex_runtime_refresh.rs b/src-tauri/src/codex_runtime_refresh.rs
index 83a4f40c..16343a36 100644
--- a/src-tauri/src/codex_runtime_refresh.rs
+++ b/src-tauri/src/codex_runtime_refresh.rs
@@ -851,31 +851,10 @@ async fn query_refresh_targets() -> Result {
Ok(classify_refresh_targets(&processes))
}
-#[cfg(target_os = "windows")]
-fn resolve_windows_codex_aumid() -> Option {
- let output = powershell_utf8_output(
- r#"
-[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
-Get-StartApps |
- Where-Object { $_.AppID -match '^OpenAI\.Codex(?:\.Preview)?_.*!App$' } |
- Select-Object -First 1 -ExpandProperty AppID
-"#,
- )
- .ok()?;
- let aumid = output.lines().next()?.trim();
- (!aumid.is_empty()).then(|| aumid.to_string())
-}
-
fn select_launch_target(
aumid: Option,
executable: Option,
- timezone_injection_enabled: bool,
) -> Option {
- if timezone_injection_enabled {
- if let Some(executable) = executable.clone() {
- return Some(CodexRuntimeLaunchTarget::DesktopExecutable(executable));
- }
- }
#[cfg(target_os = "windows")]
if let Some(aumid) = aumid {
return Some(CodexRuntimeLaunchTarget::WindowsAumid(aumid));
@@ -885,16 +864,16 @@ fn select_launch_target(
executable.map(CodexRuntimeLaunchTarget::DesktopExecutable)
}
-fn resolve_launch_target() -> Option {
+fn resolve_launch_target() -> Result
)}
+ {status?.state === "renderer_only" && (
+
+ Windows 应用包启动无法传入进程时区,只会尝试同步页面时区;
+ app-server 时区未覆盖,重复刷新无法解除此限制。
+
+ )}
{(requestError || status?.lastError) && (