diff --git a/libs/cua-driver/rust/crates/pip-preview/src/lib.rs b/libs/cua-driver/rust/crates/pip-preview/src/lib.rs index e54c9698b5..9ea83e2a10 100644 --- a/libs/cua-driver/rust/crates/pip-preview/src/lib.rs +++ b/libs/cua-driver/rust/crates/pip-preview/src/lib.rs @@ -17,14 +17,27 @@ use std::sync::OnceLock; /// Canonical `~/.cua-driver/config.json` path matching what the per-platform -/// `set_config` tools write to. Returns `None` when `$HOME` is unset -/// (sandboxed CI). +/// `set_config` tools write to. Resolves `$HOME` first (Unix/macOS) and falls +/// back to `%USERPROFILE%` (Windows, where `HOME` is usually unset). Returns +/// `None` when neither is set (sandboxed CI). pub fn default_config_path() -> Option { - std::env::var("HOME") - .ok() + std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) .map(|h| std::path::PathBuf::from(h).join(".cua-driver").join("config.json")) } +/// Read a single key from `~/.cua-driver/config.json` as a raw JSON value, +/// returning `None` when the file is missing/malformed or the key is absent. +/// Used by the per-platform `load_driver_config` helpers to rehydrate the +/// in-memory `DriverConfig` at process startup so `set_config` writes survive +/// across stateless `cua-driver call` invocations. +pub fn read_config_value(key: &str) -> Option { + let path = default_config_path()?; + let text = std::fs::read_to_string(&path).ok()?; + let json: serde_json::Value = serde_json::from_str(&text).ok()?; + json.get(key).cloned() +} + /// Merge a single `key`/`value` into `~/.cua-driver/config.json`, /// preserving any other keys that are already there. Used by the /// per-platform `set_config` tools to persist `experimental_pip` / diff --git a/libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs b/libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs index edbd49b1cb..66cfb6c0a3 100644 --- a/libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs +++ b/libs/cua-driver/rust/crates/platform-linux/src/tools/impl_.rs @@ -26,6 +26,22 @@ impl Default for DriverConfig { fn default() -> Self { Self { capture_mode: "som".into(), max_image_dimension: 1568 } } } +/// Load `DriverConfig` from `~/.cua-driver/config.json`, falling back to +/// defaults for any missing/malformed keys. Called once at `ToolState` +/// construction (i.e. on every fresh `cua-driver call` process) so that a +/// prior `set_config capture_mode=vision` survives across stateless one-shot +/// invocations — matching the macOS daemon's startup load. See #2008. +pub fn load_driver_config() -> DriverConfig { + let mut cfg = DriverConfig::default(); + if let Some(v) = pip_preview::read_config_value("capture_mode").and_then(|v| v.as_str().map(str::to_owned)) { + cfg.capture_mode = v; + } + if let Some(v) = pip_preview::read_config_value("max_image_dimension").and_then(|v| v.as_u64()) { + if let Ok(v32) = u32::try_from(v) { cfg.max_image_dimension = v32; } + } + cfg +} + pub struct ResizeRegistry { ratios: std::sync::Mutex>, } @@ -91,7 +107,7 @@ impl ToolState { resize_registry: Arc::new(ResizeRegistry::new()), zoom_registry: Arc::new(ZoomRegistry::new()), mouse_hold: std::sync::Mutex::new(Default::default()), - config: Arc::new(RwLock::new(DriverConfig::default())), + config: Arc::new(RwLock::new(load_driver_config())), }) } } @@ -3410,11 +3426,23 @@ impl Tool for SetConfigTool { ) { match key { "capture_mode" => match val.as_str() { - Some(s) => { cfg.capture_mode = s.to_owned(); parts.push(format!("capture_mode={s}")); } + Some(s) => { + cfg.capture_mode = s.to_owned(); + if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(s.to_owned())) { + tracing::warn!("set_config: failed to persist capture_mode: {e}"); + } + parts.push(format!("capture_mode={s}")); + } None => return ToolResult::error(format!("`capture_mode` must be a string, got {val}.")), }, "max_image_dimension" => match val.as_u64() { - Some(n) => { cfg.max_image_dimension = n as u32; parts.push(format!("max_image_dimension={n}")); } + Some(n) => { + cfg.max_image_dimension = n as u32; + if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(n)) { + tracing::warn!("set_config: failed to persist max_image_dimension: {e}"); + } + parts.push(format!("max_image_dimension={n}")); + } None => return ToolResult::error(format!("`max_image_dimension` must be an integer, got {val}.")), }, "experimental_pip" => match val.as_bool() { @@ -3447,11 +3475,17 @@ impl Tool for SetConfigTool { } // Legacy per-field shape. if let Some(mode) = args.opt_str("capture_mode") { + if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(mode.clone())) { + tracing::warn!("set_config: failed to persist capture_mode: {e}"); + } parts.push(format!("capture_mode={mode}")); cfg.capture_mode = mode; } if let Some(dim) = args.opt_u64("max_image_dimension") { cfg.max_image_dimension = dim as u32; + if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(dim)) { + tracing::warn!("set_config: failed to persist max_image_dimension: {e}"); + } parts.push(format!("max_image_dimension={dim}")); } if let Some(enabled) = args.get("experimental_pip").and_then(|v| v.as_bool()) { diff --git a/libs/cua-driver/rust/crates/platform-windows/src/tools/impl_.rs b/libs/cua-driver/rust/crates/platform-windows/src/tools/impl_.rs index 9fb49e2059..1e4249c6db 100644 --- a/libs/cua-driver/rust/crates/platform-windows/src/tools/impl_.rs +++ b/libs/cua-driver/rust/crates/platform-windows/src/tools/impl_.rs @@ -166,6 +166,22 @@ impl Default for DriverConfig { fn default() -> Self { Self { capture_mode: "som".into(), max_image_dimension: 1568 } } } +/// Load `DriverConfig` from `~/.cua-driver/config.json`, falling back to +/// defaults for any missing/malformed keys. Called once at `ToolState` +/// construction (i.e. on every fresh `cua-driver call` process) so that a +/// prior `set_config capture_mode=vision` survives across stateless one-shot +/// invocations — matching the macOS daemon's startup load. See #2008. +pub fn load_driver_config() -> DriverConfig { + let mut cfg = DriverConfig::default(); + if let Some(v) = pip_preview::read_config_value("capture_mode").and_then(|v| v.as_str().map(str::to_owned)) { + cfg.capture_mode = v; + } + if let Some(v) = pip_preview::read_config_value("max_image_dimension").and_then(|v| v.as_u64()) { + if let Ok(v32) = u32::try_from(v) { cfg.max_image_dimension = v32; } + } + cfg +} + pub struct ResizeRegistry { ratios: std::sync::Mutex>, } @@ -219,7 +235,7 @@ impl ToolState { cursor_registry: Arc::new(CursorRegistry::new()), resize_registry: Arc::new(ResizeRegistry::new()), zoom_registry: Arc::new(ZoomRegistry::new()), - config: Arc::new(RwLock::new(DriverConfig::default())), + config: Arc::new(RwLock::new(load_driver_config())), }) } } @@ -5014,11 +5030,23 @@ impl Tool for SetConfigTool { ) { match key { "capture_mode" => match val.as_str() { - Some(s) => { cfg.capture_mode = s.to_owned(); applied = true; } + Some(s) => { + cfg.capture_mode = s.to_owned(); + if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(s.to_owned())) { + tracing::warn!("set_config: failed to persist capture_mode: {e}"); + } + applied = true; + } None => return ToolResult::error(format!("`capture_mode` must be a string, got {val}.")), }, "max_image_dimension" => match val.as_u64() { - Some(n) => { cfg.max_image_dimension = n as u32; applied = true; } + Some(n) => { + cfg.max_image_dimension = n as u32; + if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(n)) { + tracing::warn!("set_config: failed to persist max_image_dimension: {e}"); + } + applied = true; + } None => return ToolResult::error(format!("`max_image_dimension` must be an integer, got {val}.")), }, "experimental_pip" => match val.as_bool() { @@ -5051,10 +5079,18 @@ impl Tool for SetConfigTool { } // Legacy per-field shape. if let Some(mode) = args.get("capture_mode").and_then(|v| v.as_str()) { - cfg.capture_mode = mode.to_owned(); applied = true; + cfg.capture_mode = mode.to_owned(); + if let Err(e) = pip_preview::write_config_key("capture_mode", Value::String(mode.to_owned())) { + tracing::warn!("set_config: failed to persist capture_mode: {e}"); + } + applied = true; } if let Some(dim) = args.get("max_image_dimension").and_then(|v| v.as_u64()) { - cfg.max_image_dimension = dim as u32; applied = true; + cfg.max_image_dimension = dim as u32; + if let Err(e) = pip_preview::write_config_key("max_image_dimension", Value::from(dim)) { + tracing::warn!("set_config: failed to persist max_image_dimension: {e}"); + } + applied = true; } if let Some(enabled) = args.get("experimental_pip").and_then(|v| v.as_bool()) { if let Err(e) = pip_preview::write_config_key("experimental_pip", Value::Bool(enabled)) {