Files
EasyCLIProxyAPI/src-tauri/src/main.rs
T
lzt404 b90445143c feat(claude-code): align model mapping UI with Claude Code settings
- Add per-role model rows with inline 1M context controls
- Add bulk action to apply Sonnet settings to all roles
- Persist startup model and context settings in official settings.json fields
- Preserve custom role mappings and Subagent configuration
- Add multilingual labels and UI regression coverage
- Validate behavior with real Claude Code 2.1.292
2026-10-08 18:36:58 +08:00

2847 lines
91 KiB
Rust

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod agents;
mod app_settings;
mod app_update;
mod claude_catalog;
mod codex_catalog;
mod codex_sessions;
mod configuration_watcher;
mod core_config;
mod core_runtime;
mod desktop_theme;
mod instance_lock;
mod management_api;
mod network_proxy;
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "windows"
))]
mod native_i18n;
mod oauth_browser;
mod plugins;
mod progress;
mod provider_health;
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "windows"
))]
mod tray;
mod usage;
#[cfg(test)]
use configuration_watcher::nearest_existing_watch_directory;
use management_api::{
format_management_request_error, management_authorization, management_endpoint,
management_http_client, read_management_text, read_management_value,
};
use oauth_browser::*;
#[cfg(test)]
use provider_health::{
provider_health_content_type_is_streaming, provider_health_stream_has_terminal_success,
provider_health_stream_has_text,
};
use agents::*;
use app_settings::*;
use app_update::*;
use core_config::*;
use core_runtime::*;
use flate2::read::GzDecoder;
use futures_util::StreamExt;
use instance_lock::*;
#[cfg(target_os = "macos")]
use objc2::MainThreadMarker;
#[cfg(target_os = "macos")]
use objc2_app_kit::NSEvent;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "macos")]
use std::sync::Arc;
use std::{
collections::{HashMap, HashSet},
env, fs,
fs::File,
io::{self, Read, Seek, SeekFrom, Write},
net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, UdpSocket},
path::{Component, Path, PathBuf},
process::{Child, Command, Stdio},
sync::{LazyLock, Mutex},
thread,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use tar::Archive;
#[cfg(any(target_os = "linux", target_os = "windows"))]
use tauri::menu::PredefinedMenuItem;
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIcon};
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "windows"
))]
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, TrayIconBuilder, TrayIconEvent},
};
use tauri::{Emitter, LogicalSize, Manager};
use tauri_plugin_autostart::ManagerExt as AutostartManagerExt;
use tauri_plugin_opener::OpenerExt;
use tokio_util::sync::CancellationToken;
#[cfg(any(
target_os = "linux",
target_os = "macos",
target_os = "windows"
))]
use tray::*;
use zip::ZipArchive;
const RELEASE_PAGE_URL: &str = "https://github.com/router-for-me/CLIProxyAPI/releases/latest";
const RELEASE_ATOM_URL: &str = "https://github.com/router-for-me/CLIProxyAPI/releases.atom";
const RELEASE_DOWNLOAD_PREFIX: &str =
"https://github.com/router-for-me/CLIProxyAPI/releases/download/";
#[cfg(windows)]
const APP_UPDATE_MANIFEST_URL: &str = "https://github.com/router-for-me/EasyCLIProxyAPI/releases/latest/download/portable-update-windows.json";
#[cfg(target_os = "linux")]
const APP_UPDATE_MANIFEST_URL: &str = "https://github.com/router-for-me/EasyCLIProxyAPI/releases/latest/download/portable-update-linux.json";
#[cfg(target_os = "macos")]
const APP_UPDATE_MANIFEST_URL: &str = "https://github.com/router-for-me/EasyCLIProxyAPI/releases/latest/download/portable-update-darwin-v2.json";
const APP_RELEASE_DOWNLOAD_PREFIX: &str =
"https://github.com/router-for-me/EasyCLIProxyAPI/releases/download/";
#[cfg(windows)]
const APP_UPDATE_MANIFEST_NAME: &str = "portable-update-windows.json";
#[cfg(target_os = "linux")]
const APP_UPDATE_MANIFEST_NAME: &str = "portable-update-linux.json";
#[cfg(target_os = "macos")]
const APP_UPDATE_MANIFEST_NAME: &str = "portable-update-darwin-v2.json";
const CODEX_MODEL_CATALOG_URL: &str = "https://raw.githubusercontent.com/router-for-me/EasyCLIProxyAPI/main/src-tauri/resources/codex_models/model-catalog.json";
const CODEX_MODEL_CATALOG_OVERRIDE_DIR: &str = "codex_models";
const CODEX_MODEL_CATALOG_SOURCE_FILE: &str = "model-catalog.json";
const MAX_CODEX_MODEL_CATALOG_BYTES: usize = 4 * 1024 * 1024;
const APP_UPDATE_PROGRESS_EVENT: &str = "app-update-progress";
const PORTABLE_APP_MANIFEST_FILE: &str = "portable-app.json";
#[cfg(windows)]
const PORTABLE_APP_BINARY: &str = "EasyCLIProxyAPI.exe";
#[cfg(target_os = "linux")]
const PORTABLE_APP_BINARY: &str = "EasyCLIProxyAPI";
const CORE_INSTALL_PROGRESS_EVENT: &str = "core-install-progress";
const CORE_STATUS_EVENT: &str = "core-status-changed";
const CONFIG_FILES_CHANGED_EVENT: &str = "config-files-changed";
const VERSION_DOWNLOAD_SOURCE_CHANGED_EVENT: &str = "version-download-source-changed";
static VERSION_SOURCE_DETECTION_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
#[cfg(any(target_os = "linux", target_os = "windows"))]
const WINDOWS_CLOSE_REQUEST_EVENT: &str = "windows-close-requested";
const CORE_METADATA_FILE: &str = "cpa-gui-meta.json";
const CORE_CONFIG_FILE: &str = "config.yaml";
const CORE_EXAMPLE_CONFIG_FILE: &str = "config.example.yaml";
const CORE_VERSION_FILE: &str = "core-version.txt";
const CORE_CHECKSUMS_FILE: &str = "checksums.txt";
const GUI_CONFIG_FILE: &str = "config.toml";
const LEGACY_GUI_CONFIG_FILE: &str = "cpa-gui.yaml";
const MIN_MAIN_WINDOW_WIDTH: u32 = 640;
const MIN_MAIN_WINDOW_HEIGHT: u32 = 600;
const MAX_SAVED_WINDOW_DIMENSION: u32 = 16_384;
const DEFAULT_MAIN_WINDOW_WIDTH: u32 = 1280;
const DEFAULT_MAIN_WINDOW_HEIGHT: u32 = 800;
const LEGACY_DEFAULT_MAIN_WINDOW_WIDTH: u32 = 1531;
const LEGACY_DEFAULT_MAIN_WINDOW_HEIGHT: u32 = 891;
const OAUTH_DIR_NAME: &str = "oauth";
const DEFAULT_AUTH_DIR: &str = "../oauth";
const DEFAULT_API_KEY: &str = "123456";
const DEFAULT_AGENT_TERMINAL: &str = "auto";
const DEFAULT_API_KEY_INITIAL_REMARK: &str = "Default key";
const DEFAULT_REQUEST_RETRY: u32 = 3;
const DEFAULT_MAX_RETRY_CREDENTIALS: u32 = 0;
const DEFAULT_MAX_RETRY_INTERVAL: i64 = 30;
const DEFAULT_STREAMING_BOOTSTRAP_RETRIES: u32 = 0;
const DEFAULT_DISABLE_COOLING: bool = false;
const DEFAULT_LOGS_MAX_TOTAL_SIZE_MB: u32 = 0;
const DEFAULT_ERROR_LOGS_MAX_FILES: u32 = 10;
const DEFAULT_REDIS_USAGE_QUEUE_RETENTION_SECONDS: u32 = 60;
const LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY: &str = "123456";
const MANAGED_AGENT_PROVIDER_ID: &str = "cpa-gui";
const ZCODE_PROVIDER_CONFIG_FILE: &str = "provider_config.json";
const KIMI_CODE_CONFIG_FILE: &str = "config.toml";
const GROK_BUILD_CONFIG_FILE: &str = "config.toml";
const DEEPSEEK_HARNESS_PROVIDER_ID: &str = "easy-cliproxyapi";
const DEEPSEEK_HARNESS_CREDENTIAL: &str = "EASYCLIPROXYAPI_API_KEY";
const DEEPSEEK_HARNESS_CREDENTIALS_VERSION: u64 = 1;
const DEEPSEEK_HARNESS_DEFAULT_WEB_PORT: u16 = 3080;
const DEEPSEEK_HARNESS_SETTINGS_FILE: &str = "settings.yaml";
const DEEPSEEK_HARNESS_CREDENTIALS_FILE: &str = ".credentials.yaml";
const PI_AGENT_ID: &str = "pi";
const PI_AGENT_NAME: &str = "Pi";
const OMP_AGENT_PROVIDER_ID: &str = "easy-cliproxyapi";
const OMP_AGENT_API_KEY_ENV: &str = "EASYCLIPROXYAPI_API_KEY";
const OMP_AGENT_API: &str = "openai-responses";
const OMP_MODELS_FILE: &str = "models.yml";
const OMP_SETTINGS_FILE: &str = "config.yml";
const PI_CLIPROXYAPI_PACKAGE: &str = "npm:@router-for-me/pi-cliproxyapi-provider";
const PI_CLIPROXYAPI_NPM_LATEST_URL: &str =
"https://registry.npmjs.org/@router-for-me%2Fpi-cliproxyapi-provider/latest";
const PI_CLIPROXYAPI_PROVIDER_ID: &str = "cliproxyapi";
const PI_AGENT_CONFIG_FILE: &str = "cliproxyapi.json";
const PI_AGENT_SETTINGS_FILE: &str = "settings.json";
const CODEX_MODEL_CATALOG_FILE: &str = "cpa-gui-model-catalog.json";
const CODEX_OAUTH_LOGIN_REQUIRED_ERROR: &str = "CODEX_OAUTH_LOGIN_REQUIRED";
const CLAUDE_DESKTOP_PROFILE_ID: &str = "00000000-0000-4000-8000-000000831700";
const CLAUDE_DESKTOP_OPUS_MODEL_ID: &str = "claude-opus-5-cpa";
const CLAUDE_DESKTOP_SONNET_MODEL_ID: &str = "claude-sonnet-5-cpa";
const CLAUDE_DESKTOP_HAIKU_MODEL_ID: &str = "claude-haiku-4-5-cpa";
const LEGACY_CLAUDE_DESKTOP_MODEL_IDS: [&str; 3] =
["claude-opus-5", "claude-sonnet-4-6", "claude-haiku-4-5"];
const MANAGED_CLAUDE_OPUS_ALIAS_DISPLAY_NAME: &str = "EasyCLIProxyAPI managed Claude Opus mapping";
const MANAGED_CLAUDE_SONNET_ALIAS_DISPLAY_NAME: &str =
"EasyCLIProxyAPI managed Claude Sonnet mapping";
const MANAGED_CLAUDE_HAIKU_ALIAS_DISPLAY_NAME: &str =
"EasyCLIProxyAPI managed Claude Haiku mapping";
const DEFAULT_CLAUDE_CONTEXT_WINDOW: u64 = 200_000;
const CLAUDE_DESKTOP_EXTENDED_CONTEXT_WINDOW: u64 = 1_000_000;
const CLAUDE_CODE_MAX_CONTEXT_TOKENS_ENV: &str = "CLAUDE_CODE_MAX_CONTEXT_TOKENS";
const CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV: &str =
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
const CLAUDE_CODE_AUTO_MODE_SERVER_ENV: &str = "CLAUDE_CODE_AUTO_MODE_SERVER";
const CLAUDE_AUTOCOMPACT_PCT_OVERRIDE_ENV: &str = "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE";
const DISABLE_AUTO_COMPACT_ENV: &str = "DISABLE_AUTO_COMPACT";
const DEFAULT_CLAUDE_AUTO_COMPACT_PCT: u8 = 90;
const MODEL_ALIAS_CONFIG_SECTIONS: &[&str] = &[
"codex-api-key",
"openai-compatibility",
"claude-api-key",
"gemini-api-key",
];
#[cfg(test)]
const LEGACY_AGENT_MODIFICATION_STATE_VERSION: u8 = 1;
#[cfg(test)]
const AGENT_MODIFICATION_STATE_VERSION: u8 = 2;
#[cfg(test)]
const AGENT_APPLIED_STATE_VERSION: u8 = 4;
#[cfg(test)]
const AGENT_CONFIGURATION_REVISION: u8 = 1;
#[cfg(test)]
const AGENT_PHASE_APPLYING: &str = "applying";
#[cfg(test)]
const AGENT_PHASE_ACTIVE: &str = "active";
#[cfg(test)]
const AGENT_PHASE_RESTORING: &str = "restoring";
#[cfg(test)]
const AGENT_PHASE_RECOVERY: &str = "recovery";
#[cfg(test)]
const AGENT_MODIFICATION_STATE_CONFLICT: &str = "conflict";
const USER_AGENT: &str = concat!(
"CPA-GUI/",
env!("CARGO_PKG_VERSION"),
" (+https://github.com/router-for-me/CLIProxyAPI)"
);
const APP_USER_AGENT: &str = concat!(
"EasyCLIProxyAPI/",
env!("CARGO_PKG_VERSION"),
" (+https://github.com/router-for-me/EasyCLIProxyAPI)"
);
static CORE_CONFIG_FILE_LOCK: Mutex<()> = Mutex::new(());
static AGENT_CONFIG_FILE_LOCK: Mutex<()> = Mutex::new(());
#[cfg(test)]
static CODEX_APPLIED_STATES: LazyLock<
Mutex<std::collections::HashMap<PathBuf, AgentAppliedState>>,
> = LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
static CONFIG_WRITE_HASHES: LazyLock<Mutex<std::collections::HashMap<PathBuf, String>>> =
LazyLock::new(|| Mutex::new(std::collections::HashMap::new()));
fn normalize_app_locale(locale: &str) -> &'static str {
let normalized = locale.trim().to_ascii_lowercase();
if normalized.starts_with("ja") {
"ja"
} else if normalized == "zh-tw"
|| normalized == "zh-hk"
|| normalized == "zh-mo"
|| normalized.starts_with("zh-hant")
{
"zh-TW"
} else if normalized.starts_with("zh") {
"zh-CN"
} else {
"en"
}
}
#[derive(Default)]
struct CoreDownloadState {
inner: Mutex<CoreDownloadInner>,
}
#[derive(Default)]
struct CoreDownloadInner {
running: bool,
token: Option<CancellationToken>,
task: CoreInstallTask,
}
#[derive(Default)]
struct AppUpdateState {
inner: Mutex<AppUpdateInner>,
}
#[derive(Default)]
struct AppUpdateInner {
task: AppUpdateTask,
token: Option<CancellationToken>,
}
#[derive(Default)]
struct CoreProcessState {
child: Mutex<Option<CoreChild>>,
adopted_processes: Mutex<Vec<AdoptedCoreProcess>>,
starting: AtomicBool,
shutting_down: AtomicBool,
shutdown_complete: AtomicBool,
}
#[derive(Default)]
struct DeepSeekHarnessProcessState {
process: Mutex<Option<ManagedDeepSeekHarnessProcess>>,
}
struct ManagedDeepSeekHarnessProcess {
child: Child,
mode: String,
launch: DeepSeekHarnessLaunchSnapshot,
}
#[derive(Clone)]
struct DeepSeekHarnessLaunchSnapshot {
executable: PathBuf,
working_directory: PathBuf,
arguments: Vec<String>,
options: Option<DeepSeekHarnessLaunchOptions>,
}
#[derive(Clone)]
struct AdoptedCoreProcess {
process_id: u32,
binary_path: PathBuf,
}
struct GuiConfigState {
inner: Mutex<GuiConfigFile>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct SavedWindowSize {
width: u32,
height: u32,
}
struct MainWindowSizeState {
inner: Mutex<Option<SavedWindowSize>>,
}
#[derive(Default)]
struct AgentConfigStatusCache {
refresh_lock: Mutex<()>,
entry: Mutex<Option<AgentConfigStatusCacheEntry>>,
}
struct AgentConfigStatusCacheEntry {
port: u16,
api_key_sha256: String,
statuses: Vec<AgentConfigStatus>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct ConfigFilesChangedPayload {
paths: Vec<String>,
errors: Vec<String>,
}
impl AgentConfigStatusCache {
fn get(&self, port: u16, api_key: &str) -> Result<Option<Vec<AgentConfigStatus>>, String> {
let api_key_sha256 = sha256_bytes(api_key.as_bytes());
self.entry
.lock()
.map(|entry| {
entry
.as_ref()
.filter(|entry| entry.port == port && entry.api_key_sha256 == api_key_sha256)
.map(|entry| entry.statuses.clone())
})
.map_err(|_| "Agent configuration status cache lock is poisoned".to_string())
}
fn replace(
&self,
port: u16,
api_key: &str,
statuses: Vec<AgentConfigStatus>,
) -> Result<(), String> {
let mut current = self
.entry
.lock()
.map_err(|_| "Agent configuration status cache lock is poisoned".to_string())?;
*current = Some(AgentConfigStatusCacheEntry {
port,
api_key_sha256: sha256_bytes(api_key.as_bytes()),
statuses,
});
Ok(())
}
fn clear(&self) -> Result<(), String> {
let mut current = self
.entry
.lock()
.map_err(|_| "Agent configuration status cache lock is poisoned".to_string())?;
*current = None;
Ok(())
}
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CorePlatform {
os: String,
arch: String,
asset_os: String,
asset_arch: String,
archive_kind: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreStatus {
installed: bool,
running: bool,
ready: bool,
starting: bool,
managed: bool,
process_id: Option<u32>,
current_version: Option<String>,
install_dir: String,
binary_path: Option<String>,
message: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreLatest {
version: String,
asset_name: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AppUpdateInfo {
current_version: String,
latest_version: String,
update_available: bool,
release_url: String,
release_notes: HashMap<String, String>,
published_at: String,
auto_update_supported: bool,
download_size_bytes: Option<u64>,
unsupported_reason: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PortableUpdateManifest {
schema_version: u32,
version: String,
published_at: String,
release_url: String,
#[serde(default, deserialize_with = "deserialize_release_notes")]
release_notes: HashMap<String, String>,
assets: std::collections::HashMap<String, PortableUpdateAsset>,
#[serde(default)]
full_assets: Option<std::collections::HashMap<String, PortableUpdateAsset>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PortableUpdateAsset {
url: String,
#[serde(default)]
fallback_urls: Vec<String>,
sha256: String,
size_bytes: u64,
}
#[derive(Debug, Deserialize)]
struct GitcodeRelease {
tag_name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PortableAppManifest {
schema_version: u32,
application: String,
version: String,
platform: String,
arch: String,
#[serde(default)]
auto_update: bool,
}
#[derive(Clone)]
struct PendingAppUpdate {
version: String,
asset: PortableUpdateAsset,
#[cfg_attr(not(windows), allow(dead_code))]
arch: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AppUpdateTask {
running: bool,
cancellable: bool,
phase: String,
target_version: Option<String>,
downloaded_bytes: u64,
total_bytes: Option<u64>,
percent: Option<f64>,
message: Option<String>,
}
impl Default for AppUpdateTask {
fn default() -> Self {
Self {
running: false,
cancellable: false,
phase: "idle".to_string(),
target_version: None,
downloaded_bytes: 0,
total_bytes: None,
percent: None,
message: None,
}
}
}
#[cfg(any(windows, target_os = "linux"))]
struct PortablePackagePayload {
manifest: PortableAppManifest,
core_archive_name: Option<String>,
}
#[cfg(any(windows, target_os = "linux"))]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PortableUpdateDescriptor {
parent_pid: u32,
current_exe: PathBuf,
staged_exe: PathBuf,
current_manifest: PathBuf,
staged_manifest: PathBuf,
backup_exe: PathBuf,
backup_manifest: PathBuf,
current_core_version: PathBuf,
staged_core_version: PathBuf,
backup_core_version: PathBuf,
staged_core_archive: PathBuf,
target_core_archive: PathBuf,
install_core_archive: bool,
ack_path: PathBuf,
work_dir: PathBuf,
target_version: String,
}
#[cfg(target_os = "macos")]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct MacosUpdateDescriptor {
parent_pid: u32,
current_app: PathBuf,
staged_app: PathBuf,
backup_app: PathBuf,
executable_relative_path: PathBuf,
ack_path: PathBuf,
work_dir: PathBuf,
target_version: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct BundledCoreInfo {
version: String,
asset_name: String,
size_bytes: u64,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreInstallResult {
version: String,
asset_name: String,
install_dir: String,
binary_path: Option<String>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreInstallTask {
running: bool,
cancellable: bool,
phase: String,
downloaded: u64,
total: Option<u64>,
percent: Option<f64>,
message: Option<String>,
result: Option<CoreInstallResult>,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
struct GuiConfigFile {
locale: String,
port: u16,
allow_lan: bool,
host: String,
run_on_startup: bool,
start_core_on_launch: bool,
silent_start: bool,
close_behavior: WindowsCloseBehavior,
default_terminal: String,
window_width: Option<u32>,
window_height: Option<u32>,
auth_dir: String,
auth_dir_user_selected: bool,
#[serde(deserialize_with = "deserialize_gui_api_keys")]
api_keys: Vec<GuiApiKeyEntry>,
api_access_remarks: Vec<GuiApiAccessRemark>,
management_secret_key: String,
debug: bool,
commercial_mode: bool,
logging_to_file: bool,
logs_max_total_size_mb: u32,
error_logs_max_files: u32,
usage_statistics_enabled: bool,
#[serde(rename = "usage_statistics_disabled", skip_serializing_if = "Option::is_none")]
usage_statistics_disabled: Option<bool>,
redis_usage_queue_retention_seconds: u32,
request_log: bool,
plugins_enabled: bool,
routing_strategy: String,
proxy_url: String,
proxy_override: bool,
download_source: VersionDownloadSource,
custom_download_mirrors: Vec<String>,
active_custom_download_mirror: String,
prefer_gitcode_downloads: bool,
routing_session_affinity: bool,
routing_session_affinity_ttl: String,
disable_cooling: bool,
request_retry: u32,
max_retry_credentials: u32,
max_retry_interval: i64,
streaming_bootstrap_retries: u32,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum VersionDownloadSource {
#[default]
Github,
Gitcode,
GhProxy,
GhFast,
Custom,
}
impl VersionDownloadSource {
fn as_str(self) -> &'static str {
match self {
Self::Github => "github",
Self::Gitcode => "gitcode",
Self::GhProxy => "gh-proxy",
Self::GhFast => "gh-fast",
Self::Custom => "custom",
}
}
fn from_str(value: &str) -> Option<Self> {
match value.trim() {
"github" => Some(Self::Github),
"gitcode" => Some(Self::Gitcode),
"gh-proxy" => Some(Self::GhProxy),
"gh-fast" => Some(Self::GhFast),
"custom" => Some(Self::Custom),
_ => None,
}
}
fn display_name(self) -> &'static str {
match self {
Self::Github => "GitHub",
Self::Gitcode => "GitCode",
Self::GhProxy => "gh-proxy.com",
Self::GhFast => "ghfast.top",
Self::Custom => "Custom mirror",
}
}
fn github_proxy_prefix(self) -> Option<&'static str> {
match self {
Self::GhProxy => Some("https://gh-proxy.com/"),
Self::GhFast => Some("https://ghfast.top/"),
Self::Custom => None,
_ => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct VersionDownloadCandidate {
source: VersionDownloadSource,
custom_url: Option<String>,
}
impl VersionDownloadCandidate {
fn builtin(source: VersionDownloadSource) -> Self {
Self {
source,
custom_url: None,
}
}
fn custom(url: &str) -> Self {
Self {
source: VersionDownloadSource::Custom,
custom_url: Some(url.to_string()),
}
}
fn key(&self) -> String {
self.custom_url
.as_ref()
.map(|url| format!("custom:{url}"))
.unwrap_or_else(|| self.source.as_str().to_string())
}
fn display_name(&self) -> String {
self.custom_url
.as_ref()
.and_then(|url| reqwest::Url::parse(url).ok())
.and_then(|url| url.host_str().map(str::to_string))
.unwrap_or_else(|| self.source.display_name().to_string())
}
fn proxy_prefix(&self) -> Option<&str> {
self.custom_url
.as_deref()
.or_else(|| self.source.github_proxy_prefix())
}
}
fn version_source_url(source: &VersionDownloadCandidate, github_url: &str) -> String {
source
.proxy_prefix()
.map(|prefix| format!("{prefix}{github_url}"))
.unwrap_or_else(|| github_url.to_string())
}
fn version_download_source_candidates(
preferred: VersionDownloadCandidate,
gitcode_available: bool,
custom_mirrors: &[String],
) -> Vec<VersionDownloadCandidate> {
let mut candidates = vec![
preferred,
VersionDownloadCandidate::builtin(VersionDownloadSource::Github),
VersionDownloadCandidate::builtin(VersionDownloadSource::Gitcode),
VersionDownloadCandidate::builtin(VersionDownloadSource::GhProxy),
VersionDownloadCandidate::builtin(VersionDownloadSource::GhFast),
];
candidates.retain(|candidate| {
gitcode_available || candidate.source != VersionDownloadSource::Gitcode
});
candidates.extend(
custom_mirrors
.iter()
.map(|url| VersionDownloadCandidate::custom(url)),
);
candidates
.into_iter()
.fold(Vec::new(), |mut sources, source| {
if !sources.contains(&source) {
sources.push(source);
}
sources
})
}
fn normalize_custom_download_mirror_url(value: &str) -> Result<String, String> {
let mut url =
reqwest::Url::parse(value.trim()).map_err(|error| format!("Invalid mirror URL: {error}"))?;
if url.scheme() != "https"
|| url.host_str().is_none()
|| url.port().is_some()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
return Err("Mirror URL must be an HTTPS URL without authentication, port, query parameters, or fragment".to_string());
}
if !url.path().ends_with('/') {
let path = format!("{}/", url.path());
url.set_path(&path);
}
Ok(url.to_string())
}
impl GuiConfigFile {
fn selected_download_candidate(&self) -> VersionDownloadCandidate {
if self.download_source == VersionDownloadSource::Custom
&& self
.custom_download_mirrors
.contains(&self.active_custom_download_mirror)
{
VersionDownloadCandidate::custom(&self.active_custom_download_mirror)
} else {
VersionDownloadCandidate::builtin(self.download_source)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum WindowsCloseAction {
Exit,
MinimizeToTray,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
enum WindowsCloseBehavior {
#[default]
Ask,
Exit,
MinimizeToTray,
}
impl WindowsCloseBehavior {
fn as_str(self) -> &'static str {
match self {
Self::Ask => "ask",
Self::Exit => "exit",
Self::MinimizeToTray => "minimize-to-tray",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct GuiApiKeyEntry {
key: String,
#[serde(default)]
remark: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct GuiApiAccessRemark {
provider_section: String,
api_key_hash: String,
#[serde(default)]
record_hash: String,
remark: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiAccessRemarkLocator {
provider_name: String,
base_url: String,
api_keys: Vec<String>,
#[serde(default)]
config_identity: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiAccessRemarkQuery {
provider_section: String,
#[serde(flatten)]
locator: ApiAccessRemarkLocator,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiAccessRemarkUpdate {
provider_section: String,
previous_records: Vec<ApiAccessRemarkLocator>,
records: Vec<ApiAccessRemarkLocator>,
all_records: Vec<ApiAccessRemarkLocator>,
remark: String,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum GuiApiKeyInput {
Legacy(String),
Entry(GuiApiKeyEntry),
}
fn deserialize_gui_api_keys<'de, D>(deserializer: D) -> Result<Vec<GuiApiKeyEntry>, D::Error>
where
D: serde::Deserializer<'de>,
{
let entries = Vec::<GuiApiKeyInput>::deserialize(deserializer)?;
Ok(entries
.into_iter()
.map(|entry| match entry {
GuiApiKeyInput::Legacy(key) => GuiApiKeyEntry {
remark: String::new(),
key,
},
GuiApiKeyInput::Entry(entry) => entry,
})
.collect())
}
impl Default for GuiConfigFile {
fn default() -> Self {
Self {
locale: "en".to_string(),
port: 8317,
allow_lan: false,
host: "127.0.0.1".to_string(),
run_on_startup: false,
start_core_on_launch: true,
silent_start: false,
close_behavior: WindowsCloseBehavior::Ask,
default_terminal: DEFAULT_AGENT_TERMINAL.to_string(),
window_width: Some(DEFAULT_MAIN_WINDOW_WIDTH),
window_height: Some(DEFAULT_MAIN_WINDOW_HEIGHT),
auth_dir: DEFAULT_AUTH_DIR.to_string(),
auth_dir_user_selected: false,
api_keys: vec![default_api_key_entry()],
api_access_remarks: Vec::new(),
management_secret_key: LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string(),
debug: false,
commercial_mode: false,
logging_to_file: false,
logs_max_total_size_mb: DEFAULT_LOGS_MAX_TOTAL_SIZE_MB,
error_logs_max_files: DEFAULT_ERROR_LOGS_MAX_FILES,
usage_statistics_enabled: true,
usage_statistics_disabled: None,
redis_usage_queue_retention_seconds: DEFAULT_REDIS_USAGE_QUEUE_RETENTION_SECONDS,
request_log: false,
plugins_enabled: false,
routing_strategy: "round-robin".to_string(),
proxy_url: String::new(),
proxy_override: false,
download_source: VersionDownloadSource::Github,
custom_download_mirrors: Vec::new(),
active_custom_download_mirror: String::new(),
prefer_gitcode_downloads: false,
routing_session_affinity: false,
routing_session_affinity_ttl: String::new(),
disable_cooling: DEFAULT_DISABLE_COOLING,
request_retry: DEFAULT_REQUEST_RETRY,
max_retry_credentials: DEFAULT_MAX_RETRY_CREDENTIALS,
max_retry_interval: DEFAULT_MAX_RETRY_INTERVAL,
streaming_bootstrap_retries: DEFAULT_STREAMING_BOOTSTRAP_RETRIES,
}
}
}
#[derive(Default, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
struct GuiConfigPresence {
locale: Option<String>,
port: Option<u16>,
allow_lan: Option<bool>,
host: Option<String>,
auth_dir: Option<String>,
api_keys: Option<Vec<GuiApiKeyInput>>,
api_access_remarks: Option<Vec<GuiApiAccessRemark>>,
management_secret_key: Option<String>,
close_behavior: Option<WindowsCloseBehavior>,
default_terminal: Option<String>,
start_core_on_launch: Option<bool>,
silent_start: Option<bool>,
debug: Option<bool>,
commercial_mode: Option<bool>,
logging_to_file: Option<bool>,
logs_max_total_size_mb: Option<u32>,
error_logs_max_files: Option<u32>,
usage_statistics_enabled: Option<bool>,
redis_usage_queue_retention_seconds: Option<u32>,
request_log: Option<bool>,
plugins_enabled: Option<bool>,
routing_strategy: Option<String>,
proxy_url: Option<String>,
proxy_override: Option<bool>,
download_source: Option<VersionDownloadSource>,
custom_download_mirrors: Option<Vec<String>>,
active_custom_download_mirror: Option<String>,
prefer_gitcode_downloads: Option<bool>,
routing_session_affinity: Option<bool>,
routing_session_affinity_ttl: Option<String>,
disable_cooling: Option<bool>,
request_retry: Option<u32>,
max_retry_credentials: Option<u32>,
max_retry_interval: Option<i64>,
streaming_bootstrap_retries: Option<u32>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct GuiSettings {
host: String,
port: u16,
allow_lan: bool,
run_on_startup: bool,
close_behavior: WindowsCloseBehavior,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct VersionSourceSettings {
source: String,
gitcode_available: bool,
custom_mirrors: Vec<String>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct SoftwareSettings {
close_behavior: WindowsCloseBehavior,
autostart_enabled: bool,
start_core_on_launch: bool,
silent_start_enabled: bool,
default_terminal: String,
available_terminals: Vec<AgentTerminalOption>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SoftwareSettingsInput {
close_behavior: WindowsCloseBehavior,
autostart_enabled: bool,
start_core_on_launch: bool,
silent_start_enabled: bool,
default_terminal: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentConfigStatus {
id: String,
name: String,
supported_platform: bool,
installed: bool,
plugin_installed: bool,
launch_targets: Vec<AgentLaunchTarget>,
version: Option<String>,
cli_version: Option<String>,
app_version: Option<String>,
plugin_version: Option<String>,
config_paths: Vec<String>,
config_exists: bool,
config_valid: bool,
configured: bool,
configuration_synchronized: bool,
connection_state: String,
current_model: Option<String>,
oauth_configuration: bool,
codex_native_oauth: bool,
modification_enabled: bool,
modification_state: String,
backup_available: bool,
applied_model: Option<String>,
claude_code_model_mappings: Option<ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
warnings: Vec<String>,
error: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentLaunchTarget {
id: String,
label: String,
detail: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DeepSeekHarnessLaunchOptions {
mode: String,
web_host: Option<String>,
web_port: Option<u16>,
open_browser: Option<bool>,
#[serde(default)]
trusted_hosts: Vec<String>,
task: Option<String>,
profile: Option<String>,
#[serde(default)]
patches: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize)]
#[serde(rename_all = "camelCase")]
struct DeepSeekHarnessProcessStatus {
running: bool,
pid: Option<u32>,
mode: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentConfigActionResult {
outcome: String,
enabled: bool,
model: Option<String>,
changed_files: Vec<String>,
conflict_files: Vec<String>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PiProviderUpdateStatus {
installed_version: Option<String>,
latest_version: Option<String>,
update_available: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CodexModelCatalogUpdateResult {
outcome: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentModelOption {
name: String,
alias: Option<String>,
#[serde(default)]
is_alias: bool,
#[serde(default)]
context_window: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
input_modalities: Option<Vec<String>>,
#[serde(skip)]
harness_metadata: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ClaudeDesktopModelMappings {
#[serde(default, skip_serializing_if = "Option::is_none")]
desktop_models: Option<Vec<ClaudeDesktopModelMapping>>,
#[serde(default)]
opus: String,
#[serde(default)]
sonnet: String,
#[serde(default)]
haiku: String,
#[serde(default)]
opus_1m: bool,
#[serde(default)]
sonnet_1m: bool,
#[serde(default)]
haiku_1m: bool,
#[serde(default = "default_claude_code_max_context_tokens")]
max_context_tokens: u64,
#[serde(default = "default_claude_auto_compact_pct")]
auto_compact_pct: u8,
#[serde(default)]
disable_auto_compact: bool,
/// Keep Claude Code's session/default model selection under CPA control.
/// Defaults to true for backwards compatibility with existing profiles.
#[serde(default = "default_true")]
manage_default_model: bool,
/// Set CLAUDE_CODE_SUBAGENT_MODEL when enabled; otherwise allow inheritance.
/// Defaults to true for backwards compatibility with existing profiles.
#[serde(default = "default_true")]
manage_subagent_model: bool,
/// Optional explicit model written to ANTHROPIC_MODEL.
#[serde(default)]
startup_model: Option<String>,
/// Optional explicit model written to CLAUDE_CODE_SUBAGENT_MODEL.
#[serde(default)]
subagent_model: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ClaudeDesktopModelMapping {
#[serde(default)]
model: String,
#[serde(default)]
alias: String,
#[serde(default)]
context_1m: bool,
}
impl ClaudeDesktopModelMapping {
fn model_id(&self) -> &str {
if self.alias.trim().is_empty() {
self.model.trim()
} else {
self.alias.trim()
}
}
fn source_or_alias(&self) -> &str {
if self.model.trim().is_empty() {
self.alias.trim()
} else {
self.model.trim()
}
}
fn has_mapping(&self) -> bool {
!self.model.trim().is_empty()
&& !self.alias.trim().is_empty()
&& !self.model.trim().eq_ignore_ascii_case(self.alias.trim())
}
}
fn default_claude_code_max_context_tokens() -> u64 {
DEFAULT_CLAUDE_CONTEXT_WINDOW
}
fn default_claude_auto_compact_pct() -> u8 {
DEFAULT_CLAUDE_AUTO_COMPACT_PCT
}
fn default_true() -> bool {
true
}
impl ClaudeDesktopModelMappings {
fn all(model: &str) -> Self {
Self {
desktop_models: None,
opus: model.to_string(),
sonnet: model.to_string(),
haiku: model.to_string(),
opus_1m: false,
sonnet_1m: false,
haiku_1m: false,
max_context_tokens: default_claude_code_max_context_tokens(),
auto_compact_pct: default_claude_auto_compact_pct(),
disable_auto_compact: false,
manage_default_model: true,
manage_subagent_model: true,
startup_model: None,
subagent_model: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct CodexModelDefinition {
id: String,
display_name: Option<String>,
description: Option<String>,
context_window: Option<u64>,
reasoning_levels: Vec<String>,
supports_tools: Option<bool>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ThinkingAliasEntry {
source_model: String,
alias: String,
effort: Option<String>,
provider: String,
kind: String,
oauth_channel: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct SpeedAliasEntry {
source_model: String,
alias: String,
service_tier: String,
provider: String,
kind: String,
oauth_channel: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
struct ThinkingAliasSource {
id: String,
model: String,
display_name: Option<String>,
provider: String,
kind: String,
protocol: String,
reasoning_levels: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ThinkingAliasSourceLocation {
Oauth {
channel: &'static str,
force_mapping: bool,
},
ConfigModel {
section: &'static str,
provider_index: usize,
model_index: usize,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ResolvedThinkingAliasSource {
source: ThinkingAliasSource,
location: ThinkingAliasSourceLocation,
}
#[cfg(test)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AgentModificationRecord {
version: u8,
client: String,
phase: String,
model: String,
files: Vec<AgentModificationFile>,
}
#[cfg(test)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AgentModificationFile {
path: PathBuf,
backup_path: PathBuf,
existed_before: bool,
original_sha256: Option<String>,
managed_sha256: String,
}
#[cfg(test)]
struct AgentModificationInspection {
enabled: bool,
state: String,
backup_available: bool,
warnings: Vec<String>,
}
#[cfg(test)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AgentAppliedState {
version: u8,
client: String,
model: String,
#[serde(default)]
configuration_revision: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
claude_desktop_model_mappings: Option<ClaudeDesktopModelMappings>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
backup_files: Vec<AgentAppliedBackupFile>,
updated_at_unix: u64,
}
#[cfg(test)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AgentAppliedBackupFile {
path: PathBuf,
backup_path: PathBuf,
existed_before: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum AgentClient {
ClaudeCode,
ClaudeDesktop,
Codex,
OpenCode,
OpenClaw,
Hermes,
DeepSeekHarness,
ZCode,
WorkBuddy,
AntigravityCli,
KimiCode,
GrokBuild,
Omp,
}
#[derive(Clone, Debug)]
enum DesktopAppTarget {
Application(PathBuf),
#[cfg(target_os = "windows")]
WindowsAppId(String),
}
impl AgentClient {
fn parse(value: &str) -> Result<Self, String> {
match value.trim().to_ascii_lowercase().as_str() {
"claude-code" => Ok(Self::ClaudeCode),
"claude-desktop" => Ok(Self::ClaudeDesktop),
"codex" => Ok(Self::Codex),
"opencode" => Ok(Self::OpenCode),
"openclaw" => Ok(Self::OpenClaw),
"hermes" => Ok(Self::Hermes),
"deepseek-harness" => Ok(Self::DeepSeekHarness),
"zcode" => Ok(Self::ZCode),
"workbuddy" => Ok(Self::WorkBuddy),
"antigravity-cli" => Ok(Self::AntigravityCli),
"kimi-code" => Ok(Self::KimiCode),
"grok-build" => Ok(Self::GrokBuild),
"omp" => Ok(Self::Omp),
_ => Err(format!("Unsupported agent client: {value}")),
}
}
fn id(self) -> &'static str {
match self {
Self::ClaudeCode => "claude-code",
Self::ClaudeDesktop => "claude-desktop",
Self::Codex => "codex",
Self::OpenCode => "opencode",
Self::OpenClaw => "openclaw",
Self::Hermes => "hermes",
Self::DeepSeekHarness => "deepseek-harness",
Self::ZCode => "zcode",
Self::WorkBuddy => "workbuddy",
Self::AntigravityCli => "antigravity-cli",
Self::KimiCode => "kimi-code",
Self::GrokBuild => "grok-build",
Self::Omp => "omp",
}
}
fn name(self) -> &'static str {
match self {
Self::ClaudeCode => "Claude Code",
Self::ClaudeDesktop => "Claude Desktop",
Self::Codex => "Codex",
Self::OpenCode => "OpenCode",
Self::OpenClaw => "OpenClaw",
Self::Hermes => "Hermes Agent",
Self::DeepSeekHarness => "DeepSeek Harness",
Self::ZCode => "ZCode",
Self::WorkBuddy => "WorkBuddy",
Self::AntigravityCli => "Antigravity CLI",
Self::KimiCode => "Kimi Code",
Self::GrokBuild => "Grok Build",
Self::Omp => "Oh My Pi",
}
}
fn supported_platform(self) -> bool {
self != Self::ClaudeDesktop
|| cfg!(any(
target_os = "windows",
target_os = "macos",
target_os = "linux"
))
}
fn executable_names(self) -> &'static [&'static str] {
match self {
Self::ClaudeCode => &["claude"],
Self::ClaudeDesktop => &[],
Self::Codex => &["codex"],
Self::OpenCode => &["opencode"],
Self::OpenClaw => &["openclaw"],
Self::Hermes => &["hermes"],
Self::DeepSeekHarness => &["dsh"],
Self::ZCode => &["zcode"],
Self::WorkBuddy => &[],
Self::AntigravityCli => &["agy"],
Self::KimiCode => &["kimi"],
Self::GrokBuild => &["grok"],
Self::Omp => &["omp"],
}
}
}
struct AgentFileUpdate {
path: PathBuf,
after: String,
}
struct AgentConfigurationOptions<'a> {
models: &'a [AgentModelOption],
codex_catalog: Option<&'a str>,
oauth_configuration: bool,
claude_code_model_mappings: Option<&'a ClaudeDesktopModelMappings>,
claude_desktop_model_mappings: Option<&'a ClaudeDesktopModelMappings>,
}
struct PreparedAgentModels {
models: Vec<AgentModelOption>,
codex_catalog: Option<String>,
}
type FileSnapshot = (PathBuf, Option<Vec<u8>>);
#[cfg(test)]
type AgentRecordExtension = (AgentModificationRecord, Vec<FileSnapshot>);
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GuiNetworkSettings {
port: u16,
allow_lan: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GuiNetworkRoutingSettings {
port: u16,
allow_lan: bool,
routing_session_affinity: bool,
routing_session_affinity_ttl: String,
#[serde(default)]
disable_cooling: bool,
request_retry: u32,
max_retry_credentials: u32,
max_retry_interval: i64,
streaming_bootstrap_retries: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GuiNetworkEndpointSettings {
host: String,
port: u16,
#[serde(default)]
proxy_url: Option<String>,
#[serde(default)]
proxy_override: Option<bool>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GuiRetrySettings {
#[serde(default)]
disable_cooling: bool,
request_retry: u32,
max_retry_credentials: u32,
max_retry_interval: i64,
streaming_bootstrap_retries: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct GuiSessionRoutingSettings {
routing_session_affinity: bool,
routing_session_affinity_ttl: String,
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreTlsSettings {
enabled: bool,
cert: String,
key: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreSensitiveWordsSettings {
antigravity_sensitive_words: Vec<String>,
devin_sensitive_words: Vec<String>,
}
#[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CoreLoggingSettingsInput {
debug: bool,
commercial_mode: bool,
logging_to_file: bool,
logs_max_total_size_mb: u32,
error_logs_max_files: u32,
usage_statistics_enabled: bool,
redis_usage_queue_retention_seconds: u32,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreConfigSettings {
#[serde(skip_serializing)]
host: String,
#[serde(skip_serializing)]
port: u16,
#[serde(skip_serializing)]
auth_dir: String,
api_keys: Vec<String>,
management_secret_configured: bool,
debug: bool,
commercial_mode: bool,
logging_to_file: bool,
logs_max_total_size_mb: u32,
error_logs_max_files: u32,
usage_statistics_enabled: bool,
redis_usage_queue_retention_seconds: u32,
request_log: bool,
plugins_enabled: bool,
routing_strategy: String,
proxy_url: String,
routing_session_affinity: bool,
routing_session_affinity_ttl: String,
disable_cooling: bool,
request_retry: u32,
max_retry_credentials: u32,
max_retry_interval: i64,
streaming_bootstrap_retries: u32,
#[allow(dead_code)]
#[serde(skip_serializing)]
management_secret_key: Option<String>,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreApiKeyView {
api_key: String,
remark: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct CoreConfigView {
host: String,
api_keys: Vec<CoreApiKeyView>,
management_secret_configured: bool,
port: u16,
allow_lan: bool,
debug: bool,
commercial_mode: bool,
logging_to_file: bool,
logs_max_total_size_mb: u32,
error_logs_max_files: u32,
usage_statistics_enabled: bool,
redis_usage_queue_retention_seconds: u32,
request_log: bool,
plugins_enabled: bool,
routing_strategy: String,
proxy_url: String,
proxy_override: bool,
routing_session_affinity: bool,
routing_session_affinity_ttl: String,
disable_cooling: bool,
request_retry: u32,
max_retry_credentials: u32,
max_retry_interval: i64,
streaming_bootstrap_retries: u32,
}
impl Default for CoreInstallTask {
fn default() -> Self {
Self {
running: false,
cancellable: false,
phase: "Idle".to_string(),
downloaded: 0,
total: None,
percent: None,
message: None,
result: None,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CoreMetadata {
version: String,
asset_name: String,
installed_at_unix: u64,
}
struct DownloadedArchive {
size: u64,
sha256: String,
}
impl CoreDownloadState {
fn start(&self, token: CancellationToken, version: Option<String>) -> Result<(), String> {
let mut inner = self
.inner
.lock()
.map_err(|_| "Kernel installation status lock is poisoned".to_string())?;
if inner.running {
return Err("A kernel installation task is already running".to_string());
}
inner.running = true;
inner.token = Some(token);
inner.task = CoreInstallTask {
running: true,
cancellable: true,
phase: version
.map(|version| format!("Preparing to install {version}"))
.unwrap_or_else(|| "Preparing to install the latest version".to_string()),
downloaded: 0,
total: None,
percent: None,
message: None,
result: None,
};
Ok(())
}
fn cancel(&self) {
if let Ok(inner) = self.inner.lock() {
if let Some(token) = &inner.token {
token.cancel();
}
}
}
fn snapshot(&self) -> CoreInstallTask {
self.inner
.lock()
.map(|inner| inner.task.clone())
.unwrap_or_default()
}
fn progress(
&self,
window: &tauri::Window,
phase: &str,
downloaded: u64,
total: Option<u64>,
cancellable: bool,
) {
let percent = total
.filter(|total| *total > 0)
.map(|total| downloaded as f64 * 100.0 / total as f64);
let task = {
let Ok(mut inner) = self.inner.lock() else {
return;
};
inner.task.running = inner.running;
inner.task.cancellable = cancellable;
inner.task.phase = phase.to_string();
inner.task.downloaded = downloaded;
inner.task.total = total;
inner.task.percent = percent;
inner.task.clone()
};
let _ = window.emit(CORE_INSTALL_PROGRESS_EVENT, task);
}
fn finish(&self, window: &tauri::Window, result: Result<CoreInstallResult, String>) {
let task = {
let Ok(mut inner) = self.inner.lock() else {
return;
};
inner.running = false;
inner.token = None;
inner.task.running = false;
inner.task.cancellable = false;
match result {
Ok(result) => {
inner.task.phase = "Installation complete".to_string();
inner.task.downloaded = 1;
inner.task.total = Some(1);
inner.task.percent = Some(100.0);
inner.task.message = Some(format!("{} installation complete", result.version));
inner.task.result = Some(result);
}
Err(error) => {
inner.task.phase = if error.to_ascii_lowercase().contains("cancel") {
"Canceled".to_string()
} else {
"Installation failed".to_string()
};
inner.task.message = Some(error);
inner.task.result = None;
}
}
inner.task.clone()
};
let _ = window.emit(CORE_INSTALL_PROGRESS_EVENT, task);
}
}
impl AppUpdateState {
fn snapshot(&self) -> AppUpdateTask {
self.inner
.lock()
.map(|inner| inner.task.clone())
.unwrap_or_default()
}
fn set_available(&self, task: AppUpdateTask) {
if let Ok(mut inner) = self.inner.lock() {
if !inner.task.running {
inner.task = task;
}
}
}
fn start(&self, token: CancellationToken) -> Result<(), String> {
let mut inner = self
.inner
.lock()
.map_err(|_| "Application update status lock is poisoned".to_string())?;
if inner.task.running {
return Err("An application update task is already running".to_string());
}
inner.token = Some(token);
inner.task = AppUpdateTask {
running: true,
cancellable: true,
phase: "checking".to_string(),
..AppUpdateTask::default()
};
Ok(())
}
fn start_download(&self, pending: &PendingAppUpdate) -> Result<(), String> {
let mut inner = self
.inner
.lock()
.map_err(|_| "Application update status lock is poisoned".to_string())?;
if !inner.task.running || inner.task.phase != "checking" {
return Err("Application update is not in the checking stage".to_string());
}
if inner
.token
.as_ref()
.is_none_or(CancellationToken::is_cancelled)
{
return Err("Application update download canceled".to_string());
}
inner.task = AppUpdateTask {
running: true,
cancellable: true,
phase: "downloading".to_string(),
target_version: Some(pending.version.clone()),
downloaded_bytes: 0,
total_bytes: Some(pending.asset.size_bytes),
percent: Some(0.0),
message: None,
};
Ok(())
}
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
fn update_task<F>(&self, update: F) -> AppUpdateTask
where
F: FnOnce(&mut AppUpdateTask),
{
let Ok(mut inner) = self.inner.lock() else {
return AppUpdateTask::default();
};
update(&mut inner.task);
inner.task.clone()
}
fn finish(&self, phase: &str, message: Option<String>) -> AppUpdateTask {
let Ok(mut inner) = self.inner.lock() else {
return AppUpdateTask::default();
};
inner.task.running = false;
inner.task.cancellable = false;
inner.task.phase = phase.to_string();
inner.task.message = message;
inner.token = None;
inner.task.clone()
}
fn cancel(&self) {
if let Ok(inner) = self.inner.lock() {
if let Some(token) = &inner.token {
token.cancel();
}
}
}
}
impl CoreProcessState {
fn new(starting: bool) -> Self {
Self {
child: Mutex::new(None),
adopted_processes: Mutex::new(Vec::new()),
starting: AtomicBool::new(starting),
shutting_down: AtomicBool::new(false),
shutdown_complete: AtomicBool::new(false),
}
}
fn is_shutting_down(&self) -> bool {
self.shutting_down.load(Ordering::Acquire)
}
fn ensure_active(&self) -> Result<(), String> {
if self.is_shutting_down() {
Err("Application is exiting; kernel operation canceled".to_string())
} else {
Ok(())
}
}
fn is_starting(&self) -> bool {
self.starting.load(Ordering::Acquire)
}
fn set_starting(&self, starting: bool) {
self.starting.store(starting, Ordering::Release);
}
fn managed_pid(&self) -> Option<u32> {
if let Ok(mut child) = self.child.lock() {
if let Some(process) = child.as_mut() {
if let Ok(None) = process.try_wait() {
return Some(process.id());
}
*child = None;
}
}
self.adopted_processes
.lock()
.ok()
.and_then(|mut processes| {
processes.retain(|process| {
process_executable_path(process.process_id)
.as_deref()
.is_some_and(|path| executable_paths_match(&process.binary_path, path))
});
processes.first().map(|process| process.process_id)
})
}
fn take_child(&self) -> Option<CoreChild> {
self.child.lock().ok().and_then(|mut child| child.take())
}
fn adopt_process_ids(&self, binary_path: &Path, process_ids: Vec<u32>) -> Result<(), String> {
let mut adopted = self
.adopted_processes
.lock()
.map_err(|_| "Adopted kernel process status lock is poisoned".to_string())?;
*adopted = process_ids
.into_iter()
.filter(|process_id| is_process_alive(*process_id))
.map(|process_id| AdoptedCoreProcess {
process_id,
binary_path: binary_path.to_path_buf(),
})
.collect();
adopted.sort_unstable_by_key(|process| process.process_id);
adopted.dedup_by_key(|process| process.process_id);
Ok(())
}
fn clear_adopted_processes(&self) -> Result<(), String> {
self.adopted_processes
.lock()
.map(|mut processes| processes.clear())
.map_err(|_| "Adopted kernel process status lock is poisoned".to_string())
}
fn take_adopted_processes(&self) -> Vec<AdoptedCoreProcess> {
self.adopted_processes
.lock()
.map(|mut processes| std::mem::take(&mut *processes))
.unwrap_or_default()
}
fn store_child(&self, child: CoreChild) -> Result<u32, String> {
self.ensure_active()?;
let pid = child.id();
self.clear_adopted_processes()?;
let mut managed_child = self
.child
.lock()
.map_err(|_| "Kernel process status lock is poisoned".to_string())?;
*managed_child = Some(child);
Ok(pid)
}
}
impl MainWindowSizeState {
fn new(size: Option<SavedWindowSize>) -> Self {
Self {
inner: Mutex::new(size),
}
}
fn snapshot(&self) -> Result<Option<SavedWindowSize>, String> {
self.inner
.lock()
.map(|size| *size)
.map_err(|_| "Main window size status lock is poisoned".to_string())
}
fn replace(&self, size: SavedWindowSize) -> Result<(), String> {
let mut current = self
.inner
.lock()
.map_err(|_| "Main window size status lock is poisoned".to_string())?;
*current = Some(size);
Ok(())
}
}
impl GuiConfigState {
fn new(config: GuiConfigFile) -> Self {
Self {
inner: Mutex::new(config),
}
}
fn snapshot(&self) -> Result<GuiConfigFile, String> {
self.inner
.lock()
.map(|config| config.clone())
.map_err(|_| "GUI configuration status lock is poisoned".to_string())
}
fn replace_external(&self, config: GuiConfigFile) -> Result<(), String> {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
*current = config;
Ok(())
}
fn replace_core_settings_external(
&self,
settings: &CoreConfigSettings,
) -> Result<GuiConfigFile, String> {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
let mut config = current.clone();
apply_core_settings_to_gui_config(&mut config, settings);
apply_external_core_proxy_override(&mut config, settings)?;
sanitize_gui_config(&mut config)?;
validate_gui_config(&config)?;
*current = config.clone();
Ok(config)
}
fn update_network(&self, port: u16, allow_lan: bool) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.port = port;
config.allow_lan = allow_lan;
config.host = if allow_lan { "0.0.0.0" } else { "127.0.0.1" }.to_string();
Ok(())
})
}
fn update_network_routing(&self, settings: &GuiConfigFile) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.port = settings.port;
config.allow_lan = settings.allow_lan;
config.host = settings.host.clone();
config.proxy_url = settings.proxy_url.clone();
config.routing_session_affinity = settings.routing_session_affinity;
config.routing_session_affinity_ttl = settings.routing_session_affinity_ttl.clone();
config.disable_cooling = settings.disable_cooling;
config.request_retry = settings.request_retry;
config.max_retry_credentials = settings.max_retry_credentials;
config.max_retry_interval = settings.max_retry_interval;
config.streaming_bootstrap_retries = settings.streaming_bootstrap_retries;
Ok(())
})
}
fn update_network_endpoint(&self, settings: &GuiConfigFile) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.host = settings.host.clone();
config.allow_lan = settings.allow_lan;
config.port = settings.port;
config.proxy_url = settings.proxy_url.clone();
config.proxy_override = settings.proxy_override;
Ok(())
})
}
fn update_retry_settings(&self, settings: &GuiConfigFile) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.disable_cooling = settings.disable_cooling;
config.request_retry = settings.request_retry;
config.max_retry_credentials = settings.max_retry_credentials;
config.max_retry_interval = settings.max_retry_interval;
config.streaming_bootstrap_retries = settings.streaming_bootstrap_retries;
Ok(())
})
}
fn update_session_routing(&self, settings: &GuiConfigFile) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.routing_session_affinity = settings.routing_session_affinity;
config.routing_session_affinity_ttl = settings.routing_session_affinity_ttl.clone();
Ok(())
})
}
fn set_run_on_startup(&self, run_on_startup: bool) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.run_on_startup = run_on_startup;
Ok(())
})
}
fn set_close_behavior(
&self,
close_behavior: WindowsCloseBehavior,
) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.close_behavior = close_behavior;
Ok(())
})
}
fn set_software_preferences(
&self,
close_behavior: WindowsCloseBehavior,
start_core_on_launch: bool,
silent_start: bool,
default_terminal: String,
) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.close_behavior = close_behavior;
config.start_core_on_launch = start_core_on_launch;
config.silent_start = silent_start;
config.default_terminal = normalize_agent_terminal(&default_terminal);
Ok(())
})
}
fn set_locale(&self, locale: String) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.locale = normalize_app_locale(&locale).to_string();
Ok(())
})
}
fn set_download_source(
&self,
download_source: VersionDownloadSource,
) -> Result<GuiConfigFile, String> {
self.set_download_candidate(VersionDownloadCandidate::builtin(download_source))
}
fn set_download_candidate(
&self,
candidate: VersionDownloadCandidate,
) -> Result<GuiConfigFile, String> {
self.update(|config| {
if let Some(url) = &candidate.custom_url {
if !config.custom_download_mirrors.contains(url) {
return Err("Custom mirror does not exist".to_string());
}
config.active_custom_download_mirror = url.clone();
}
config.download_source = candidate.source;
config.prefer_gitcode_downloads = candidate.source == VersionDownloadSource::Gitcode;
Ok(())
})
}
fn switch_download_source_after_failure(
&self,
expected_source: &VersionDownloadCandidate,
fallback_source: &VersionDownloadCandidate,
) -> Result<Option<GuiConfigFile>, String> {
let mut current = self
.inner
.lock()
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
if current.selected_download_candidate() != *expected_source
|| expected_source == fallback_source
{
return Ok(None);
}
let mut config = current.clone();
config.download_source = fallback_source.source;
if let Some(url) = &fallback_source.custom_url {
config.active_custom_download_mirror = url.clone();
}
config.prefer_gitcode_downloads = fallback_source.source == VersionDownloadSource::Gitcode;
sanitize_gui_config(&mut config)?;
write_gui_config(&config)?;
*current = config.clone();
Ok(Some(config))
}
fn set_window_size(&self, size: SavedWindowSize) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.window_width = Some(size.width);
config.window_height = Some(size.height);
Ok(())
})
}
fn set_management_secret_key(&self, secret_key: String) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.management_secret_key = secret_key;
Ok(())
})
}
fn sync_core_settings(&self, settings: &CoreConfigSettings) -> Result<GuiConfigFile, String> {
self.sync_core_settings_internal(settings, None, false, false)
}
fn sync_core_logging_settings(&self, settings: &CoreConfigSettings) -> Result<GuiConfigFile, String> {
self.sync_core_settings_internal(settings, None, false, true)
}
fn sync_core_settings_external(
&self,
settings: &CoreConfigSettings,
) -> Result<GuiConfigFile, String> {
self.sync_core_settings_internal(settings, None, true, false)
}
fn sync_core_settings_with_api_key(
&self,
settings: &CoreConfigSettings,
added_api_key: Option<GuiApiKeyEntry>,
) -> Result<GuiConfigFile, String> {
self.sync_core_settings_internal(settings, added_api_key, false, false)
}
fn sync_core_settings_internal(
&self,
settings: &CoreConfigSettings,
added_api_key: Option<GuiApiKeyEntry>,
apply_external_proxy: bool,
save_usage_preference: bool,
) -> Result<GuiConfigFile, String> {
self.update(|config| {
config.api_keys = merge_core_api_keys_with_gui_metadata(
&config.api_keys,
&settings.api_keys,
added_api_key.as_ref(),
);
config.host = settings.host.clone();
config.port = settings.port;
config.allow_lan = !is_loopback_host(&settings.host);
config.auth_dir = settings.auth_dir.clone();
config.debug = settings.debug;
config.commercial_mode = settings.commercial_mode;
config.logging_to_file = settings.logging_to_file;
config.logs_max_total_size_mb = settings.logs_max_total_size_mb;
config.error_logs_max_files = settings.error_logs_max_files;
config.usage_statistics_enabled = settings.usage_statistics_enabled;
if save_usage_preference {
config.usage_statistics_disabled = (!settings.usage_statistics_enabled).then_some(true);
}
config.redis_usage_queue_retention_seconds =
settings.redis_usage_queue_retention_seconds;
config.request_log = settings.request_log;
if let Some(secret_key) = settings
.management_secret_key
.as_deref()
.filter(|secret_key| !is_hashed_management_secret_key(secret_key))
{
config.management_secret_key = secret_key.to_string();
}
config.plugins_enabled = settings.plugins_enabled;
config.routing_strategy = settings.routing_strategy.clone();
config.routing_session_affinity = settings.routing_session_affinity;
config.routing_session_affinity_ttl = settings.routing_session_affinity_ttl.clone();
config.disable_cooling = settings.disable_cooling;
config.request_retry = settings.request_retry;
config.max_retry_credentials = settings.max_retry_credentials;
config.max_retry_interval = settings.max_retry_interval;
config.streaming_bootstrap_retries = settings.streaming_bootstrap_retries;
if apply_external_proxy {
apply_external_core_proxy_override(config, settings)?;
}
Ok(())
})
}
fn update<F>(&self, update: F) -> Result<GuiConfigFile, String>
where
F: FnOnce(&mut GuiConfigFile) -> Result<(), String>,
{
let mut current = self
.inner
.lock()
.map_err(|_| "GUI configuration status lock is poisoned".to_string())?;
let mut config = current.clone();
update(&mut config)?;
sanitize_gui_config(&mut config)?;
write_gui_config(&config)?;
*current = config.clone();
Ok(config)
}
}
impl From<&GuiConfigFile> for GuiSettings {
fn from(config: &GuiConfigFile) -> Self {
Self {
host: config.host.clone(),
port: config.port,
allow_lan: config.allow_lan,
run_on_startup: config.run_on_startup,
close_behavior: config.close_behavior,
}
}
}
impl From<&GuiConfigFile> for CoreConfigSettings {
fn from(config: &GuiConfigFile) -> Self {
Self {
host: config.host.clone(),
port: config.port,
auth_dir: config.auth_dir.clone(),
api_keys: gui_api_key_values(&config.api_keys),
management_secret_configured: !config.management_secret_key.is_empty(),
debug: config.debug,
commercial_mode: config.commercial_mode,
logging_to_file: config.logging_to_file,
logs_max_total_size_mb: config.logs_max_total_size_mb,
error_logs_max_files: config.error_logs_max_files,
usage_statistics_enabled: config.usage_statistics_enabled,
redis_usage_queue_retention_seconds: config.redis_usage_queue_retention_seconds,
request_log: config.request_log,
plugins_enabled: config.plugins_enabled,
routing_strategy: config.routing_strategy.clone(),
proxy_url: config.proxy_url.clone(),
routing_session_affinity: config.routing_session_affinity,
routing_session_affinity_ttl: config.routing_session_affinity_ttl.clone(),
disable_cooling: config.disable_cooling,
request_retry: config.request_retry,
max_retry_credentials: config.max_retry_credentials,
max_retry_interval: config.max_retry_interval,
streaming_bootstrap_retries: config.streaming_bootstrap_retries,
management_secret_key: Some(config.management_secret_key.clone()),
}
}
}
impl From<&GuiConfigFile> for CoreConfigView {
fn from(config: &GuiConfigFile) -> Self {
Self {
host: config.host.clone(),
api_keys: config
.api_keys
.iter()
.map(|entry| CoreApiKeyView {
api_key: entry.key.clone(),
remark: entry.remark.clone(),
})
.collect(),
management_secret_configured: !config.management_secret_key.is_empty(),
port: config.port,
allow_lan: config.allow_lan,
debug: config.debug,
commercial_mode: config.commercial_mode,
logging_to_file: config.logging_to_file,
logs_max_total_size_mb: config.logs_max_total_size_mb,
error_logs_max_files: config.error_logs_max_files,
usage_statistics_enabled: config.usage_statistics_enabled,
redis_usage_queue_retention_seconds: config.redis_usage_queue_retention_seconds,
request_log: config.request_log,
plugins_enabled: config.plugins_enabled,
routing_strategy: config.routing_strategy.clone(),
proxy_url: config.proxy_url.clone(),
proxy_override: config.proxy_override,
routing_session_affinity: config.routing_session_affinity,
routing_session_affinity_ttl: config.routing_session_affinity_ttl.clone(),
disable_cooling: config.disable_cooling,
request_retry: config.request_retry,
max_retry_credentials: config.max_retry_credentials,
max_retry_interval: config.max_retry_interval,
streaming_bootstrap_retries: config.streaming_bootstrap_retries,
}
}
}
#[derive(Deserialize)]
struct GithubRelease {
tag_name: String,
assets: Vec<GithubAsset>,
}
#[derive(Deserialize)]
struct GithubAsset {
name: String,
browser_download_url: String,
#[serde(default)]
fallback_download_urls: Vec<String>,
size: Option<u64>,
digest: Option<String>,
}
fn main() {
let helper_args = env::args_os().collect::<Vec<_>>();
if antigravity_helper_requested(&helper_args) {
let code = run_antigravity_helper(&helper_args).unwrap_or_else(|error| {
eprintln!("{error}");
1
});
std::process::exit(code);
}
let mut args = env::args_os();
while let Some(argument) = args.next() {
if argument == "--portable-update-helper" {
let result = args
.next()
.map(PathBuf::from)
.ok_or_else(|| "Application update helper is missing its descriptor file".to_string())
.and_then(|path| run_portable_update_helper(&path));
if let Err(error) = result {
eprintln!("{error}");
}
return;
}
}
let _instance_guard = match acquire_app_instance_guard() {
Ok(guard) => guard,
Err(error) => {
eprintln!("{error}");
return;
}
};
let portable_update_ack = portable_update_ack_argument();
let gui_config = match load_or_create_gui_config() {
Ok(config) => config,
Err(error) => {
eprintln!("{error}");
if portable_update_ack.is_some() {
return;
}
let mut config = GuiConfigFile::default();
// If the GUI configuration cannot be read before the kernel starts,
// use the shared recovery key so requests and the generated kernel
// configuration stay synchronized.
config.management_secret_key = LEGACY_DEFAULT_MANAGEMENT_SECRET_KEY.to_string();
if let Err(sanitize_error) = sanitize_gui_config(&mut config) {
eprintln!("Failed to initialize fixed credentials directory: {sanitize_error}");
}
config
}
};
let initial_window_size = configured_window_size(&gui_config);
let start_hidden = should_start_hidden(&gui_config);
let app = tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
None,
))
.plugin(
tauri_plugin_opener::Builder::new()
.open_js_links_on_click(false)
.build(),
)
.manage(CoreDownloadState::default())
.manage(AppUpdateState::default())
.manage(CoreProcessState::new(gui_config.start_core_on_launch))
.manage(DeepSeekHarnessProcessState::default())
.manage(usage::UsageCollectorState::default())
.manage(GuiConfigState::new(gui_config))
.manage(MainWindowSizeState::new(initial_window_size))
.manage(AgentConfigStatusCache::default());
let app = app.on_window_event(|window, event| {
if window.label() != "main" {
return;
}
let observed_size = match event {
tauri::WindowEvent::Resized(physical_size) => {
window.scale_factor().ok().and_then(|scale_factor| {
logical_window_size_from_physical(physical_size, scale_factor)
})
}
tauri::WindowEvent::ScaleFactorChanged {
scale_factor,
new_inner_size,
..
} => logical_window_size_from_physical(new_inner_size, *scale_factor),
_ => None,
};
if let Some(observed_size) = observed_size {
let window_size_state = window.state::<MainWindowSizeState>();
if let Err(error) = window_size_state.replace(observed_size) {
eprintln!("Failed to record main window size: {error}");
}
}
});
#[cfg(target_os = "macos")]
let app = app.on_window_event(|window, event| {
if window.label() == "main" {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
set_macos_dock_visible(window.app_handle(), false);
if let Err(error) = window.hide() {
eprintln!("Failed to hide main window: {error}");
set_macos_dock_visible(window.app_handle(), true);
}
}
}
});
#[cfg(any(target_os = "linux", target_os = "windows"))]
let app = app.on_window_event(|window, event| {
if window.label() == "main" {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
if let Err(error) = window.emit(WINDOWS_CLOSE_REQUEST_EVENT, ()) {
eprintln!("Failed to show desktop close confirmation: {error}");
}
}
}
});
let app = app
.setup(move |app| {
if let Err(error) = network_proxy::refresh(app.state::<GuiConfigState>().inner()) {
eprintln!("Failed to read startup proxy settings: {error}");
}
if let Err(error) = codex_catalog::validate_embedded_catalog() {
eprintln!("Invalid bundled Codex model catalog: {error}");
}
if let Err(error) = load_codex_model_catalog_override(app.handle()) {
eprintln!("Failed to load Codex model catalog update file; using bundled catalog: {error}");
}
let catalog_update_app = app.handle().clone();
if let Err(error) = load_codex_model_customizations(app.handle()) {
eprintln!("Failed to load Codex custom model configuration: {error}");
}
tauri::async_runtime::spawn(async move {
if let Err(error) = update_codex_model_catalog_inner(&catalog_update_app).await {
eprintln!("Failed to update Codex model catalog in the background; continuing with current catalog: {error}");
}
});
if let Err(error) = restore_main_window_size(app.handle()) {
eprintln!("{error}");
}
#[cfg(target_os = "macos")]
setup_macos_tray(app)?;
#[cfg(target_os = "linux")]
if let Err(error) = setup_windows_tray(app) {
eprintln!(
"Failed to set up Linux tray; keeping the main window available: {error}"
);
}
#[cfg(target_os = "windows")]
setup_windows_tray(app)?;
if let Err(error) = configure_initial_main_window(app.handle(), start_hidden) {
eprintln!("Failed to configure startup window state: {error}");
}
if let Err(error) =
configuration_watcher::start_configuration_file_watcher(app.handle().clone())
{
eprintln!("Failed to start configuration file watcher: {error}");
}
network_proxy::start_monitor(app.handle().clone());
start_codex_model_catalog_sync(app.handle().clone());
let usage_app = app.handle().clone();
tauri::async_runtime::spawn_blocking(move || {
if let Err(error) = usage::initialize_usage_storage() {
eprintln!("Failed to initialize usage records directory: {error}");
}
usage::start_usage_collector(usage_app);
});
let agent_status_app = app.handle().clone();
tauri::async_runtime::spawn_blocking(move || {
let gui_config_state = agent_status_app.state::<GuiConfigState>();
let cache = agent_status_app.state::<AgentConfigStatusCache>();
if let Err(error) = refresh_agent_config_status_cache(
&agent_status_app,
gui_config_state.inner(),
cache.inner(),
) {
eprintln!("Failed to refresh agent configuration status in the background: {error}");
}
});
let core_app = app.handle().clone();
tauri::async_runtime::spawn_blocking(move || {
let Ok(_guard) = CORE_OPERATION_LOCK.lock() else {
return;
};
let gui_config_state = core_app.state::<GuiConfigState>();
let process_state = core_app.state::<CoreProcessState>();
if process_state.ensure_active().is_err() {
process_state.set_starting(false);
return;
}
let Ok(config) = gui_config_state.snapshot() else {
process_state.set_starting(false);
return;
};
if config.start_core_on_launch {
if let Ok(status) =
current_core_status(Some(process_state.inner()), Some(config.port))
{
emit_core_status(&core_app, &status);
}
}
match auto_install_bundled_core_if_needed(&core_app) {
Ok(true) => eprintln!("Automatically installed or upgraded to the CPA kernel version bundled with the application"),
Ok(false) => {}
Err(error) => eprintln!("Failed to automatically install CPA offline kernel: {error}"),
}
let adopted_process_ids = match adopt_existing_core_processes(process_state.inner())
{
Ok(process_ids) => process_ids,
Err(error) => {
eprintln!("Failed to scan for and adopt the CPA kernel in the current directory: {error}");
Vec::new()
}
};
if !adopted_process_ids.is_empty() {
eprintln!(
"Adopted running CPA kernel in the current directory: PID {}",
adopted_process_ids
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ")
);
} else if should_start_core_on_launch(&config) {
if let Err(error) = start_core_process_inner(process_state.inner(), &config) {
eprintln!("Failed to automatically start CPA kernel: {error}");
}
}
process_state.set_starting(false);
if let Ok(status) =
current_core_status(Some(process_state.inner()), Some(config.port))
{
emit_core_status(&core_app, &status);
}
});
if let Some(ack_path) = portable_update_ack.as_ref() {
fs::write(ack_path, env!("CARGO_PKG_VERSION").as_bytes())
.map_err(|error| format!("Failed to write application update startup confirmation: {error}"))?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
desktop_theme::get_linux_system_theme,
health_check,
detect_core_platform,
get_core_status,
get_gui_settings,
get_usage_view_preferences,
save_usage_view_preferences,
resolve_api_access_remarks,
save_api_access_remark,
set_app_locale,
resolve_windows_close_request,
get_software_settings,
save_software_settings,
get_agent_config_statuses,
create_agent_config_backup,
list_agent_config_backups,
preview_agent_config_backup,
restore_agent_config_backup,
delete_agent_config_backup,
preview_agent_config_template,
apply_agent_config_template,
refresh_agent_config_statuses,
get_agent_models,
check_pi_provider_update,
install_pi_provider,
update_pi_provider,
repair_pi_provider,
uninstall_pi_provider,
check_codex_oauth_login,
update_codex_model_catalog,
get_deepseek_harness_model_catalog_editor,
save_deepseek_harness_model_catalog_editor,
get_codex_model_catalog_editor,
save_codex_model_catalog_editor,
get_thinking_aliases,
get_model_alias_sources,
get_model_alias_edit_source,
get_thinking_alias_sources,
create_thinking_alias,
delete_thinking_alias,
get_speed_aliases,
get_speed_alias_sources,
create_speed_alias,
delete_speed_alias,
apply_agent_config,
clear_codex_config,
restore_codex_official_config,
close_codex_config_modification,
set_agent_config_enabled,
update_agent_config,
launch_agent,
get_deepseek_harness_process_status,
stop_deepseek_harness_process,
restart_deepseek_harness_process,
restart_agent_app,
restart_codex_app,
restart_opencode_app,
get_lan_ipv4,
save_gui_settings,
save_network_routing_settings,
save_network_endpoint_settings,
save_retry_settings,
save_session_routing_settings,
get_core_tls_settings,
save_core_tls_settings,
get_core_sensitive_words_settings,
save_core_sensitive_words_settings,
get_core_config_settings,
get_extended_core_config,
save_extended_core_config,
save_core_logging_settings,
set_core_request_log,
add_core_api_key,
update_core_api_key,
delete_core_api_key,
set_core_management_secret_key,
clear_core_management_secret_key,
management_api::management_request,
plugins::get_plugin_support,
plugins::get_plugin_resource_url,
provider_health::provider_health_probe,
provider_health::get_core_models,
provider_health::core_health_probe,
management_api::upload_auth_file,
management_api::open_auth_files_directory,
management_api::open_core_logs_directory,
set_core_plugins_enabled,
set_core_routing_strategy,
set_core_proxy_url,
set_core_session_affinity,
set_core_session_affinity_ttl,
management_api::start_oauth_login,
management_api::get_oauth_status,
management_api::submit_oauth_callback,
list_oauth_browsers,
open_oauth_url,
open_external_url,
get_version_source_settings,
set_download_source,
add_custom_download_mirror,
remove_custom_download_mirror,
set_prefer_gitcode_downloads,
check_app_update,
get_app_update_task,
start_app_update,
cancel_app_update,
check_latest_core,
detect_bundled_core,
install_core_version,
install_bundled_core,
cancel_core_install,
get_core_install_task,
usage::get_usage_collector_status,
usage::get_usage_overview,
usage::get_usage_analysis,
usage::get_usage_events,
usage::save_usage_events_export,
usage::get_usage_pricing,
usage::get_usage_storage_settings,
usage::repair_usage_cache_records,
usage::save_usage_storage_settings,
usage::shrink_usage_database,
usage::save_usage_model_price,
usage::delete_usage_model_price,
usage::preview_usage_model_prices,
usage::apply_usage_model_prices,
start_core_process,
stop_core_process,
restart_core_process,
codex_sessions::list_codex_sessions,
codex_sessions::delete_codex_sessions,
codex_sessions::repair_codex_session_metadata,
codex_sessions::preview_codex_session_index_cleanup,
codex_sessions::apply_codex_session_index_cleanup
])
.build(tauri::generate_context!())
.expect("failed to build app");
app.run(|app_handle, event| match event {
#[cfg(target_os = "macos")]
tauri::RunEvent::Reopen {
has_visible_windows: false,
..
} => show_main_window(app_handle),
tauri::RunEvent::ExitRequested { api, code, .. } => {
let process_state = app_handle.state::<CoreProcessState>();
if process_state.shutdown_complete.load(Ordering::Acquire) {
return;
}
api.prevent_exit();
if process_state.shutting_down.swap(true, Ordering::AcqRel) {
return;
}
if let Err(error) = persist_main_window_size(app_handle) {
eprintln!("Failed to save main window size: {error}");
}
app_handle.state::<CoreDownloadState>().cancel();
let app_handle = app_handle.clone();
tauri::async_runtime::spawn_blocking(move || {
let _guard = CORE_OPERATION_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner());
let process_state = app_handle.state::<CoreProcessState>();
let gui_config_state = app_handle.state::<GuiConfigState>();
shutdown_managed_core(process_state.inner(), gui_config_state.inner());
process_state
.shutdown_complete
.store(true, Ordering::Release);
app_handle.exit(code.unwrap_or(0));
});
}
tauri::RunEvent::Exit => {
usage::stop_usage_collector(app_handle);
let deepseek_process_state = app_handle.state::<DeepSeekHarnessProcessState>();
if let Err(error) = stop_managed_deepseek_harness(deepseek_process_state.inner()) {
eprintln!("Failed to stop DeepSeek Harness: {error}");
}
let gui_config_state = app_handle.state::<GuiConfigState>();
let process_state = app_handle.state::<CoreProcessState>();
if !process_state.shutdown_complete.load(Ordering::Acquire) {
process_state.shutting_down.store(true, Ordering::Release);
shutdown_managed_core(process_state.inner(), gui_config_state.inner());
}
}
_ => {}
});
}
#[cfg(test)]
mod tests;