diff --git a/crates/pi-natives/src/desktop/linux/ax.rs b/crates/pi-natives/src/desktop/linux/ax.rs index f27e1616b..1b37980d6 100644 --- a/crates/pi-natives/src/desktop/linux/ax.rs +++ b/crates/pi-natives/src/desktop/linux/ax.rs @@ -104,16 +104,6 @@ impl AtSpiAx { }) } - pub(crate) fn raise_window(&mut self, id: &str) -> CoreResult<()> { - let windows = self.windows()?; - let window = windows - .into_iter() - .find(|window| window.id == id) - .ok_or_else(|| DesktopError::window_not_found(format!("Wayland window {id} not found")))?; - let root = self.window_root(&window)?; - self.focus(&root) - } - async fn apps( connection: &atspi::AccessibilityConnection, ) -> Result, String> { diff --git a/crates/pi-natives/src/desktop/linux/wayland/mod.rs b/crates/pi-natives/src/desktop/linux/wayland/mod.rs index b949166fb..5638fc936 100644 --- a/crates/pi-natives/src/desktop/linux/wayland/mod.rs +++ b/crates/pi-natives/src/desktop/linux/wayland/mod.rs @@ -42,25 +42,19 @@ impl WaylandBackend { Self { display, ax, ax_error, input, input_error, displays: Vec::new() } } - fn background_error(target: &Target, kind: &str) -> CoreResult<()> { + fn window_input_error(target: &Target, kind: &str) -> CoreResult<()> { if let Target::Window(id) = target { return Err(DesktopError::background_unavailable(format!( - "window {id} wayland-compositor-focus-only: Wayland cannot target a non-focused \ - window for {kind}; use ax actions or delivery:\"foreground\"" + "window {id} wayland-compositor-focus-only: Wayland cannot programmatically activate \ + a non-focused window for {kind}; only the currently focused surface is reachable; \ + use ax actions or desktop input" ))); } Ok(()) } - fn prepare_input(&mut self, target: &Target, mode: DeliveryMode, kind: &str) -> CoreResult<()> { - if mode == DeliveryMode::Background { - Self::background_error(target, kind)?; - } - if mode == DeliveryMode::Foreground - && let Target::Window(id) = target - { - self.raise_window(id)?; - } + fn prepare_input(&self, target: &Target, kind: &str) -> CoreResult<()> { + Self::window_input_error(target, kind)?; if self.input.is_none() { return Err(self.input_error.clone().unwrap_or_else(|| { DesktopError::permission_denied( @@ -110,7 +104,7 @@ impl Backend for WaylandBackend { input: self.input.is_some(), ax: self.ax.is_some(), background_window_input: false, - delivery_modes: vec!["foreground".to_string(), "background".to_string()], + delivery_modes: vec!["background".to_string()], capture_permission: "prompt-or-granted".to_string(), input_permission: if self.input.is_some() { "granted".to_string() @@ -200,9 +194,9 @@ impl Backend for WaylandBackend { target: &Target, ev: PointerEvent, _frame: &FrameGeometry, - mode: DeliveryMode, + _mode: DeliveryMode, ) -> CoreResult<()> { - self.prepare_input(target, mode, "pointer input")?; + self.prepare_input(target, "pointer input")?; self .input .as_mut() @@ -210,8 +204,8 @@ impl Backend for WaylandBackend { .pointer(ev) } - fn type_text(&mut self, target: &Target, text: &str, mode: DeliveryMode) -> CoreResult<()> { - self.prepare_input(target, mode, "keyboard input")?; + fn type_text(&mut self, target: &Target, text: &str, _mode: DeliveryMode) -> CoreResult<()> { + self.prepare_input(target, "keyboard input")?; self .input .as_mut() @@ -223,9 +217,9 @@ impl Backend for WaylandBackend { &mut self, target: &Target, keys: &[KeyName], - mode: DeliveryMode, + _mode: DeliveryMode, ) -> CoreResult<()> { - self.prepare_input(target, mode, "keyboard input")?; + self.prepare_input(target, "keyboard input")?; self .input .as_mut() @@ -234,16 +228,10 @@ impl Backend for WaylandBackend { } fn raise_window(&mut self, id: &str) -> CoreResult<()> { - self - .ax - .as_mut() - .ok_or_else(|| { - self - .ax_error - .clone() - .unwrap_or_else(DesktopError::ax_unsupported) - })? - .raise_window(id) + Err(DesktopError::background_unavailable(format!( + "window {id} wayland-compositor-focus-only: Wayland cannot programmatically activate a \ + non-focused window; only the currently focused surface is reachable" + ))) } fn ax(&mut self) -> Option<&mut dyn AxBackend> { @@ -255,12 +243,50 @@ impl Backend for WaylandBackend { mod tests { use super::*; + fn backend_without_services() -> WaylandBackend { + WaylandBackend { + display: DisplaySelector::All, + ax: None, + ax_error: None, + input: None, + input_error: None, + displays: Vec::new(), + } + } + #[test] - fn window_background_delivery_is_structurally_rejected() { + fn window_foreground_delivery_reports_compositor_constraint() { + let mut backend = backend_without_services(); let target = Target::Window("w1".to_string()); - let err = WaylandBackend::background_error(&target, "pointer input") - .expect_err("window background input must fail"); + let err = backend + .type_text(&target, "hello", DeliveryMode::Foreground) + .expect_err("window foreground input must fail"); assert_eq!(err.code.as_str(), "BackgroundUnavailable"); - assert!(err.message.contains("wayland-compositor-focus-only")); + assert_eq!( + err.message, + "window w1 wayland-compositor-focus-only: Wayland cannot programmatically activate a \ + non-focused window for keyboard input; only the currently focused surface is reachable; \ + use ax actions or desktop input" + ); + } + + #[test] + fn window_raise_reports_compositor_constraint() { + let mut backend = backend_without_services(); + let err = backend + .raise_window("w1") + .expect_err("Wayland window raise must fail"); + assert_eq!(err.code.as_str(), "BackgroundUnavailable"); + assert_eq!( + err.message, + "window w1 wayland-compositor-focus-only: Wayland cannot programmatically activate a \ + non-focused window; only the currently focused surface is reachable" + ); + } + + #[test] + fn capabilities_do_not_advertise_foreground_delivery() { + let mut backend = backend_without_services(); + assert_eq!(backend.capabilities().delivery_modes, ["background"]); } } diff --git a/docs/computer-use.md b/docs/computer-use.md index e5c095091..a6dbd938e 100644 --- a/docs/computer-use.md +++ b/docs/computer-use.md @@ -89,7 +89,7 @@ The `desktop` object exposes the same screenshot and input surface for the all-d Pixel coordinates always belong to the most recent screenshot of the same target. Coordinate input before that capture is rejected. A resized/closed target or changed display layout invalidates the frame; capture again instead of guessing. Screenshots display automatically and are also saved at the captured resolution, subject to `computer.maxWidth` / `computer.maxHeight` and any effective model-transport cap. When a capture is scaled, the tool reports both the saved capture dimensions and the native source dimensions. `{ silent: true }` suppresses display in loops. -Input defaults to `delivery: "background"`, which avoids changing the user's focus, pointer, or window order. If the OS or application cannot target that event safely, the call throws `BackgroundUnavailable`. Use AX, or explicitly retry with `delivery: "foreground"`, which briefly activates the target and restores focus afterward. macOS keyboard delivery to one of several windows in the same app and all Wayland per-window native input require this fallback. +Input defaults to `delivery: "background"`, which avoids changing the user's focus, pointer, or window order. If the OS or application cannot target that event safely, the call throws `BackgroundUnavailable`. On macOS, use AX or explicitly retry with `delivery: "foreground"`, which briefly activates the target and restores focus afterward. Wayland compositors accept native input only for the currently focused surface and do not permit omp to activate an arbitrary window, so per-window native input and `raise()` are unavailable; use AX actions, or desktop input after focusing the target yourself. ## Accessibility-first automation diff --git a/docs/tools/computer.md b/docs/tools/computer.md index ca5e3abcf..4820cb80c 100644 --- a/docs/tools/computer.md +++ b/docs/tools/computer.md @@ -149,11 +149,11 @@ Native errors are surfaced as `ToolError` text prefixed by the stable code name: Tool/worker errors include `Computer session is closed`, `Computer worker is busy`, `Timed out starting computer worker`, `Computer code execution timed out after ms`, read-only mutation errors, and the worker-restart message above. -Recover by refreshing the exact target screenshot after coordinate-frame errors, taking a new AX snapshot after `StaleRef`, using AX or explicit foreground delivery after `BackgroundUnavailable`, and inspecting `desktop.capabilities()` for platform/permission failures. +Recover by refreshing the exact target screenshot after coordinate-frame errors, taking a new AX snapshot after `StaleRef`, using AX or a delivery mode listed by `desktop.capabilities()` after `BackgroundUnavailable`, and inspecting those capabilities for platform/permission failures. ## Platform constraints -Current native backends support macOS, Linux X11, Linux Wayland portal capture/input where available, and Windows; other targets depend on native-addon support. Capabilities and permission state are runtime facts—inspect `desktop.capabilities()` rather than assuming them. Wayland has no per-window background native input; use AX or foreground delivery. See [Scriptable computer use: Platforms](../computer-use.md#platforms) for prerequisites and permission details. +Current native backends support macOS, Linux X11, Linux Wayland portal capture/input where available, and Windows; other targets depend on native-addon support. Capabilities and permission state are runtime facts—inspect `desktop.capabilities()` rather than assuming them. Wayland compositors do not permit omp to activate arbitrary windows, so per-window native input and `raise()` are unavailable; use AX actions, or desktop input after focusing the target yourself. See [Scriptable computer use: Platforms](../computer-use.md#platforms) for prerequisites and permission details. ## Critical constraints diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 456e88609..817e22ee7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Reported Wayland per-window native input and window activation as unavailable instead of advertising a foreground-delivery path that compositors reject. ([#7702](https://github.com/can1357/oh-my-pi/issues/7702)) + ## [17.2.9] - 2026-08-05 ### Breaking Changes