Merge pull request #105 from router-for-me/dev

Dev
This commit is contained in:
lzt404 authored and GitHub committed 2026-08-10 11:02:12 +08:00
commit 78165f18f8
23 files changed
+832 -2459

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"
+64 -140
View File
@@ -228,6 +228,17 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
dependencies = [
"dirs 4.0.0",
"thiserror 1.0.69",
"winreg 0.10.1",
]
[[package]]
name = "autocfg"
version = "1.5.1"
@@ -582,7 +593,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "cpa-gui"
version = "0.2.16"
version = "0.2.17"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -605,7 +616,7 @@ dependencies = [
"tar",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-autostart",
"tauri-plugin-opener",
"tokio",
"tokio-util",
@@ -804,13 +815,33 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys 0.3.7",
]
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
"dirs-sys 0.5.0",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users 0.4.6",
"winapi",
]
[[package]]
@@ -821,7 +852,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"redox_users 0.5.2",
"windows-sys 0.61.2",
]
@@ -948,7 +979,7 @@ dependencies = [
"rustc_version",
"toml 1.1.3+spec-1.1.0",
"vswhom",
"winreg",
"winreg 0.55.0",
]
[[package]]
@@ -2488,7 +2519,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -3035,6 +3065,17 @@ dependencies = [
"bitflags 2.13.0",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror 1.0.69",
]
[[package]]
name = "redox_users"
version = "0.5.2"
@@ -3170,30 +3211,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"
@@ -3910,7 +3927,7 @@ dependencies = [
"anyhow",
"bytes",
"cookie",
"dirs",
"dirs 6.0.0",
"dunce",
"embed_plist",
"getrandom 0.3.4",
@@ -3960,7 +3977,7 @@ checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
dependencies = [
"anyhow",
"cargo_toml",
"dirs",
"dirs 6.0.0",
"glob",
"heck 0.5.0",
"json-patch",
@@ -4031,45 +4048,17 @@ dependencies = [
]
[[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"
name = "tauri-plugin-autostart"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"auto-launch",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.3+spec-1.1.0",
"url",
]
[[package]]
@@ -4569,7 +4558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc"
dependencies = [
"crossbeam-channel",
"dirs",
"dirs 6.0.0",
"libappindicator",
"muda",
"objc2",
@@ -5219,15 +5208,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 +5241,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 +5278,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 +5290,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 +5302,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 +5320,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 +5332,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 +5344,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 +5356,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"
@@ -5465,6 +5380,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.55.0"
@@ -5497,7 +5421,7 @@ dependencies = [
"block2",
"cookie",
"crossbeam-channel",
"dirs",
"dirs 6.0.0",
"dom_query",
"dpi",
"dunce",
+3 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "cpa-gui"
# Single source of truth for the desktop app version.
version = "0.2.16"
version = "0.2.17"
description = "CPA desktop management shell"
authors = ["lzt404"]
edition = "2021"
@@ -17,7 +17,7 @@ futures-util = "0.3"
getrandom = "0.3"
json5 = "0.4"
notify = "7"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "socks", "stream"] }
rusqlite = { version = "0.32.1", features = ["bundled"] }
semver = "1"
serde = { version = "1", features = ["derive"] }
@@ -27,7 +27,7 @@ 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-autostart = "2"
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");
+447 -1137
View File
File diff suppressed because it is too large. Load diff
+15 -22
View File
@@ -1,6 +1,6 @@
use super::{
current_core_status, management_authorization, management_endpoint, management_http_client,
CoreProcessState, GuiConfigFile, GuiConfigState,
apply_configured_proxy, current_core_status, management_authorization, management_endpoint,
management_http_client, CoreProcessState, GuiConfigFile, GuiConfigState,
};
use chrono::{DateTime, Local};
use rusqlite::{
@@ -1595,28 +1595,23 @@ pub(crate) async fn sync_usage_model_prices(
gui_config_state: tauri::State<'_, GuiConfigState>,
) -> Result<ModelPriceSyncResult, String> {
let config = gui_config_state.snapshot()?;
let mut client_builder = reqwest::Client::builder()
let client_builder = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30));
let proxy_url = config.proxy_url.trim();
let proxy_valid = if proxy_url.is_empty() {
true
} else if let Ok(proxy) = reqwest::Proxy::all(proxy_url) {
client_builder = client_builder.proxy(proxy);
true
let client = if proxy_url.is_empty() {
client_builder.build().ok()
} else {
false
apply_configured_proxy(client_builder, proxy_url)
.ok()
.and_then(|builder| builder.build().ok())
};
let remote_content = if proxy_valid {
match client_builder.build() {
Ok(client) => match client.get(MODEL_PRICE_SYNC_URL).send().await {
Ok(response) if response.status().is_success() => response.text().await.ok(),
_ => None,
},
Err(_) => None,
}
} else {
None
let remote_content = match client {
Some(client) => match client.get(MODEL_PRICE_SYNC_URL).send().await {
Ok(response) if response.status().is_success() => response.text().await.ok(),
_ => None,
},
None => None,
};
let now = Local::now().timestamp_millis();
let (remote_prices, used_builtin) = match remote_content
@@ -2193,6 +2188,7 @@ mod tests {
allow_lan: false,
host: "127.0.0.1".to_string(),
run_on_startup: false,
silent_start: false,
close_behavior: crate::WindowsCloseBehavior::Ask,
window_width: None,
window_height: None,
@@ -2206,9 +2202,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!({
+2 -1
View File
@@ -16,7 +16,8 @@
"width": 1531,
"height": 891,
"minWidth": 640,
"minHeight": 600
"minHeight": 600,
"visible": false
}
],
"security": {
+12 -42
View File
@@ -22,7 +22,7 @@ export const jaOverrides = {
'app.nav.authFiles': '認証ファイル',
'app.nav.quota': 'クォータ照会',
'app.nav.usageRecords': '使用履歴',
'app.nav.agents': 'エージェント',
'app.nav.agents': 'エージェント設定',
'app.coreRequired.title': '先にコアを起動してください',
'app.coreRequired.description': '設定、OAuth、API 接続などを使用するには CPA コアを起動してください。',
'app.contact.title': 'QQ でお問い合わせ',
@@ -340,15 +340,19 @@ export const jaOverrides = {
'config.network.allowLanHint': 'LAN 内の他のデバイスからこのプロキシへのアクセスを許可します',
'config.network.proxyUrl': 'プロキシ URL',
'config.network.proxyPlaceholder': '例:socks5://127.0.0.1:7890',
'config.network.proxyHint': 'グローバル上流プロキシ。空欄の場合は使用しません',
'config.network.proxyHint': 'コアとアプリ本体の外部サービスに使用するグローバルプロキシ。空欄の場合は使用しません',
'config.network.sessionAffinity': 'セッションアフィニティ',
'config.network.sessionAffinityHint': '同じセッションでは以前選択した認証情報を優先します',
'config.network.sessionTtl': 'セッションアフィニティ TTL',
'config.network.sessionTtlHint': '例:1h。空欄の場合はコアのデフォルト値を使用します',
'config.notice.softwareCloseBehaviorUpdated': 'アプリの終了動作を更新しました',
'config.notice.softwareUpdated': 'ソフトウェア設定を保存しました',
'config.software.title': 'ソフトウェア設定',
'config.software.description': 'ウィンドウの閉じるボタンをクリックしたときのデフォルト動作を設定します。「毎回確認」を選ぶと確認ダイアログに戻ります。',
'config.software.autostart': 'システム起動時に開始',
'config.software.autostartDescription': 'サインイン後に EasyCLIProxyAPI を自動的に起動します',
'config.software.silentStart': 'サイレント起動',
'config.software.silentStartDescription': 'メインウィンドウを表示せず、システムトレイで直接起動します',
'config.software.closeBehavior': '終了動作',
'config.software.closeBehaviorDescription': 'ウィンドウを閉じたときにアプリを終了するか、システムトレイで実行を続けるかを選択します',
'config.software.behavior.ask': '毎回確認',
'config.software.behavior.minimize': 'トレイに最小化',
'config.software.behavior.exit': 'アプリを終了',
@@ -394,14 +398,12 @@ export const jaOverrides = {
'authFiles.runtimeDeleteError': '実行時認証ファイルはディスクから削除できません',
'authFiles.deleteConfirm': '「{name}」を削除しますか?',
'authFiles.deleted': '認証ファイルを削除しました',
'authFiles.deleteAllConfirm': 'ディスク上の認証ファイルをすべて削除しますか?実行時認証は削除されません。',
'authFiles.deletedAll': 'ディスク上の認証ファイルをすべて削除しました',
'common.copyFailed': 'コピーに失敗しました',
'authFiles.title': '認証ファイル',
'authFiles.summary': 'ファイル {files} 件 · 無効 {disabled} 件',
'common.refresh': '更新',
'authFiles.clearDisk': 'ディスクファイルを消去',
'common.upload': 'アップロード',
'authFiles.import': 'インポート',
'authFiles.searchPlaceholder': 'ファイル名、アカウント、プロバイダーを検索',
'authFiles.filter.allProviders': 'すべてのプロバイダー',
'authFiles.filter.allStatuses': 'すべての状態',
@@ -421,7 +423,7 @@ export const jaOverrides = {
'authFiles.models.settings': '公開モデルを設定',
'authFiles.models.button': 'モデル',
'authFiles.copyName': 'ファイル名をコピー',
'authFiles.openDirectory': '認証情報フォルダーを開く',
'authFiles.openDirectory': 'フォルダーを開く',
'common.enable': '有効化',
'common.disable': '無効化',
'authFiles.runtimeFootnote': '{count} 件の認証情報は実行時ストレージ由来です。対応するプロバイダーまたは認証元でのみ管理できます。',
@@ -499,11 +501,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 +532,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}',
@@ -570,33 +566,21 @@ export const jaOverrides = {
'agents.model.current': '現在適用中:{model}',
'agents.model.firstSelection': '利用可能なモデル {count} 件。初回は先頭を選択します',
'agents.modify.title': '設定変更',
'agents.modify.oauthConfiguration': 'OAuth 設定方式',
'agents.modify.oauthConfiguration': 'OAuth ログイン設定方式',
'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': 'Claude Code の作業ディレクトリを選択',
'agents.claudeCodeLaunch.dialogDescription': '環境変数を設定して Claude Code を起動する前に、作業ディレクトリを選択してください。',
'agents.claudeCodeLaunch.workingDirectory': '作業ディレクトリ',
'agents.claudeCodeLaunch.browse': '参照',
'agents.claudeCodeLaunch.neverAskAgain': '今後この確認を表示しない',
'agents.claudeCodeLaunch.directoryRequired': '先に作業ディレクトリを選択してください',
'agents.claudeCodeLaunch.launch': 'Claude Code を起動',
'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 +588,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 +606,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} 行目の形式が無効です',
+24 -84
View File
@@ -25,7 +25,7 @@ export const zhCN = {
'app.nav.authFiles': '认证文件',
'app.nav.quota': '额度查询',
'app.nav.usageRecords': '使用记录',
'app.nav.agents': '智能体',
'app.nav.agents': '智能体配置',
'app.coreRequired.title': '请先启动内核',
'app.coreRequired.description': '启动 CPA 内核后,才能使用配置、OAuth、API 接入等功能。',
'app.contact.title': '通过 QQ 联系我们',
@@ -292,10 +292,14 @@ export const zhCN = {
'config.notice.sessionAffinityEnabled': '会话粘性路由已启用',
'config.notice.sessionAffinityDisabled': '会话粘性路由已停用',
'config.notice.sessionTtlUpdated': '会话粘性 TTL 已保存',
'config.notice.softwareCloseBehaviorUpdated': '软件关闭行为已更新',
'config.notice.softwareUpdated': '软件设置已保存',
'config.software.title': '软件设置',
'config.software.description': '设置点击窗口关闭按钮时的默认行为。选择“每次询问”即可恢复关闭确认弹窗。',
'config.software.autostart': '开机自启',
'config.software.autostartDescription': '登录系统后自动启动 EasyCLIProxyAPI',
'config.software.silentStart': '静默启动',
'config.software.silentStartDescription': '启动应用时不显示主窗口,直接在系统托盘运行',
'config.software.closeBehavior': '关闭行为',
'config.software.closeBehaviorDescription': '决定关闭窗口时是退出程序还是继续在系统托盘运行',
'config.software.behavior.ask': '每次询问',
'config.software.behavior.minimize': '最小化到托盘',
'config.software.behavior.exit': '退出程序',
@@ -350,7 +354,7 @@ export const zhCN = {
'config.network.allowLanHint': '启用后将监听0.0.0.0',
'config.network.proxyUrl': '代理 URL',
'config.network.proxyPlaceholder': '例如 socks5://127.0.0.1:7890',
'config.network.proxyHint': '全局上游代理;留空表示不使用',
'config.network.proxyHint': '用于内核和软件本体访问外部服务的全局代理;留空表示不使用',
'config.network.sessionAffinity': '会话粘性路由',
'config.network.sessionAffinityHint': '同一会话优先复用已选择的凭据',
'config.network.sessionTtl': '会话粘性 TTL',
@@ -397,14 +401,12 @@ export const zhCN = {
'authFiles.runtimeDeleteError': '运行时认证文件不能从磁盘删除',
'authFiles.deleteConfirm': '确定删除“{name}”吗?',
'authFiles.deleted': '认证文件已删除',
'authFiles.deleteAllConfirm': '确定删除所有磁盘认证文件吗?运行时认证不会被删除。',
'authFiles.deletedAll': '磁盘认证文件已全部删除',
'common.copyFailed': '复制失败',
'authFiles.title': '认证文件',
'authFiles.summary': '{files} 个文件 · {disabled} 个停用',
'common.refresh': '刷新',
'authFiles.clearDisk': '清空磁盘文件',
'common.upload': '上传',
'authFiles.import': '导入',
'authFiles.searchPlaceholder': '搜索文件名、账号或提供商',
'authFiles.filter.allProviders': '全部提供商',
'authFiles.filter.allStatuses': '全部状态',
@@ -424,7 +426,7 @@ export const zhCN = {
'authFiles.models.settings': '设置开放模型',
'authFiles.models.button': '模型',
'authFiles.copyName': '复制文件名',
'authFiles.openDirectory': '打开凭证所在文件夹',
'authFiles.openDirectory': '打开文件夹',
'common.enable': '启用',
'common.disable': '停用',
'authFiles.runtimeFootnote': '{count} 个凭据来自运行时存储,只能在对应提供商或认证源中管理。',
@@ -502,11 +504,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 +535,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,34 +568,22 @@ 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': '选择 Claude Code 工作目录',
'agents.claudeCodeLaunch.dialogDescription': '需要先选择工作目录,然后才能设置环境变量并启动 Claude Code。',
'agents.claudeCodeLaunch.workingDirectory': '工作目录',
'agents.claudeCodeLaunch.browse': '浏览',
'agents.claudeCodeLaunch.neverAskAgain': '永久关闭此提示',
'agents.claudeCodeLaunch.directoryRequired': '请先选择工作目录',
'agents.claudeCodeLaunch.launch': '启动 Claude Code',
'agents.model.readingAvailable': '正在读取可用模型',
'agents.model.current': '当前已应用:{model}',
'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项',
'agents.modify.title': '配置修改',
'agents.modify.oauthConfiguration': 'Oauth配置方式',
'agents.modify.oauthConfiguration': 'OAuth登录配置方式',
'agents.oauthLoginRequired.title': '需要登录 Codex',
'agents.oauthLoginRequired.enableDescription': '未检测到有效的 Codex 登录凭据,无法开启 OAuth 配置模式。请先在 Codex 应用中登录 ChatGPT 账号,如果 Codex 应用无法触发登录,请先使用',
'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 +591,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 +609,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} 行格式无效',
@@ -986,7 +956,7 @@ export const en: Record<MessageKey, string> = {
'app.nav.authFiles': 'Auth Files',
'app.nav.quota': 'Quota Lookup',
'app.nav.usageRecords': 'Usage',
'app.nav.agents': 'Agents',
'app.nav.agents': 'Agent Configuration',
'app.coreRequired.title': 'Start the core first',
'app.coreRequired.description': 'Start the CPA core to use settings, OAuth, API access, and other features.',
'app.contact.title': 'Contact us on QQ',
@@ -1253,10 +1223,14 @@ export const en: Record<MessageKey, string> = {
'config.notice.sessionAffinityEnabled': 'Session affinity enabled',
'config.notice.sessionAffinityDisabled': 'Session affinity disabled',
'config.notice.sessionTtlUpdated': 'Session affinity TTL saved',
'config.notice.softwareCloseBehaviorUpdated': 'App close behavior updated',
'config.notice.softwareUpdated': 'Software settings saved',
'config.software.title': 'Software Settings',
'config.software.description': 'Choose what happens by default when you click the window close button. Select “Ask every time” to restore the confirmation dialog.',
'config.software.autostart': 'Launch at startup',
'config.software.autostartDescription': 'Automatically launch EasyCLIProxyAPI when you sign in',
'config.software.silentStart': 'Silent Launch',
'config.software.silentStartDescription': 'Start without showing the main window and run directly in the system tray',
'config.software.closeBehavior': 'Close behavior',
'config.software.closeBehaviorDescription': 'Choose whether closing the window exits the app or keeps it running in the system tray',
'config.software.behavior.ask': 'Ask every time',
'config.software.behavior.minimize': 'Minimize to Tray',
'config.software.behavior.exit': 'Exit App',
@@ -1311,7 +1285,7 @@ export const en: Record<MessageKey, string> = {
'config.network.allowLanHint': 'Allow other devices on the LAN to access this proxy',
'config.network.proxyUrl': 'Proxy URL',
'config.network.proxyPlaceholder': 'For example, socks5://127.0.0.1:7890',
'config.network.proxyHint': 'Global upstream proxy; leave blank to disable',
'config.network.proxyHint': 'Global proxy for the core and app external services; leave blank to disable',
'config.network.sessionAffinity': 'Session Affinity',
'config.network.sessionAffinityHint': 'Prefer the previously selected credential for the same session',
'config.network.sessionTtl': 'Session Affinity TTL',
@@ -1358,14 +1332,12 @@ export const en: Record<MessageKey, string> = {
'authFiles.runtimeDeleteError': 'Runtime authentication files cannot be deleted from disk',
'authFiles.deleteConfirm': 'Delete “{name}”?',
'authFiles.deleted': 'Authentication file deleted',
'authFiles.deleteAllConfirm': 'Delete all on-disk authentication files? Runtime credentials will not be deleted.',
'authFiles.deletedAll': 'All on-disk authentication files deleted',
'common.copyFailed': 'Copy failed',
'authFiles.title': 'Authentication Files',
'authFiles.summary': '{files} files · {disabled} disabled',
'common.refresh': 'Refresh',
'authFiles.clearDisk': 'Clear Disk Files',
'common.upload': 'Upload',
'authFiles.import': 'Import',
'authFiles.searchPlaceholder': 'Search by file name, account, or provider',
'authFiles.filter.allProviders': 'All Providers',
'authFiles.filter.allStatuses': 'All Statuses',
@@ -1385,7 +1357,7 @@ export const en: Record<MessageKey, string> = {
'authFiles.models.settings': 'Configure open models',
'authFiles.models.button': 'Models',
'authFiles.copyName': 'Copy file name',
'authFiles.openDirectory': 'Open credential folder',
'authFiles.openDirectory': 'Open Folder',
'common.enable': 'Enable',
'common.disable': 'Disable',
'authFiles.runtimeFootnote': '{count} credentials come from runtime storage and can only be managed through their provider or authentication source.',
@@ -1463,11 +1435,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 +1466,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,34 +1499,22 @@ 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 Claude Code Working Directory',
'agents.claudeCodeLaunch.dialogDescription': 'Select a working directory before setting the environment variables and starting Claude Code.',
'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 Claude Code',
'agents.model.readingAvailable': 'Loading available models',
'agents.model.current': 'Currently applied: {model}',
'agents.model.firstSelection': '{count} models available; the first is selected by default',
'agents.modify.title': 'Configuration Changes',
'agents.modify.oauthConfiguration': 'OAuth configuration mode',
'agents.modify.oauthConfiguration': 'OAuth sign-in configuration mode',
'agents.oauthLoginRequired.title': 'Codex sign-in required',
'agents.oauthLoginRequired.enableDescription': 'No valid Codex sign-in credentials were found, so OAuth configuration mode cannot be enabled. Sign in to your ChatGPT account in the Codex app. If the Codex app cannot trigger sign-in, use',
'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 +1522,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 +1540,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',
+4
View File
@@ -1,4 +1,8 @@
const phraseReplacements: ReadonlyArray<readonly [string, string]> = [
['登录系统后自动启动', '登入系統後自動啟動'],
['系统托盘', '系統匣'],
['开机自启', '開機自啟'],
['行为', '行為'],
['兼容格式', '相容格式'],
['历史', '歷史'],
['高级设置', '進階設定'],
+2 -348
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,29 +534,21 @@ export function AgentsPage() {
createClaudeModelMappings(''),
);
const [claudeCustomMapping, setClaudeCustomMapping] = useState(false);
const [claudeCodeLaunchDialogOpen, setClaudeCodeLaunchDialogOpen] = useState(false);
const [claudeCodeLaunchDirectory, setClaudeCodeLaunchDirectory] = useState('');
const [claudeCodeSuppressDirectoryPrompt, setClaudeCodeSuppressDirectoryPrompt] = useState(false);
const [claudeCodeDirectoryError, setClaudeCodeDirectoryError] = 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);
@@ -672,19 +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);
setClaudeCodeLaunchDialogOpen(false);
setClaudeCodeDirectoryError('');
claudeModelMappingsDirtyRef.current = false;
}, [selected]);
@@ -766,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()
@@ -811,13 +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 modelHint = modelSelectionError
|| modelError
|| (modelLoading
@@ -1092,121 +1046,6 @@ export function AgentsPage() {
}
};
const openClaudeCodeLaunchDialog = () => {
setClaudeCodeLaunchDirectory(activeStatus?.claudeCodeWorkingDirectory ?? '');
setClaudeCodeSuppressDirectoryPrompt(false);
setClaudeCodeDirectoryError('');
setClaudeCodeLaunchDialogOpen(true);
};
const chooseClaudeCodeWorkingDirectory = async () => {
setBusyAction('directory');
setClaudeCodeDirectoryError('');
try {
const selectedDirectory = await open({
directory: true,
multiple: false,
defaultPath: claudeCodeLaunchDirectory || activeStatus?.claudeCodeWorkingDirectory || undefined,
title: t('agents.claudeCodeLaunch.dialogTitle'),
});
if (typeof selectedDirectory === 'string') {
setClaudeCodeLaunchDirectory(selectedDirectory);
}
} catch (requestError) {
setClaudeCodeDirectoryError(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 (selected === 'claude-code' && workingDirectory) {
setClaudeCodeLaunchDialogOpen(false);
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'))
) {
openClaudeCodeLaunchDialog();
setClaudeCodeDirectoryError(
message.includes('CLAUDE_CODE_WORKING_DIRECTORY_INVALID') ? message : '',
);
} else if (selected === 'claude-code' && workingDirectory) {
setClaudeCodeDirectoryError(message);
} else if (!handleOAuthLoginError(requestError, 'launch')) {
setLaunchError(message);
}
} finally {
setBusyAction(null);
}
};
const launchAgent = async (target: AgentLaunchTarget | null) => {
if (!target) return;
if (selected === 'claude-code' && !activeStatus?.claudeCodeWorkingDirectoryPromptDisabled) {
openClaudeCodeLaunchDialog();
return;
}
await invokeAgentLaunch(target);
};
const launchClaudeCodeFromDialog = async () => {
const target = defaultLaunchTarget;
if (!target) return;
const workingDirectory = claudeCodeLaunchDirectory.trim();
if (!workingDirectory) {
setClaudeCodeDirectoryError(t('agents.claudeCodeLaunch.directoryRequired'));
return;
}
setClaudeCodeDirectoryError('');
await invokeAgentLaunch(
target,
workingDirectory,
claudeCodeSuppressDirectoryPrompt,
);
};
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);
@@ -1227,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),
);
@@ -1517,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')}
@@ -1595,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}
@@ -1683,85 +1439,6 @@ export function AgentsPage() {
</section>
</div>
{claudeCodeLaunchDialogOpen ? (
<div className="config-dialog-backdrop" onMouseDown={(event) => {
if (event.currentTarget === event.target && !busy) {
setClaudeCodeLaunchDialogOpen(false);
}
}}>
<section
className="config-dialog agent-claude-code-launch-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="agent-claude-code-launch-title"
>
<div className="config-dialog-heading">
<div>
<h2 id="agent-claude-code-launch-title">
{t('agents.claudeCodeLaunch.dialogTitle')}
</h2>
</div>
</div>
<p>{t('agents.claudeCodeLaunch.dialogDescription')}</p>
<div className="config-dialog-field">
<span>{t('agents.claudeCodeLaunch.workingDirectory')}</span>
<button
type="button"
className="agent-claude-code-directory-picker"
onClick={() => void chooseClaudeCodeWorkingDirectory()}
disabled={busy}
autoFocus
>
<span>
<small>{claudeCodeLaunchDirectory
? t('agents.claudeCodeLaunch.selectedDirectory')
: t('agents.claudeCodeLaunch.noDirectory')}</small>
<strong title={claudeCodeLaunchDirectory || undefined}>
{claudeCodeLaunchDirectory || t('agents.claudeCodeLaunch.chooseDirectory')}
</strong>
</span>
<b>{busyAction === 'directory'
? t('agents.claudeCodeLaunch.choosing')
: t('agents.claudeCodeLaunch.browse')}</b>
</button>
</div>
<label className="agent-claude-code-suppress-prompt">
<input
type="checkbox"
checked={claudeCodeSuppressDirectoryPrompt}
onChange={(event) => setClaudeCodeSuppressDirectoryPrompt(event.currentTarget.checked)}
disabled={busy}
/>
<span>{t('agents.claudeCodeLaunch.neverAskAgain')}</span>
</label>
{claudeCodeDirectoryError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{claudeCodeDirectoryError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button
type="button"
className="secondary-button"
onClick={() => setClaudeCodeLaunchDialogOpen(false)}
disabled={busy}
>
{t('common.cancel')}
</button>
<button
type="button"
className="primary-button"
onClick={() => void launchClaudeCodeFromDialog()}
disabled={busy || !claudeCodeLaunchDirectory.trim()}
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : null}
{t('agents.claudeCodeLaunch.launch')}
</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">
@@ -1810,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">
+4 -21
View File
@@ -4,12 +4,12 @@ import {
Copy,
FileDown,
FolderOpen,
Import,
LoaderCircle,
Pencil,
RefreshCw,
Search,
Trash2,
Upload,
X,
} from 'lucide-react';
import antigravityIcon from '../assets/icons/antigravity.svg';
@@ -437,21 +437,6 @@ export function AuthFileManagementPage() {
}
};
const deleteAll = async () => {
if (!window.confirm(t('authFiles.deleteAllConfirm'))) return;
setBusy(true);
setError('');
try {
await managementApi.delete('/auth-files', { query: { all: true } });
setNotice(t('authFiles.deletedAll'));
await loadFiles();
} catch (requestError) {
setError(String(requestError));
} finally {
setBusy(false);
}
};
const openAuthFilesDirectory = async () => {
setBusy(true);
setError('');
@@ -476,7 +461,6 @@ export function AuthFileManagementPage() {
const disabledCount = files.filter((file) => readBoolean(file, 'disabled')).length;
const runtimeCount = files.filter(isRuntimeOnly).length;
const diskCount = files.length - runtimeCount;
return (
<section className="page management-page auth-files-page">
@@ -490,11 +474,11 @@ export function AuthFileManagementPage() {
<button type="button" className="secondary-button compact-button" onClick={() => void loadFiles()} disabled={loading || busy}>
<RefreshCw size={16} />{t('common.refresh')}
</button>
<button type="button" className="secondary-button compact-button" onClick={deleteAll} disabled={loading || busy || diskCount === 0}>
<Trash2 size={16} />{t('authFiles.clearDisk')}
<button type="button" className="secondary-button compact-button" onClick={() => void openAuthFilesDirectory()} disabled={busy}>
<FolderOpen size={16} />{t('authFiles.openDirectory')}
</button>
<button type="button" className="primary-button compact-button" onClick={() => fileInputRef.current?.click()} disabled={busy}>
<Upload size={16} />{t('common.upload')}
<Import size={16} />{t('authFiles.import')}
</button>
<input ref={fileInputRef} type="file" accept=".json,application/json" multiple hidden onChange={(event) => void handleUpload(event)} />
</div>
@@ -547,7 +531,6 @@ export function AuthFileManagementPage() {
{providerKey(file) ? <button type="button" className="secondary-button compact-button" onClick={() => void openOauthModels(file)} disabled={busy} title={t('authFiles.models.settings')}>{t('authFiles.models.button')}</button> : null}
<button type="button" className="secondary-button compact-button auth-file-priority-button" onClick={() => openPriorityEditor(file)} disabled={busy} title={t('authFiles.priority.hint')}><Pencil size={14} />{t('authFiles.priority.button', { priority })}</button>
<button type="button" className="icon-button quiet" onClick={() => void copyName(name)} disabled={busy} title={t('authFiles.copyName')}>{copied === name ? <Check size={16} /> : <Copy size={16} />}</button>
<button type="button" className="icon-button quiet" onClick={() => void openAuthFilesDirectory()} disabled={busy} title={t('authFiles.openDirectory')}><FolderOpen size={16} /></button>
<button type="button" className="secondary-button compact-button" onClick={() => void toggleStatus(file)} disabled={busy}>{disabled ? t('common.enable') : t('common.disable')}</button>
<button type="button" className="icon-button danger" onClick={() => void deleteFile(file)} disabled={busy || isRuntimeOnly(file)} title={t('common.delete')}><Trash2 size={16} /></button>
</div>
-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>
);
}
+116 -30
View File
@@ -59,8 +59,10 @@ type DraftRefreshMode = 'replace' | 'preserve';
type NetworkDraftDirty = Record<NetworkDraftField, boolean>;
type GuiSettings = {
type SoftwareSettings = {
closeBehavior: CloseBehavior;
autostartEnabled: boolean;
silentStartEnabled: boolean;
};
const cleanNetworkDraft = (): NetworkDraftDirty => ({
@@ -80,9 +82,11 @@ export function ConfigPanelPage() {
const { t } = useI18n();
const { status: coreStatus, publishStatus, refreshStatus } = useCoreRuntime();
const [settings, setSettings] = useState<CoreConfigSettings | null>(null);
const [softwareSettings, setSoftwareSettings] = useState<GuiSettings | null>(null);
const [softwareSettings, setSoftwareSettings] = useState<SoftwareSettings | null>(null);
const [softwareSettingsLoading, setSoftwareSettingsLoading] = useState(true);
const [softwareCloseBehaviorDraft, setSoftwareCloseBehaviorDraft] = useState<CloseBehavior>('ask');
const [softwareAutostartDraft, setSoftwareAutostartDraft] = useState(false);
const [softwareSilentStartDraft, setSoftwareSilentStartDraft] = useState(false);
const [softwareSavedStatusVisible, setSoftwareSavedStatusVisible] = useState(false);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState('');
@@ -196,9 +200,11 @@ export function ConfigPanelPage() {
setSoftwareSettingsLoading(true);
setSoftwareSavedStatusVisible(false);
try {
const result = await invoke<GuiSettings>('get_gui_settings');
const result = await invoke<SoftwareSettings>('get_software_settings');
setSoftwareSettings(result);
setSoftwareCloseBehaviorDraft(result.closeBehavior);
setSoftwareAutostartDraft(result.autostartEnabled);
setSoftwareSilentStartDraft(result.silentStartEnabled);
} catch (error) {
setSoftwareSettings(null);
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
@@ -461,23 +467,36 @@ export function ConfigPanelPage() {
}
};
const saveSoftwareCloseBehavior = async () => {
const saveSoftwareSettings = async () => {
if (!softwareSettings || busyAction !== null) return;
if (softwareCloseBehaviorDraft === softwareSettings.closeBehavior) return;
if (
softwareCloseBehaviorDraft === softwareSettings.closeBehavior
&& softwareAutostartDraft === softwareSettings.autostartEnabled
&& softwareSilentStartDraft === softwareSettings.silentStartEnabled
) return;
setBusyAction('software');
try {
const result = await invoke<GuiSettings>('save_app_close_behavior', {
behavior: softwareCloseBehaviorDraft,
const result = await invoke<SoftwareSettings>('save_software_settings', {
settings: {
closeBehavior: softwareCloseBehaviorDraft,
autostartEnabled: softwareAutostartDraft,
silentStartEnabled: softwareSilentStartDraft,
},
});
setSoftwareSettings(result);
setSoftwareCloseBehaviorDraft(result.closeBehavior);
setSoftwareAutostartDraft(result.autostartEnabled);
setSoftwareSilentStartDraft(result.silentStartEnabled);
setSoftwareSavedStatusVisible(true);
showNotice(t('config.notice.softwareCloseBehaviorUpdated'), 'success');
showNotice(t('config.notice.softwareUpdated'), 'success');
} catch (error) {
setSoftwareCloseBehaviorDraft(softwareSettings.closeBehavior);
setSoftwareAutostartDraft(softwareSettings.autostartEnabled);
setSoftwareSilentStartDraft(softwareSettings.silentStartEnabled);
setSoftwareSavedStatusVisible(false);
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
void loadSoftwareSettings();
} finally {
setBusyAction(null);
}
@@ -504,18 +523,25 @@ export function ConfigPanelPage() {
: '';
const softwareCloseBehaviorDirty = softwareSettings !== null
&& softwareCloseBehaviorDraft !== softwareSettings.closeBehavior;
const softwareAutostartDirty = softwareSettings !== null
&& softwareAutostartDraft !== softwareSettings.autostartEnabled;
const softwareSilentStartDirty = softwareSettings !== null
&& softwareSilentStartDraft !== softwareSettings.silentStartEnabled;
const softwareSettingsDirty = softwareCloseBehaviorDirty
|| softwareAutostartDirty
|| softwareSilentStartDirty;
const softwareStatusLabel = softwareSettingsLoading
? t('common.loading')
: softwareSettings === null
? t('common.unavailable')
: softwareCloseBehaviorDirty
: softwareSettingsDirty
? t('config.network.unsaved')
: softwareSavedStatusVisible
? t('config.network.saved')
: '';
const softwareStatusIsSaved = !softwareSettingsLoading
&& softwareSettings !== null
&& !softwareCloseBehaviorDirty
&& !softwareSettingsDirty
&& softwareSavedStatusVisible;
const networkStatusIsSaved = !loading
&& settings !== null
@@ -1033,8 +1059,8 @@ export function ConfigPanelPage() {
<button
type="button"
className="primary-button compact-button"
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null || !softwareCloseBehaviorDirty}
onClick={() => void saveSoftwareCloseBehavior()}
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null || !softwareSettingsDirty}
onClick={() => void saveSoftwareSettings()}
>
<Check size={16} aria-hidden="true" />
{busyAction === 'software' ? t('common.saving') : t('config.network.confirmSave')}
@@ -1042,25 +1068,85 @@ export function ConfigPanelPage() {
</div>
</div>
<div className="config-software-content">
<div className="config-software-description">
<p>{t('config.software.description')}</p>
<div className="config-software-settings-list">
<div className="config-software-setting-row">
<div className="config-software-setting-copy">
<span className="config-software-setting-icon" aria-hidden="true">
<Clock3 size={18} />
</span>
<div>
<strong>{t('config.software.autostart')}</strong>
<small>{t('config.software.autostartDescription')}</small>
</div>
</div>
<label className="switch-control" title={t('config.software.autostart')}>
<input
type="checkbox"
role="switch"
aria-label={t('config.software.autostart')}
checked={softwareAutostartDraft}
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null}
onChange={(event) => {
setSoftwareSavedStatusVisible(false);
setSoftwareAutostartDraft(event.currentTarget.checked);
}}
/>
<span className="switch-track" />
</label>
</div>
<div className="config-software-setting-row">
<div className="config-software-setting-copy">
<span className="config-software-setting-icon" aria-hidden="true">
<EyeOff size={18} />
</span>
<div>
<strong>{t('config.software.silentStart')}</strong>
<small>{t('config.software.silentStartDescription')}</small>
</div>
</div>
<label className="switch-control" title={t('config.software.silentStart')}>
<input
type="checkbox"
role="switch"
aria-label={t('config.software.silentStart')}
checked={softwareSilentStartDraft}
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null}
onChange={(event) => {
setSoftwareSavedStatusVisible(false);
setSoftwareSilentStartDraft(event.currentTarget.checked);
}}
/>
<span className="switch-track" />
</label>
</div>
<div className="config-software-setting-row config-software-close-row">
<div className="config-software-setting-copy">
<span className="config-software-setting-icon" aria-hidden="true">
<X size={18} />
</span>
<div>
<strong>{t('config.software.closeBehavior')}</strong>
<small>{t('config.software.closeBehaviorDescription')}</small>
</div>
</div>
<label className="config-software-select">
<span className="sr-only">{t('config.software.closeBehavior')}</span>
<select
className="config-network-input"
value={softwareCloseBehaviorDraft}
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null}
onChange={(event) => {
setSoftwareSavedStatusVisible(false);
setSoftwareCloseBehaviorDraft(event.currentTarget.value as CloseBehavior);
}}
>
<option value="ask">{t('config.software.behavior.ask')}</option>
<option value="minimize-to-tray">{t('config.software.behavior.minimize')}</option>
<option value="exit">{t('config.software.behavior.exit')}</option>
</select>
</label>
</div>
</div>
<label className="config-software-field">
<span>{t('config.software.closeBehavior')}</span>
<select
className="config-network-input"
value={softwareCloseBehaviorDraft}
disabled={softwareSettingsLoading || softwareSettings === null || busyAction !== null}
onChange={(event) => {
setSoftwareSavedStatusVisible(false);
setSoftwareCloseBehaviorDraft(event.currentTarget.value as CloseBehavior);
}}
>
<option value="ask">{t('config.software.behavior.ask')}</option>
<option value="minimize-to-tray">{t('config.software.behavior.minimize')}</option>
<option value="exit">{t('config.software.behavior.exit')}</option>
</select>
</label>
</div>
</section>
</div>
+22 -12
View File
@@ -116,7 +116,8 @@ export const defaultModelAlias = (
) => {
const normalizedModel = model?.trim() ?? '';
const normalizedEffort = effort.trim().toLowerCase();
if (!normalizedModel || (!normalizedEffort && !fast)) return '';
if (!normalizedModel) return '';
if (!normalizedEffort && !fast) return `${normalizedModel}-alias`;
return `${normalizedModel}${normalizedEffort ? `-${normalizedEffort}` : ''}${fast ? '-fast' : ''}`;
};
@@ -273,10 +274,10 @@ export function ThinkingAliasesPage() {
setSelectedSourceId(source.id);
if (!source.supportsReasoning) {
setEffort('');
setFastEnabled(source.supportsFast);
setFastEnabled(source.supportsFast && (fastEnabled || !source.supportsReasoning));
} else {
setEffort('');
setFastEnabled(false);
setFastEnabled(source.supportsFast && fastEnabled);
}
setCustomEffortOpen(false);
setAlias('');
@@ -343,10 +344,6 @@ export function ThinkingAliasesPage() {
setError(t('aliases.error.emptyAlias'));
return;
}
if (!normalizedEffort && !fastEnabled) {
setError(t('aliases.error.emptyOptions'));
return;
}
if (fastEnabled && !selectedSource.supportsFast) {
setError(t('aliases.error.unsupportedFast'));
return;
@@ -371,12 +368,23 @@ export function ThinkingAliasesPage() {
alias: normalizedAlias,
effort: normalizedEffort,
}));
} else {
} else if (fastEnabled) {
await invoke<SpeedAliasEntry[]>('create_speed_alias', {
sourceId: selectedSource.id,
alias: normalizedAlias,
});
setNotice(t('speedAliases.created', { alias: normalizedAlias }));
} else {
await invoke<ThinkingAliasEntry[]>('create_thinking_alias', {
sourceId: selectedSource.id,
alias: normalizedAlias,
effort: '',
fast: false,
});
setNotice(t('aliases.created', {
alias: normalizedAlias,
effort: t('aliases.effort.none'),
}));
}
setAlias('');
await load();
@@ -397,7 +405,7 @@ export function ThinkingAliasesPage() {
setError('');
setNotice('');
try {
if (entry.effort) {
if (entry.effort || !entry.serviceTier) {
await invoke<ThinkingAliasEntry[]>('delete_thinking_alias', {
alias: entry.alias,
});
@@ -629,7 +637,7 @@ export function ThinkingAliasesPage() {
type="checkbox"
checked={fastEnabled}
onChange={(event) => setFastEnabled(event.currentTarget.checked)}
disabled={Boolean(busyAlias) || !selectedSource || !selectedSource.supportsFast}
disabled={Boolean(busyAlias) || Boolean(selectedSource && !selectedSource.supportsFast)}
aria-label={t('aliases.fast.title')}
/>
<span className="switch-track" />
@@ -651,7 +659,9 @@ export function ThinkingAliasesPage() {
placeholder={selectedSource
? normalizedEffort
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort })
: t('speedAliases.aliasName.example', { model: selectedSource.model })
: fastEnabled
? t('speedAliases.aliasName.example', { model: selectedSource.model })
: t('aliases.aliasName.example', { model: selectedSource.model, effort: 'alias' })
: t('aliases.aliasName.selectFirst')}
disabled={Boolean(busyAlias)}
/>
@@ -674,7 +684,7 @@ export function ThinkingAliasesPage() {
type="button"
className="primary-button thinking-alias-create"
onClick={() => void createAlias()}
disabled={loading || Boolean(busyAlias) || !selectedSource || (!normalizedEffort && !fastEnabled) || !alias.trim()}
disabled={loading || Boolean(busyAlias)}
>
{busyAction === 'create'
? <LoaderCircle size={16} className="spin" />
-1
View File
@@ -15,7 +15,6 @@ export type CodexSessionPage = {
offset: number;
limit: number;
hasMore: boolean;
repairOnLaunch: boolean;
warnings: string[];
};
+105 -265
View File
@@ -2449,45 +2449,87 @@ textarea:focus-visible {
gap: 18px;
}
.config-software-panel > .config-panel-heading {
width: min(960px, 100%);
margin: 0 auto;
}
.config-software-content {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 360px);
align-items: stretch;
gap: 16px;
width: min(960px, 100%);
margin: 0 auto;
}
.config-software-description,
.config-software-field {
min-width: 0;
min-height: 112px;
padding: 16px;
.config-software-setting-icon {
display: inline-flex;
flex: none;
align-items: center;
justify-content: center;
border: 1px solid var(--theme-e3e1db);
border-radius: 8px;
background: var(--theme-faf8f2);
background: var(--theme-fffdf8);
color: var(--theme-777067);
}
.config-software-description {
display: flex;
align-items: center;
.config-software-setting-copy > div {
display: grid;
min-width: 0;
gap: 4px;
}
.config-software-description p {
margin: 0;
color: var(--theme-6d6760);
.config-software-setting-copy strong {
color: var(--theme-39352f);
font-size: var(--font-size-body);
line-height: var(--line-height-body);
}
.config-software-field {
display: grid;
align-content: center;
gap: 8px;
color: var(--theme-39352f);
font-size: var(--font-size-body);
font-weight: 700;
.config-software-settings-list {
overflow: hidden;
border: 1px solid var(--theme-d5d2cb);
border-radius: 10px;
background: var(--theme-fffdf8);
}
.config-software-field .config-network-input {
.config-software-setting-row {
display: flex;
min-height: 92px;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 17px 20px;
}
.config-software-setting-row + .config-software-setting-row {
border-top: 1px solid var(--theme-e3e1db);
}
.config-software-setting-copy {
display: flex;
min-width: 0;
align-items: center;
gap: 14px;
}
.config-software-setting-icon {
width: 40px;
height: 40px;
background: var(--theme-faf8f2);
}
.config-software-setting-copy small {
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
font-weight: 400;
line-height: var(--line-height-body);
}
.config-software-select {
display: grid;
width: 228px;
flex: none;
}
.config-software-select .config-network-input {
height: 42px;
}
@@ -2506,13 +2548,43 @@ textarea:focus-visible {
}
@media (max-width: 700px) {
.config-software-content {
grid-template-columns: minmax(0, 1fr);
.config-software-panel .config-panel-heading {
align-items: flex-start;
}
.config-software-description,
.config-software-field {
min-height: 0;
.config-software-setting-row {
min-height: 84px;
gap: 16px;
padding: 15px 16px;
}
.config-software-close-row {
align-items: flex-start;
flex-direction: column;
}
.config-software-select {
width: 100%;
padding-left: 54px;
}
}
@media (max-width: 480px) {
.config-software-setting-row {
padding: 14px;
}
.config-software-setting-copy {
gap: 11px;
}
.config-software-setting-icon {
width: 36px;
height: 36px;
}
.config-software-select {
padding-left: 47px;
}
}
@@ -5361,7 +5433,6 @@ textarea:focus-visible {
padding: 0 2px;
}
.agent-config-footer,
.agent-status-grid span {
display: flex;
align-items: center;
@@ -5372,16 +5443,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 +5563,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 +5795,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 +5816,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 +5913,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;
@@ -7266,6 +7155,9 @@ textarea:focus-visible {
}
.app-update-dialog {
grid-template-rows: none;
height: auto;
min-height: 0;
gap: 16px;
}
@@ -7306,8 +7198,7 @@ textarea:focus-visible {
}
.agent-status-grid,
.agent-status-messages,
.agent-config-footer {
.agent-status-messages {
grid-column: 1 / -1;
}
@@ -7324,30 +7215,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 +7797,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: '简体中文' },
+1 -1
View File
@@ -12,7 +12,7 @@ describe('模型别名默认名称', () => {
expect(defaultModelAlias('gpt-5.6-sol', 'XHigh', false)).toBe('gpt-5.6-sol-xhigh');
expect(defaultModelAlias('gpt-5.6-sol', '', true)).toBe('gpt-5.6-sol-fast');
expect(defaultModelAlias('gpt-5.6-sol', 'xhigh', true)).toBe('gpt-5.6-sol-xhigh-fast');
expect(defaultModelAlias('gpt-5.6-sol', '', false)).toBe('');
expect(defaultModelAlias('gpt-5.6-sol', '', false)).toBe('gpt-5.6-sol-alias');
});
test('默认别名与现有模型重名时递增数字后缀', () => {