From b393d676053fb203ee7195fdbdde68f84fdc6a20 Mon Sep 17 00:00:00 2001 From: ben Date: Tue, 10 Feb 2026 23:22:01 -0800 Subject: [PATCH] feat(desktop): default to control-chrome executable without npx (#534) --- .../skills/browser-setup-devtools/SKILL.md | 2 +- opencode.jsonc | 6 +-- packages/app/src/app/constants.ts | 4 +- .../desktop/src-tauri/src/engine/spawn.rs | 13 ++++- packages/desktop/src-tauri/src/openwrk/mod.rs | 16 +++++- packages/desktop/src-tauri/src/paths.rs | 54 +++++++++++++++++++ .../desktop/src-tauri/src/workspace/files.rs | 6 +-- 7 files changed, 88 insertions(+), 13 deletions(-) diff --git a/.opencode/skills/browser-setup-devtools/SKILL.md b/.opencode/skills/browser-setup-devtools/SKILL.md index b3f78a1d..48f216c6 100644 --- a/.opencode/skills/browser-setup-devtools/SKILL.md +++ b/.opencode/skills/browser-setup-devtools/SKILL.md @@ -24,7 +24,7 @@ description: Guide users through browser automation setup using Chrome DevTools 4. If DevTools MCP calls fail: - Ask the user to open Chrome and keep it running. - Retry `chrome-devtools_list_pages`. - - If it still fails, ensure `opencode.jsonc` includes `mcp.chrome-devtools` with command `['npx','-y','chrome-devtools-mcp@latest']` and ask the user to restart OpenWork/OpenCode. + - If it still fails, ensure `opencode.jsonc` includes `mcp.control-chrome` with command `['chrome-devtools-mcp']` and ask the user to restart OpenWork/OpenCode. - Retry the DevTools MCP check. 5. If DevTools MCP is ready: - Offer a first task ("Let's try opening a webpage"). diff --git a/opencode.jsonc b/opencode.jsonc index 4998ac5a..93679a35 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,12 +1,10 @@ { "$schema": "https://opencode.ai/config.json", "mcp": { - "chrome-devtools": { + "control-chrome": { "type": "local", "command": [ - "npx", - "-y", - "chrome-devtools-mcp@latest" + "chrome-devtools-mcp" ] } }, diff --git a/packages/app/src/app/constants.ts b/packages/app/src/app/constants.ts index 1244b60d..bb230a98 100644 --- a/packages/app/src/app/constants.ts +++ b/packages/app/src/app/constants.ts @@ -75,10 +75,10 @@ export const MCP_QUICK_CONNECT: McpDirectoryInfo[] = [ oauth: false, }, { - name: "Chrome DevTools", + name: "Control Chrome", description: "Drive Chrome tabs with browser automation.", type: "local", - command: ["npx", "-y", "chrome-devtools-mcp@latest"], + command: ["chrome-devtools-mcp"], oauth: false, }, ]; diff --git a/packages/desktop/src-tauri/src/engine/spawn.rs b/packages/desktop/src-tauri/src/engine/spawn.rs index ea8ea1fa..850cd4d9 100644 --- a/packages/desktop/src-tauri/src/engine/spawn.rs +++ b/packages/desktop/src-tauri/src/engine/spawn.rs @@ -1,11 +1,12 @@ use std::path::Path; use tauri::async_runtime::Receiver; -use tauri::AppHandle; +use tauri::{AppHandle, Manager}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; use crate::paths::{candidate_xdg_config_dirs, candidate_xdg_data_dirs, maybe_infer_xdg_home}; +use crate::paths::{prepended_path_env, sidecar_path_candidates}; pub fn find_free_port() -> Result { let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).map_err(|e| e.to_string())?; @@ -77,6 +78,16 @@ pub fn spawn_engine( command = command.env("OPENCODE_CLIENT", "openwork"); command = command.env("OPENWORK", "1"); + let resource_dir = app.path().resource_dir().ok(); + let current_bin_dir = tauri::process::current_binary(&app.env()) + .ok() + .and_then(|path| path.parent().map(|parent| parent.to_path_buf())); + let sidecar_paths = + sidecar_path_candidates(resource_dir.as_deref(), current_bin_dir.as_deref()); + if let Some(path_env) = prepended_path_env(&sidecar_paths) { + command = command.env("PATH", path_env); + } + if let Some(username) = opencode_username { if !username.trim().is_empty() { command = command.env("OPENCODE_SERVER_USERNAME", username); diff --git a/packages/desktop/src-tauri/src/openwrk/mod.rs b/packages/desktop/src-tauri/src/openwrk/mod.rs index 5cac41c4..90e75783 100644 --- a/packages/desktop/src-tauri/src/openwrk/mod.rs +++ b/packages/desktop/src-tauri/src/openwrk/mod.rs @@ -4,11 +4,12 @@ use std::path::{Path, PathBuf}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; -use tauri::AppHandle; +use tauri::{AppHandle, Manager}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; use crate::paths::home_dir; +use crate::paths::{prepended_path_env, sidecar_path_candidates}; use crate::types::{ OpenwrkBinaryState, OpenwrkDaemonState, OpenwrkOpencodeState, OpenwrkSidecarInfo, OpenwrkStatus, OpenwrkWorkspace, @@ -237,8 +238,19 @@ pub fn spawn_openwrk_daemon( } } + let mut command = command.args(args); + + let resource_dir = app.path().resource_dir().ok(); + let current_bin_dir = tauri::process::current_binary(&app.env()) + .ok() + .and_then(|path| path.parent().map(|parent| parent.to_path_buf())); + let sidecar_paths = + sidecar_path_candidates(resource_dir.as_deref(), current_bin_dir.as_deref()); + if let Some(path_env) = prepended_path_env(&sidecar_paths) { + command = command.env("PATH", path_env); + } + command - .args(args) .spawn() .map_err(|e| format!("Failed to start openwrk: {e}")) } diff --git a/packages/desktop/src-tauri/src/paths.rs b/packages/desktop/src-tauri/src/paths.rs index 2e79e569..43a51a4e 100644 --- a/packages/desktop/src-tauri/src/paths.rs +++ b/packages/desktop/src-tauri/src/paths.rs @@ -90,3 +90,57 @@ pub fn resolve_in_path(name: &str) -> Option { } None } + +pub fn sidecar_path_candidates( + resource_dir: Option<&Path>, + current_bin_dir: Option<&Path>, +) -> Vec { + let mut candidates = Vec::new(); + + if let Some(current_bin_dir) = current_bin_dir { + candidates.push(current_bin_dir.to_path_buf()); + } + + if let Some(resource_dir) = resource_dir { + candidates.push(resource_dir.join("sidecars")); + candidates.push(resource_dir.to_path_buf()); + } + + candidates.push(PathBuf::from("src-tauri/sidecars")); + + let mut unique = Vec::new(); + for candidate in candidates { + if !candidate.is_dir() { + continue; + } + if unique + .iter() + .any(|existing: &PathBuf| existing == &candidate) + { + continue; + } + unique.push(candidate); + } + + unique +} + +pub fn prepended_path_env(prefixes: &[PathBuf]) -> Option { + let mut entries = Vec::::new(); + + for prefix in prefixes { + if prefix.is_dir() { + entries.push(prefix.clone()); + } + } + + if let Some(existing) = env::var_os("PATH") { + entries.extend(env::split_paths(&existing)); + } + + if entries.is_empty() { + return None; + } + + env::join_paths(entries).ok() +} diff --git a/packages/desktop/src-tauri/src/workspace/files.rs b/packages/desktop/src-tauri/src/workspace/files.rs index 65334c59..cf2ad5ac 100644 --- a/packages/desktop/src-tauri/src/workspace/files.rs +++ b/packages/desktop/src-tauri/src/workspace/files.rs @@ -460,12 +460,12 @@ pub fn ensure_workspace_files(workspace_path: &str, preset: &str) -> Result<(), _ => serde_json::Map::new(), }; - if !mcp_obj.contains_key("chrome-devtools") { + if !mcp_obj.contains_key("control-chrome") { mcp_obj.insert( - "chrome-devtools".to_string(), + "control-chrome".to_string(), serde_json::json!({ "type": "local", - "command": ["npx", "-y", "chrome-devtools-mcp@latest"] + "command": ["chrome-devtools-mcp"] }), ); config_changed = true;