mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Open Windows app workspaces via deep link (#26500)
## Summary Fixes #26423. On Windows, `codex app PATH` detected Codex Desktop and launched the app shell target, then only printed a manual instruction to open the workspace. The Desktop app already supports `codex://threads/new?path=...`, so the CLI can open the requested workspace directly. This updates the Windows launcher to normalize the workspace path, encode it into a `codex://threads/new` deep link, and open that URL when Codex Desktop is installed. The installer fallback still opens the Windows installer and prints the workspace path for after installation.
This commit is contained in:
committed by
GitHub
Unverified
parent
3acd71fedb
commit
e781816ead
@@ -11,13 +11,11 @@ pub async fn run_windows_app_open_or_install(
|
||||
workspace: PathBuf,
|
||||
download_url_override: Option<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Some(app_id) = find_codex_app_id().await? {
|
||||
eprintln!("Opening Codex Desktop...");
|
||||
open_installed_codex_app(&app_id).await?;
|
||||
eprintln!(
|
||||
"In Codex Desktop, open workspace {workspace}.",
|
||||
workspace = display_workspace_path(&workspace)
|
||||
);
|
||||
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}...");
|
||||
open_url(&codex_new_thread_url(&workspace_path)).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -28,14 +26,11 @@ pub async fn run_windows_app_open_or_install(
|
||||
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 {workspace}.",
|
||||
workspace = display_workspace_path(&workspace)
|
||||
);
|
||||
eprintln!("After installing Codex Desktop, open workspace {display_workspace}.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_codex_app_id() -> anyhow::Result<Option<String>> {
|
||||
async fn codex_app_is_installed() -> anyhow::Result<bool> {
|
||||
let output = Command::new("powershell.exe")
|
||||
.arg("-NoProfile")
|
||||
.arg("-Command")
|
||||
@@ -45,20 +40,10 @@ async fn find_codex_app_id() -> anyhow::Result<Option<String>> {
|
||||
.context("failed to invoke `powershell.exe`")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let app_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if app_id.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(app_id))
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_installed_codex_app(app_id: &str) -> anyhow::Result<()> {
|
||||
let target = format!("shell:AppsFolder\\{app_id}");
|
||||
open_shell_target(&target).await
|
||||
Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())
|
||||
}
|
||||
|
||||
async fn open_url(url: &str) -> anyhow::Result<()> {
|
||||
@@ -78,15 +63,11 @@ async fn open_url(url: &str) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_shell_target(target: &str) -> anyhow::Result<()> {
|
||||
// Explorer can successfully hand off shell targets and still return exit code 1.
|
||||
let _status = Command::new("explorer.exe")
|
||||
.arg(target)
|
||||
.status()
|
||||
.await
|
||||
.with_context(|| format!("failed to open {target}"))?;
|
||||
|
||||
Ok(())
|
||||
fn codex_new_thread_url(workspace: &str) -> String {
|
||||
let mut serializer = url::form_urlencoded::Serializer::new(String::new());
|
||||
serializer.append_pair("path", workspace);
|
||||
let query = serializer.finish();
|
||||
format!("codex://threads/new?{query}")
|
||||
}
|
||||
|
||||
fn display_workspace_path(workspace: &Path) -> String {
|
||||
@@ -102,6 +83,7 @@ fn display_workspace_path(workspace: &Path) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::codex_new_thread_url;
|
||||
use super::display_workspace_path;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
@@ -129,4 +111,20 @@ mod tests {
|
||||
r"C:\Users\fcoury\code\codex"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_new_thread_url_encodes_windows_workspace_path() {
|
||||
assert_eq!(
|
||||
codex_new_thread_url(r"C:\Users\akuma\repos\koba"),
|
||||
r"codex://threads/new?path=C%3A%5CUsers%5Cakuma%5Crepos%5Ckoba"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_new_thread_url_preserves_verbatim_workspace_path() {
|
||||
assert_eq!(
|
||||
codex_new_thread_url(r"\\?\C:\Users\akuma\repos\koba"),
|
||||
r"codex://threads/new?path=%5C%5C%3F%5CC%3A%5CUsers%5Cakuma%5Crepos%5Ckoba"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user