diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..73df3e6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +env: + CARGO_TERM_COLOR: always + +jobs: + rust-checks: + name: Rust Checks + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + + - name: Cache cargo registry and build + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-tauri/target + key: cargo-${{ runner.os }}-${{ hashFiles('src-tauri/Cargo.lock') }} + restore-keys: cargo-${{ runner.os }}- + + - name: cargo fmt --check + run: cargo fmt --check + working-directory: src-tauri + + - name: cargo clippy + run: cargo clippy -- -D warnings + working-directory: src-tauri + + - name: cargo check + run: cargo check + working-directory: src-tauri + + frontend-checks: + name: Frontend Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + working-directory: web + run: npm install + + - name: Build bundle + working-directory: web + run: npx esbuild app.js --bundle --outfile=dist/bundle.js --format=esm --platform=browser 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/build.rs b/src-tauri/build.rs index 795b9b7..d860e1e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/src-tauri/src/agent.rs b/src-tauri/src/agent.rs index 2f7f392..73cbd37 100644 --- a/src-tauri/src/agent.rs +++ b/src-tauri/src/agent.rs @@ -1,37 +1,50 @@ +use crate::config::AgentKind; use std::fs; use std::path::PathBuf; use std::process::Command; -use sysinfo::System; use std::time::Duration; +use sysinfo::System; use tokio::time::sleep; -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(); - let files = ["SingletonLock", "SingletonCookie", "SingletonSocket", "DevToolsActivePort"]; +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); - for (_pid, process) in sys.processes() { - let name_match = process.name().to_string_lossy().contains("Codex"); - let exe_match = process.exe() - .map(|e| e.to_string_lossy().contains("/Applications/Codex.app/")) + let name_patterns = kind.process_name_patterns(); + let bin_patterns = kind.binary_path_patterns(); + for process in sys.processes().values() { + 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| { + 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 +53,27 @@ 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,45 +89,81 @@ 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); } } sleep(Duration::from_millis(500)).await; } - + 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..8977f4b 100644 --- a/src-tauri/src/cdp.rs +++ b/src-tauri/src/cdp.rs @@ -1,10 +1,12 @@ +use crate::config::AgentKind; +use futures_util::{SinkExt, StreamExt}; use serde::Deserialize; use serde_json::Value; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; -use tokio_tungstenite::connect_async; +use tokio::net::TcpStream; use tokio_tungstenite::tungstenite::Message; -use futures_util::{StreamExt, SinkExt}; +use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; #[derive(Debug, Deserialize)] pub struct Target { @@ -22,29 +24,48 @@ pub async fn list_targets(port: u16) -> Result, String> { let response = reqwest::get(&url) .await .map_err(|e| format!("HTTP error listTargets: {}", e))?; - + let targets: Vec = response .json() .await .map_err(|e| format!("Failed to parse targets: {}", e))?; - + 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 { @@ -54,10 +75,12 @@ async fn make_cdp_request( "method": method, "params": params }); - - ws_stream.send(Message::Text(payload.to_string().into())).await + + ws_stream + .send(Message::Text(payload.to_string().into())) + .await .map_err(|e| format!("Failed to send CDP message: {}", e))?; - + tokio::time::timeout(Duration::from_secs(10), async { loop { if let Some(msg_result) = ws_stream.next().await { @@ -84,34 +107,63 @@ 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 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 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, _): (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?; - - let result = make_cdp_request(&mut ws_stream, "Page.addScriptToEvaluateOnNewDocument", serde_json::json!({ "source": script })).await?; - - let identifier = result.get("identifier") + make_cdp_request( + &mut ws_stream, + "Runtime.evaluate", + serde_json::json!({ "expression": script }), + ) + .await?; + + let result = make_cdp_request( + &mut ws_stream, + "Page.addScriptToEvaluateOnNewDocument", + serde_json::json!({ "source": script }), + ) + .await?; + + let identifier = result + .get("identifier") .and_then(|v| v.as_str()) .ok_or("Failed to get identifier")? .to_string(); - + let _ = ws_stream.close(None).await; 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 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 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, _): (WebSocketStream>, _) = + connect_async(ws_url.as_str()) + .await + .map_err(|e| format!("WebSocket connect failed: {}", e))?; + let clear_script = r#" (function() { const style = document.getElementById('agent-theme-style'); @@ -119,28 +171,45 @@ pub async fn clear_theme(port: u16, identifier: Option<&str>) -> Result<(), Stri console.log('Agent theme cleared.'); })(); "#; - + make_cdp_request(&mut ws_stream, "Page.enable", serde_json::json!({})).await?; - make_cdp_request(&mut ws_stream, "Runtime.evaluate", serde_json::json!({ "expression": clear_script })).await?; - + make_cdp_request( + &mut ws_stream, + "Runtime.evaluate", + serde_json::json!({ "expression": clear_script }), + ) + .await?; + if let Some(id) = identifier { - let _ = make_cdp_request(&mut ws_stream, "Page.removeScriptToEvaluateOnNewDocument", serde_json::json!({ "identifier": id })).await; + let _ = make_cdp_request( + &mut ws_stream, + "Page.removeScriptToEvaluateOnNewDocument", + serde_json::json!({ "identifier": id }), + ) + .await; } - + let _ = ws_stream.close(None).await; 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 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 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, _): (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?; - + let _ = ws_stream.close(None).await; Ok(()) } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 4cbcdc6..6c2cd4e 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -1,8 +1,91 @@ +use lazy_static::lazy_static; 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, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum AgentKind { + #[default] + Codex, + Antigravity, +} + +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")] @@ -11,6 +94,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 +105,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..b53ecf8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,12 +1,14 @@ -pub mod config; pub mod agent; pub mod cdp; +pub mod config; pub mod theme; -use tauri::{AppHandle, State, Manager}; +use crate::config::{load_config, update_config, AgentKind, AppConfig}; +use crate::theme::{ + delete_custom_theme, generate_injection_script, get_theme, get_themes, save_custom_theme, Theme, +}; use std::sync::Mutex; -use crate::config::{AppConfig, update_config, load_config}; -use crate::theme::{get_themes, get_theme, save_custom_theme, delete_custom_theme, generate_injection_script, Theme}; +use tauri::{AppHandle, Manager, State}; struct AppState { pub cdp_port: Mutex>, @@ -23,70 +25,106 @@ async fn set_enabled(enabled: bool) -> Result { Ok(update_config(|c| c.enabled = enabled)) } +#[tauri::command] +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; + c.active_identifier = None; + })) +} + +#[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> { +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); } } - + let port = *state.cdp_port.lock().unwrap(); if let Some(p) = port { - let script = generate_injection_script(&theme)?; - - let identifier = cdp::inject_theme(p, &script).await?; - + let script = generate_injection_script(&theme, &kind)?; + + let identifier = cdp::inject_theme(p, &kind, &script).await?; + update_config(|c| { c.enabled = true; c.selected_theme_id = theme_id.clone(); c.active_identifier = Some(identifier.clone()); }); - + *state.active_identifier.lock().unwrap() = Some(identifier); } - + Ok(()) } #[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() { - let ident = state.active_identifier.lock().unwrap().clone().or(config.active_identifier); - cdp::clear_theme(p, ident.as_deref()).await?; - + if agent::is_agent_process_running(&kind) { + let ident = state + .active_identifier + .lock() + .unwrap() + .clone() + .or(config.active_identifier); + cdp::clear_theme(p, &kind, ident.as_deref()).await?; + update_config(|c| { c.enabled = false; c.active_identifier = None; @@ -94,7 +132,7 @@ async fn clear_theme(state: State<'_, AppState>) -> Result<(), String> { *state.active_identifier.lock().unwrap() = None; } } - + Ok(()) } @@ -104,7 +142,11 @@ async fn get_all_themes(app: AppHandle) -> Result, String> { } #[tauri::command] -async fn upload_custom_theme(_app: AppHandle, bg_base64: String, preview_base64: String) -> Result<(), String> { +async fn upload_custom_theme( + _app: AppHandle, + bg_base64: String, + preview_base64: String, +) -> Result<(), String> { save_custom_theme(&bg_base64, &preview_base64)?; Ok(()) } @@ -120,7 +162,10 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_log::Builder::default().build()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { - let _ = app.get_webview_window("main").expect("no main window").set_focus(); + let _ = app + .get_webview_window("main") + .expect("no main window") + .set_focus(); })) .manage(AppState { cdp_port: Mutex::new(None), @@ -129,6 +174,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,21 +187,25 @@ 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 { - update_config(|c| { c.active_identifier = Some(ident.clone()); }); + 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); } } @@ -163,7 +214,7 @@ pub fn run() { } } }); - + Ok(()) }) .run(tauri::generate_context!()) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ad5fe83..69c3a72 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - app_lib::run(); + app_lib::run(); } diff --git a/src-tauri/src/theme.rs b/src-tauri/src/theme.rs index 4692cf9..3dd75cb 100644 --- a/src-tauri/src/theme.rs +++ b/src-tauri/src/theme.rs @@ -1,8 +1,8 @@ +use crate::config::{get_config_dir, AgentKind}; +use base64::{engine::general_purpose::STANDARD as base64_engine, Engine as _}; 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 tauri::AppHandle; use tauri::Manager; @@ -29,13 +29,17 @@ pub fn get_internal_themes_dir(app: &AppHandle) -> PathBuf { // 1. Tauri resource_dir (bundled apps + some dev setups) if let Ok(res_dir) = app.path().resource_dir() { let themes = res_dir.join("themes"); - if themes.exists() { return themes; } + if themes.exists() { + return themes; + } } // 2. macOS bundle: exe at Contents/MacOS/, themes at Contents/Resources/themes if let Ok(exe) = std::env::current_exe() { if let Some(contents) = exe.parent().and_then(|p| p.parent()) { let themes = contents.join("Resources").join("themes"); - if themes.exists() { return themes; } + if themes.exists() { + return themes; + } } } // 3. Dev fallback: CWD is src-tauri, themes is ../themes @@ -49,7 +53,7 @@ pub fn get_custom_theme_dir() -> PathBuf { pub fn get_themes(app: &AppHandle) -> Vec { let mut themes = vec![]; let internal_dir = get_internal_themes_dir(app); - + if internal_dir.exists() { if let Ok(entries) = fs::read_dir(&internal_dir) { for entry in entries.flatten() { @@ -104,10 +108,16 @@ pub fn save_custom_theme(bg_base64: &str, preview_base64: &str) -> Result<(), St let preview_data = parse_base64_data_uri(preview_base64)?; if bg_data.len() > MAX_THEME_IMAGE_BYTES { - return Err(format!("Background image too large ({}MB max)", MAX_THEME_IMAGE_BYTES / 1024 / 1024)); + return Err(format!( + "Background image too large ({}MB max)", + MAX_THEME_IMAGE_BYTES / 1024 / 1024 + )); } if preview_data.len() > MAX_THEME_IMAGE_BYTES { - return Err(format!("Preview image too large ({}MB max)", MAX_THEME_IMAGE_BYTES / 1024 / 1024)); + return Err(format!( + "Preview image too large ({}MB max)", + MAX_THEME_IMAGE_BYTES / 1024 / 1024 + )); } fs::write(custom_dir.join("bg.jpg"), bg_data).map_err(|e| e.to_string())?; @@ -130,13 +140,31 @@ 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" }; - let bg_data_uri = format!("data:image/{};base64,{}", bg_ext, base64_engine.encode(&bg_bytes)); + 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) + ); - let script = format!(r#" + let script = format!( + r#" (function() {{ // Clear existing theme first const existingStyle = document.getElementById('agent-theme-style'); @@ -175,7 +203,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; @@ -210,7 +238,114 @@ pub fn generate_injection_script(theme: &Theme) -> Result { console.log('Agent theme applied successfully.'); }})(); - "#, bg_data_uri); + "#, + bg_data_uri + ); + + 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..18cedc4 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); + } else { + 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..ddad87e 100644 --- a/web/dist/bundle.js +++ b/web/dist/bundle.js @@ -1,538 +1,568 @@ -(() => { - 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); + } + } + 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); } - 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 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); } - 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 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})`; + } + } + 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); + } else { + 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); } - btnSaveCrop.addEventListener("click", async () => { - const cropped = performCrop(); - if (!cropped) { - alert("\u88C1\u526A\u51FA\u9519\uFF0C\u8BF7\u91CD\u8BD5\uFF01"); - return; + }); + 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..d65738a 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -3,20 +3,28 @@ - Codex Theme Companion + Agent Theme Companion -
+
@@ -26,17 +34,25 @@
🎨
-

Codex Theme Companion

-

让你的 Codex App 焕发个性色彩

+

Agent Theme Companion

+

让你的 Agent App 焕发个性色彩

- Codex 状态 + Agent 应用 +
+ + +
+
+ +
+ Agent 状态
- - 检测中... + + 检测中...
@@ -46,7 +62,7 @@

Codex Theme Companion

- 自动启动 Codex + 自动启动 Agent
- - + +
@@ -82,18 +98,18 @@

选择背景主题

- +
- +