From 73c80201f0bd114e6c7db41fabb1c881da24803f Mon Sep 17 00:00:00 2001 From: creatixchu Date: Thu, 24 Sep 2026 11:30:18 +0800 Subject: [PATCH] fix(cua): refuse keys before system panel input is ready --- packages/experimental/cua/native/README.txt | 4 +++- packages/experimental/cua/native/src/panels.rs | 10 ++++++++-- packages/experimental/cua/native/tests/e2e.py | 13 ++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/experimental/cua/native/README.txt b/packages/experimental/cua/native/README.txt index 065bbd6730..396e547256 100644 --- a/packages/experimental/cua/native/README.txt +++ b/packages/experimental/cua/native/README.txt @@ -26,7 +26,9 @@ Element click uses AXPress only. Coordinate click, scroll and drag validate wind The responsible host/helper needs macOS Accessibility permission for AX state and input, and Screen Recording permission for screenshots. Capability preflight and permission errors do not grant permissions. Capture refuses a window that moves or resizes while the image is being read. Unassociated system panels and accessory-only applications are not selectable. -Stable error codes are `INVALID_REQUEST` (malformed envelope), `INVALID_ARGUMENT` (invalid parameters), `UNSUPPORTED` (unknown operation), `APP_UNAVAILABLE`, `AMBIGUOUS_APP`, `STALE_APP`, `STALE_SNAPSHOT`, `WINDOW_UNAVAILABLE`, `WINDOW_CHANGED`, `PERMISSION_DENIED`, and `NATIVE_ERROR` (remaining operating-system failures). An error never promises that a partially dispatched input was rolled back. The host must not retry input automatically. +Stable error codes are `INVALID_REQUEST` (malformed envelope), `INVALID_ARGUMENT` (invalid parameters), `UNSUPPORTED` (unknown operation), `APP_UNAVAILABLE`, `AMBIGUOUS_APP`, `STALE_APP`, `STALE_SNAPSHOT`, `WINDOW_UNAVAILABLE`, `WINDOW_CHANGED`, `PERMISSION_DENIED`, `PANEL_NOT_READY` (system panel recipient unavailable before input), and `NATIVE_ERROR` (remaining operating-system failures). An error never promises that a partially dispatched input was rolled back. The host must not retry input automatically. + +Use `--panels-only` to isolate the system file-panel checks. Fixture windows can join application sets so Stage Manager does not shrink them during background input. Run `python3 packages/experimental/cua/native/tests/e2e.py` after building to exercise an isolated two-window AppKit fixture. It independently records button counts, Unicode text, actual scroll position and drag release position. The check also verifies a real NSOpenPanel sheet, path-entry keyboard routing and the selected file URL. It covers scoped snapshot replacement, screenshot dimensions, stale app cleanup and EOF shutdown. `--artifacts DIRECTORY` writes fixture-only screenshots and a private protocol transcript; the transcript contains the running-app inventory and is not a publication artifact. diff --git a/packages/experimental/cua/native/src/panels.rs b/packages/experimental/cua/native/src/panels.rs index ec0fc5f419..b336638b44 100644 --- a/packages/experimental/cua/native/src/panels.rs +++ b/packages/experimental/cua/native/src/panels.rs @@ -106,7 +106,10 @@ pub fn keyboard_pid(pid: i32) -> Result { }) .collect(); let Some((recipient, remote_window)) = panel_keyboard_target(pid, &targets)? else { - return Ok(pid); + return Err(crate::failure( + "PANEL_NOT_READY", + "System file panel has no ready input recipient; read fresh state before input", + )); }; let service = Element(unsafe { AXUIElementCreateApplication(recipient) }); let mut focus = service.attribute("AXFocusedWindow"); @@ -117,7 +120,10 @@ pub fn keyboard_pid(pid: i32) -> Result { } focus = focused.attribute("AXParent"); } - bail!("System file panel focus changed; activate the application and read fresh state"); + return Err(crate::failure( + "PANEL_NOT_READY", + "System file panel focus changed; activate the application and read fresh state", + )); } current = element.attribute("AXParent"); } diff --git a/packages/experimental/cua/native/tests/e2e.py b/packages/experimental/cua/native/tests/e2e.py index 4ea0ae2581..3db55e2e7e 100644 --- a/packages/experimental/cua/native/tests/e2e.py +++ b/packages/experimental/cua/native/tests/e2e.py @@ -41,7 +41,18 @@ def check_file_panel(call, owner, primary_params, primary, directory, wait_state assert "Increment primary" not in scoped["tree"], scoped call("activate", owner) call("getState", {**primary_params, "windowId": sheet_id, "maxElements": 2000}) - call("pressKey", {**owner, "key": "g", "modifiers": ["command", "shift"]}) + deadline = time.monotonic() + 5 + while True: + try: + call("pressKey", {**owner, "key": "g", "modifiers": ["command", "shift"]}) + break + except AssertionError as error: + # PANEL_NOT_READY guarantees no key was dispatched while ViewBridge starts. + reply = error.args[0] if error.args else None + if not isinstance(reply, dict) or reply.get("error", {}).get("code") != "PANEL_NOT_READY" or time.monotonic() >= deadline: + raise + call("getState", {**primary_params, "windowId": sheet_id, "maxElements": 2000}) + time.sleep(0.025) goto = wait_observation(lambda observed: any(e["identifier"] == "PathTextField" for e in observed["elements"]), "Command+Shift+G did not reach the system panel") path_field = next(e for e in goto["elements"] if e["identifier"] == "PathTextField")