From 50b82133850d940627cf307d77e280619bfafe25 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Fri, 17 Jul 2026 20:37:38 +0000 Subject: [PATCH] Support ChatGPT-branded Desktop app builds (#33901) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why The Desktop app can use Codex or ChatGPT branding while retaining stable platform identities. CLI discovery and TUI handoff should not depend on a single display name or hardcoded executable path. ## What changed - On macOS, search for both `ChatGPT.app` and `Codex.app`, and accept only bundles with the `com.openai.codex` identifier. - On Windows, detect installs by their package app ID and resolve the `codex` protocol executable from the AppX manifest before handing off a TUI session. - Use “Desktop app” consistently in CLI and TUI user-facing text. ## Testing Add coverage for selecting a ChatGPT-named Codex bundle and rejecting the classic ChatGPT bundle. GitOrigin-RevId: b84a56eb6e960712152e1a9b023ab530ec9a354a --- codex-rs/cli/src/app_cmd.rs | 2 +- codex-rs/cli/src/desktop_app/mac.rs | 88 ++++++++++++++++--- codex-rs/cli/src/desktop_app/windows.rs | 11 ++- codex-rs/cli/src/main.rs | 2 +- codex-rs/tui/src/app/history_ui.rs | 39 +++++--- ...ts__desktop_thread_open_error_history.snap | 2 +- ..._tests__desktop_thread_opened_history.snap | 2 +- ...mmand_popup__tests__command_popup_app.snap | 4 +- ...p__tests__command_popup_default_items.snap | 2 +- codex-rs/tui/src/slash_command.rs | 2 +- codex-rs/tui/src/tooltips.rs | 4 +- codex-rs/tui/tooltips.txt | 2 +- 12 files changed, 119 insertions(+), 41 deletions(-) diff --git a/codex-rs/cli/src/app_cmd.rs b/codex-rs/cli/src/app_cmd.rs index c28182b4c5e4..44c22b68c5f4 100644 --- a/codex-rs/cli/src/app_cmd.rs +++ b/codex-rs/cli/src/app_cmd.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; #[derive(Debug, Parser)] pub struct AppCommand { - /// Workspace path to open in Codex Desktop. + /// Workspace path to open in the Desktop app. #[arg(value_name = "PATH", default_value = ".")] pub path: PathBuf, diff --git a/codex-rs/cli/src/desktop_app/mac.rs b/codex-rs/cli/src/desktop_app/mac.rs index 85928b551fc9..0a36baa62ffd 100644 --- a/codex-rs/cli/src/desktop_app/mac.rs +++ b/codex-rs/cli/src/desktop_app/mac.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use tempfile::Builder; use tokio::process::Command; +const CODEX_BUNDLE_IDENTIFIER: &str = "com.openai.codex"; const CODEX_DMG_URL_ARM64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"; const CODEX_DMG_URL_X64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex-latest-x64.dmg"; @@ -13,15 +14,15 @@ pub async fn run_mac_app_open_or_install( workspace: PathBuf, download_url_override: Option, ) -> anyhow::Result<()> { - if let Some(app_path) = find_existing_codex_app_path() { + if let Some(app_path) = find_existing_codex_app_path(&codex_app_search_dirs()) { eprintln!( - "Opening Codex Desktop at {app_path}...", + "Opening Desktop app at {app_path}...", app_path = app_path.display() ); open_codex_app(&app_path, &workspace).await?; return Ok(()); } - eprintln!("Codex Desktop not found; downloading installer..."); + eprintln!("Desktop app not found; downloading installer..."); let download_url = download_url_override.unwrap_or_else(|| { let default_url = if is_apple_silicon_mac() { CODEX_DMG_URL_ARM64 @@ -32,9 +33,9 @@ pub async fn run_mac_app_open_or_install( }); let installed_app = download_and_install_codex_to_user_applications(&download_url) .await - .context("failed to download/install Codex Desktop")?; + .context("failed to download/install Desktop app")?; eprintln!( - "Launching Codex Desktop from {installed_app}...", + "Launching Desktop app from {installed_app}...", installed_app = installed_app.display() ); open_codex_app(&installed_app, &workspace).await?; @@ -63,20 +64,40 @@ fn is_apple_silicon_mac() -> bool { || macos_sysctl_flag("hw.optional.arm64").unwrap_or(false) } -fn find_existing_codex_app_path() -> Option { - candidate_codex_app_paths() - .into_iter() - .find(|candidate| candidate.is_dir()) +fn find_existing_codex_app_path(applications_dirs: &[PathBuf]) -> Option { + applications_dirs + .iter() + .flat_map(|dir| ["ChatGPT.app", "Codex.app"].map(|app_name| dir.join(app_name))) + .find(|candidate| is_codex_app_bundle(candidate)) } -fn candidate_codex_app_paths() -> Vec { - let mut paths = vec![PathBuf::from("/Applications/Codex.app")]; +fn codex_app_search_dirs() -> Vec { + let mut paths = vec![PathBuf::from("/Applications")]; if let Some(home) = std::env::var_os("HOME") { - paths.push(PathBuf::from(home).join("Applications").join("Codex.app")); + paths.push(PathBuf::from(home).join("Applications")); } paths } +fn is_codex_app_bundle(app_path: &Path) -> bool { + if !app_path.is_dir() { + return false; + } + + std::process::Command::new("/usr/bin/plutil") + .arg("-extract") + .arg("CFBundleIdentifier") + .arg("raw") + .arg("-o") + .arg("-") + .arg(app_path.join("Contents/Info.plist")) + .output() + .is_ok_and(|output| { + output.status.success() + && String::from_utf8_lossy(&output.stdout).trim() == CODEX_BUNDLE_IDENTIFIER + }) +} + async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> { eprintln!( "Opening workspace {workspace}...", @@ -121,7 +142,7 @@ async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyho let dmg_path = tmp_root.join("Codex.dmg"); download_dmg(dmg_url, &dmg_path).await?; - eprintln!("Mounting Codex Desktop installer..."); + eprintln!("Mounting Desktop app installer..."); let mount_point = mount_dmg(&dmg_path).await?; eprintln!( "Installer mounted at {mount_point}.", @@ -148,7 +169,7 @@ async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyho async fn install_codex_app_bundle(app_in_volume: &Path) -> anyhow::Result { for applications_dir in candidate_applications_dirs()? { eprintln!( - "Installing Codex Desktop into {applications_dir}...", + "Installing Desktop app into {applications_dir}...", applications_dir = applications_dir.display() ); std::fs::create_dir_all(&applications_dir).with_context(|| { @@ -303,10 +324,49 @@ fn parse_hdiutil_attach_mount_point(output: &str) -> Option { #[cfg(test)] mod tests { use super::codex_new_thread_url; + use super::find_existing_codex_app_path; use super::parse_hdiutil_attach_mount_point; use pretty_assertions::assert_eq; + use std::fs; use std::path::Path; + fn write_app_bundle(app_path: &Path, bundle_identifier: &str) { + let contents_path = app_path.join("Contents"); + fs::create_dir_all(&contents_path).expect("create app bundle"); + fs::write( + contents_path.join("Info.plist"), + format!( + r#"CFBundleIdentifier{bundle_identifier}"# + ), + ) + .expect("write Info.plist"); + } + + #[test] + fn finds_chatgpt_app_with_codex_bundle_identifier() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + let app_path = temp_dir.path().join("ChatGPT.app"); + write_app_bundle(&app_path, "com.openai.codex"); + + assert_eq!( + find_existing_codex_app_path(&[temp_dir.path().to_path_buf()]), + Some(app_path) + ); + } + + #[test] + fn ignores_classic_chatgpt_app() { + let temp_dir = tempfile::tempdir().expect("create temp dir"); + write_app_bundle(&temp_dir.path().join("ChatGPT.app"), "com.openai.chat"); + let codex_app_path = temp_dir.path().join("Codex.app"); + write_app_bundle(&codex_app_path, "com.openai.codex"); + + assert_eq!( + find_existing_codex_app_path(&[temp_dir.path().to_path_buf()]), + Some(codex_app_path) + ); + } + #[test] fn parses_mount_point_from_tab_separated_hdiutil_output() { let output = "/dev/disk2s1\tApple_HFS\tCodex\t/Volumes/Codex\n"; diff --git a/codex-rs/cli/src/desktop_app/windows.rs b/codex-rs/cli/src/desktop_app/windows.rs index 717c54dda48a..ab0f757321c8 100644 --- a/codex-rs/cli/src/desktop_app/windows.rs +++ b/codex-rs/cli/src/desktop_app/windows.rs @@ -14,27 +14,30 @@ pub async fn run_windows_app_open_or_install( let workspace_path = workspace.display().to_string(); let display_workspace = display_workspace_path(&workspace); if codex_app_is_installed().await? { - eprintln!("Opening Codex Desktop workspace {display_workspace}..."); + eprintln!("Opening workspace {display_workspace} in the Desktop app..."); open_url(&codex_new_thread_url(&workspace_path)).await?; return Ok(()); } - eprintln!("Codex Desktop not found; opening Windows installer..."); + eprintln!("Desktop app not found; opening Windows installer..."); let download_url = download_url_override .as_deref() .unwrap_or(CODEX_WINDOWS_INSTALLER_URL); if open_url(download_url).await.is_err() && download_url_override.is_none() { open_url(CODEX_MICROSOFT_STORE_WEB_URL).await?; } - eprintln!("After installing Codex Desktop, open workspace {display_workspace}."); + eprintln!("After installing the Desktop app, open workspace {display_workspace}."); Ok(()) } async fn codex_app_is_installed() -> anyhow::Result { + // This package identity is stable across Codex- and ChatGPT-branded builds. let output = Command::new("powershell.exe") .arg("-NoProfile") .arg("-Command") - .arg("Get-StartApps -Name 'Codex' | Select-Object -First 1 -ExpandProperty AppID") + .arg( + "Get-StartApps | Where-Object AppID -Like 'OpenAI.Codex_*!App' | Select-Object -First 1 -ExpandProperty AppID", + ) .output() .await .context("failed to invoke `powershell.exe`")?; diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 04d59c24abf4..433c317e0a92 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -150,7 +150,7 @@ enum Subcommand { /// [experimental] Manage the app-server daemon with remote control enabled. RemoteControl(RemoteControlCommand), - /// Launch the Codex desktop app (opens the app installer if missing). + /// Launch the Desktop app (opens the app installer if missing). #[cfg(any(target_os = "macos", target_os = "windows"))] App(app_cmd::AppCommand), diff --git a/codex-rs/tui/src/app/history_ui.rs b/codex-rs/tui/src/app/history_ui.rs index d527df7a9014..c707ef8d13c3 100644 --- a/codex-rs/tui/src/app/history_ui.rs +++ b/codex-rs/tui/src/app/history_ui.rs @@ -5,7 +5,7 @@ use super::*; -const DESKTOP_THREAD_OPENED_MESSAGE: &str = "Opened this session in Codex Desktop."; +const DESKTOP_THREAD_OPENED_MESSAGE: &str = "Opened this session in the Desktop app."; impl App { pub(super) fn insert_history_cell(&mut self, tui: &mut tui::Tui, cell: Box) { @@ -179,7 +179,7 @@ impl App { fn desktop_thread_open_error_message(err: &str) -> String { format!( - "Failed to open this session in Codex Desktop: {err}. Install or launch Codex Desktop and try again." + "Failed to open this session in the Desktop app: {err}. Install or launch the Desktop app and try again." ) } @@ -205,7 +205,7 @@ fn open_desktop_thread_url(url: &str) -> Result<(), String> { .arg("-Command") .arg(&script) .output() - .map_err(|err| format!("failed to launch Codex Desktop through PowerShell: {err}"))?; + .map_err(|err| format!("failed to launch the Desktop app through PowerShell: {err}"))?; if output.status.success() { return Ok(()); @@ -214,7 +214,7 @@ fn open_desktop_thread_url(url: &str) -> Result<(), String> { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); if stderr.is_empty() { Err(format!( - "failed to launch Codex Desktop through PowerShell with {}", + "failed to launch the Desktop app through PowerShell with {}", output.status )) } else { @@ -230,21 +230,36 @@ fn windows_desktop_app_launch_script(url: &str) -> String { $ErrorActionPreference = 'Stop' $url = {url} -$installLocation = (Get-AppxPackage -Name OpenAI.Codex -ErrorAction SilentlyContinue).InstallLocation -if ([string]::IsNullOrWhiteSpace($installLocation)) {{ - Write-Error 'Codex Desktop package is not installed' +$package = Get-AppxPackage -Name OpenAI.Codex -ErrorAction SilentlyContinue +if ($null -eq $package) {{ + Write-Error 'Desktop app package is not installed' exit 1 }} -$appDir = Join-Path $installLocation 'app' -$exe = Join-Path $appDir 'Codex.exe' +$manifest = Get-AppxPackageManifest -Package $package.PackageFullName +$application = $manifest.Package.Applications.Application | + Where-Object {{ + @($_.Extensions.Extension) | Where-Object {{ + $_.Category -eq 'windows.protocol' -and $_.Protocol.Name -eq 'codex' + }} + }} | + Select-Object -First 1 +if ($null -eq $application -or [string]::IsNullOrWhiteSpace($application.Executable)) {{ + Write-Error 'Desktop app package does not declare a codex protocol executable' + exit 1 +}} + +# Launch the package-declared protocol executable rather than an internal Electron shim. +# Windows can deny direct starts of internal executables under WindowsApps. +$exe = Join-Path $package.InstallLocation $application.Executable +$appDir = Split-Path -Parent $exe $app = Join-Path $appDir 'resources\app.asar' if (-not (Test-Path $exe)) {{ - Write-Error "Codex Desktop executable not found at $exe" + Write-Error "Desktop app executable not found at $exe" exit 1 }} if (-not (Test-Path $app)) {{ - Write-Error "Codex Desktop app bundle not found at $app" + Write-Error "Desktop app bundle not found at $app" exit 1 }} @@ -260,7 +275,7 @@ fn powershell_single_quoted_string(value: &str) -> String { #[cfg(not(any(target_os = "macos", target_os = "windows")))] fn open_desktop_thread_url(_url: &str) -> Result<(), String> { - Err("Codex Desktop is only available on macOS and Windows".to_string()) + Err("The Desktop app is only available on macOS and Windows".to_string()) } #[cfg(test)] diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_open_error_history.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_open_error_history.snap index 256dd969359d..16e3549a2023 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_open_error_history.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_open_error_history.snap @@ -2,4 +2,4 @@ source: tui/src/app/history_ui_tests.rs expression: render_cell(&cell) --- -■ Failed to open this session in Codex Desktop: launch failed. Install or launch Codex Desktop and try again. +■ Failed to open this session in the Desktop app: launch failed. Install or launch the Desktop app and try again. diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_opened_history.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_opened_history.snap index a1f5264511e9..8e34f8e8a645 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_opened_history.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__history_ui__tests__desktop_thread_opened_history.snap @@ -2,4 +2,4 @@ source: tui/src/app/history_ui_tests.rs expression: render_cell(&cell) --- -• Opened this session in Codex Desktop. +• Opened this session in the Desktop app. diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap index 95e28a39c542..51d3ed54a7f4 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_app.snap @@ -5,13 +5,13 @@ expression: "format!(\"{buf:?}\")" Buffer { area: Rect { x: 0, y: 0, width: 72, height: 2 }, content: [ - " /app continue this session in Codex Desktop ", + " /app continue this session in the Desktop app ", " /approve approve one retry of a recent auto-review denial ", ], styles: [ x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 2, y: 0, fg: Cyan, bg: Reset, underline: Reset, modifier: BOLD, - x: 50, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, + x: 52, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 3, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD, x: 6, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, x: 12, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM, diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap index 0dac0121f604..93b3d93a8f30 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__command_popup__tests__command_popup_default_items.snap @@ -20,7 +20,7 @@ expression: commands /delete - permanently delete this session and exit /resume - resume a saved chat /fork - fork the current chat -/app - continue this session in Codex Desktop +/app - continue this session in the Desktop app /init - create an AGENTS.md file with instructions for Codex /compact - summarize conversation to prevent hitting the context limit /agent - switch the active agent thread diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 646380a2fa59..b3a2ddc51916 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -93,7 +93,7 @@ impl SlashCommand { SlashCommand::Delete => "permanently delete this session and exit", SlashCommand::Clear => "clear the terminal and start a new chat", SlashCommand::Fork => "fork the current chat", - SlashCommand::App => "continue this session in Codex Desktop", + SlashCommand::App => "continue this session in the Desktop app", SlashCommand::Quit | SlashCommand::Exit => "exit Codex", SlashCommand::Copy => "copy last response as markdown", SlashCommand::Raw => "toggle raw scrollback mode for copy-friendly terminal selection", diff --git a/codex-rs/tui/src/tooltips.rs b/codex-rs/tui/src/tooltips.rs index 8eb156156e0e..6bdc4cf52d68 100644 --- a/codex-rs/tui/src/tooltips.rs +++ b/codex-rs/tui/src/tooltips.rs @@ -9,10 +9,10 @@ const ANNOUNCEMENT_TIP_URL: &str = const IS_MACOS: bool = cfg!(target_os = "macos"); const IS_WINDOWS: bool = cfg!(target_os = "windows"); -const APP_TOOLTIP: &str = "Try the **Codex App**. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; +const APP_TOOLTIP: &str = "Try the **Desktop app**. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; const FAST_TOOLTIP: &str = "*New* Use **/fast** to enable our fastest inference with increased plan usage."; -const OTHER_TOOLTIP: &str = "*New* Build faster with the **Codex App**. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; +const OTHER_TOOLTIP: &str = "*New* Build faster with the **Desktop app**. Run 'codex app' or visit https://chatgpt.com/codex?app-landing-page=true"; const OTHER_TOOLTIP_NON_MAC: &str = "*New* Build faster with Codex."; const FREE_GO_TOOLTIP: &str = "*New* For a limited time, Codex is included in your plan for free – let’s build together."; diff --git a/codex-rs/tui/tooltips.txt b/codex-rs/tui/tooltips.txt index 6927ad511103..c48383e23424 100644 --- a/codex-rs/tui/tooltips.txt +++ b/codex-rs/tui/tooltips.txt @@ -11,7 +11,7 @@ Use /fork to branch the current chat into a new thread. Use /side to start a side conversation in a temporary fork without polluting the main thread. Use /init to create an AGENTS.md with project-specific guidance. Use /mcp to list configured MCP tools. -Run `codex app` to open Codex Desktop (it installs on macOS if needed). +Run `codex app` to open the Desktop app (it installs on macOS if needed). Use /personality to customize how Codex communicates. Use /rename to rename your threads for easier thread resuming. Use the OpenAI docs MCP for API questions; enable it with `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`.