feat(tui): add /app desktop handoff (#25638)

This commit is contained in:
Felipe Coury authored and GitHub committed 2026-06-03 20:30:15 -03:00
1 parent 8285cd278b
commit 80b65e9945
12 files changed
+244 -1

No files matched your search

+3
View File
@@ -403,6 +403,9 @@ impl App {
AppEvent::OpenUrlInBrowser { url } => {
self.open_url_in_browser(url);
}
AppEvent::OpenDesktopThread { thread_id } => {
self.open_desktop_thread(thread_id);
}
AppEvent::PetSelected { pet_id } => {
self.handle_pet_selected(tui, pet_id);
}
+107 -1
View File
@@ -1,10 +1,12 @@
//! Terminal history and clear-screen UI helpers for the TUI app.
//! Terminal history, desktop handoff, and clear-screen UI helpers for the TUI app.
//!
//! This module owns rendering the fresh session header, clearing inline or alternate-screen UI
//! state, and resetting transcript-related app state after `/clear` or Ctrl-L.
use super::*;
const DESKTOP_THREAD_OPENED_MESSAGE: &str = "Opened this session in Codex Desktop.";
impl App {
pub(super) fn open_url_in_browser(&mut self, url: String) {
if let Err(err) = webbrowser::open(&url) {
@@ -17,6 +19,20 @@ impl App {
.add_info_message(format!("Opened {url} in your browser."), /*hint*/ None);
}
pub(super) fn open_desktop_thread(&mut self, thread_id: ThreadId) {
let url = format!("codex://threads/{thread_id}");
if let Err(err) = open_desktop_thread_url(&url) {
self.chat_widget
.add_error_message(desktop_thread_open_error_message(&err));
return;
}
self.chat_widget.add_info_message(
DESKTOP_THREAD_OPENED_MESSAGE.to_string(),
/*hint*/ None,
);
}
pub(super) fn clear_ui_header_lines_with_version(
&self,
width: u16,
@@ -99,3 +115,93 @@ impl App {
self.backtrack_render_pending = false;
}
}
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."
)
}
#[cfg(target_os = "macos")]
fn open_desktop_thread_url(url: &str) -> Result<(), String> {
let status = std::process::Command::new("open")
.arg(url)
.status()
.map_err(|err| format!("failed to invoke `open`: {err}"))?;
if status.success() {
Ok(())
} else {
Err(format!("`open {url}` exited with {status}"))
}
}
#[cfg(target_os = "windows")]
fn open_desktop_thread_url(url: &str) -> Result<(), String> {
let script = windows_desktop_app_launch_script(url);
let output = std::process::Command::new("powershell.exe")
.arg("-NoProfile")
.arg("-Command")
.arg(&script)
.output()
.map_err(|err| format!("failed to launch Codex Desktop through PowerShell: {err}"))?;
if output.status.success() {
return Ok(());
}
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 {}",
output.status
))
} else {
Err(stderr)
}
}
#[cfg(target_os = "windows")]
fn windows_desktop_app_launch_script(url: &str) -> String {
let url = powershell_single_quoted_string(url);
format!(
r#"
$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'
exit 1
}}
$appDir = Join-Path $installLocation 'app'
$exe = Join-Path $appDir 'Codex.exe'
$app = Join-Path $appDir 'resources\app.asar'
if (-not (Test-Path $exe)) {{
Write-Error "Codex Desktop executable not found at $exe"
exit 1
}}
if (-not (Test-Path $app)) {{
Write-Error "Codex Desktop app bundle not found at $app"
exit 1
}}
Start-Process -FilePath $exe -WorkingDirectory $appDir -ArgumentList @('resources\app.asar', $url)
"#
)
}
#[cfg(target_os = "windows")]
fn powershell_single_quoted_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[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())
}
#[cfg(test)]
#[path = "history_ui_tests.rs"]
mod tests;
+29
View File
@@ -0,0 +1,29 @@
use super::*;
use crate::history_cell;
use crate::history_cell::HistoryCell;
#[test]
fn desktop_thread_opened_history_snapshot() {
let cell = history_cell::new_info_event(
DESKTOP_THREAD_OPENED_MESSAGE.to_string(),
/*hint*/ None,
);
insta::assert_snapshot!("desktop_thread_opened_history", render_cell(&cell));
}
#[test]
fn desktop_thread_open_error_history_snapshot() {
let cell = history_cell::new_error_event(desktop_thread_open_error_message("launch failed"));
insta::assert_snapshot!("desktop_thread_open_error_history", render_cell(&cell));
}
fn render_cell(cell: &impl HistoryCell) -> String {
let lines = cell.display_lines(/*width*/ 80);
lines
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
}
@@ -0,0 +1,5 @@
---
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.
@@ -0,0 +1,5 @@
---
source: tui/src/app/history_ui_tests.rs
expression: render_cell(&cell)
---
• Opened this session in Codex Desktop.