refactor(agents): remove direct client launch functionality

This commit is contained in:
lzt404 committed 2026-08-07 23:04:18 +08:00
1 parent 7ee28e10b0
commit d2f238f829
17 files changed
+62 -2299

No files matched your search

+2 -2
View File
@@ -99,8 +99,8 @@ proxy. Supported clients include:
- Pi (with the CLIProxyAPI provider extension)
For supported clients, the application can synchronize the available model catalog, select a
default model, back up the original configuration before applying managed settings, restore the
previous configuration, and launch an available desktop or CLI entry point.
default model, back up the original configuration before applying managed settings, and restore the
previous configuration.
## Additional Capabilities
+2 -2
View File
@@ -94,8 +94,8 @@ API 接入页面按照协议或 Provider 管理上游 API 凭证和服务地址
- Hermes Agent
- Pi(通过 CLIProxyAPI provider 插件)
对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置、
恢复之前的配置,以及启动可用的桌面端或命令行入口。
对于受支持的客户端,软件可以同步可用模型目录、选择默认模型、在应用托管配置前备份原始配置,
以及恢复之前的配置。
## 其他功能
-3
View File
@@ -6,7 +6,6 @@
"name": "cpa-gui",
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.7.1",
"lucide-react": "^1.24.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -200,8 +199,6 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw=="],
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.2", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-pX0IGm1I3I6wc+zeKYcq1GSqogK6okCNX5fOdaNU5ab1AjGS6l1E5wFNjEb7meg7ZFSp0JUs+0jQGQNyOvLrsg=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
-1
View File
@@ -12,7 +12,6 @@
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-dialog": "^2.7.1",
"lucide-react": "^1.24.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
+1 -143
View File
@@ -605,7 +605,6 @@ dependencies = [
"tar",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tokio",
"tokio-util",
@@ -2488,7 +2487,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -3170,30 +3168,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -4030,48 +4004,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.3+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.4"
@@ -5219,15 +5151,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -5261,30 +5184,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -5315,12 +5221,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -5333,12 +5233,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -5351,24 +5245,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -5381,12 +5263,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -5399,12 +5275,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -5417,12 +5287,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -5435,12 +5299,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.5.40"
-1
View File
@@ -27,7 +27,6 @@ serde_yaml = "0.9"
sha2 = "0.10"
tar = "0.4"
tauri = { version = "~2.11", features = ["tray-icon"] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2"
tokio = { version = "1", features = ["macros", "sync", "time"] }
tokio-util = "0.7"
+1 -2
View File
@@ -8,7 +8,6 @@
"core:event:allow-unlisten",
"core:window:allow-set-theme",
"core:window:allow-set-background-color",
"core:webview:allow-set-webview-background-color",
"dialog:allow-open"
"core:webview:allow-set-webview-background-color"
]
}
+6 -224
View File
@@ -16,8 +16,6 @@ use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{Emitter, Manager};
use crate::GuiConfigState;
const DEFAULT_PAGE_SIZE: usize = 50;
const MAX_PAGE_SIZE: usize = 100;
const SQLITE_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
@@ -76,7 +74,6 @@ pub(crate) struct CodexSessionPage {
offset: usize,
limit: usize,
has_more: bool,
repair_on_launch: bool,
warnings: Vec<String>,
}
@@ -153,25 +150,6 @@ pub(crate) struct SessionIndexCleanupResult {
backup_path: Option<String>,
}
#[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 {
enabled: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RepairOnLaunchSetting {
enabled: bool,
}
#[derive(Debug)]
struct DatabaseDeletePlan {
path: PathBuf,
@@ -213,7 +191,6 @@ struct SessionIndexMetadata {
#[tauri::command]
pub(crate) async fn list_codex_sessions(
app: tauri::AppHandle,
gui_config_state: tauri::State<'_, GuiConfigState>,
request: Option<ListCodexSessionsRequest>,
) -> Result<CodexSessionPage, String> {
let user_home = app
@@ -221,13 +198,12 @@ pub(crate) async fn list_codex_sessions(
.home_dir()
.map_err(|error| format!("无法获取用户目录: {error}"))?;
let codex_home = resolve_codex_home(&user_home);
let repair_on_launch = gui_config_state.snapshot()?.codex_session_repair_on_launch;
let request = request.unwrap_or(ListCodexSessionsRequest {
offset: 0,
limit: DEFAULT_PAGE_SIZE,
});
tauri::async_runtime::spawn_blocking(move || {
list_codex_sessions_from_home(&codex_home, request.offset, request.limit, repair_on_launch)
list_codex_sessions_from_home(&codex_home, request.offset, request.limit)
})
.await
.map_err(|error| format!("读取 Codex 会话任务失败: {error}"))?
@@ -308,62 +284,6 @@ pub(crate) async fn apply_codex_session_index_cleanup(
.map_err(|error| format!("清理 Codex 会话索引任务失败: {error}"))?
}
#[tauri::command]
pub(crate) fn get_codex_session_repair_on_launch(
gui_config_state: tauri::State<'_, GuiConfigState>,
) -> Result<RepairOnLaunchSetting, String> {
let config = gui_config_state.snapshot()?;
Ok(RepairOnLaunchSetting {
enabled: config.codex_session_repair_on_launch,
})
}
#[tauri::command]
pub(crate) fn set_codex_session_repair_on_launch(
gui_config_state: tauri::State<'_, GuiConfigState>,
request: SetRepairOnLaunchRequest,
) -> Result<RepairOnLaunchSetting, String> {
let config = gui_config_state.set_codex_session_repair_on_launch(request.enabled)?;
Ok(RepairOnLaunchSetting {
enabled: config.codex_session_repair_on_launch,
})
}
#[tauri::command]
pub(crate) async fn close_chatgpt_app() -> Result<ChatGptCloseResult, String> {
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<CodexSessionRepairResult, String> {
let codex_home = resolve_codex_home(user_home);
repair_before_codex_launch_from_home(&codex_home)
}
fn repair_before_codex_launch_from_home(
codex_home: &Path,
) -> Result<CodexSessionRepairResult, String> {
let target_provider = resolve_current_codex_provider(codex_home)?;
match repair_codex_session_metadata_from_home(codex_home, &target_provider, |_| {}) {
Ok(result) => Ok(result),
Err(error) if is_locked_error_message(&error) => Ok(CodexSessionRepairResult {
target_provider,
changed_rollout_files: 0,
sqlite_rows_updated: 0,
skipped_locked_files: Vec::new(),
backup_path: None,
encrypted_content_warning: None,
warnings: vec![format!(
"启动前恢复历史会话时遇到占用文件,已跳过且继续启动:{error}"
)],
}),
Err(error) => Err(error),
}
}
fn resolve_codex_home(user_home: &Path) -> PathBuf {
existing_env_directory("CODEX_HOME").unwrap_or_else(|| user_home.join(".codex"))
}
@@ -521,7 +441,6 @@ fn list_codex_sessions_from_home(
codex_home: &Path,
offset: usize,
requested_limit: usize,
repair_on_launch: bool,
) -> Result<CodexSessionPage, String> {
let limit = requested_limit.clamp(1, MAX_PAGE_SIZE);
let fetch_limit = offset.saturating_add(limit).saturating_add(1);
@@ -556,7 +475,6 @@ fn list_codex_sessions_from_home(
offset,
limit,
has_more,
repair_on_launch,
warnings,
})
}
@@ -2362,11 +2280,6 @@ fn codex_app_process_ids() -> Result<Vec<u32>, String> {
windows_app_process_ids(&["ChatGPT.exe", "Codex.exe"])
}
#[cfg(target_os = "windows")]
fn chatgpt_app_process_ids() -> Result<Vec<u32>, String> {
windows_app_process_ids(&["ChatGPT.exe"])
}
#[cfg(target_os = "windows")]
fn windows_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
use std::os::windows::process::CommandExt;
@@ -2412,11 +2325,6 @@ fn codex_app_process_ids() -> Result<Vec<u32>, String> {
unix_app_process_ids(&["ChatGPT", "Codex"])
}
#[cfg(not(target_os = "windows"))]
fn chatgpt_app_process_ids() -> Result<Vec<u32>, String> {
unix_app_process_ids(&["ChatGPT"])
}
#[cfg(not(target_os = "windows"))]
fn unix_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
let mut ids = Vec::new();
@@ -2445,108 +2353,6 @@ fn unix_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
Ok(ids)
}
#[cfg(target_os = "windows")]
fn close_chatgpt_app_processes() -> Result<ChatGptCloseResult, String> {
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::<Vec<_>>()
.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<ChatGptCloseResult, String> {
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::<Vec<_>>()
.join("、")
));
}
Ok(ChatGptCloseResult {
was_running: true,
closed_processes: process_ids.len(),
})
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2613,14 +2419,14 @@ mod tests {
.unwrap();
drop(automation);
let page = list_codex_sessions_from_home(&root, 0, 50, false).unwrap();
let page = list_codex_sessions_from_home(&root, 0, 50).unwrap();
assert_eq!(page.sessions.len(), 2);
assert_eq!(page.sessions[0].id, "thread-1");
assert_eq!(page.sessions[0].title, "First new");
assert_eq!(page.sessions[0].updated_at_ms, Some(4000));
assert_eq!(page.sessions[1].id, "thread-2");
assert!(page.sessions[1].archived);
let second_page = list_codex_sessions_from_home(&root, 1, 1, false).unwrap();
let second_page = list_codex_sessions_from_home(&root, 1, 1).unwrap();
assert_eq!(second_page.sessions.len(), 1);
assert_eq!(second_page.sessions[0].id, "thread-2");
assert!(!second_page.has_more);
@@ -2653,7 +2459,7 @@ mod tests {
)
.unwrap();
let page = list_codex_sessions_from_home(&root, 0, 50, false).unwrap();
let page = list_codex_sessions_from_home(&root, 0, 50).unwrap();
assert_eq!(page.sessions.len(), 2);
assert_eq!(page.sessions[0].id, "api-thread");
assert_eq!(page.sessions[0].title, "API session");
@@ -2682,7 +2488,7 @@ mod tests {
drop(connection);
fs::write(sqlite.join("broken.db"), b"not a sqlite database").unwrap();
let page = list_codex_sessions_from_home(&root, 0, 0, true).unwrap();
let page = list_codex_sessions_from_home(&root, 0, 0).unwrap();
assert_eq!(page.limit, 1);
assert_eq!(page.sessions.len(), 1);
assert_eq!(page.sessions[0].id, "optional-thread");
@@ -2692,9 +2498,8 @@ mod tests {
.warnings
.iter()
.any(|warning| warning.contains("broken.db")));
assert!(page.repair_on_launch);
assert_eq!(
list_codex_sessions_from_home(&root, 0, usize::MAX, false)
list_codex_sessions_from_home(&root, 0, usize::MAX)
.unwrap()
.limit,
MAX_PAGE_SIZE
@@ -3092,29 +2897,6 @@ mod tests {
assert!(!is_locked_error_message("database disk image is malformed"));
}
#[test]
fn launch_hook_repairs_sessions_for_the_current_provider() {
let root = test_root("repair-launch-hook");
fs::create_dir_all(root.join("sessions")).unwrap();
let rollout = root.join("sessions/rollout.jsonl");
fs::write(
&rollout,
"{\"type\":\"session_meta\",\"payload\":{\"id\":\"thread-1\",\"model_provider\":\"legacy\"}}\n",
)
.unwrap();
fs::write(root.join("config.toml"), "model_provider = \"openai\"\n").unwrap();
let result = repair_before_codex_launch_from_home(&root).unwrap();
assert_eq!(result.target_provider, "openai");
assert!(fs::read_to_string(&rollout).unwrap().contains("openai"));
fs::write(root.join("config.toml"), "model_provider = \"cpa-gui\"\n").unwrap();
let result = repair_before_codex_launch_from_home(&root).unwrap();
assert_eq!(result.target_provider, "cpa-gui");
assert_eq!(result.changed_rollout_files, 1);
assert!(fs::read_to_string(&rollout).unwrap().contains("cpa-gui"));
fs::remove_dir_all(root).unwrap();
}
#[test]
fn session_index_cleanup_preserves_live_and_unknown_records_and_checks_snapshot() {
let root = test_root("index");
+43 -1092
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -2201,9 +2201,6 @@ mod tests {
proxy_url: String::new(),
routing_session_affinity: false,
routing_session_affinity_ttl: String::new(),
codex_session_repair_on_launch: false,
claude_code_working_directory: String::new(),
claude_code_working_directory_prompt_disabled: false,
};
let record = normalize_usage_record(
serde_json::json!({
+1 -33
View File
@@ -499,11 +499,6 @@ export const jaOverrides = {
'agents.sessions.databaseMissing': 'セッションデータベースが見つかりません',
'agents.sessions.repairTitle': '過去のセッションを復元',
'agents.sessions.repairDescription': '他の Provider のセッションを現在のセッション Provider に同期します。',
'agents.sessions.autoRepair': '過去のセッションを自動復元',
'agents.sessions.settingLoading': '読み込み中',
'agents.sessions.settingOn': 'オン',
'agents.sessions.settingOff': 'オフ',
'agents.sessions.settingLoadFailed': '自動復元設定の読み込みに失敗しました:{error}',
'agents.sessions.repairNow': '今すぐ復元',
'agents.sessions.repairing': '復元中',
'agents.sessions.loadFailed': 'Codex セッションの読み込みに失敗しました:{error}',
@@ -535,7 +530,6 @@ export const jaOverrides = {
'agents.sessions.deleteResult': '{deleted} 件を削除、{failed} 件失敗。{details}{backup}',
'agents.sessions.deleteFailed': 'セッションの削除に失敗しました:{error}',
'agents.sessions.backupAt': 'バックアップ先:{path}',
'agents.sessions.settingFailed': '自動復元設定の保存に失敗しました:{error}',
'agents.sessions.repairResult': '過去のセッションを {provider} に復元しました:rollout {files} ファイルを移行、データベース {rows} 行を更新、使用中の {skipped} ファイルをスキップしました。',
'agents.sessions.repairFailed': '過去のセッションの復元に失敗しました:{error}',
'agents.sessions.progressCount': '{processed}/{total}',
@@ -574,29 +568,17 @@ export const jaOverrides = {
'agents.claudeDesktopMapping.title': 'Claude Desktop モデル → 上流モデル',
'agents.claudeDesktopMapping.description': '各 Claude Desktop モデルが実際に呼び出す上流モデルを選択します。',
'agents.claudeCodeMapping.title': 'Claude Code モデル → 上流モデル',
'agents.claudeCodeMapping.description': '各タスク役割に対して、起動時に設定する上流モデルを選択します。',
'agents.claudeCodeMapping.description': '各タスク役割に対して使用する上流モデルを選択します。',
'agents.claudeDesktopMapping.customMapping': 'カスタムマッピング',
'agents.claudeDesktopMapping.customMappingHint': '有効にするとエイリアスモデルのみ、無効にするとエイリアス未設定の元モデルのみ表示します。',
'agents.claudeDesktopMapping.opus': 'Claude Opus 5 のマッピング先',
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 のマッピング先',
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 のマッピング先',
'agents.claudeCodeLaunch.noDirectory': '未選択',
'agents.claudeCodeLaunch.chooseDirectory': 'ディレクトリを選択',
'agents.claudeCodeLaunch.selectedDirectory': '選択した作業ディレクトリ',
'agents.claudeCodeLaunch.choosing': '選択中',
'agents.claudeCodeLaunch.dialogTitle': '{client} の起動ディレクトリを選択',
'agents.claudeCodeLaunch.dialogDescription': '{client} を起動する前に、作業ディレクトリを選択してください。',
'agents.claudeCodeLaunch.workingDirectory': '作業ディレクトリ',
'agents.claudeCodeLaunch.browse': '参照',
'agents.claudeCodeLaunch.neverAskAgain': '今後この確認を表示しない',
'agents.claudeCodeLaunch.directoryRequired': '先に作業ディレクトリを選択してください',
'agents.claudeCodeLaunch.launch': '{client} を起動',
'agents.oauthLoginRequired.title': 'Codex へのログインが必要です',
'agents.oauthLoginRequired.enableDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定方式を有効にできません。Codex アプリで ChatGPT アカウントにログインしてください。Codex アプリでログインを開始できない場合は、先に',
'agents.oauthLoginRequired.enableClearConfiguration': '設定を消去',
'agents.oauthLoginRequired.enableDescriptionSuffix': 'を使用してください。',
'agents.oauthLoginRequired.applyDescription': '有効な Codex のログイン認証情報が見つからないため、OAuth 設定を適用できません。Codex で ChatGPT アカウントにログインしてから、設定を適用してください。',
'agents.oauthLoginRequired.launchDescription': 'OAuth 設定方式が有効ですが、有効な Codex のログイン認証情報が見つかりません。起動する前に Codex で ChatGPT アカウントにログインしてください。',
'agents.oauthLoginRequired.confirm': '了解しました',
'agents.modify.apply': '設定変更を適用',
'agents.modify.update': '設定を更新',
@@ -604,15 +586,6 @@ export const jaOverrides = {
'agents.modify.default': 'デフォルト設定',
'agents.modify.clear': '設定を消去',
'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。',
'agents.launch.enableFirst': '先に設定変更を適用してください',
'agents.launch.applyFirst': '先に選択中のモデルを適用してください',
'agents.launch.starting': '起動中',
'agents.launch.startCli': 'CLI を起動',
'agents.launch.startApp': 'App を起動',
'agents.launch.start': '{target} を起動',
'agents.launch.unavailable': '起動できません',
'agents.launch.closeChatgpt': 'ChatGPT App を終了',
'agents.launch.closingChatgpt': '終了中',
'agents.pi.installTitle': 'Pi CLIProxyAPI プロバイダー',
'agents.pi.installDescription': 'プラグインをインストールまたは更新し、CPA 認証情報、既定のプロバイダー、既定のモデルを修復します',
'agents.pi.install': 'Pi プラグインをインストール',
@@ -631,11 +604,6 @@ 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} 行目の形式が無効です',
+2 -66
View File
@@ -502,11 +502,6 @@ export const zhCN = {
'agents.sessions.databaseMissing': '未发现会话数据库',
'agents.sessions.repairTitle': '恢复历史会话',
'agents.sessions.repairDescription': '将其他渠道的会话同步到当前会话',
'agents.sessions.autoRepair': '自动恢复历史会话',
'agents.sessions.settingLoading': '读取中',
'agents.sessions.settingOn': '已开启',
'agents.sessions.settingOff': '已关闭',
'agents.sessions.settingLoadFailed': '读取自动恢复设置失败:{error}',
'agents.sessions.repairNow': '立即恢复',
'agents.sessions.repairing': '正在恢复',
'agents.sessions.loadFailed': '读取 Codex 会话失败:{error}',
@@ -538,7 +533,6 @@ export const zhCN = {
'agents.sessions.deleteResult': '已删除 {deleted} 个,失败 {failed} 个。{details}{backup}',
'agents.sessions.deleteFailed': '删除会话失败:{error}',
'agents.sessions.backupAt': '备份位置:{path}',
'agents.sessions.settingFailed': '保存自动恢复设置失败:{error}',
'agents.sessions.repairResult': '历史会话已恢复到 {provider}:迁移 {files} 个 rollout 文件、更新 {rows} 条数据库记录、跳过 {skipped} 个占用文件。',
'agents.sessions.repairFailed': '恢复历史会话失败:{error}',
'agents.sessions.progressCount': '{processed}/{total}',
@@ -572,23 +566,12 @@ export const zhCN = {
'agents.claudeDesktopMapping.title': 'Claude Desktop 模型 → 上游模型',
'agents.claudeDesktopMapping.description': '选择每个 Claude Desktop 模型实际调用的上游模型。',
'agents.claudeCodeMapping.title': 'Claude Code 模型 → 上游模型',
'agents.claudeCodeMapping.description': '为不同任务角色选择启动时注入的上游模型。',
'agents.claudeCodeMapping.description': '为不同任务角色选择使用的上游模型。',
'agents.claudeDesktopMapping.customMapping': '自定义映射',
'agents.claudeDesktopMapping.customMappingHint': '开启后只显示别名模型;关闭后只显示未设置别名的原始模型。',
'agents.claudeDesktopMapping.opus': 'Claude Opus 5 映射到',
'agents.claudeDesktopMapping.sonnet': 'Claude Sonnet 4.6 映射到',
'agents.claudeDesktopMapping.haiku': 'Claude Haiku 4.5 映射到',
'agents.claudeCodeLaunch.noDirectory': '尚未选择',
'agents.claudeCodeLaunch.chooseDirectory': '选择目录',
'agents.claudeCodeLaunch.selectedDirectory': '已选择的工作目录',
'agents.claudeCodeLaunch.choosing': '选择中',
'agents.claudeCodeLaunch.dialogTitle': '选择 {client} 启动目录',
'agents.claudeCodeLaunch.dialogDescription': '需要先选择工作目录,然后才能启动 {client}。',
'agents.claudeCodeLaunch.workingDirectory': '工作目录',
'agents.claudeCodeLaunch.browse': '浏览',
'agents.claudeCodeLaunch.neverAskAgain': '永久关闭此提示',
'agents.claudeCodeLaunch.directoryRequired': '请先选择工作目录',
'agents.claudeCodeLaunch.launch': '启动 {client}',
'agents.model.readingAvailable': '正在读取可用模型',
'agents.model.current': '当前已应用:{model}',
'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项',
@@ -599,7 +582,6 @@ export const zhCN = {
'agents.oauthLoginRequired.enableClearConfiguration': '清空配置',
'agents.oauthLoginRequired.enableDescriptionSuffix': '功能。',
'agents.oauthLoginRequired.applyDescription': '无法应用 OAuth 配置:未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再应用配置。',
'agents.oauthLoginRequired.launchDescription': '已开启 OAuth 配置模式,但未检测到有效的 Codex 登录凭据。请先在 Codex 中登录 ChatGPT 账号,然后再启动。',
'agents.oauthLoginRequired.confirm': '我知道了',
'agents.modify.apply': '应用配置修改',
'agents.modify.update': '更新配置',
@@ -607,15 +589,6 @@ export const zhCN = {
'agents.modify.default': '默认配置',
'agents.modify.clear': '清空配置',
'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。',
'agents.launch.enableFirst': '请先应用配置修改',
'agents.launch.applyFirst': '请先应用当前模型选择',
'agents.launch.starting': '启动中',
'agents.launch.startCli': '启动 CLI',
'agents.launch.startApp': '启动 App',
'agents.launch.start': '启动 {target}',
'agents.launch.unavailable': '无法启动',
'agents.launch.closeChatgpt': '关闭 ChatGPT App',
'agents.launch.closingChatgpt': '关闭中',
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
'agents.pi.installDescription': '安装或更新插件,并修复 CPA 凭据、默认 provider 与默认模型',
'agents.pi.install': '安装 Pi 插件',
@@ -634,11 +607,6 @@ 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} 行格式无效',
@@ -1463,11 +1431,6 @@ export const en: Record<MessageKey, string> = {
'agents.sessions.databaseMissing': 'No session database found',
'agents.sessions.repairTitle': 'Restore Historical Sessions',
'agents.sessions.repairDescription': 'Sync sessions from other providers to the current session provider.',
'agents.sessions.autoRepair': 'Automatically restore historical sessions',
'agents.sessions.settingLoading': 'Loading',
'agents.sessions.settingOn': 'On',
'agents.sessions.settingOff': 'Off',
'agents.sessions.settingLoadFailed': 'Failed to load the automatic-restoration setting: {error}',
'agents.sessions.repairNow': 'Restore now',
'agents.sessions.repairing': 'Restoring',
'agents.sessions.loadFailed': 'Failed to load Codex sessions: {error}',
@@ -1499,7 +1462,6 @@ export const en: Record<MessageKey, string> = {
'agents.sessions.deleteResult': 'Deleted {deleted}; failed {failed}. {details}{backup}',
'agents.sessions.deleteFailed': 'Failed to delete sessions: {error}',
'agents.sessions.backupAt': 'Backup: {path}',
'agents.sessions.settingFailed': 'Failed to save the automatic-restoration setting: {error}',
'agents.sessions.repairResult': 'Historical sessions restored to {provider}: migrated {files} rollout files, updated {rows} database rows, and skipped {skipped} locked files.',
'agents.sessions.repairFailed': 'Failed to restore historical sessions: {error}',
'agents.sessions.progressCount': '{processed}/{total}',
@@ -1533,23 +1495,12 @@ export const en: Record<MessageKey, string> = {
'agents.claudeDesktopMapping.title': 'Claude Desktop Model → Upstream Model',
'agents.claudeDesktopMapping.description': 'Choose the upstream model each Claude Desktop model actually calls.',
'agents.claudeCodeMapping.title': 'Claude Code Model → Upstream Model',
'agents.claudeCodeMapping.description': 'Choose the upstream model injected at launch for each task role.',
'agents.claudeCodeMapping.description': 'Choose the upstream model used for each task role.',
'agents.claudeDesktopMapping.customMapping': 'Custom Mapping',
'agents.claudeDesktopMapping.customMappingHint': 'When enabled, only aliased models are shown; when disabled, only original models without aliases are shown.',
'agents.claudeDesktopMapping.opus': 'Map Claude Opus 5 to',
'agents.claudeDesktopMapping.sonnet': 'Map Claude Sonnet 4.6 to',
'agents.claudeDesktopMapping.haiku': 'Map Claude Haiku 4.5 to',
'agents.claudeCodeLaunch.noDirectory': 'Not selected',
'agents.claudeCodeLaunch.chooseDirectory': 'Choose Directory',
'agents.claudeCodeLaunch.selectedDirectory': 'Selected working directory',
'agents.claudeCodeLaunch.choosing': 'Choosing',
'agents.claudeCodeLaunch.dialogTitle': 'Choose a Launch Directory for {client}',
'agents.claudeCodeLaunch.dialogDescription': 'Select a working directory before starting {client}.',
'agents.claudeCodeLaunch.workingDirectory': 'Working directory',
'agents.claudeCodeLaunch.browse': 'Browse',
'agents.claudeCodeLaunch.neverAskAgain': 'Never show this prompt again',
'agents.claudeCodeLaunch.directoryRequired': 'Select a working directory first',
'agents.claudeCodeLaunch.launch': 'Start {client}',
'agents.model.readingAvailable': 'Loading available models',
'agents.model.current': 'Currently applied: {model}',
'agents.model.firstSelection': '{count} models available; the first is selected by default',
@@ -1560,7 +1511,6 @@ export const en: Record<MessageKey, string> = {
'agents.oauthLoginRequired.enableClearConfiguration': 'Clear Configuration',
'agents.oauthLoginRequired.enableDescriptionSuffix': 'first.',
'agents.oauthLoginRequired.applyDescription': 'OAuth configuration cannot be applied because no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex, then apply the configuration.',
'agents.oauthLoginRequired.launchDescription': 'OAuth configuration mode is enabled, but no valid Codex sign-in credentials were found. Sign in to your ChatGPT account in Codex before launching it.',
'agents.oauthLoginRequired.confirm': 'Got it',
'agents.modify.apply': 'Apply Configuration Changes',
'agents.modify.update': 'Update Configuration',
@@ -1568,15 +1518,6 @@ export const en: Record<MessageKey, string> = {
'agents.modify.default': 'Default Configuration',
'agents.modify.clear': 'Clear Configuration',
'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.',
'agents.launch.enableFirst': 'Apply the configuration changes first',
'agents.launch.applyFirst': 'Apply the selected model first',
'agents.launch.starting': 'Starting',
'agents.launch.startCli': 'Start CLI',
'agents.launch.startApp': 'Start App',
'agents.launch.start': 'Start {target}',
'agents.launch.unavailable': 'Cannot Start',
'agents.launch.closeChatgpt': 'Close ChatGPT App',
'agents.launch.closingChatgpt': 'Closing',
'agents.pi.installTitle': 'Pi CLIProxyAPI provider',
'agents.pi.installDescription': 'Install or update the plugin and repair CPA credentials, the default provider, and the default model.',
'agents.pi.install': 'Install Pi Plugin',
@@ -1595,11 +1536,6 @@ export const en: Record<MessageKey, string> = {
'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',
+2 -371
View File
@@ -10,20 +10,15 @@ import {
} from 'react';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { open } from '@tauri-apps/plugin-dialog';
import {
AlertTriangle,
AppWindow,
BadgeCheck,
Bot,
Check,
ChevronDown,
LoaderCircle,
Play,
Power,
RefreshCw,
Search,
Terminal,
Trash2,
Wrench,
X,
@@ -49,7 +44,6 @@ import {
} from '../services/agentConfigurationDraft';
import type { ModelOption } from '../services/modelService';
import { getCurrentLocale, translate, useI18n } from '../i18n';
import { CodexSessionAutoRestoreCard } from './CodexSessionAutoRestoreCard';
import { CodexSessionsPanel } from './CodexSessionsPanel';
type AgentClientId =
@@ -69,8 +63,6 @@ type AgentConfigStatus = {
supportedPlatform: boolean;
installed: boolean;
pluginInstalled: boolean;
executablePath: string | null;
launchTargets: AgentLaunchTarget[];
version: string | null;
cliVersion: string | null;
appVersion: string | null;
@@ -85,18 +77,10 @@ type AgentConfigStatus = {
appliedModel: string | null;
claudeCodeModelMappings: ClaudeModelMappings | null;
claudeDesktopModelMappings: ClaudeModelMappings | null;
claudeCodeWorkingDirectory: string | null;
claudeCodeWorkingDirectoryPromptDisabled: boolean;
warnings: string[];
error: string | null;
};
type AgentLaunchTarget = {
id: string;
label: string;
detail: string;
};
type AgentConfigActionResult = {
outcome: 'applied' | 'default';
enabled: boolean;
@@ -111,12 +95,7 @@ type PiProviderUpdateStatus = {
updateAvailable: boolean;
};
type ChatGptCloseResult = {
wasRunning: boolean;
closedProcesses: number;
};
type OAuthLoginRequiredAction = 'enable' | 'apply' | 'launch';
type OAuthLoginRequiredAction = 'enable' | 'apply';
type ClaudeModelMappings = {
opus: string;
@@ -555,30 +534,21 @@ export function AgentsPage() {
createClaudeModelMappings(''),
);
const [claudeCustomMapping, setClaudeCustomMapping] = useState(false);
const [launchDirectoryDialogOpen, setLaunchDirectoryDialogOpen] = useState(false);
const [launchDirectory, setLaunchDirectory] = useState('');
const [launchDirectoryTarget, setLaunchDirectoryTarget] = useState<AgentLaunchTarget | null>(null);
const [suppressLaunchDirectoryPrompt, setSuppressLaunchDirectoryPrompt] = useState(false);
const [launchDirectoryError, setLaunchDirectoryError] = useState('');
const [loading, setLoading] = useState(true);
const [modelLoading, setModelLoading] = useState(false);
const [busyAction, setBusyAction] = useState<
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'update-pi' | 'repair-pi' | 'uninstall-pi' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | 'oauth-check' | 'directory' | null
'apply' | 'close-config' | 'default' | 'clear' | 'install-pi' | 'update-pi' | 'repair-pi' | 'uninstall-pi' | 'oauth-check' | null
>(null);
const busy = busyAction !== null;
const [detectionError, setDetectionError] = useState('');
const [modelError, setModelError] = useState('');
const [modelSelectionError, setModelSelectionError] = useState('');
const [configurationError, setConfigurationError] = useState('');
const [launchError, setLaunchError] = useState('');
const [defaultError, setDefaultError] = useState('');
const [defaultConfirmOpen, setDefaultConfirmOpen] = useState(false);
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 [oauthLoginRequiredAction, setOauthLoginRequiredAction] = useState<OAuthLoginRequiredAction | null>(null);
const [oauthConfigurationDraft, setOauthConfigurationDraft] = useState<boolean | null>(null);
const [piProviderUpdateStatus, setPiProviderUpdateStatus] = useState<PiProviderUpdateStatus | null>(null);
@@ -673,20 +643,13 @@ export function AgentsPage() {
setActiveSubpage(DEFAULT_AGENT_SUBPAGE);
setModelSelectionError('');
setConfigurationError('');
setLaunchError('');
setDefaultError('');
setDefaultConfirmOpen(false);
setClearError('');
setClearNotice('');
setClearConfirmOpen(false);
setCloseAppError('');
setCloseAppNotice('');
setCloseAppConfirmOpen(false);
setOauthLoginRequiredAction(null);
setOauthConfigurationDraft(null);
setLaunchDirectoryDialogOpen(false);
setLaunchDirectoryTarget(null);
setLaunchDirectoryError('');
claudeModelMappingsDirtyRef.current = false;
}, [selected]);
@@ -768,10 +731,6 @@ export function AgentsPage() {
selectedModel,
]);
const activeLaunchTargets = activeStatus?.launchTargets ?? [];
const defaultLaunchTarget = activeLaunchTargets[0] ?? null;
const cliLaunchTarget = activeLaunchTargets.find((target) => target.id === 'cli') ?? null;
const appLaunchTarget = activeLaunchTargets.find((target) => target.id === 'app') ?? null;
const appliedModel = activeStatus?.appliedModel ?? activeStatus?.currentModel ?? '';
const modelDraftChanged = !isClaudeModelMappingClient && Boolean(
selectedModel.trim()
@@ -813,19 +772,6 @@ export function AgentsPage() {
? claudeMappingsReady
: selectedModelOption),
);
const launchEnabled = Boolean(
activeStatus?.supportedPlatform
&& activeStatus.installed
&& (selected === 'codex'
|| (activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')),
);
const canLaunchTarget = (target: AgentLaunchTarget | null) => launchEnabled && Boolean(target);
const requiresLaunchDirectory = (target: AgentLaunchTarget | null) => Boolean(
target
&& (selected === 'claude-code'
|| selected === 'pi'
|| (selected === 'codex' && target.id === 'cli')),
);
const modelHint = modelSelectionError
|| modelError
|| (modelLoading
@@ -1100,130 +1046,6 @@ export function AgentsPage() {
}
};
const openLaunchDirectoryDialog = (target: AgentLaunchTarget) => {
setLaunchDirectory(
selected === 'claude-code' ? activeStatus?.claudeCodeWorkingDirectory ?? '' : '',
);
setLaunchDirectoryTarget(target);
setSuppressLaunchDirectoryPrompt(false);
setLaunchDirectoryError('');
setLaunchDirectoryDialogOpen(true);
};
const chooseLaunchDirectory = async () => {
setBusyAction('directory');
setLaunchDirectoryError('');
try {
const selectedDirectory = await open({
directory: true,
multiple: false,
defaultPath: launchDirectory || activeStatus?.claudeCodeWorkingDirectory || undefined,
title: t('agents.claudeCodeLaunch.dialogTitle', { client: activeDefinition.name }),
});
if (typeof selectedDirectory === 'string') {
setLaunchDirectory(selectedDirectory);
}
} catch (requestError) {
setLaunchDirectoryError(String(requestError));
} finally {
setBusyAction(null);
}
};
const invokeAgentLaunch = async (
target: AgentLaunchTarget,
workingDirectory: string | null = null,
suppressWorkingDirectoryPrompt: boolean | null = null,
) => {
if (!target) return;
const launchAction = selected === 'codex'
? target.id === 'cli' ? 'launch-cli' : 'launch-app'
: 'launch';
setBusyAction(launchAction);
setLaunchError('');
try {
if (draftChanged) {
throw new Error(t('agents.error.applyFirst'));
}
await invoke('launch_agent', {
client: selected,
target: target.id,
workingDirectory,
suppressWorkingDirectoryPrompt,
});
if (requiresLaunchDirectory(target) && workingDirectory) {
setLaunchDirectoryDialogOpen(false);
setLaunchDirectoryTarget(null);
if (selected === 'claude-code') {
await reloadStatusesAfterAction();
}
}
} catch (requestError) {
const message = String(requestError);
if (
selected === 'claude-code'
&& (message.includes('CLAUDE_CODE_WORKING_DIRECTORY_REQUIRED')
|| message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID'))
) {
openLaunchDirectoryDialog(target);
setLaunchDirectoryError(
message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID') ? message : '',
);
} else if (requiresLaunchDirectory(target) && workingDirectory) {
setLaunchDirectoryError(message);
} else if (!handleOAuthLoginError(requestError, 'launch')) {
setLaunchError(message);
}
} finally {
setBusyAction(null);
}
};
const launchAgent = async (target: AgentLaunchTarget | null) => {
if (!target) return;
if (
requiresLaunchDirectory(target)
&& !(selected === 'claude-code' && activeStatus?.claudeCodeWorkingDirectoryPromptDisabled)
) {
openLaunchDirectoryDialog(target);
return;
}
await invokeAgentLaunch(target);
};
const launchFromDirectoryDialog = async () => {
const target = launchDirectoryTarget;
if (!target) return;
const workingDirectory = launchDirectory.trim();
if (!workingDirectory) {
setLaunchDirectoryError(t('agents.claudeCodeLaunch.directoryRequired'));
return;
}
setLaunchDirectoryError('');
await invokeAgentLaunch(
target,
workingDirectory,
selected === 'claude-code' ? suppressLaunchDirectoryPrompt : null,
);
};
const closeChatGptApp = async () => {
setBusyAction('close-app');
setCloseAppError('');
setCloseAppNotice('');
try {
const result = await invoke<ChatGptCloseResult>('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);
@@ -1244,16 +1066,6 @@ 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),
);
@@ -1534,7 +1346,6 @@ export function AgentsPage() {
<div className="agent-modification-control">
{selected === 'codex' ? (
<div className="agent-codex-options">
<CodexSessionAutoRestoreCard />
<label
className="agent-oauth-configuration"
title={t('agents.modify.oauthConfiguration')}
@@ -1612,78 +1423,6 @@ export function AgentsPage() {
</section>
)}
<div className="agent-config-footer">
<div className="agent-launch-control">
<div className="agent-launch-actions">
{selected === 'codex' ? (
<>
<button
type="button"
className="secondary-button agent-launch-button"
onClick={() => void launchAgent(cliLaunchTarget)}
disabled={busy || !canLaunchTarget(cliLaunchTarget) || draftChanged}
title={draftChanged
? t('agents.launch.applyFirst')
: cliLaunchTarget?.detail ?? t('agents.launch.unavailable')}
>
{busyAction === 'launch-cli' ? <LoaderCircle size={16} className="spin" /> : <Terminal size={16} />}
{busyAction === 'launch-cli' ? t('agents.launch.starting') : t('agents.launch.startCli')}
</button>
<button
type="button"
className="primary-button agent-launch-button"
onClick={() => void launchAgent(appLaunchTarget)}
disabled={busy || !canLaunchTarget(appLaunchTarget) || draftChanged}
title={draftChanged
? t('agents.launch.applyFirst')
: appLaunchTarget?.detail ?? t('agents.launch.unavailable')}
>
{busyAction === 'launch-app' ? <LoaderCircle size={16} className="spin" /> : <AppWindow size={16} />}
{busyAction === 'launch-app' ? t('agents.launch.starting') : t('agents.launch.startApp')}
</button>
</>
) : (
<button
type="button"
className="primary-button agent-launch-button"
onClick={() => void launchAgent(defaultLaunchTarget)}
disabled={busy || !canLaunchTarget(defaultLaunchTarget) || draftChanged}
title={draftChanged
? t('agents.launch.applyFirst')
: activeStatus?.modificationState === 'applied'
? defaultLaunchTarget?.detail
: t('agents.launch.enableFirst')}
>
{busyAction === 'launch'
? <LoaderCircle size={16} className="spin" />
: <Play size={16} />}
{busyAction === 'launch' ? t('agents.launch.starting') : defaultLaunchTarget ? t('agents.launch.start', { target: defaultLaunchTarget.label }) : t('agents.launch.unavailable')}
</button>
)}
{selected === 'codex' ? (
<button
type="button"
className="danger-button agent-close-app-button"
onClick={openCloseAppConfirmation}
disabled={busy}
>
{busyAction === 'close-app' ? <LoaderCircle size={16} className="spin" /> : <Power size={16} />}
{busyAction === 'close-app' ? t('agents.launch.closingChatgpt') : t('agents.launch.closeChatgpt')}
</button>
) : null}
</div>
{launchError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{launchError}
</span>
) : null}
{closeAppNotice ? (
<span className="agent-inline-message" role="status" aria-live="polite">
{closeAppNotice}
</span>
) : null}
</div>
</div>
</div>
) : null}
@@ -1700,91 +1439,6 @@ export function AgentsPage() {
</section>
</div>
{launchDirectoryDialogOpen ? (
<div className="config-dialog-backdrop" onMouseDown={(event) => {
if (event.currentTarget === event.target && !busy) {
setLaunchDirectoryDialogOpen(false);
setLaunchDirectoryTarget(null);
}
}}>
<section
className="config-dialog agent-claude-code-launch-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="agent-launch-directory-title"
>
<div className="config-dialog-heading">
<div>
<h2 id="agent-launch-directory-title">
{t('agents.claudeCodeLaunch.dialogTitle', { client: activeDefinition.name })}
</h2>
</div>
</div>
<p>{t('agents.claudeCodeLaunch.dialogDescription', { client: activeDefinition.name })}</p>
<div className="config-dialog-field">
<span>{t('agents.claudeCodeLaunch.workingDirectory')}</span>
<button
type="button"
className="agent-claude-code-directory-picker"
onClick={() => void chooseLaunchDirectory()}
disabled={busy}
autoFocus
>
<span>
<small>{launchDirectory
? t('agents.claudeCodeLaunch.selectedDirectory')
: t('agents.claudeCodeLaunch.noDirectory')}</small>
<strong title={launchDirectory || undefined}>
{launchDirectory || t('agents.claudeCodeLaunch.chooseDirectory')}
</strong>
</span>
<b>{busyAction === 'directory'
? t('agents.claudeCodeLaunch.choosing')
: t('agents.claudeCodeLaunch.browse')}</b>
</button>
</div>
{selected === 'claude-code' ? (
<label className="agent-claude-code-suppress-prompt">
<input
type="checkbox"
checked={suppressLaunchDirectoryPrompt}
onChange={(event) => setSuppressLaunchDirectoryPrompt(event.currentTarget.checked)}
disabled={busy}
/>
<span>{t('agents.claudeCodeLaunch.neverAskAgain')}</span>
</label>
) : null}
{launchDirectoryError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{launchDirectoryError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button
type="button"
className="secondary-button"
onClick={() => {
setLaunchDirectoryDialogOpen(false);
setLaunchDirectoryTarget(null);
}}
disabled={busy}
>
{t('common.cancel')}
</button>
<button
type="button"
className="primary-button"
onClick={() => void launchFromDirectoryDialog()}
disabled={busy || !launchDirectory.trim() || !launchDirectoryTarget}
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : null}
{t('agents.claudeCodeLaunch.launch', { client: activeDefinition.name })}
</button>
</div>
</section>
</div>
) : null}
{defaultConfirmOpen ? (
<div className="config-dialog-backdrop">
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-default-title">
@@ -1833,29 +1487,6 @@ export function AgentsPage() {
</div>
) : null}
{closeAppConfirmOpen ? (
<div className="config-dialog-backdrop">
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-close-app-title">
<div className="config-dialog-heading">
<div><AlertTriangle size={19} /><h2 id="agent-close-app-title">{t('agents.closeApp.title')}</h2></div>
</div>
<p>{t('agents.closeApp.description')}</p>
{closeAppError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{closeAppError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button type="button" className="secondary-button" onClick={closeCloseAppConfirmation} disabled={busy}>{t('common.cancel')}</button>
<button type="button" className="danger-button" onClick={() => void closeChatGptApp()} disabled={busy}>
{busyAction === 'close-app' ? <LoaderCircle size={16} className="spin" /> : <Power size={16} />}
{t('agents.closeApp.confirm')}
</button>
</div>
</section>
</div>
) : null}
{oauthLoginRequiredAction ? (
<div className="config-dialog-backdrop">
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-oauth-login-required-title">
-104
View File
@@ -1,104 +0,0 @@
import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { LoaderCircle } from 'lucide-react';
import { useI18n } from '../i18n';
type RepairOnLaunchSetting = {
enabled: boolean;
};
type StatusMessage = {
text: string;
};
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
export function CodexSessionAutoRestoreCard() {
const { t } = useI18n();
const [enabled, setEnabled] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [status, setStatus] = useState<StatusMessage | null>(null);
useEffect(() => {
let disposed = false;
void invoke<RepairOnLaunchSetting>('get_codex_session_repair_on_launch')
.then((result) => {
if (!disposed) setEnabled(result.enabled);
})
.catch((error) => {
if (!disposed) {
setStatus({
text: t('agents.sessions.settingLoadFailed', { error: errorMessage(error) }),
});
}
})
.finally(() => {
if (!disposed) setLoading(false);
});
return () => {
disposed = true;
};
}, [t]);
const updateSetting = async (nextEnabled: boolean) => {
setSaving(true);
setStatus(null);
try {
const result = await invoke<RepairOnLaunchSetting>('set_codex_session_repair_on_launch', {
request: { enabled: nextEnabled },
});
setEnabled(result.enabled);
} catch (error) {
setStatus({
text: t('agents.sessions.settingFailed', { error: errorMessage(error) }),
});
} finally {
setSaving(false);
}
};
return (
<section
className={`agent-core-setting-section codex-auto-restore-card ${enabled ? 'enabled' : ''}`}
aria-busy={loading || saving}
>
<div className="agent-section-heading">
<div>
<strong>{t('agents.sessions.autoRepair')}</strong>
</div>
<label className="codex-auto-restore-toggle">
{loading || saving ? <LoaderCircle size={14} className="spin" aria-hidden="true" /> : null}
<strong>
{loading
? t('agents.sessions.settingLoading')
: enabled
? t('agents.sessions.settingOn')
: t('agents.sessions.settingOff')}
</strong>
<span className="switch-control codex-auto-restore-switch">
<input
type="checkbox"
role="switch"
aria-label={t('agents.sessions.autoRepair')}
checked={enabled}
disabled={loading || saving}
onChange={(event) => void updateSetting(event.currentTarget.checked)}
/>
<span className="switch-track" />
</span>
</label>
</div>
{status ? (
<small
className="codex-auto-restore-message error"
role="alert"
>
{status.text}
</small>
) : null}
</section>
);
}
-1
View File
@@ -15,7 +15,6 @@ export type CodexSessionPage = {
offset: number;
limit: number;
hasMore: boolean;
repairOnLaunch: boolean;
warnings: string[];
};
+2 -237
View File
@@ -5361,7 +5361,6 @@ textarea:focus-visible {
padding: 0 2px;
}
.agent-config-footer,
.agent-status-grid span {
display: flex;
align-items: center;
@@ -5372,16 +5371,6 @@ textarea:focus-visible {
gap: 14px;
}
.agent-launch-actions {
display: flex;
min-width: 0;
max-width: 100%;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.agent-status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -5502,81 +5491,6 @@ textarea:focus-visible {
opacity: 0.62;
}
.agent-claude-code-launch-dialog {
grid-template-rows: auto;
width: min(620px, calc(100vw - 32px));
height: auto;
min-height: 0;
}
.agent-claude-code-launch-dialog > p {
margin: 0;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
line-height: var(--line-height-relaxed);
}
.agent-claude-code-directory-picker {
display: flex;
width: 100%;
min-width: 0;
min-height: 64px;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 11px 13px;
border: 1px solid var(--theme-d8d2c8);
border-radius: 9px;
background: var(--theme-fff);
color: var(--theme-2d2a26);
text-align: left;
transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
}
.agent-claude-code-directory-picker:hover:not(:disabled) {
border-color: var(--theme-bdb5a9);
background: var(--theme-faf8f4);
}
.agent-claude-code-directory-picker:focus-visible {
border-color: var(--theme-8d8377);
outline: 0;
box-shadow: 0 0 0 3px rgba(45, 42, 38, 0.1);
}
.agent-claude-code-directory-picker > span {
display: grid;
min-width: 0;
gap: 4px;
}
.agent-claude-code-directory-picker small {
color: var(--theme-8a847c);
font-size: var(--font-size-meta);
}
.agent-claude-code-directory-picker strong {
overflow: hidden;
font-size: var(--font-size-label);
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-claude-code-directory-picker > b {
flex: none;
color: var(--theme-8069ad);
font-size: var(--font-size-label);
}
.agent-claude-code-suppress-prompt {
display: flex;
align-items: center;
gap: 8px;
color: var(--theme-5f5a53);
font-size: var(--font-size-label);
cursor: pointer;
}
.agent-section-heading {
display: flex;
align-items: center;
@@ -5809,17 +5723,6 @@ textarea:focus-visible {
min-height: 0;
}
.codex-auto-restore-card {
height: 100%;
min-height: 54px;
gap: 6px;
padding: 10px 12px;
border-color: var(--theme-d8d2c8);
border-radius: 9px;
background: var(--theme-faf8f4);
transition: border-color 160ms ease, background-color 160ms ease, box-shadow 160ms ease;
}
.agent-codex-options {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -5841,54 +5744,6 @@ textarea:focus-visible {
line-height: var(--line-height-tight);
}
.codex-auto-restore-toggle {
display: inline-flex;
flex: none;
align-items: center;
gap: 8px;
cursor: pointer;
user-select: none;
}
.codex-auto-restore-switch input:focus-visible + .switch-track {
outline: 3px solid var(--ui-focus-ring);
outline-offset: 2px;
}
.codex-auto-restore-toggle:has(input:disabled) {
cursor: not-allowed;
opacity: 0.55;
}
.codex-auto-restore-toggle > strong {
min-width: 34px;
color: var(--theme-5f5a53);
font-size: var(--font-size-meta);
font-weight: 600;
text-align: right;
}
.codex-auto-restore-card.enabled {
border-color: var(--theme-9db8a5);
background: var(--theme-f3f8f4);
}
.codex-auto-restore-card.enabled .codex-auto-restore-toggle > strong {
color: var(--theme-2f6b3f);
}
.codex-auto-restore-message {
max-width: 100%;
color: var(--theme-2f6b3f);
font-size: var(--font-size-meta);
line-height: var(--line-height-body);
overflow-wrap: anywhere;
}
.codex-auto-restore-message.error {
color: var(--theme-9f2d20);
}
.agent-modification-control {
display: grid;
min-width: 0;
@@ -5986,44 +5841,6 @@ textarea:focus-visible {
margin-top: -6px;
}
.agent-config-footer {
min-height: 48px;
padding-top: 14px;
border-top: 1px solid var(--theme-e3e1db);
}
.agent-config-footer > .agent-launch-control {
display: grid;
min-width: 0;
max-width: 100%;
justify-items: end;
gap: 5px;
margin-left: auto;
}
.agent-launch-control > .agent-inline-message {
max-width: 360px;
text-align: right;
}
.agent-config-footer .primary-button {
min-width: 120px;
}
.agent-config-footer .agent-close-app-button {
min-width: 120px;
}
.agent-launch-actions > button {
min-width: 0;
max-width: 100%;
height: auto;
padding-block: 8px;
line-height: var(--line-height-tight);
overflow-wrap: anywhere;
white-space: normal;
}
.usage-records-page {
display: grid;
gap: 14px;
@@ -7306,8 +7123,7 @@ textarea:focus-visible {
}
.agent-status-grid,
.agent-status-messages,
.agent-config-footer {
.agent-status-messages {
grid-column: 1 / -1;
}
@@ -7324,30 +7140,6 @@ textarea:focus-visible {
}
@container agent-panel (max-width: 760px) {
.agent-config-footer {
align-items: stretch;
flex-direction: column;
}
.agent-config-footer > .agent-launch-control {
width: 100%;
justify-items: stretch;
margin-left: 0;
}
.agent-launch-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
}
.agent-launch-actions > .primary-button:last-child {
grid-column: 1 / -1;
}
.agent-launch-actions > .agent-close-app-button {
grid-column: 1 / -1;
}
}
@container agent-panel (max-width: 640px) {
@@ -7930,43 +7722,16 @@ textarea:focus-visible {
height: auto;
}
.agent-config-footer,
.agent-section-heading {
align-items: stretch;
flex-direction: column;
}
.codex-auto-restore-toggle {
width: 100%;
min-width: 0;
justify-content: space-between;
}
.codex-auto-restore-toggle > strong {
text-align: left;
}
.agent-config-footer .primary-button {
width: 100%;
}
.agent-modification-control > .agent-inline-message,
.agent-launch-control > .agent-inline-message {
.agent-modification-control > .agent-inline-message {
max-width: none;
text-align: left;
}
.agent-config-footer > .agent-launch-control {
width: 100%;
justify-items: stretch;
}
.agent-launch-actions {
width: 100%;
align-items: stretch;
flex-direction: column;
}
.oauth-card {
grid-template-rows: auto auto auto;
min-height: 0;
-14
View File
@@ -32,20 +32,6 @@ describe('i18n', () => {
expect(translate('en', 'kernel.access.openaiDescription')).toBe('OpenAI-compatible format');
});
it('translates direct Codex launch actions', () => {
expect(translate('zh-CN', 'agents.launch.startCli')).toBe('启动 CLI');
expect(translate('zh-TW', 'agents.launch.startApp')).toBe('啟動 App');
expect(translate('ja', 'agents.launch.startCli')).toBe('CLI を起動');
expect(translate('en', 'agents.launch.startApp')).toBe('Start App');
});
it('uses the concise automatic session restoration label', () => {
expect(translate('zh-CN', 'agents.sessions.autoRepair')).toBe('自动恢复历史会话');
expect(translate('zh-TW', 'agents.sessions.autoRepair')).toBe('自動恢復歷史工作階段');
expect(translate('ja', 'agents.sessions.autoRepair')).toBe('過去のセッションを自動復元');
expect(translate('en', 'agents.sessions.autoRepair')).toBe('Automatically restore historical sessions');
});
it('uses each language native name independently of the active locale', () => {
expect(languageOptions).toEqual([
{ value: 'zh-CN', nativeLabel: '简体中文' },