From 0765d1ab2c2c85f336d4bcdb2ed8498fe5b7fe16 Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Wed, 27 May 2026 23:20:53 +0800 Subject: [PATCH 1/8] feat: add Antigravity agent support - Add AgentKind enum (Codex/Antigravity) with per-agent paths, process detection, and CDP target matching - Update agent.rs to accept AgentKind for all functions (launch, kill, port detection) - Update cdp.rs find_main_target to dispatch on agent kind (Codex: app://-/index.html, Antigravity: https://127.0.0.1:*/ with title Antigravity) - Add generate_antigravity_injection_script targeting #root, .bg-sidebar, .bg-background, and Antigravity CSS vars - Add set_selected_agent and set_auto_launch Tauri commands - Add agent selector UI (Codex/Antigravity toggle buttons) in frontend status bar - Agent switching auto-applies the current theme to the new agent --- src-tauri/Cargo.lock | 42 +- src-tauri/src/agent.rs | 103 ++-- src-tauri/src/cdp.rs | 46 +- src-tauri/src/config.rs | 93 ++++ src-tauri/src/lib.rs | 53 +- src-tauri/src/theme.rs | 107 +++- web/app.js | 115 +++-- web/dist/bundle.js | 1043 ++++++++++++++++++++------------------- web/dist/index.html | 84 ++-- web/dist/style.css | 41 +- web/index.html | 62 ++- web/style.css | 34 ++ 12 files changed, 1121 insertions(+), 702 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9178906..3096742 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8,6 +8,27 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "agent-theme-companion" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "dirs", + "futures-util", + "lazy_static", + "log", + "reqwest", + "serde", + "serde_json", + "sysinfo", + "tauri", + "tauri-build", + "tauri-plugin-log", + "tauri-plugin-single-instance", + "tokio", + "tokio-tungstenite", +] + [[package]] name = "ahash" version = "0.7.8" @@ -608,27 +629,6 @@ dependencies = [ "cc", ] -[[package]] -name = "codex-theme-companion" -version = "0.1.0" -dependencies = [ - "base64 0.22.1", - "dirs", - "futures-util", - "lazy_static", - "log", - "reqwest", - "serde", - "serde_json", - "sysinfo", - "tauri", - "tauri-build", - "tauri-plugin-log", - "tauri-plugin-single-instance", - "tokio", - "tokio-tungstenite", -] - [[package]] name = "combine" version = "4.6.7" diff --git a/src-tauri/src/agent.rs b/src-tauri/src/agent.rs index 2f7f392..b657ebd 100644 --- a/src-tauri/src/agent.rs +++ b/src-tauri/src/agent.rs @@ -4,34 +4,41 @@ use std::process::Command; use sysinfo::System; use std::time::Duration; use tokio::time::sleep; +use crate::config::AgentKind; -pub fn get_agent_data_dir() -> PathBuf { +pub fn get_agent_data_dir(kind: &AgentKind) -> PathBuf { let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("~")); home.join("Library") .join("Application Support") - .join("Codex") + .join(kind.data_dir_name()) } -pub fn get_devtools_port_file() -> PathBuf { - get_agent_data_dir().join("DevToolsActivePort") +pub fn get_devtools_port_file(kind: &AgentKind) -> PathBuf { + get_agent_data_dir(kind).join("DevToolsActivePort") } -pub fn clean_locks() { - log::info!("Cleaning Agent locks..."); - let dir = get_agent_data_dir(); +pub fn clean_locks(kind: &AgentKind) { + log::info!("Cleaning {:?} locks...", kind); + let dir = get_agent_data_dir(kind); let files = ["SingletonLock", "SingletonCookie", "SingletonSocket", "DevToolsActivePort"]; for f in files { let _ = fs::remove_file(dir.join(f)); } } -pub fn is_agent_process_running() -> bool { +pub fn is_agent_process_running(kind: &AgentKind) -> bool { let mut sys = System::new_all(); sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); + let name_patterns = kind.process_name_patterns(); + let bin_patterns = kind.binary_path_patterns(); for (_pid, process) in sys.processes() { - let name_match = process.name().to_string_lossy().contains("Codex"); + let pname = process.name().to_string_lossy(); + let name_match = name_patterns.iter().any(|p| pname.contains(p)); let exe_match = process.exe() - .map(|e| e.to_string_lossy().contains("/Applications/Codex.app/")) + .map(|e| { + let estr = e.to_string_lossy(); + bin_patterns.iter().any(|p| estr.contains(p)) + }) .unwrap_or(false); if name_match || exe_match { return true; @@ -40,23 +47,23 @@ pub fn is_agent_process_running() -> bool { false } -pub fn force_kill_agent() { - log::info!("Force killing existing Agent processes..."); - let _ = Command::new("pkill").arg("-9").arg("-f").arg("/Applications/Codex\\.app/").status(); - let _ = Command::new("pkill").arg("-9").arg("-f").arg("SkyComputerUseClient").status(); - let _ = Command::new("pkill").arg("-9").arg("-f").arg("Codex Computer Use\\.app").status(); +pub fn force_kill_agent(kind: &AgentKind) { + log::info!("Force killing {:?} processes...", kind); + for pattern in kind.pkill_patterns() { + let _ = Command::new("pkill").arg("-9").arg("-f").arg(pattern).status(); + } // Wait brief moment for _ in 0..10 { - if !is_agent_process_running() { + if !is_agent_process_running(kind) { break; } std::thread::sleep(Duration::from_millis(100)); } } -pub fn read_port_from_file() -> Option { - let file = get_devtools_port_file(); +pub fn read_port_from_file(kind: &AgentKind) -> Option { + let file = get_devtools_port_file(kind); if !file.exists() { return None; } @@ -72,39 +79,46 @@ pub fn read_port_from_file() -> Option { None } -pub async fn launch_agent(force_clean: bool) -> Result { +pub async fn launch_agent(kind: &AgentKind, force_clean: bool) -> Result { if force_clean { - force_kill_agent(); - clean_locks(); - } else if is_agent_process_running() { - if let Some(active_port) = read_port_from_file() { - // Test if responsive - let url = format!("http://127.0.0.1:{}/json/list", active_port); - if reqwest::get(&url).await.is_ok() { - log::info!("Agent is already running and listening on port {}", active_port); + force_kill_agent(kind); + clean_locks(kind); + } else if is_agent_process_running(kind) { + if let Some(active_port) = read_port_from_file(kind) { + // Test if responsive via the browser WebSocket endpoint + let version_url = format!("http://127.0.0.1:{}/json/version", active_port); + if reqwest::get(&version_url).await.is_ok() { + log::info!("{:?} is already running and listening on port {}", kind, active_port); + return Ok(active_port); + } + // Also try /json/list (works for both Codex and Antigravity) + let list_url = format!("http://127.0.0.1:{}/json/list", active_port); + if reqwest::get(&list_url).await.is_ok() { + log::info!("{:?} is already running and listening on port {}", kind, active_port); return Ok(active_port); } } - log::info!("Agent process found but debug port is unresponsive. Restarting..."); - force_kill_agent(); - clean_locks(); + log::info!("{:?} process found but debug port is unresponsive. Restarting...", kind); + force_kill_agent(kind); + clean_locks(kind); } else { - clean_locks(); + clean_locks(kind); } - log::info!("Starting Agent App..."); - let mut child = Command::new("/Applications/Codex.app/Contents/MacOS/Codex") + let binary = kind.binary_path(); + log::info!("Starting {:?} at {:?}...", kind, binary); + let mut child = Command::new(&binary) .arg("--remote-debugging-port=0") .arg("--remote-allow-origins=*") .spawn() - .map_err(|e| format!("Failed to start Agent: {}", e))?; + .map_err(|e| format!("Failed to start {:?}: {}", kind, e))?; // Polling for DevTools port for _ in 1..=30 { - if let Some(port) = read_port_from_file() { + if let Some(port) = read_port_from_file(kind) { let url = format!("http://127.0.0.1:{}/json/list", port); if reqwest::get(&url).await.is_ok() { - log::info!("Bound to port {}", port); + log::info!("{:?} bound to port {}", kind, port); return Ok(port); } } @@ -112,5 +126,20 @@ pub async fn launch_agent(force_clean: bool) -> Result { } let _ = child.kill(); - Err("Timeout waiting for Agent DevToolsActivePort to become available".to_string()) + Err(format!("Timeout waiting for {:?} DevToolsActivePort to become available", kind)) +} + +/// Convenience: read the selected agent kind from the current config. +pub fn current_kind() -> AgentKind { + crate::config::load_config().selected_agent +} + +/// Convenience: detect if the selected agent's process is running. +pub fn is_current_running() -> bool { + is_agent_process_running(¤t_kind()) +} + +/// Convenience: get the DevTools port for the selected agent. +pub fn current_port() -> Option { + read_port_from_file(¤t_kind()) } diff --git a/src-tauri/src/cdp.rs b/src-tauri/src/cdp.rs index 61038e7..d163064 100644 --- a/src-tauri/src/cdp.rs +++ b/src-tauri/src/cdp.rs @@ -2,9 +2,11 @@ use serde::Deserialize; use serde_json::Value; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; -use tokio_tungstenite::connect_async; +use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; use tokio_tungstenite::tungstenite::Message; +use tokio::net::TcpStream; use futures_util::{StreamExt, SinkExt}; +use crate::config::AgentKind; #[derive(Debug, Deserialize)] pub struct Target { @@ -31,20 +33,28 @@ pub async fn list_targets(port: u16) -> Result, String> { Ok(targets) } -pub fn find_main_target(targets: &[Target]) -> Option<&Target> { - // Try to find the exact Codex main window by URL - if let Some(main) = targets.iter().find(|t| t.url == "app://-/index.html" && t.target_type == "page") { - return Some(main); +/// Find the main window target for the given agent kind. +pub fn find_main_target<'a>(targets: &'a [Target], kind: &AgentKind) -> Option<&'a Target> { + match kind { + AgentKind::Codex => { + if let Some(main) = targets.iter().find(|t| t.url == "app://-/index.html" && t.target_type == "page") { + return Some(main); + } + targets.iter().find(|t| t.target_type == "page" && t.title == "Codex") + } + AgentKind::Antigravity => { + if let Some(main) = targets.iter().find(|t| t.url.starts_with("https://127.0.0.1:") && t.target_type == "page" && t.title == "Antigravity") { + return Some(main); + } + targets.iter().find(|t| t.target_type == "page" && t.title.contains("Antigravity")) + } } - - // Fallback to exact title match - targets.iter().find(|t| t.target_type == "page" && t.title == "Codex") } static NEXT_ID: AtomicUsize = AtomicUsize::new(1); async fn make_cdp_request( - ws_stream: &mut tokio_tungstenite::WebSocketStream>, + ws_stream: &mut WebSocketStream>, method: &str, params: Value, ) -> Result { @@ -84,12 +94,12 @@ async fn make_cdp_request( .map_err(|_| format!("CDP request timeout (method: {})", method))? } -pub async fn inject_theme(port: u16, script: &str) -> Result { +pub async fn inject_theme(port: u16, kind: &AgentKind, script: &str) -> Result { let targets = list_targets(port).await?; - let target = find_main_target(&targets).ok_or("Could not find Agent main window target")?; + let target = find_main_target(&targets, kind).ok_or("Could not find Agent main window target")?; let ws_url = target.web_socket_debugger_url.as_ref().ok_or("Target has no WebSocket URL")?; - let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| format!("WebSocket connect failed: {}", e))?; + let (mut ws_stream, _): (WebSocketStream>, _) = connect_async(ws_url.as_str()).await.map_err(|e| format!("WebSocket connect failed: {}", e))?; make_cdp_request(&mut ws_stream, "Page.enable", serde_json::json!({})).await?; make_cdp_request(&mut ws_stream, "Runtime.evaluate", serde_json::json!({ "expression": script })).await?; @@ -105,12 +115,12 @@ pub async fn inject_theme(port: u16, script: &str) -> Result { Ok(identifier) } -pub async fn clear_theme(port: u16, identifier: Option<&str>) -> Result<(), String> { +pub async fn clear_theme(port: u16, kind: &AgentKind, identifier: Option<&str>) -> Result<(), String> { let targets = list_targets(port).await?; - let target = find_main_target(&targets).ok_or("Could not find Agent main window target")?; + let target = find_main_target(&targets, kind).ok_or("Could not find Agent main window target")?; let ws_url = target.web_socket_debugger_url.as_ref().ok_or("Target has no WebSocket URL")?; - let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| format!("WebSocket connect failed: {}", e))?; + let (mut ws_stream, _): (WebSocketStream>, _) = connect_async(ws_url.as_str()).await.map_err(|e| format!("WebSocket connect failed: {}", e))?; let clear_script = r#" (function() { @@ -131,12 +141,12 @@ pub async fn clear_theme(port: u16, identifier: Option<&str>) -> Result<(), Stri Ok(()) } -pub async fn reload_page(port: u16) -> Result<(), String> { +pub async fn reload_page(port: u16, kind: &AgentKind) -> Result<(), String> { let targets = list_targets(port).await?; - let target = find_main_target(&targets).ok_or("Could not find Agent main window target")?; + let target = find_main_target(&targets, kind).ok_or("Could not find Agent main window target")?; let ws_url = target.web_socket_debugger_url.as_ref().ok_or("Target has no WebSocket URL")?; - let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| format!("WebSocket connect failed: {}", e))?; + let (mut ws_stream, _): (WebSocketStream>, _) = connect_async(ws_url.as_str()).await.map_err(|e| format!("WebSocket connect failed: {}", e))?; make_cdp_request(&mut ws_stream, "Page.enable", serde_json::json!({})).await?; make_cdp_request(&mut ws_stream, "Page.reload", serde_json::json!({})).await?; diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 4cbcdc6..c64c34e 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,9 +1,99 @@ use serde::{Deserialize, Serialize}; +use std::fmt; use std::fs; use std::path::PathBuf; use std::sync::Mutex; use lazy_static::lazy_static; +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AgentKind { + Codex, + Antigravity, +} + +impl Default for AgentKind { + fn default() -> Self { + AgentKind::Codex + } +} + +impl fmt::Display for AgentKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AgentKind::Codex => write!(f, "Codex"), + AgentKind::Antigravity => write!(f, "Antigravity"), + } + } +} + +impl AgentKind { + pub fn display_name_zh(&self) -> &str { + match self { + AgentKind::Codex => "Codex", + AgentKind::Antigravity => "Antigravity", + } + } + + pub fn display_name_en(&self) -> &str { + match self { + AgentKind::Codex => "Codex", + AgentKind::Antigravity => "Antigravity", + } + } + + /// Data directory name under ~/Library/Application Support/ + pub fn data_dir_name(&self) -> &str { + match self { + AgentKind::Codex => "Codex", + AgentKind::Antigravity => "Antigravity", + } + } + + /// Binary path inside the .app bundle + pub fn binary_path(&self) -> PathBuf { + let app_name = match self { + AgentKind::Codex => "Codex.app", + AgentKind::Antigravity => "Antigravity.app", + }; + PathBuf::from("/Applications") + .join(app_name) + .join("Contents") + .join("MacOS") + .join(self.data_dir_name()) + } + + /// Process name patterns for detection + pub fn process_name_patterns(&self) -> Vec<&'static str> { + match self { + AgentKind::Codex => vec!["Codex"], + AgentKind::Antigravity => vec!["Antigravity"], + } + } + + /// Binary path patterns for detection + pub fn binary_path_patterns(&self) -> Vec<&'static str> { + match self { + AgentKind::Codex => vec!["/Applications/Codex.app/"], + AgentKind::Antigravity => vec!["/Applications/Antigravity.app/"], + } + } + + /// pkill patterns for force kill + pub fn pkill_patterns(&self) -> Vec<&'static str> { + match self { + AgentKind::Codex => vec![ + "/Applications/Codex\\.app/", + "SkyComputerUseClient", + "Codex Computer Use\\.app", + ], + AgentKind::Antigravity => vec![ + "/Applications/Antigravity\\.app/", + ], + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AppConfig { @@ -11,6 +101,8 @@ pub struct AppConfig { pub selected_theme_id: String, pub auto_launch_agent: bool, pub active_identifier: Option, + #[serde(default)] + pub selected_agent: AgentKind, } impl Default for AppConfig { @@ -20,6 +112,7 @@ impl Default for AppConfig { selected_theme_id: "carton".to_string(), auto_launch_agent: true, active_identifier: None, + selected_agent: AgentKind::default(), } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f8c0ad4..044f6fb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,7 +5,7 @@ pub mod theme; use tauri::{AppHandle, State, Manager}; use std::sync::Mutex; -use crate::config::{AppConfig, update_config, load_config}; +use crate::config::{AppConfig, AgentKind, update_config, load_config}; use crate::theme::{get_themes, get_theme, save_custom_theme, delete_custom_theme, generate_injection_script, Theme}; struct AppState { @@ -23,37 +23,54 @@ async fn set_enabled(enabled: bool) -> Result { Ok(update_config(|c| c.enabled = enabled)) } +#[tauri::command] +async fn set_selected_agent(agent: AgentKind) -> Result { + Ok(update_config(|c| c.selected_agent = agent)) +} + +#[tauri::command] +async fn set_auto_launch(enabled: bool) -> Result { + Ok(update_config(|c| c.auto_launch_agent = enabled)) +} + #[tauri::command] async fn get_agent_status(state: State<'_, AppState>) -> Result { - let is_running = agent::is_agent_process_running(); + let config = load_config(); + let kind = &config.selected_agent; + let is_running = agent::is_agent_process_running(kind); let port = *state.cdp_port.lock().unwrap(); Ok(serde_json::json!({ "running": is_running, - "cdpPort": port + "cdpPort": port, + "agent": kind.to_string().to_lowercase() })) } #[tauri::command] async fn restart_agent(state: State<'_, AppState>) -> Result<(), String> { - let port = agent::launch_agent(true).await?; + let config = load_config(); + let kind = config.selected_agent; + let port = agent::launch_agent(&kind, true).await?; *state.cdp_port.lock().unwrap() = Some(port); Ok(()) } #[tauri::command] async fn apply_theme(app: AppHandle, state: State<'_, AppState>, theme_id: String) -> Result<(), String> { + let config = load_config(); + let kind = config.selected_agent; let theme = get_theme(&app, &theme_id).ok_or("Theme not found")?; // Read port once, release lock before any async work let need_launch = { let p = state.cdp_port.lock().unwrap(); - p.is_none() || !agent::is_agent_process_running() + p.is_none() || !agent::is_agent_process_running(&kind) }; if need_launch { - let config = load_config(); - if config.auto_launch_agent { - let new_port = agent::launch_agent(false).await?; + let cfg = load_config(); + if cfg.auto_launch_agent { + let new_port = agent::launch_agent(&kind, false).await?; *state.cdp_port.lock().unwrap() = Some(new_port); } } @@ -61,9 +78,9 @@ async fn apply_theme(app: AppHandle, state: State<'_, AppState>, theme_id: Strin let port = *state.cdp_port.lock().unwrap(); if let Some(p) = port { - let script = generate_injection_script(&theme)?; + let script = generate_injection_script(&theme, &kind)?; - let identifier = cdp::inject_theme(p, &script).await?; + let identifier = cdp::inject_theme(p, &kind, &script).await?; update_config(|c| { c.enabled = true; @@ -80,12 +97,13 @@ async fn apply_theme(app: AppHandle, state: State<'_, AppState>, theme_id: Strin #[tauri::command] async fn clear_theme(state: State<'_, AppState>) -> Result<(), String> { let config = load_config(); + let kind = config.selected_agent; let port = *state.cdp_port.lock().unwrap(); if let Some(p) = port { - if agent::is_agent_process_running() { + if agent::is_agent_process_running(&kind) { let ident = state.active_identifier.lock().unwrap().clone().or(config.active_identifier); - cdp::clear_theme(p, ident.as_deref()).await?; + cdp::clear_theme(p, &kind, ident.as_deref()).await?; update_config(|c| { c.enabled = false; @@ -129,6 +147,8 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ get_config, set_enabled, + set_selected_agent, + set_auto_launch, get_agent_status, restart_agent, apply_theme, @@ -140,20 +160,21 @@ pub fn run() { .setup(|app| { // Check auto launch let config = load_config(); + let kind = config.selected_agent; let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { if config.auto_launch_agent { - log::info!("Auto-launch enabled, checking Agent..."); - if let Ok(port) = agent::launch_agent(false).await { + log::info!("Auto-launch enabled, checking {:?}...", kind); + if let Ok(port) = agent::launch_agent(&kind, false).await { let state: State<'_, AppState> = app_handle.state(); *state.cdp_port.lock().unwrap() = Some(port); if config.enabled { log::info!("Auto-applying theme {}", config.selected_theme_id); if let Some(theme) = get_theme(&app_handle, &config.selected_theme_id) { - if let Ok(script) = generate_injection_script(&theme) { - if let Ok(ident) = cdp::inject_theme(port, &script).await { + if let Ok(script) = generate_injection_script(&theme, &kind) { + if let Ok(ident) = cdp::inject_theme(port, &kind, &script).await { update_config(|c| { c.active_identifier = Some(ident.clone()); }); *state.active_identifier.lock().unwrap() = Some(ident); } diff --git a/src-tauri/src/theme.rs b/src-tauri/src/theme.rs index 4692cf9..7040e54 100644 --- a/src-tauri/src/theme.rs +++ b/src-tauri/src/theme.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use base64::{Engine as _, engine::general_purpose::STANDARD as base64_engine}; -use crate::config::get_config_dir; +use crate::config::{get_config_dir, AgentKind}; use tauri::AppHandle; use tauri::Manager; @@ -130,7 +130,15 @@ fn parse_base64_data_uri(uri: &str) -> Result, String> { base64_engine.decode(parts[1]).map_err(|e| e.to_string()) } -pub fn generate_injection_script(theme: &Theme) -> Result { +/// Dispatch to the correct injection script based on agent kind. +pub fn generate_injection_script(theme: &Theme, kind: &AgentKind) -> Result { + match kind { + AgentKind::Codex => generate_codex_injection_script(theme), + AgentKind::Antigravity => generate_antigravity_injection_script(theme), + } +} + +fn generate_codex_injection_script(theme: &Theme) -> Result { let bg_path = theme.dir.join(&theme.background); let bg_bytes = fs::read(&bg_path).map_err(|e| format!("Failed to read background {:?}: {}", bg_path, e))?; let bg_ext = if theme.background.ends_with(".png") { "png" } else { "jpeg" }; @@ -175,7 +183,7 @@ pub fn generate_injection_script(theme: &Theme) -> Result { --sk-background: transparent !important; }} - /* 覆盖整个界面的深色透明遮罩 */ + /* Dark overlay covering the entire UI */ #sky-root::before {{ content: ''; position: fixed; @@ -214,3 +222,96 @@ pub fn generate_injection_script(theme: &Theme) -> Result { Ok(script) } + +fn generate_antigravity_injection_script(theme: &Theme) -> Result { + let bg_path = theme.dir.join(&theme.background); + let bg_bytes = fs::read(&bg_path).map_err(|e| format!("Failed to read background {:?}: {}", bg_path, e))?; + let bg_ext = if theme.background.ends_with(".png") { "png" } else { "jpeg" }; + let bg_data_uri = format!("data:image/{};base64,{}", bg_ext, base64_engine.encode(&bg_bytes)); + + // Antigravity DOM structure (observed via CDP): + // body.theme-standalone.theme-light + // div#root > div.relative.w-screen.h-screen > ... > div.flex.flex-col > ... + // Sidebar: div[role="navigation"][aria-label="Sidebar"] with .bg-sidebar + // Main content: .bg-background, .text-foreground + // CSS vars: --foreground, --background, --sidebar + let script = format!(r#" + (function() {{ + const existingStyle = document.getElementById('agent-theme-style'); + if (existingStyle) existingStyle.remove(); + + const style = document.createElement('style'); + style.id = 'agent-theme-style'; + style.textContent = ` + body {{ + background-image: url('{}') !important; + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-attachment: fixed !important; + }} + + /* Transparent background for all nested containers */ + #root, + #root > div, + #root > div > div, + #root > div > div > div {{ + background-color: transparent !important; + }} + + /* Sidebar glass effect */ + [role="navigation"][aria-label="Sidebar"], + .bg-sidebar {{ + background-color: rgba(26, 26, 26, 0.75) !important; + backdrop-filter: blur(12px) !important; + -webkit-backdrop-filter: blur(12px) !important; + border-right: 1px solid rgba(255, 255, 255, 0.1) !important; + }} + + /* Main content area glass effect */ + .bg-background {{ + background-color: transparent !important; + }} + + /* Override Antigravity CSS variables for dark translucent look */ + :root {{ + --background: rgba(0, 0, 0, 0) !important; + --foreground: rgba(255, 255, 255, 0.95) !important; + --sidebar: rgba(26, 26, 26, 0.75) !important; + }} + + /* Glass panels: dialogs, modals */ + [role="dialog"], + .bg-card {{ + background-color: rgba(26, 26, 26, 0.75) !important; + backdrop-filter: blur(12px) !important; + -webkit-backdrop-filter: blur(12px) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + }} + + /* Dark overlay */ + #root::before {{ + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.4) !important; + pointer-events: none; + z-index: 1; + }} + + #root > * {{ + position: relative; + z-index: 2; + }} + `; + document.head.appendChild(style); + + console.log('Antigravity theme applied successfully.'); + }})(); + "#, bg_data_uri); + + Ok(script) +} diff --git a/web/app.js b/web/app.js index 3ca8a09..647991c 100644 --- a/web/app.js +++ b/web/app.js @@ -24,6 +24,8 @@ const btnStartAgent = document.getElementById('btn-start-agent'); const btnRestartAgent = document.getElementById('btn-restart-agent'); const themesGrid = document.getElementById('themes-grid'); const notificationText = document.getElementById('notification-text'); +const btnAgentCodex = document.getElementById('btn-agent-codex'); +const btnAgentAntigravity = document.getElementById('btn-agent-antigravity'); // Modal Elements const uploadModal = document.getElementById('upload-modal'); @@ -44,37 +46,6 @@ function notify(text, type = 'info') { } // Fetch Status -async function refreshStatus() { - try { - const status = await invoke('get_agent_status'); - const config = await invoke('get_config'); - appConfig = config; - - // Update Agent Process status - if (status.running) { - statusDot.className = 'dot online'; - statusText.textContent = '正在运行'; - btnStartAgent.disabled = true; - } else { - statusDot.className = 'dot offline'; - statusText.textContent = '未运行'; - btnStartAgent.disabled = false; - } - - // Update CDP port - cdpPortText.textContent = status.cdpPort || '未绑定'; - - // Update Toggles - autoLaunchToggle.checked = appConfig.autoLaunchAgent; - themeEnabledToggle.checked = appConfig.enabled; - - return status; - } catch (err) { - notify('无法获取后端状态', 'error'); - console.error(err); - } -} - // Fetch Themes list async function loadThemes() { try { @@ -534,9 +505,42 @@ btnSaveCrop.addEventListener('click', async () => { } }); +// Agent Selector +function setupAgentSelector() { + const btns = document.querySelectorAll('.agent-btn'); + btns.forEach(btn => { + btn.addEventListener('click', async () => { + const agent = btn.dataset.agent; + if (appConfig && appConfig.selectedAgent === agent) return; + + notify(`切换到 ${agent === 'codex' ? 'Codex' : 'Antigravity'}...`, 'info'); + try { + await invoke('set_selected_agent', { agent }); + await refreshStatus(); + await loadThemes(); + + // Auto-apply theme to the new agent if enabled + if (appConfig && appConfig.enabled && appConfig.selectedThemeId) { + await applyTheme(appConfig.selectedThemeId); + } + + notify(`已切换到 ${agent === 'codex' ? 'Codex' : 'Antigravity'}`, 'info'); + } catch (err) { + notify(`切换失败: ${err}`, 'error'); + console.error(err); + } + }); + }); +} + // Event Listeners for DOM Toggles & Actions -autoLaunchToggle.addEventListener('change', (e) => { - updateConfig({ autoLaunchAgent: e.target.checked }); +autoLaunchToggle.addEventListener('change', async (e) => { + try { + await invoke('set_auto_launch', { enabled: e.target.checked }); + await refreshStatus(); + } catch (err) { + console.error('Failed to update auto launch:', err); + } }); themeEnabledToggle.addEventListener('change', async (e) => { @@ -561,6 +565,7 @@ btnCancelCrop.addEventListener('click', openUploadModal); // Init on Load async function init() { + setupAgentSelector(); await refreshStatus(); await loadThemes(); @@ -569,3 +574,47 @@ async function init() { } window.addEventListener('DOMContentLoaded', init); +async function refreshStatus() { + try { + const status = await invoke('get_agent_status'); + const config = await invoke('get_config'); + appConfig = config; + + // Update Agent Process status + if (status.running) { + statusDot.className = 'dot online'; + statusText.textContent = '正在运行'; + btnStartAgent.disabled = true; + } else { + statusDot.className = 'dot offline'; + statusText.textContent = '未运行'; + btnStartAgent.disabled = false; + } + + // Update CDP port + cdpPortText.textContent = status.cdpPort || '未绑定'; + + // Update Toggles + autoLaunchToggle.checked = appConfig.autoLaunchAgent; + themeEnabledToggle.checked = appConfig.enabled; + + // Update agent selector buttons + updateAgentSelector(appConfig.selectedAgent || 'codex'); + + return status; + } catch (err) { + notify('无法获取后端状态', 'error'); + console.error(err); + } +} + +function updateAgentSelector(agent) { + const btns = document.querySelectorAll('.agent-btn'); + btns.forEach(btn => { + if (btn.dataset.agent === agent) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); +} diff --git a/web/dist/bundle.js b/web/dist/bundle.js index 3db45f7..0601821 100644 --- a/web/dist/bundle.js +++ b/web/dist/bundle.js @@ -1,538 +1,567 @@ -(() => { - var __getOwnPropNames = Object.getOwnPropertyNames; - var __esm = (fn, res) => function __init() { - return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; - }; - var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; - }; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __esm = (fn, res) => function __init() { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; +}; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; - // node_modules/@tauri-apps/api/external/tslib/tslib.es6.js - function __classPrivateFieldGet(receiver, state, kind, f) { - if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); - if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); - return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +// node_modules/@tauri-apps/api/external/tslib/tslib.es6.js +function __classPrivateFieldGet(receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +} +function __classPrivateFieldSet(receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value; +} +var init_tslib_es6 = __esm({ + "node_modules/@tauri-apps/api/external/tslib/tslib.es6.js"() { } - function __classPrivateFieldSet(receiver, state, value, kind, f) { - if (kind === "m") throw new TypeError("Private method is not writable"); - if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); - if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); - return kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value), value; - } - var init_tslib_es6 = __esm({ - "node_modules/@tauri-apps/api/external/tslib/tslib.es6.js"() { - } - }); +}); - // node_modules/@tauri-apps/api/core.js - function transformCallback(callback, once = false) { - return window.__TAURI_INTERNALS__.transformCallback(callback, once); - } - async function invoke(cmd, args = {}, options) { - return window.__TAURI_INTERNALS__.invoke(cmd, args, options); - } - function convertFileSrc(filePath, protocol = "asset") { - return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol); - } - var _Channel_onmessage, _Channel_nextMessageIndex, _Channel_pendingMessages, _Channel_messageEndIndex, _Resource_rid, SERIALIZE_TO_IPC_FN, Channel; - var init_core = __esm({ - "node_modules/@tauri-apps/api/core.js"() { - init_tslib_es6(); - SERIALIZE_TO_IPC_FN = "__TAURI_TO_IPC_KEY__"; - Channel = class { - constructor(onmessage) { - _Channel_onmessage.set(this, void 0); - _Channel_nextMessageIndex.set(this, 0); - _Channel_pendingMessages.set(this, []); - _Channel_messageEndIndex.set(this, void 0); - __classPrivateFieldSet(this, _Channel_onmessage, onmessage || (() => { - }), "f"); - this.id = transformCallback((rawMessage) => { - const index = rawMessage.index; - if ("end" in rawMessage) { - if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { - this.cleanupCallback(); - } else { - __classPrivateFieldSet(this, _Channel_messageEndIndex, index, "f"); - } - return; - } - const message = rawMessage.message; +// node_modules/@tauri-apps/api/core.js +function transformCallback(callback, once = false) { + return window.__TAURI_INTERNALS__.transformCallback(callback, once); +} +async function invoke(cmd, args = {}, options) { + return window.__TAURI_INTERNALS__.invoke(cmd, args, options); +} +function convertFileSrc(filePath, protocol = "asset") { + return window.__TAURI_INTERNALS__.convertFileSrc(filePath, protocol); +} +var _Channel_onmessage, _Channel_nextMessageIndex, _Channel_pendingMessages, _Channel_messageEndIndex, _Resource_rid, SERIALIZE_TO_IPC_FN, Channel; +var init_core = __esm({ + "node_modules/@tauri-apps/api/core.js"() { + init_tslib_es6(); + SERIALIZE_TO_IPC_FN = "__TAURI_TO_IPC_KEY__"; + Channel = class { + constructor(onmessage) { + _Channel_onmessage.set(this, void 0); + _Channel_nextMessageIndex.set(this, 0); + _Channel_pendingMessages.set(this, []); + _Channel_messageEndIndex.set(this, void 0); + __classPrivateFieldSet(this, _Channel_onmessage, onmessage || (() => { + }), "f"); + this.id = transformCallback((rawMessage) => { + const index = rawMessage.index; + if ("end" in rawMessage) { if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { - __classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message); - __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); - while (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") in __classPrivateFieldGet(this, _Channel_pendingMessages, "f")) { - const message2 = __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; - __classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message2); - delete __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; - __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); - } - if (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") === __classPrivateFieldGet(this, _Channel_messageEndIndex, "f")) { - this.cleanupCallback(); - } + this.cleanupCallback(); } else { - __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[index] = message; + __classPrivateFieldSet(this, _Channel_messageEndIndex, index, "f"); } - }); - } - cleanupCallback() { - window.__TAURI_INTERNALS__.unregisterCallback(this.id); - } - set onmessage(handler) { - __classPrivateFieldSet(this, _Channel_onmessage, handler, "f"); - } - get onmessage() { - return __classPrivateFieldGet(this, _Channel_onmessage, "f"); - } - [(_Channel_onmessage = /* @__PURE__ */ new WeakMap(), _Channel_nextMessageIndex = /* @__PURE__ */ new WeakMap(), _Channel_pendingMessages = /* @__PURE__ */ new WeakMap(), _Channel_messageEndIndex = /* @__PURE__ */ new WeakMap(), SERIALIZE_TO_IPC_FN)]() { - return `__CHANNEL__:${this.id}`; - } - toJSON() { - return this[SERIALIZE_TO_IPC_FN](); - } - }; - _Resource_rid = /* @__PURE__ */ new WeakMap(); - } - }); - - // app.js - var require_app = __commonJS({ - "app.js"() { - init_core(); - var appConfig = null; - var currentThemes = []; - var cropImageSrc = null; - var cropImageObj = null; - var imageX = 0; - var imageY = 0; - var imageScale = 1; - var isDragging = false; - var startX = 0; - var startY = 0; - var statusDot = document.getElementById("codex-status-dot"); - var statusText = document.getElementById("codex-status-text"); - var cdpPortText = document.getElementById("cdp-port-text"); - var autoLaunchToggle = document.getElementById("auto-launch-toggle"); - var themeEnabledToggle = document.getElementById("theme-enabled-toggle"); - var btnStartCodex = document.getElementById("btn-start-codex"); - var btnRestartCodex = document.getElementById("btn-restart-codex"); - var themesGrid = document.getElementById("themes-grid"); - var notificationText = document.getElementById("notification-text"); - var uploadModal = document.getElementById("upload-modal"); - var btnCloseModal = document.getElementById("btn-close-modal"); - var dropZone = document.getElementById("drop-zone"); - var fileInput = document.getElementById("file-input"); - var cropArea = document.getElementById("crop-area"); - var cropImage = document.getElementById("crop-image"); - var cropBox = document.getElementById("crop-box"); - var zoomSlider = document.getElementById("zoom-slider"); - var btnCancelCrop = document.getElementById("btn-cancel-crop"); - var btnSaveCrop = document.getElementById("btn-save-crop"); - function notify(text, type = "info") { - notificationText.textContent = text; - notificationText.className = `notification-info ${type}`; - } - async function refreshStatus() { - try { - const status = await invoke("get_codex_status"); - const config = await invoke("get_config"); - appConfig = config; - if (status.running) { - statusDot.className = "dot online"; - statusText.textContent = "\u6B63\u5728\u8FD0\u884C"; - btnStartCodex.disabled = true; - } else { - statusDot.className = "dot offline"; - statusText.textContent = "\u672A\u8FD0\u884C"; - btnStartCodex.disabled = false; - } - cdpPortText.textContent = status.cdpPort || "\u672A\u7ED1\u5B9A"; - autoLaunchToggle.checked = appConfig.autoLaunchCodex; - themeEnabledToggle.checked = appConfig.enabled; - return status; - } catch (err) { - notify("\u65E0\u6CD5\u83B7\u53D6\u540E\u7AEF\u72B6\u6001", "error"); - console.error(err); - } - } - async function loadThemes() { - try { - currentThemes = await invoke("get_all_themes"); - renderThemesGrid(); - } catch (err) { - notify("\u65E0\u6CD5\u83B7\u53D6\u4E3B\u9898\u5217\u8868", "error"); - console.error(err); - } - } - function renderThemesGrid() { - themesGrid.innerHTML = ""; - if (currentThemes.length === 0) { - themesGrid.innerHTML = '
\u6682\u65E0\u53EF\u7528\u4E3B\u9898
'; - return; - } - currentThemes.forEach((theme) => { - const isActive = appConfig && appConfig.selectedThemeId === theme.id; - const card = document.createElement("div"); - card.className = `card theme-card${isActive ? " active" : ""}`; - card.dataset.id = theme.id; - const previewDiv = document.createElement("div"); - previewDiv.className = "theme-preview"; - const previewPath = theme.dir + "/" + theme.preview; - previewDiv.style.backgroundImage = `url(${convertFileSrc(previewPath)})`; - card.appendChild(previewDiv); - const overlay = document.createElement("div"); - overlay.className = "theme-overlay"; - card.appendChild(overlay); - const info = document.createElement("div"); - info.className = "theme-info"; - const details = document.createElement("div"); - details.className = "theme-details"; - const name = document.createElement("h3"); - name.textContent = theme.displayName.zh; - details.appendChild(name); - const description = document.createElement("p"); - description.textContent = theme.isCustom ? "\u7528\u6237\u4E0A\u4F20\u80CC\u666F" : "\u5185\u7F6E\u58C1\u7EB8"; - details.appendChild(description); - const badges = document.createElement("div"); - badges.className = "theme-meta-badges"; - if (theme.isCustom) { - const customBadge = document.createElement("span"); - customBadge.className = "badge badge-custom"; - customBadge.textContent = "Custom"; - badges.appendChild(customBadge); + return; } - details.appendChild(badges); - info.appendChild(details); - const actionText = document.createElement("span"); - actionText.className = "theme-action"; - actionText.textContent = isActive ? "\u5E94\u7528\u4E2D" : "\u4F7F\u7528\u4E3B\u9898"; - info.appendChild(actionText); - card.appendChild(info); - if (theme.isCustom) { - const deleteBtn = document.createElement("button"); - deleteBtn.className = "delete-theme-btn"; - deleteBtn.innerHTML = "×"; - deleteBtn.title = "\u5220\u9664\u81EA\u5B9A\u4E49\u4E3B\u9898"; - deleteBtn.addEventListener("click", async (e) => { - e.stopPropagation(); - if (confirm("\u786E\u8BA4\u5220\u9664\u81EA\u5B9A\u4E49\u80CC\u666F\u4E3B\u9898\uFF1F")) { - await deleteCustomTheme(); - } - }); - card.appendChild(deleteBtn); + const message = rawMessage.message; + if (index == __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")) { + __classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message); + __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); + while (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") in __classPrivateFieldGet(this, _Channel_pendingMessages, "f")) { + const message2 = __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; + __classPrivateFieldGet(this, _Channel_onmessage, "f").call(this, message2); + delete __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f")]; + __classPrivateFieldSet(this, _Channel_nextMessageIndex, __classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") + 1, "f"); + } + if (__classPrivateFieldGet(this, _Channel_nextMessageIndex, "f") === __classPrivateFieldGet(this, _Channel_messageEndIndex, "f")) { + this.cleanupCallback(); + } + } else { + __classPrivateFieldGet(this, _Channel_pendingMessages, "f")[index] = message; } - card.addEventListener("click", () => applyTheme(theme.id)); - themesGrid.appendChild(card); - }); - const uploadCard = document.createElement("div"); - uploadCard.className = "upload-card"; - const uploadIcon = document.createElement("div"); - uploadIcon.className = "upload-icon"; - uploadIcon.textContent = "\u2795"; - uploadCard.appendChild(uploadIcon); - const uploadText = document.createElement("span"); - uploadText.textContent = "\u81EA\u5B9A\u4E49\u80CC\u666F"; - uploadCard.appendChild(uploadText); - uploadCard.addEventListener("click", () => { - openUploadModal(); }); - themesGrid.appendChild(uploadCard); } - async function applyTheme(themeId) { - notify(`\u6B63\u5728\u5E94\u7528\u4E3B\u9898 "${themeId}"...`, "info"); - try { - await invoke("apply_theme", { themeId }); - notify("\u4E3B\u9898\u5E94\u7528\u6210\u529F\uFF01", "info"); - await refreshStatus(); - loadThemes(); - } catch (err) { - notify(`\u5E94\u7528\u4E3B\u9898\u5931\u8D25: ${err}`, "error"); - console.error(err); - } + cleanupCallback() { + window.__TAURI_INTERNALS__.unregisterCallback(this.id); } - async function clearActiveTheme() { - notify("\u6B63\u5728\u6E05\u9664\u5F53\u524D\u4E3B\u9898...", "info"); - try { - await invoke("clear_theme"); - notify("\u4E3B\u9898\u5DF2\u6210\u529F\u6E05\u9664\u3002", "info"); - await refreshStatus(); - loadThemes(); - } catch (err) { - notify(`\u6E05\u9664\u4E3B\u9898\u5931\u8D25: ${err}`, "error"); - themeEnabledToggle.checked = true; - console.error(err); - } + set onmessage(handler) { + __classPrivateFieldSet(this, _Channel_onmessage, handler, "f"); } - async function deleteCustomTheme() { - notify("\u6B63\u5728\u5220\u9664\u81EA\u5B9A\u4E49\u4E3B\u9898...", "info"); - try { - await invoke("delete_custom_theme_cmd"); - notify("\u81EA\u5B9A\u4E49\u4E3B\u9898\u5DF2\u5220\u9664\u3002", "info"); - await refreshStatus(); - loadThemes(); - } catch (err) { - notify(`\u5220\u9664\u5931\u8D25: ${err}`, "error"); - console.error(err); - } - } - async function startCodex(forceClean = false) { - notify("\u6B63\u5728\u542F\u52A8 Codex App...", "info"); - try { - await invoke("restart_codex"); - notify("Codex \u5DF2\u542F\u52A8\uFF01", "info"); - setTimeout(refreshStatus, 3e3); - } catch (err) { - notify(`\u542F\u52A8\u5931\u8D25: ${err}`, "error"); - console.error(err); - } + get onmessage() { + return __classPrivateFieldGet(this, _Channel_onmessage, "f"); } - async function restartCodex() { - notify("\u6B63\u5728\u91CD\u542F Codex App...", "info"); - try { - await invoke("restart_codex"); - notify("Codex \u5DF2\u6210\u529F\u91CD\u542F\uFF01", "info"); - setTimeout(refreshStatus, 3e3); - } catch (err) { - notify(`\u91CD\u542F\u5931\u8D25: ${err}`, "error"); - console.error(err); - } + [(_Channel_onmessage = /* @__PURE__ */ new WeakMap(), _Channel_nextMessageIndex = /* @__PURE__ */ new WeakMap(), _Channel_pendingMessages = /* @__PURE__ */ new WeakMap(), _Channel_messageEndIndex = /* @__PURE__ */ new WeakMap(), SERIALIZE_TO_IPC_FN)]() { + return `__CHANNEL__:${this.id}`; } - async function updateConfig(updates) { - try { - if (updates.enabled !== void 0) { - await invoke("set_enabled", { enabled: updates.enabled }); - } - await refreshStatus(); - } catch (err) { - console.error("Failed to update config:", err); - } + toJSON() { + return this[SERIALIZE_TO_IPC_FN](); } - function openUploadModal() { - dropZone.style.display = "flex"; - cropArea.style.display = "none"; - btnCancelCrop.style.display = "none"; - btnSaveCrop.style.display = "none"; - fileInput.value = ""; - uploadModal.classList.add("show"); + }; + _Resource_rid = /* @__PURE__ */ new WeakMap(); + } +}); + +// app.js +var require_app = __commonJS({ + "app.js"() { + init_core(); + var appConfig = null; + var currentThemes = []; + var cropImageSrc = null; + var cropImageObj = null; + var imageX = 0; + var imageY = 0; + var imageScale = 1; + var isDragging = false; + var startX = 0; + var startY = 0; + var statusDot = document.getElementById("agent-status-dot"); + var statusText = document.getElementById("agent-status-text"); + var cdpPortText = document.getElementById("cdp-port-text"); + var autoLaunchToggle = document.getElementById("auto-launch-toggle"); + var themeEnabledToggle = document.getElementById("theme-enabled-toggle"); + var btnStartAgent = document.getElementById("btn-start-agent"); + var btnRestartAgent = document.getElementById("btn-restart-agent"); + var themesGrid = document.getElementById("themes-grid"); + var notificationText = document.getElementById("notification-text"); + var btnAgentCodex = document.getElementById("btn-agent-codex"); + var btnAgentAntigravity = document.getElementById("btn-agent-antigravity"); + var uploadModal = document.getElementById("upload-modal"); + var btnCloseModal = document.getElementById("btn-close-modal"); + var dropZone = document.getElementById("drop-zone"); + var fileInput = document.getElementById("file-input"); + var cropArea = document.getElementById("crop-area"); + var cropImage = document.getElementById("crop-image"); + var cropBox = document.getElementById("crop-box"); + var zoomSlider = document.getElementById("zoom-slider"); + var btnCancelCrop = document.getElementById("btn-cancel-crop"); + var btnSaveCrop = document.getElementById("btn-save-crop"); + function notify(text, type = "info") { + notificationText.textContent = text; + notificationText.className = `notification-info ${type}`; + } + async function loadThemes() { + try { + currentThemes = await invoke("get_all_themes"); + renderThemesGrid(); + } catch (err) { + notify("\u65E0\u6CD5\u83B7\u53D6\u4E3B\u9898\u5217\u8868", "error"); + console.error(err); } - function closeUploadModal() { - uploadModal.classList.remove("show"); - cropImageSrc = null; - cropImageObj = null; + } + function renderThemesGrid() { + themesGrid.innerHTML = ""; + if (currentThemes.length === 0) { + themesGrid.innerHTML = '
\u6682\u65E0\u53EF\u7528\u4E3B\u9898
'; + return; } - dropZone.addEventListener("dragover", (e) => { - e.preventDefault(); - dropZone.classList.add("hover"); - }); - dropZone.addEventListener("dragleave", () => { - dropZone.classList.remove("hover"); - }); - dropZone.addEventListener("drop", (e) => { - e.preventDefault(); - dropZone.classList.remove("hover"); - const files = e.dataTransfer.files; - if (files.length > 0) { - handleSelectedFile(files[0]); + currentThemes.forEach((theme) => { + const isActive = appConfig && appConfig.selectedThemeId === theme.id; + const card = document.createElement("div"); + card.className = `card theme-card${isActive ? " active" : ""}`; + card.dataset.id = theme.id; + const previewDiv = document.createElement("div"); + previewDiv.className = "theme-preview"; + const previewPath = theme.dir + "/" + theme.preview; + previewDiv.style.backgroundImage = `url(${convertFileSrc(previewPath)})`; + card.appendChild(previewDiv); + const overlay = document.createElement("div"); + overlay.className = "theme-overlay"; + card.appendChild(overlay); + const info = document.createElement("div"); + info.className = "theme-info"; + const details = document.createElement("div"); + details.className = "theme-details"; + const name = document.createElement("h3"); + name.textContent = theme.displayName.zh; + details.appendChild(name); + const description = document.createElement("p"); + description.textContent = theme.isCustom ? "\u7528\u6237\u4E0A\u4F20\u80CC\u666F" : "\u5185\u7F6E\u58C1\u7EB8"; + details.appendChild(description); + const badges = document.createElement("div"); + badges.className = "theme-meta-badges"; + if (theme.isCustom) { + const customBadge = document.createElement("span"); + customBadge.className = "badge badge-custom"; + customBadge.textContent = "Custom"; + badges.appendChild(customBadge); } - }); - dropZone.addEventListener("click", () => { - fileInput.click(); - }); - fileInput.addEventListener("change", (e) => { - const files = e.target.files; - if (files.length > 0) { - handleSelectedFile(files[0]); + details.appendChild(badges); + info.appendChild(details); + const actionText = document.createElement("span"); + actionText.className = "theme-action"; + actionText.textContent = isActive ? "\u5E94\u7528\u4E2D" : "\u4F7F\u7528\u4E3B\u9898"; + info.appendChild(actionText); + card.appendChild(info); + if (theme.isCustom) { + const deleteBtn = document.createElement("button"); + deleteBtn.className = "delete-theme-btn"; + deleteBtn.innerHTML = "×"; + deleteBtn.title = "\u5220\u9664\u81EA\u5B9A\u4E49\u4E3B\u9898"; + deleteBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + if (confirm("\u786E\u8BA4\u5220\u9664\u81EA\u5B9A\u4E49\u80CC\u666F\u4E3B\u9898\uFF1F")) { + await deleteCustomTheme(); + } + }); + card.appendChild(deleteBtn); } + card.addEventListener("click", () => applyTheme(theme.id)); + themesGrid.appendChild(card); }); - function handleSelectedFile(file) { - if (!file.type.startsWith("image/")) { - alert("\u8BF7\u9009\u62E9\u6709\u6548\u7684\u56FE\u7247\u6587\u4EF6\uFF01"); - return; - } - const reader = new FileReader(); - reader.onload = (e) => { - cropImageSrc = e.target.result; - initCropper(); - }; - reader.readAsDataURL(file); + const uploadCard = document.createElement("div"); + uploadCard.className = "upload-card"; + const uploadIcon = document.createElement("div"); + uploadIcon.className = "upload-icon"; + uploadIcon.textContent = "\u2795"; + uploadCard.appendChild(uploadIcon); + const uploadText = document.createElement("span"); + uploadText.textContent = "\u81EA\u5B9A\u4E49\u80CC\u666F"; + uploadCard.appendChild(uploadText); + uploadCard.addEventListener("click", () => { + openUploadModal(); + }); + themesGrid.appendChild(uploadCard); + } + async function applyTheme(themeId) { + notify(`\u6B63\u5728\u5E94\u7528\u4E3B\u9898 "${themeId}"...`, "info"); + try { + await invoke("apply_theme", { themeId }); + notify("\u4E3B\u9898\u5E94\u7528\u6210\u529F\uFF01", "info"); + await refreshStatus(); + loadThemes(); + } catch (err) { + notify(`\u5E94\u7528\u4E3B\u9898\u5931\u8D25: ${err}`, "error"); + console.error(err); } - function initCropper() { - dropZone.style.display = "none"; - cropArea.style.display = "flex"; - btnCancelCrop.style.display = "inline-block"; - btnSaveCrop.style.display = "inline-block"; - cropImage.src = cropImageSrc; - cropImageObj = new Image(); - cropImageObj.src = cropImageSrc; - cropImageObj.onload = () => { - zoomSlider.value = 100; - imageScale = 1; - const cropContainerNode = document.querySelector(".crop-container"); - const containerWidth = cropContainerNode.clientWidth || 630; - const containerHeight = cropContainerNode.clientHeight || 350; - const imgWidth = cropImageObj.width; - const imgHeight = cropImageObj.height; - const scaleX = containerWidth / imgWidth; - const scaleY = containerHeight / imgHeight; - imageScale = Math.max(scaleX, scaleY); - if (imageScale > 1) imageScale = 1; - zoomSlider.min = Math.floor(imageScale * 50); - zoomSlider.max = Math.floor(imageScale * 300); - zoomSlider.value = Math.floor(imageScale * 100); - imageX = 0; - imageY = 0; - updateImageStyle(); - }; + } + async function clearActiveTheme() { + notify("\u6B63\u5728\u6E05\u9664\u5F53\u524D\u4E3B\u9898...", "info"); + try { + await invoke("clear_theme"); + notify("\u4E3B\u9898\u5DF2\u6210\u529F\u6E05\u9664\u3002", "info"); + await refreshStatus(); + loadThemes(); + } catch (err) { + notify(`\u6E05\u9664\u4E3B\u9898\u5931\u8D25: ${err}`, "error"); + themeEnabledToggle.checked = true; + console.error(err); } - cropArea.addEventListener("mousedown", (e) => { - if (e.target === cropImage || e.target.id === "crop-area" || e.target.className === "crop-container") { - isDragging = true; - startX = e.clientX - imageX; - startY = e.clientY - imageY; - e.preventDefault(); - } - }); - window.addEventListener("mousemove", (e) => { - if (!isDragging) return; - const newX = e.clientX - startX; - const newY = e.clientY - startY; - const container = cropContainer.getBoundingClientRect(); - const imgW = (cropImage.naturalWidth || cropImage.width) * imageScale; - const imgH = (cropImage.naturalHeight || cropImage.height) * imageScale; - const maxX = Math.max(0, (imgW - container.width) / 2); - const maxY = Math.max(0, (imgH - container.height) / 2); - imageX = Math.max(-maxX, Math.min(maxX, newX)); - imageY = Math.max(-maxY, Math.min(maxY, newY)); - updateImageStyle(); - }); - window.addEventListener("mouseup", () => { - isDragging = false; - }); - zoomSlider.addEventListener("input", (e) => { - imageScale = parseFloat(e.target.value) / 100; - updateImageStyle(); - }); - function updateImageStyle() { - if (cropImage) { - cropImage.style.transform = `translate(${imageX}px, ${imageY}px) scale(${imageScale})`; - } + } + async function deleteCustomTheme() { + notify("\u6B63\u5728\u5220\u9664\u81EA\u5B9A\u4E49\u4E3B\u9898...", "info"); + try { + await invoke("delete_custom_theme_cmd"); + notify("\u81EA\u5B9A\u4E49\u4E3B\u9898\u5DF2\u5220\u9664\u3002", "info"); + await refreshStatus(); + loadThemes(); + } catch (err) { + notify(`\u5220\u9664\u5931\u8D25: ${err}`, "error"); + console.error(err); + } + } + async function startAgent(forceClean = false) { + notify("\u6B63\u5728\u542F\u52A8 Agent App...", "info"); + try { + await invoke("restart_agent"); + notify("Agent \u5DF2\u542F\u52A8\uFF01", "info"); + setTimeout(refreshStatus, 3e3); + } catch (err) { + notify(`\u542F\u52A8\u5931\u8D25: ${err}`, "error"); + console.error(err); + } + } + async function restartAgent() { + notify("\u6B63\u5728\u91CD\u542F Agent App...", "info"); + try { + await invoke("restart_agent"); + notify("Agent \u5DF2\u6210\u529F\u91CD\u542F\uFF01", "info"); + setTimeout(refreshStatus, 3e3); + } catch (err) { + notify(`\u91CD\u542F\u5931\u8D25: ${err}`, "error"); + console.error(err); } - function performCrop() { - if (!cropImageObj) return null; - const bgSize = 2048; - const previewSize = 640; + } + function openUploadModal() { + dropZone.style.display = "flex"; + cropArea.style.display = "none"; + btnCancelCrop.style.display = "none"; + btnSaveCrop.style.display = "none"; + fileInput.value = ""; + uploadModal.classList.add("show"); + } + function closeUploadModal() { + uploadModal.classList.remove("show"); + cropImageSrc = null; + cropImageObj = null; + } + dropZone.addEventListener("dragover", (e) => { + e.preventDefault(); + dropZone.classList.add("hover"); + }); + dropZone.addEventListener("dragleave", () => { + dropZone.classList.remove("hover"); + }); + dropZone.addEventListener("drop", (e) => { + e.preventDefault(); + dropZone.classList.remove("hover"); + const files = e.dataTransfer.files; + if (files.length > 0) { + handleSelectedFile(files[0]); + } + }); + dropZone.addEventListener("click", () => { + fileInput.click(); + }); + fileInput.addEventListener("change", (e) => { + const files = e.target.files; + if (files.length > 0) { + handleSelectedFile(files[0]); + } + }); + function handleSelectedFile(file) { + if (!file.type.startsWith("image/")) { + alert("\u8BF7\u9009\u62E9\u6709\u6548\u7684\u56FE\u7247\u6587\u4EF6\uFF01"); + return; + } + const reader = new FileReader(); + reader.onload = (e) => { + cropImageSrc = e.target.result; + initCropper(); + }; + reader.readAsDataURL(file); + } + function initCropper() { + dropZone.style.display = "none"; + cropArea.style.display = "flex"; + btnCancelCrop.style.display = "inline-block"; + btnSaveCrop.style.display = "inline-block"; + cropImage.src = cropImageSrc; + cropImageObj = new Image(); + cropImageObj.src = cropImageSrc; + cropImageObj.onload = () => { + zoomSlider.value = 100; + imageScale = 1; const cropContainerNode = document.querySelector(".crop-container"); - const cropBoxNode = document.getElementById("crop-box"); - const containerRect = cropContainerNode.getBoundingClientRect(); - const boxRect = cropBoxNode.getBoundingClientRect(); - const containerW = containerRect.width || 630; - const containerH = containerRect.height || 350; - const boxSize = boxRect.width || 300; - const boxLeft = boxRect.left - containerRect.left; - const boxTop = boxRect.top - containerRect.top; - const imgW = cropImageObj.width; - const imgH = cropImageObj.height; - const imgCenterX = containerW / 2; - const imgCenterY = containerH / 2; - const initialLeft = imgCenterX - imgW / 2; - const initialTop = imgCenterY - imgH / 2; - const currentCenterX = initialLeft + imgW / 2 + imageX; - const currentCenterY = initialTop + imgH / 2 + imageY; - const currentLeft = currentCenterX - imgW * imageScale / 2; - const currentTop = currentCenterY - imgH * imageScale / 2; - const relX = (boxLeft - currentLeft) / imageScale; - const relY = (boxTop - currentTop) / imageScale; - const relSize = boxSize / imageScale; - const bgCanvas = document.createElement("canvas"); - bgCanvas.width = bgSize; - bgCanvas.height = bgSize; - const bgCtx = bgCanvas.getContext("2d"); - bgCtx.fillStyle = "#050202"; - bgCtx.fillRect(0, 0, bgSize, bgSize); - bgCtx.drawImage( - cropImageObj, - relX, - relY, - relSize, - relSize, - // Source - 0, - 0, - bgSize, - bgSize - // Destination - ); - const previewCanvas = document.createElement("canvas"); - previewCanvas.width = previewSize; - previewCanvas.height = previewSize; - const previewCtx = previewCanvas.getContext("2d"); - previewCtx.drawImage( - bgCanvas, - 0, - 0, - bgSize, - bgSize, - 0, - 0, - previewSize, - previewSize - ); - const bgData = bgCanvas.toDataURL("image/jpeg", 0.9); - const previewData = previewCanvas.toDataURL("image/jpeg", 0.9); - return { - bgImage: bgData, - previewImage: previewData - }; + const containerWidth = cropContainerNode.clientWidth || 630; + const containerHeight = cropContainerNode.clientHeight || 350; + const imgWidth = cropImageObj.width; + const imgHeight = cropImageObj.height; + const scaleX = containerWidth / imgWidth; + const scaleY = containerHeight / imgHeight; + imageScale = Math.max(scaleX, scaleY); + if (imageScale > 1) imageScale = 1; + zoomSlider.min = Math.floor(imageScale * 50); + zoomSlider.max = Math.floor(imageScale * 300); + zoomSlider.value = Math.floor(imageScale * 100); + imageX = 0; + imageY = 0; + updateImageStyle(); + }; + } + cropArea.addEventListener("mousedown", (e) => { + if (e.target === cropImage || e.target.id === "crop-area" || e.target.className === "crop-container") { + isDragging = true; + startX = e.clientX - imageX; + startY = e.clientY - imageY; + e.preventDefault(); + } + }); + window.addEventListener("mousemove", (e) => { + if (!isDragging) return; + const newX = e.clientX - startX; + const newY = e.clientY - startY; + const container = cropContainer.getBoundingClientRect(); + const imgW = (cropImage.naturalWidth || cropImage.width) * imageScale; + const imgH = (cropImage.naturalHeight || cropImage.height) * imageScale; + const maxX = Math.max(0, (imgW - container.width) / 2); + const maxY = Math.max(0, (imgH - container.height) / 2); + imageX = Math.max(-maxX, Math.min(maxX, newX)); + imageY = Math.max(-maxY, Math.min(maxY, newY)); + updateImageStyle(); + }); + window.addEventListener("mouseup", () => { + isDragging = false; + }); + zoomSlider.addEventListener("input", (e) => { + imageScale = parseFloat(e.target.value) / 100; + updateImageStyle(); + }); + function updateImageStyle() { + if (cropImage) { + cropImage.style.transform = `translate(${imageX}px, ${imageY}px) scale(${imageScale})`; } - btnSaveCrop.addEventListener("click", async () => { - const cropped = performCrop(); - if (!cropped) { - alert("\u88C1\u526A\u51FA\u9519\uFF0C\u8BF7\u91CD\u8BD5\uFF01"); - return; + } + function performCrop() { + if (!cropImageObj) return null; + const bgSize = 2048; + const previewSize = 640; + const cropContainerNode = document.querySelector(".crop-container"); + const cropBoxNode = document.getElementById("crop-box"); + const containerRect = cropContainerNode.getBoundingClientRect(); + const boxRect = cropBoxNode.getBoundingClientRect(); + const containerW = containerRect.width || 630; + const containerH = containerRect.height || 350; + const boxSize = boxRect.width || 300; + const boxLeft = boxRect.left - containerRect.left; + const boxTop = boxRect.top - containerRect.top; + const imgW = cropImageObj.width; + const imgH = cropImageObj.height; + const imgCenterX = containerW / 2; + const imgCenterY = containerH / 2; + const initialLeft = imgCenterX - imgW / 2; + const initialTop = imgCenterY - imgH / 2; + const currentCenterX = initialLeft + imgW / 2 + imageX; + const currentCenterY = initialTop + imgH / 2 + imageY; + const currentLeft = currentCenterX - imgW * imageScale / 2; + const currentTop = currentCenterY - imgH * imageScale / 2; + const relX = (boxLeft - currentLeft) / imageScale; + const relY = (boxTop - currentTop) / imageScale; + const relSize = boxSize / imageScale; + const bgCanvas = document.createElement("canvas"); + bgCanvas.width = bgSize; + bgCanvas.height = bgSize; + const bgCtx = bgCanvas.getContext("2d"); + bgCtx.fillStyle = "#050202"; + bgCtx.fillRect(0, 0, bgSize, bgSize); + bgCtx.drawImage( + cropImageObj, + relX, + relY, + relSize, + relSize, + // Source + 0, + 0, + bgSize, + bgSize + // Destination + ); + const previewCanvas = document.createElement("canvas"); + previewCanvas.width = previewSize; + previewCanvas.height = previewSize; + const previewCtx = previewCanvas.getContext("2d"); + previewCtx.drawImage( + bgCanvas, + 0, + 0, + bgSize, + bgSize, + 0, + 0, + previewSize, + previewSize + ); + const bgData = bgCanvas.toDataURL("image/jpeg", 0.9); + const previewData = previewCanvas.toDataURL("image/jpeg", 0.9); + return { + bgImage: bgData, + previewImage: previewData + }; + } + btnSaveCrop.addEventListener("click", async () => { + const cropped = performCrop(); + if (!cropped) { + alert("\u88C1\u526A\u51FA\u9519\uFF0C\u8BF7\u91CD\u8BD5\uFF01"); + return; + } + notify("\u6B63\u5728\u4FDD\u5B58\u5E76\u5E94\u7528\u81EA\u5B9A\u4E49\u80CC\u666F...", "info"); + closeUploadModal(); + try { + await invoke("upload_custom_theme", { + bgBase64: cropped.bgImage, + previewBase64: cropped.previewImage + }); + notify("\u81EA\u5B9A\u4E49\u80CC\u666F\u4FDD\u5B58\u5E76\u5E94\u7528\u6210\u529F\uFF01", "info"); + await refreshStatus(); + loadThemes(); + await applyTheme("custom"); + } catch (err) { + notify(`\u4E0A\u4F20\u8BF7\u6C42\u5931\u8D25: ${err}`, "error"); + console.error(err); + } + }); + function setupAgentSelector() { + const btns = document.querySelectorAll(".agent-btn"); + btns.forEach((btn) => { + btn.addEventListener("click", async () => { + const agent = btn.dataset.agent; + if (appConfig && appConfig.selectedAgent === agent) return; + notify(`\u5207\u6362\u5230 ${agent === "codex" ? "Codex" : "Antigravity"}...`, "info"); + try { + await invoke("set_selected_agent", { agent }); + await refreshStatus(); + await loadThemes(); + if (appConfig && appConfig.enabled && appConfig.selectedThemeId) { + await applyTheme(appConfig.selectedThemeId); + } + notify(`\u5DF2\u5207\u6362\u5230 ${agent === "codex" ? "Codex" : "Antigravity"}`, "info"); + } catch (err) { + notify(`\u5207\u6362\u5931\u8D25: ${err}`, "error"); + console.error(err); + } + }); + }); + } + autoLaunchToggle.addEventListener("change", async (e) => { + try { + await invoke("set_auto_launch", { enabled: e.target.checked }); + await refreshStatus(); + } catch (err) { + console.error("Failed to update auto launch:", err); + } + }); + themeEnabledToggle.addEventListener("change", async (e) => { + themeEnabledToggle.disabled = true; + if (e.target.checked) { + if (appConfig && appConfig.selectedThemeId) { + await applyTheme(appConfig.selectedThemeId); + } else { + await applyTheme("carton"); } - notify("\u6B63\u5728\u4FDD\u5B58\u5E76\u5E94\u7528\u81EA\u5B9A\u4E49\u80CC\u666F...", "info"); - closeUploadModal(); - try { - await invoke("upload_custom_theme", { - bgBase64: cropped.bgImage, - previewBase64: cropped.previewImage - }); - notify("\u81EA\u5B9A\u4E49\u80CC\u666F\u4FDD\u5B58\u5E76\u5E94\u7528\u6210\u529F\uFF01", "info"); - await refreshStatus(); - loadThemes(); - await applyTheme("custom"); - } catch (err) { - notify(`\u4E0A\u4F20\u8BF7\u6C42\u5931\u8D25: ${err}`, "error"); - console.error(err); + } else { + await clearActiveTheme(); + } + themeEnabledToggle.disabled = false; + }); + btnStartAgent.addEventListener("click", () => startAgent(false)); + btnRestartAgent.addEventListener("click", () => restartAgent()); + btnCloseModal.addEventListener("click", closeUploadModal); + btnCancelCrop.addEventListener("click", openUploadModal); + async function init() { + setupAgentSelector(); + await refreshStatus(); + await loadThemes(); + setInterval(refreshStatus, 15e3); + } + window.addEventListener("DOMContentLoaded", init); + async function refreshStatus() { + try { + const status = await invoke("get_agent_status"); + const config = await invoke("get_config"); + appConfig = config; + if (status.running) { + statusDot.className = "dot online"; + statusText.textContent = "\u6B63\u5728\u8FD0\u884C"; + btnStartAgent.disabled = true; + } else { + statusDot.className = "dot offline"; + statusText.textContent = "\u672A\u8FD0\u884C"; + btnStartAgent.disabled = false; } - }); - autoLaunchToggle.addEventListener("change", (e) => { - updateConfig({ autoLaunchCodex: e.target.checked }); - }); - themeEnabledToggle.addEventListener("change", async (e) => { - themeEnabledToggle.disabled = true; - if (e.target.checked) { - if (appConfig && appConfig.selectedThemeId) { - await applyTheme(appConfig.selectedThemeId); - } else { - await applyTheme("carton"); - } + cdpPortText.textContent = status.cdpPort || "\u672A\u7ED1\u5B9A"; + autoLaunchToggle.checked = appConfig.autoLaunchAgent; + themeEnabledToggle.checked = appConfig.enabled; + updateAgentSelector(appConfig.selectedAgent || "codex"); + return status; + } catch (err) { + notify("\u65E0\u6CD5\u83B7\u53D6\u540E\u7AEF\u72B6\u6001", "error"); + console.error(err); + } + } + function updateAgentSelector(agent) { + const btns = document.querySelectorAll(".agent-btn"); + btns.forEach((btn) => { + if (btn.dataset.agent === agent) { + btn.classList.add("active"); } else { - await clearActiveTheme(); + btn.classList.remove("active"); } - themeEnabledToggle.disabled = false; }); - btnStartCodex.addEventListener("click", () => startCodex(false)); - btnRestartCodex.addEventListener("click", () => restartCodex()); - btnCloseModal.addEventListener("click", closeUploadModal); - btnCancelCrop.addEventListener("click", openUploadModal); - async function init() { - await refreshStatus(); - await loadThemes(); - setInterval(refreshStatus, 15e3); - } - window.addEventListener("DOMContentLoaded", init); } - }); - require_app(); -})(); + } +}); +export default require_app(); diff --git a/web/dist/index.html b/web/dist/index.html index 1199775..e3f24a0 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -3,20 +3,28 @@ - Codex Theme Companion + Agent Theme Companion -
+
@@ -26,38 +34,11 @@
🎨
-

Codex Theme Companion

-

让你的 Codex App 焕发个性色彩

+

Agent Theme Companion

+

让你的 Agent App 焕发个性色彩

-
-
- Codex 状态 -
- - 检测中... -
-
- -
- CDP 调试端口 - - -
- -
- 自动启动 Codex - -
- -
- - -
-
@@ -85,7 +66,7 @@

选择背景主题

系统通知: 服务已就绪。 - + @@ -127,3 +108,38 @@

添加自定义背景

+
+
+ Agent 应用 +
+ + +
+
+ +
+ Agent 状态 +
+ + 检测中... +
+
+ +
+ CDP 调试端口 + - +
+ +
+ 自动启动 Agent + +
+ +
+ + +
+
diff --git a/web/dist/style.css b/web/dist/style.css index f99c457..6e9be5d 100644 --- a/web/dist/style.css +++ b/web/dist/style.css @@ -1,4 +1,4 @@ -/* CSS Design System for Codex Theme Companion */ +/* CSS Design System for Agent Theme Companion */ :root { --color-bg-base: #0f0707; --color-bg-panel: rgba(26, 14, 14, 0.55); @@ -390,11 +390,6 @@ input:checked + .slider:before { text-transform: uppercase; } -.badge-mascot { - background-color: var(--color-accent); - color: #120c00; -} - .badge-custom { background-color: var(--color-primary); color: white; @@ -709,3 +704,37 @@ input:checked + .slider:before { color: var(--color-text-secondary); font-weight: 500; } + +/* Agent Selector Buttons */ +.agent-selector { + display: flex; + gap: 0; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid var(--color-border); + overflow: hidden; +} + +.agent-btn { + padding: 6px 16px; + font-size: 0.85rem; + font-weight: 500; + font-family: var(--font-family); + color: var(--color-text-muted); + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.agent-btn:hover { + color: var(--color-text-secondary); + background: rgba(255, 255, 255, 0.05); +} + +.agent-btn.active { + color: var(--color-text-primary); + background: var(--color-primary); + border-radius: 6px; + box-shadow: 0 0 8px rgba(255, 71, 71, 0.3); +} diff --git a/web/index.html b/web/index.html index 5e1134e..e3f24a0 100644 --- a/web/index.html +++ b/web/index.html @@ -39,33 +39,6 @@

Agent Theme Companion

-
-
- Agent 状态 -
- - 检测中... -
-
- -
- CDP 调试端口 - - -
- -
- 自动启动 Agent - -
- -
- - -
-
@@ -135,3 +108,38 @@

添加自定义背景

+
+
+ Agent 应用 +
+ + +
+
+ +
+ Agent 状态 +
+ + 检测中... +
+
+ +
+ CDP 调试端口 + - +
+ +
+ 自动启动 Agent + +
+ +
+ + +
+
diff --git a/web/style.css b/web/style.css index 2ed8757..6e9be5d 100644 --- a/web/style.css +++ b/web/style.css @@ -704,3 +704,37 @@ input:checked + .slider:before { color: var(--color-text-secondary); font-weight: 500; } + +/* Agent Selector Buttons */ +.agent-selector { + display: flex; + gap: 0; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid var(--color-border); + overflow: hidden; +} + +.agent-btn { + padding: 6px 16px; + font-size: 0.85rem; + font-weight: 500; + font-family: var(--font-family); + color: var(--color-text-muted); + background: transparent; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.agent-btn:hover { + color: var(--color-text-secondary); + background: rgba(255, 255, 255, 0.05); +} + +.agent-btn.active { + color: var(--color-text-primary); + background: var(--color-primary); + border-radius: 6px; + box-shadow: 0 0 8px rgba(255, 71, 71, 0.3); +} From e1d767e592b019dd85f506c3c25b0079f8ef6b74 Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Wed, 27 May 2026 23:30:13 +0800 Subject: [PATCH 2/8] fix: address Devin review findings 1. Move status bar HTML back inside
before - was placed after causing all getElementById to return null 2. Reset cdp_port and active_identifier when switching agents - set_selected_agent now takes State and clears stale port --- src-tauri/src/lib.rs | 5 ++- web/dist/index.html | 88 ++++++++++++++++++++++---------------------- web/index.html | 88 ++++++++++++++++++++++---------------------- 3 files changed, 92 insertions(+), 89 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 044f6fb..7cf443e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -24,7 +24,10 @@ async fn set_enabled(enabled: bool) -> Result { } #[tauri::command] -async fn set_selected_agent(agent: AgentKind) -> Result { +async fn set_selected_agent(state: State<'_, AppState>, agent: AgentKind) -> Result { + // Clear stale port/identifier from previous agent + *state.cdp_port.lock().unwrap() = None; + *state.active_identifier.lock().unwrap() = None; Ok(update_config(|c| c.selected_agent = agent)) } diff --git a/web/dist/index.html b/web/dist/index.html index e3f24a0..eacce08 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -23,7 +23,7 @@ var el = document.getElementById('error-log'); el.style.display = 'block'; var p = document.createElement('p'); - p.textContent = 'Promise: ' + e.reason; + p.textContent = 'Unhandled: ' + e.reason; el.appendChild(p); }); @@ -39,6 +39,41 @@

Agent Theme Companion

+
+
+ Agent 应用 +
+ + +
+
+ +
+ Agent 状态 +
+ + 检测中... +
+
+ +
+ CDP 调试端口 + - +
+ +
+ 自动启动 Agent + +
+ +
+ + +
+
@@ -63,22 +98,22 @@

选择背景主题

- +
- +