fix(agents): show Codex CLI and App versions separately

This commit is contained in:
lzt404 committed 2026-08-03 17:03:39 +08:00
1 parent 748996036e
commit 5f11001ffa
5 files changed
+223 -18

No files matched your search

+189 -13
View File
@@ -657,6 +657,8 @@ struct AgentConfigStatus {
executable_path: Option<String>,
launch_targets: Vec<AgentLaunchTarget>,
version: Option<String>,
cli_version: Option<String>,
app_version: Option<String>,
config_paths: Vec<String>,
config_exists: bool,
config_valid: bool,
@@ -3308,6 +3310,7 @@ fn inspect_pi_provider_status(home: &Path, port: u16, api_key: &str) -> AgentCon
"unconfigured"
};
let error = errors.into_iter().next();
let cli_version = executable.as_deref().and_then(read_agent_version);
AgentConfigStatus {
id: PI_AGENT_ID.to_string(),
name: PI_AGENT_NAME.to_string(),
@@ -3316,7 +3319,9 @@ fn inspect_pi_provider_status(home: &Path, port: u16, api_key: &str) -> AgentCon
plugin_installed,
executable_path: launch_targets.first().map(|target| target.detail.clone()),
launch_targets,
version: executable.as_deref().and_then(read_agent_version),
version: cli_version.clone(),
cli_version,
app_version: None,
config_paths: vec![path_to_string(&config_path), path_to_string(&settings_path)],
config_exists,
config_valid,
@@ -3692,18 +3697,20 @@ fn inspect_agent_config(
};
let launch_targets = agent_launch_targets(client, home);
let executable = find_agent_executable(client, home);
let version = if client == AgentClient::ClaudeDesktop {
read_claude_desktop_version(home)
} else {
executable.as_deref().and_then(read_agent_version)
let cli_version = executable.as_deref().and_then(read_agent_version);
let app_version = match client {
AgentClient::ClaudeDesktop => read_claude_desktop_version(home),
AgentClient::Codex => read_codex_app_version(home),
_ => None,
};
let installed = version.is_some();
let version = cli_version.clone().or_else(|| app_version.clone());
let installed = agent_client_is_installed(client, &launch_targets, version.as_deref());
let mut warnings = Vec::new();
if !client.supported_platform() {
warnings.push("当前平台不支持 Claude Desktop 3P 配置".to_string());
} else if launch_targets.is_empty() && config_exists {
warnings.push("只检测到配置文件,未在 PATH 中找到客户端命令".to_string());
} else if !launch_targets.is_empty() && version.is_none() {
} else if !installed && !launch_targets.is_empty() && version.is_none() {
warnings.push("检测到客户端入口,但无法读取有效版本,未标记为已安装".to_string());
}
if let Some(message) = error.as_ref() {
@@ -3721,6 +3728,8 @@ fn inspect_agent_config(
executable_path: launch_targets.first().map(|target| target.detail.clone()),
launch_targets,
version,
cli_version,
app_version,
config_paths: paths.iter().map(|path| path_to_string(path)).collect(),
config_exists,
config_valid,
@@ -3954,6 +3963,22 @@ fn is_managed_agent_base_url(value: &str) -> bool {
value.starts_with("http://127.0.0.1:") || value.starts_with("http://localhost:")
}
fn agent_client_is_installed(
client: AgentClient,
launch_targets: &[AgentLaunchTarget],
version: Option<&str>,
) -> bool {
if client == AgentClient::Codex {
// Codex can be installed as either a CLI or a desktop app. Discovery
// of either real entry is enough; version lookup is independent.
launch_targets
.iter()
.any(|target| target.id == "cli" || target.id == "app")
} else {
version.is_some()
}
}
fn agent_launch_targets(client: AgentClient, home: &Path) -> Vec<AgentLaunchTarget> {
let mut targets = Vec::new();
if client == AgentClient::ClaudeDesktop {
@@ -3974,12 +3999,6 @@ fn agent_launch_targets(client: AgentClient, home: &Path) -> Vec<AgentLaunchTarg
label: "ChatGPT App".to_string(),
detail: codex_app_target_detail(&target),
});
} else if let Some(executable) = find_agent_executable(AgentClient::Codex, home) {
targets.push(AgentLaunchTarget {
id: "app".to_string(),
label: "ChatGPT App".to_string(),
detail: format!("{} app", path_to_string(&executable)),
});
}
}
@@ -4091,6 +4110,49 @@ fn read_claude_desktop_version(home: &Path) -> Option<String> {
find_claude_desktop_executable(home).and_then(|path| read_agent_version(&path))
}
fn read_codex_app_version(home: &Path) -> Option<String> {
#[cfg(target_os = "windows")]
{
match find_codex_app_target(home)? {
CodexAppTarget::WindowsAppId(_) => read_windows_codex_store_version(),
CodexAppTarget::Application(path) => {
read_windows_executable_version(&path).or_else(|| read_agent_version(&path))
}
}
}
#[cfg(target_os = "macos")]
{
let CodexAppTarget::Application(path) = find_codex_app_target(home)?;
read_macos_app_version(&path)
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let _ = home;
None
}
}
#[cfg(target_os = "macos")]
fn read_macos_app_version(application: &Path) -> Option<String> {
let info_plist = application.join("Contents/Info.plist");
for key in ["CFBundleShortVersionString", "CFBundleVersion"] {
let output = Command::new("/usr/bin/plutil")
.args(["-extract", key, "raw", "-o", "-"])
.arg(&info_plist)
.output()
.ok()?;
if !output.status.success() {
continue;
}
if let Some(version) =
normalize_detected_agent_version(&String::from_utf8_lossy(&output.stdout))
{
return Some(version);
}
}
None
}
#[cfg(target_os = "windows")]
fn windows_system_root() -> PathBuf {
env::var_os("SystemRoot")
@@ -4239,6 +4301,42 @@ if ($package -and $package.Version) {
parse_windows_claude_desktop_version_output(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(target_os = "windows")]
fn read_windows_codex_store_version() -> Option<String> {
const VERSION_SCRIPT: &str = r#"
$ErrorActionPreference = 'SilentlyContinue'
$ProgressPreference = 'SilentlyContinue'
$package = @(Get-AppxPackage) |
Where-Object {
$_.Name -in @('OpenAI.Codex', 'OpenAI.CodexBeta', 'OpenAI.ChatGPT') -or
$_.PackageFamilyName -match '^OpenAI\.(Codex|CodexBeta|ChatGPT)_'
} |
Sort-Object { [version]$_.Version } -Descending |
Select-Object -First 1
if ($package -and $package.Version) {
Write-Output "VERSION:$($package.Version)"
}
"#;
let encoded_command = windows_powershell_encoded_command(VERSION_SCRIPT);
let mut command = Command::new(windows_powershell_executable());
command.args([
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
&encoded_command,
]);
configure_background_command(&mut command);
let output = command.output().ok()?;
if !output.status.success() {
return None;
}
parse_windows_codex_version_output(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(target_os = "windows")]
fn find_windows_registered_codex_app_target() -> Option<CodexAppTarget> {
const DISCOVERY_SCRIPT: &str = r#"
@@ -4342,6 +4440,43 @@ fn windows_powershell_encoded_command(script: &str) -> String {
STANDARD.encode(bytes)
}
#[cfg(target_os = "windows")]
fn windows_powershell_single_quoted_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[cfg(target_os = "windows")]
fn read_windows_executable_version(path: &Path) -> Option<String> {
let path = windows_powershell_single_quoted_literal(&path_to_string(path));
let script = format!(
r#"
$ErrorActionPreference = 'SilentlyContinue'
$item = Get-Item -LiteralPath {path}
$version = $item.VersionInfo.ProductVersion
if ($version) {{
Write-Output "VERSION:$version"
}}
"#
);
let encoded_command = windows_powershell_encoded_command(&script);
let mut command = Command::new(windows_powershell_executable());
command.args([
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-EncodedCommand",
&encoded_command,
]);
configure_background_command(&mut command);
let output = command.output().ok()?;
if !output.status.success() {
return None;
}
parse_windows_codex_version_output(&String::from_utf8_lossy(&output.stdout))
}
#[cfg(target_os = "windows")]
fn parse_windows_codex_app_discovery_output(output: &str) -> Option<CodexAppTarget> {
output.lines().find_map(|line| {
@@ -4373,6 +4508,15 @@ fn parse_windows_claude_desktop_version_output(output: &str) -> Option<String> {
})
}
#[cfg(target_os = "windows")]
fn parse_windows_codex_version_output(output: &str) -> Option<String> {
output.lines().find_map(|line| {
line.trim()
.strip_prefix("VERSION:")
.and_then(normalize_detected_agent_version)
})
}
#[cfg(target_os = "windows")]
fn find_windows_codex_app_id_via_registry() -> Option<String> {
const PACKAGES_KEY: &str = r"HKCU\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages";
@@ -20826,6 +20970,32 @@ oauth-model-alias:
assert_eq!(GuiConfigFile::default().locale, "zh-CN");
}
#[test]
fn codex_installation_is_detected_from_either_cli_or_app_entry() {
let cli_target = vec![AgentLaunchTarget {
id: "cli".to_string(),
label: "Codex CLI".to_string(),
detail: "codex".to_string(),
}];
let app_target = vec![AgentLaunchTarget {
id: "app".to_string(),
label: "ChatGPT App".to_string(),
detail: "Microsoft Store · OpenAI.Codex_2p2nqsd0c76g0!App".to_string(),
}];
assert!(agent_client_is_installed(
AgentClient::Codex,
&cli_target,
None
));
assert!(agent_client_is_installed(
AgentClient::Codex,
&app_target,
None
));
assert!(!agent_client_is_installed(AgentClient::Codex, &[], None));
}
#[cfg(target_os = "windows")]
#[test]
fn windows_chatgpt_discovery_parser_accepts_registered_app_and_executable() {
@@ -20893,6 +21063,12 @@ oauth-model-alias:
);
assert!(parse_windows_claude_desktop_version_output("VERSION:\r\n").is_none());
assert!(parse_windows_claude_desktop_version_output("VERSION:unknown\r\n").is_none());
assert_eq!(
parse_windows_codex_version_output("VERSION:26.727.6591.0\r\n").as_deref(),
Some("26.727.6591.0")
);
assert!(parse_windows_codex_version_output("VERSION:\r\n").is_none());
assert!(parse_windows_codex_version_output("VERSION:unknown\r\n").is_none());
}
#[test]
+2
View File
@@ -556,6 +556,8 @@ export const jaOverrides = {
'agents.clientDetected': 'クライアントを検出しました',
'agents.clientNotDetected': 'クライアントを検出できません',
'agents.clientVersion': 'クライアントバージョン',
'agents.cliVersion': 'CLI バージョン',
'agents.appVersion': 'App バージョン',
'agents.notFetched': '未取得',
'agents.useModel': '使用モデル',
'agents.pending': '未適用',
+4
View File
@@ -559,6 +559,8 @@ export const zhCN = {
'agents.clientDetected': '已检测到客户端',
'agents.clientNotDetected': '未检测到客户端',
'agents.clientVersion': '客户端版本',
'agents.cliVersion': 'CLI 版本',
'agents.appVersion': 'App 版本',
'agents.notFetched': '未获取',
'agents.useModel': '使用模型',
'agents.pending': '尚未应用',
@@ -1510,6 +1512,8 @@ export const en: Record<MessageKey, string> = {
'agents.clientDetected': 'Client detected',
'agents.clientNotDetected': 'Client not detected',
'agents.clientVersion': 'Client Version',
'agents.cliVersion': 'CLI Version',
'agents.appVersion': 'App Version',
'agents.notFetched': 'Not available',
'agents.useModel': 'Model',
'agents.pending': 'Not Applied',
+20 -5
View File
@@ -66,6 +66,8 @@ type AgentConfigStatus = {
executablePath: string | null;
launchTargets: AgentLaunchTarget[];
version: string | null;
cliVersion: string | null;
appVersion: string | null;
configValid: boolean;
configured: boolean;
currentModel: string | null;
@@ -1259,15 +1261,28 @@ export function AgentsPage() {
role="tabpanel"
aria-labelledby="agent-subpage-tab-core"
>
<div className="agent-status-grid">
<div className={`agent-status-grid ${selected === 'codex' ? 'codex-status-grid' : ''}`}>
<div>
<span><BadgeCheck size={14} />{t('agents.installStatus')}</span>
<strong>{activeStatus?.installed ? t('agents.clientDetected') : t('agents.clientNotDetected')}</strong>
</div>
<div>
<span>{t('agents.clientVersion')}</span>
<strong title={activeStatus?.version ?? undefined}>{activeStatus?.version ?? t('agents.notFetched')}</strong>
</div>
{selected === 'codex' ? (
<>
<div>
<span>{t('agents.cliVersion')}</span>
<strong title={activeStatus?.cliVersion ?? undefined}>{activeStatus?.cliVersion ?? t('agents.notFetched')}</strong>
</div>
<div>
<span>{t('agents.appVersion')}</span>
<strong title={activeStatus?.appVersion ?? undefined}>{activeStatus?.appVersion ?? t('agents.notFetched')}</strong>
</div>
</>
) : (
<div>
<span>{t('agents.clientVersion')}</span>
<strong title={activeStatus?.version ?? undefined}>{activeStatus?.version ?? t('agents.notFetched')}</strong>
</div>
)}
</div>
{activeStatus?.error || (!isPiClient && activeStatus?.warnings.length) ? (
+8
View File
@@ -5305,6 +5305,10 @@ textarea:focus-visible {
min-height: 76px;
}
.agent-status-grid.codex-status-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.agent-status-grid > div {
display: grid;
min-width: 0;
@@ -7282,6 +7286,10 @@ textarea:focus-visible {
min-height: 0;
}
.agent-status-grid.codex-status-grid {
grid-template-columns: 1fr;
}
.codex-session-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}