diff --git a/config.example.toml b/config.example.toml index 49e3551..8845749 100644 --- a/config.example.toml +++ b/config.example.toml @@ -80,3 +80,4 @@ model = "claude-sonnet-4-5" # browser = "chrome-headless" # Default. Alternative: "safari" # chrome_binary = "/path/to/chrome" # Optional: custom Chrome path # chromedriver_binary = "/path/to/driver" # Optional: custom ChromeDriver path +# persistent_chrome = true # Keep chromedriver running between sessions for faster startup diff --git a/crates/g3-config/src/lib.rs b/crates/g3-config/src/lib.rs index c57e727..adcad7d 100644 --- a/crates/g3-config/src/lib.rs +++ b/crates/g3-config/src/lib.rs @@ -179,6 +179,10 @@ pub struct WebDriverConfig { pub chromedriver_binary: Option, #[serde(default)] pub browser: WebDriverBrowser, + #[serde(default)] + /// Keep chromedriver running after session ends for faster subsequent startups + /// When true, chromedriver process is not killed on webdriver_quit + pub persistent_chrome: bool, } impl Default for AgentConfig { diff --git a/crates/g3-core/src/tools/webdriver.rs b/crates/g3-core/src/tools/webdriver.rs index 959177d..4fa7bc5 100644 --- a/crates/g3-core/src/tools/webdriver.rs +++ b/crates/g3-core/src/tools/webdriver.rs @@ -11,6 +11,25 @@ use crate::ToolCall; use super::executor::ToolContext; +// ───────────────────────────────────────────────────────────────────────────── +// Port checking helpers +// ───────────────────────────────────────────────────────────────────────────── + +/// Check if chromedriver is already running on the given port. +async fn check_chromedriver_running(port: u16) -> bool { + // Try to connect to the chromedriver status endpoint + let url = format!("http://localhost:{}/status", port); + match reqwest::Client::new() + .get(&url) + .timeout(std::time::Duration::from_millis(500)) + .send() + .await + { + Ok(response) => response.status().is_success(), + Err(_) => false, + } +} + // ───────────────────────────────────────────────────────────────────────────── // Session helpers // ───────────────────────────────────────────────────────────────────────────── @@ -119,6 +138,32 @@ async fn start_safari_driver(ctx: &ToolContext<'_, W>) -> Result(ctx: &ToolContext<'_, W>) -> Result { let port = ctx.config.webdriver.chrome_port; + // Check if chromedriver is already running on this port + let already_running = check_chromedriver_running(port).await; + + if already_running { + // Try to connect to existing chromedriver + let driver_result = match &ctx.config.webdriver.chrome_binary { + Some(binary) => { + g3_computer_control::ChromeDriver::with_port_headless_and_binary(port, Some(binary)) + .await + } + None => g3_computer_control::ChromeDriver::with_port_headless(port).await, + }; + + if let Ok(driver) = driver_result { + let session = + std::sync::Arc::new(tokio::sync::Mutex::new(WebDriverSession::Chrome(driver))); + *ctx.webdriver_session.write().await = Some(session); + // Don't store process - we didn't start it + return Ok( + "✅ WebDriver session started (reusing existing chromedriver)." + .to_string(), + ); + } + // If connection failed, fall through to start a new one + } + // Use configured chromedriver binary or fall back to 'chromedriver' in PATH let chromedriver_cmd = ctx .config @@ -599,22 +644,33 @@ pub async fn execute_webdriver_quit( Ok(_) => { debug!("WebDriver session closed successfully"); - // Kill the driver process - if let Some(mut process) = ctx.webdriver_process.write().await.take() { + // Kill the driver process (unless persistent_chrome is enabled for Chrome) + use g3_config::WebDriverBrowser; + let is_chrome = matches!(&ctx.config.webdriver.browser, WebDriverBrowser::ChromeHeadless); + let keep_running = is_chrome && ctx.config.webdriver.persistent_chrome; + + if keep_running { + debug!("Keeping chromedriver running (persistent_chrome enabled)"); + // Still take the process handle but don't kill it + let _ = ctx.webdriver_process.write().await.take(); + } else if let Some(mut process) = ctx.webdriver_process.write().await.take() { if let Err(e) = process.kill().await { warn!("Failed to kill driver process: {}", e); } else { debug!("Driver process terminated"); } - } + } // Return appropriate message based on browser type - use g3_config::WebDriverBrowser; let driver_name = match &ctx.config.webdriver.browser { WebDriverBrowser::Safari => "safaridriver", WebDriverBrowser::ChromeHeadless => "chromedriver", }; - Ok(format!("✅ WebDriver session closed and {} stopped", driver_name)) + if keep_running { + Ok(format!("✅ WebDriver session closed ({} still running for reuse)", driver_name)) + } else { + Ok(format!("✅ WebDriver session closed and {} stopped", driver_name)) + } } Err(e) => Ok(format!("❌ Failed to quit WebDriver: {}", e)), }