From b758465bb7ad8810013ee415d82ede1e91c09532 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Sun, 17 May 2026 15:10:55 -0600 Subject: [PATCH 01/16] chore(desktop): add "NewInstance" action to .desktop files and update packaging scripts --- .../.local/share/applications/codex-desktop.desktop | 5 +++++ packaging/linux/codex-desktop.desktop | 6 +++++- scripts/lib/package-common.sh | 12 +++++++++++- tests/scripts_smoke.sh | 11 +++++++++-- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop index 19d5c3c3..b1f1a85c 100644 --- a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop +++ b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop @@ -12,3 +12,8 @@ Keywords=codex;openai;ai;coding; StartupNotify=true StartupWMClass=codex-desktop Icon=codex-desktop +Actions=NewInstance; + +[Desktop Action NewInstance] +Name=Open New Instance +Exec=env CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop %U diff --git a/packaging/linux/codex-desktop.desktop b/packaging/linux/codex-desktop.desktop index bce60717..74b775e0 100644 --- a/packaging/linux/codex-desktop.desktop +++ b/packaging/linux/codex-desktop.desktop @@ -10,7 +10,11 @@ MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar; StartupNotify=true StartupWMClass=Codex X-GNOME-WMClass=Codex -Actions=CheckForUpdates;InstallReadyUpdate; +Actions=NewInstance;CheckForUpdates;InstallReadyUpdate; + +[Desktop Action NewInstance] +Name=Open New Instance +Exec=env CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop %u [Desktop Action CheckForUpdates] Name=Check for Updates diff --git a/scripts/lib/package-common.sh b/scripts/lib/package-common.sh index cb01e6df..d1fdf08e 100755 --- a/scripts/lib/package-common.sh +++ b/scripts/lib/package-common.sh @@ -97,12 +97,22 @@ render_desktop_entry() { mv "$rendered_target" "$target" else awk ' + BEGIN { actions_rewritten = 0 } /^\[Desktop Action CheckForUpdates\]$/ { skip = 1; next } /^\[Desktop Action InstallReadyUpdate\]$/ { skip = 1; next } /^\[/ { skip = 0 } skip { next } - /^Actions=/ { next } + /^Actions=/ { + print "Actions=NewInstance;" + actions_rewritten = 1 + next + } { print } + END { + if (actions_rewritten == 0) { + print "Actions=NewInstance;" + } + } ' "$rendered_target" > "$target" rm -f "$rendered_target" fi diff --git a/tests/scripts_smoke.sh b/tests/scripts_smoke.sh index af19ada2..97c5beee 100755 --- a/tests/scripts_smoke.sh +++ b/tests/scripts_smoke.sh @@ -329,8 +329,11 @@ SCRIPT assert_not_contains "$pkg_root/DEBIAN/control" "polkit" assert_not_contains "$pkg_root/DEBIAN/control" "Local auto-updates" assert_contains "$pkg_root/DEBIAN/control" "without codex-update-manager" - assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Actions=CheckForUpdates" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Actions=NewInstance;" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Desktop Action NewInstance" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop %u" assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Desktop Action CheckForUpdates" + assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "InstallReadyUpdate" assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "codex-update-manager" assert_not_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" "systemctl" assert_not_contains "$pkg_root/opt/codex-desktop/.codex-linux/codex-packaged-runtime.sh" "codex-update-manager" @@ -1496,11 +1499,15 @@ PY assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "StartupWMClass=Codex" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "X-GNOME-WMClass=Codex" - assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "Actions=CheckForUpdates;InstallReadyUpdate;" + assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "Actions=NewInstance;CheckForUpdates;InstallReadyUpdate;" + assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "[Desktop Action NewInstance]" + assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop %u" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "codex-update-manager check-now" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "codex-update-manager install-ready" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "@HOME@/.local/bin/codex-desktop %U" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;" + assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "Actions=NewInstance;" + assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop %U" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" "CODEX_USER_LOCAL_OZONE_PLATFORM" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" 'exec "${APP_DIR}/start.sh" --x11 "$@"' assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" 'exec "${APP_DIR}/start.sh" --wayland "$@"' From 6048dea87c9c14e14ccb3a39477f82252b6fd76c Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:42:15 -0600 Subject: [PATCH 02/16] chore(updater): add cache cleanup utilities and integrate workspace pruning logic --- updater/src/app.rs | 63 +++++++++++++++++++++++++++++++++++++++-- updater/src/builder.rs | 29 +++++++++++++++++++ updater/src/main.rs | 1 + updater/src/rollback.rs | 9 ++++-- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/updater/src/app.rs b/updater/src/app.rs index 7bbfbd33..7380f11e 100644 --- a/updater/src/app.rs +++ b/updater/src/app.rs @@ -1,7 +1,7 @@ //! Application entrypoints and orchestration for the local updater daemon. use crate::{ - builder, + builder, cache_cleanup, cli::{Cli, Commands}, codex_cli, config::{RuntimeConfig, RuntimePaths}, @@ -106,6 +106,35 @@ fn sync_and_persist( persist_if_changed(paths, state, &original_state) } +fn normalize_workspace_dir_and_persist( + state: &mut PersistedState, + paths: &RuntimePaths, +) -> Result<()> { + let original_state = state.clone(); + cache_cleanup::normalize_artifact_workspace_dir(&paths.cache_dir, state); + persist_if_changed(paths, state, &original_state) +} + +fn maybe_prune_workspace_cache(workspace_root: &Path, state: &PersistedState) { + match cache_cleanup::prune_unreferenced_workspaces(workspace_root, state) { + Ok(summary) if summary.pruned_workspaces > 0 => { + info!( + pruned_workspaces = summary.pruned_workspaces, + workspace_root = %workspace_root.display(), + "pruned unreferenced updater workspaces" + ); + } + Ok(_) => {} + Err(error) => { + warn!( + ?error, + workspace_root = %workspace_root.display(), + "failed to prune unreferenced updater workspaces" + ); + } + } +} + fn set_status( state: &mut PersistedState, paths: &RuntimePaths, @@ -192,6 +221,8 @@ async fn run_daemon( sync_and_persist(config, state, paths)?; recover_interrupted_install(state, paths)?; codex_cli::reconcile_if_present(state, paths)?; + normalize_workspace_dir_and_persist(state, paths)?; + maybe_prune_workspace_cache(&config.workspace_root, state); maybe_notify_cli_missing(state, paths, config.notifications)?; maybe_notify_installed(state, paths, config.notifications)?; if packaged_runtime_removed(config) { @@ -250,6 +281,8 @@ async fn run_check_now( sync_and_persist(config, state, paths)?; recover_interrupted_install(state, paths)?; codex_cli::reconcile_if_present(state, paths)?; + normalize_workspace_dir_and_persist(state, paths)?; + maybe_prune_workspace_cache(&config.workspace_root, state); maybe_notify_cli_missing(state, paths, config.notifications)?; maybe_notify_installed(state, paths, config.notifications)?; if if_stale && upstream_check_is_fresh(config, state) { @@ -272,6 +305,7 @@ fn upstream_check_is_fresh(config: &RuntimeConfig, state: &PersistedState) -> bo fn run_status(state: &mut PersistedState, paths: &RuntimePaths, json: bool) -> Result<()> { codex_cli::reconcile_if_present(state, paths)?; complete_pending_install_if_already_installed(state, paths)?; + normalize_workspace_dir_and_persist(state, paths)?; if json { println!("{}", serde_json::to_string_pretty(state)?); @@ -611,6 +645,7 @@ async fn run_check_cycle( .clone() .expect("candidate version should be set before local build"); builder::build_update(config, state, paths, &candidate_version, &downloaded.path).await?; + maybe_prune_workspace_cache(&config.workspace_root, state); maybe_notify_update_ready(state, paths, config.notifications)?; Ok(()) } @@ -618,6 +653,7 @@ async fn run_check_cycle( if let Err(error) = result { mark_failed_and_persist(state, paths, error.to_string())?; + maybe_prune_workspace_cache(&config.workspace_root, state); let _ = notify_failure(config, state, paths, &error); return Err(error); } @@ -703,7 +739,7 @@ async fn reconcile_pending_install( return Ok(()); } - trigger_install(state, paths, &package_path).await?; + trigger_install(state, paths, &config.workspace_root, &package_path).await?; } _ => {} } @@ -792,7 +828,7 @@ async fn run_install_ready( } clear_install_auth_required_event(state, paths)?; - trigger_install(state, paths, &package_path).await + trigger_install(state, paths, &config.workspace_root, &package_path).await } fn complete_pending_install_if_already_installed( @@ -822,6 +858,7 @@ fn complete_pending_install_if_already_installed( } state.error_message = None; state.notified_events.clear(); + cache_cleanup::normalize_artifact_workspace_dir(&paths.cache_dir, state); persist_state(paths, state)?; info!("recovered pending install state because the candidate version is already installed or superseded"); Ok(true) @@ -845,6 +882,7 @@ fn recover_interrupted_install(state: &mut PersistedState, paths: &RuntimePaths) } state.error_message = None; state.notified_events.clear(); + cache_cleanup::normalize_artifact_workspace_dir(&paths.cache_dir, state); persist_state(paths, state)?; info!("recovered interrupted install state because the candidate version is already installed"); return Ok(()); @@ -874,6 +912,7 @@ fn recover_interrupted_install(state: &mut PersistedState, paths: &RuntimePaths) state.status = UpdateStatus::ReadyToInstall; state.error_message = Some("Previous install attempt was interrupted before completion".to_string()); + cache_cleanup::normalize_artifact_workspace_dir(&paths.cache_dir, state); persist_state(paths, state)?; info!(package = %package_path.display(), "recovered interrupted install state back to ready_to_install"); Ok(()) @@ -1056,6 +1095,7 @@ fn maybe_send_notification(enabled: bool, summary: &str, body: &str) { async fn trigger_install( state: &mut PersistedState, paths: &RuntimePaths, + workspace_root: &Path, package_path: &Path, ) -> Result<()> { state.status = UpdateStatus::Installing; @@ -1080,8 +1120,10 @@ async fn trigger_install( state.rollback_blocked_candidate_version = None; state.error_message = None; state.notified_events.clear(); + cache_cleanup::normalize_artifact_workspace_dir(workspace_root, state); persist_state(paths, state)?; let _ = maybe_notify_installed(state, paths, true); + maybe_prune_workspace_cache(workspace_root, state); return Ok(()); } @@ -1764,6 +1806,10 @@ mod tests { let superseded_package_path = temp.path().join("superseded.deb"); std::fs::write(&superseded_package_path, b"deb")?; state.artifact_paths.package_path = Some(superseded_package_path); + state.artifact_paths.workspace_dir = Some( + temp.path() + .join("cache/workspaces/2026.04.28.082247+abcdef12"), + ); assert!(complete_pending_install_if_already_installed( &mut state, &paths @@ -1772,6 +1818,7 @@ mod tests { assert_eq!(state.status, UpdateStatus::Installed); assert_eq!(state.candidate_version, None); assert_eq!(state.artifact_paths.package_path, None); + assert_eq!(state.artifact_paths.workspace_dir, None); assert_eq!(state.error_message, None); crate::rollback::record_current_package_as_known_good(&mut state); assert_eq!(state.artifact_paths.rollback_package_path, None); @@ -1799,6 +1846,10 @@ mod tests { let superseded_package_path = temp.path().join("superseded-status.deb"); std::fs::write(&superseded_package_path, b"deb")?; state.artifact_paths.package_path = Some(superseded_package_path); + state.artifact_paths.workspace_dir = Some( + temp.path() + .join("cache/workspaces/2026.04.28.082247+abcdef12"), + ); let original_home = std::env::var_os("HOME"); let original_path = std::env::var_os("PATH"); @@ -1845,6 +1896,7 @@ mod tests { assert_eq!(state.status, UpdateStatus::Installed); assert_eq!(state.candidate_version, None); assert_eq!(state.artifact_paths.package_path, None); + assert_eq!(state.artifact_paths.workspace_dir, None); Ok(()) } @@ -1980,12 +2032,17 @@ mod tests { state.installed_version = "2026.04.01.035152".to_string(); state.candidate_version = Some("2026.03.27.025604+1086e799".to_string()); state.artifact_paths.package_path = Some(package_path); + state.artifact_paths.workspace_dir = Some( + temp.path() + .join("cache/workspaces/2026.03.27.025604+1086e799"), + ); recover_interrupted_install(&mut state, &paths)?; assert_eq!(state.status, UpdateStatus::Installed); assert_eq!(state.candidate_version, None); assert_eq!(state.artifact_paths.package_path, None); + assert_eq!(state.artifact_paths.workspace_dir, None); assert_eq!(state.error_message, None); Ok(()) } diff --git a/updater/src/builder.rs b/updater/src/builder.rs index 73b28a86..d851d7b2 100644 --- a/updater/src/builder.rs +++ b/updater/src/builder.rs @@ -90,6 +90,14 @@ pub async fn build_update( Command::new(workspace.bundle_dir.join("install.sh")) .arg(dmg_path) .env("CODEX_INSTALL_DIR", &workspace.app_dir) + .env( + "CODEX_PATCH_REPORT_JSON", + workspace.reports_dir.join("patch-report.json"), + ) + .env( + "CODEX_REBUILD_REPORT_JSON", + workspace.reports_dir.join("rebuild-report.json"), + ) .env( "CODEX_MANAGED_NODE_SOURCE", config.builder_bundle_root.join("node-runtime"), @@ -147,6 +155,7 @@ struct BuilderWorkspace { bundle_dir: PathBuf, dist_dir: PathBuf, app_dir: PathBuf, + reports_dir: PathBuf, install_log: PathBuf, build_log: PathBuf, } @@ -158,6 +167,7 @@ impl BuilderWorkspace { let dist_dir = workspace_dir.join("dist"); let app_dir = workspace_dir.join("codex-app"); let logs_dir = workspace_dir.join("logs"); + let reports_dir = workspace_dir.join("reports"); let install_log = logs_dir.join("install.log"); let build_log = logs_dir.join("build-package.log"); @@ -168,12 +178,15 @@ impl BuilderWorkspace { fs::create_dir_all(&logs_dir) .with_context(|| format!("Failed to create {}", logs_dir.display()))?; + fs::create_dir_all(&reports_dir) + .with_context(|| format!("Failed to create {}", reports_dir.display()))?; Ok(Self { workspace_dir, bundle_dir, dist_dir, app_dir, + reports_dir, install_log, build_log, }) @@ -599,6 +612,14 @@ set -euo pipefail mkdir -p "${CODEX_INSTALL_DIR}" echo launcher > "${CODEX_INSTALL_DIR}/start.sh" chmod +x "${CODEX_INSTALL_DIR}/start.sh" +if [ -n "${CODEX_PATCH_REPORT_JSON:-}" ]; then + mkdir -p "$(dirname "$CODEX_PATCH_REPORT_JSON")" + printf '{"patches":[]}\n' > "${CODEX_PATCH_REPORT_JSON}" +fi +if [ -n "${CODEX_REBUILD_REPORT_JSON:-}" ]; then + mkdir -p "$(dirname "$CODEX_REBUILD_REPORT_JSON")" + printf '{"appDir":"%s"}\n' "${CODEX_INSTALL_DIR}" > "${CODEX_REBUILD_REPORT_JSON}" +fi "#, )?; #[cfg(unix)] @@ -698,6 +719,14 @@ chmod +x "${CODEX_INSTALL_DIR}/start.sh" .workspace_dir .join("builder/linux-features/features.example.json") .exists()); + assert!(artifacts + .workspace_dir + .join("reports/patch-report.json") + .exists()); + assert!(artifacts + .workspace_dir + .join("reports/rebuild-report.json") + .exists()); assert!( is_native_package_file(&artifacts.package_path), "expected a native package (.deb, .rpm, or .pkg.tar.zst), got {}", diff --git a/updater/src/main.rs b/updater/src/main.rs index 991231ea..0a8299de 100644 --- a/updater/src/main.rs +++ b/updater/src/main.rs @@ -2,6 +2,7 @@ mod app; mod builder; +mod cache_cleanup; mod cli; mod codex_cli; mod config; diff --git a/updater/src/rollback.rs b/updater/src/rollback.rs index 4c714086..9a6aae1a 100644 --- a/updater/src/rollback.rs +++ b/updater/src/rollback.rs @@ -1,6 +1,7 @@ //! Manual rollback support for the local update manager. use crate::{ + cache_cleanup, config::{RuntimeConfig, RuntimePaths}, install, install_rollback, liveness, notify, state::{PersistedState, UpdateStatus}, @@ -57,10 +58,11 @@ pub async fn run( return Ok(()); } - trigger_rollback(state, paths, &package_path).await + trigger_rollback(config, state, paths, &package_path).await } async fn trigger_rollback( + config: &RuntimeConfig, state: &mut PersistedState, paths: &RuntimePaths, package_path: &Path, @@ -94,6 +96,7 @@ async fn trigger_rollback( blocked_candidate, ); state.save(&paths.state_file)?; + let _ = cache_cleanup::prune_unreferenced_workspaces(&config.workspace_root, state); println!("Rolled back Codex Desktop to {}.", state.installed_version); return Ok(()); } @@ -146,6 +149,7 @@ fn apply_successful_rollback_state( state.rollback_blocked_candidate_version = blocked_candidate; state.error_message = None; state.notified_events.clear(); + cache_cleanup::normalize_artifact_workspace_dir(Path::new(""), state); } #[cfg(test)] @@ -226,7 +230,7 @@ mod tests { state.status = UpdateStatus::Installing; state.artifact_paths = ArtifactPaths { dmg_path: None, - workspace_dir: None, + workspace_dir: Some(temp.path().join("workspaces/2026.05.04.131500+badcafe0")), package_path: Some(update_path), rollback_package_path: Some(rollback_path.clone()), }; @@ -248,6 +252,7 @@ mod tests { state.artifact_paths.rollback_package_path.as_deref(), Some(rollback_path.as_path()) ); + assert_eq!(state.artifact_paths.workspace_dir, None); assert_eq!( state.last_known_good_version.as_deref(), Some("2026.05.02.120000") From 89809dcc8a7210b8d39a609d0f6f487a26f365ed Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:42:45 -0600 Subject: [PATCH 03/16] chore(updater): bump version to 0.8.1 and update changelog with workspace pruning logic --- CHANGELOG.md | 1 + Cargo.lock | 2 +- README.md | 2 +- updater/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b127ec13..e65005b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Fixed +- `codex-update-manager` now prunes unreferenced updater workspaces under `~/.cache/codex-update-manager/workspaces`, removing heavy build artifacts (`builder/`, `codex-app/`, `dist/`) while preserving lightweight diagnostics such as `logs/` and rebuild reports. - The Chrome native-messaging host now evicts stale browser clients when a newer Codex browser client connects, preventing old Node REPL sessions from repeatedly reattaching CDP and driving extension service-worker CPU. - The bundled Chrome plugin is now auto-installed during app startup, matching Browser Use, so the plugin page no longer falls back to an install button after restart when the Linux native host is already staged. - Nix builds, installer apps, and dev shells now use modern `7zz`, and the installer dependency check accepts `7zz` without requiring a separate legacy `7z` binary. diff --git a/Cargo.lock b/Cargo.lock index a8f00b9a..9300fcf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,7 +486,7 @@ dependencies = [ [[package]] name = "codex-update-manager" -version = "0.8.0" +version = "0.8.1" dependencies = [ "anyhow", "chrono", diff --git a/README.md b/README.md index 9bebc44a..e13392d2 100644 --- a/README.md +++ b/README.md @@ -511,7 +511,7 @@ pacman -Qlp dist/codex-desktop-*.pkg.tar.zst | sed -n '1,40p' ## Versioning -`codex-update-manager` current crate version: `0.8.0` +`codex-update-manager` current crate version: `0.8.1` SemVer policy: diff --git a/updater/Cargo.toml b/updater/Cargo.toml index f4d18bfd..03a9a82d 100644 --- a/updater/Cargo.toml +++ b/updater/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codex-update-manager" -version = "0.8.0" +version = "0.8.1" edition = "2021" [dependencies] From 623b1c7132cc990d6898c2422eed7e213b52424f Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:44:11 -0600 Subject: [PATCH 04/16] chore(tests): add `ready-to-show` window state patch test for Linux apps --- scripts/patch-linux-window-ui.test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/scripts/patch-linux-window-ui.test.js b/scripts/patch-linux-window-ui.test.js index dbe2a7b3..f41f8033 100644 --- a/scripts/patch-linux-window-ui.test.js +++ b/scripts/patch-linux-window-ui.test.js @@ -37,6 +37,7 @@ const { applyLinuxAppSunsetPatch, applyLinuxOpaqueBackgroundPatch, applyLinuxOpaqueWindowsDefaultPatch, + applyLinuxReadyToShowWindowStatePatch, applyLinuxSetIconPatch, applyLinuxRemoteControlConfigPreservationPatch, applyLinuxSingleInstancePatch, @@ -297,6 +298,7 @@ test("default core patch descriptors are grouped and unique", () => { const ids = descriptors.map((descriptor) => descriptor.id); const expectedIds = [ "linux-quit-guard", + "linux-ready-to-show-window-state", "linux-explicit-quit-prompt-bypass", "linux-explicit-quit-drain-timeout", "linux-explicit-tray-quit", @@ -1226,6 +1228,9 @@ test("adds Linux launch actions through current setSecondInstanceArgsHandler bun assert.match(launchPatched, /codexLinuxGetSetting=e=>process\.platform!==`linux`\|\|j\.globalState\.get\(e\)!==!1/); assert.match(launchPatched, /codexLinuxStartLaunchActionSocket=\(\)=>/); + assert.match(launchPatched, /codexLinuxDefaultLaunchActionSocket=\(\)=>/); + assert.match(launchPatched, /process\.env\.CODEX_DESKTOP_LAUNCH_ACTION_SOCKET\?\.trim\(\)\|\|codexLinuxDefaultLaunchActionSocket\(\)/); + assert.match(launchPatched, /process\.env\.CODEX_LINUX_INSTANCE_ID\?\.trim\(\)/); assert.match(launchPatched, /f\.default\.createServer/); assert.match(launchPatched, /o\.mkdirSync\(i\.default\.dirname\(e\)/); assert.match(launchPatched, /R\.desktopNotificationManager\.dismissByNavigationPath\(e\)/); @@ -1262,6 +1267,7 @@ test("adds Linux launch actions when captured window identifiers contain dollar assert.match(patched, /codexLinuxHandleLaunchActionArgs=async e=>\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)\?!0:/); assert.match(patched, /codexLinuxHandleLaunchActionArgsFallback=\(e,t\)=>\{if\(typeof codexLinuxIsQuitInProgress===`function`&&codexLinuxIsQuitInProgress\(\)\)return;/); assert.match(patched, /codexLinuxStartLaunchActionSocket=\(\)=>/); + assert.match(patched, /codexLinuxDefaultLaunchActionSocket=\(\)=>/); assert.match(patched, /codexLinuxPrewarmHotkeyWindow=\(\)=>/); assert.match(patched, /e\.includes\(`--new-chat`\)/); assert.match(patched, /e\.includes\(`--quick-chat`\)/); @@ -1269,6 +1275,25 @@ test("adds Linux launch actions when captured window identifiers contain dollar assert.match(patched, /e\.includes\(`--hotkey-window`\)/); }); +test("gates ready-to-show maximize behind restored maximized state", () => { + const source = [ + "let E=x?.isMaximized===!0,D={once(){},isDestroyed(){return false},maximize(){},setIcon(){}};", + "E&&process.platform===`linux`&&D.setIcon(process.resourcesPath+`/../content/webview/assets/app-test.png`),", + "D.once(`ready-to-show`,()=>{D.isDestroyed()||D.maximize()});", + ].join(""); + + const patched = applyPatchTwice(applyLinuxReadyToShowWindowStatePatch, source); + + assert.match( + patched, + /E&&D\.once\(`ready-to-show`,\(\)=>\{D\.isDestroyed\(\)\|\|D\.maximize\(\)\}\);/, + ); + assert.doesNotMatch( + patched, + /(^|[^&])D\.once\(`ready-to-show`,\(\)=>\{D\.isDestroyed\(\)\|\|D\.maximize\(\)\}\);/, + ); +}); + test("skips the launch-action patch without throwing when upstream startup architecture changes", () => { const source = [ "async function Sg(){", From 1da142063013c87e6de97431b9d8445a875e4236 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:44:21 -0600 Subject: [PATCH 05/16] chore(patches): add `ready-to-show` window state patch integration for Linux apps --- scripts/patch-linux-window-ui.js | 2 ++ .../core/all-linux/main-process/window-shell/patch.js | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/scripts/patch-linux-window-ui.js b/scripts/patch-linux-window-ui.js index 1e85d76c..966e1951 100644 --- a/scripts/patch-linux-window-ui.js +++ b/scripts/patch-linux-window-ui.js @@ -64,6 +64,7 @@ const { applyLinuxMenuPatch, applyLinuxOpaqueBackgroundPatch, applyLinuxQuitGuardPatch, + applyLinuxReadyToShowWindowStatePatch, applyLinuxRemoteControlConfigPreservationPatch, applyLinuxSetIconPatch, applyLinuxSingleInstancePatch, @@ -167,6 +168,7 @@ module.exports = { applyLinuxOpaqueBackgroundPatch, applyLinuxOpaqueWindowsDefaultPatch, applyLinuxQuitGuardPatch, + applyLinuxReadyToShowWindowStatePatch, applyLinuxRemoteControlConfigPreservationPatch, applyLinuxSetIconPatch, applyLinuxSettingsPersistencePatch, diff --git a/scripts/patches/core/all-linux/main-process/window-shell/patch.js b/scripts/patches/core/all-linux/main-process/window-shell/patch.js index d9e30879..7903add4 100644 --- a/scripts/patches/core/all-linux/main-process/window-shell/patch.js +++ b/scripts/patches/core/all-linux/main-process/window-shell/patch.js @@ -4,6 +4,7 @@ const { applyLinuxWindowOptionsPatch, applyLinuxMenuPatch, applyLinuxSetIconPatch, + applyLinuxReadyToShowWindowStatePatch, applyLinuxOpaqueBackgroundPatch, applyLinuxFileManagerPatch, applyLinuxTrayPatch, @@ -34,6 +35,13 @@ module.exports = [ ciPolicy: "optional", apply: (source, context) => applyLinuxSetIconPatch(source, context.iconAsset), }, + { + id: "linux-ready-to-show-window-state", + phase: "main-bundle", + order: 75, + ciPolicy: "optional", + apply: applyLinuxReadyToShowWindowStatePatch, + }, { id: "linux-opaque-background", phase: "main-bundle", From 1b94d9c7ff7aff88cceb397184de4ca9191a91d3 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:44:27 -0600 Subject: [PATCH 06/16] chore(patches): enhance Linux launch actions with support for scoped app and instance IDs --- scripts/patches/launch-actions.js | 2 +- scripts/patches/main-process.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/scripts/patches/launch-actions.js b/scripts/patches/launch-actions.js index c647511b..c13800db 100644 --- a/scripts/patches/launch-actions.js +++ b/scripts/patches/launch-actions.js @@ -132,7 +132,7 @@ function buildSemanticLinuxLaunchActionPatch({ ? `process.platform===\`linux\`&&codexLinuxStartLaunchActionSocket();${setterVar}(e=>{codexLinuxHandleLaunchActionArgsFallback(e,()=>{${fallbackFn}()})});` : `process.platform===\`linux\`&&(${appVar}.app.on(\`before-quit\`,codexLinuxBeforeQuitHandler),${disposableVar}.add(()=>{${appVar}.app.off(\`before-quit\`,codexLinuxBeforeQuitHandler)}),codexLinuxStartLaunchActionSocket(),${appVar}.app.on(\`second-instance\`,codexLinuxSecondInstanceHandler),${disposableVar}.add(()=>{${appVar}.app.off(\`second-instance\`,codexLinuxSecondInstanceHandler)}));${setterVar}(e=>{codexLinuxHandleLaunchActionArgsFallback(e,()=>{${fallbackFn}()})});`; - return `${quitState}codexLinuxGetSetting=e=>process.platform!==\`linux\`||${globalStateExpr}.get(e)!==!1,codexLinuxIsTrayEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.systemTray}\`),codexLinuxIsWarmStartEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.warmStart}\`),codexLinuxIsPromptWindowEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.promptWindow}\`),${openerFn}=async(e,t)=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let ${currentWindowVar}=${windowManagerVar}.getPrimaryWindow(${hostExpr}),${createdWindowVar}=${currentWindowVar}??await ${windowManagerVar}.createFreshLocalWindow(e);${createdWindowVar}!=null&&(${notificationPrefix}${currentWindowVar}!=null&&t.navigateExistingWindow&&${routeVar}.navigateToRoute(${createdWindowVar},e),${focusFn}(${createdWindowVar}))},codexLinuxGetHotkeyWindowController=()=>typeof ${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController===\`function\`?${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController():${windowManagerVar}.hotkeyWindowLifecycleManager,codexLinuxShowHotkeyWindow=async()=>{let e=codexLinuxGetHotkeyWindowController();typeof e.openHome===\`function\`?await e.openHome():typeof e.show===\`function\`?await e.show():await ${windowManagerVar}.ensureHostWindow(${hostExpr})},codexLinuxOpenQuickChat=async()=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let e=${windowManagerVar}.getPrimaryWindow(${hostExpr}),t=e??await ${windowManagerVar}.createFreshLocalWindow(\`/\`);t!=null&&(${windowManagerVar}.windowManager.sendMessageToWindow(t,{type:\`new-quick-chat\`}),${focusFn}(t))},codexLinuxHasDeepLink=e=>Array.isArray(e)&&e.some(e=>typeof e===\`string\`&&(e.startsWith(\`codex://\`)||e.startsWith(\`codex-browser-sidebar://\`))),codexLinuxHandleLaunchActionArgs=async e=>(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())?!0:codexLinuxHasDeepLink(e)&&${deepLinksVar}.deepLinks.queueProcessArgs(e)?!0:Array.isArray(e)&&(e.includes(\`--prompt-chat\`)||e.includes(\`--hotkey-window\`))?(codexLinuxIsPromptWindowEnabled()?(await codexLinuxShowHotkeyWindow(),!0):!1):Array.isArray(e)&&e.includes(\`--quick-chat\`)?(await codexLinuxOpenQuickChat(),!0):Array.isArray(e)&&e.includes(\`--new-chat\`)?(await ${openerFn}(\`/\`,{navigateExistingWindow:!0}),!0):!1,codexLinuxHandleLaunchActionArgsFallback=(e,t)=>{if(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())return;codexLinuxHandleLaunchActionArgs(e).then(e=>{e||t()}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action\`,{kind:\`linux-launch-action-failed\`}),t()})},codexLinuxPrewarmHotkeyWindow=()=>{if(!codexLinuxIsPromptWindowEnabled())return;try{let e=codexLinuxGetHotkeyWindowController();typeof e.prewarm===\`function\`&&e.prewarm()}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to prewarm Linux hotkey window\`,{kind:\`linux-hotkey-window-prewarm-failed\`})}},codexLinuxStartLaunchActionSocket=()=>{let e=process.env.CODEX_DESKTOP_LAUNCH_ACTION_SOCKET?.trim();if(process.platform!==\`linux\`||!e||!codexLinuxIsWarmStartEnabled())return;try{${fsVar}.mkdirSync(${pathVar}.default.dirname(e),{recursive:!0,mode:448}),${fsVar}.rmSync(e,{force:!0});let t=${netVar}.default.createServer(t=>{let n=\`\`,r=!1,i=()=>{if(r)return;r=!0;let i=[];try{let e=JSON.parse(n.trim());Array.isArray(e.argv)&&(i=e.argv.filter(e=>typeof e===\`string\`))}catch(e){t.end?.(\`error\\n\`);return}codexLinuxHandleLaunchActionArgs(i).then(e=>e?void 0:${fallbackFn}()).then(()=>{t.end?.(\`ok\\n\`)}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action socket\`,{kind:\`linux-launch-action-socket-failed\`}),t.end?.(\`error\\n\`)})};t.setEncoding?.(\`utf8\`),t.on(\`data\`,e=>{n+=e,n.includes(\`\\n\`)?i():n.length>65536&&t.destroy()}),t.on(\`end\`,i)});t.on(\`error\`,e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed Linux launch action socket\`,{kind:\`linux-launch-action-socket-error\`})}),t.listen(e),${disposableVar}.add(()=>{t.close(),${fsVar}.rmSync(e,{force:!0})})}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to start Linux launch action socket\`,{kind:\`linux-launch-action-socket-start-failed\`})}}${directHandler};${startup}`; + return `${quitState}codexLinuxGetSetting=e=>process.platform!==\`linux\`||${globalStateExpr}.get(e)!==!1,codexLinuxIsTrayEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.systemTray}\`),codexLinuxIsWarmStartEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.warmStart}\`),codexLinuxIsPromptWindowEnabled=()=>codexLinuxGetSetting(\`${linuxSettingsKeys.promptWindow}\`),codexLinuxLaunchActionAppId=()=>{let e=process.env.CODEX_LINUX_APP_ID||process.env.CODEX_APP_ID||\`codex-desktop\`;return/^[A-Za-z0-9._-]+$/.test(e)?e:\`codex-desktop\`},codexLinuxLaunchActionInstanceId=()=>{let e=process.env.CODEX_LINUX_INSTANCE_ID?.trim();return e&&/^[A-Za-z0-9._-]+$/.test(e)?e:null},codexLinuxDefaultLaunchActionSocket=()=>{let e=codexLinuxLaunchActionAppId(),t=codexLinuxLaunchActionInstanceId(),n=process.env.XDG_RUNTIME_DIR?.trim();if(n&&n.length>0)return t?${pathVar}.default.join(n,e,\`instances\`,t,\`launch-action.sock\`):${pathVar}.default.join(n,e,\`launch-action.sock\`);let r=process.env.XDG_STATE_HOME?.trim(),i=process.env.HOME?.trim();if((!r||r.length===0)&&i&&i.length>0)r=${pathVar}.default.join(i,\`.local\`,\`state\`);if(!r||r.length===0)return null;return t?${pathVar}.default.join(r,e,\`instances\`,t,\`launch-action.sock\`):${pathVar}.default.join(r,e,\`launch-action.sock\`)},${openerFn}=async(e,t)=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let ${currentWindowVar}=${windowManagerVar}.getPrimaryWindow(${hostExpr}),${createdWindowVar}=${currentWindowVar}??await ${windowManagerVar}.createFreshLocalWindow(e);${createdWindowVar}!=null&&(${notificationPrefix}${currentWindowVar}!=null&&t.navigateExistingWindow&&${routeVar}.navigateToRoute(${createdWindowVar},e),${focusFn}(${createdWindowVar}))},codexLinuxGetHotkeyWindowController=()=>typeof ${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController===\`function\`?${windowManagerVar}.hotkeyWindowLifecycleManager.ensureHotkeyWindowController():${windowManagerVar}.hotkeyWindowLifecycleManager,codexLinuxShowHotkeyWindow=async()=>{let e=codexLinuxGetHotkeyWindowController();typeof e.openHome===\`function\`?await e.openHome():typeof e.show===\`function\`?await e.show():await ${windowManagerVar}.ensureHostWindow(${hostExpr})},codexLinuxOpenQuickChat=async()=>{${windowManagerVar}.hotkeyWindowLifecycleManager.hide();let e=${windowManagerVar}.getPrimaryWindow(${hostExpr}),t=e??await ${windowManagerVar}.createFreshLocalWindow(\`/\`);t!=null&&(${windowManagerVar}.windowManager.sendMessageToWindow(t,{type:\`new-quick-chat\`}),${focusFn}(t))},codexLinuxHasDeepLink=e=>Array.isArray(e)&&e.some(e=>typeof e===\`string\`&&(e.startsWith(\`codex://\`)||e.startsWith(\`codex-browser-sidebar://\`))),codexLinuxHandleLaunchActionArgs=async e=>(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())?!0:codexLinuxHasDeepLink(e)&&${deepLinksVar}.deepLinks.queueProcessArgs(e)?!0:Array.isArray(e)&&(e.includes(\`--prompt-chat\`)||e.includes(\`--hotkey-window\`))?(codexLinuxIsPromptWindowEnabled()?(await codexLinuxShowHotkeyWindow(),!0):!1):Array.isArray(e)&&e.includes(\`--quick-chat\`)?(await codexLinuxOpenQuickChat(),!0):Array.isArray(e)&&e.includes(\`--new-chat\`)?(await ${openerFn}(\`/\`,{navigateExistingWindow:!0}),!0):!1,codexLinuxHandleLaunchActionArgsFallback=(e,t)=>{if(typeof codexLinuxIsQuitInProgress===\`function\`&&codexLinuxIsQuitInProgress())return;codexLinuxHandleLaunchActionArgs(e).then(e=>{e||t()}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action\`,{kind:\`linux-launch-action-failed\`}),t()})},codexLinuxPrewarmHotkeyWindow=()=>{if(!codexLinuxIsPromptWindowEnabled())return;try{let e=codexLinuxGetHotkeyWindowController();typeof e.prewarm===\`function\`&&e.prewarm()}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to prewarm Linux hotkey window\`,{kind:\`linux-hotkey-window-prewarm-failed\`})}},codexLinuxStartLaunchActionSocket=()=>{let e=process.env.CODEX_DESKTOP_LAUNCH_ACTION_SOCKET?.trim()||codexLinuxDefaultLaunchActionSocket();if(process.platform!==\`linux\`||!e||!codexLinuxIsWarmStartEnabled())return;try{${fsVar}.mkdirSync(${pathVar}.default.dirname(e),{recursive:!0,mode:448}),${fsVar}.rmSync(e,{force:!0});let t=${netVar}.default.createServer(t=>{let n=\`\`,r=!1,i=()=>{if(r)return;r=!0;let i=[];try{let e=JSON.parse(n.trim());Array.isArray(e.argv)&&(i=e.argv.filter(e=>typeof e===\`string\`))}catch(e){t.end?.(\`error\\n\`);return}codexLinuxHandleLaunchActionArgs(i).then(e=>e?void 0:${fallbackFn}()).then(()=>{t.end?.(\`ok\\n\`)}).catch(e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to handle Linux launch action socket\`,{kind:\`linux-launch-action-socket-failed\`}),t.end?.(\`error\\n\`)})};t.setEncoding?.(\`utf8\`),t.on(\`data\`,e=>{n+=e,n.includes(\`\\n\`)?i():n.length>65536&&t.destroy()}),t.on(\`end\`,i)});t.on(\`error\`,e=>{${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed Linux launch action socket\`,{kind:\`linux-launch-action-socket-error\`})}),t.listen(e),${disposableVar}.add(()=>{t.close(),${fsVar}.rmSync(e,{force:!0})})}catch(e){${reporterVar}.reportNonFatal(e instanceof Error?e:\`Failed to start Linux launch action socket\`,{kind:\`linux-launch-action-socket-start-failed\`})}}${directHandler};${startup}`; } function applyCurrentSemanticLinuxLaunchActionArgsPatch(currentSource) { diff --git a/scripts/patches/main-process.js b/scripts/patches/main-process.js index ad220a76..fb3b9521 100644 --- a/scripts/patches/main-process.js +++ b/scripts/patches/main-process.js @@ -133,6 +133,35 @@ function applyLinuxSetIconPatch(currentSource, iconAsset) { return currentSource; } +function applyLinuxReadyToShowWindowStatePatch(currentSource) { + const alreadyPatchedRegex = + /[A-Za-z_$][\w$]*&&[A-Za-z_$][\w$]*\.once\(`ready-to-show`,\(\)=>\{[A-Za-z_$][\w$]*\.isDestroyed\(\)\|\|[A-Za-z_$][\w$]*\.maximize\(\)\}\)/; + if (alreadyPatchedRegex.test(currentSource)) { + return currentSource; + } + + const readyToShowMaximizeRegex = + /([A-Za-z_$][\w$]*)\.once\(`ready-to-show`,\(\)=>\{\1\.isDestroyed\(\)\|\|\1\.maximize\(\)\}\)/g; + let patchedAny = false; + const patchedSource = currentSource.replace(readyToShowMaximizeRegex, (_match, windowVar, offset, source) => { + const prefix = source.slice(Math.max(0, offset - 120), offset); + const maximizedStateMatch = prefix.match(/([A-Za-z_$][\w$]*)&&process\.platform===`linux`&&[A-Za-z_$][\w$]*\.setIcon\(/); + const maximizedStateVar = maximizedStateMatch?.[1] ?? "false"; + patchedAny = true; + return `${maximizedStateVar}&&${windowVar}.once(\`ready-to-show\`,()=>{${windowVar}.isDestroyed()||${windowVar}.maximize()})`; + }); + + if (patchedAny) { + return patchedSource; + } + + if (currentSource.includes("ready-to-show") && currentSource.includes(".maximize()")) { + console.warn("WARN: Could not find ready-to-show maximize hook — skipping Linux window-state patch"); + } + + return currentSource; +} + function applyLinuxOpaqueBackgroundPatch(currentSource) { if ( currentSource.includes("===`linux`&&!OM(") || @@ -874,6 +903,7 @@ module.exports = { applyLinuxMenuPatch, applyLinuxOpaqueBackgroundPatch, applyLinuxQuitGuardPatch, + applyLinuxReadyToShowWindowStatePatch, applyLinuxRemoteControlConfigPreservationPatch, applyLinuxSetIconPatch, applyLinuxSingleInstancePatch, From e4a0ba73293c6624e13534defdc62e4b22d5281c Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:47:34 -0600 Subject: [PATCH 07/16] chore(desktop): refine .desktop file actions and update scripts for improved Linux instance handling --- .../share/applications/codex-desktop.desktop | 5 +-- packaging/linux/codex-desktop.desktop | 2 +- scripts/lib/package-common.sh | 32 ++++++++++++++++--- tests/scripts_smoke.sh | 17 ++++++---- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop index b1f1a85c..1b10063d 100644 --- a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop +++ b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop @@ -10,10 +10,11 @@ Categories=Development;IDE; MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar; Keywords=codex;openai;ai;coding; StartupNotify=true -StartupWMClass=codex-desktop +StartupWMClass=Codex +X-GNOME-WMClass=Codex Icon=codex-desktop Actions=NewInstance; [Desktop Action NewInstance] Name=Open New Instance -Exec=env CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop %U +Exec=env CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop --new-instance diff --git a/packaging/linux/codex-desktop.desktop b/packaging/linux/codex-desktop.desktop index 74b775e0..9d8c4cac 100644 --- a/packaging/linux/codex-desktop.desktop +++ b/packaging/linux/codex-desktop.desktop @@ -14,7 +14,7 @@ Actions=NewInstance;CheckForUpdates;InstallReadyUpdate; [Desktop Action NewInstance] Name=Open New Instance -Exec=env CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop %u +Exec=env BAMF_DESKTOP_FILE_HINT=/usr/share/applications/codex-desktop.desktop CHROME_DESKTOP=codex-desktop.desktop CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop --new-instance [Desktop Action CheckForUpdates] Name=Check for Updates diff --git a/scripts/lib/package-common.sh b/scripts/lib/package-common.sh index d1fdf08e..30f2ac26 100755 --- a/scripts/lib/package-common.sh +++ b/scripts/lib/package-common.sh @@ -88,11 +88,33 @@ render_desktop_entry() { display_name="$(sed_escape_replacement "${PACKAGE_DISPLAY_NAME:-Codex Desktop}")" comment="$(sed_escape_replacement "${PACKAGE_COMMENT:-Run Codex Desktop on Linux}")" - sed \ - -e "s/codex-desktop/$package_name/g" \ - -e "s/^Name=.*/Name=$display_name/g" \ - -e "s/^Comment=.*/Comment=$comment/g" \ - "$DESKTOP_TEMPLATE" > "$rendered_target" + awk \ + -v package_name="$package_name" \ + -v display_name="$display_name" \ + -v comment="$comment" ' + BEGIN { in_desktop_entry = 0 } + /^\[Desktop Entry\]$/ { + in_desktop_entry = 1 + gsub(/codex-desktop/, package_name) + print + next + } + /^\[/ { + in_desktop_entry = 0 + } + { + gsub(/codex-desktop/, package_name) + if (in_desktop_entry && /^Name=/) { + print "Name=" display_name + next + } + if (in_desktop_entry && /^Comment=/) { + print "Comment=" comment + next + } + print + } + ' "$DESKTOP_TEMPLATE" > "$rendered_target" if package_with_updater_enabled; then mv "$rendered_target" "$target" else diff --git a/tests/scripts_smoke.sh b/tests/scripts_smoke.sh index 97c5beee..5a095130 100755 --- a/tests/scripts_smoke.sh +++ b/tests/scripts_smoke.sh @@ -138,6 +138,9 @@ SCRIPT assert_file_exists "$dist_dir/codex-desktop_2026.03.24.120000+deadbeef_amd64.deb" assert_file_exists "$pkg_root/DEBIAN/prerm" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Name=Open New Instance" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Name=Check for Updates" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Name=Install Ready Update" assert_file_exists "$pkg_root/DEBIAN/postrm" assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/package-common.sh" assert_file_exists "$pkg_root/opt/codex-desktop/update-builder/scripts/lib/patch-chrome-plugin.js" @@ -268,8 +271,8 @@ SCRIPT assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "CHROME_DESKTOP=codex-cua-lab.desktop" assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "/usr/bin/codex-cua-lab %u" assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;" - assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "StartupWMClass=Codex" - assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "X-GNOME-WMClass=Codex" + assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "StartupWMClass=codex-cua-lab" + assert_contains "$pkg_root/usr/share/applications/codex-cua-lab.desktop" "X-GNOME-WMClass=codex-cua-lab" assert_contains "$pkg_root/opt/codex-cua-lab/.codex-linux/codex-packaged-runtime.sh" 'CHROME_DESKTOP="codex-cua-lab.desktop"' } @@ -331,7 +334,7 @@ SCRIPT assert_contains "$pkg_root/DEBIAN/control" "without codex-update-manager" assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Actions=NewInstance;" assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Desktop Action NewInstance" - assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop %u" + assert_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop --new-instance" assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "Desktop Action CheckForUpdates" assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "InstallReadyUpdate" assert_not_contains "$pkg_root/usr/share/applications/codex-desktop.desktop" "codex-update-manager" @@ -1497,17 +1500,17 @@ PY assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "BAMF_DESKTOP_FILE_HINT" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "/usr/bin/codex-desktop %u" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;" - assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "StartupWMClass=Codex" - assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "X-GNOME-WMClass=Codex" + assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "StartupWMClass=codex-desktop" + assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "X-GNOME-WMClass=codex-desktop" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "Actions=NewInstance;CheckForUpdates;InstallReadyUpdate;" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "[Desktop Action NewInstance]" - assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop %u" + assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop --new-instance" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "codex-update-manager check-now" assert_contains "$REPO_DIR/packaging/linux/codex-desktop.desktop" "codex-update-manager install-ready" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "@HOME@/.local/bin/codex-desktop %U" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar;" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "Actions=NewInstance;" - assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop %U" + assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop" "CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop --new-instance" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" "CODEX_USER_LOCAL_OZONE_PLATFORM" assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" 'exec "${APP_DIR}/start.sh" --x11 "$@"' assert_contains "$REPO_DIR/contrib/user-local-install/files/.local/bin/codex-desktop" 'exec "${APP_DIR}/start.sh" --wayland "$@"' From 79a998bf87150737d181f97b306c81ff736110e5 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 13:47:44 -0600 Subject: [PATCH 08/16] chore(updater): add `cache_cleanup.rs` with workspace pruning and cleanup logic --- updater/src/cache_cleanup.rs | 328 +++++++++++++++++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 updater/src/cache_cleanup.rs diff --git a/updater/src/cache_cleanup.rs b/updater/src/cache_cleanup.rs new file mode 100644 index 00000000..ead9d5b6 --- /dev/null +++ b/updater/src/cache_cleanup.rs @@ -0,0 +1,328 @@ +//! Cleanup helpers for updater-managed build workspaces. + +use crate::state::{PersistedState, UpdateStatus}; +use anyhow::{Context, Result}; +use std::{ + collections::BTreeSet, + fs, + path::{Path, PathBuf}, +}; + +const HEAVY_WORKSPACE_DIRS: &[&str] = &["builder", "codex-app", "dist"]; + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct CleanupSummary { + pub pruned_workspaces: usize, +} + +pub fn prune_unreferenced_workspaces( + workspace_root: &Path, + state: &PersistedState, +) -> Result { + let workspaces_root = workspace_root.join("workspaces"); + if !workspaces_root.is_dir() { + return Ok(CleanupSummary::default()); + } + + let protected = protected_workspaces(workspace_root, state); + let mut summary = CleanupSummary::default(); + + for entry in fs::read_dir(&workspaces_root) + .with_context(|| format!("Failed to read {}", workspaces_root.display()))? + { + let entry = entry?; + let workspace_dir = entry.path(); + if !entry.file_type()?.is_dir() || protected.contains(&workspace_dir) { + continue; + } + + let mut pruned = false; + for heavy_dir in HEAVY_WORKSPACE_DIRS { + let target = workspace_dir.join(heavy_dir); + if target.exists() { + fs::remove_dir_all(&target) + .with_context(|| format!("Failed to remove {}", target.display()))?; + pruned = true; + } + } + + if directory_is_empty(&workspace_dir)? { + fs::remove_dir(&workspace_dir) + .with_context(|| format!("Failed to remove {}", workspace_dir.display()))?; + pruned = true; + } + + if pruned { + summary.pruned_workspaces += 1; + } + } + + Ok(summary) +} + +pub fn derive_workspace_dir( + workspace_root: &Path, + artifact_path: Option<&Path>, +) -> Option { + let artifact_path = artifact_path?; + let workspaces_root = workspace_root.join("workspaces"); + if let Ok(relative) = artifact_path.strip_prefix(&workspaces_root) { + if let Some(component) = relative.components().next() { + return Some(workspaces_root.join(component.as_os_str())); + } + } + + derive_workspace_dir_from_any_workspaces_ancestor(artifact_path) +} + +pub fn normalize_artifact_workspace_dir(workspace_root: &Path, state: &mut PersistedState) { + state.artifact_paths.workspace_dir = state + .artifact_paths + .package_path + .as_deref() + .and_then(|path| derive_workspace_dir(workspace_root, Some(path))) + .or_else(|| { + state + .artifact_paths + .rollback_package_path + .as_deref() + .and_then(|path| derive_workspace_dir(workspace_root, Some(path))) + }) + .or_else(|| { + should_protect_explicit_workspace_dir(&state.status) + .then(|| state.artifact_paths.workspace_dir.clone()) + .flatten() + }); +} + +fn protected_workspaces(workspace_root: &Path, state: &PersistedState) -> BTreeSet { + let mut protected = BTreeSet::new(); + + for artifact_path in [ + state.artifact_paths.package_path.as_deref(), + state.artifact_paths.rollback_package_path.as_deref(), + ] + .into_iter() + .flatten() + { + if let Some(workspace_dir) = derive_workspace_dir(workspace_root, Some(artifact_path)) { + protected.insert(workspace_dir); + } + } + + if should_protect_explicit_workspace_dir(&state.status) { + if let Some(workspace_dir) = state.artifact_paths.workspace_dir.clone() { + protected.insert(workspace_dir); + } + } + + protected +} + +fn should_protect_explicit_workspace_dir(status: &UpdateStatus) -> bool { + matches!( + status, + UpdateStatus::PreparingWorkspace + | UpdateStatus::PatchingApp + | UpdateStatus::BuildingPackage + | UpdateStatus::Failed + ) +} + +fn derive_workspace_dir_from_any_workspaces_ancestor(path: &Path) -> Option { + let mut child = path.to_path_buf(); + for ancestor in path.ancestors() { + if ancestor + .file_name() + .is_some_and(|name| name == "workspaces") + { + return Some(child); + } + child = ancestor.to_path_buf(); + } + None +} + +fn directory_is_empty(path: &Path) -> Result { + Ok(fs::read_dir(path) + .with_context(|| format!("Failed to read {}", path.display()))? + .next() + .is_none()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::{ArtifactPaths, PersistedState, UpdateStatus}; + use anyhow::Result; + use std::{fs, path::PathBuf}; + + fn create_workspace(root: &std::path::Path, name: &str) -> Result { + let workspace = root.join("workspaces").join(name); + fs::create_dir_all(workspace.join("builder"))?; + fs::create_dir_all(workspace.join("codex-app"))?; + fs::create_dir_all(workspace.join("dist"))?; + fs::create_dir_all(workspace.join("logs"))?; + fs::create_dir_all(workspace.join("reports"))?; + fs::write(workspace.join("builder/build.txt"), b"builder")?; + fs::write(workspace.join("codex-app/app.txt"), b"app")?; + fs::write(workspace.join("dist/pkg.deb"), b"pkg")?; + fs::write(workspace.join("logs/install.log"), b"log")?; + fs::write(workspace.join("reports/rebuild-report.json"), b"{}")?; + fs::write(workspace.join("metadata.json"), b"{}")?; + Ok(workspace) + } + + #[test] + fn referenced_package_workspace_is_not_pruned() -> Result<()> { + let temp = tempfile::tempdir()?; + let workspace = create_workspace(temp.path(), "2026.05.19.131017+6d440c71")?; + let package_path = workspace.join("dist/pkg.deb"); + + let mut state = PersistedState::new(true); + state.artifact_paths.package_path = Some(package_path); + + let summary = prune_unreferenced_workspaces(temp.path(), &state)?; + + assert_eq!(summary.pruned_workspaces, 0); + assert!(workspace.join("builder").exists()); + assert!(workspace.join("codex-app").exists()); + assert!(workspace.join("dist").exists()); + Ok(()) + } + + #[test] + fn rollback_workspace_is_not_pruned() -> Result<()> { + let temp = tempfile::tempdir()?; + let workspace = create_workspace(temp.path(), "2026.05.18.010207+6d440c71")?; + let rollback_path = workspace.join("dist/pkg.deb"); + + let mut state = PersistedState::new(true); + state.artifact_paths.rollback_package_path = Some(rollback_path); + + let summary = prune_unreferenced_workspaces(temp.path(), &state)?; + + assert_eq!(summary.pruned_workspaces, 0); + assert!(workspace.join("builder").exists()); + assert!(workspace.join("codex-app").exists()); + assert!(workspace.join("dist").exists()); + Ok(()) + } + + #[test] + fn unreferenced_workspace_prunes_heavy_artifacts_and_keeps_debug_files() -> Result<()> { + let temp = tempfile::tempdir()?; + let workspace = create_workspace(temp.path(), "2026.05.17.120457+6d440c71")?; + let state = PersistedState::new(true); + + let summary = prune_unreferenced_workspaces(temp.path(), &state)?; + + assert_eq!(summary.pruned_workspaces, 1); + assert!(!workspace.join("builder").exists()); + assert!(!workspace.join("codex-app").exists()); + assert!(!workspace.join("dist").exists()); + assert!(workspace.join("logs/install.log").exists()); + assert!(workspace.join("reports/rebuild-report.json").exists()); + assert!(workspace.join("metadata.json").exists()); + Ok(()) + } + + #[test] + fn empty_workspace_is_removed_after_prune() -> Result<()> { + let temp = tempfile::tempdir()?; + let workspace_root = temp.path().join("workspaces"); + let workspace = workspace_root.join("2026.05.16.231927+6d440c71"); + fs::create_dir_all(workspace.join("builder"))?; + fs::write(workspace.join("builder/build.txt"), b"builder")?; + + let state = PersistedState::new(true); + let summary = prune_unreferenced_workspaces(temp.path(), &state)?; + + assert_eq!(summary.pruned_workspaces, 1); + assert!(!workspace.exists()); + Ok(()) + } + + #[test] + fn active_workspace_dir_is_protected_only_while_build_or_failed() -> Result<()> { + let temp = tempfile::tempdir()?; + let workspace = create_workspace(temp.path(), "2026.05.15.233058+5937a9b4")?; + let mut state = PersistedState::new(true); + state.status = UpdateStatus::PatchingApp; + state.artifact_paths.workspace_dir = Some(workspace.clone()); + + let summary = prune_unreferenced_workspaces(temp.path(), &state)?; + assert_eq!(summary.pruned_workspaces, 0); + assert!(workspace.join("builder").exists()); + + state.status = UpdateStatus::Installed; + let summary = prune_unreferenced_workspaces(temp.path(), &state)?; + assert_eq!(summary.pruned_workspaces, 1); + assert!(!workspace.join("builder").exists()); + Ok(()) + } + + #[test] + fn workspace_dir_is_derived_from_retained_package_path() { + let workspace_root = PathBuf::from("/cache"); + let package_path = + workspace_root.join("workspaces/2026.05.04.033705+b0c9ccab/dist/codex.deb"); + + let derived = derive_workspace_dir(&workspace_root, Some(package_path.as_path())); + + assert_eq!( + derived, + Some(workspace_root.join("workspaces/2026.05.04.033705+b0c9ccab")) + ); + } + + #[test] + fn workspace_dir_is_not_derived_for_paths_outside_workspace_root() { + let workspace_root = PathBuf::from("/cache"); + let package_path = PathBuf::from("/tmp/codex.deb"); + + let derived = derive_workspace_dir(&workspace_root, Some(package_path.as_path())); + + assert_eq!(derived, None); + } + + #[test] + fn normalize_state_clears_stale_workspace_dir_for_superseded_candidate() { + let workspace_root = PathBuf::from("/cache"); + let mut state = PersistedState::new(true); + state.status = UpdateStatus::Installed; + state.artifact_paths = ArtifactPaths { + dmg_path: None, + workspace_dir: Some(workspace_root.join("workspaces/2026.04.28.082247+abcdef12")), + package_path: None, + rollback_package_path: None, + }; + + normalize_artifact_workspace_dir(&workspace_root, &mut state); + + assert_eq!(state.artifact_paths.workspace_dir, None); + } + + #[test] + fn normalize_state_points_workspace_dir_at_rollback_package_when_available() { + let workspace_root = PathBuf::from("/cache"); + let rollback_path = workspace_root.join( + "workspaces/2026.05.01.010203+99999999/dist/codex-desktop-2026.05.01.010203-1-x86_64.pkg.tar.zst", + ); + let mut state = PersistedState::new(true); + state.status = UpdateStatus::Installed; + state.artifact_paths = ArtifactPaths { + dmg_path: None, + workspace_dir: None, + package_path: Some(rollback_path.clone()), + rollback_package_path: Some(rollback_path), + }; + + normalize_artifact_workspace_dir(&workspace_root, &mut state); + + assert_eq!( + state.artifact_paths.workspace_dir, + Some(workspace_root.join("workspaces/2026.05.01.010203+99999999")) + ); + } +} From 5323ca190347a38bd71b45ea5d29aa9842f1795a Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 15:32:19 -0600 Subject: [PATCH 09/16] chore(desktop): update WMClass fields in .desktop files to `codex-desktop` for consistency --- .../files/.local/share/applications/codex-desktop.desktop | 4 ++-- packaging/appimage/codex-desktop.desktop | 4 ++-- packaging/linux/codex-desktop.desktop | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop index 1b10063d..9a2dea3a 100644 --- a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop +++ b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop @@ -10,8 +10,8 @@ Categories=Development;IDE; MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar; Keywords=codex;openai;ai;coding; StartupNotify=true -StartupWMClass=Codex -X-GNOME-WMClass=Codex +StartupWMClass=codex-desktop +X-GNOME-WMClass=codex-desktop Icon=codex-desktop Actions=NewInstance; diff --git a/packaging/appimage/codex-desktop.desktop b/packaging/appimage/codex-desktop.desktop index 394ef319..27e14447 100644 --- a/packaging/appimage/codex-desktop.desktop +++ b/packaging/appimage/codex-desktop.desktop @@ -8,6 +8,6 @@ Type=Application Categories=Development; MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar; StartupNotify=true -StartupWMClass=Codex -X-GNOME-WMClass=Codex +StartupWMClass=codex-desktop +X-GNOME-WMClass=codex-desktop X-AppImage-Version=__VERSION__ diff --git a/packaging/linux/codex-desktop.desktop b/packaging/linux/codex-desktop.desktop index 9d8c4cac..f1cadb4c 100644 --- a/packaging/linux/codex-desktop.desktop +++ b/packaging/linux/codex-desktop.desktop @@ -8,8 +8,8 @@ Type=Application Categories=Development; MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar; StartupNotify=true -StartupWMClass=Codex -X-GNOME-WMClass=Codex +StartupWMClass=codex-desktop +X-GNOME-WMClass=codex-desktop Actions=NewInstance;CheckForUpdates;InstallReadyUpdate; [Desktop Action NewInstance] From 337818c5e4f7694510a8fbc907f424eafad6aaa4 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 20:41:00 -0600 Subject: [PATCH 10/16] chore(desktop): add `codex-desktop-entry-doctor.sh` for managing and repairing .desktop files on Linux --- packaging/linux/codex-desktop-entry-doctor.sh | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 packaging/linux/codex-desktop-entry-doctor.sh diff --git a/packaging/linux/codex-desktop-entry-doctor.sh b/packaging/linux/codex-desktop-entry-doctor.sh new file mode 100644 index 00000000..2277fcb1 --- /dev/null +++ b/packaging/linux/codex-desktop-entry-doctor.sh @@ -0,0 +1,123 @@ +#!/bin/sh + +codex_desktop_refresh_desktop_database() { + codex_desktop_db_dir="${1:-}" + [ -n "$codex_desktop_db_dir" ] || return 0 + + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "$codex_desktop_db_dir" >/dev/null 2>&1 || true + fi +} + +codex_desktop_write_user_local_entry() { + codex_desktop_template_path="${1:?missing desktop template path}" + codex_desktop_target_path="${2:?missing desktop target path}" + codex_desktop_home_dir="${3:?missing home directory}" + + mkdir -p "$(dirname "$codex_desktop_target_path")" + sed "s|@HOME@|${codex_desktop_home_dir}|g" \ + "$codex_desktop_template_path" > "$codex_desktop_target_path" + chmod 0644 "$codex_desktop_target_path" + codex_desktop_refresh_desktop_database "$(dirname "$codex_desktop_target_path")" +} + +codex_desktop_entry_has_sidebar_mime() { + grep -Eq '^MimeType=.*x-scheme-handler/codex-browser-sidebar([;]|$)' "$1" +} + +codex_desktop_entry_has_new_window_action() { + grep -Eq '^Actions=.*new-window([;]|$)' "$1" && + grep -Eq '^\[Desktop Action new-window\]$' "$1" +} + +codex_desktop_entry_is_legacy_generated() { + codex_desktop_file="${1:?missing desktop entry path}" + [ -f "$codex_desktop_file" ] || return 1 + + grep -q '^Name=Codex Desktop$' "$codex_desktop_file" || return 1 + grep -Eq '(^Exec=.*codex-desktop|^TryExec=.*codex-desktop|^Icon=codex-desktop$)' \ + "$codex_desktop_file" || return 1 + + if grep -Eq 'codex-desktop-open-next|^Actions=NewWindow([;]|$)|^\[Desktop Action NewWindow\]$|^Actions=NewInstance([;]|$)|^\[Desktop Action NewInstance\]$' \ + "$codex_desktop_file"; then + return 0 + fi + + if ! codex_desktop_entry_has_sidebar_mime "$codex_desktop_file"; then + return 0 + fi + + if ! codex_desktop_entry_has_new_window_action "$codex_desktop_file"; then + return 0 + fi + + return 1 +} + +codex_desktop_next_backup_path() { + codex_desktop_backup_target="${1:?missing desktop entry path}.bak" + codex_desktop_backup_index=0 + + while [ -e "$codex_desktop_backup_target" ]; do + codex_desktop_backup_index=$((codex_desktop_backup_index + 1)) + codex_desktop_backup_target="${1}.bak.${codex_desktop_backup_index}" + done + + printf '%s\n' "$codex_desktop_backup_target" +} + +codex_desktop_repair_shadow_entry() { + codex_desktop_target_path="${1:?missing desktop entry path}" + codex_desktop_backup_target="" + + if ! codex_desktop_entry_is_legacy_generated "$codex_desktop_target_path"; then + return 1 + fi + + codex_desktop_backup_target="$(codex_desktop_next_backup_path "$codex_desktop_target_path")" + mv "$codex_desktop_target_path" "$codex_desktop_backup_target" + codex_desktop_refresh_desktop_database "$(dirname "$codex_desktop_target_path")" +} + +codex_desktop_repair_system_package_shadow_entries() { + codex_desktop_package_name="${1:-codex-desktop}" + codex_desktop_target_file="${codex_desktop_package_name}.desktop" + + if ! command -v runuser >/dev/null 2>&1 || ! command -v getent >/dev/null 2>&1; then + return 0 + fi + + for codex_desktop_runtime_dir in /run/user/*; do + [ -d "$codex_desktop_runtime_dir" ] || continue + + codex_desktop_uid="$(basename "$codex_desktop_runtime_dir")" + case "$codex_desktop_uid" in + ''|*[!0-9]*|0) + continue + ;; + esac + + codex_desktop_passwd_entry="$(getent passwd "$codex_desktop_uid" || true)" + [ -n "$codex_desktop_passwd_entry" ] || continue + + codex_desktop_user_name="$(printf '%s\n' "$codex_desktop_passwd_entry" | cut -d: -f1)" + codex_desktop_home_dir="$(printf '%s\n' "$codex_desktop_passwd_entry" | cut -d: -f6)" + [ -n "$codex_desktop_user_name" ] || continue + [ -n "$codex_desktop_home_dir" ] || continue + [ "$codex_desktop_home_dir" != "/" ] || continue + + codex_desktop_user_entry="$codex_desktop_home_dir/.local/share/applications/$codex_desktop_target_file" + if ! codex_desktop_entry_is_legacy_generated "$codex_desktop_user_entry"; then + continue + fi + + codex_desktop_backup_target="$(codex_desktop_next_backup_path "$codex_desktop_user_entry")" + runuser -u "$codex_desktop_user_name" -- mv \ + "$codex_desktop_user_entry" "$codex_desktop_backup_target" >/dev/null 2>&1 || true + runuser -u "$codex_desktop_user_name" -- sh -c ' + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database "$1" >/dev/null 2>&1 || true + fi + ' sh "$codex_desktop_home_dir/.local/share/applications" >/dev/null 2>&1 || true + done +} From 9a4bc1db1acf352182097703e91905086cb4cfdb Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 20:41:36 -0600 Subject: [PATCH 11/16] chore(scripts): update packaging scripts to dynamically handle shadow entries based on `$PACKAGE_NAME` --- scripts/build-deb.sh | 5 ++++- scripts/build-pacman.sh | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/build-deb.sh b/scripts/build-deb.sh index 53f8c2a5..4c897e64 100755 --- a/scripts/build-deb.sh +++ b/scripts/build-deb.sh @@ -86,7 +86,10 @@ CONTROL fi chmod 0644 "$PKG_ROOT/DEBIAN/control" if package_with_updater_enabled; then - sed -e "s|/opt/codex-desktop|/opt/$PACKAGE_NAME|g" "$POSTINST_TEMPLATE" > "$PKG_ROOT/DEBIAN/postinst" + sed \ + -e "s|/opt/codex-desktop|/opt/$PACKAGE_NAME|g" \ + -e "s|codex_desktop_repair_system_package_shadow_entries codex-desktop|codex_desktop_repair_system_package_shadow_entries $PACKAGE_NAME|g" \ + "$POSTINST_TEMPLATE" > "$PKG_ROOT/DEBIAN/postinst" cp "$PRERM_TEMPLATE" "$PKG_ROOT/DEBIAN/prerm" cp "$POSTRM_TEMPLATE" "$PKG_ROOT/DEBIAN/postrm" chmod 0755 "$PKG_ROOT/DEBIAN/postinst" "$PKG_ROOT/DEBIAN/prerm" "$PKG_ROOT/DEBIAN/postrm" diff --git a/scripts/build-pacman.sh b/scripts/build-pacman.sh index 4a24f1bf..effdd4c2 100755 --- a/scripts/build-pacman.sh +++ b/scripts/build-pacman.sh @@ -92,6 +92,7 @@ main() { "$PKGBUILD_TEMPLATE" >"$build_root/PKGBUILD" if package_with_updater_enabled; then sed -e "s|/opt/codex-desktop|/opt/$PACKAGE_NAME|g" \ + -e "s|codex_desktop_repair_system_package_shadow_entries codex-desktop|codex_desktop_repair_system_package_shadow_entries $PACKAGE_NAME|g" \ "$INSTALL_HOOKS" >"$build_root/${PACKAGE_NAME}.install" else write_no_updater_pacman_install_hooks "$build_root/${PACKAGE_NAME}.install" From 070cfa03ca393af9deafa3f584ca805c9f11a40f Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 20:41:50 -0600 Subject: [PATCH 12/16] chore(desktop): add `new-window` action to .desktop files and integrate repair logic for shadow entries --- .../.local/share/applications/codex-desktop.desktop | 10 +++++----- packaging/appimage/codex-desktop.desktop | 5 +++++ packaging/linux/codex-desktop.desktop | 6 +++--- packaging/linux/codex-desktop.install | 8 ++++++++ packaging/linux/codex-desktop.spec | 5 +++++ 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop index 9a2dea3a..f845eb7b 100644 --- a/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop +++ b/contrib/user-local-install/files/.local/share/applications/codex-desktop.desktop @@ -3,7 +3,7 @@ Type=Application Version=1.0 Name=Codex Desktop Comment=OpenAI Codex desktop wrapper for Linux -Exec=@HOME@/.local/bin/codex-desktop %U +Exec=env BAMF_DESKTOP_FILE_HINT=@HOME@/.local/share/applications/codex-desktop.desktop CHROME_DESKTOP=codex-desktop.desktop @HOME@/.local/bin/codex-desktop %U TryExec=@HOME@/.local/bin/codex-desktop Terminal=false Categories=Development;IDE; @@ -13,8 +13,8 @@ StartupNotify=true StartupWMClass=codex-desktop X-GNOME-WMClass=codex-desktop Icon=codex-desktop -Actions=NewInstance; +Actions=new-window; -[Desktop Action NewInstance] -Name=Open New Instance -Exec=env CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop --new-instance +[Desktop Action new-window] +Name=New Window +Exec=env BAMF_DESKTOP_FILE_HINT=@HOME@/.local/share/applications/codex-desktop.desktop CHROME_DESKTOP=codex-desktop.desktop CODEX_MULTI_LAUNCH=1 @HOME@/.local/bin/codex-desktop --new-instance diff --git a/packaging/appimage/codex-desktop.desktop b/packaging/appimage/codex-desktop.desktop index 27e14447..e321f875 100644 --- a/packaging/appimage/codex-desktop.desktop +++ b/packaging/appimage/codex-desktop.desktop @@ -11,3 +11,8 @@ StartupNotify=true StartupWMClass=codex-desktop X-GNOME-WMClass=codex-desktop X-AppImage-Version=__VERSION__ +Actions=new-window; + +[Desktop Action new-window] +Name=New Window +Exec=env CHROME_DESKTOP=codex-desktop.desktop CODEX_MULTI_LAUNCH=1 AppRun --new-instance diff --git a/packaging/linux/codex-desktop.desktop b/packaging/linux/codex-desktop.desktop index f1cadb4c..776c6569 100644 --- a/packaging/linux/codex-desktop.desktop +++ b/packaging/linux/codex-desktop.desktop @@ -10,10 +10,10 @@ MimeType=x-scheme-handler/codex;x-scheme-handler/codex-browser-sidebar; StartupNotify=true StartupWMClass=codex-desktop X-GNOME-WMClass=codex-desktop -Actions=NewInstance;CheckForUpdates;InstallReadyUpdate; +Actions=new-window;CheckForUpdates;InstallReadyUpdate; -[Desktop Action NewInstance] -Name=Open New Instance +[Desktop Action new-window] +Name=New Window Exec=env BAMF_DESKTOP_FILE_HINT=/usr/share/applications/codex-desktop.desktop CHROME_DESKTOP=codex-desktop.desktop CODEX_MULTI_LAUNCH=1 /usr/bin/codex-desktop --new-instance [Desktop Action CheckForUpdates] diff --git a/packaging/linux/codex-desktop.install b/packaging/linux/codex-desktop.install index 02f83b4b..8b25ef29 100644 --- a/packaging/linux/codex-desktop.install +++ b/packaging/linux/codex-desktop.install @@ -1,13 +1,21 @@ SERVICE_HELPER="/opt/codex-desktop/update-builder/packaging/linux/codex-update-manager-user-service.sh" +DESKTOP_ENTRY_DOCTOR="/opt/codex-desktop/.codex-linux/codex-desktop-entry-doctor.sh" if [ -f "$SERVICE_HELPER" ]; then # shellcheck source=/opt/codex-desktop/update-builder/packaging/linux/codex-update-manager-user-service.sh . "$SERVICE_HELPER" fi +if [ -f "$DESKTOP_ENTRY_DOCTOR" ]; then + # shellcheck source=/opt/codex-desktop/.codex-linux/codex-desktop-entry-doctor.sh + . "$DESKTOP_ENTRY_DOCTOR" +fi post_install() { if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database /usr/share/applications >/dev/null 2>&1 || true fi + if [ -f "$DESKTOP_ENTRY_DOCTOR" ]; then + codex_desktop_repair_system_package_shadow_entries codex-desktop || true + fi if [ -f "$SERVICE_HELPER" ]; then codex_ensure_user_service_running || true fi diff --git a/packaging/linux/codex-desktop.spec b/packaging/linux/codex-desktop.spec index 53df3eea..e4932f9d 100644 --- a/packaging/linux/codex-desktop.spec +++ b/packaging/linux/codex-desktop.spec @@ -51,6 +51,11 @@ cp -a "__RPM_STAGING_DIR__/." "%{buildroot}/" if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database /usr/share/applications >/dev/null 2>&1 || true fi +DESKTOP_ENTRY_DOCTOR=/opt/__PACKAGE_NAME__/.codex-linux/codex-desktop-entry-doctor.sh +if [ -f "$DESKTOP_ENTRY_DOCTOR" ]; then + . "$DESKTOP_ENTRY_DOCTOR" + codex_desktop_repair_system_package_shadow_entries __PACKAGE_NAME__ || true +fi %if __PACKAGE_WITH_UPDATER__ SERVICE_HELPER=/opt/__PACKAGE_NAME__/update-builder/packaging/linux/codex-update-manager-user-service.sh From dd2cb0b0bdd37156c7afcb001f5701554dd15635 Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 20:42:05 -0600 Subject: [PATCH 13/16] chore(postinst): integrate desktop entry repair logic into post-install scripts --- packaging/linux/codex-update-manager.postinst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packaging/linux/codex-update-manager.postinst b/packaging/linux/codex-update-manager.postinst index bbb5f60a..7a738f63 100644 --- a/packaging/linux/codex-update-manager.postinst +++ b/packaging/linux/codex-update-manager.postinst @@ -2,10 +2,16 @@ set -eu SERVICE_HELPER="/opt/codex-desktop/update-builder/packaging/linux/codex-update-manager-user-service.sh" +DESKTOP_ENTRY_DOCTOR="/opt/codex-desktop/.codex-linux/codex-desktop-entry-doctor.sh" if [ -f "$SERVICE_HELPER" ]; then # shellcheck source=/opt/codex-desktop/update-builder/packaging/linux/codex-update-manager-user-service.sh . "$SERVICE_HELPER" codex_ensure_user_service_running || true fi +if [ -f "$DESKTOP_ENTRY_DOCTOR" ]; then + # shellcheck source=/opt/codex-desktop/.codex-linux/codex-desktop-entry-doctor.sh + . "$DESKTOP_ENTRY_DOCTOR" + codex_desktop_repair_system_package_shadow_entries codex-desktop || true +fi exit 0 From f3206a305e949a8878cbaa148e167b6a9a339cdc Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 20:42:22 -0600 Subject: [PATCH 14/16] chore(scripts): integrate `codex-desktop-entry-doctor.sh` and refactor desktop entry handling logic --- contrib/user-local-install/install-user-local.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/contrib/user-local-install/install-user-local.sh b/contrib/user-local-install/install-user-local.sh index fd87924c..bae4208c 100755 --- a/contrib/user-local-install/install-user-local.sh +++ b/contrib/user-local-install/install-user-local.sh @@ -5,6 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" FILES_DIR="${SCRIPT_DIR}/files" SCRIPT_REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" SOURCE_REPO_ROOT="${CODEX_USER_LOCAL_SOURCE_REPO_DIR:-$SCRIPT_REPO_ROOT}" +DESKTOP_ENTRY_DOCTOR="${SOURCE_REPO_ROOT}/packaging/linux/codex-desktop-entry-doctor.sh" OPT_ROOT="${HOME}/.local/opt/codex-desktop-linux" OPT_BIN_DIR="${OPT_ROOT}/bin" OPT_LIB_DIR="${OPT_ROOT}/lib/codex-desktop-linux" @@ -19,6 +20,9 @@ FROM_UPDATE=0 ENABLE_TIMER=0 USER_LOCAL_OZONE_PLATFORM_SETTING="" +# shellcheck disable=SC1090 +. "$DESKTOP_ENTRY_DOCTOR" + while [ $# -gt 0 ]; do case "$1" in --from-update) @@ -109,7 +113,10 @@ set -euo pipefail exec "${HOME}/.local/opt/codex-desktop-linux/bin/codex-desktop-version" "$@" EOF - sed "s|@HOME@|${HOME}|g" "${FILES_DIR}/.local/share/applications/codex-desktop.desktop" > "${HOME}/.local/share/applications/codex-desktop.desktop" + codex_desktop_write_user_local_entry \ + "${FILES_DIR}/.local/share/applications/codex-desktop.desktop" \ + "${HOME}/.local/share/applications/codex-desktop.desktop" \ + "${HOME}" copy_file "${FILES_DIR}/.config/systemd/user/codex-desktop-update.service" "${systemd_user_dir}/codex-desktop-update.service" copy_file "${FILES_DIR}/.config/systemd/user/codex-desktop-update.timer" "${systemd_user_dir}/codex-desktop-update.timer" @@ -145,10 +152,6 @@ if command -v systemctl >/dev/null 2>&1; then fi fi -if command -v update-desktop-database >/dev/null 2>&1; then - update-desktop-database "${HOME}/.local/share/applications" >/dev/null 2>&1 || true -fi - if [ "$FROM_UPDATE" -eq 0 ] && [ -x "${HOME}/.local/bin/codex-desktop-update" ]; then "${HOME}/.local/bin/codex-desktop-update" --record-only >/dev/null 2>&1 || true fi From 2d7f71f13df6d0f67a9e0131949a56481ae588df Mon Sep 17 00:00:00 2001 From: Leavi15 Date: Tue, 19 May 2026 20:42:50 -0600 Subject: [PATCH 15/16] chore(scripts): extend packaging scripts with `codex-desktop-entry-doctor.sh` and refine desktop entry repair logic --- scripts/lib/package-common.sh | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/scripts/lib/package-common.sh b/scripts/lib/package-common.sh index 30f2ac26..f87ef4c7 100755 --- a/scripts/lib/package-common.sh +++ b/scripts/lib/package-common.sh @@ -125,14 +125,14 @@ render_desktop_entry() { /^\[/ { skip = 0 } skip { next } /^Actions=/ { - print "Actions=NewInstance;" + print "Actions=new-window;" actions_rewritten = 1 next } { print } END { if (actions_rewritten == 0) { - print "Actions=NewInstance;" + print "Actions=new-window;" } } ' "$rendered_target" > "$target" @@ -251,6 +251,13 @@ SCRIPT chmod 0644 "$target" } +render_desktop_entry_doctor_helper() { + local target="$1" + + cp "$REPO_DIR/packaging/linux/codex-desktop-entry-doctor.sh" "$target" + chmod 0644 "$target" +} + write_no_updater_deb_postinst() { local target="$1" local package_name @@ -265,11 +272,17 @@ if command -v update-desktop-database >/dev/null 2>&1; then fi CLEANUP_HELPER="/opt/$package_name/.codex-linux/codex-no-updater-transition-cleanup.sh" +DESKTOP_ENTRY_DOCTOR="/opt/$package_name/.codex-linux/codex-desktop-entry-doctor.sh" if [ -f "\$CLEANUP_HELPER" ]; then # shellcheck source=/opt/$package_name/.codex-linux/codex-no-updater-transition-cleanup.sh . "\$CLEANUP_HELPER" codex_no_updater_cleanup_update_manager_service || true fi +if [ -f "\$DESKTOP_ENTRY_DOCTOR" ]; then + # shellcheck source=/opt/$package_name/.codex-linux/codex-desktop-entry-doctor.sh + . "\$DESKTOP_ENTRY_DOCTOR" + codex_desktop_repair_system_package_shadow_entries $package_name || true +fi exit 0 SCRIPT @@ -304,6 +317,7 @@ write_no_updater_pacman_install_hooks() { package_name="$(sed_escape_replacement "$PACKAGE_NAME")" cat > "$target" <