Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
42 changes: 21 additions & 21 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}
143 changes: 98 additions & 45 deletions src-tauri/src/agent.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<u16> {
let file = get_devtools_port_file();
pub fn read_port_from_file(kind: &AgentKind) -> Option<u16> {
let file = get_devtools_port_file(kind);
if !file.exists() {
return None;
}
Expand All @@ -72,45 +89,81 @@ pub fn read_port_from_file() -> Option<u16> {
None
}

pub async fn launch_agent(force_clean: bool) -> Result<u16, String> {
pub async fn launch_agent(kind: &AgentKind, force_clean: bool) -> Result<u16, String> {
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(&current_kind())
}

/// Convenience: get the DevTools port for the selected agent.
pub fn current_port() -> Option<u16> {
read_port_from_file(&current_kind())
}
Loading
Loading