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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/cli/src/app_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
88 changes: 74 additions & 14 deletions codex-rs/cli/src/desktop_app/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -13,15 +14,15 @@ pub async fn run_mac_app_open_or_install(
workspace: PathBuf,
download_url_override: Option<String>,
) -> 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
Expand All @@ -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?;
Expand Down Expand Up @@ -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<PathBuf> {
candidate_codex_app_paths()
.into_iter()
.find(|candidate| candidate.is_dir())
fn find_existing_codex_app_path(applications_dirs: &[PathBuf]) -> Option<PathBuf> {
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<PathBuf> {
let mut paths = vec![PathBuf::from("/Applications/Codex.app")];
fn codex_app_search_dirs() -> Vec<PathBuf> {
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}...",
Expand Down Expand Up @@ -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}.",
Expand All @@ -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<PathBuf> {
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(|| {
Expand Down Expand Up @@ -303,10 +324,49 @@ fn parse_hdiutil_attach_mount_point(output: &str) -> Option<String> {
#[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#"<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>{bundle_identifier}</string></dict></plist>"#
),
)
.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";
Expand Down
11 changes: 7 additions & 4 deletions codex-rs/cli/src/desktop_app/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
// 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`")?;
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
39 changes: 27 additions & 12 deletions codex-rs/tui/src/app/history_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn HistoryCell>) {
Expand Down Expand Up @@ -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."
)
}

Expand All @@ -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(());
Expand All @@ -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 {
Expand All @@ -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
}}

Expand All @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/slash_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/tui/src/tooltips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/tooltips.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading