From a8a0d54d906143a86b64c2f8bdc254f404d07fa5 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Sun, 26 Jul 2026 20:24:41 +0800 Subject: [PATCH] feat: improve Codex launch controls and add ChatGPT app shutdown --- src-tauri/src/codex_sessions.rs | 175 +++++++++++++-- src-tauri/src/main.rs | 18 +- src/i18n/ja.ts | 16 +- src/i18n/resources.ts | 32 +-- src/pages/AgentsPage.tsx | 191 ++++++++++++----- src/pages/CodexSessionAutoRestoreCard.tsx | 1 - src/styles.css | 248 ++++++++++++++++------ 7 files changed, 526 insertions(+), 155 deletions(-) diff --git a/src-tauri/src/codex_sessions.rs b/src-tauri/src/codex_sessions.rs index e505f58..e7b8eee 100644 --- a/src-tauri/src/codex_sessions.rs +++ b/src-tauri/src/codex_sessions.rs @@ -152,6 +152,13 @@ pub(crate) struct SessionIndexCleanupResult { backup_path: Option, } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct ChatGptCloseResult { + was_running: bool, + closed_processes: usize, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct SetRepairOnLaunchRequest { @@ -315,6 +322,13 @@ pub(crate) fn set_codex_session_repair_on_launch( }) } +#[tauri::command] +pub(crate) async fn close_chatgpt_app() -> Result { + tauri::async_runtime::spawn_blocking(close_chatgpt_app_processes) + .await + .map_err(|error| format!("关闭 ChatGPT App 任务失败: {error}"))? +} + pub(crate) fn repair_before_codex_launch( user_home: &Path, ) -> Result { @@ -2099,10 +2113,20 @@ fn ensure_codex_apps_stopped() -> Result<(), String> { #[cfg(target_os = "windows")] fn codex_app_process_ids() -> Result, String> { + windows_app_process_ids(&["ChatGPT.exe", "Codex.exe"]) +} + +#[cfg(target_os = "windows")] +fn chatgpt_app_process_ids() -> Result, String> { + windows_app_process_ids(&["ChatGPT.exe"]) +} + +#[cfg(target_os = "windows")] +fn windows_app_process_ids(names: &[&str]) -> Result, String> { use std::os::windows::process::CommandExt; const CREATE_NO_WINDOW: u32 = 0x08000000; let mut ids = Vec::new(); - for name in ["ChatGPT.exe", "Codex.exe"] { + for name in names { let filter = format!("IMAGENAME eq {name}"); let output = Command::new("tasklist") .args(["/FI", &filter, "/FO", "CSV", "/NH"]) @@ -2115,28 +2139,42 @@ fn codex_app_process_ids() -> Result, String> { String::from_utf8_lossy(&output.stderr).trim() )); } - ids.extend( - String::from_utf8_lossy(&output.stdout) - .lines() - .filter_map(|line| { - let columns = line - .trim() - .trim_matches('"') - .split("\",\"") - .collect::>(); - columns.get(1)?.parse::().ok() - }), - ); + ids.extend(parse_windows_tasklist_process_ids(&output.stdout)); } ids.sort_unstable(); ids.dedup(); Ok(ids) } +#[cfg(target_os = "windows")] +fn parse_windows_tasklist_process_ids(output: &[u8]) -> Vec { + String::from_utf8_lossy(output) + .lines() + .filter_map(|line| { + let columns = line + .trim() + .trim_matches('"') + .split("\",\"") + .collect::>(); + columns.get(1)?.parse::().ok() + }) + .collect() +} + #[cfg(not(target_os = "windows"))] fn codex_app_process_ids() -> Result, String> { + unix_app_process_ids(&["ChatGPT", "Codex"]) +} + +#[cfg(not(target_os = "windows"))] +fn chatgpt_app_process_ids() -> Result, String> { + unix_app_process_ids(&["ChatGPT"]) +} + +#[cfg(not(target_os = "windows"))] +fn unix_app_process_ids(names: &[&str]) -> Result, String> { let mut ids = Vec::new(); - for name in ["ChatGPT", "Codex"] { + for name in names { let output = Command::new("pgrep") .args(["-x", name]) .output() @@ -2161,6 +2199,108 @@ fn codex_app_process_ids() -> Result, String> { Ok(ids) } +#[cfg(target_os = "windows")] +fn close_chatgpt_app_processes() -> Result { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + let process_ids = chatgpt_app_process_ids()?; + if process_ids.is_empty() { + return Ok(ChatGptCloseResult { + was_running: false, + closed_processes: 0, + }); + } + + let output = Command::new("taskkill") + .args(["/IM", "ChatGPT.exe", "/T", "/F"]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map_err(|error| format!("无法关闭 ChatGPT.exe: {error}"))?; + let mut remaining = chatgpt_app_process_ids()?; + for _ in 0..20 { + if remaining.is_empty() { + break; + } + std::thread::sleep(Duration::from_millis(100)); + remaining = chatgpt_app_process_ids()?; + } + if !remaining.is_empty() { + let detail = String::from_utf8_lossy(if output.stderr.is_empty() { + &output.stdout + } else { + &output.stderr + }); + return Err(format!( + "ChatGPT App 未能完全关闭(剩余进程:{}){}{}", + remaining + .iter() + .map(u32::to_string) + .collect::>() + .join("、"), + if detail.trim().is_empty() { "" } else { ":" }, + detail.trim() + )); + } + + Ok(ChatGptCloseResult { + was_running: true, + closed_processes: process_ids.len(), + }) +} + +#[cfg(not(target_os = "windows"))] +fn close_chatgpt_app_processes() -> Result { + let process_ids = chatgpt_app_process_ids()?; + if process_ids.is_empty() { + return Ok(ChatGptCloseResult { + was_running: false, + closed_processes: 0, + }); + } + + let mut terminate = Command::new("kill"); + terminate.arg("-TERM"); + for process_id in &process_ids { + terminate.arg(process_id.to_string()); + } + let _ = terminate.status(); + + for _ in 0..20 { + if chatgpt_app_process_ids()?.is_empty() { + return Ok(ChatGptCloseResult { + was_running: true, + closed_processes: process_ids.len(), + }); + } + std::thread::sleep(Duration::from_millis(100)); + } + + let remaining = chatgpt_app_process_ids()?; + let mut force = Command::new("kill"); + force.arg("-KILL"); + for process_id in &remaining { + force.arg(process_id.to_string()); + } + let _ = force.status(); + std::thread::sleep(Duration::from_millis(100)); + let remaining = chatgpt_app_process_ids()?; + if !remaining.is_empty() { + return Err(format!( + "ChatGPT App 未能完全关闭(剩余进程:{})", + remaining + .iter() + .map(u32::to_string) + .collect::>() + .join("、") + )); + } + + Ok(ChatGptCloseResult { + was_running: true, + closed_processes: process_ids.len(), + }) +} + #[cfg(test)] mod tests { use super::*; @@ -2168,6 +2308,13 @@ mod tests { static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0); + #[cfg(target_os = "windows")] + #[test] + fn windows_tasklist_parser_accepts_csv_rows_and_ignores_status_messages() { + let output = b"\"ChatGPT.exe\",\"1234\",\"Console\",\"1\",\"10,000 K\"\r\nINFO: No tasks are running which match the specified criteria.\r\n\"ChatGPT.exe\",\"5678\",\"Console\",\"1\",\"20,000 K\"\r\n"; + assert_eq!(parse_windows_tasklist_process_ids(output), vec![1234, 5678]); + } + fn test_root(name: &str) -> PathBuf { let path = std::env::temp_dir().join(format!( "easy-cli-proxy-codex-sessions-{name}-{}-{}", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2fc1d51..77199da 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1773,7 +1773,7 @@ async fn launch_agent( status.modification_enabled, &status.modification_state, )?; - if client == AgentClient::Codex && config.codex_session_repair_on_launch { + if client == AgentClient::Codex && config.codex_session_repair_on_launch && status.configured { let repair_home = home.clone(); tauri::async_runtime::spawn_blocking(move || { codex_sessions::repair_before_codex_launch(&repair_home) @@ -1827,7 +1827,7 @@ fn validate_agent_launch_modification( enabled: bool, state: &str, ) -> Result<(), String> { - if enabled && state == "applied" { + if client == AgentClient::Codex || (enabled && state == "applied") { return Ok(()); } Err(format!( @@ -12869,7 +12869,8 @@ fn main() { codex_sessions::preview_codex_session_index_cleanup, codex_sessions::apply_codex_session_index_cleanup, codex_sessions::get_codex_session_repair_on_launch, - codex_sessions::set_codex_session_repair_on_launch + codex_sessions::set_codex_session_repair_on_launch, + codex_sessions::close_chatgpt_app ]) .build(tauri::generate_context!()) .expect("failed to build app"); @@ -15437,13 +15438,20 @@ custom_option = "keep-original" } #[test] - fn agent_launch_requires_an_active_managed_configuration() { + fn codex_launch_is_independent_from_managed_configuration_state() { assert!(validate_agent_launch_modification(AgentClient::Codex, true, "applied").is_ok()); - assert!(validate_agent_launch_modification(AgentClient::Codex, false, "applied").is_err()); + assert!( + validate_agent_launch_modification(AgentClient::Codex, false, "unconfigured").is_ok() + ); assert!( validate_agent_launch_modification(AgentClient::Codex, true, "external-changed",) + .is_ok() + ); + assert!( + validate_agent_launch_modification(AgentClient::OpenCode, false, "unconfigured") .is_err() ); + assert!(validate_agent_launch_modification(AgentClient::OpenCode, true, "applied").is_ok()); } #[test] diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 7d19e23..9880d02 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -387,7 +387,6 @@ export const jaOverrides = { 'agents.sessions.repairTitle': '過去のセッションを復元', 'agents.sessions.repairDescription': '他の Provider のセッションを現在のセッション Provider に同期します。', 'agents.sessions.autoRepair': 'Codex 起動前に過去のセッションを自動復元', - 'agents.sessions.autoRestoreDescription': 'このアプリから Codex を起動するときにセッションを自動同期します。', 'agents.sessions.autoRepairEnabled': '起動前の自動復元を有効にしました。', 'agents.sessions.autoRepairDisabled': '起動前の自動復元を無効にしました。', 'agents.sessions.settingLoading': '読み込み中', @@ -453,10 +452,8 @@ export const jaOverrides = { 'agents.clientVersion': 'クライアントバージョン', 'agents.notFetched': '未取得', 'agents.useModel': '使用モデル', - 'agents.useModelDescription': 'コアで公開中のモデルを同期し、選択したモデルを既定に設定します', 'agents.pending': '未適用', 'agents.model.readingAvailable': '利用可能なモデルを読み込んでいます', - 'agents.model.cannotConfigure': '選択可能なモデルがないため、設定変更を適用できません', 'agents.model.current': '現在適用中:{model}', 'agents.model.firstSelection': '利用可能なモデル {count} 件。初回は先頭を選択します', 'agents.modify.title': '設定変更', @@ -466,22 +463,22 @@ export const jaOverrides = { 'agents.modify.externalChanged': '外部の設定変更を検出し、その内容を保持しています。「設定変更を適用」をクリックするまで管理対象フィールドは書き換えません。', 'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。', 'agents.modify.applied': 'アプリが管理するフィールドは適用済みで、それ以外のユーザー設定は保持されています。', - 'agents.modify.unconfigured': 'モデルを選択して設定変更を適用してください。設定のコピーやバックアップは作成しません。', - 'agents.footer.changed': 'モデル選択が変更されました。先に「設定変更を適用」をクリックしてください', + 'agents.footer.changed': 'モデル選択が変更されました。設定変更を適用すると新しいモデルが使用されます。', 'agents.footer.applied': '設定変更は適用済みです。アプリは関連フィールドだけを管理します。', - 'agents.footer.externalChanged': '外部変更を保持しています。再適用するまでこのクライアントは起動できません。', + 'agents.footer.externalChanged': '外部変更を保持しています。設定変更を適用すると管理対象フィールドが再度書き込まれます。', 'agents.footer.invalidManaged': '設定が無効です。変更を適用して修復するか、デフォルト設定にリセットしてください。', 'agents.footer.unsupported': '現在のシステムではこのクライアントを設定できません', 'agents.footer.installFirst': 'クライアントをインストールしてから再検出してください', 'agents.footer.noCommand': '設定ファイルは検出しましたが、クライアントコマンドが見つかりません', 'agents.footer.invalidConfig': '元の設定形式が無効なため、安全に変更できません', - 'agents.footer.enableFirst': '先に「設定変更を適用」をクリックし、CPA モデル設定を反映してからクライアントを起動してください', 'agents.launchMethods': 'Codex の起動方法', 'agents.launch.enableFirst': '先に設定変更を適用してください', 'agents.launch.applyFirst': '先に選択中のモデルを適用してください', 'agents.launch.starting': '起動中', 'agents.launch.start': '{target} を起動', 'agents.launch.unavailable': '起動できません', + 'agents.launch.closeChatgpt': 'ChatGPT App を終了', + 'agents.launch.closingChatgpt': '終了中', 'agents.default.title': 'デフォルト設定にリセット', 'agents.default.description': '選択中のモデルを使って {name} の関連設定ファイルをすべて書き直し、その他のユーザー内容を削除します。バックアップは作成されず、元に戻せません。', 'agents.default.confirm': 'デフォルト設定を書き込む', @@ -489,6 +486,11 @@ export const jaOverrides = { 'agents.clear.description': 'Codex 設定ディレクトリの auth.json と config.toml を完全に削除し、このアプリが記録した適用状態をリセットします。バックアップは作成されず、元に戻せません。', 'agents.clear.confirm': '設定を消去', 'agents.clear.success': 'Codex 設定を消去しました。', + 'agents.closeApp.title': 'ChatGPT App を終了', + 'agents.closeApp.description': 'このコンピューターで実行中の ChatGPT App プロセスとその子プロセスをすべて終了します。生成中の応答や未保存の状態が失われる可能性があります。', + 'agents.closeApp.confirm': 'App を終了', + 'agents.closeApp.success': 'ChatGPT App を終了しました({count} プロセス)。', + 'agents.closeApp.notRunning': 'ChatGPT App は実行されていません。', 'apiAccess.compatibleName': 'OpenAI 互換 {number}', 'apiAccess.error.headerMissingColon': 'カスタムヘッダーの {number} 行目にコロンがありません', 'apiAccess.error.headerInvalid': 'カスタムヘッダーの {number} 行目の形式が無効です', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 194334f..8c3ae15 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -390,7 +390,6 @@ export const zhCN = { 'agents.sessions.repairTitle': '恢复历史会话', 'agents.sessions.repairDescription': '将其他渠道的会话同步到当前会话', 'agents.sessions.autoRepair': '启动 Codex 前自动恢复历史会话', - 'agents.sessions.autoRestoreDescription': '从本软件启动 Codex 时自动同步会话', 'agents.sessions.autoRepairEnabled': '已开启启动前自动恢复历史会话。', 'agents.sessions.autoRepairDisabled': '已关闭启动前自动恢复历史会话。', 'agents.sessions.settingLoading': '读取中', @@ -456,10 +455,8 @@ export const zhCN = { 'agents.clientVersion': '客户端版本', 'agents.notFetched': '未获取', 'agents.useModel': '使用模型', - 'agents.useModelDescription': '同步内核当前开放的模型,并将所选项设为默认模型', 'agents.pending': '尚未应用', 'agents.model.readingAvailable': '正在读取可用模型', - 'agents.model.cannotConfigure': '无可选模型,无法应用配置修改', 'agents.model.current': '当前已应用:{model}', 'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项', 'agents.modify.title': '配置修改', @@ -469,22 +466,22 @@ export const zhCN = { 'agents.modify.externalChanged': '检测到外部配置变化。软件会保留这些内容;点击“应用配置修改”后才会重新写入受管字段。', 'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。', 'agents.modify.applied': '软件负责的配置字段已应用,其他用户内容保持不变。', - 'agents.modify.unconfigured': '选择模型后可应用配置修改;不会创建配置副本或备份。', - 'agents.footer.changed': '模型选择已变化,请先点击“应用配置修改”', + 'agents.footer.changed': '模型选择已变化;应用配置修改后将使用新模型。', 'agents.footer.applied': '配置修改已应用;软件只管理相关字段。', - 'agents.footer.externalChanged': '外部配置已保留。重新应用前无法启动该客户端。', + 'agents.footer.externalChanged': '外部配置已保留;应用配置修改后才会重新写入受管字段。', 'agents.footer.invalidManaged': '配置当前无效,请应用修改进行修复,或重置为默认配置。', 'agents.footer.unsupported': '当前系统无法配置该客户端', 'agents.footer.installFirst': '请先安装客户端并重新检测', 'agents.footer.noCommand': '检测到配置文件,但没有找到客户端命令', 'agents.footer.invalidConfig': '原配置格式异常,无法安全修改', - 'agents.footer.enableFirst': '请先点击“应用配置修改”,确保 CPA 模型配置生效后再启动客户端', 'agents.launchMethods': 'Codex 启动方式', 'agents.launch.enableFirst': '请先应用配置修改', 'agents.launch.applyFirst': '请先应用当前模型选择', 'agents.launch.starting': '启动中', 'agents.launch.start': '启动 {target}', 'agents.launch.unavailable': '无法启动', + 'agents.launch.closeChatgpt': '关闭 ChatGPT App', + 'agents.launch.closingChatgpt': '关闭中', 'agents.default.title': '重置为默认配置', 'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。', 'agents.default.confirm': '确认写入默认配置', @@ -492,6 +489,11 @@ export const zhCN = { 'agents.clear.description': '这会永久删除 Codex 配置目录中的 auth.json 和 config.toml,并重置软件记录的应用状态。该操作没有备份且无法恢复。', 'agents.clear.confirm': '确认清空配置', 'agents.clear.success': 'Codex 配置已清空。', + 'agents.closeApp.title': '关闭 ChatGPT App', + 'agents.closeApp.description': '这会关闭本机所有正在运行的 ChatGPT App 进程及其子进程。正在生成的回复或尚未保存的状态可能丢失。', + 'agents.closeApp.confirm': '确认关闭 App', + 'agents.closeApp.success': 'ChatGPT App 已关闭({count} 个进程)。', + 'agents.closeApp.notRunning': 'ChatGPT App 当前没有运行。', 'apiAccess.compatibleName': 'OpenAI 兼容 {number}', 'apiAccess.error.headerMissingColon': '自定义请求头第 {number} 行缺少冒号', 'apiAccess.error.headerInvalid': '自定义请求头第 {number} 行格式无效', @@ -1174,7 +1176,6 @@ export const en: Record = { 'agents.sessions.repairTitle': 'Restore Historical Sessions', 'agents.sessions.repairDescription': 'Sync sessions from other providers to the current session provider.', 'agents.sessions.autoRepair': 'Restore historical sessions before launching Codex', - 'agents.sessions.autoRestoreDescription': 'Automatically sync sessions when Codex is launched from this app.', 'agents.sessions.autoRepairEnabled': 'Automatic restoration before launch is enabled.', 'agents.sessions.autoRepairDisabled': 'Automatic restoration before launch is disabled.', 'agents.sessions.settingLoading': 'Loading', @@ -1240,10 +1241,8 @@ export const en: Record = { 'agents.clientVersion': 'Client Version', 'agents.notFetched': 'Not available', 'agents.useModel': 'Model', - 'agents.useModelDescription': 'Synchronize models currently exposed by the core and set the selection as the default', 'agents.pending': 'Not Applied', 'agents.model.readingAvailable': 'Loading available models', - 'agents.model.cannotConfigure': 'No models are available, so configuration changes cannot be applied', 'agents.model.current': 'Currently applied: {model}', 'agents.model.firstSelection': '{count} models available; the first is selected by default', 'agents.modify.title': 'Configuration Changes', @@ -1253,22 +1252,22 @@ export const en: Record = { 'agents.modify.externalChanged': 'External configuration changes were detected and preserved. Managed fields are rewritten only after you click Apply Configuration Changes.', 'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.', 'agents.modify.applied': 'The fields managed by the app are applied; all other user content remains unchanged.', - 'agents.modify.unconfigured': 'Select a model and apply the configuration. No configuration copy or backup is created.', - 'agents.footer.changed': 'The model selection changed. Click “Apply Configuration Changes” first.', + 'agents.footer.changed': 'The model selection changed. Apply the configuration to use the new model.', 'agents.footer.applied': 'Configuration changes are applied; only the relevant fields are managed by the app.', - 'agents.footer.externalChanged': 'External changes were preserved. Apply them before launching this client.', + 'agents.footer.externalChanged': 'External changes were preserved. Managed fields are rewritten when you apply the configuration.', 'agents.footer.invalidManaged': 'The configuration is invalid. Apply changes to repair it or reset it to the default configuration.', 'agents.footer.unsupported': 'This client cannot be configured on the current system', 'agents.footer.installFirst': 'Install the client, then detect again', 'agents.footer.noCommand': 'A configuration file was found, but no client command is available', 'agents.footer.invalidConfig': 'The original configuration is invalid and cannot be modified safely', - 'agents.footer.enableFirst': 'Click “Apply Configuration Changes” before launching so the CPA model configuration is active', 'agents.launchMethods': 'Codex launch methods', 'agents.launch.enableFirst': 'Apply the configuration changes first', 'agents.launch.applyFirst': 'Apply the selected model first', 'agents.launch.starting': 'Starting', 'agents.launch.start': 'Start {target}', 'agents.launch.unavailable': 'Cannot Start', + 'agents.launch.closeChatgpt': 'Close ChatGPT App', + 'agents.launch.closingChatgpt': 'Closing', 'agents.default.title': 'Reset to Default Configuration', 'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. No backup is created and the operation cannot be undone.', 'agents.default.confirm': 'Write Default Configuration', @@ -1276,6 +1275,11 @@ export const en: Record = { 'agents.clear.description': 'This permanently deletes auth.json and config.toml from the Codex configuration directory and resets the application state tracked by this app. No backup is created and the operation cannot be undone.', 'agents.clear.confirm': 'Clear Configuration', 'agents.clear.success': 'The Codex configuration has been cleared.', + 'agents.closeApp.title': 'Close ChatGPT App', + 'agents.closeApp.description': 'This closes all running ChatGPT App processes and their child processes on this computer. In-progress responses or unsaved state may be lost.', + 'agents.closeApp.confirm': 'Close App', + 'agents.closeApp.success': 'ChatGPT App was closed ({count} processes).', + 'agents.closeApp.notRunning': 'ChatGPT App is not running.', 'apiAccess.compatibleName': 'OpenAI Compatible {number}', 'apiAccess.error.headerMissingColon': 'Custom header line {number} is missing a colon', 'apiAccess.error.headerInvalid': 'Custom header line {number} is invalid', diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 1344e11..498b24e 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -18,6 +18,7 @@ import { ChevronDown, LoaderCircle, Play, + Power, RefreshCw, Search, Sparkles, @@ -83,6 +84,11 @@ type AgentConfigActionResult = { conflictFiles: string[]; }; +type ChatGptCloseResult = { + wasRunning: boolean; + closedProcesses: number; +}; + type AgentDefinition = { id: AgentClientId; name: string; @@ -489,7 +495,7 @@ export function AgentsPage() { const [launchTargetByClient, setLaunchTargetByClient] = useState>>({}); const [loading, setLoading] = useState(true); const [modelLoading, setModelLoading] = useState(false); - const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | null>(null); + const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | 'close-app' | null>(null); const busy = busyAction !== null; const [detectionError, setDetectionError] = useState(''); const [modelError, setModelError] = useState(''); @@ -501,6 +507,9 @@ export function AgentsPage() { const [clearError, setClearError] = useState(''); const [clearNotice, setClearNotice] = useState(''); const [clearConfirmOpen, setClearConfirmOpen] = useState(false); + const [closeAppError, setCloseAppError] = useState(''); + const [closeAppNotice, setCloseAppNotice] = useState(''); + const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false); const loadStatuses = useCallback(async (forceRefresh = false) => { const command = forceRefresh @@ -582,6 +591,9 @@ export function AgentsPage() { setClearError(''); setClearNotice(''); setClearConfirmOpen(false); + setCloseAppError(''); + setCloseAppNotice(''); + setCloseAppConfirmOpen(false); }, [selected]); useEffect(() => { @@ -624,10 +636,41 @@ export function AgentsPage() { const canLaunch = Boolean( activeStatus?.supportedPlatform && activeStatus.installed - && activeStatus.modificationEnabled - && activeStatus.modificationState === 'applied' - && selectedLaunchTarget, + && selectedLaunchTarget + && (selected === 'codex' + || (activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')), ); + const modelHint = modelSelectionError + || modelError + || (modelLoading + ? t('agents.model.readingAvailable') + : models.length === 0 + ? '' + : activeStatus?.modificationState === 'applied' + ? t('agents.model.current', { model: appliedModel || '—' }) + : t('agents.model.firstSelection', { count: models.length })); + const modificationDescription = activeStatus?.modificationState === 'external-changed' + ? t('agents.modify.externalChanged') + : activeStatus?.modificationState === 'invalid' + ? t('agents.modify.invalid') + : activeStatus?.modificationState === 'applied' + ? t('agents.modify.applied') + : ''; + const footerMessage = activeStatus?.modificationState === 'applied' + ? draftChanged + ? t('agents.footer.changed') + : t('agents.footer.applied') + : activeStatus?.modificationState === 'external-changed' + ? t('agents.footer.externalChanged') + : activeStatus?.modificationState === 'invalid' + ? t('agents.footer.invalidManaged') + : !activeStatus?.supportedPlatform + ? t('agents.footer.unsupported') + : !activeStatus.installed + ? t('agents.footer.installFirst') + : activeStatus.launchTargets.length === 0 + ? t('agents.footer.noCommand') + : ''; const refreshModels = () => { void loadModels(); @@ -728,7 +771,7 @@ export function AgentsPage() { setBusyAction('launch'); setLaunchError(''); try { - if (draftChanged) { + if (selected !== 'codex' && draftChanged) { throw new Error(t('agents.error.applyFirst')); } await invoke('launch_agent', { client: selected, target: selectedLaunchTarget?.id }); @@ -739,6 +782,23 @@ export function AgentsPage() { } }; + const closeChatGptApp = async () => { + setBusyAction('close-app'); + setCloseAppError(''); + setCloseAppNotice(''); + try { + const result = await invoke('close_chatgpt_app'); + setCloseAppConfirmOpen(false); + setCloseAppNotice(result.wasRunning + ? t('agents.closeApp.success', { count: result.closedProcesses }) + : t('agents.closeApp.notRunning')); + } catch (requestError) { + setCloseAppError(String(requestError)); + } finally { + setBusyAction(null); + } + }; + const openDefaultConfirmation = () => { setDefaultError(''); setDefaultConfirmOpen(true); @@ -759,6 +819,16 @@ export function AgentsPage() { setClearConfirmOpen(false); }; + const openCloseAppConfirmation = () => { + setCloseAppError(''); + setCloseAppConfirmOpen(true); + }; + + const closeCloseAppConfirmation = () => { + setCloseAppError(''); + setCloseAppConfirmOpen(false); + }; + const availableSubpages = agentSubpages.filter( (subpage) => !subpage.clients || subpage.clients.includes(selected), ); @@ -864,7 +934,7 @@ export function AgentsPage() {
-
{t('agents.useModel')}{t('agents.useModelDescription')}
+
{t('agents.useModel')}
{draftChanged ? {t('agents.pending')} : null}
- - {modelSelectionError - || modelError - || (modelLoading - ? t('agents.model.readingAvailable') - : models.length === 0 - ? t('agents.model.cannotConfigure') - : activeStatus?.modificationState === 'applied' - ? t('agents.model.current', { model: appliedModel || '—' }) - : t('agents.model.firstSelection', { count: models.length }))} - + {modelHint ? ( + + {modelHint} + + ) : null}
{t('agents.modify.title')} - {activeStatus?.modificationState === 'external-changed' - ? t('agents.modify.externalChanged') - : activeStatus?.modificationState === 'invalid' - ? t('agents.modify.invalid') - : activeStatus?.modificationState === 'applied' - ? t('agents.modify.applied') - : t('agents.modify.unconfigured')} + {modificationDescription ? {modificationDescription} : null}
@@ -957,24 +1015,12 @@ export function AgentsPage() { {selected === 'codex' ? : null}
-
- {activeStatus?.modificationState === 'applied' ? : } - {activeStatus?.modificationState === 'applied' - ? draftChanged - ? t('agents.footer.changed') - : t('agents.footer.applied') - : activeStatus?.modificationState === 'external-changed' - ? t('agents.footer.externalChanged') - : activeStatus?.modificationState === 'invalid' - ? t('agents.footer.invalidManaged') - : !activeStatus?.supportedPlatform - ? t('agents.footer.unsupported') - : !activeStatus.installed - ? t('agents.footer.installFirst') - : activeStatus.launchTargets.length === 0 - ? t('agents.footer.noCommand') - : t('agents.footer.enableFirst')} -
+ {footerMessage ? ( +
+ {activeStatus?.modificationState === 'applied' ? : } + {footerMessage} +
+ ) : null}
{activeLaunchTargets.length > 1 ? ( @@ -1003,23 +1049,41 @@ export function AgentsPage() { disabled={ busy || !canLaunch - || draftChanged + || (selected !== 'codex' && draftChanged) } - title={draftChanged - ? t('agents.launch.applyFirst') - : activeStatus?.modificationState === 'applied' - ? selectedLaunchTarget?.detail - : t('agents.launch.enableFirst')} + title={selected === 'codex' + ? selectedLaunchTarget?.detail + : draftChanged + ? t('agents.launch.applyFirst') + : activeStatus?.modificationState === 'applied' + ? selectedLaunchTarget?.detail + : t('agents.launch.enableFirst')} > {busyAction === 'launch' ? : } {busyAction === 'launch' ? t('agents.launch.starting') : selectedLaunchTarget ? t('agents.launch.start', { target: selectedLaunchTarget.label }) : t('agents.launch.unavailable')} + {selected === 'codex' ? ( + + ) : null}
{launchError ? ( {launchError} ) : null} + {closeAppNotice ? ( + + {closeAppNotice} + + ) : null}
@@ -1085,6 +1149,29 @@ export function AgentsPage() {
) : null} + + {closeAppConfirmOpen ? ( +
+
+
+

{t('agents.closeApp.title')}

+
+

{t('agents.closeApp.description')}

+ {closeAppError ? ( + + {closeAppError} + + ) : null} +
+ + +
+
+
+ ) : null} ); } diff --git a/src/pages/CodexSessionAutoRestoreCard.tsx b/src/pages/CodexSessionAutoRestoreCard.tsx index 2bb85be..941def0 100644 --- a/src/pages/CodexSessionAutoRestoreCard.tsx +++ b/src/pages/CodexSessionAutoRestoreCard.tsx @@ -77,7 +77,6 @@ export function CodexSessionAutoRestoreCard() {
{t('agents.sessions.autoRepair')} - {t('agents.sessions.autoRestoreDescription')}