From a793acdf54ecbe0debf1170f98d625e3d5d562f0 Mon Sep 17 00:00:00 2001 From: "trycua-release[bot]" Date: Fri, 31 Jul 2026 18:38:05 -0500 Subject: [PATCH 01/16] feat(cua-driver): expose post-action window rebind targets (cherry picked from commit ef1e30abb96d3e7ffbd0007309b369e2958230d6) --- libs/cua-driver/contract/manifest.json | 532 +++++++++++++++++- .../cua-driver/docs/action-result-contract.md | 20 +- .../python/src/cua_driver/__init__.py | 4 + .../crates/cua-driver-contract/src/lib.rs | 12 +- .../crates/cua-driver-contract/src/outputs.rs | 53 +- .../crates/platform-macos/src/tools/click.rs | 15 +- .../crates/platform-macos/src/tools/drag.rs | 42 +- .../crates/platform-macos/src/tools/hotkey.rs | 1 + .../crates/platform-macos/src/tools/scroll.rs | 42 +- .../platform-macos/src/tools/set_value.rs | 1 + .../platform-macos/src/tools/type_text.rs | 1 + .../src/window_change_detector.rs | 56 ++ 12 files changed, 713 insertions(+), 66 deletions(-) diff --git a/libs/cua-driver/contract/manifest.json b/libs/cua-driver/contract/manifest.json index 9965f5f455..973351f38d 100644 --- a/libs/cua-driver/contract/manifest.json +++ b/libs/cua-driver/contract/manifest.json @@ -172,7 +172,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -181,9 +182,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -227,6 +256,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -575,7 +647,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -584,9 +657,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -630,6 +731,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -1565,7 +1709,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -1574,9 +1719,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -1620,6 +1793,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2088,7 +2304,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -2097,9 +2314,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2143,6 +2388,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2305,7 +2593,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -2314,9 +2603,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2360,6 +2677,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2542,7 +2902,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -2551,9 +2912,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2597,6 +2986,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -3026,7 +3458,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -3035,9 +3468,37 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -3430,6 +3891,49 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ diff --git a/libs/cua-driver/docs/action-result-contract.md b/libs/cua-driver/docs/action-result-contract.md index 409dbfcc7e..2ce75ba53d 100644 --- a/libs/cua-driver/docs/action-result-contract.md +++ b/libs/cua-driver/docs/action-result-contract.md @@ -31,8 +31,9 @@ Every successful action returns a closed `structuredContent` object: | `route` | `accessibility`, `synthetic_events`, `global_input`, `dom`, `trusted_input` | | `delivery.mode` | `background`, `foreground`, `not_applicable`, `unknown` | | `evidence[].kind` | `value_readback`, `window_change` | -| `escalation.target` | `pixel`, `foreground`, `page`, `session` | -| `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required` | +| `window_change` | read-only `new_windows` plus `foreground_changed` topology facts | +| `escalation.target` | `pixel`, `foreground`, `page`, `session`, `rebind` | +| `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required`, `surface_changed` | The action-result tools are: @@ -45,9 +46,11 @@ The action-result tools are: Other mutating tools such as application launch, window activation, browser navigation, dialogs, uploads, and downloads retain their own typed results. -The contract is deliberately closed. It does not echo selectors, coordinates, -scope, targets, platform transport names, diagnostic pointers, or the old -`verified` boolean. +The contract is deliberately closed. It does not echo request selectors, +coordinates, scope, platform transport names, diagnostic pointers, or the old +`verified` boolean. A post-action `window_change` is the one target-bearing +exception: it reports newly observed native windows so a caller can leave a +stale or blocked anchor without activating the replacement surface. The invariants are: @@ -123,6 +126,13 @@ An optional escalation is advice, not an automatic retry: | `foreground` | explicitly select foreground delivery when session policy permits | | `page` | bind the native window to a supported browser page route | | `session` | prepare or explicitly widen the session only when policy permits | +| `rebind` | bind to `escalation.window` when present, otherwise correlate `window_change.new_windows` with `list_windows`; refresh window state before acting | + +Handle `rebind` before pixel, page, or foreground retries. The driver discovers +the new surface through a read-only post-action window diff and preserves the +existing focus guard; rebinding is logical and must not raise the window. When +multiple candidates are reported, refresh `list_windows`, select the blocking +surface, and resume background window-scoped interaction. SDK integrators, OpenClaw, Hermes, and other agent hosts can implement different policies above this same narrow fact contract without duplicating platform diff --git a/libs/cua-driver/python/src/cua_driver/__init__.py b/libs/cua-driver/python/src/cua_driver/__init__.py index 5a83149c70..65477f52e6 100644 --- a/libs/cua-driver/python/src/cua_driver/__init__.py +++ b/libs/cua-driver/python/src/cua_driver/__init__.py @@ -58,6 +58,8 @@ ActionEvidenceKind, ActionResult, ActionRoute, + ActionWindowChange, + ActionWindowTarget, BoundsExpectation, CaptureScope, ClickButton, @@ -180,6 +182,8 @@ def _create_private_worker_python_sdk(cls, options): "ActionEvidenceKind", "ActionResult", "ActionRoute", + "ActionWindowChange", + "ActionWindowTarget", "BoundsExpectation", "CaptureScope", "ClickButton", diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs index a5a72e84dc..506bd1b6db 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs @@ -39,12 +39,12 @@ pub use inputs::{ pub use outputs::{ advertised_output_schema, refusal_envelope_schema, ActionDelivery, ActionDeliveryMode, ActionEffect, ActionEscalation, ActionEscalationReason, ActionEscalationTarget, ActionEvidence, - ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, - ClipboardReadOutput, ClipboardWriteOutput, CursorMotionOutput, CursorPointOutput, - CursorPositionOutput, CursorThemeOutput, CursorVisualOutput, DesktopStateOutput, - EffectiveScope, EndSessionOutput, GetAgentCursorStateOutput, ListSessionsOutput, - ScreenSizeOutput, SessionClientKindOutput, SessionLifecycleState, SessionOutput, - SessionStateOutput, SessionTransportOutput, SetAgentCursorEnabledOutput, + ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, ActionWindowChange, + ActionWindowTarget, ClipboardReadOutput, ClipboardWriteOutput, CursorMotionOutput, + CursorPointOutput, CursorPositionOutput, CursorThemeOutput, CursorVisualOutput, + DesktopStateOutput, EffectiveScope, EndSessionOutput, GetAgentCursorStateOutput, + ListSessionsOutput, ScreenSizeOutput, SessionClientKindOutput, SessionLifecycleState, + SessionOutput, SessionStateOutput, SessionTransportOutput, SetAgentCursorEnabledOutput, SetAgentCursorMotionOutput, SetAgentCursorThemeOutput, StartSessionOutput, ToolOutput, }; pub use verification::{ diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs index 849ed0b0b6..26884909e2 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs @@ -450,6 +450,25 @@ pub struct ActionEvidence { pub kind: ActionEvidenceKind, } +/// A newly discovered native interaction surface after an action. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] +#[serde(deny_unknown_fields)] +pub struct ActionWindowTarget { + pub pid: i64, + pub window_id: u64, + pub app_name: String, + pub title: String, +} + +/// Read-only post-action topology change used to rediscover a blocking modal, +/// popup, or new top-level window without activating it. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] +#[serde(deny_unknown_fields)] +pub struct ActionWindowChange { + pub new_windows: Vec, + pub foreground_changed: bool, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] #[serde(rename_all = "snake_case")] pub enum ActionEscalationTarget { @@ -457,6 +476,7 @@ pub enum ActionEscalationTarget { Foreground, Page, Session, + Rebind, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] @@ -467,6 +487,7 @@ pub enum ActionEscalationReason { EffectUnconfirmed, SuspectedNoop, PermissionRequired, + SurfaceChanged, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -474,6 +495,8 @@ pub enum ActionEscalationReason { pub struct ActionEscalation { pub target: ActionEscalationTarget, pub reason: ActionEscalationReason, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub window: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -486,6 +509,8 @@ pub struct ActionResult { #[serde(default, skip_serializing_if = "Option::is_none")] pub evidence: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + pub window_change: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub escalation: Option, } @@ -625,6 +650,7 @@ mod tests { evidence: Some(vec![ActionEvidence { kind: ActionEvidenceKind::ValueReadback, }]), + window_change: None, escalation: None, } } @@ -638,7 +664,14 @@ mod tests { let properties = schema["properties"].as_object().expect("properties"); assert_eq!( properties.keys().map(String::as_str).collect::>(), - ["delivery", "effect", "escalation", "evidence", "route"] + [ + "delivery", + "effect", + "escalation", + "evidence", + "route", + "window_change", + ] ); assert_eq!( properties["effect"]["enum"], @@ -694,7 +727,7 @@ mod tests { assert_eq!(escalation["required"], json!(["target", "reason"])); assert_eq!( escalation["properties"]["target"]["enum"], - json!(["pixel", "foreground", "page", "session"]) + json!(["pixel", "foreground", "page", "session", "rebind"]) ); assert_eq!( escalation["properties"]["reason"]["enum"], @@ -703,9 +736,23 @@ mod tests { "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ]) ); + + let window_change = object_variant(&properties["window_change"]); + assert_eq!(window_change["additionalProperties"], false); + assert_eq!( + window_change["required"], + json!(["new_windows", "foreground_changed"]) + ); + let window = &window_change["properties"]["new_windows"]["items"]; + assert_eq!(window["additionalProperties"], false); + assert_eq!( + window["required"], + json!(["pid", "window_id", "app_name", "title"]) + ); } #[test] diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs index 26b45ef591..a0ce7981db 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs @@ -746,6 +746,7 @@ impl Tool for ClickTool { screenshot from get_window_state." }); } + changes.add_to_structured(&mut structured); ToolResult::text(msg).with_structured(structured) } Ok(Err(e)) => ToolResult::error(format!("AX action failed: {e}")), @@ -1168,17 +1169,19 @@ impl Tool for ClickTool { } else { ("cgevent", "background CGEvent") }; + let mut structured = serde_json::json!({ + "path": path, + "verified": false, + "effect": "unverifiable", + "focus_without_raise": focus_without_raise + }); + changes.add_to_structured(&mut structured); ToolResult::text(format!( "✅ Posted {button_label} to pid {pid} ({mode_label}; \ not driver-verified — confirm via screenshot).{}", changes.result_suffix() )) - .with_structured(serde_json::json!({ - "path": path, - "verified": false, - "effect": "unverifiable", - "focus_without_raise": focus_without_raise - })) + .with_structured(structured) } Ok(Err(e)) => ToolResult::error(format!("{button_label} failed: {e}")), Err(e) => ToolResult::error(format!("Task error: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs index f5eb9d0395..c9fa554189 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs @@ -421,23 +421,33 @@ impl Tool for DragTool { "" }; match result { - Ok(Ok(())) => ToolResult::text(format!( - "✅ Posted drag{btn_suffix}{mod_suffix} to pid {pid} \ - from window-pixel ({}, {}) → ({}, {}), \ - screen ({}, {}) → ({}, {}) \ - in {duration_ms}ms / {steps} steps{mode_label} \ - (background CGEvent; not driver-verified — confirm via screenshot).{}", - from_x as i64, from_y as i64, - to_x as i64, to_y as i64, - from_sx as i64, from_sy as i64, - to_sx as i64, to_sy as i64, - changes.result_suffix(), - )) - .with_structured(serde_json::json!({ - "path": if fg { "cgevent_fg" } else { "cgevent" }, "verified": false, "effect": "unverifiable" - })), + Ok(Ok(())) => { + let mut structured = serde_json::json!({ + "path": if fg { "cgevent_fg" } else { "cgevent" }, + "verified": false, + "effect": "unverifiable" + }); + changes.add_to_structured(&mut structured); + ToolResult::text(format!( + "✅ Posted drag{btn_suffix}{mod_suffix} to pid {pid} \ + from window-pixel ({}, {}) → ({}, {}), \ + screen ({}, {}) → ({}, {}) \ + in {duration_ms}ms / {steps} steps{mode_label} \ + (background CGEvent; not driver-verified — confirm via screenshot).{}", + from_x as i64, + from_y as i64, + to_x as i64, + to_y as i64, + from_sx as i64, + from_sy as i64, + to_sx as i64, + to_sy as i64, + changes.result_suffix(), + )) + .with_structured(structured) + } Ok(Err(e)) => ToolResult::error(format!("drag failed: {e}")), - Err(e) => ToolResult::error(format!("Task error: {e}")), + Err(e) => ToolResult::error(format!("Task error: {e}")), } } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs index 699a8e8f12..50299f1ba8 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs @@ -505,6 +505,7 @@ impl Tool for HotkeyTool { pixel-click to focus then type_text instead.)" }); } + changes.add_to_structured(&mut structured); ToolResult::text(format!( "Pressed {key_display} on pid {pid}{label}.{}", changes.result_suffix() diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs index 3ac5b1477c..01912eee4b 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs @@ -597,17 +597,23 @@ impl Tool for ScrollTool { "" }; return match result { - Ok(Ok(())) => ToolResult::text(format!( - "✅ Sent {direction} scroll by {by} × {amount} via pixel wheel at \ - ({screen_x:.0}, {screen_y:.0}){mode_label} (background CGEvent; not \ - driver-verified — confirm via screenshot).{}", - changes.result_suffix() - )) - .with_structured(serde_json::json!({ - "path": if fg { "cgevent_fg" } else { "cgevent" }, "verified": false, "effect": "unverifiable" - })), + Ok(Ok(())) => { + let mut structured = serde_json::json!({ + "path": if fg { "cgevent_fg" } else { "cgevent" }, + "verified": false, + "effect": "unverifiable" + }); + changes.add_to_structured(&mut structured); + ToolResult::text(format!( + "✅ Sent {direction} scroll by {by} × {amount} via pixel wheel at \ + ({screen_x:.0}, {screen_y:.0}){mode_label} (background CGEvent; not \ + driver-verified — confirm via screenshot).{}", + changes.result_suffix() + )) + .with_structured(structured) + } Ok(Err(e)) => ToolResult::error(format!("Wheel scroll failed: {e}")), - Err(e) => ToolResult::error(format!("Task error: {e}")), + Err(e) => ToolResult::error(format!("Task error: {e}")), }; } @@ -692,12 +698,16 @@ impl Tool for ScrollTool { let changes = super::finish_window_observation(snapshot, &args).await; match result { - Ok(Ok(())) => ToolResult::text(format!( - "✅ Sent {direction} scroll by {by} × {amount} via keystroke \ - (background; not driver-verified — confirm via screenshot).{}", - changes.result_suffix() - )) - .with_structured(serde_json::json!({ "path": "key_events", "verified": false })), + Ok(Ok(())) => { + let mut structured = serde_json::json!({ "path": "key_events", "verified": false }); + changes.add_to_structured(&mut structured); + ToolResult::text(format!( + "✅ Sent {direction} scroll by {by} × {amount} via keystroke \ + (background; not driver-verified — confirm via screenshot).{}", + changes.result_suffix() + )) + .with_structured(structured) + } Ok(Err(e)) => ToolResult::error(format!("Scroll failed: {e}")), Err(e) => ToolResult::error(format!("Task error: {e}")), } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs index 449f6306fe..51acf27c1d 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs @@ -246,6 +246,7 @@ impl Tool for SetValueTool { manipulate the control through its pixel action." }); } + changes.add_to_structured(&mut structured); ToolResult::text(msg).with_structured(structured) } Ok(Err(e)) => ToolResult::error(format!("set_value failed: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs index 49f8eca442..ae87899b2b 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs @@ -556,6 +556,7 @@ impl Tool for TypeTextTool { screenshot shows the text didn't land." }); } + changes.add_to_structured(&mut s); s }) } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs b/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs index 3286ac6230..d52fae39cb 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs @@ -144,6 +144,31 @@ impl Changes { "\n\n🔀 Action caused a different app to become frontmost.".to_string() } } + + /// Attach the platform-neutral post-action topology record consumed by + /// the shared `ActionResult` adapter. This does not activate or raise any + /// discovered window; it only publishes the identifiers already observed + /// by the read-only window-list diff. + pub fn add_to_structured(&self, structured: &mut serde_json::Value) { + if !self.needs_restore() { + return; + } + let Some(object) = structured.as_object_mut() else { + return; + }; + object.insert( + "window_change".to_owned(), + serde_json::json!({ + "new_windows": self.new_windows.iter().map(|window| serde_json::json!({ + "window_id": u64::from(window.window_id), + "pid": i64::from(window.pid), + "app_name": window.app_name, + "title": window.title, + })).collect::>(), + "foreground_changed": self.foreground_changed, + }), + ); + } } /// Default poll deadline — new windows triggered by a click typically @@ -511,6 +536,37 @@ mod tests { ); } + #[test] + fn changes_attach_machine_readable_rebind_metadata_without_mutating_no_change() { + let changes = Changes { + new_windows: vec![WindowEvent { + window_id: 41, + pid: 123, + app_name: "TextEdit".to_owned(), + title: "Open".to_owned(), + }], + foreground_changed: false, + }; + let mut structured = serde_json::json!({"path": "ax"}); + changes.add_to_structured(&mut structured); + assert_eq!( + structured["window_change"], + serde_json::json!({ + "new_windows": [{ + "window_id": 41, + "pid": 123, + "app_name": "TextEdit", + "title": "Open", + }], + "foreground_changed": false, + }) + ); + + let mut unchanged = serde_json::json!({"path": "ax"}); + Changes::no_change().add_to_structured(&mut unchanged); + assert!(unchanged.get("window_change").is_none()); + } + /// Regression: `snapshot(prior_front)` must store the caller's /// captured front pid verbatim (rather than re-reading it inside /// the function and racing with concurrent activations). From 97e12f9b02d4307515933129277c140deddb5816 Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 14:03:47 +0800 Subject: [PATCH 02/16] docs(cua-driver): define target-scoped rediscovery architecture --- .../cua-driver/docs/action-result-contract.md | 4 +- .../docs/post-action-surface-rediscovery.md | 176 ++++++++++++++++++ 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 libs/cua-driver/docs/post-action-surface-rediscovery.md diff --git a/libs/cua-driver/docs/action-result-contract.md b/libs/cua-driver/docs/action-result-contract.md index 2ce75ba53d..58976850f8 100644 --- a/libs/cua-driver/docs/action-result-contract.md +++ b/libs/cua-driver/docs/action-result-contract.md @@ -8,7 +8,9 @@ Cua Driver 0.15 separates two facts that earlier releases mixed together: `satisfied`, `unsatisfied`, or `unknown`. The driver reports these facts. The agent harness owns task meaning, visual -reading, stop/retry decisions, and movement through the action ladder. +reading, stop/retry decisions, and movement through the action ladder. The +post-action observer and rebind boundary are specified in +[`post-action-surface-rediscovery.md`](post-action-surface-rediscovery.md). ## MCP action result diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md new file mode 100644 index 0000000000..fdd4d27a84 --- /dev/null +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -0,0 +1,176 @@ +# Post-action surface rediscovery + +This document records the implementation boundary for issue #2238. The draft +keeps Cua Driver's typed action-result contract while replacing its global +window-count heuristic with target-scoped root observation and shared candidate +validation. + +## Existing Cua Driver flow + +Cua Driver already protects foreground focus and detects visible native windows, +but the topology survives only in diagnostic text. + +```mermaid +flowchart LR + action["macOS action tool"] --> before["snapshot global layer-0 window ids"] + before --> actuator["run native actuator under focus guard"] + actuator --> after["poll global layer-0 window ids"] + after --> prose["append diagnostic prose"] + prose --> adapter["project action result without topology"] + adapter --> stale["caller remains bound to the prior window"] +``` + +## What pull request #2746 contributes + +Pull request #2746 adds a closed `window_change` record and explicit `rebind` +escalation. Those are useful public semantics and remain part of this draft. +Its detector, however, still treats every new desktop window as action-related, +selects an exact target from list length alone, and promotes any topology change +to confirmed action effect. + +```mermaid +flowchart LR + action["macOS action tool"] --> global["diff all visible desktop windows"] + global --> json["write legacy JSON"] + json --> parse["parse into shared action record"] + parse --> confirm["promote effect to confirmed"] + confirm --> count{"new window count"} + count -->|"one"| exact["emit exact rebind"] + count -->|"many"| ambiguous["emit ambiguous rebind"] +``` + +This draft preserves from #2746: + +- the portable `window_change` record; +- explicit, non-activating `rebind` advice; +- exact target metadata when one replacement surface is validated; +- generated Rust, Python, TypeScript, and manifest parity; and +- rebind-before-pixel, page, foreground, or desktop recovery guidance. + +It does not preserve the global-window causality inference, effect promotion, or +branch-local JSON mutation. + +## What is adapted from pi-computer-use + +The target-root observer follows the architecture proven in +`injaneity/pi-computer-use` commit +`022a280a377065c95736cc15f684bf1fad46479e`: + +- capture accessibility roots for the target process before and after an action; +- treat the accessibility snapshot diff as authoritative; +- use accessibility notifications, focus state, and cheap native-window polling + only to end the bounded wait early; +- report appeared, closed, and focused roots with modality facts; and +- refresh target resolution before selecting a visible modal surface. + +The design is adapted rather than copied verbatim. Cua Driver keeps its Rust +platform boundary, focus-suppression leases, action execution record, closed +contract vocabulary, and explicit harness-owned escalation policy. + +```mermaid +flowchart LR + before["snapshot target accessibility roots"] --> action["perform action"] + action --> signal["event or native-window signal wakes observer"] + signal --> after["snapshot target accessibility roots again"] + after --> delta["derive appeared closed and focused roots"] + delta --> resolver["refresh and validate a visible modal root"] +``` + +## Combined architecture + +Detection answers what changed. Resolution answers which surface is safe to +bind. Action accounting answers what the actuator proved. None substitutes for +another. + +```mermaid +flowchart LR + coordinator["shared action coordinator"] --> observer["platform root observer begins"] + observer --> actuator["native actuator runs"] + actuator --> outcome["typed actuator outcome"] + actuator --> finish["platform root observer finishes"] + finish --> delta["typed target-root delta"] + outcome --> record["action execution record"] + delta --> record + delta --> resolver["shared candidate resolver"] + resolver --> choice{"one validated blocking root"} + choice -->|"yes"| rebind["emit exact logical rebind"] + choice -->|"no"| correlate["emit candidates for list_windows correlation"] + record --> public["closed action result preserves original effect"] + rebind --> public + correlate --> public +``` + +### Platform observer + +Each platform adapter owns native root discovery. It returns typed facts and +never chooses policy. macOS uses target-process accessibility roots as the final +source of truth, with CGWindow and AX notifications as bounded early-wake +signals. Windows uses the equivalent UIA and HWND ownership facts. Linux must +return the same semantics where AT-SPI and the compositor expose them, or an +explicit unsupported limitation. + +Cross-application handoffs are reported separately from target-owned roots. +They never become exact targets from temporal proximity alone. + +### Shared action coordinator + +The coordinator starts observation immediately before dispatch and finishes it +immediately after dispatch. It attaches the typed delta directly to the action +execution record, including partial and failed delivery. Platform tools must not +serialize private topology to JSON and ask the legacy adapter to parse it back. + +### Shared resolver + +An exact rebind requires exactly one eligible candidate after validation. The +candidate must be a target-owned blocking modal, a newly focused target-owned +root, or a separately verified cross-application handoff. Otherwise the result +retains candidates and asks the harness to correlate them with `list_windows`. + +Logical rebinding never activates, raises, captures, or sends input to the new +surface. The caller refreshes window-scoped state and restarts with background +delivery. + +## Contract invariants + +- `window_change` is independent from `effect`; topology cannot promote an + unverifiable, partial, suspected-noop, or refused action to confirmed. +- `window_change` evidence means only that topology was observed. +- an exact escalation target must also appear in the accompanying topology. +- unrelated desktop windows cannot become target-owned candidates. +- an empty or unchanged delta cannot produce rebind advice. +- partial and failed delivery retain observed topology internally; successful + public action results expose it through the closed contract. +- platform adapters report limitations explicitly instead of substituting a + global desktop heuristic. + +## Recovery ladder + +```mermaid +flowchart LR + result["action result contains window change"] --> exact{"exact validated target"} + exact -->|"yes"| refresh["refresh target window without activation"] + exact -->|"no"| list["correlate candidates with list_windows"] + list --> found{"blocking surface identified"} + found -->|"yes"| refresh + found -->|"no"| desktop["request one privacy-sensitive desktop frame"] + desktop --> correlate["correlate and return to window scope"] + correlate --> refresh + refresh --> background["resume background semantic actions"] +``` + +The request-scoped desktop frame remains a separate final perception rung. It +must not silently widen persistent capture scope. + +## Validation plan + +Before the pull request is made ready: + +1. contract tests must reject inconsistent rebind targets and effect promotion; +2. shared resolver tests must cover one modal, several candidates, unrelated + windows, cross-app handoff, partial delivery, and malformed producer data; +3. macOS observer tests must cover AX-only sheets and delayed root appearance; +4. the TextEdit Open-panel harness case must consume structured topology, + rebind without activation, and prove the user's foreground app is unchanged; +5. Windows and Linux receive focused contract coverage plus either native + behavior evidence or an explicit limitation; and +6. the complete canonical macOS harness runs once on the stable candidate SHA. From d11dd6c36237b272441465fea1e162438a1d02b2 Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 14:30:04 +0800 Subject: [PATCH 03/16] docs(cua-driver): stack rediscovery diagrams for github --- .../cua-driver/docs/post-action-surface-rediscovery.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index fdd4d27a84..aa8379ba91 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -11,7 +11,7 @@ Cua Driver already protects foreground focus and detects visible native windows, but the topology survives only in diagnostic text. ```mermaid -flowchart LR +flowchart TB action["macOS action tool"] --> before["snapshot global layer-0 window ids"] before --> actuator["run native actuator under focus guard"] actuator --> after["poll global layer-0 window ids"] @@ -29,7 +29,7 @@ selects an exact target from list length alone, and promotes any topology change to confirmed action effect. ```mermaid -flowchart LR +flowchart TB action["macOS action tool"] --> global["diff all visible desktop windows"] global --> json["write legacy JSON"] json --> parse["parse into shared action record"] @@ -68,7 +68,7 @@ platform boundary, focus-suppression leases, action execution record, closed contract vocabulary, and explicit harness-owned escalation policy. ```mermaid -flowchart LR +flowchart TB before["snapshot target accessibility roots"] --> action["perform action"] action --> signal["event or native-window signal wakes observer"] signal --> after["snapshot target accessibility roots again"] @@ -83,7 +83,7 @@ bind. Action accounting answers what the actuator proved. None substitutes for another. ```mermaid -flowchart LR +flowchart TB coordinator["shared action coordinator"] --> observer["platform root observer begins"] observer --> actuator["native actuator runs"] actuator --> outcome["typed actuator outcome"] @@ -146,7 +146,7 @@ delivery. ## Recovery ladder ```mermaid -flowchart LR +flowchart TB result["action result contains window change"] --> exact{"exact validated target"} exact -->|"yes"| refresh["refresh target window without activation"] exact -->|"no"| list["correlate candidates with list_windows"] From c6721351a531de75246172a1d4c12365b2cdc4eb Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 14:35:52 +0800 Subject: [PATCH 04/16] docs(cua-driver): use static rediscovery diagrams --- .../combined.dot | 27 +++ .../combined.svg | 170 ++++++++++++++++++ .../current-cua.dot | 13 ++ .../current-cua.svg | 90 ++++++++++ .../pi-computer-use.dot | 12 ++ .../pi-computer-use.svg | 78 ++++++++ .../pr-2746.dot | 16 ++ .../pr-2746.svg | 104 +++++++++++ .../recovery-ladder.dot | 20 +++ .../recovery-ladder.svg | 118 ++++++++++++ .../docs/post-action-surface-rediscovery.md | 61 +------ 11 files changed, 653 insertions(+), 56 deletions(-) create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot new file mode 100644 index 0000000000..84633b242b --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot @@ -0,0 +1,27 @@ +digraph g { + graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; + node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; + edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; + coordinator [label="shared action coordinator"]; + observer [label="platform target-root observer begins"]; + actuator [label="native actuator runs"]; + outcome [label="typed actuator outcome"]; + finish [label="platform observer finishes"]; + delta [label="typed target-root delta"]; + record [label="action execution record"]; + resolver [label="shared candidate resolver"]; + choice [shape=diamond, label="one validated blocking root"]; + rebind [label="emit exact logical rebind"]; + correlate [label="emit candidates for correlation"]; + public [label="closed result preserves original effect"]; + coordinator -> observer -> actuator; + actuator -> outcome -> record; + actuator -> finish -> delta; + delta -> record; + delta -> resolver -> choice; + choice -> rebind [label="yes"]; + choice -> correlate [label="no"]; + record -> public; + rebind -> public; + correlate -> public; +} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg new file mode 100644 index 0000000000..fe125bfc31 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg @@ -0,0 +1,170 @@ + + + + + + +g + + +coordinator + +shared action coordinator + + + +observer + +platform target-root observer begins + + + +coordinator->observer + + + + + +actuator + +native actuator runs + + + +observer->actuator + + + + + +outcome + +typed actuator outcome + + + +actuator->outcome + + + + + +finish + +platform observer finishes + + + +actuator->finish + + + + + +record + +action execution record + + + +outcome->record + + + + + +delta + +typed target-root delta + + + +finish->delta + + + + + +delta->record + + + + + +resolver + +shared candidate resolver + + + +delta->resolver + + + + + +public + +closed result preserves original effect + + + +record->public + + + + + +choice + +one validated blocking root + + + +resolver->choice + + + + + +rebind + +emit exact logical rebind + + + +choice->rebind + + +yes + + + +correlate + +emit candidates for correlation + + + +choice->correlate + + +no + + + +rebind->public + + + + + +correlate->public + + + + + diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot new file mode 100644 index 0000000000..8d91587687 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot @@ -0,0 +1,13 @@ +digraph g { + graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; + node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; + edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; + action [label="macOS action tool"]; + before [label="snapshot global layer-0 window ids"]; + actuator [label="run native actuator under focus guard"]; + after [label="poll global layer-0 window ids"]; + prose [label="append diagnostic prose"]; + adapter [label="project action result without topology"]; + stale [label="caller remains bound to prior window"]; + action -> before -> actuator -> after -> prose -> adapter -> stale; +} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg new file mode 100644 index 0000000000..a649e2187b --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg @@ -0,0 +1,90 @@ + + + + + + +g + + +action + +macOS action tool + + + +before + +snapshot global layer-0 window ids + + + +action->before + + + + + +actuator + +run native actuator under focus guard + + + +before->actuator + + + + + +after + +poll global layer-0 window ids + + + +actuator->after + + + + + +prose + +append diagnostic prose + + + +after->prose + + + + + +adapter + +project action result without topology + + + +prose->adapter + + + + + +stale + +caller remains bound to prior window + + + +adapter->stale + + + + + diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot new file mode 100644 index 0000000000..e4990f7941 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot @@ -0,0 +1,12 @@ +digraph g { + graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; + node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; + edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; + before [label="snapshot target accessibility roots"]; + action [label="perform action"]; + signal [label="event or native-window signal wakes observer"]; + after [label="snapshot target accessibility roots again"]; + delta [label="derive appeared, closed, and focused roots"]; + resolver [label="refresh and validate visible modal root"]; + before -> action -> signal -> after -> delta -> resolver; +} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg new file mode 100644 index 0000000000..bb0aa52260 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg @@ -0,0 +1,78 @@ + + + + + + +g + + +before + +snapshot target accessibility roots + + + +action + +perform action + + + +before->action + + + + + +signal + +event or native-window signal wakes observer + + + +action->signal + + + + + +after + +snapshot target accessibility roots again + + + +signal->after + + + + + +delta + +derive appeared, closed, and focused roots + + + +after->delta + + + + + +resolver + +refresh and validate visible modal root + + + +delta->resolver + + + + + diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot new file mode 100644 index 0000000000..073d123ea7 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot @@ -0,0 +1,16 @@ +digraph g { + graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; + node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; + edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; + action [label="macOS action tool"]; + global [label="diff all visible desktop windows"]; + json [label="write legacy JSON"]; + parse [label="parse shared action record"]; + confirm [label="promote effect to confirmed"]; + count [shape=diamond, label="new window count"]; + exact [label="emit exact rebind"]; + ambiguous [label="emit ambiguous rebind"]; + action -> global -> json -> parse -> confirm -> count; + count -> exact [label="one"]; + count -> ambiguous [label="many"]; +} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg new file mode 100644 index 0000000000..849434b38c --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg @@ -0,0 +1,104 @@ + + + + + + +g + + +action + +macOS action tool + + + +global + +diff all visible desktop windows + + + +action->global + + + + + +json + +write legacy JSON + + + +global->json + + + + + +parse + +parse shared action record + + + +json->parse + + + + + +confirm + +promote effect to confirmed + + + +parse->confirm + + + + + +count + +new window count + + + +confirm->count + + + + + +exact + +emit exact rebind + + + +count->exact + + +one + + + +ambiguous + +emit ambiguous rebind + + + +count->ambiguous + + +many + + + diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot new file mode 100644 index 0000000000..8e9560a531 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot @@ -0,0 +1,20 @@ +digraph g { + graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; + node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; + edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; + result [label="action result contains window change"]; + exact [shape=diamond, label="exact validated target"]; + refresh [label="refresh target window without activation"]; + list [label="correlate candidates with list_windows"]; + found [shape=diamond, label="blocking surface identified"]; + desktop [label="request one privacy-sensitive desktop frame"]; + correlate [label="correlate and return to window scope"]; + background [label="resume background semantic actions"]; + result -> exact; + exact -> refresh [label="yes"]; + exact -> list [label="no"]; + list -> found; + found -> refresh [label="yes"]; + found -> desktop [label="no"]; + desktop -> correlate -> refresh -> background; +} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg new file mode 100644 index 0000000000..0459fdb2a4 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg @@ -0,0 +1,118 @@ + + + + + + +g + + +result + +action result contains window change + + + +exact + +exact validated target + + + +result->exact + + + + + +refresh + +refresh target window without activation + + + +exact->refresh + + +yes + + + +list + +correlate candidates with list_windows + + + +exact->list + + +no + + + +background + +resume background semantic actions + + + +refresh->background + + + + + +found + +blocking surface identified + + + +list->found + + + + + +found->refresh + + +yes + + + +desktop + +request one privacy-sensitive desktop frame + + + +found->desktop + + +no + + + +correlate + +correlate and return to window scope + + + +desktop->correlate + + + + + +correlate->refresh + + + + + diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index aa8379ba91..182ec112b2 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -10,15 +10,7 @@ validation. Cua Driver already protects foreground focus and detects visible native windows, but the topology survives only in diagnostic text. -```mermaid -flowchart TB - action["macOS action tool"] --> before["snapshot global layer-0 window ids"] - before --> actuator["run native actuator under focus guard"] - actuator --> after["poll global layer-0 window ids"] - after --> prose["append diagnostic prose"] - prose --> adapter["project action result without topology"] - adapter --> stale["caller remains bound to the prior window"] -``` +![current cua architecture](diagrams/post-action-surface-rediscovery/current-cua.svg) ## What pull request #2746 contributes @@ -28,16 +20,7 @@ Its detector, however, still treats every new desktop window as action-related, selects an exact target from list length alone, and promotes any topology change to confirmed action effect. -```mermaid -flowchart TB - action["macOS action tool"] --> global["diff all visible desktop windows"] - global --> json["write legacy JSON"] - json --> parse["parse into shared action record"] - parse --> confirm["promote effect to confirmed"] - confirm --> count{"new window count"} - count -->|"one"| exact["emit exact rebind"] - count -->|"many"| ambiguous["emit ambiguous rebind"] -``` +![pr 2746 architecture](diagrams/post-action-surface-rediscovery/pr-2746.svg) This draft preserves from #2746: @@ -67,14 +50,7 @@ The design is adapted rather than copied verbatim. Cua Driver keeps its Rust platform boundary, focus-suppression leases, action execution record, closed contract vocabulary, and explicit harness-owned escalation policy. -```mermaid -flowchart TB - before["snapshot target accessibility roots"] --> action["perform action"] - action --> signal["event or native-window signal wakes observer"] - signal --> after["snapshot target accessibility roots again"] - after --> delta["derive appeared closed and focused roots"] - delta --> resolver["refresh and validate a visible modal root"] -``` +![pi computer use architecture](diagrams/post-action-surface-rediscovery/pi-computer-use.svg) ## Combined architecture @@ -82,23 +58,7 @@ Detection answers what changed. Resolution answers which surface is safe to bind. Action accounting answers what the actuator proved. None substitutes for another. -```mermaid -flowchart TB - coordinator["shared action coordinator"] --> observer["platform root observer begins"] - observer --> actuator["native actuator runs"] - actuator --> outcome["typed actuator outcome"] - actuator --> finish["platform root observer finishes"] - finish --> delta["typed target-root delta"] - outcome --> record["action execution record"] - delta --> record - delta --> resolver["shared candidate resolver"] - resolver --> choice{"one validated blocking root"} - choice -->|"yes"| rebind["emit exact logical rebind"] - choice -->|"no"| correlate["emit candidates for list_windows correlation"] - record --> public["closed action result preserves original effect"] - rebind --> public - correlate --> public -``` +![combined architecture](diagrams/post-action-surface-rediscovery/combined.svg) ### Platform observer @@ -145,18 +105,7 @@ delivery. ## Recovery ladder -```mermaid -flowchart TB - result["action result contains window change"] --> exact{"exact validated target"} - exact -->|"yes"| refresh["refresh target window without activation"] - exact -->|"no"| list["correlate candidates with list_windows"] - list --> found{"blocking surface identified"} - found -->|"yes"| refresh - found -->|"no"| desktop["request one privacy-sensitive desktop frame"] - desktop --> correlate["correlate and return to window scope"] - correlate --> refresh - refresh --> background["resume background semantic actions"] -``` +![recovery ladder architecture](diagrams/post-action-surface-rediscovery/recovery-ladder.svg) The request-scoped desktop frame remains a separate final perception rung. It must not silently widen persistent capture scope. From fb2398ba3569b1f80a515b0299b2e03d26f567bd Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 14:37:37 +0800 Subject: [PATCH 05/16] refactor(cua-driver): remove legacy topology patching --- libs/cua-driver/contract/manifest.json | 532 +----------------- .../cua-driver/docs/action-result-contract.md | 20 +- .../python/src/cua_driver/__init__.py | 4 - .../crates/cua-driver-contract/src/lib.rs | 12 +- .../crates/cua-driver-contract/src/outputs.rs | 53 +- .../crates/platform-macos/src/tools/click.rs | 15 +- .../crates/platform-macos/src/tools/drag.rs | 42 +- .../crates/platform-macos/src/tools/hotkey.rs | 1 - .../crates/platform-macos/src/tools/scroll.rs | 42 +- .../platform-macos/src/tools/set_value.rs | 1 - .../platform-macos/src/tools/type_text.rs | 1 - .../src/window_change_detector.rs | 56 -- 12 files changed, 66 insertions(+), 713 deletions(-) diff --git a/libs/cua-driver/contract/manifest.json b/libs/cua-driver/contract/manifest.json index 973351f38d..9965f5f455 100644 --- a/libs/cua-driver/contract/manifest.json +++ b/libs/cua-driver/contract/manifest.json @@ -172,8 +172,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -182,37 +181,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -256,49 +227,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -647,8 +575,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -657,37 +584,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -731,49 +630,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -1709,8 +1565,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -1719,37 +1574,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -1793,49 +1620,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -2304,8 +2088,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -2314,37 +2097,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -2388,49 +2143,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -2593,8 +2305,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -2603,37 +2314,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -2677,49 +2360,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -2902,8 +2542,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -2912,37 +2551,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -2986,49 +2597,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -3458,8 +3026,7 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ], "type": "string" }, @@ -3468,37 +3035,9 @@ "pixel", "foreground", "page", - "session", - "rebind" + "session" ], "type": "string" - }, - "window": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": [ - "object", - "null" - ] } }, "required": [ @@ -3891,49 +3430,6 @@ "trusted_input" ], "type": "string" - }, - "window_change": { - "additionalProperties": false, - "properties": { - "foreground_changed": { - "type": "boolean" - }, - "new_windows": { - "items": { - "additionalProperties": false, - "properties": { - "app_name": { - "type": "string" - }, - "pid": { - "format": "int64", - "type": "integer" - }, - "window_id": { - "format": "uint64", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "pid", - "window_id", - "app_name", - "title" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "new_windows", - "foreground_changed" - ], - "type": [ - "object", - "null" - ] } }, "required": [ diff --git a/libs/cua-driver/docs/action-result-contract.md b/libs/cua-driver/docs/action-result-contract.md index 58976850f8..bb61a40aab 100644 --- a/libs/cua-driver/docs/action-result-contract.md +++ b/libs/cua-driver/docs/action-result-contract.md @@ -33,9 +33,8 @@ Every successful action returns a closed `structuredContent` object: | `route` | `accessibility`, `synthetic_events`, `global_input`, `dom`, `trusted_input` | | `delivery.mode` | `background`, `foreground`, `not_applicable`, `unknown` | | `evidence[].kind` | `value_readback`, `window_change` | -| `window_change` | read-only `new_windows` plus `foreground_changed` topology facts | -| `escalation.target` | `pixel`, `foreground`, `page`, `session`, `rebind` | -| `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required`, `surface_changed` | +| `escalation.target` | `pixel`, `foreground`, `page`, `session` | +| `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required` | The action-result tools are: @@ -48,11 +47,9 @@ The action-result tools are: Other mutating tools such as application launch, window activation, browser navigation, dialogs, uploads, and downloads retain their own typed results. -The contract is deliberately closed. It does not echo request selectors, -coordinates, scope, platform transport names, diagnostic pointers, or the old -`verified` boolean. A post-action `window_change` is the one target-bearing -exception: it reports newly observed native windows so a caller can leave a -stale or blocked anchor without activating the replacement surface. +The contract is deliberately closed. It does not echo selectors, coordinates, +scope, targets, platform transport names, diagnostic pointers, or the old +`verified` boolean. The invariants are: @@ -128,13 +125,6 @@ An optional escalation is advice, not an automatic retry: | `foreground` | explicitly select foreground delivery when session policy permits | | `page` | bind the native window to a supported browser page route | | `session` | prepare or explicitly widen the session only when policy permits | -| `rebind` | bind to `escalation.window` when present, otherwise correlate `window_change.new_windows` with `list_windows`; refresh window state before acting | - -Handle `rebind` before pixel, page, or foreground retries. The driver discovers -the new surface through a read-only post-action window diff and preserves the -existing focus guard; rebinding is logical and must not raise the window. When -multiple candidates are reported, refresh `list_windows`, select the blocking -surface, and resume background window-scoped interaction. SDK integrators, OpenClaw, Hermes, and other agent hosts can implement different policies above this same narrow fact contract without duplicating platform diff --git a/libs/cua-driver/python/src/cua_driver/__init__.py b/libs/cua-driver/python/src/cua_driver/__init__.py index 65477f52e6..5a83149c70 100644 --- a/libs/cua-driver/python/src/cua_driver/__init__.py +++ b/libs/cua-driver/python/src/cua_driver/__init__.py @@ -58,8 +58,6 @@ ActionEvidenceKind, ActionResult, ActionRoute, - ActionWindowChange, - ActionWindowTarget, BoundsExpectation, CaptureScope, ClickButton, @@ -182,8 +180,6 @@ def _create_private_worker_python_sdk(cls, options): "ActionEvidenceKind", "ActionResult", "ActionRoute", - "ActionWindowChange", - "ActionWindowTarget", "BoundsExpectation", "CaptureScope", "ClickButton", diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs index 506bd1b6db..a5a72e84dc 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs @@ -39,12 +39,12 @@ pub use inputs::{ pub use outputs::{ advertised_output_schema, refusal_envelope_schema, ActionDelivery, ActionDeliveryMode, ActionEffect, ActionEscalation, ActionEscalationReason, ActionEscalationTarget, ActionEvidence, - ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, ActionWindowChange, - ActionWindowTarget, ClipboardReadOutput, ClipboardWriteOutput, CursorMotionOutput, - CursorPointOutput, CursorPositionOutput, CursorThemeOutput, CursorVisualOutput, - DesktopStateOutput, EffectiveScope, EndSessionOutput, GetAgentCursorStateOutput, - ListSessionsOutput, ScreenSizeOutput, SessionClientKindOutput, SessionLifecycleState, - SessionOutput, SessionStateOutput, SessionTransportOutput, SetAgentCursorEnabledOutput, + ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, + ClipboardReadOutput, ClipboardWriteOutput, CursorMotionOutput, CursorPointOutput, + CursorPositionOutput, CursorThemeOutput, CursorVisualOutput, DesktopStateOutput, + EffectiveScope, EndSessionOutput, GetAgentCursorStateOutput, ListSessionsOutput, + ScreenSizeOutput, SessionClientKindOutput, SessionLifecycleState, SessionOutput, + SessionStateOutput, SessionTransportOutput, SetAgentCursorEnabledOutput, SetAgentCursorMotionOutput, SetAgentCursorThemeOutput, StartSessionOutput, ToolOutput, }; pub use verification::{ diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs index 26884909e2..849ed0b0b6 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs @@ -450,25 +450,6 @@ pub struct ActionEvidence { pub kind: ActionEvidenceKind, } -/// A newly discovered native interaction surface after an action. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] -#[serde(deny_unknown_fields)] -pub struct ActionWindowTarget { - pub pid: i64, - pub window_id: u64, - pub app_name: String, - pub title: String, -} - -/// Read-only post-action topology change used to rediscover a blocking modal, -/// popup, or new top-level window without activating it. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] -#[serde(deny_unknown_fields)] -pub struct ActionWindowChange { - pub new_windows: Vec, - pub foreground_changed: bool, -} - #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] #[serde(rename_all = "snake_case")] pub enum ActionEscalationTarget { @@ -476,7 +457,6 @@ pub enum ActionEscalationTarget { Foreground, Page, Session, - Rebind, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] @@ -487,7 +467,6 @@ pub enum ActionEscalationReason { EffectUnconfirmed, SuspectedNoop, PermissionRequired, - SurfaceChanged, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -495,8 +474,6 @@ pub enum ActionEscalationReason { pub struct ActionEscalation { pub target: ActionEscalationTarget, pub reason: ActionEscalationReason, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub window: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -509,8 +486,6 @@ pub struct ActionResult { #[serde(default, skip_serializing_if = "Option::is_none")] pub evidence: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub window_change: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] pub escalation: Option, } @@ -650,7 +625,6 @@ mod tests { evidence: Some(vec![ActionEvidence { kind: ActionEvidenceKind::ValueReadback, }]), - window_change: None, escalation: None, } } @@ -664,14 +638,7 @@ mod tests { let properties = schema["properties"].as_object().expect("properties"); assert_eq!( properties.keys().map(String::as_str).collect::>(), - [ - "delivery", - "effect", - "escalation", - "evidence", - "route", - "window_change", - ] + ["delivery", "effect", "escalation", "evidence", "route"] ); assert_eq!( properties["effect"]["enum"], @@ -727,7 +694,7 @@ mod tests { assert_eq!(escalation["required"], json!(["target", "reason"])); assert_eq!( escalation["properties"]["target"]["enum"], - json!(["pixel", "foreground", "page", "session", "rebind"]) + json!(["pixel", "foreground", "page", "session"]) ); assert_eq!( escalation["properties"]["reason"]["enum"], @@ -736,23 +703,9 @@ mod tests { "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required", - "surface_changed" + "permission_required" ]) ); - - let window_change = object_variant(&properties["window_change"]); - assert_eq!(window_change["additionalProperties"], false); - assert_eq!( - window_change["required"], - json!(["new_windows", "foreground_changed"]) - ); - let window = &window_change["properties"]["new_windows"]["items"]; - assert_eq!(window["additionalProperties"], false); - assert_eq!( - window["required"], - json!(["pid", "window_id", "app_name", "title"]) - ); } #[test] diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs index a0ce7981db..26b45ef591 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs @@ -746,7 +746,6 @@ impl Tool for ClickTool { screenshot from get_window_state." }); } - changes.add_to_structured(&mut structured); ToolResult::text(msg).with_structured(structured) } Ok(Err(e)) => ToolResult::error(format!("AX action failed: {e}")), @@ -1169,19 +1168,17 @@ impl Tool for ClickTool { } else { ("cgevent", "background CGEvent") }; - let mut structured = serde_json::json!({ - "path": path, - "verified": false, - "effect": "unverifiable", - "focus_without_raise": focus_without_raise - }); - changes.add_to_structured(&mut structured); ToolResult::text(format!( "✅ Posted {button_label} to pid {pid} ({mode_label}; \ not driver-verified — confirm via screenshot).{}", changes.result_suffix() )) - .with_structured(structured) + .with_structured(serde_json::json!({ + "path": path, + "verified": false, + "effect": "unverifiable", + "focus_without_raise": focus_without_raise + })) } Ok(Err(e)) => ToolResult::error(format!("{button_label} failed: {e}")), Err(e) => ToolResult::error(format!("Task error: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs index c9fa554189..f5eb9d0395 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs @@ -421,33 +421,23 @@ impl Tool for DragTool { "" }; match result { - Ok(Ok(())) => { - let mut structured = serde_json::json!({ - "path": if fg { "cgevent_fg" } else { "cgevent" }, - "verified": false, - "effect": "unverifiable" - }); - changes.add_to_structured(&mut structured); - ToolResult::text(format!( - "✅ Posted drag{btn_suffix}{mod_suffix} to pid {pid} \ - from window-pixel ({}, {}) → ({}, {}), \ - screen ({}, {}) → ({}, {}) \ - in {duration_ms}ms / {steps} steps{mode_label} \ - (background CGEvent; not driver-verified — confirm via screenshot).{}", - from_x as i64, - from_y as i64, - to_x as i64, - to_y as i64, - from_sx as i64, - from_sy as i64, - to_sx as i64, - to_sy as i64, - changes.result_suffix(), - )) - .with_structured(structured) - } + Ok(Ok(())) => ToolResult::text(format!( + "✅ Posted drag{btn_suffix}{mod_suffix} to pid {pid} \ + from window-pixel ({}, {}) → ({}, {}), \ + screen ({}, {}) → ({}, {}) \ + in {duration_ms}ms / {steps} steps{mode_label} \ + (background CGEvent; not driver-verified — confirm via screenshot).{}", + from_x as i64, from_y as i64, + to_x as i64, to_y as i64, + from_sx as i64, from_sy as i64, + to_sx as i64, to_sy as i64, + changes.result_suffix(), + )) + .with_structured(serde_json::json!({ + "path": if fg { "cgevent_fg" } else { "cgevent" }, "verified": false, "effect": "unverifiable" + })), Ok(Err(e)) => ToolResult::error(format!("drag failed: {e}")), - Err(e) => ToolResult::error(format!("Task error: {e}")), + Err(e) => ToolResult::error(format!("Task error: {e}")), } } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs index 50299f1ba8..699a8e8f12 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs @@ -505,7 +505,6 @@ impl Tool for HotkeyTool { pixel-click to focus then type_text instead.)" }); } - changes.add_to_structured(&mut structured); ToolResult::text(format!( "Pressed {key_display} on pid {pid}{label}.{}", changes.result_suffix() diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs index 01912eee4b..3ac5b1477c 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs @@ -597,23 +597,17 @@ impl Tool for ScrollTool { "" }; return match result { - Ok(Ok(())) => { - let mut structured = serde_json::json!({ - "path": if fg { "cgevent_fg" } else { "cgevent" }, - "verified": false, - "effect": "unverifiable" - }); - changes.add_to_structured(&mut structured); - ToolResult::text(format!( - "✅ Sent {direction} scroll by {by} × {amount} via pixel wheel at \ - ({screen_x:.0}, {screen_y:.0}){mode_label} (background CGEvent; not \ - driver-verified — confirm via screenshot).{}", - changes.result_suffix() - )) - .with_structured(structured) - } + Ok(Ok(())) => ToolResult::text(format!( + "✅ Sent {direction} scroll by {by} × {amount} via pixel wheel at \ + ({screen_x:.0}, {screen_y:.0}){mode_label} (background CGEvent; not \ + driver-verified — confirm via screenshot).{}", + changes.result_suffix() + )) + .with_structured(serde_json::json!({ + "path": if fg { "cgevent_fg" } else { "cgevent" }, "verified": false, "effect": "unverifiable" + })), Ok(Err(e)) => ToolResult::error(format!("Wheel scroll failed: {e}")), - Err(e) => ToolResult::error(format!("Task error: {e}")), + Err(e) => ToolResult::error(format!("Task error: {e}")), }; } @@ -698,16 +692,12 @@ impl Tool for ScrollTool { let changes = super::finish_window_observation(snapshot, &args).await; match result { - Ok(Ok(())) => { - let mut structured = serde_json::json!({ "path": "key_events", "verified": false }); - changes.add_to_structured(&mut structured); - ToolResult::text(format!( - "✅ Sent {direction} scroll by {by} × {amount} via keystroke \ - (background; not driver-verified — confirm via screenshot).{}", - changes.result_suffix() - )) - .with_structured(structured) - } + Ok(Ok(())) => ToolResult::text(format!( + "✅ Sent {direction} scroll by {by} × {amount} via keystroke \ + (background; not driver-verified — confirm via screenshot).{}", + changes.result_suffix() + )) + .with_structured(serde_json::json!({ "path": "key_events", "verified": false })), Ok(Err(e)) => ToolResult::error(format!("Scroll failed: {e}")), Err(e) => ToolResult::error(format!("Task error: {e}")), } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs index 51acf27c1d..449f6306fe 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs @@ -246,7 +246,6 @@ impl Tool for SetValueTool { manipulate the control through its pixel action." }); } - changes.add_to_structured(&mut structured); ToolResult::text(msg).with_structured(structured) } Ok(Err(e)) => ToolResult::error(format!("set_value failed: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs index ae87899b2b..49f8eca442 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs @@ -556,7 +556,6 @@ impl Tool for TypeTextTool { screenshot shows the text didn't land." }); } - changes.add_to_structured(&mut s); s }) } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs b/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs index d52fae39cb..3286ac6230 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs @@ -144,31 +144,6 @@ impl Changes { "\n\n🔀 Action caused a different app to become frontmost.".to_string() } } - - /// Attach the platform-neutral post-action topology record consumed by - /// the shared `ActionResult` adapter. This does not activate or raise any - /// discovered window; it only publishes the identifiers already observed - /// by the read-only window-list diff. - pub fn add_to_structured(&self, structured: &mut serde_json::Value) { - if !self.needs_restore() { - return; - } - let Some(object) = structured.as_object_mut() else { - return; - }; - object.insert( - "window_change".to_owned(), - serde_json::json!({ - "new_windows": self.new_windows.iter().map(|window| serde_json::json!({ - "window_id": u64::from(window.window_id), - "pid": i64::from(window.pid), - "app_name": window.app_name, - "title": window.title, - })).collect::>(), - "foreground_changed": self.foreground_changed, - }), - ); - } } /// Default poll deadline — new windows triggered by a click typically @@ -536,37 +511,6 @@ mod tests { ); } - #[test] - fn changes_attach_machine_readable_rebind_metadata_without_mutating_no_change() { - let changes = Changes { - new_windows: vec![WindowEvent { - window_id: 41, - pid: 123, - app_name: "TextEdit".to_owned(), - title: "Open".to_owned(), - }], - foreground_changed: false, - }; - let mut structured = serde_json::json!({"path": "ax"}); - changes.add_to_structured(&mut structured); - assert_eq!( - structured["window_change"], - serde_json::json!({ - "new_windows": [{ - "window_id": 41, - "pid": 123, - "app_name": "TextEdit", - "title": "Open", - }], - "foreground_changed": false, - }) - ); - - let mut unchanged = serde_json::json!({"path": "ax"}); - Changes::no_change().add_to_structured(&mut unchanged); - assert!(unchanged.get("window_change").is_none()); - } - /// Regression: `snapshot(prior_front)` must store the caller's /// captured front pid verbatim (rather than re-reading it inside /// the function and racing with concurrent activations). From 470de2deb7453fa90f62a776c071a639c27685f4 Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 14:38:55 +0800 Subject: [PATCH 06/16] docs(cua-driver): consolidate rediscovery architecture --- .../architecture-comparison.dot | 43 ++++ .../architecture-comparison.svg | 194 ++++++++++++++++++ .../combined.dot | 27 --- .../combined.svg | 170 --------------- .../current-cua.dot | 13 -- .../current-cua.svg | 90 -------- .../pi-computer-use.dot | 12 -- .../pi-computer-use.svg | 78 ------- .../pr-2746.dot | 16 -- .../pr-2746.svg | 104 ---------- .../recovery-ladder.dot | 20 -- .../recovery-ladder.svg | 118 ----------- .../docs/post-action-surface-rediscovery.md | 19 +- 13 files changed, 244 insertions(+), 660 deletions(-) create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot new file mode 100644 index 0000000000..d3ab5b08ba --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot @@ -0,0 +1,43 @@ +digraph rediscovery { + graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.25", ranksep="0.35", compound=true]; + node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=11, margin="0.12,0.08"]; + edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=9, arrowsize=0.65]; + + subgraph cluster_current { + label="current Cua"; color="#8c959f"; style="dashed"; + current_before [label="global window ids"]; + current_action [label="native action"]; + current_prose [label="diagnostic prose only"]; + current_before -> current_action -> current_prose; + } + + subgraph cluster_2746 { + label="#2746"; color="#8c959f"; style="dashed"; + pr_global [label="global window diff"]; + pr_json [label="legacy JSON adapter"]; + pr_effect [label="effect promotion and count-based rebind"]; + pr_global -> pr_json -> pr_effect; + } + + subgraph cluster_picu { + label="pi-computer-use"; color="#8c959f"; style="dashed"; + pi_before [label="target AX roots"]; + pi_action [label="native action"]; + pi_after [label="authoritative root diff"]; + pi_resolve [label="refresh modal ownership"]; + pi_before -> pi_action -> pi_after -> pi_resolve; + } + + subgraph cluster_combined { + label="combined design"; color="#8c959f"; style="dashed"; + best_observer [label="typed target-root observer"]; + best_action [label="typed actuator outcome"]; + best_record [label="shared action record"]; + best_resolver [label="shared candidate validation"]; + best_public [label="closed result preserves effect"]; + best_observer -> best_record; + best_action -> best_record; + best_observer -> best_resolver -> best_public; + best_record -> best_public; + } +} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg new file mode 100644 index 0000000000..52dd00f321 --- /dev/null +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg @@ -0,0 +1,194 @@ + + + + + + +rediscovery + +cluster_current + +current Cua + + +cluster_2746 + +#2746 + + +cluster_picu + +pi-computer-use + + +cluster_combined + +combined design + + + +current_before + +global window ids + + + +current_action + +native action + + + +current_before->current_action + + + + + +current_prose + +diagnostic prose only + + + +current_action->current_prose + + + + + +pr_global + +global window diff + + + +pr_json + +legacy JSON adapter + + + +pr_global->pr_json + + + + + +pr_effect + +effect promotion and count-based rebind + + + +pr_json->pr_effect + + + + + +pi_before + +target AX roots + + + +pi_action + +native action + + + +pi_before->pi_action + + + + + +pi_after + +authoritative root diff + + + +pi_action->pi_after + + + + + +pi_resolve + +refresh modal ownership + + + +pi_after->pi_resolve + + + + + +best_observer + +typed target-root observer + + + +best_record + +shared action record + + + +best_observer->best_record + + + + + +best_resolver + +shared candidate validation + + + +best_observer->best_resolver + + + + + +best_action + +typed actuator outcome + + + +best_action->best_record + + + + + +best_public + +closed result preserves effect + + + +best_record->best_public + + + + + +best_resolver->best_public + + + + + diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot deleted file mode 100644 index 84633b242b..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.dot +++ /dev/null @@ -1,27 +0,0 @@ -digraph g { - graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; - node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; - edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; - coordinator [label="shared action coordinator"]; - observer [label="platform target-root observer begins"]; - actuator [label="native actuator runs"]; - outcome [label="typed actuator outcome"]; - finish [label="platform observer finishes"]; - delta [label="typed target-root delta"]; - record [label="action execution record"]; - resolver [label="shared candidate resolver"]; - choice [shape=diamond, label="one validated blocking root"]; - rebind [label="emit exact logical rebind"]; - correlate [label="emit candidates for correlation"]; - public [label="closed result preserves original effect"]; - coordinator -> observer -> actuator; - actuator -> outcome -> record; - actuator -> finish -> delta; - delta -> record; - delta -> resolver -> choice; - choice -> rebind [label="yes"]; - choice -> correlate [label="no"]; - record -> public; - rebind -> public; - correlate -> public; -} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg deleted file mode 100644 index fe125bfc31..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/combined.svg +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - -g - - -coordinator - -shared action coordinator - - - -observer - -platform target-root observer begins - - - -coordinator->observer - - - - - -actuator - -native actuator runs - - - -observer->actuator - - - - - -outcome - -typed actuator outcome - - - -actuator->outcome - - - - - -finish - -platform observer finishes - - - -actuator->finish - - - - - -record - -action execution record - - - -outcome->record - - - - - -delta - -typed target-root delta - - - -finish->delta - - - - - -delta->record - - - - - -resolver - -shared candidate resolver - - - -delta->resolver - - - - - -public - -closed result preserves original effect - - - -record->public - - - - - -choice - -one validated blocking root - - - -resolver->choice - - - - - -rebind - -emit exact logical rebind - - - -choice->rebind - - -yes - - - -correlate - -emit candidates for correlation - - - -choice->correlate - - -no - - - -rebind->public - - - - - -correlate->public - - - - - diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot deleted file mode 100644 index 8d91587687..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.dot +++ /dev/null @@ -1,13 +0,0 @@ -digraph g { - graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; - node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; - edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; - action [label="macOS action tool"]; - before [label="snapshot global layer-0 window ids"]; - actuator [label="run native actuator under focus guard"]; - after [label="poll global layer-0 window ids"]; - prose [label="append diagnostic prose"]; - adapter [label="project action result without topology"]; - stale [label="caller remains bound to prior window"]; - action -> before -> actuator -> after -> prose -> adapter -> stale; -} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg deleted file mode 100644 index a649e2187b..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/current-cua.svg +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - -g - - -action - -macOS action tool - - - -before - -snapshot global layer-0 window ids - - - -action->before - - - - - -actuator - -run native actuator under focus guard - - - -before->actuator - - - - - -after - -poll global layer-0 window ids - - - -actuator->after - - - - - -prose - -append diagnostic prose - - - -after->prose - - - - - -adapter - -project action result without topology - - - -prose->adapter - - - - - -stale - -caller remains bound to prior window - - - -adapter->stale - - - - - diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot deleted file mode 100644 index e4990f7941..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.dot +++ /dev/null @@ -1,12 +0,0 @@ -digraph g { - graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; - node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; - edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; - before [label="snapshot target accessibility roots"]; - action [label="perform action"]; - signal [label="event or native-window signal wakes observer"]; - after [label="snapshot target accessibility roots again"]; - delta [label="derive appeared, closed, and focused roots"]; - resolver [label="refresh and validate visible modal root"]; - before -> action -> signal -> after -> delta -> resolver; -} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg deleted file mode 100644 index bb0aa52260..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pi-computer-use.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - -g - - -before - -snapshot target accessibility roots - - - -action - -perform action - - - -before->action - - - - - -signal - -event or native-window signal wakes observer - - - -action->signal - - - - - -after - -snapshot target accessibility roots again - - - -signal->after - - - - - -delta - -derive appeared, closed, and focused roots - - - -after->delta - - - - - -resolver - -refresh and validate visible modal root - - - -delta->resolver - - - - - diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot deleted file mode 100644 index 073d123ea7..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.dot +++ /dev/null @@ -1,16 +0,0 @@ -digraph g { - graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; - node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; - edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; - action [label="macOS action tool"]; - global [label="diff all visible desktop windows"]; - json [label="write legacy JSON"]; - parse [label="parse shared action record"]; - confirm [label="promote effect to confirmed"]; - count [shape=diamond, label="new window count"]; - exact [label="emit exact rebind"]; - ambiguous [label="emit ambiguous rebind"]; - action -> global -> json -> parse -> confirm -> count; - count -> exact [label="one"]; - count -> ambiguous [label="many"]; -} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg deleted file mode 100644 index 849434b38c..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/pr-2746.svg +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - -g - - -action - -macOS action tool - - - -global - -diff all visible desktop windows - - - -action->global - - - - - -json - -write legacy JSON - - - -global->json - - - - - -parse - -parse shared action record - - - -json->parse - - - - - -confirm - -promote effect to confirmed - - - -parse->confirm - - - - - -count - -new window count - - - -confirm->count - - - - - -exact - -emit exact rebind - - - -count->exact - - -one - - - -ambiguous - -emit ambiguous rebind - - - -count->ambiguous - - -many - - - diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot deleted file mode 100644 index 8e9560a531..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.dot +++ /dev/null @@ -1,20 +0,0 @@ -digraph g { - graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.3", ranksep="0.4"]; - node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=12, margin="0.15,0.1"]; - edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=10, arrowsize=0.7]; - result [label="action result contains window change"]; - exact [shape=diamond, label="exact validated target"]; - refresh [label="refresh target window without activation"]; - list [label="correlate candidates with list_windows"]; - found [shape=diamond, label="blocking surface identified"]; - desktop [label="request one privacy-sensitive desktop frame"]; - correlate [label="correlate and return to window scope"]; - background [label="resume background semantic actions"]; - result -> exact; - exact -> refresh [label="yes"]; - exact -> list [label="no"]; - list -> found; - found -> refresh [label="yes"]; - found -> desktop [label="no"]; - desktop -> correlate -> refresh -> background; -} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg deleted file mode 100644 index 0459fdb2a4..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/recovery-ladder.svg +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - -g - - -result - -action result contains window change - - - -exact - -exact validated target - - - -result->exact - - - - - -refresh - -refresh target window without activation - - - -exact->refresh - - -yes - - - -list - -correlate candidates with list_windows - - - -exact->list - - -no - - - -background - -resume background semantic actions - - - -refresh->background - - - - - -found - -blocking surface identified - - - -list->found - - - - - -found->refresh - - -yes - - - -desktop - -request one privacy-sensitive desktop frame - - - -found->desktop - - -no - - - -correlate - -correlate and return to window scope - - - -desktop->correlate - - - - - -correlate->refresh - - - - - diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index 182ec112b2..d67fc95410 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -5,13 +5,13 @@ keeps Cua Driver's typed action-result contract while replacing its global window-count heuristic with target-scoped root observation and shared candidate validation. +![architecture comparison](diagrams/post-action-surface-rediscovery/architecture-comparison.svg) + ## Existing Cua Driver flow Cua Driver already protects foreground focus and detects visible native windows, but the topology survives only in diagnostic text. -![current cua architecture](diagrams/post-action-surface-rediscovery/current-cua.svg) - ## What pull request #2746 contributes Pull request #2746 adds a closed `window_change` record and explicit `rebind` @@ -20,8 +20,6 @@ Its detector, however, still treats every new desktop window as action-related, selects an exact target from list length alone, and promotes any topology change to confirmed action effect. -![pr 2746 architecture](diagrams/post-action-surface-rediscovery/pr-2746.svg) - This draft preserves from #2746: - the portable `window_change` record; @@ -50,16 +48,12 @@ The design is adapted rather than copied verbatim. Cua Driver keeps its Rust platform boundary, focus-suppression leases, action execution record, closed contract vocabulary, and explicit harness-owned escalation policy. -![pi computer use architecture](diagrams/post-action-surface-rediscovery/pi-computer-use.svg) - ## Combined architecture Detection answers what changed. Resolution answers which surface is safe to bind. Action accounting answers what the actuator proved. None substitutes for another. -![combined architecture](diagrams/post-action-surface-rediscovery/combined.svg) - ### Platform observer Each platform adapter owns native root discovery. It returns typed facts and @@ -105,10 +99,11 @@ delivery. ## Recovery ladder -![recovery ladder architecture](diagrams/post-action-surface-rediscovery/recovery-ladder.svg) - -The request-scoped desktop frame remains a separate final perception rung. It -must not silently widen persistent capture scope. +Use an exact validated target when present. Otherwise correlate the candidates +with `list_windows`; use one request-scoped, privacy-sensitive desktop frame +only when structured correlation fails. After recovery, refresh window-scoped +state and resume background semantic actions. The fallback must not silently +widen persistent capture scope. ## Validation plan From f096e1f1b49eaa6dd10ba04c10d2d10a62934b6e Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 22:20:21 +0800 Subject: [PATCH 07/16] feat(cua-driver): centralize post-action surface rebinding Salvaged from #2746. Co-authored-by: Francesco Bonacci <195596869+f-trycua@users.noreply.github.com> Co-authored-by: Zane Chee --- libs/cua-driver/contract/manifest.json | 963 +++++++++++++++++- .../cua-driver/docs/action-result-contract.md | 21 +- .../docs/post-action-surface-rediscovery.md | 41 +- .../python/src/cua_driver/__init__.py | 6 + .../python/src/cua_driver/_native_contract.py | 436 ++++++-- .../rust/Skills/cua-driver/SKILL.md | 15 +- .../crates/cua-driver-contract/src/lib.rs | 15 +- .../crates/cua-driver-contract/src/outputs.rs | 145 ++- .../cua-driver-core/src/action_record.rs | 313 +++++- .../crates/cua-driver-core/src/history.rs | 1 - .../crates/cua-driver-core/src/protocol.rs | 3 + .../rust/crates/cua-driver-core/src/tool.rs | 7 + .../installed_app_textedit_macos_test.rs | 146 +++ .../crates/platform-linux/src/tools/impl_.rs | 3 + .../crates/platform-macos/src/ax/bindings.rs | 11 +- .../crates/platform-macos/src/focus_guard.rs | 13 +- .../rust/crates/platform-macos/src/lib.rs | 4 +- .../src/post_action_observer.rs | 368 +++++++ .../crates/platform-macos/src/tools/click.rs | 38 +- .../crates/platform-macos/src/tools/drag.rs | 12 +- .../src/tools/get_desktop_state.rs | 1 + .../src/tools/get_window_state.rs | 1 + .../crates/platform-macos/src/tools/hotkey.rs | 17 +- .../crates/platform-macos/src/tools/mod.rs | 72 +- .../platform-macos/src/tools/press_key.rs | 21 +- .../crates/platform-macos/src/tools/scroll.rs | 20 +- .../platform-macos/src/tools/set_value.rs | 11 +- .../platform-macos/src/tools/type_text.rs | 88 +- .../crates/platform-macos/src/tools/zoom.rs | 1 + .../src/window_change_detector.rs | 530 ---------- .../platform-windows/src/tools/impl_.rs | 3 + .../src/native/cua_driver_contract.ts | 226 +++- scripts/ci/macos/run-rust-e2e.sh | 3 + 33 files changed, 2552 insertions(+), 1003 deletions(-) create mode 100644 libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs delete mode 100644 libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs diff --git a/libs/cua-driver/contract/manifest.json b/libs/cua-driver/contract/manifest.json index 9965f5f455..8bb415826d 100644 --- a/libs/cua-driver/contract/manifest.json +++ b/libs/cua-driver/contract/manifest.json @@ -172,7 +172,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -181,9 +182,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -201,8 +244,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -227,6 +269,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -575,7 +674,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -584,9 +684,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -604,8 +746,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -630,6 +771,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -1565,7 +1763,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -1574,9 +1773,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -1594,8 +1835,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -1620,6 +1860,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -1736,7 +2033,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -1745,9 +2043,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -1765,8 +2105,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -1791,6 +2130,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2088,7 +2484,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -2097,9 +2494,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2117,8 +2556,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -2143,6 +2581,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2305,7 +2800,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -2314,9 +2810,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2334,8 +2872,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -2360,6 +2897,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2542,7 +3136,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -2551,9 +3146,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -2571,8 +3208,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -2597,6 +3233,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -3026,7 +3719,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -3035,9 +3729,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -3055,8 +3791,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -3081,6 +3816,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -3375,7 +4167,8 @@ "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ], "type": "string" }, @@ -3384,9 +4177,51 @@ "pixel", "foreground", "page", - "session" + "session", + "rebind" ], "type": "string" + }, + "window": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": [ + "object", + "null" + ] } }, "required": [ @@ -3404,8 +4239,7 @@ "properties": { "kind": { "enum": [ - "value_readback", - "window_change" + "value_readback" ], "type": "string" } @@ -3430,6 +4264,63 @@ "trusted_input" ], "type": "string" + }, + "window_change": { + "additionalProperties": false, + "properties": { + "foreground_changed": { + "type": "boolean" + }, + "new_windows": { + "items": { + "additionalProperties": false, + "properties": { + "app_name": { + "type": "string" + }, + "kind": { + "enum": [ + "window", + "dialog", + "sheet", + "popover" + ], + "type": "string" + }, + "modal": { + "type": "boolean" + }, + "pid": { + "format": "int64", + "type": "integer" + }, + "window_id": { + "format": "uint64", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "pid", + "window_id", + "app_name", + "title", + "kind", + "modal" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "new_windows", + "foreground_changed" + ], + "type": [ + "object", + "null" + ] } }, "required": [ diff --git a/libs/cua-driver/docs/action-result-contract.md b/libs/cua-driver/docs/action-result-contract.md index bb61a40aab..5eb7f0fb89 100644 --- a/libs/cua-driver/docs/action-result-contract.md +++ b/libs/cua-driver/docs/action-result-contract.md @@ -32,9 +32,10 @@ Every successful action returns a closed `structuredContent` object: | `effect` | `confirmed`, `partial`, `unverifiable`, `suspected_noop`, `refused` | | `route` | `accessibility`, `synthetic_events`, `global_input`, `dom`, `trusted_input` | | `delivery.mode` | `background`, `foreground`, `not_applicable`, `unknown` | -| `evidence[].kind` | `value_readback`, `window_change` | -| `escalation.target` | `pixel`, `foreground`, `page`, `session` | -| `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required` | +| `evidence[].kind` | `value_readback` | +| `window_change.new_windows[]` | target-scoped, owner-verified `pid`, `window_id`, application, title, surface kind, and modality | +| `escalation.target` | `pixel`, `foreground`, `page`, `session`, `rebind` | +| `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required`, `surface_changed` | The action-result tools are: @@ -53,7 +54,7 @@ scope, targets, platform transport names, diagnostic pointers, or the old The invariants are: -- `confirmed` has publishable readback or window-change evidence; +- `confirmed` has publishable value readback; topology alone cannot confirm an action; - `partial` has `delivery.delivered_count`; - `refused` has neither delivery nor evidence. @@ -74,8 +75,15 @@ resolution semantics; the guard does not replace or reinterpret them. An action that reached an actuator but lacks a trusted readback is `unverifiable`, not `confirmed`. Screenshot change, native API acceptance, -event receipt, and operator observation may remain useful internal diagnostics, -but they do not independently justify `confirmed`. +event receipt, window topology, and operator observation may remain useful +facts, but they do not independently justify `confirmed`. + +On macOS, one action decorator snapshots accessibility roots for the target PID +before and after dispatch. It reports new windows, dialogs, sheets, and popovers +without activating them. An exact `rebind` target is present only when one new +owner-verified root is modal or focused; ambiguous changes require +`list_windows`. Windows and Linux currently emit no topology record rather than +substituting a global desktop heuristic. ## Verification remains separate @@ -125,6 +133,7 @@ An optional escalation is advice, not an automatic retry: | `foreground` | explicitly select foreground delivery when session policy permits | | `page` | bind the native window to a supported browser page route | | `session` | prepare or explicitly widen the session only when policy permits | +| `rebind` | refresh the validated window target without activation; use `list_windows` when no exact target is present | SDK integrators, OpenClaw, Hermes, and other agent hosts can implement different policies above this same narrow fact contract without duplicating platform diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index d67fc95410..0ab8d63924 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -1,6 +1,7 @@ # Post-action surface rediscovery -This document records the implementation boundary for issue #2238. The draft +This document records the implementation boundary for issue +[#2238](https://github.com/trycua/cua/issues/2238). The change keeps Cua Driver's typed action-result contract while replacing its global window-count heuristic with target-scoped root observation and shared candidate validation. @@ -12,15 +13,16 @@ validation. Cua Driver already protects foreground focus and detects visible native windows, but the topology survives only in diagnostic text. -## What pull request #2746 contributes +## What pull request [#2746](https://github.com/trycua/cua/pull/2746) contributes -Pull request #2746 adds a closed `window_change` record and explicit `rebind` -escalation. Those are useful public semantics and remain part of this draft. +Pull request [#2746](https://github.com/trycua/cua/pull/2746) adds a closed `window_change` record and explicit `rebind` +escalation. Those are useful public semantics and remain in this implementation. Its detector, however, still treats every new desktop window as action-related, selects an exact target from list length alone, and promotes any topology change to confirmed action effect. -This draft preserves from #2746: +This implementation preserves from +[#2746](https://github.com/trycua/cua/pull/2746): - the portable `window_change` record; - explicit, non-activating `rebind` advice; @@ -33,7 +35,7 @@ branch-local JSON mutation. ## What is adapted from pi-computer-use -The target-root observer follows the architecture proven in +The target-root observer adapts the architecture proven in `injaneity/pi-computer-use` commit `022a280a377065c95736cc15f684bf1fad46479e`: @@ -56,22 +58,24 @@ another. ### Platform observer -Each platform adapter owns native root discovery. It returns typed facts and -never chooses policy. macOS uses target-process accessibility roots as the final -source of truth, with CGWindow and AX notifications as bounded early-wake -signals. Windows uses the equivalent UIA and HWND ownership facts. Linux must -return the same semantics where AT-SPI and the compositor expose them, or an -explicit unsupported limitation. +Each platform adapter owns native root discovery and never chooses rebind +policy. macOS diffs target-process accessibility windows and their sheet, +dialog, and popover children, then verifies each mapped CGWindow owner. This +keeps AppKit's out-of-process Open/Save panel service addressable without +scanning unrelated desktop changes. Windows and Linux currently omit topology instead +of substituting an unscoped desktop heuristic; equivalent UIA or AT-SPI +observers can later feed the same core resolver. -Cross-application handoffs are reported separately from target-owned roots. -They never become exact targets from temporal proximity alone. +A cross-application handoff becomes exact only when it appears in the target's +AX roots and WindowServer independently verifies the mapped owner. Temporal +proximity alone never creates a candidate. ### Shared action coordinator -The coordinator starts observation immediately before dispatch and finishes it -immediately after dispatch. It attaches the typed delta directly to the action -execution record, including partial and failed delivery. Platform tools must not -serialize private topology to JSON and ask the legacy adapter to parse it back. +One macOS action decorator starts observation immediately before dispatch and +finishes it afterward. The core dispatch seam attaches its typed delta to the +action execution record, including partial and failed delivery. Platform tools +do not serialize topology to JSON or ask the legacy adapter to parse it back. ### Shared resolver @@ -88,7 +92,6 @@ delivery. - `window_change` is independent from `effect`; topology cannot promote an unverifiable, partial, suspected-noop, or refused action to confirmed. -- `window_change` evidence means only that topology was observed. - an exact escalation target must also appear in the accompanying topology. - unrelated desktop windows cannot become target-owned candidates. - an empty or unchanged delta cannot produce rebind advice. diff --git a/libs/cua-driver/python/src/cua_driver/__init__.py b/libs/cua-driver/python/src/cua_driver/__init__.py index 5a83149c70..8e06329e73 100644 --- a/libs/cua-driver/python/src/cua_driver/__init__.py +++ b/libs/cua-driver/python/src/cua_driver/__init__.py @@ -58,6 +58,9 @@ ActionEvidenceKind, ActionResult, ActionRoute, + ActionSurfaceKind, + ActionWindowChange, + ActionWindowTarget, BoundsExpectation, CaptureScope, ClickButton, @@ -180,6 +183,9 @@ def _create_private_worker_python_sdk(cls, options): "ActionEvidenceKind", "ActionResult", "ActionRoute", + "ActionSurfaceKind", + "ActionWindowChange", + "ActionWindowTarget", "BoundsExpectation", "CaptureScope", "ClickButton", diff --git a/libs/cua-driver/python/src/cua_driver/_native_contract.py b/libs/cua-driver/python/src/cua_driver/_native_contract.py index 61a69eb96c..ced7ab90c1 100644 --- a/libs/cua-driver/python/src/cua_driver/_native_contract.py +++ b/libs/cua-driver/python/src/cua_driver/_native_contract.py @@ -899,6 +899,8 @@ class ActionEscalationTarget(enum.Enum): SESSION = 3 + REBIND = 4 + class _UniffiFfiConverterTypeActionEscalationTarget(_UniffiConverterRustBuffer): @@ -913,6 +915,8 @@ def read(buf): return ActionEscalationTarget.PAGE if variant == 4: return ActionEscalationTarget.SESSION + if variant == 5: + return ActionEscalationTarget.REBIND raise InternalError("Raw enum value doesn't match any cases") @staticmethod @@ -925,6 +929,8 @@ def check_lower(value): return if value == ActionEscalationTarget.SESSION: return + if value == ActionEscalationTarget.REBIND: + return raise ValueError(value) @staticmethod @@ -937,6 +943,8 @@ def write(value, buf): buf.write_i32(3) if value == ActionEscalationTarget.SESSION: buf.write_i32(4) + if value == ActionEscalationTarget.REBIND: + buf.write_i32(5) @@ -957,6 +965,8 @@ class ActionEscalationReason(enum.Enum): PERMISSION_REQUIRED = 4 + SURFACE_CHANGED = 5 + class _UniffiFfiConverterTypeActionEscalationReason(_UniffiConverterRustBuffer): @@ -973,6 +983,8 @@ def read(buf): return ActionEscalationReason.SUSPECTED_NOOP if variant == 5: return ActionEscalationReason.PERMISSION_REQUIRED + if variant == 6: + return ActionEscalationReason.SURFACE_CHANGED raise InternalError("Raw enum value doesn't match any cases") @staticmethod @@ -987,6 +999,8 @@ def check_lower(value): return if value == ActionEscalationReason.PERMISSION_REQUIRED: return + if value == ActionEscalationReason.SURFACE_CHANGED: + return raise ValueError(value) @staticmethod @@ -1001,25 +1015,253 @@ def write(value, buf): buf.write_i32(4) if value == ActionEscalationReason.PERMISSION_REQUIRED: buf.write_i32(5) + if value == ActionEscalationReason.SURFACE_CHANGED: + buf.write_i32(6) + + + +class _UniffiFfiConverterInt64(_UniffiConverterPrimitiveInt): + CLASS_NAME = "i64" + VALUE_MIN = -2**63 + VALUE_MAX = 2**63 + + @staticmethod + def read(buf): + return buf.read_i64() + + @staticmethod + def write(value, buf): + buf.write_i64(value) + +class _UniffiFfiConverterUInt64(_UniffiConverterPrimitiveInt): + CLASS_NAME = "u64" + VALUE_MIN = 0 + VALUE_MAX = 2**64 + + @staticmethod + def read(buf): + return buf.read_u64() + + @staticmethod + def write(value, buf): + buf.write_u64(value) + +class _UniffiFfiConverterString: + @staticmethod + def check_lower(value): + if not isinstance(value, str): + raise TypeError("argument must be str, not {}".format(type(value).__name__)) + return value + + @staticmethod + def read(buf): + size = buf.read_i32() + if size < 0: + raise InternalError("Unexpected negative string length") + utf8_bytes = buf.read(size) + return utf8_bytes.decode("utf-8") + + @staticmethod + def write(value, buf): + utf8_bytes = value.encode("utf-8") + buf.write_i32(len(utf8_bytes)) + buf.write(utf8_bytes) + + @staticmethod + def lift(buf): + with buf.consume_with_stream() as stream: + return stream.read(stream.remaining()).decode("utf-8") + + @staticmethod + def lower(value): + with _UniffiRustBuffer.alloc_with_builder() as builder: + builder.write(value.encode("utf-8")) + return builder.finalize() + + + + + + +class ActionSurfaceKind(enum.Enum): + + WINDOW = 0 + + DIALOG = 1 + + SHEET = 2 + + POPOVER = 3 + + + +class _UniffiFfiConverterTypeActionSurfaceKind(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + variant = buf.read_i32() + if variant == 1: + return ActionSurfaceKind.WINDOW + if variant == 2: + return ActionSurfaceKind.DIALOG + if variant == 3: + return ActionSurfaceKind.SHEET + if variant == 4: + return ActionSurfaceKind.POPOVER + raise InternalError("Raw enum value doesn't match any cases") + + @staticmethod + def check_lower(value): + if value == ActionSurfaceKind.WINDOW: + return + if value == ActionSurfaceKind.DIALOG: + return + if value == ActionSurfaceKind.SHEET: + return + if value == ActionSurfaceKind.POPOVER: + return + raise ValueError(value) + + @staticmethod + def write(value, buf): + if value == ActionSurfaceKind.WINDOW: + buf.write_i32(1) + if value == ActionSurfaceKind.DIALOG: + buf.write_i32(2) + if value == ActionSurfaceKind.SHEET: + buf.write_i32(3) + if value == ActionSurfaceKind.POPOVER: + buf.write_i32(4) + + + +class _UniffiFfiConverterBoolean: + @classmethod + def check_lower(cls, value): + return not not value + + @classmethod + def lower(cls, value): + return 1 if value else 0 + + @staticmethod + def lift(value): + return value != 0 + + @classmethod + def read(cls, buf): + return cls.lift(buf.read_u8()) + + @classmethod + def write(cls, value, buf): + buf.write_u8(value) + +@dataclass +class ActionWindowTarget: + """ + A target-owned native interaction root discovered after an action. +""" + def __init__(self, *, pid:int, window_id:int, app_name:str, title:str, kind:ActionSurfaceKind, modal:bool): + self.pid = pid + self.window_id = window_id + self.app_name = app_name + self.title = title + self.kind = kind + self.modal = modal + + + + + def __str__(self): + return "ActionWindowTarget(pid={}, window_id={}, app_name={}, title={}, kind={}, modal={})".format(self.pid, self.window_id, self.app_name, self.title, self.kind, self.modal) + def __eq__(self, other): + if self.pid != other.pid: + return False + if self.window_id != other.window_id: + return False + if self.app_name != other.app_name: + return False + if self.title != other.title: + return False + if self.kind != other.kind: + return False + if self.modal != other.modal: + return False + return True + +class _UniffiFfiConverterTypeActionWindowTarget(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return ActionWindowTarget( + pid=_UniffiFfiConverterInt64.read(buf), + window_id=_UniffiFfiConverterUInt64.read(buf), + app_name=_UniffiFfiConverterString.read(buf), + title=_UniffiFfiConverterString.read(buf), + kind=_UniffiFfiConverterTypeActionSurfaceKind.read(buf), + modal=_UniffiFfiConverterBoolean.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiFfiConverterInt64.check_lower(value.pid) + _UniffiFfiConverterUInt64.check_lower(value.window_id) + _UniffiFfiConverterString.check_lower(value.app_name) + _UniffiFfiConverterString.check_lower(value.title) + _UniffiFfiConverterTypeActionSurfaceKind.check_lower(value.kind) + _UniffiFfiConverterBoolean.check_lower(value.modal) + + @staticmethod + def write(value, buf): + _UniffiFfiConverterInt64.write(value.pid, buf) + _UniffiFfiConverterUInt64.write(value.window_id, buf) + _UniffiFfiConverterString.write(value.app_name, buf) + _UniffiFfiConverterString.write(value.title, buf) + _UniffiFfiConverterTypeActionSurfaceKind.write(value.kind, buf) + _UniffiFfiConverterBoolean.write(value.modal, buf) + +class _UniffiFfiConverterOptionalTypeActionWindowTarget(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + if value is not None: + _UniffiFfiConverterTypeActionWindowTarget.check_lower(value) + @classmethod + def write(cls, value, buf): + if value is None: + buf.write_u8(0) + return + buf.write_u8(1) + _UniffiFfiConverterTypeActionWindowTarget.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.read_u8() + if flag == 0: + return None + elif flag == 1: + return _UniffiFfiConverterTypeActionWindowTarget.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") @dataclass class ActionEscalation: - def __init__(self, *, target:ActionEscalationTarget, reason:ActionEscalationReason): + def __init__(self, *, target:ActionEscalationTarget, reason:ActionEscalationReason, window:typing.Optional[ActionWindowTarget]): self.target = target self.reason = reason + self.window = window def __str__(self): - return "ActionEscalation(target={}, reason={})".format(self.target, self.reason) + return "ActionEscalation(target={}, reason={}, window={})".format(self.target, self.reason, self.window) def __eq__(self, other): if self.target != other.target: return False if self.reason != other.reason: return False + if self.window != other.window: + return False return True class _UniffiFfiConverterTypeActionEscalation(_UniffiConverterRustBuffer): @@ -1028,17 +1270,20 @@ def read(buf): return ActionEscalation( target=_UniffiFfiConverterTypeActionEscalationTarget.read(buf), reason=_UniffiFfiConverterTypeActionEscalationReason.read(buf), + window=_UniffiFfiConverterOptionalTypeActionWindowTarget.read(buf), ) @staticmethod def check_lower(value): _UniffiFfiConverterTypeActionEscalationTarget.check_lower(value.target) _UniffiFfiConverterTypeActionEscalationReason.check_lower(value.reason) + _UniffiFfiConverterOptionalTypeActionWindowTarget.check_lower(value.window) @staticmethod def write(value, buf): _UniffiFfiConverterTypeActionEscalationTarget.write(value.target, buf) _UniffiFfiConverterTypeActionEscalationReason.write(value.reason, buf) + _UniffiFfiConverterOptionalTypeActionWindowTarget.write(value.window, buf) @@ -1049,8 +1294,6 @@ class ActionEvidenceKind(enum.Enum): VALUE_READBACK = 0 - WINDOW_CHANGE = 1 - class _UniffiFfiConverterTypeActionEvidenceKind(_UniffiConverterRustBuffer): @@ -1059,24 +1302,18 @@ def read(buf): variant = buf.read_i32() if variant == 1: return ActionEvidenceKind.VALUE_READBACK - if variant == 2: - return ActionEvidenceKind.WINDOW_CHANGE raise InternalError("Raw enum value doesn't match any cases") @staticmethod def check_lower(value): if value == ActionEvidenceKind.VALUE_READBACK: return - if value == ActionEvidenceKind.WINDOW_CHANGE: - return raise ValueError(value) @staticmethod def write(value, buf): if value == ActionEvidenceKind.VALUE_READBACK: buf.write_i32(1) - if value == ActionEvidenceKind.WINDOW_CHANGE: - buf.write_i32(2) @@ -1319,6 +1556,93 @@ def read(cls, buf): else: raise InternalError("Unexpected flag byte for optional type") +class _UniffiFfiConverterSequenceTypeActionWindowTarget(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + for item in value: + _UniffiFfiConverterTypeActionWindowTarget.check_lower(item) + + @classmethod + def write(cls, value, buf): + items = len(value) + buf.write_i32(items) + for item in value: + _UniffiFfiConverterTypeActionWindowTarget.write(item, buf) + + @classmethod + def read(cls, buf): + count = buf.read_i32() + if count < 0: + raise InternalError("Unexpected negative sequence length") + + return [ + _UniffiFfiConverterTypeActionWindowTarget.read(buf) for i in range(count) + ] + +@dataclass +class ActionWindowChange: + """ + Read-only topology observed for the action's target process. +""" + def __init__(self, *, new_windows:typing.List[ActionWindowTarget], foreground_changed:bool): + self.new_windows = new_windows + self.foreground_changed = foreground_changed + + + + + def __str__(self): + return "ActionWindowChange(new_windows={}, foreground_changed={})".format(self.new_windows, self.foreground_changed) + def __eq__(self, other): + if self.new_windows != other.new_windows: + return False + if self.foreground_changed != other.foreground_changed: + return False + return True + +class _UniffiFfiConverterTypeActionWindowChange(_UniffiConverterRustBuffer): + @staticmethod + def read(buf): + return ActionWindowChange( + new_windows=_UniffiFfiConverterSequenceTypeActionWindowTarget.read(buf), + foreground_changed=_UniffiFfiConverterBoolean.read(buf), + ) + + @staticmethod + def check_lower(value): + _UniffiFfiConverterSequenceTypeActionWindowTarget.check_lower(value.new_windows) + _UniffiFfiConverterBoolean.check_lower(value.foreground_changed) + + @staticmethod + def write(value, buf): + _UniffiFfiConverterSequenceTypeActionWindowTarget.write(value.new_windows, buf) + _UniffiFfiConverterBoolean.write(value.foreground_changed, buf) + +class _UniffiFfiConverterOptionalTypeActionWindowChange(_UniffiConverterRustBuffer): + @classmethod + def check_lower(cls, value): + if value is not None: + _UniffiFfiConverterTypeActionWindowChange.check_lower(value) + + @classmethod + def write(cls, value, buf): + if value is None: + buf.write_u8(0) + return + + buf.write_u8(1) + _UniffiFfiConverterTypeActionWindowChange.write(value, buf) + + @classmethod + def read(cls, buf): + flag = buf.read_u8() + if flag == 0: + return None + elif flag == 1: + return _UniffiFfiConverterTypeActionWindowChange.read(buf) + else: + raise InternalError("Unexpected flag byte for optional type") + class _UniffiFfiConverterOptionalTypeActionEscalation(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -1346,18 +1670,19 @@ def read(cls, buf): @dataclass class ActionResult: - def __init__(self, *, effect:ActionEffect, route:ActionRoute, delivery:typing.Optional[ActionDelivery], evidence:typing.Optional[typing.List[ActionEvidence]], escalation:typing.Optional[ActionEscalation]): + def __init__(self, *, effect:ActionEffect, route:ActionRoute, delivery:typing.Optional[ActionDelivery], evidence:typing.Optional[typing.List[ActionEvidence]], window_change:typing.Optional[ActionWindowChange], escalation:typing.Optional[ActionEscalation]): self.effect = effect self.route = route self.delivery = delivery self.evidence = evidence + self.window_change = window_change self.escalation = escalation def __str__(self): - return "ActionResult(effect={}, route={}, delivery={}, evidence={}, escalation={})".format(self.effect, self.route, self.delivery, self.evidence, self.escalation) + return "ActionResult(effect={}, route={}, delivery={}, evidence={}, window_change={}, escalation={})".format(self.effect, self.route, self.delivery, self.evidence, self.window_change, self.escalation) def __eq__(self, other): if self.effect != other.effect: return False @@ -1367,6 +1692,8 @@ def __eq__(self, other): return False if self.evidence != other.evidence: return False + if self.window_change != other.window_change: + return False if self.escalation != other.escalation: return False return True @@ -1379,6 +1706,7 @@ def read(buf): route=_UniffiFfiConverterTypeActionRoute.read(buf), delivery=_UniffiFfiConverterOptionalTypeActionDelivery.read(buf), evidence=_UniffiFfiConverterOptionalSequenceTypeActionEvidence.read(buf), + window_change=_UniffiFfiConverterOptionalTypeActionWindowChange.read(buf), escalation=_UniffiFfiConverterOptionalTypeActionEscalation.read(buf), ) @@ -1388,6 +1716,7 @@ def check_lower(value): _UniffiFfiConverterTypeActionRoute.check_lower(value.route) _UniffiFfiConverterOptionalTypeActionDelivery.check_lower(value.delivery) _UniffiFfiConverterOptionalSequenceTypeActionEvidence.check_lower(value.evidence) + _UniffiFfiConverterOptionalTypeActionWindowChange.check_lower(value.window_change) _UniffiFfiConverterOptionalTypeActionEscalation.check_lower(value.escalation) @staticmethod @@ -1396,6 +1725,7 @@ def write(value, buf): _UniffiFfiConverterTypeActionRoute.write(value.route, buf) _UniffiFfiConverterOptionalTypeActionDelivery.write(value.delivery, buf) _UniffiFfiConverterOptionalSequenceTypeActionEvidence.write(value.evidence, buf) + _UniffiFfiConverterOptionalTypeActionWindowChange.write(value.window_change, buf) _UniffiFfiConverterOptionalTypeActionEscalation.write(value.escalation, buf) class _UniffiFfiConverterFloat64(_UniffiConverterPrimitiveFloat): @@ -1486,51 +1816,6 @@ def write(value, buf): _UniffiFfiConverterFloat64.write(value.height, buf) _UniffiFfiConverterOptionalFloat64.write(value.tolerance_px, buf) -class _UniffiFfiConverterUInt64(_UniffiConverterPrimitiveInt): - CLASS_NAME = "u64" - VALUE_MIN = 0 - VALUE_MAX = 2**64 - - @staticmethod - def read(buf): - return buf.read_u64() - - @staticmethod - def write(value, buf): - buf.write_u64(value) - -class _UniffiFfiConverterString: - @staticmethod - def check_lower(value): - if not isinstance(value, str): - raise TypeError("argument must be str, not {}".format(type(value).__name__)) - return value - - @staticmethod - def read(buf): - size = buf.read_i32() - if size < 0: - raise InternalError("Unexpected negative string length") - utf8_bytes = buf.read(size) - return utf8_bytes.decode("utf-8") - - @staticmethod - def write(value, buf): - utf8_bytes = value.encode("utf-8") - buf.write_i32(len(utf8_bytes)) - buf.write(utf8_bytes) - - @staticmethod - def lift(buf): - with buf.consume_with_stream() as stream: - return stream.read(stream.remaining()).decode("utf-8") - - @staticmethod - def lower(value): - with _UniffiRustBuffer.alloc_with_builder() as builder: - builder.write(value.encode("utf-8")) - return builder.finalize() - @@ -1908,27 +2193,6 @@ def write(value, buf): _UniffiFfiConverterOptionalTypeClickButton.write(value.button, buf) _UniffiFfiConverterOptionalUInt32.write(value.count, buf) -class _UniffiFfiConverterBoolean: - @classmethod - def check_lower(cls, value): - return not not value - - @classmethod - def lower(cls, value): - return 1 if value else 0 - - @staticmethod - def lift(value): - return value != 0 - - @classmethod - def read(cls, buf): - return cls.lift(buf.read_u8()) - - @classmethod - def write(cls, value, buf): - buf.write_u8(value) - @dataclass class ClipboardReadInput: def __init__(self, *, include_text:bool, session:typing.Optional[str]): @@ -5060,19 +5324,6 @@ def write(value, buf): _UniffiFfiConverterOptionalTypeDesktopScope.write(value.scope, buf) _UniffiFfiConverterOptionalString.write(value.session, buf) -class _UniffiFfiConverterInt64(_UniffiConverterPrimitiveInt): - CLASS_NAME = "i64" - VALUE_MIN = -2**63 - VALUE_MAX = 2**63 - - @staticmethod - def read(buf): - return buf.read_i64() - - @staticmethod - def write(value, buf): - buf.write_i64(value) - class _UniffiFfiConverterSequenceTypeStatePredicate(_UniffiConverterRustBuffer): @classmethod def check_lower(cls, value): @@ -5305,6 +5556,7 @@ def write(value, buf): "ActionDeliveryMode", "ActionEscalationTarget", "ActionEscalationReason", + "ActionSurfaceKind", "ActionEvidenceKind", "ActionEffect", "ActionRoute", @@ -5325,8 +5577,10 @@ def write(value, buf): "EffectiveScope", "Platform", "ActionDelivery", + "ActionWindowTarget", "ActionEscalation", "ActionEvidence", + "ActionWindowChange", "ActionResult", "BoundsExpectation", "ClickInput", diff --git a/libs/cua-driver/rust/Skills/cua-driver/SKILL.md b/libs/cua-driver/rust/Skills/cua-driver/SKILL.md index 2ad9f79edf..0aa9ccd82c 100644 --- a/libs/cua-driver/rust/Skills/cua-driver/SKILL.md +++ b/libs/cua-driver/rust/Skills/cua-driver/SKILL.md @@ -354,11 +354,11 @@ to pixel clicks and desktop actions too. ### Read action facts without confusing them with task success A successful action returns `effect` and `route`, with optional typed -`delivery`, `evidence`, and `escalation`. These fields describe the actuator; -they do not declare the user's task complete. +`delivery`, `evidence`, `window_change`, and `escalation`. Action effect +accounts for the actuator; topology does not declare the action or task +successful. -- `confirmed` means the driver has publishable value readback or window-change - evidence for that action. +- `confirmed` means the driver has publishable value readback for that action. - `partial` means only `delivery.delivered_count` was delivered. - `unverifiable` means the driver cannot prove the effect. - `suspected_noop` means available evidence suggests no useful change. @@ -375,11 +375,14 @@ An optional escalation is a harness instruction, never an automatic retry: stack admits the tool and exact target; - `page`: bind the native window to a supported browser page route; - `session`: a legacy compatibility signal from an older capture-scope daemon; - current callers choose a desktop target on the specific action instead. + current callers choose a desktop target on the specific action instead; +- `rebind`: refresh the exact `escalation.window` target without activating it. + If the target is absent, correlate `window_change.new_windows` with one fresh + `list_windows` call before choosing a window. Branch on the closed reason vocabulary: `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, -`suspected_noop`, and `permission_required`. +`suspected_noop`, `permission_required`, and `surface_changed`. After any action, keep using `verify_state` or a fresh state snapshot for the actual task postcondition. The multimodal harness owns visual reading and the diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs index a5a72e84dc..dde4e312ad 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs @@ -39,13 +39,14 @@ pub use inputs::{ pub use outputs::{ advertised_output_schema, refusal_envelope_schema, ActionDelivery, ActionDeliveryMode, ActionEffect, ActionEscalation, ActionEscalationReason, ActionEscalationTarget, ActionEvidence, - ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, - ClipboardReadOutput, ClipboardWriteOutput, CursorMotionOutput, CursorPointOutput, - CursorPositionOutput, CursorThemeOutput, CursorVisualOutput, DesktopStateOutput, - EffectiveScope, EndSessionOutput, GetAgentCursorStateOutput, ListSessionsOutput, - ScreenSizeOutput, SessionClientKindOutput, SessionLifecycleState, SessionOutput, - SessionStateOutput, SessionTransportOutput, SetAgentCursorEnabledOutput, - SetAgentCursorMotionOutput, SetAgentCursorThemeOutput, StartSessionOutput, ToolOutput, + ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, ActionSurfaceKind, + ActionWindowChange, ActionWindowTarget, ClipboardReadOutput, ClipboardWriteOutput, + CursorMotionOutput, CursorPointOutput, CursorPositionOutput, CursorThemeOutput, + CursorVisualOutput, DesktopStateOutput, EffectiveScope, EndSessionOutput, + GetAgentCursorStateOutput, ListSessionsOutput, ScreenSizeOutput, SessionClientKindOutput, + SessionLifecycleState, SessionOutput, SessionStateOutput, SessionTransportOutput, + SetAgentCursorEnabledOutput, SetAgentCursorMotionOutput, SetAgentCursorThemeOutput, + StartSessionOutput, ToolOutput, }; pub use verification::{ BoundsExpectation, ElementPredicate, ElementSelector, PredicateOutcome, StatePredicate, diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs index 849ed0b0b6..fb209d3c1c 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs @@ -441,7 +441,6 @@ pub struct ActionDelivery { #[serde(rename_all = "snake_case")] pub enum ActionEvidenceKind { ValueReadback, - WindowChange, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -450,6 +449,35 @@ pub struct ActionEvidence { pub kind: ActionEvidenceKind, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] +#[serde(rename_all = "snake_case")] +pub enum ActionSurfaceKind { + Window, + Dialog, + Sheet, + Popover, +} + +/// A target-owned native interaction root discovered after an action. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] +#[serde(deny_unknown_fields)] +pub struct ActionWindowTarget { + pub pid: i64, + pub window_id: u64, + pub app_name: String, + pub title: String, + pub kind: ActionSurfaceKind, + pub modal: bool, +} + +/// Read-only topology observed for the action's target process. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] +#[serde(deny_unknown_fields)] +pub struct ActionWindowChange { + pub new_windows: Vec, + pub foreground_changed: bool, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] #[serde(rename_all = "snake_case")] pub enum ActionEscalationTarget { @@ -457,6 +485,7 @@ pub enum ActionEscalationTarget { Foreground, Page, Session, + Rebind, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] @@ -467,6 +496,7 @@ pub enum ActionEscalationReason { EffectUnconfirmed, SuspectedNoop, PermissionRequired, + SurfaceChanged, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -474,6 +504,8 @@ pub enum ActionEscalationReason { pub struct ActionEscalation { pub target: ActionEscalationTarget, pub reason: ActionEscalationReason, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub window: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] @@ -486,6 +518,8 @@ pub struct ActionResult { #[serde(default, skip_serializing_if = "Option::is_none")] pub evidence: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] + pub window_change: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub escalation: Option, } @@ -495,6 +529,8 @@ pub enum ActionResultValidationError { PartialRequiresDeliveredCount, RefusedCannotHaveDelivery, RefusedCannotHaveEvidence, + RebindRequiresWindowChange, + RebindWindowMustBeCandidate, } impl std::fmt::Display for ActionResultValidationError { @@ -504,6 +540,8 @@ impl std::fmt::Display for ActionResultValidationError { Self::PartialRequiresDeliveredCount => "partial effect requires delivered_count", Self::RefusedCannotHaveDelivery => "refused effect cannot include delivery", Self::RefusedCannotHaveEvidence => "refused effect cannot include evidence", + Self::RebindRequiresWindowChange => "rebind escalation requires a window change", + Self::RebindWindowMustBeCandidate => "rebind window must be an observed candidate", }) } } @@ -514,10 +552,11 @@ impl ActionResult { pub fn validate_invariants(&self) -> Result<(), ActionResultValidationError> { match self.effect { ActionEffect::Confirmed - if self - .evidence - .as_ref() - .is_none_or(|evidence| evidence.is_empty()) => + if self.evidence.as_ref().is_none_or(|evidence| { + !evidence + .iter() + .any(|item| item.kind == ActionEvidenceKind::ValueReadback) + }) => { Err(ActionResultValidationError::ConfirmedRequiresEvidence) } @@ -536,6 +575,29 @@ impl ActionResult { ActionEffect::Refused if self.evidence.is_some() => { Err(ActionResultValidationError::RefusedCannotHaveEvidence) } + _ if self + .escalation + .as_ref() + .is_some_and(|escalation| escalation.target == ActionEscalationTarget::Rebind) + && self.window_change.is_none() => + { + Err(ActionResultValidationError::RebindRequiresWindowChange) + } + _ if self + .escalation + .as_ref() + .and_then(|escalation| escalation.window.as_ref()) + .is_some_and(|window| { + self.window_change.as_ref().is_none_or(|change| { + !change + .new_windows + .iter() + .any(|candidate| candidate == window) + }) + }) => + { + Err(ActionResultValidationError::RebindWindowMustBeCandidate) + } _ => Ok(()), } } @@ -625,6 +687,7 @@ mod tests { evidence: Some(vec![ActionEvidence { kind: ActionEvidenceKind::ValueReadback, }]), + window_change: None, escalation: None, } } @@ -638,7 +701,14 @@ mod tests { let properties = schema["properties"].as_object().expect("properties"); assert_eq!( properties.keys().map(String::as_str).collect::>(), - ["delivery", "effect", "escalation", "evidence", "route"] + [ + "delivery", + "effect", + "escalation", + "evidence", + "route", + "window_change" + ] ); assert_eq!( properties["effect"]["enum"], @@ -686,7 +756,7 @@ mod tests { assert_eq!(evidence["required"], json!(["kind"])); assert_eq!( evidence["properties"]["kind"]["enum"], - json!(["value_readback", "window_change"]) + json!(["value_readback"]) ); let escalation = object_variant(&properties["escalation"]); @@ -694,7 +764,7 @@ mod tests { assert_eq!(escalation["required"], json!(["target", "reason"])); assert_eq!( escalation["properties"]["target"]["enum"], - json!(["pixel", "foreground", "page", "session"]) + json!(["pixel", "foreground", "page", "session", "rebind"]) ); assert_eq!( escalation["properties"]["reason"]["enum"], @@ -703,9 +773,28 @@ mod tests { "delivery_failed", "effect_unconfirmed", "suspected_noop", - "permission_required" + "permission_required", + "surface_changed" ]) ); + assert!(escalation["properties"].get("window").is_some()); + + let window_change = object_variant(&properties["window_change"]); + assert_eq!(window_change["additionalProperties"], false); + assert_eq!( + window_change["required"], + json!(["new_windows", "foreground_changed"]) + ); + let target = &window_change["properties"]["new_windows"]["items"]; + assert_eq!(target["additionalProperties"], false); + assert_eq!( + target["required"], + json!(["pid", "window_id", "app_name", "title", "kind", "modal"]) + ); + assert_eq!( + target["properties"]["kind"]["enum"], + json!(["window", "dialog", "sheet", "popover"]) + ); } #[test] @@ -800,7 +889,7 @@ mod tests { ); result.delivery = None; result.evidence = Some(vec![ActionEvidence { - kind: ActionEvidenceKind::WindowChange, + kind: ActionEvidenceKind::ValueReadback, }]); assert_eq!( result.validate_invariants(), @@ -809,4 +898,40 @@ mod tests { result.evidence = None; assert_eq!(result.validate_invariants(), Ok(())); } + + #[test] + fn topology_cannot_confirm_effect_or_invent_an_exact_rebind() { + let target = ActionWindowTarget { + pid: 42, + window_id: 7, + app_name: "Editor".into(), + title: "Open".into(), + kind: ActionSurfaceKind::Sheet, + modal: true, + }; + let mut result = confirmed_result(); + result.evidence = None; + result.window_change = Some(ActionWindowChange { + new_windows: vec![target.clone()], + foreground_changed: false, + }); + assert_eq!( + result.validate_invariants(), + Err(ActionResultValidationError::ConfirmedRequiresEvidence) + ); + + result.effect = ActionEffect::Unverifiable; + result.escalation = Some(ActionEscalation { + target: ActionEscalationTarget::Rebind, + reason: ActionEscalationReason::SurfaceChanged, + window: Some(ActionWindowTarget { + window_id: 8, + ..target + }), + }); + assert_eq!( + result.validate_invariants(), + Err(ActionResultValidationError::RebindWindowMustBeCandidate) + ); + } } diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs index 4b21971a6a..1b12522e03 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs @@ -187,13 +187,92 @@ pub enum EvidenceKind { AccessibilityReadback, BrowserReadback, ValueReadback, - WindowChange, NativeApiResult, ScreenshotComparison, EventReceipt, OperatorObservation, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ActionSurfaceKind { + Window, + Dialog, + Sheet, + Popover, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ActionSurfaceTarget { + pub pid: i64, + pub window_id: u64, + pub app_name: String, + pub title: String, + pub kind: ActionSurfaceKind, + pub modal: bool, +} + +/// A target-owned candidate passed from a platform observer to shared policy. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ActionSurfaceCandidate { + pub target: ActionSurfaceTarget, + pub focused: bool, +} + +/// Typed platform observation with an optional validated exact rebind. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ActionSurfaceDelta { + pub new_windows: Vec, + pub foreground_changed: bool, + pub rebind: Option, +} + +impl ActionSurfaceDelta { + pub fn changed(&self) -> bool { + self.foreground_changed || !self.new_windows.is_empty() + } +} + +/// Apply the same fail-closed exact-target rule to every platform observer. +/// Ownership must already be proven by target-scoped native enumeration. +pub fn resolve_surface_delta( + candidates: Vec, + foreground_changed: bool, +) -> Option { + let mut unique: Vec = Vec::new(); + for candidate in candidates { + if let Some(index) = unique.iter().position(|existing| { + existing.target.pid == candidate.target.pid + && existing.target.window_id == candidate.target.window_id + }) { + if (candidate.target.modal && !unique[index].target.modal) + || (candidate.focused && !unique[index].focused) + { + unique[index] = candidate; + } + } else { + unique.push(candidate); + } + } + let new_windows: Vec = unique + .iter() + .map(|candidate| candidate.target.clone()) + .collect(); + if new_windows.is_empty() && !foreground_changed { + return None; + } + let eligible: Vec = unique + .iter() + .filter(|candidate| candidate.focused || candidate.target.modal) + .map(|candidate| candidate.target.clone()) + .collect(); + let rebind = (eligible.len() == 1).then(|| eligible[0].clone()); + Some(ActionSurfaceDelta { + new_windows, + foreground_changed, + rebind, + }) +} + /// A failed or superseded transport attempt. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ActionAttempt { @@ -241,6 +320,7 @@ pub struct ActionExecutionRecord { pub fallbacks: Vec, pub evidence: Vec, pub escalation: Option, + pub surface_delta: Option, pub delivered_count: Option, pub detail: Option, } @@ -260,6 +340,7 @@ impl ActionExecutionRecord { fallbacks: Vec::new(), evidence: Vec::new(), escalation: None, + surface_delta: None, delivered_count: None, detail: None, } @@ -275,7 +356,16 @@ impl ActionExecutionRecord { pub fn validate(&self) -> Result<(), ActionRecordValidationError> { match self.effect { - ActionEffect::Confirmed if projected_evidence(&self.evidence).is_none() => { + ActionEffect::Confirmed + if !self.evidence.iter().any(|evidence| { + matches!( + evidence.kind, + EvidenceKind::AccessibilityReadback + | EvidenceKind::BrowserReadback + | EvidenceKind::ValueReadback + ) + }) => + { Err(ActionRecordValidationError::ConfirmedRequiresEvidence) } ActionEffect::Partial @@ -293,6 +383,15 @@ impl ActionExecutionRecord { } } + /// Attach observer facts without changing the actuator's effect or + /// overwriting its internal escalation account. + pub fn observe_surface_delta(&mut self, delta: ActionSurfaceDelta) { + if !delta.changed() { + return; + } + self.surface_delta = Some(delta); + } + pub fn stable_projection( &self, ) -> Result { @@ -306,6 +405,7 @@ impl ActionExecutionRecord { }), evidence: projected_evidence(&self.evidence), escalation: self.escalation.clone(), + surface_delta: self.surface_delta.clone(), }) } @@ -360,60 +460,82 @@ impl ActionExecutionRecord { | ProjectedEvidenceKind::ValueReadback => { cua_driver_contract::ActionEvidenceKind::ValueReadback } - ProjectedEvidenceKind::WindowChange => { - cua_driver_contract::ActionEvidenceKind::WindowChange - } }, }) .collect() }), - escalation: projection.escalation.map(|escalation| { - use cua_driver_contract::{ - ActionEscalation, ActionEscalationReason, ActionEscalationTarget, - }; - - let (target, reason) = match escalation.kind { - EscalationKind::ActivateTarget - | EscalationKind::RetryWithForegroundDelivery => ( - ActionEscalationTarget::Foreground, - ActionEscalationReason::DeliveryFailed, - ), - EscalationKind::RetryWithPixelTarget => ( - ActionEscalationTarget::Pixel, - ActionEscalationReason::EffectUnconfirmed, - ), - EscalationKind::RetryWithPageAction => ( - ActionEscalationTarget::Page, - ActionEscalationReason::EffectUnconfirmed, - ), - EscalationKind::RefreshPageState => ( - ActionEscalationTarget::Page, - ActionEscalationReason::RouteUnavailable, - ), - EscalationKind::RequestPermission | EscalationKind::ElevateAccess => ( - ActionEscalationTarget::Session, - ActionEscalationReason::PermissionRequired, - ), - EscalationKind::ExpandCaptureScope => ( - ActionEscalationTarget::Session, - ActionEscalationReason::RouteUnavailable, - ), - EscalationKind::PrepareSession => ( - ActionEscalationTarget::Session, - ActionEscalationReason::RouteUnavailable, - ), - }; - ActionEscalation { - target, - reason: if projection.effect == ActionEffect::SuspectedNoop - && reason != ActionEscalationReason::PermissionRequired - { - ActionEscalationReason::SuspectedNoop - } else { - reason - }, + window_change: projection.surface_delta.as_ref().map(|delta| { + cua_driver_contract::ActionWindowChange { + new_windows: delta + .new_windows + .iter() + .map(public_surface_target) + .collect(), + foreground_changed: delta.foreground_changed, } }), + escalation: projection + .surface_delta + .as_ref() + .and_then(|delta| { + delta + .changed() + .then(|| cua_driver_contract::ActionEscalation { + target: cua_driver_contract::ActionEscalationTarget::Rebind, + reason: cua_driver_contract::ActionEscalationReason::SurfaceChanged, + window: delta.rebind.as_ref().map(public_surface_target), + }) + }) + .or_else(|| { + projection.escalation.map(|escalation| { + use cua_driver_contract::{ + ActionEscalation, ActionEscalationReason, ActionEscalationTarget, + }; + + let (target, reason) = match escalation.kind { + EscalationKind::ActivateTarget + | EscalationKind::RetryWithForegroundDelivery => ( + ActionEscalationTarget::Foreground, + ActionEscalationReason::DeliveryFailed, + ), + EscalationKind::RetryWithPixelTarget => ( + ActionEscalationTarget::Pixel, + ActionEscalationReason::EffectUnconfirmed, + ), + EscalationKind::RetryWithPageAction => ( + ActionEscalationTarget::Page, + ActionEscalationReason::EffectUnconfirmed, + ), + EscalationKind::RefreshPageState => ( + ActionEscalationTarget::Page, + ActionEscalationReason::RouteUnavailable, + ), + EscalationKind::RequestPermission | EscalationKind::ElevateAccess => ( + ActionEscalationTarget::Session, + ActionEscalationReason::PermissionRequired, + ), + EscalationKind::ExpandCaptureScope => ( + ActionEscalationTarget::Session, + ActionEscalationReason::RouteUnavailable, + ), + EscalationKind::PrepareSession => ( + ActionEscalationTarget::Session, + ActionEscalationReason::RouteUnavailable, + ), + }; + ActionEscalation { + target, + reason: if projection.effect == ActionEffect::SuspectedNoop + && reason != ActionEscalationReason::PermissionRequired + { + ActionEscalationReason::SuspectedNoop + } else { + reason + }, + window: None, + } + }) + }), }) } @@ -536,11 +658,32 @@ impl ActionExecutionRecord { "kind": escalation_kind_name(escalation.kind), "detail": escalation.detail, })), + "surface_delta": self.surface_delta.as_ref().map(|delta| serde_json::json!({ + "foreground_changed": delta.foreground_changed, + "new_window_ids": delta.new_windows.iter().map(|window| window.window_id).collect::>(), + "rebind_window_id": delta.rebind.as_ref().map(|window| window.window_id), + })), "detail": self.detail, }) } } +fn public_surface_target(target: &ActionSurfaceTarget) -> cua_driver_contract::ActionWindowTarget { + cua_driver_contract::ActionWindowTarget { + pid: target.pid, + window_id: target.window_id, + app_name: target.app_name.clone(), + title: target.title.clone(), + kind: match target.kind { + ActionSurfaceKind::Window => cua_driver_contract::ActionSurfaceKind::Window, + ActionSurfaceKind::Dialog => cua_driver_contract::ActionSurfaceKind::Dialog, + ActionSurfaceKind::Sheet => cua_driver_contract::ActionSurfaceKind::Sheet, + ActionSurfaceKind::Popover => cua_driver_contract::ActionSurfaceKind::Popover, + }, + modal: target.modal, + } +} + pub fn is_action_tool(tool_name: &str) -> bool { cua_driver_contract::is_action_result_tool(tool_name) } @@ -916,7 +1059,6 @@ fn projected_evidence(evidence: &[ActionEvidence]) -> Option ProjectedEvidenceKind::AccessibilityReadback, EvidenceKind::BrowserReadback => ProjectedEvidenceKind::BrowserReadback, EvidenceKind::ValueReadback => ProjectedEvidenceKind::ValueReadback, - EvidenceKind::WindowChange => ProjectedEvidenceKind::WindowChange, EvidenceKind::NativeApiResult | EvidenceKind::ScreenshotComparison | EvidenceKind::EventReceipt @@ -974,7 +1116,6 @@ fn evidence_kind_name(kind: EvidenceKind) -> &'static str { EvidenceKind::AccessibilityReadback => "accessibility_readback", EvidenceKind::BrowserReadback => "browser_readback", EvidenceKind::ValueReadback => "value_readback", - EvidenceKind::WindowChange => "window_change", EvidenceKind::NativeApiResult => "native_api_result", EvidenceKind::ScreenshotComparison => "screenshot_comparison", EvidenceKind::EventReceipt => "event_receipt", @@ -1099,6 +1240,7 @@ pub struct ActionOutcomeProjection { pub delivery: Option, pub evidence: Option>, pub escalation: Option, + pub surface_delta: Option, } /// Published delivery accounting; the original request is intentionally absent. @@ -1120,7 +1262,6 @@ pub enum ProjectedEvidenceKind { AccessibilityReadback, BrowserReadback, ValueReadback, - WindowChange, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -1212,6 +1353,65 @@ mod tests { .is_ok()); } + #[test] + fn surface_observation_preserves_actuator_effect() { + let mut record = ActionExecutionRecord::new( + ActionEffect::Unverifiable, + ActionTransport::MacosAxAction, + RequestedDelivery::Background, + ); + let target = ActionSurfaceTarget { + pid: 42, + window_id: 7, + app_name: "Editor".into(), + title: "Open".into(), + kind: ActionSurfaceKind::Sheet, + modal: true, + }; + let parent = ActionSurfaceTarget { + kind: ActionSurfaceKind::Window, + modal: false, + ..target.clone() + }; + record.observe_surface_delta( + resolve_surface_delta( + vec![ + ActionSurfaceCandidate { + target: parent, + focused: false, + }, + ActionSurfaceCandidate { + target, + focused: false, + }, + ], + false, + ) + .expect("surface delta"), + ); + + assert_eq!(record.effect, ActionEffect::Unverifiable); + let public = record.public_result().expect("public result"); + assert_eq!( + public.effect, + cua_driver_contract::ActionEffect::Unverifiable + ); + assert_eq!( + public + .window_change + .as_ref() + .map(|change| change.new_windows.len()), + Some(1), + "several AX roots for one native window are one rebind address" + ); + assert_eq!( + public.escalation.and_then(|escalation| escalation.window), + public + .window_change + .and_then(|change| change.new_windows.into_iter().next()) + ); + } + #[test] fn partial_requires_delivered_count() { assert_eq!( @@ -1364,7 +1564,11 @@ mod tests { .unwrap(); assert_eq!( result.escalation, - Some(cua_driver_contract::ActionEscalation { target, reason }) + Some(cua_driver_contract::ActionEscalation { + target, + reason, + window: None, + }) ); } @@ -1648,6 +1852,7 @@ mod tests { Some(cua_driver_contract::ActionEscalation { target: cua_driver_contract::ActionEscalationTarget::Page, reason: cua_driver_contract::ActionEscalationReason::RouteUnavailable, + window: None, }) ); diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/history.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/history.rs index a4e686afb8..3153f60ac6 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/history.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/history.rs @@ -2182,7 +2182,6 @@ fn evidence_name(value: ProjectedEvidenceKind) -> &'static str { ProjectedEvidenceKind::AccessibilityReadback => "accessibility_readback", ProjectedEvidenceKind::BrowserReadback => "browser_readback", ProjectedEvidenceKind::ValueReadback => "value_readback", - ProjectedEvidenceKind::WindowChange => "window_change", } } diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/protocol.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/protocol.rs index e572f48542..e36d57fb91 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/protocol.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/protocol.rs @@ -294,6 +294,9 @@ pub struct ToolResult { /// the MCP result envelope or its legacy structured payload. #[serde(skip)] pub action_record: Option, + /// Typed platform observation consumed by the shared dispatch seam. + #[serde(skip)] + pub surface_delta: Option, } impl ToolResult { diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs index c9d82805f0..94332d02ff 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs @@ -1546,6 +1546,13 @@ impl ToolRegistry { ); } } + if let Some(delta) = result.surface_delta.take() { + if let Some(record) = result.action_record.as_mut() { + record.observe_surface_delta(delta); + } else { + result.surface_delta = Some(delta); + } + } if resolved_name == "launch_app" && result.is_error != Some(true) { if let (Some(before), Some(pid)) = ( launch_snapshot.as_ref(), diff --git a/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs b/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs index edf11916f6..4ea6e62481 100644 --- a/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs +++ b/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs @@ -133,3 +133,149 @@ fn background_type_on_native_cocoa_is_ax_verified() { Observation::delivered(passed, Default::default()) }); } + +/// A background shortcut that opens TextEdit's AppKit file panel reports one +/// owner-verified rebind target while preserving the actuator's effect and the +/// user's foreground application. +#[test] +#[ignore] +fn background_open_panel_returns_a_typed_rebind() { + use cua_driver_testkit::e2e::{ + execute_case, recording_evidence, CaseSpec, Delivery, DriverRoute, Observation, OracleKind, + Scope, Targeting, + }; + use cua_driver_testkit::observer::TargetWindow; + use cua_driver_testkit::sentinel::run_with_background_oracles; + use cua_driver_testkit::{Driver, McpDriver}; + + let cell_id = "macos-textedit-open-panel-background-rebind"; + let case = CaseSpec::delivered( + cell_id, + "textedit", + "appkit", + "hotkey", + Targeting::Ax, + Delivery::Background, + Scope::Window, + DriverRoute::MacosCgEventPid, + vec![ + OracleKind::AxState, + OracleKind::Focus, + OracleKind::ZOrder, + OracleKind::Cursor, + OracleKind::NoLeakedInput, + ], + ); + execute_case(case, |evidence| { + let mut driver = McpDriver::spawn_macos_daemon_proxy_named(cell_id) + .expect("start installed macOS daemon proxy"); + *evidence = recording_evidence(driver.recording_dir()); + + let launch = driver.call( + "launch_app", + serde_json::json!({ "bundle_id": "com.apple.TextEdit" }), + ); + assert!( + !launch.is_error(), + "could not launch TextEdit: {}", + launch.text() + ); + let pid = launch.structured()["pid"].as_i64().expect("TextEdit pid"); + let window_id = launch.structured()["windows"] + .as_array() + .into_iter() + .flatten() + .filter_map(|window| window["window_id"].as_u64()) + .find(|window_id| { + let state = driver.call( + "get_window_state", + serde_json::json!({ + "pid": pid, + "window_id": window_id, + "include_screenshot": false + }), + ); + state.structured()["elements"] + .as_array() + .is_some_and(|elements| { + elements + .iter() + .any(|element| element["role"] == "AXTextArea") + }) + }) + .expect("TextEdit document window"); + + let (opened, mut passed) = run_with_background_oracles( + &mut driver, + TargetWindow { + pid: pid as u32, + native_id: window_id, + }, + |driver| { + driver.call( + "hotkey", + serde_json::json!({ + "pid": pid, + "window_id": window_id, + "keys": ["cmd", "o"], + "delivery_mode": "background" + }), + ) + }, + ) + .unwrap_or_else(|error| panic!("background Open-panel contract failed: {error}")); + assert!(!opened.is_error(), "hotkey errored: {}", opened.text()); + assert_eq!( + opened.action_effect(), + Some("unverifiable"), + "topology must not promote the hotkey effect: {}", + opened.text() + ); + + let change = &opened.structured()["window_change"]; + let candidates = change["new_windows"] + .as_array() + .expect("typed window_change candidates"); + assert_eq!(candidates.len(), 1, "expected one Open-panel root"); + let candidate = &candidates[0]; + let panel_pid = candidate["pid"].as_i64().expect("panel owner pid"); + let panel_window_id = candidate["window_id"].as_u64().expect("panel window id"); + assert_ne!( + panel_pid, pid, + "AppKit panel service ownership was not resolved" + ); + assert!(candidate["modal"].as_bool().unwrap_or(false)); + assert_eq!(opened.structured()["escalation"]["target"], "rebind"); + assert_eq!( + opened.structured()["escalation"]["window"], + *candidate, + "exact rebind must be one observed owner-verified candidate" + ); + + let panel = driver.call( + "get_window_state", + serde_json::json!({ + "pid": panel_pid, + "window_id": panel_window_id, + "include_screenshot": false + }), + ); + assert!( + !panel.is_error(), + "rebound panel was not addressable: {}", + panel.text() + ); + passed.push(OracleKind::AxState); + + let _ = driver.call( + "press_key", + serde_json::json!({ + "pid": panel_pid, + "window_id": panel_window_id, + "key": "escape", + "delivery_mode": "background" + }), + ); + Observation::delivered(passed, Default::default()) + }); +} 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 e71a9c8f34..7713397b29 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 @@ -946,6 +946,7 @@ impl Tool for GetWindowStateTool { is_error: None, structured_content: Some(structured), action_record: None, + surface_delta: None, } } Ok(Err(e)) => ToolResult::error(format!("Capture error: {e}")), @@ -6411,6 +6412,7 @@ impl Tool for GetDesktopStateTool { is_error: None, structured_content: Some(structured), action_record: None, + surface_delta: None, } } Ok(Err(e)) => ToolResult::error(format!("Capture error: {e}")), @@ -7240,6 +7242,7 @@ impl Tool for ZoomTool { "mime_type": "image/jpeg" })), action_record: None, + surface_delta: None, } } Ok(Err(e)) => ToolResult::error(format!("Zoom failed: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/ax/bindings.rs b/libs/cua-driver/rust/crates/platform-macos/src/ax/bindings.rs index f50f240905..7d8f976069 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/ax/bindings.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/ax/bindings.rs @@ -693,7 +693,16 @@ pub unsafe fn ax_get_window_id(element: AXUIElementRef) -> Option { /// /// `element` must be valid, and the caller must release every returned element. pub unsafe fn copy_ax_windows(element: AXUIElementRef) -> Vec { - let attr = CFStr::new("AXWindows"); + copy_element_array_attr(element, "AXWindows") +} + +/// Read an AX attribute containing an array of accessibility elements. +/// Every returned element is retained and must be released by the caller. +pub unsafe fn copy_element_array_attr( + element: AXUIElementRef, + attribute: &str, +) -> Vec { + let attr = CFStr::new(attribute); let mut value: CFTypeRef = std::ptr::null(); let err = AXUIElementCopyAttributeValue(element, attr.as_concrete_TypeRef(), &mut value); if err != kAXErrorSuccess || value.is_null() { diff --git a/libs/cua-driver/rust/crates/platform-macos/src/focus_guard.rs b/libs/cua-driver/rust/crates/platform-macos/src/focus_guard.rs index b94288ace6..468903facf 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/focus_guard.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/focus_guard.rs @@ -29,9 +29,9 @@ //! This Rust port only ships **layer 3** (the reactive suppressor). //! Layers 1+2 require AX assertion + AX attribute write/restore machinery //! that isn't yet ported — and empirically the layer-3 reactive guard -//! catches the majority of side-effects when combined with -//! `WindowChangeDetector`'s wildcard lease at the snapshot→detect -//! boundary. This gap is a known, intentional limitation. +//! catches the majority of side-effects when combined with the action +//! decorator's cross-application suppression lease. This gap is a known, +//! intentional limitation. //! //! ## Why this is a separate module //! @@ -56,12 +56,11 @@ use crate::focus_steal; /// Wrap an async closure `f` with a targeted focus-steal suppressor. /// /// - `target_pid` — the pid the action is dispatched to. `Some(pid)` is -/// the standard case; `None` skips the targeted entry entirely -/// (caller relies on the surrounding `WindowChangeDetector` wildcard -/// lease). +/// the standard case; `None` skips the targeted entry entirely because the +/// surrounding action decorator owns cross-application suppression. /// - `prior_frontmost` — the pid to restore focus to if the target /// activates. Typically captured from `apps::frontmost_pid()` before -/// the snapshot. +/// dispatch. /// - `origin` — short static label for tracing, e.g. `"click.AXPress"`. /// - `f` — the action to run with suppression armed. /// diff --git a/libs/cua-driver/rust/crates/platform-macos/src/lib.rs b/libs/cua-driver/rust/crates/platform-macos/src/lib.rs index c1a426c2a7..3049d1e624 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/lib.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/lib.rs @@ -31,6 +31,8 @@ pub mod permissions; #[cfg(target_os = "macos")] pub mod pip; #[cfg(target_os = "macos")] +mod post_action_observer; +#[cfg(target_os = "macos")] pub mod recording_hooks; #[cfg(target_os = "macos")] pub mod session; @@ -41,8 +43,6 @@ pub mod tools; #[cfg(target_os = "macos")] pub mod video_sckit; #[cfg(target_os = "macos")] -pub mod window_change_detector; -#[cfg(target_os = "macos")] pub mod windows; use cua_driver_core::tool::ToolRegistry; diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs new file mode 100644 index 0000000000..cfae0849ea --- /dev/null +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -0,0 +1,368 @@ +//! Target-scoped post-action accessibility-root observation. +//! +//! The decorator in this module is the only action topology producer on +//! macOS. It snapshots the addressed process, invokes the actuator, then +//! attaches one typed delta to `ToolResult`. Native notifications and global +//! window lists are not used as causality evidence. + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +use async_trait::async_trait; +use core_foundation::base::{CFRelease, CFTypeRef}; +use cua_driver_core::action_record::{ + resolve_surface_delta, ActionSurfaceCandidate, ActionSurfaceDelta, ActionSurfaceKind, + ActionSurfaceTarget, +}; +use cua_driver_core::protocol::ToolResult; +use cua_driver_core::tool::{ProtectedResourceOwnership, Tool, ToolDef}; +use serde_json::Value; + +use crate::ax::bindings::{ + ax_get_window_id, copy_ax_windows, copy_bool_attr, copy_element_array_attr, copy_string_attr, + AXUIElementCreateApplication, AXUIElementRef, AXUIElementSetMessagingTimeout, +}; + +const OBSERVATION_TIMEOUT: Duration = Duration::from_millis(1000); +const POLL_INTERVAL: Duration = Duration::from_millis(50); + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct RootKey { + window_id: Option, + parent_window_id: Option, + role: String, + subrole: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct Root { + key: RootKey, + target: Option, + focused: bool, +} + +type RootSnapshot = HashMap; + +pub struct ObservedActionTool { + inner: Box, +} + +impl ObservedActionTool { + pub fn new(inner: Box) -> Self { + Self { inner } + } +} + +#[async_trait] +impl Tool for ObservedActionTool { + fn def(&self) -> &ToolDef { + self.inner.def() + } + + async fn protected_resource_ownership( + &self, + adapter_id: &str, + args: &Value, + ) -> ProtectedResourceOwnership { + self.inner + .protected_resource_ownership(adapter_id, args) + .await + } + + async fn protected_resource_scope( + &self, + adapter_id: &str, + args: &Value, + ) -> Result, String> { + self.inner.protected_resource_scope(adapter_id, args).await + } + + async fn validate_protected_resource_scope( + &self, + adapter_id: &str, + args: &Value, + approved_scope: &Value, + ) -> Result<(), String> { + self.inner + .validate_protected_resource_scope(adapter_id, args, approved_scope) + .await + } + + async fn invoke(&self, args: Value) -> ToolResult { + let Some(pid) = args + .get("pid") + .and_then(Value::as_i64) + .and_then(|pid| i32::try_from(pid).ok()) + .filter(|pid| *pid > 0) + else { + return self.inner.invoke(args).await; + }; + let prior_front = crate::apps::frontmost_pid(); + let suppress_cross_app = args + .get("delivery_mode") + .and_then(Value::as_str) + .is_none_or(|mode| !mode.eq_ignore_ascii_case("foreground")); + let _suppression = prior_front + .filter(|_| suppress_cross_app) + .map(|restore_to| { + crate::focus_steal::begin_suppression_allowing( + pid, + restore_to, + "ObservedActionTool", + ) + }); + + let before = tokio::task::spawn_blocking(move || snapshot_roots(pid)) + .await + .unwrap_or_default(); + let mut result = self.inner.invoke(args).await; + let delta = tokio::task::spawn_blocking(move || observe_delta(pid, prior_front, before)) + .await + .ok() + .flatten(); + if let Some(delta) = delta { + result.surface_delta = Some(delta); + } + result + } +} + +fn observe_delta( + pid: i32, + prior_front: Option, + before: RootSnapshot, +) -> Option { + let deadline = Instant::now() + OBSERVATION_TIMEOUT; + loop { + let after = snapshot_roots(pid); + let appeared: Vec<&Root> = after + .iter() + .filter(|(key, _)| !before.contains_key(*key)) + .map(|(_, root)| root) + .collect(); + let foreground_changed = matches!( + (prior_front, crate::apps::frontmost_pid()), + (Some(before), Some(after)) if before != after + ); + if !appeared.is_empty() || foreground_changed { + let candidates = appeared + .into_iter() + .filter_map(|root| { + root.target.clone().map(|target| ActionSurfaceCandidate { + target, + focused: root.focused, + }) + }) + .collect(); + return resolve_surface_delta(candidates, foreground_changed); + } + if Instant::now() >= deadline { + return None; + } + std::thread::sleep(POLL_INTERVAL); + } +} + +fn snapshot_roots(pid: i32) -> RootSnapshot { + unsafe { + let app = AXUIElementCreateApplication(pid); + if app.is_null() { + return HashMap::new(); + } + AXUIElementSetMessagingTimeout(app, 0.25); + let app_name = crate::apps::get_app_name_for_pid(pid).unwrap_or_default(); + let window_server = crate::windows::all_windows(); + let windows = copy_ax_windows(app); + let mut roots = HashMap::new(); + for window in windows { + let parent_window_id = ax_get_window_id(window); + insert_root( + &mut roots, + window, + parent_window_id, + pid, + &app_name, + &window_server, + ); + for child in copy_element_array_attr(window, "AXChildren") { + let role = copy_string_attr(child, "AXRole").unwrap_or_default(); + if matches!(role.as_str(), "AXSheet" | "AXDialog" | "AXPopover") { + insert_root( + &mut roots, + child, + parent_window_id, + pid, + &app_name, + &window_server, + ); + } + CFRelease(child as CFTypeRef); + } + CFRelease(window as CFTypeRef); + } + CFRelease(app as CFTypeRef); + roots + } +} + +unsafe fn insert_root( + roots: &mut RootSnapshot, + element: AXUIElementRef, + parent_window_id: Option, + pid: i32, + app_name: &str, + window_server: &[crate::windows::WindowInfo], +) { + let role = copy_string_attr(element, "AXRole").unwrap_or_default(); + let subrole = copy_string_attr(element, "AXSubrole").unwrap_or_default(); + let title = copy_string_attr(element, "AXTitle").unwrap_or_default(); + let own_window_id = ax_get_window_id(element); + let effective_window_id = own_window_id.or(parent_window_id); + let modal = copy_bool_attr(element, "AXModal").unwrap_or(false) + || role == "AXSheet" + || role == "AXDialog" + || subrole.to_ascii_lowercase().contains("dialog") + || subrole.to_ascii_lowercase().contains("modal"); + let kind = if role == "AXSheet" { + ActionSurfaceKind::Sheet + } else if role == "AXDialog" || subrole.to_ascii_lowercase().contains("dialog") { + ActionSurfaceKind::Dialog + } else if role == "AXPopover" || subrole.to_ascii_lowercase().contains("popover") { + ActionSurfaceKind::Popover + } else { + ActionSurfaceKind::Window + }; + let key = RootKey { + window_id: own_window_id, + parent_window_id, + role, + subrole, + }; + let target = effective_window_id.and_then(|window_id| { + surface_owner(window_server, pid, window_id, app_name).map(|(owner_pid, owner_app_name)| { + ActionSurfaceTarget { + pid: i64::from(owner_pid), + window_id: u64::from(window_id), + app_name: owner_app_name, + title, + kind, + modal, + } + }) + }); + roots.insert( + key.clone(), + Root { + key, + target, + focused: copy_bool_attr(element, "AXFocused").unwrap_or(false), + }, + ); +} + +fn surface_owner( + windows: &[crate::windows::WindowInfo], + target_pid: i32, + window_id: u32, + target_app_name: &str, +) -> Option<(i32, String)> { + match crate::windows::resolve_window_owner_in(windows, target_pid, window_id) { + crate::windows::WindowOwner::SamePid => Some((target_pid, target_app_name.to_owned())), + crate::windows::WindowOwner::ForeignPid { + owner_pid, + owner_app_name, + } => Some((owner_pid, owner_app_name)), + crate::windows::WindowOwner::Unknown => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn root(window_id: u64, modal: bool, focused: bool) -> Root { + let target = ActionSurfaceTarget { + pid: 42, + window_id, + app_name: "Editor".into(), + title: "Dialog".into(), + kind: ActionSurfaceKind::Dialog, + modal, + }; + Root { + key: RootKey { + window_id: Some(window_id as u32), + parent_window_id: None, + role: "AXWindow".into(), + subrole: "AXDialog".into(), + }, + target: Some(target), + focused, + } + } + + #[test] + fn exact_rebind_requires_one_validated_candidate() { + let modal = root(7, true, false); + let delta = resolve_surface_delta( + vec![ActionSurfaceCandidate { + target: modal.target.clone().expect("target"), + focused: modal.focused, + }], + false, + ) + .expect("delta"); + assert_eq!( + delta.rebind.as_ref().map(|target| target.window_id), + Some(7) + ); + + let focused = root(8, false, true); + let delta = resolve_surface_delta( + vec![modal, focused] + .into_iter() + .map(|root| ActionSurfaceCandidate { + target: root.target.expect("target"), + focused: root.focused, + }) + .collect(), + false, + ) + .expect("delta"); + assert!(delta.rebind.is_none()); + } + + #[test] + fn verified_panel_service_owner_becomes_the_rebind_pid() { + let windows = vec![crate::windows::WindowInfo { + window_id: 7, + pid: 99, + app_name: "Open and Save Panel Service".into(), + title: "Open".into(), + bounds: crate::windows::WindowBounds { + x: 0.0, + y: 0.0, + width: 640.0, + height: 480.0, + }, + layer: 0, + z_index: 1, + is_on_screen: true, + current_space_id: None, + on_current_space: None, + space_ids: None, + }]; + assert_eq!( + surface_owner(&windows, 42, 7, "TextEdit"), + Some((99, "Open and Save Panel Service".into())) + ); + assert_eq!(surface_owner(&[], 42, 7, "TextEdit"), None); + } + + #[test] + fn unrelated_foreground_change_has_no_exact_target() { + let delta = resolve_surface_delta(Vec::new(), true).expect("delta"); + assert!(delta.new_windows.is_empty()); + assert!(delta.rebind.is_none()); + } +} diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs index 26b45ef591..ba1ac1806b 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/click.rs @@ -29,7 +29,6 @@ use crate::ax::bindings::{ element_screen_rect, kAXErrorSuccess, AXUIElementPerformAction, AXUIElementRef, }; use crate::focus_guard; -use crate::window_change_detector::WindowChangeDetector; use core_foundation::base::{CFRelease, TCFType}; use super::ToolState; @@ -601,19 +600,8 @@ impl Tool for ClickTool { selection_pixel = None; } - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // Capture prior frontmost, arm the wildcard suppressor in the - // snapshot, then arm a targeted suppressor across the AX action - // itself via FocusGuard. After the action returns, detect any - // new-window / foreground side-effects and append a one-liner - // suffix matching Swift's wording. let prior_front = apps::frontmost_pid(); let foreground = delivery_mode.is_foreground(); - let snapshot = if foreground { - WindowChangeDetector::snapshot_without_suppression(prior_front) - } else { - WindowChangeDetector::snapshot(prior_front) - }; // Run AX work on a blocking thread (can't block async executor). // Use `effective_action` so button=right rewrites press → show_menu. @@ -686,13 +674,10 @@ impl Tool for ClickTool { ) .await; - // Drop the wildcard lease + detect window/foreground side-effects. - let changes = super::finish_window_observation(snapshot, &args).await; - match result { Ok(Ok(( ( - mut msg, + msg, needs_webkit_delay, suspected_noop, selection_verified, @@ -705,7 +690,6 @@ impl Tool for ClickTool { if needs_webkit_delay { tokio::time::sleep(std::time::Duration::from_millis(800)).await; } - msg.push_str(&changes.result_suffix()); // AX dispatch went through, but AXPerformAction returning // success does not confirm the on-screen effect (many elements // no-op silently). A click is never driver-verifiable (no @@ -975,22 +959,7 @@ impl Tool for ClickTool { .cursor_registry .update_position(&cursor_key, screen_x, screen_y); - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // A pixel click can land on a "Sign In" button that opens a sheet - // or a Safari link that activates a new tab — same side-effect - // shape as the AX path, so we wrap identically. let prior_front = apps::frontmost_pid(); - let snapshot = match activation_policy { - PixelActivationPolicy::SuppressTarget => { - WindowChangeDetector::snapshot(prior_front) - } - PixelActivationPolicy::AllowTargetWithoutRaise => { - WindowChangeDetector::snapshot_allowing_activation(prior_front, pid) - } - PixelActivationPolicy::ForegroundAssist => { - WindowChangeDetector::snapshot_without_suppression(prior_front) - } - }; // Restore the Swift background-click prologue that was left // disconnected in the original Rust port. It makes an opaque @@ -1151,8 +1120,6 @@ impl Tool for ClickTool { } } - let changes = super::finish_window_observation(snapshot, &args).await; - let button_label = match button_str.as_str() { "right" => "right-click", "middle" => "middle-click", @@ -1170,8 +1137,7 @@ impl Tool for ClickTool { }; ToolResult::text(format!( "✅ Posted {button_label} to pid {pid} ({mode_label}; \ - not driver-verified — confirm via screenshot).{}", - changes.result_suffix() + not driver-verified — confirm via screenshot)." )) .with_structured(serde_json::json!({ "path": path, diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs index f5eb9d0395..1d7d0db405 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/drag.rs @@ -21,7 +21,6 @@ use super::ToolState; use crate::apps; use crate::focus_guard; use crate::input::mouse::DragButton; -use crate::window_change_detector::WindowChangeDetector; pub struct DragTool { pub state: Arc, @@ -287,13 +286,7 @@ impl Tool for DragTool { } crate::cursor::overlay::animate_cursor_to(cursor_key.clone(), from_sx, from_sy).await; - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // Drags can trigger drag-and-drop side-effects that spawn helper - // windows (drop on Dock, drop on background app icon) and the - // mouseDown half-event alone can activate the target app on some - // Chromium builds. Wrap to catch + report both. let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); // Dispatch blocking drag synthesis. let mods_owned = modifiers.clone(); @@ -395,8 +388,6 @@ impl Tool for DragTool { .update_position(&cursor_key, to_sx, to_sy); } - let changes = super::finish_window_observation(snapshot, &args).await; - if let Some(wid) = window_id { crate::cursor::overlay::send_command( cursor_key.clone(), @@ -426,12 +417,11 @@ impl Tool for DragTool { from window-pixel ({}, {}) → ({}, {}), \ screen ({}, {}) → ({}, {}) \ in {duration_ms}ms / {steps} steps{mode_label} \ - (background CGEvent; not driver-verified — confirm via screenshot).{}", + (background CGEvent; not driver-verified — confirm via screenshot).", from_x as i64, from_y as i64, to_x as i64, to_y as i64, from_sx as i64, from_sy as i64, to_sx as i64, to_sy as i64, - changes.result_suffix(), )) .with_structured(serde_json::json!({ "path": if fg { "cgevent_fg" } else { "cgevent" }, "verified": false, "effect": "unverifiable" diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs index 652a372ade..126b735ad6 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs @@ -128,6 +128,7 @@ impl Tool for GetDesktopStateTool { is_error: None, structured_content: Some(structured), action_record: None, + surface_delta: None, } } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs index 378023c169..19cc3696f4 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs @@ -652,6 +652,7 @@ impl Tool for GetWindowStateTool { is_error: None, structured_content: Some(structured), action_record: None, + surface_delta: None, } } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs index 699a8e8f12..2a7a7da1a3 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/hotkey.rs @@ -11,7 +11,6 @@ use std::sync::Arc; use crate::apps; use crate::focus_guard; -use crate::window_change_detector::WindowChangeDetector; use super::ToolState; @@ -391,14 +390,7 @@ impl Tool for HotkeyTool { } }; - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // Hotkeys like Cmd+N, Cmd+W, Cmd+T explicitly open/close - // windows. The NSMenu path also briefly activates the target via - // SLPSSetFrontProcessWithOptions which can race the wildcard - // suppressor — wrapping ensures both side-effects are observed - // and the prior frontmost is restored if the activation lingers. let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); let result = focus_guard::with_focus_suppressed( Some(pid), @@ -479,8 +471,6 @@ impl Tool for HotkeyTool { ) .await; - let changes = super::finish_window_observation(snapshot, &args).await; - match result { Ok(Ok(())) => { let label = if fg { @@ -505,11 +495,8 @@ impl Tool for HotkeyTool { pixel-click to focus then type_text instead.)" }); } - ToolResult::text(format!( - "Pressed {key_display} on pid {pid}{label}.{}", - changes.result_suffix() - )) - .with_structured(structured) + ToolResult::text(format!("Pressed {key_display} on pid {pid}{label}.")) + .with_structured(structured) } Ok(Err(e)) => ToolResult::error(format!("hotkey failed: {e}")), Err(e) => ToolResult::error(format!("Task error: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/mod.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/mod.rs index 68287e0c93..df457b524d 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/mod.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/mod.rs @@ -111,6 +111,12 @@ mod pid_window_target_tests { #[cfg(test)] mod background_input_regression_tests; +fn observed_action(tool: T) -> Box { + Box::new(crate::post_action_observer::ObservedActionTool::new( + Box::new(tool), + )) +} + fn pid_window_guarded( tool: T, candidates: &WindowTargetCandidates, @@ -121,6 +127,16 @@ fn pid_window_guarded( )) } +fn observed_pid_window_action( + tool: T, + candidates: &WindowTargetCandidates, +) -> Box { + Box::new(PidOnlyWindowTargetGuard::new( + observed_action(tool), + candidates.clone(), + )) +} + pub use check_permissions::{ request_from_launchservices_host as request_permissions_from_launchservices_host, PERMISSIONS_HOST_REQUEST_ARG, @@ -281,42 +297,6 @@ pub(crate) async fn acquire_background_mutation(pid: i32) -> BackgroundMutationL } } -/// Finish the post-action observation window. Embedded interactive clients -/// that already observe the target continuously may opt out through the -/// private registry argument to avoid adding a one-second acknowledgement -/// delay to every input event. Regular MCP callers retain the full observer. -pub(crate) async fn finish_window_observation( - snapshot: crate::window_change_detector::Snapshot, - args: &serde_json::Value, -) -> crate::window_change_detector::Changes { - if args - .get("_skip_window_change_detection") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false) - { - drop(snapshot); - crate::window_change_detector::Changes::no_change() - } else { - snapshot.detect_async().await - } -} - -#[cfg(test)] -mod interactive_observation_tests { - use super::*; - - #[tokio::test] - async fn embedded_interactive_input_can_finish_without_polling() { - let snapshot = crate::window_change_detector::WindowChangeDetector::snapshot(None); - let changes = finish_window_observation( - snapshot, - &serde_json::json!({"_skip_window_change_detection": true}), - ) - .await; - assert!(!changes.needs_restore()); - } -} - /// px-focus for the keyboard family (type_text / press_key / hotkey): focus the /// element at (x,y) before a keystroke — the *element px action* form of a /// keyboard tool. Prefer non-destructive AX focus so an existing selection is @@ -907,40 +887,40 @@ pub fn register_all( &pid_window_candidates, )); registry.register(Box::new(set_window_frame::SetWindowFrameTool)); - registry.register(Box::new(invoke_menu::InvokeMenuTool)); - registry.register(pid_window_guarded( + registry.register(observed_action(invoke_menu::InvokeMenuTool)); + registry.register(observed_pid_window_action( click::ClickTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( double_click::DoubleClickTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( right_click::RightClickTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( drag::DragTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( type_text::TypeTextTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( press_key::PressKeyTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( hotkey::HotkeyTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( set_value::SetValueTool::new(state.clone()), &pid_window_candidates, )); - registry.register(pid_window_guarded( + registry.register(observed_pid_window_action( scroll::ScrollTool::new(state.clone()), &pid_window_candidates, )); diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/press_key.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/press_key.rs index 8378796e41..fcf0199a13 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/press_key.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/press_key.rs @@ -19,7 +19,6 @@ use crate::ax::bindings::{ copy_bool_attr, copy_string_attr, focused_element_of_pid, AXUIElementRef, }; use crate::focus_guard; -use crate::window_change_detector::WindowChangeDetector; use super::ToolState; @@ -403,16 +402,7 @@ impl Tool for PressKeyTool { } }; - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // Single-key presses can fire autocomplete (Return on a search - // box opens a results popover) or trigger menu shortcuts that - // open windows. Wrapping mirrors the hotkey path. - // - // The AX focus_element() pre-write also runs inside the closure - // so any reflex activations it triggers are caught by both the - // wildcard snapshot suppressor and the targeted FocusGuard lease. let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); let result = focus_guard::with_focus_suppressed( Some(pid), @@ -471,8 +461,6 @@ impl Tool for PressKeyTool { ) .await; - let changes = super::finish_window_observation(snapshot, &args).await; - let delivery_outcome = match result { Ok(result) => map_delivery_outcome(result), Err(error) => { @@ -494,12 +482,9 @@ impl Tool for PressKeyTool { "verified": confirmed, "effect": if confirmed { "confirmed" } else { "unverifiable" }, }); - ToolResult::text(format!( - "✅ Pressed {display_key} on pid {pid}{label}.{}", - changes.result_suffix() - )) - .with_structured(structured) - .with_action_record(action_record(confirmed, fg)) + ToolResult::text(format!("✅ Pressed {display_key} on pid {pid}{label}.")) + .with_structured(structured) + .with_action_record(action_record(confirmed, fg)) } PressKeyDeliveryOutcome::Failed(error) => delivery_failed(error), } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs index 3ac5b1477c..290dd71534 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/scroll.rs @@ -15,7 +15,6 @@ use crate::ax::bindings::{ perform_action, AXUIElementRef, }; use crate::focus_guard; -use crate::window_change_detector::WindowChangeDetector; use super::ToolState; @@ -544,7 +543,6 @@ impl Tool for ScrollTool { ); let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); let WheelTarget { screen_x, @@ -590,7 +588,6 @@ impl Tool for ScrollTool { ) .await; - let changes = super::finish_window_observation(snapshot, &args).await; let mode_label = if fg { " (delivery_mode:foreground)" } else { @@ -600,8 +597,7 @@ impl Tool for ScrollTool { Ok(Ok(())) => ToolResult::text(format!( "✅ Sent {direction} scroll by {by} × {amount} via pixel wheel at \ ({screen_x:.0}, {screen_y:.0}){mode_label} (background CGEvent; not \ - driver-verified — confirm via screenshot).{}", - changes.result_suffix() + driver-verified — confirm via screenshot)." )) .with_structured(serde_json::json!({ "path": if fg { "cgevent_fg" } else { "cgevent" }, "verified": false, "effect": "unverifiable" @@ -651,16 +647,7 @@ impl Tool for ScrollTool { } } - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // Scroll keystrokes (PageDown / arrow) into search-box autocomplete - // can spawn floating helper windows; rare but real. Wrap for parity - // with the other action tools. - // - // The AX focus_element() pre-write also runs inside the closure so - // any reflex activations it triggers are caught by both the wildcard - // snapshot suppressor and the targeted FocusGuard lease. let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); let result = focus_guard::with_focus_suppressed( Some(pid), @@ -689,13 +676,10 @@ impl Tool for ScrollTool { ) .await; - let changes = super::finish_window_observation(snapshot, &args).await; - match result { Ok(Ok(())) => ToolResult::text(format!( "✅ Sent {direction} scroll by {by} × {amount} via keystroke \ - (background; not driver-verified — confirm via screenshot).{}", - changes.result_suffix() + (background; not driver-verified — confirm via screenshot)." )) .with_structured(serde_json::json!({ "path": "key_events", "verified": false })), Ok(Err(e)) => ToolResult::error(format!("Scroll failed: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs index 449f6306fe..8413063f82 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/set_value.rs @@ -25,7 +25,6 @@ use crate::ax::bindings::{ set_number_attr, set_string_attr, AXUIElementRef, }; use crate::focus_guard; -use crate::window_change_detector::WindowChangeDetector; use core_foundation::base::CFRelease; use super::ToolState; @@ -204,12 +203,7 @@ impl Tool for SetValueTool { Some(window_id), ); - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // AXValue writes on popups / sliders can cause reflex activations - // in Chromium-based apps; the AXPopUpButton path also AXPresses a - // child option which can trigger app activation in some setups. let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); let result = focus_guard::with_focus_suppressed( Some(pid), @@ -224,14 +218,11 @@ impl Tool for SetValueTool { ) .await; - let changes = snapshot.detect_async().await; - match result { Ok(Ok(mut outcome)) => { apply_surface_trust(&mut outcome, ax_echo_surface); apply_verification_label(&mut outcome); - let mut msg = outcome.detail; - msg.push_str(&changes.result_suffix()); + let msg = outcome.detail; let verified = outcome.verified.unwrap_or(false); let mut structured = serde_json::json!({ "path": "ax", diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs index 49f8eca442..21b81c5c19 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/type_text.rs @@ -36,7 +36,6 @@ use crate::ax::bindings::{ copy_string_attr, focused_element_of_pid, kAXErrorSuccess, set_string_attr, AXUIElementRef, }; use crate::focus_guard; -use crate::window_change_detector::WindowChangeDetector; use core_foundation::base::CFRelease; use cua_driver_core::background_input::BackgroundRefusal; @@ -356,13 +355,7 @@ impl Tool for TypeTextTool { let text_clone = text.clone(); let char_count = text.chars().count(); - // ── Focus-suppression wrap (Swift WindowChangeDetector + FocusGuard) ── - // Typing into a field can trigger autocomplete popovers or - // Chrome/Safari's "Save Password?" prompt, both of which open - // helper windows. Wrap so callers see them in the result suffix - // and the wildcard suppressor catches reflex activations. let prior_front = apps::frontmost_pid(); - let snapshot = WindowChangeDetector::snapshot(prior_front); // Terminal-emulator short-circuit: when the target pid belongs // to a known terminal (Ghostty / Terminal.app / iTerm2 / …), the @@ -394,8 +387,6 @@ impl Tool for TypeTextTool { ) .await; - let changes = super::finish_window_observation(snapshot, &args).await; - // Unwrap the delivery envelope: a structured refusal means no // actuator ran and the caller gets the exact reason. let result = match result { @@ -492,33 +483,32 @@ impl Tool for TypeTextTool { .to_string(), ) }; - ToolResult::text(format!( - "{mark} {char_count} char(s){detail}.{note}{}", - changes.result_suffix() - )) - .with_structured({ - // `effect` mirrors `verified`'s read-back tri-state: a TRUSTED - // positive read-back is "confirmed"; an unreadable/unchanged - // AXValue, a dropped CGEvent rung, or an Electron AX echo we - // refuse to trust is "unverifiable". - let mut s = serde_json::json!({ - "path": path, - "characters": char_count, - "requested_chars": char_count, - "verified": verified, - "effect": if verified { "confirmed" } else { "unverifiable" }, - }); - if let Some(delivered_chars) = delivered_chars { - s["delivered_chars"] = serde_json::json!(delivered_chars); - } - if untrusted_web_readback { - // Web-content AXValue read-back. A real browser TAB → the - // `page` tool (drives the DOM via CDP) is the reliable rung; - // an embedded web view (Electron, no CDP) → the element px - // action. It's a renderer/DOM-focus problem, never a - // foreground one. - let escalation = - match web_readback_next_rung(electron_web_content, used_pixel_focus) { + ToolResult::text(format!("{mark} {char_count} char(s){detail}.{note}")) + .with_structured({ + // `effect` mirrors `verified`'s read-back tri-state: a TRUSTED + // positive read-back is "confirmed"; an unreadable/unchanged + // AXValue, a dropped CGEvent rung, or an Electron AX echo we + // refuse to trust is "unverifiable". + let mut s = serde_json::json!({ + "path": path, + "characters": char_count, + "requested_chars": char_count, + "verified": verified, + "effect": if verified { "confirmed" } else { "unverifiable" }, + }); + if let Some(delivered_chars) = delivered_chars { + s["delivered_chars"] = serde_json::json!(delivered_chars); + } + if untrusted_web_readback { + // Web-content AXValue read-back. A real browser TAB → the + // `page` tool (drives the DOM via CDP) is the reliable rung; + // an embedded web view (Electron, no CDP) → the element px + // action. It's a renderer/DOM-focus problem, never a + // foreground one. + let escalation = match web_readback_next_rung( + electron_web_content, + used_pixel_focus, + ) { Some("px") => Some(( "px", "Electron web view — AXValue read-back cannot prove \ @@ -542,22 +532,22 @@ impl Tool for TypeTextTool { )), _ => None, }; - if let Some((recommended, reason)) = escalation { + if let Some((recommended, reason)) = escalation { + s["escalation"] = serde_json::json!({ + "recommended": recommended, + "reason": reason, + }); + } + } else if !verified && path != PATH_KEY_EVENTS_FG { s["escalation"] = serde_json::json!({ - "recommended": recommended, - "reason": reason, + "recommended": "foreground", + "reason": "background insert could not be confirmed — \ + re-call with delivery_mode:\"foreground\" if a \ + screenshot shows the text didn't land." }); } - } else if !verified && path != PATH_KEY_EVENTS_FG { - s["escalation"] = serde_json::json!({ - "recommended": "foreground", - "reason": "background insert could not be confirmed — \ - re-call with delivery_mode:\"foreground\" if a \ - screenshot shows the text didn't land." - }); - } - s - }) + s + }) } Ok(Err(e)) => ToolResult::error(format!("type_text failed: {e}")), Err(e) => ToolResult::error(format!("Task error: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs index f389448f9a..1e22377bb0 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs @@ -117,6 +117,7 @@ impl Tool for ZoomTool { "mime_type": "image/jpeg" })), action_record: None, + surface_delta: None, } } Ok(Err(e)) => ToolResult::error(format!("Zoom failed: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs b/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs deleted file mode 100644 index 3286ac6230..0000000000 --- a/libs/cua-driver/rust/crates/platform-macos/src/window_change_detector.rs +++ /dev/null @@ -1,530 +0,0 @@ -//! Window-change detector — Rust port of Swift's -//! `WindowChangeDetector` (`libs/cua-driver/Sources/CuaDriverServer/Tools/WindowChangeDetector.swift`). -//! -//! ## What this does -//! -//! Action tools (click, type_text, hotkey, …) on a backgrounded app can -//! trigger window/foreground side-effects: a "Sign In" button opens a -//! modal sheet, a Safari link spawns a new tab, an autocomplete dropdown -//! pops a helper window. The Rust port mirrors Swift's -//! snapshot → action → detect cycle so tool results can: -//! -//! 1. Surface the side-effect to the agent (one-line suffix on the -//! tool result, matching Swift verbatim). -//! 2. Arm a **wildcard** focus-steal suppression entry that covers the -//! full snapshot→detect window. Wildcards (`target_pid = None`) -//! catch any activation other than the prior frontmost — so even an -//! app we didn't know about (Safari activating because a UTM Gallery -//! link routed to it) is suppressed before the first compositor -//! frame. -//! -//! ## Usage -//! -//! ```ignore -//! // Callers capture frontmost BEFORE the snapshot so the wildcard -//! // suppressor and the snapshot's recorded frontmost agree on the -//! // pid to restore to — avoids a race where another app activates -//! // between the caller's `frontmost_pid()` and the detector's own. -//! let prior_front = apps::frontmost_pid(); -//! let snapshot = WindowChangeDetector::snapshot(prior_front); -//! // … perform action … -//! let changes = snapshot.detect(); -//! // changes.result_suffix() — append to ToolResult text. -//! ``` -//! -//! Dropping the `Snapshot` ends the suppression lease (RAII). `detect()` -//! also drops the lease before returning — the lease's `Drop` is -//! idempotent so explicit-detect + later-drop is safe. - -use std::collections::HashSet; -use std::time::{Duration, Instant}; - -use crate::apps; -use crate::focus_steal::{self, SuppressionLease}; -use crate::windows::{self, WindowInfo}; - -/// One window that appeared between `snapshot()` and `detect()`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WindowEvent { - pub window_id: u32, - pub pid: i32, - pub app_name: String, - pub title: String, -} - -/// Categorical diff entry. We mirror Swift which only emits -/// `WindowEvent` rows for *new* windows — closed/changed never appear -/// in the result suffix — but keep them as enum variants for future -/// extensibility and so unit tests can pin down the diff semantics. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum WindowChange { - Opened(WindowEvent), - Closed { window_id: u32 }, -} - -/// State captured immediately before the action fires. -/// -/// Holds: -/// - `window_ids` — the set of visible layer-0 window IDs at snapshot -/// time. `detect()` diffs against this. -/// - `front_pid` — the OS frontmost pid at snapshot time. `detect()` -/// reports whether a *different* pid became frontmost. The wildcard -/// suppressor in `focus_steal` will normally restore the original -/// front before `detect()`'s poll loop observes the change, so this -/// field is best-effort. -/// - `_lease` — the wildcard suppression lease. Dropping the snapshot -/// ends suppression. Held inside `Option` so `detect()` can take it -/// and drop early. -pub struct Snapshot { - window_ids: HashSet, - front_pid: Option, - _lease: Option, -} - -/// Result of `detect()` — what changed during the action window. -#[derive(Debug, Clone)] -pub struct Changes { - pub new_windows: Vec, - pub foreground_changed: bool, -} - -impl Changes { - pub fn no_change() -> Self { - Self { - new_windows: Vec::new(), - foreground_changed: false, - } - } - - /// True when we found evidence that the action triggered a cross-app - /// side-effect that required (or would have required) a foreground - /// restore. Matches Swift's `Changes.needsRestore`. - pub fn needs_restore(&self) -> bool { - self.foreground_changed || !self.new_windows.is_empty() - } - - /// One-liner summary to append to a tool result, or empty string - /// when nothing interesting happened. - /// - /// Format mirrors Swift `WindowChangeDetector.Changes.resultSuffix` - /// **verbatim** so MCP callers that key off the suffix wording - /// don't need a per-binary special case. - pub fn result_suffix(&self) -> String { - if !self.needs_restore() { - return String::new(); - } - - if !self.new_windows.is_empty() { - // Group by app name (stable order), join titles per app. - let mut by_app: std::collections::BTreeMap<&str, Vec<&str>> = - std::collections::BTreeMap::new(); - for w in &self.new_windows { - by_app.entry(&w.app_name).or_default().push(&w.title); - } - let summaries: Vec = by_app - .into_iter() - .map(|(app, titles)| { - let titles: Vec = titles - .into_iter() - .filter(|t| !t.is_empty()) - .map(|t| format!("\"{t}\"")) - .collect(); - if titles.is_empty() { - app.to_string() - } else { - format!("{app} ({})", titles.join(", ")) - } - }) - .collect(); - format!( - "\n\n🪟 Action opened new window(s): {}.", - summaries.join("; ") - ) - } else { - "\n\n🔀 Action caused a different app to become frontmost.".to_string() - } - } -} - -/// Default poll deadline — new windows triggered by a click typically -/// appear within ~200ms on macOS; 1.0s gives the wildcard suppressor -/// time to fire and settle. -const DEFAULT_TIMEOUT: Duration = Duration::from_millis(1000); - -/// Default inter-poll interval. Matches Swift's 50ms. -const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(50); - -/// Public API. Mirrors Swift `enum WindowChangeDetector` — no state of -/// its own; all state lives inside the returned `Snapshot`. -pub struct WindowChangeDetector; - -impl WindowChangeDetector { - /// Capture the current window set + frontmost pid and arm the - /// wildcard focus-steal suppressor. Call immediately before - /// dispatching the action. - /// - /// `prior_front` is the frontmost pid the **caller** already - /// observed — typically captured one line earlier via - /// `apps::frontmost_pid()` for the surrounding `focus_guard` - /// lease. We use the caller's value (not a fresh re-read) so the - /// wildcard suppressor's `restore_to` matches what the focus-guard - /// lease saw; a race where another app became frontmost between - /// the caller's read and this method would otherwise leave the - /// two leases targeting different pids. - /// - /// Returns `Snapshot`. Drop ends suppression (via the held - /// `SuppressionLease`); call `Snapshot::detect()` to consume the - /// snapshot and get a `Changes` summary. - /// - /// Safe to call from any thread — `CGWindowListCopyWindowInfo` is - /// documented as thread-safe. - pub fn snapshot(prior_front: Option) -> Snapshot { - Self::capture(prior_front, true, None) - } - - /// Capture the same before-state without arming reactive focus suppression. - /// Foreground delivery owns its temporary activation and restoration, so a - /// wildcard lease would race the target while the action is settling. - pub fn snapshot_without_suppression(prior_front: Option) -> Snapshot { - Self::capture(prior_front, false, None) - } - - /// Capture the before-state and suppress cross-app activations while - /// allowing one intentional target activation. - /// - /// The raw background pixel-click path needs this middle ground: - /// focus-without-raise makes `allowed_pid` AppKit-active so its event queue - /// accepts the click, but a link or hand-off that activates a different app - /// must still restore the user's original foreground. - pub fn snapshot_allowing_activation(prior_front: Option, allowed_pid: i32) -> Snapshot { - Self::capture(prior_front, true, Some(allowed_pid)) - } - - fn capture( - prior_front: Option, - suppress_focus: bool, - allowed_pid: Option, - ) -> Snapshot { - let window_ids: HashSet = windows::visible_windows() - .into_iter() - .filter(|w| w.layer == 0) - .map(|w| w.window_id) - .collect(); - - // Arm wildcard suppression — covers snapshot → detect window. - // restore_to = caller-captured frontmost; target = wildcard - // (any other pid). If there's no frontmost (rare — screensaver, - // login window), we skip the lease; foreground-change tracking - // still runs. - let lease = prior_front - .filter(|_| suppress_focus) - .map(|restore_to| match allowed_pid { - Some(pid) => focus_steal::begin_suppression_allowing( - pid, - restore_to, - "WindowChangeDetector.snapshot_allowing_activation", - ), - None => focus_steal::begin_suppression( - None, // wildcard - restore_to, - "WindowChangeDetector.snapshot", - ), - }); - - Snapshot { - window_ids, - front_pid: prior_front, - _lease: lease, - } - } -} - -impl Snapshot { - /// Frontmost pid at snapshot time, if any. - pub fn front_pid(&self) -> Option { - self.front_pid - } - - /// Poll for up to `DEFAULT_TIMEOUT` for new windows or a - /// foreground-app change. Returns as soon as a change is detected - /// or the timeout elapses. - /// - /// Consumes the snapshot — the wildcard suppression lease is - /// dropped when this returns (covers the full action + detection - /// window). - pub fn detect(self) -> Changes { - self.detect_with(DEFAULT_TIMEOUT, DEFAULT_POLL_INTERVAL) - } - - /// Async wrapper around `detect()` — runs the synchronous poll - /// loop on a `spawn_blocking` thread so it doesn't stall the - /// tokio runtime. Most action-tool call sites should prefer this - /// over the blocking `detect()`. - pub async fn detect_async(self) -> Changes { - // Move the Snapshot (and its embedded lease) onto the blocking - // thread; the lease's Drop runs there when detect_with returns. - tokio::task::spawn_blocking(move || self.detect()) - .await - .unwrap_or_else(|_| Changes::no_change()) - } - - /// Same as `detect()` but with configurable timing — exposed for - /// tests / callers that want a tighter or looser poll window. - pub fn detect_with(self, timeout: Duration, poll_interval: Duration) -> Changes { - let deadline = Instant::now() + timeout; - loop { - std::thread::sleep(poll_interval); - - let current: Vec = windows::visible_windows() - .into_iter() - .filter(|w| w.layer == 0) - .collect(); - let current_ids: HashSet = current.iter().map(|w| w.window_id).collect(); - - let new_windows: Vec = current - .iter() - .filter(|w| !self.window_ids.contains(&w.window_id)) - .map(|w| WindowEvent { - window_id: w.window_id, - pid: w.pid, - app_name: w.app_name.clone(), - title: w.title.clone(), - }) - .collect(); - // Diff the other direction too — keeps unit tests honest - // even though Swift's result_suffix only uses opened windows. - let _closed: Vec = self - .window_ids - .iter() - .copied() - .filter(|id| !current_ids.contains(id)) - .collect(); - - let current_front = apps::frontmost_pid(); - let foreground_changed = match (self.front_pid, current_front) { - (Some(orig), Some(cur)) => orig != cur, - _ => false, - }; - - if !new_windows.is_empty() || foreground_changed { - return Changes { - new_windows, - foreground_changed, - }; - } - if Instant::now() >= deadline { - return Changes::no_change(); - } - } - } - - // ── Internal helpers — also used by unit tests via the `pub(super)` - // path so the diff logic can be exercised without driving the live - // window enumerator. ──────────────────────────────────────────── - - /// Pure-function diff: given the snapshot's window-id set + a - /// list of currently-visible windows, return the (opened, closed) - /// classification. - /// - /// `#[allow(dead_code)]`: today only the `#[cfg(test)]` block below - /// constructs this — production callers `wait_for_window_change` / - /// `wait_for_window_close` keep the (opened, closed) split inline. - /// Kept `pub(crate)` because the doc comment near the top of this - /// `impl` block calls it out as the entry point for unit-testing the - /// diff logic without driving the live window enumerator. - #[allow(dead_code)] - pub(crate) fn diff( - snapshot_ids: &HashSet, - current: &[WindowInfo], - ) -> (Vec, Vec) { - let current_ids: HashSet = current.iter().map(|w| w.window_id).collect(); - let opened: Vec = current - .iter() - .filter(|w| !snapshot_ids.contains(&w.window_id)) - .map(|w| WindowEvent { - window_id: w.window_id, - pid: w.pid, - app_name: w.app_name.clone(), - title: w.title.clone(), - }) - .collect(); - let closed: Vec = snapshot_ids - .iter() - .copied() - .filter(|id| !current_ids.contains(id)) - .collect(); - (opened, closed) - } -} - -// ── Tests ──────────────────────────────────────────────────────────────────── - -#[cfg(test)] -mod tests { - use super::*; - use crate::windows::WindowBounds; - - fn win(id: u32, pid: i32, app: &str, title: &str) -> WindowInfo { - WindowInfo { - window_id: id, - pid, - app_name: app.into(), - title: title.into(), - bounds: WindowBounds { - x: 0., - y: 0., - width: 100., - height: 100., - }, - layer: 0, - z_index: 0, - is_on_screen: true, - current_space_id: None, - on_current_space: None, - space_ids: None, - } - } - - #[test] - fn diff_finds_opened_window() { - let snap: HashSet = [1, 2].into_iter().collect(); - let cur = vec![ - win(1, 100, "Safari", "Home"), - win(2, 100, "Safari", "Tab2"), - win(3, 101, "Mail", "Inbox"), - ]; - let (opened, closed) = Snapshot::diff(&snap, &cur); - assert_eq!(opened.len(), 1); - assert_eq!(opened[0].window_id, 3); - assert_eq!(opened[0].app_name, "Mail"); - assert_eq!(opened[0].title, "Inbox"); - assert!(closed.is_empty()); - } - - #[test] - fn diff_finds_closed_window() { - let snap: HashSet = [1, 2, 3].into_iter().collect(); - let cur = vec![win(1, 100, "Safari", "Home")]; - let (opened, closed) = Snapshot::diff(&snap, &cur); - assert!(opened.is_empty()); - assert_eq!(closed.len(), 2); - let closed_set: HashSet = closed.into_iter().collect(); - assert!(closed_set.contains(&2)); - assert!(closed_set.contains(&3)); - } - - #[test] - fn diff_no_change() { - let snap: HashSet = [1, 2].into_iter().collect(); - let cur = vec![win(1, 100, "Safari", "A"), win(2, 100, "Safari", "B")]; - let (opened, closed) = Snapshot::diff(&snap, &cur); - assert!(opened.is_empty()); - assert!(closed.is_empty()); - } - - #[test] - fn changes_result_suffix_no_change_is_empty() { - let c = Changes::no_change(); - assert_eq!(c.result_suffix(), ""); - assert!(!c.needs_restore()); - } - - #[test] - fn changes_result_suffix_single_new_window_with_title() { - let c = Changes { - new_windows: vec![WindowEvent { - window_id: 99, - pid: 100, - app_name: "Chrome".into(), - title: "New Tab".into(), - }], - foreground_changed: false, - }; - assert!(c.needs_restore()); - assert_eq!( - c.result_suffix(), - "\n\n🪟 Action opened new window(s): Chrome (\"New Tab\")." - ); - } - - #[test] - fn changes_result_suffix_groups_windows_by_app() { - let c = Changes { - new_windows: vec![ - WindowEvent { - window_id: 1, - pid: 100, - app_name: "Chrome".into(), - title: "Tab A".into(), - }, - WindowEvent { - window_id: 2, - pid: 100, - app_name: "Chrome".into(), - title: "Tab B".into(), - }, - WindowEvent { - window_id: 3, - pid: 101, - app_name: "Mail".into(), - title: "".into(), - }, - ], - foreground_changed: true, - }; - let suffix = c.result_suffix(); - // BTreeMap sort order is alphabetical by app name → Chrome before Mail. - assert_eq!( - suffix, - "\n\n🪟 Action opened new window(s): Chrome (\"Tab A\", \"Tab B\"); Mail." - ); - } - - #[test] - fn changes_result_suffix_foreground_change_only() { - let c = Changes { - new_windows: vec![], - foreground_changed: true, - }; - assert!(c.needs_restore()); - assert_eq!( - c.result_suffix(), - "\n\n🔀 Action caused a different app to become frontmost." - ); - } - - #[test] - fn changes_result_suffix_empty_title_is_dropped() { - let c = Changes { - new_windows: vec![WindowEvent { - window_id: 1, - pid: 100, - app_name: "Finder".into(), - title: "".into(), - }], - foreground_changed: false, - }; - // No title → just the app name, no parentheses. - assert_eq!( - c.result_suffix(), - "\n\n🪟 Action opened new window(s): Finder." - ); - } - - /// Regression: `snapshot(prior_front)` must store the caller's - /// captured front pid verbatim (rather than re-reading it inside - /// the function and racing with concurrent activations). - #[test] - fn snapshot_stores_caller_prior_front() { - // Use an obviously bogus pid so we'd notice if the impl silently - // fell back to the live frontmost on this test runner. - let bogus_prior = Some(424242_i32); - let snap = WindowChangeDetector::snapshot(bogus_prior); - assert_eq!(snap.front_pid(), bogus_prior); - - // None must round-trip too — and must skip the lease without - // panicking (no frontmost to restore to). - let snap_none = WindowChangeDetector::snapshot(None); - assert_eq!(snap_none.front_pid(), None); - } -} 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 ef1477e1d6..3de83088b0 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 @@ -1509,6 +1509,7 @@ impl Tool for GetWindowStateTool { is_error: None, structured_content: Some(structured), action_record: None, + surface_delta: None, } } Err(e) => ToolResult::error(format!("Error: {e}")), @@ -7473,6 +7474,7 @@ impl Tool for GetDesktopStateTool { is_error: None, structured_content: Some(structured), action_record: None, + surface_delta: None, } } } @@ -8425,6 +8427,7 @@ impl Tool for ZoomTool { "mime_type": "image/jpeg" })), action_record: None, + surface_delta: None, } } Ok(Err(e)) => ToolResult::error(format!("Zoom failed: {e}")), diff --git a/libs/cua-driver/typescript/src/native/cua_driver_contract.ts b/libs/cua-driver/typescript/src/native/cua_driver_contract.ts index 48aaa34a5b..3f489f3750 100644 --- a/libs/cua-driver/typescript/src/native/cua_driver_contract.ts +++ b/libs/cua-driver/typescript/src/native/cua_driver_contract.ts @@ -101,7 +101,8 @@ export enum ActionEscalationTarget { Pixel, Foreground, Page, - Session + Session, + Rebind } const FfiConverterTypeActionEscalationTarget = (() => { @@ -114,6 +115,7 @@ const FfiConverterTypeActionEscalationTarget = (() => { case 2: return ActionEscalationTarget.Foreground; case 3: return ActionEscalationTarget.Page; case 4: return ActionEscalationTarget.Session; + case 5: return ActionEscalationTarget.Rebind; default: throw new UniffiInternalError.UnexpectedEnumCase(); } } @@ -123,6 +125,7 @@ const FfiConverterTypeActionEscalationTarget = (() => { case ActionEscalationTarget.Foreground: return ordinalConverter.write(2, into); case ActionEscalationTarget.Page: return ordinalConverter.write(3, into); case ActionEscalationTarget.Session: return ordinalConverter.write(4, into); + case ActionEscalationTarget.Rebind: return ordinalConverter.write(5, into); } } allocationSize(value: TypeName): number { @@ -137,7 +140,8 @@ export enum ActionEscalationReason { DeliveryFailed, EffectUnconfirmed, SuspectedNoop, - PermissionRequired + PermissionRequired, + SurfaceChanged } const FfiConverterTypeActionEscalationReason = (() => { @@ -151,6 +155,7 @@ const FfiConverterTypeActionEscalationReason = (() => { case 3: return ActionEscalationReason.EffectUnconfirmed; case 4: return ActionEscalationReason.SuspectedNoop; case 5: return ActionEscalationReason.PermissionRequired; + case 6: return ActionEscalationReason.SurfaceChanged; default: throw new UniffiInternalError.UnexpectedEnumCase(); } } @@ -161,6 +166,7 @@ const FfiConverterTypeActionEscalationReason = (() => { case ActionEscalationReason.EffectUnconfirmed: return ordinalConverter.write(3, into); case ActionEscalationReason.SuspectedNoop: return ordinalConverter.write(4, into); case ActionEscalationReason.PermissionRequired: return ordinalConverter.write(5, into); + case ActionEscalationReason.SurfaceChanged: return ordinalConverter.write(6, into); } } allocationSize(value: TypeName): number { @@ -170,9 +176,128 @@ const FfiConverterTypeActionEscalationReason = (() => { return new FFIConverter(); })(); +const stringConverter = (() => { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + return { + stringToBytes: (s: string) => encoder.encode(s), + bytesToString: (ab: UniffiByteArray) => decoder.decode(ab), + stringByteLength: (s: string) => encoder.encode(s).byteLength, + writeStringIntoBuffer: (s: string, buf: any, offset: number): number => { + const view = new Uint8Array( + buf.arrayBuffer, + offset, + buf.arrayBuffer.byteLength - offset, + ); + return encoder.encodeInto(s, view).written; + }, + readStringFromBuffer: (buf: any, offset: number, length: number): string => + decoder.decode(new Uint8Array(buf.arrayBuffer, offset, length)), + }; +})(); +const FfiConverterString = uniffiCreateFfiConverterString(stringConverter); + +export enum ActionSurfaceKind { + Window, + Dialog, + Sheet, + Popover +} + +const FfiConverterTypeActionSurfaceKind = (() => { + const ordinalConverter = FfiConverterInt32; + type TypeName = ActionSurfaceKind; + class FFIConverter extends AbstractFfiConverterByteArray { + read(from: RustBuffer): TypeName { + switch (ordinalConverter.read(from)) { + case 1: return ActionSurfaceKind.Window; + case 2: return ActionSurfaceKind.Dialog; + case 3: return ActionSurfaceKind.Sheet; + case 4: return ActionSurfaceKind.Popover; + default: throw new UniffiInternalError.UnexpectedEnumCase(); + } + } + write(value: TypeName, into: RustBuffer): void { + switch (value) { + case ActionSurfaceKind.Window: return ordinalConverter.write(1, into); + case ActionSurfaceKind.Dialog: return ordinalConverter.write(2, into); + case ActionSurfaceKind.Sheet: return ordinalConverter.write(3, into); + case ActionSurfaceKind.Popover: return ordinalConverter.write(4, into); + } + } + allocationSize(value: TypeName): number { + return ordinalConverter.allocationSize(0); + } + } + return new FFIConverter(); +})(); + +/** + * A target-owned native interaction root discovered after an action. + */ +export type ActionWindowTarget = { + pid: bigint, + windowId: bigint, + appName: string, + title: string, + kind: ActionSurfaceKind, + modal: boolean +} + +/** + * Generated factory for {@link ActionWindowTarget} record objects. + */ +export const ActionWindowTarget = (() => { + const defaults = () => ({ + }); + const create = (() => { + return uniffiCreateRecord>(defaults); + })(); + return Object.freeze({ + create, + new: create, + defaults: () => Object.freeze(defaults()) as Partial, + }); +})(); + +const FfiConverterTypeActionWindowTarget = (() => { + type TypeName = ActionWindowTarget; + class FFIConverter extends AbstractFfiConverterByteArray { + read(from: RustBuffer): TypeName { + return { + pid: FfiConverterInt64.read(from), + windowId: FfiConverterUInt64.read(from), + appName: FfiConverterString.read(from), + title: FfiConverterString.read(from), + kind: FfiConverterTypeActionSurfaceKind.read(from), + modal: FfiConverterBool.read(from) + }; + } + write(value: TypeName, into: RustBuffer): void { + FfiConverterInt64.write(value.pid, into); + FfiConverterUInt64.write(value.windowId, into); + FfiConverterString.write(value.appName, into); + FfiConverterString.write(value.title, into); + FfiConverterTypeActionSurfaceKind.write(value.kind, into); + FfiConverterBool.write(value.modal, into); + } + allocationSize(value: TypeName): number { + return FfiConverterInt64.allocationSize(value.pid) + + FfiConverterUInt64.allocationSize(value.windowId) + + FfiConverterString.allocationSize(value.appName) + + FfiConverterString.allocationSize(value.title) + + FfiConverterTypeActionSurfaceKind.allocationSize(value.kind) + + FfiConverterBool.allocationSize(value.modal); + + } + }; + return new FFIConverter(); +})(); + export type ActionEscalation = { target: ActionEscalationTarget, - reason: ActionEscalationReason + reason: ActionEscalationReason, + window?: ActionWindowTarget } /** @@ -197,16 +322,19 @@ const FfiConverterTypeActionEscalation = (() => { read(from: RustBuffer): TypeName { return { target: FfiConverterTypeActionEscalationTarget.read(from), - reason: FfiConverterTypeActionEscalationReason.read(from) + reason: FfiConverterTypeActionEscalationReason.read(from), + window: FfiConverterOptionalTypeActionWindowTarget.read(from) }; } write(value: TypeName, into: RustBuffer): void { FfiConverterTypeActionEscalationTarget.write(value.target, into); FfiConverterTypeActionEscalationReason.write(value.reason, into); + FfiConverterOptionalTypeActionWindowTarget.write(value.window, into); } allocationSize(value: TypeName): number { return FfiConverterTypeActionEscalationTarget.allocationSize(value.target) + - FfiConverterTypeActionEscalationReason.allocationSize(value.reason); + FfiConverterTypeActionEscalationReason.allocationSize(value.reason) + + FfiConverterOptionalTypeActionWindowTarget.allocationSize(value.window); } }; @@ -214,8 +342,7 @@ const FfiConverterTypeActionEscalation = (() => { })(); export enum ActionEvidenceKind { - ValueReadback, - WindowChange + ValueReadback } const FfiConverterTypeActionEvidenceKind = (() => { @@ -225,14 +352,12 @@ const FfiConverterTypeActionEvidenceKind = (() => { read(from: RustBuffer): TypeName { switch (ordinalConverter.read(from)) { case 1: return ActionEvidenceKind.ValueReadback; - case 2: return ActionEvidenceKind.WindowChange; default: throw new UniffiInternalError.UnexpectedEnumCase(); } } write(value: TypeName, into: RustBuffer): void { switch (value) { case ActionEvidenceKind.ValueReadback: return ordinalConverter.write(1, into); - case ActionEvidenceKind.WindowChange: return ordinalConverter.write(2, into); } } allocationSize(value: TypeName): number { @@ -360,11 +485,58 @@ const FfiConverterTypeActionRoute = (() => { return new FFIConverter(); })(); +/** + * Read-only topology observed for the action's target process. + */ +export type ActionWindowChange = { + newWindows: Array, + foregroundChanged: boolean +} + +/** + * Generated factory for {@link ActionWindowChange} record objects. + */ +export const ActionWindowChange = (() => { + const defaults = () => ({ + }); + const create = (() => { + return uniffiCreateRecord>(defaults); + })(); + return Object.freeze({ + create, + new: create, + defaults: () => Object.freeze(defaults()) as Partial, + }); +})(); + +const FfiConverterTypeActionWindowChange = (() => { + type TypeName = ActionWindowChange; + class FFIConverter extends AbstractFfiConverterByteArray { + read(from: RustBuffer): TypeName { + return { + newWindows: FfiConverterSequenceTypeActionWindowTarget.read(from), + foregroundChanged: FfiConverterBool.read(from) + }; + } + write(value: TypeName, into: RustBuffer): void { + FfiConverterSequenceTypeActionWindowTarget.write(value.newWindows, into); + FfiConverterBool.write(value.foregroundChanged, into); + } + allocationSize(value: TypeName): number { + return FfiConverterSequenceTypeActionWindowTarget.allocationSize(value.newWindows) + + FfiConverterBool.allocationSize(value.foregroundChanged); + + } + }; + return new FFIConverter(); +})(); + export type ActionResult = { effect: ActionEffect, route: ActionRoute, delivery?: ActionDelivery, evidence?: Array, + windowChange?: ActionWindowChange, escalation?: ActionEscalation } @@ -393,6 +565,7 @@ const FfiConverterTypeActionResult = (() => { route: FfiConverterTypeActionRoute.read(from), delivery: FfiConverterOptionalTypeActionDelivery.read(from), evidence: FfiConverterOptionalSequenceTypeActionEvidence.read(from), + windowChange: FfiConverterOptionalTypeActionWindowChange.read(from), escalation: FfiConverterOptionalTypeActionEscalation.read(from) }; } @@ -401,6 +574,7 @@ const FfiConverterTypeActionResult = (() => { FfiConverterTypeActionRoute.write(value.route, into); FfiConverterOptionalTypeActionDelivery.write(value.delivery, into); FfiConverterOptionalSequenceTypeActionEvidence.write(value.evidence, into); + FfiConverterOptionalTypeActionWindowChange.write(value.windowChange, into); FfiConverterOptionalTypeActionEscalation.write(value.escalation, into); } allocationSize(value: TypeName): number { @@ -408,6 +582,7 @@ const FfiConverterTypeActionResult = (() => { FfiConverterTypeActionRoute.allocationSize(value.route) + FfiConverterOptionalTypeActionDelivery.allocationSize(value.delivery) + FfiConverterOptionalSequenceTypeActionEvidence.allocationSize(value.evidence) + + FfiConverterOptionalTypeActionWindowChange.allocationSize(value.windowChange) + FfiConverterOptionalTypeActionEscalation.allocationSize(value.escalation); } @@ -470,27 +645,6 @@ const FfiConverterTypeBoundsExpectation = (() => { return new FFIConverter(); })(); -const stringConverter = (() => { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - return { - stringToBytes: (s: string) => encoder.encode(s), - bytesToString: (ab: UniffiByteArray) => decoder.decode(ab), - stringByteLength: (s: string) => encoder.encode(s).byteLength, - writeStringIntoBuffer: (s: string, buf: any, offset: number): number => { - const view = new Uint8Array( - buf.arrayBuffer, - offset, - buf.arrayBuffer.byteLength - offset, - ); - return encoder.encodeInto(s, view).written; - }, - readStringFromBuffer: (buf: any, offset: number, length: number): string => - decoder.decode(new Uint8Array(buf.arrayBuffer, offset, length)), - }; -})(); -const FfiConverterString = uniffiCreateFfiConverterString(stringConverter); - // Enum: ActionTarget export enum ActionTarget_Tags { @@ -3747,6 +3901,9 @@ const FfiConverterTypePlatform = (() => { // FfiConverter for number | undefined const FfiConverterOptionalUInt32 = new FfiConverterOptional(FfiConverterUInt32); +// FfiConverter for ActionWindowTarget | undefined +const FfiConverterOptionalTypeActionWindowTarget = new FfiConverterOptional(FfiConverterTypeActionWindowTarget); + // FfiConverter for ActionDelivery | undefined const FfiConverterOptionalTypeActionDelivery = new FfiConverterOptional(FfiConverterTypeActionDelivery); @@ -3756,6 +3913,12 @@ const FfiConverterSequenceTypeActionEvidence = new FfiConverterArray(FfiConverte // FfiConverter for Array | undefined const FfiConverterOptionalSequenceTypeActionEvidence = new FfiConverterOptional(FfiConverterSequenceTypeActionEvidence); +// FfiConverter for Array +const FfiConverterSequenceTypeActionWindowTarget = new FfiConverterArray(FfiConverterTypeActionWindowTarget); + +// FfiConverter for ActionWindowChange | undefined +const FfiConverterOptionalTypeActionWindowChange = new FfiConverterOptional(FfiConverterTypeActionWindowChange); + // FfiConverter for ActionEscalation | undefined const FfiConverterOptionalTypeActionEscalation = new FfiConverterOptional(FfiConverterTypeActionEscalation); @@ -3857,7 +4020,10 @@ export default Object.freeze({ FfiConverterTypeActionEvidenceKind, FfiConverterTypeActionResult, FfiConverterTypeActionRoute, + FfiConverterTypeActionSurfaceKind, FfiConverterTypeActionTarget, + FfiConverterTypeActionWindowChange, + FfiConverterTypeActionWindowTarget, FfiConverterTypeBoundsExpectation, FfiConverterTypeCaptureScope, FfiConverterTypeClickButton, diff --git a/scripts/ci/macos/run-rust-e2e.sh b/scripts/ci/macos/run-rust-e2e.sh index d631746aed..b938450f83 100755 --- a/scripts/ci/macos/run-rust-e2e.sh +++ b/scripts/ci/macos/run-rust-e2e.sh @@ -390,6 +390,9 @@ EOF run_test installed-app-textedit cargo test -p cua-driver --test installed_app_textedit_macos_test -- \ --ignored --exact background_type_on_native_cocoa_is_ax_verified \ --nocapture --test-threads=1 + run_test installed-app-textedit-rebind cargo test -p cua-driver --test installed_app_textedit_macos_test -- \ + --ignored --exact background_open_panel_returns_a_typed_rebind \ + --nocapture --test-threads=1 fi fi if [[ "${SUITE}" == capture || "${SUITE}" == all ]]; then From 14af671be839f17f4c7d7d3340cb1724e718b34a Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 22:23:34 +0800 Subject: [PATCH 08/16] refactor(cua-driver): keep root observation fail closed --- .../platform-macos/src/post_action_observer.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs index cfae0849ea..bdcd82fcb4 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -36,7 +36,6 @@ struct RootKey { #[derive(Clone, Debug, Eq, PartialEq)] struct Root { - key: RootKey, target: Option, focused: bool, } @@ -154,7 +153,9 @@ fn observe_delta( }) }) .collect(); - return resolve_surface_delta(candidates, foreground_changed); + if let Some(delta) = resolve_surface_delta(candidates, foreground_changed) { + return Some(delta); + } } if Instant::now() >= deadline { return None; @@ -251,9 +252,8 @@ unsafe fn insert_root( }) }); roots.insert( - key.clone(), + key, Root { - key, target, focused: copy_bool_attr(element, "AXFocused").unwrap_or(false), }, @@ -290,12 +290,6 @@ mod tests { modal, }; Root { - key: RootKey { - window_id: Some(window_id as u32), - parent_window_id: None, - role: "AXWindow".into(), - subrole: "AXDialog".into(), - }, target: Some(target), focused, } From 25385afedbe4384aae068421571626ba9d2da6e3 Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 22:29:51 +0800 Subject: [PATCH 09/16] docs(cua-driver): fix rediscovery diagram rendering --- .../architecture-comparison.dot | 10 +++++----- .../architecture-comparison.png | Bin 0 -> 118125 bytes .../architecture-comparison.svg | 17 +++++++++-------- .../docs/post-action-surface-rediscovery.md | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.png diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot index d3ab5b08ba..a11d630f0e 100644 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot @@ -1,10 +1,10 @@ digraph rediscovery { - graph [rankdir=TB, bgcolor="transparent", pad="0.2", nodesep="0.25", ranksep="0.35", compound=true]; + graph [rankdir=TB, bgcolor="#ffffff", fontcolor="#1f2328", pad="0.2", nodesep="0.25", ranksep="0.35", compound=true]; node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=11, margin="0.12,0.08"]; edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=9, arrowsize=0.65]; subgraph cluster_current { - label="current Cua"; color="#8c959f"; style="dashed"; + label="before this PR"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; current_before [label="global window ids"]; current_action [label="native action"]; current_prose [label="diagnostic prose only"]; @@ -12,7 +12,7 @@ digraph rediscovery { } subgraph cluster_2746 { - label="#2746"; color="#8c959f"; style="dashed"; + label="#2746"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; pr_global [label="global window diff"]; pr_json [label="legacy JSON adapter"]; pr_effect [label="effect promotion and count-based rebind"]; @@ -20,7 +20,7 @@ digraph rediscovery { } subgraph cluster_picu { - label="pi-computer-use"; color="#8c959f"; style="dashed"; + label="pi-computer-use"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; pi_before [label="target AX roots"]; pi_action [label="native action"]; pi_after [label="authoritative root diff"]; @@ -29,7 +29,7 @@ digraph rediscovery { } subgraph cluster_combined { - label="combined design"; color="#8c959f"; style="dashed"; + label="this PR"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; best_observer [label="typed target-root observer"]; best_action [label="typed actuator outcome"]; best_record [label="shared action record"]; diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.png b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..896de0cabf9123f6fd1f029c5aca05e92fc8fdf5 GIT binary patch literal 118125 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 diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md deleted file mode 100644 index c72f85f439..0000000000 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ /dev/null @@ -1,51 +0,0 @@ -# Post-action surface rediscovery - -Issue [#2238](https://github.com/trycua/cua/issues/2238) describes a stale-target failure: an action opens a sheet, dialog, popover, or Open/Save panel, but the caller remains bound to the blocked parent window. Cua Driver's old macOS detector compared all desktop windows and appended changes to diagnostic prose. It could neither prove causality nor return a stable rebind address. - -This implementation resolves the structured-rebind rung. It does not add the issue's later privacy-sensitive desktop-frame fallback or inert-resnapshot backstop. - -![Post-action rediscovery architecture before and after this change](diagrams/post-action-surface-rediscovery/architecture-comparison.png) - -## Sources - -The implementation retains Cua Driver's native action routes, focus-suppression leases, shared action record, closed result contract, and harness-owned escalation policy. From [#2746](https://github.com/trycua/cua/pull/2746), it retains typed `window_change` metadata, explicit `rebind` advice, and generated Rust, Python, TypeScript, and manifest bindings. - -The target-root observer adapts `injaneity/pi-computer-use` commit `022a280a377065c95736cc15f684bf1fad46479e`: snapshot the target accessibility domain before dispatch, use a cheap target-window signature only to end the bounded wait early, then treat one accessibility snapshot diff as authoritative. If the early signal beats the accessibility tree, bounded catch-up retries let it settle. Appeared roots plus modal or focused state resolve the next interaction surface. - -The persistent AXObserver ring is intentionally not copied. That commit established that sheet creation emits no reliable AX notification, so events improve latency for some roots but do not improve correctness. Cua Driver's bounded signature poll preserves the useful early-exit behavior without adding observer lifecycle state. - -Cua Driver adds its existing focus-suppression lease, typed action record, and WindowServer ownership verification at the macOS platform boundary. AX discovery finishes before ownership is sampled; unresolved appeared roots retry only ownership, so a foreign panel cannot be discarded because WindowServer lagged the accessibility tree. An incomplete owner set can return known candidates but never an exact target. `AXSheets` and `AXChildren` are merged because recent macOS versions do not expose sheets consistently through one attribute. - -## Flow - -```text -macOS action decorator - → target accessibility snapshot - → native actuator - → target accessibility diff - → WindowServer ownership proof - → shared candidate resolver - → typed action record - → closed action result -``` - -The observer reports facts; the shared resolver chooses policy; the action record accounts for actuator effect. A surface change cannot promote `effect` to `confirmed` because confirmation still requires value readback. - -An exact rebind requires one owner-verified candidate that is modal or focused. Otherwise the result retains candidates without inventing a target, and the caller can correlate them with one fresh `list_windows` call. Rebinding itself never activates, raises, captures, or sends input to the new surface. - -Windows and Linux currently omit topology instead of substituting an unscoped desktop heuristic. - -## Removed path - -The implementation removes the global `WindowChangeDetector`, per-tool diagnostic suffix mutations, the dead observation-skip hook, topology transport through legacy JSON, and `window_change` as duplicate effect evidence. One macOS action decorator now owns observation for every affected action tool. - -## Contract invariants - -- `window_change` is independent from actuator effect. -- an exact escalation target also appears in `window_change.new_windows`. -- unrelated foreground or desktop changes do not create a surface delta. -- ambiguous candidates never produce an exact target. -- partial and failed delivery retain observed topology internally. -- logical rebinding does not change foreground focus or z-order. - -The canonical TextEdit Open-panel case exercises the foreign panel-service owner, exact rebound addressability, unchanged foreground and z-order, stable cursor position, and input isolation. The complete macOS Lume matrix remains the readiness gate for the final candidate SHA. diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs index 5b03224a10..3dfef3af00 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -190,6 +190,8 @@ fn appeared_roots(before: &RootSnapshot, after: &RootSnapshot) -> Vec { .collect() } +/// Resolve ownership after AX has established which roots appeared. WindowServer +/// may lag AX, so unresolved roots retry ownership only and block an exact target. fn resolve_appeared_roots( pid: i32, roots: &[Root], From 4e7bc5b20f567217034b4d0a50d542d67bd65eb0 Mon Sep 17 00:00:00 2001 From: injaneity <44902825+injaneity@users.noreply.github.com> Date: Wed, 26 Aug 2026 03:02:42 -0700 Subject: [PATCH 16/16] fix(cua-driver): unify native action scheduling --- .../crates/cua-driver-contract/src/lib.rs | 8 + .../rust/crates/cua-driver-core/src/lib.rs | 1 + .../src/native_action_scheduler.rs | 125 ++++++++++ .../rust/crates/cua-driver-core/src/tool.rs | 215 +++++++++--------- 4 files changed, 239 insertions(+), 110 deletions(-) create mode 100644 libs/cua-driver/rust/crates/cua-driver-core/src/native_action_scheduler.rs diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs index 506bd1b6db..58dfd250bd 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs @@ -96,6 +96,14 @@ pub fn is_action_result_tool(name: &str) -> bool { ACTION_RESULT_TOOLS.contains(&name) } +/// Whether an action uses the native desktop interaction lane. +/// +/// Browser actions own their scheduling inside the browser engine; every other +/// action-result tool runs against native desktop state. +pub fn is_desktop_action_result_tool(name: &str) -> bool { + is_action_result_tool(name) && !name.starts_with("browser_") +} + #[derive( Debug, Clone, diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs index 4638b9c264..e984b3ceaf 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/lib.rs @@ -66,6 +66,7 @@ pub mod ffmpeg_install; pub mod health_report; pub mod history; pub mod image_utils; +pub(crate) mod native_action_scheduler; pub mod page; pub mod pip_hook; pub mod policy; diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/native_action_scheduler.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/native_action_scheduler.rs new file mode 100644 index 0000000000..da06c13dbd --- /dev/null +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/native_action_scheduler.rs @@ -0,0 +1,125 @@ +//! Keyed scheduling for native action resources. +//! +//! The scheduler owns one bounded lease lifecycle for physical desktop input +//! and fail-fast process-scoped text admission. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +use tokio::sync::{Mutex as AsyncMutex, OwnedMutexGuard}; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) enum NativeActionResource { + PhysicalDesktop, + TextInputProcess(i64), +} + +pub(crate) struct NativeActionScheduler { + gates: Mutex>>>, +} + +impl Default for NativeActionScheduler { + fn default() -> Self { + Self { + gates: Mutex::new(HashMap::new()), + } + } +} + +impl NativeActionScheduler { + fn new() -> Self { + Self::default() + } + + fn gate(&self, resource: NativeActionResource) -> Arc> { + let mut gates = self + .gates + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(gate) = gates.get(&resource).and_then(Weak::upgrade) { + return gate; + } + + // Weak entries do not keep completed process resources alive. Prune + // them when admitting a new resource so transient pids stay bounded. + gates.retain(|_, gate| gate.strong_count() > 0); + let gate = Arc::new(AsyncMutex::new(())); + gates.insert(resource, Arc::downgrade(&gate)); + gate + } + + pub(crate) async fn lock(&self, resource: NativeActionResource) -> OwnedMutexGuard<()> { + self.gate(resource).lock_owned().await + } + + pub(crate) fn try_lock(&self, resource: NativeActionResource) -> Option> { + self.gate(resource).try_lock_owned().ok() + } + + #[cfg(test)] + fn retained_gate_count(&self) -> usize { + self.gates + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .len() + } +} + +pub(crate) fn global() -> &'static NativeActionScheduler { + static SCHEDULER: OnceLock = OnceLock::new(); + SCHEDULER.get_or_init(NativeActionScheduler::new) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + fn text(pid: i64) -> NativeActionResource { + NativeActionResource::TextInputProcess(pid) + } + + #[tokio::test] + async fn same_resource_serializes_while_independent_resources_proceed() { + let scheduler = NativeActionScheduler::new(); + let first = scheduler.lock(text(7)).await; + + assert!(scheduler.try_lock(text(7)).is_none()); + tokio::time::timeout(Duration::from_millis(20), scheduler.lock(text(8))) + .await + .expect("independent resource should not block"); + + drop(first); + tokio::time::timeout(Duration::from_millis(20), scheduler.lock(text(7))) + .await + .expect("completed resource should release its lane"); + } + + #[tokio::test] + async fn cancelled_waiter_does_not_keep_or_poison_a_resource() { + let scheduler = NativeActionScheduler::new(); + let first = scheduler.lock(text(7)).await; + + assert!( + tokio::time::timeout(Duration::from_millis(20), scheduler.lock(text(7))) + .await + .is_err() + ); + drop(first); + + tokio::time::timeout(Duration::from_millis(20), scheduler.lock(text(7))) + .await + .expect("cancelling a waiter must leave the resource usable"); + } + + #[tokio::test] + async fn completed_process_gates_are_pruned() { + let scheduler = NativeActionScheduler::new(); + for pid in 1..65 { + drop(scheduler.lock(text(pid)).await); + } + + let _live = scheduler.lock(text(65)).await; + assert_eq!(scheduler.retained_gate_count(), 1); + } +} diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs index a6e49f63a5..9ad2c9f4c2 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs @@ -1,8 +1,8 @@ //! Tool trait and registry. -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::path::{Component, Path, PathBuf}; -use std::sync::{Arc, Mutex, OnceLock}; +use std::sync::{Arc, Mutex}; use std::time::Duration; use async_trait::async_trait; @@ -14,6 +14,7 @@ thread_local! { } use crate::{ + native_action_scheduler::{global as native_action_scheduler, NativeActionResource}, pip_hook, protocol::{Content, ToolResult}, recording::{now_ms, screenshot_for, RecordingSession}, @@ -71,54 +72,57 @@ pub fn with_runtime_scope(scope: String, action: impl FnOnce() -> T) -> T { action() } -fn desktop_action_coordinator() -> &'static tokio::sync::Mutex<()> { - static COORDINATOR: OnceLock> = OnceLock::new(); - COORDINATOR.get_or_init(|| tokio::sync::Mutex::new(())) -} - -fn active_text_input_pids() -> &'static Mutex> { - static ACTIVE: OnceLock>> = OnceLock::new(); - ACTIVE.get_or_init(|| Mutex::new(HashSet::new())) -} - #[derive(Debug)] -struct TextInputAdmission { - pid: i64, +struct ActionLeasePlan { + serialize_desktop: bool, + text_input_pid: Option, } -impl Drop for TextInputAdmission { - fn drop(&mut self) { - active_text_input_pids() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .remove(&self.pid); +impl ActionLeasePlan { + fn for_call(tool_name: &str, args: &Value) -> Self { + let pid = args + .get("pid") + .and_then(Value::as_i64) + .filter(|pid| *pid > 0); + Self { + serialize_desktop: cua_driver_contract::is_desktop_action_result_tool(tool_name) + || tool_name == "bring_to_front", + text_input_pid: if matches!(tool_name, "type_text" | "type_text_chars") { + pid + } else { + None + }, + } } -} -fn try_admit_text_input( - tool_name: &str, - args: &Value, -) -> Result, ToolResult> { - if tool_name != "type_text" { - return Ok(None); - } - let Some(pid) = args - .get("pid") - .and_then(Value::as_i64) - .filter(|pid| *pid > 0) - else { - // Desktop-scoped input has no stable process identity. It continues to - // use the process-wide physical action coordinator below. - return Ok(None); - }; - let mut active = active_text_input_pids() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if !active.insert(pid) { - let message = format!("text input is already active for pid {pid}"); - return Err(protected_refusal("input_busy", &message)); - } - Ok(Some(TextInputAdmission { pid })) + fn try_admit_text(&self) -> Result>, ToolResult> { + let Some(pid) = self.text_input_pid else { + return Ok(None); + }; + native_action_scheduler() + .try_lock(NativeActionResource::TextInputProcess(pid)) + .map(Some) + .ok_or_else(|| { + protected_refusal( + "input_busy", + &format!("text input is already active for pid {pid}"), + ) + }) + } + + async fn acquire_desktop(&self) -> Option> { + if !self.serialize_desktop { + return None; + } + let scheduler = native_action_scheduler(); + let resource = NativeActionResource::PhysicalDesktop; + // Avoid a dispatch yield between fresh target proof and native input + // when the process-wide resource is uncontended. + match scheduler.try_lock(resource) { + Some(admission) => Some(admission), + None => Some(scheduler.lock(resource).await), + } + } } pub use cua_driver_contract::{CAPABILITY_VERSION, TOOLS_LIST_SCHEMA_VERSION}; @@ -1136,14 +1140,13 @@ impl ToolRegistry { return result; } - // A queued text mutation can become stale while another agent is - // typing into the same process. Refuse that overlap before consent, - // cursor, recording, or platform focus behavior can begin. The guard - // is process-global so independent registries cannot interleave text - // through separate platform workers, and RAII releases it on task - // cancellation as well as normal completion. - let _text_input_admission = match try_admit_text_input(resolved_name, &public_args) { - Ok(admission) => admission, + // Build one immutable lease plan from normalized public arguments. + // Fail-fast text admission happens before consent or platform behavior; + // the desktop lease starts immediately before evidence capture and is + // held through the complete decorated invocation. + let action_lease_plan = ActionLeasePlan::for_call(resolved_name, &public_args); + let _text_input_lease = match action_lease_plan.try_admit_text() { + Ok(admissions) => admissions, Err(mut refusal) => { restore_public_runtime_result(&mut refusal, &runtime_prefix); return refusal; @@ -1496,20 +1499,7 @@ impl ToolRegistry { "start_recording" | "stop_recording" | "get_recording_state" | "replay_trajectory" ); let private_consent_turn = is_existing_profile_prepare(resolved_name, &args); - let _desktop_action = if is_physical_desktop_action(resolved_name) { - let coordinator = desktop_action_coordinator(); - // Avoid yielding the dispatch task when the process-wide input - // lane is uncontended. On Windows, that yield creates a window in - // which the foreground target can lose keyboard eligibility - // between the fixture's focus proof and SendInput. Contended - // runtimes still wait and serialize through the same mutex. - Some(match coordinator.try_lock() { - Ok(guard) => guard, - Err(_) => coordinator.lock().await, - }) - } else { - None - }; + let _desktop_action_lease = action_lease_plan.acquire_desktop().await; let pending_turn = should_record .then(|| { if private_consent_turn { @@ -1525,9 +1515,8 @@ impl ToolRegistry { let mut result = tool.invoke(args.clone()).await; drop(lifecycle_dispatch); // The platform worker has exited, so another text operation for this - // pid may now start even while result projection and evidence capture - // finish for the completed call. - drop(_text_input_admission); + // pid may start while result projection finishes. + drop(_text_input_lease); if result.action_record.is_none() { if let Some(structured) = result.structured_content.as_ref() { result.action_record = crate::action_record::ActionExecutionRecord::from_legacy( @@ -1573,7 +1562,7 @@ impl ToolRegistry { // capture or result shaping. Keeping the global desktop lock through // recording/PiP screenshots would unnecessarily block an unrelated // runtime after the input side effect has already completed. - drop(_desktop_action); + drop(_desktop_action_lease); restore_public_runtime_result(&mut result, &runtime_prefix); // Preserve the producer's private summary for recording/replay before // the public ActionResult projection deliberately replaces legacy @@ -2544,28 +2533,6 @@ fn canonical_proposed_path(raw: &str) -> Result { Ok(canonical.to_string_lossy().into_owned()) } -fn is_physical_desktop_action(tool: &str) -> bool { - matches!( - tool, - "click" - | "double_click" - | "right_click" - | "scroll" - | "drag" - | "mouse_drag" - | "parallel_mouse_drag" - | "move_cursor" - | "mouse_button_down" - | "mouse_button_up" - | "type_text" - | "press_key" - | "hotkey" - | "set_value" - | "bring_to_front" - | "set_window_frame" - ) -} - /// Bucket that owns the processes a call is allowed to terminate. /// /// A call that declares a session keys its launches to that session, so @@ -2760,9 +2727,9 @@ fn restore_public_runtime_value(value: &mut Value, runtime_prefix: &str) -> bool #[cfg(test)] mod runtime_isolation_tests { use super::{ - canonical_proposed_path, desktop_action_coordinator, namespace_runtime_args, - publish_action_result, restore_public_runtime_result, try_admit_text_input, - TrustedInvocationEvidence, DISPATCH_RUNTIME_SCOPE, + canonical_proposed_path, namespace_runtime_args, native_action_scheduler, + publish_action_result, restore_public_runtime_result, ActionLeasePlan, + NativeActionResource, TrustedInvocationEvidence, DISPATCH_RUNTIME_SCOPE, }; use crate::{ authorization::PermissionMode, @@ -4477,8 +4444,31 @@ resources: assert_eq!(owner.unwrap(), (44, 0)); } + #[test] + fn one_lease_plan_covers_native_actions_text_and_browser_exclusion() { + let click = ActionLeasePlan::for_call("click", &serde_json::json!({"pid": 42})); + assert!(click.serialize_desktop); + assert_eq!(click.text_input_pid, None); + + let menu = ActionLeasePlan::for_call("invoke_menu", &serde_json::json!({"pid": 42})); + assert!(menu.serialize_desktop); + assert_eq!(menu.text_input_pid, None); + + let text = ActionLeasePlan::for_call("type_text_chars", &serde_json::json!({"pid": 42})); + assert!(text.serialize_desktop); + assert_eq!(text.text_input_pid, Some(42)); + + let browser = ActionLeasePlan::for_call("browser_click", &serde_json::json!({})); + assert!(!browser.serialize_desktop); + assert_eq!(browser.text_input_pid, None); + + let read = ActionLeasePlan::for_call("get_window_state", &serde_json::json!({"pid": 42})); + assert!(!read.serialize_desktop); + assert_eq!(read.text_input_pid, None); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn physical_desktop_actions_are_admitted_one_at_a_time() { + async fn desktop_actions_are_admitted_one_at_a_time() { let active = Arc::new(AtomicUsize::new(0)); let max_active = Arc::new(AtomicUsize::new(0)); let mut tasks = Vec::new(); @@ -4486,7 +4476,9 @@ resources: let active = active.clone(); let max_active = max_active.clone(); tasks.push(tokio::spawn(async move { - let _admission = desktop_action_coordinator().lock().await; + let _admission = native_action_scheduler() + .lock(NativeActionResource::PhysicalDesktop) + .await; let now = active.fetch_add(1, Ordering::SeqCst) + 1; max_active.fetch_max(now, Ordering::SeqCst); tokio::task::yield_now().await; @@ -4502,11 +4494,13 @@ resources: #[test] fn overlapping_text_input_for_one_pid_fails_fast_and_releases_after_completion() { let pid = 8_675_410; - let first = try_admit_text_input("type_text", &serde_json::json!({"pid": pid})) - .expect("first text operation should be admitted") - .expect("pid-scoped text operation should receive a guard"); + let first_plan = ActionLeasePlan::for_call("type_text", &serde_json::json!({"pid": pid})); + let first = first_plan + .try_admit_text() + .expect("first text operation should be admitted"); - let refusal = try_admit_text_input("type_text", &serde_json::json!({"pid": pid})) + let refusal = first_plan + .try_admit_text() .expect_err("overlapping text operation should fail fast"); assert_eq!(refusal.is_error, Some(true)); assert_eq!( @@ -4518,17 +4512,18 @@ resources: Some("input_busy") ); - let other_pid = try_admit_text_input("type_text", &serde_json::json!({"pid": pid + 1})) - .expect("a different pid should have an independent input lane") - .expect("pid-scoped text operation should receive a guard"); + let other_plan = + ActionLeasePlan::for_call("type_text", &serde_json::json!({"pid": pid + 1})); + let other_pid = other_plan + .try_admit_text() + .expect("a different pid should have an independent input lane"); drop(other_pid); drop(first); - assert!( - try_admit_text_input("type_text", &serde_json::json!({"pid": pid})) - .expect("completed text operation should release its pid") - .is_some() - ); + assert!(first_plan + .try_admit_text() + .expect("completed text operation should release its pid") + .is_some()); } #[test]

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 literal 0 HcmV?d00001 diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg index 52dd00f321..7f60096213 100644 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg +++ b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg @@ -8,25 +8,26 @@ viewBox="0.00 0.00 886.80 304.30" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> rediscovery + cluster_current - -current Cua + +before this PR cluster_2746 - -#2746 + +#2746 cluster_picu - -pi-computer-use + +pi-computer-use cluster_combined - -combined design + +this PR diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index 0ab8d63924..41fb07baa2 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -6,7 +6,7 @@ keeps Cua Driver's typed action-result contract while replacing its global window-count heuristic with target-scoped root observation and shared candidate validation. -![architecture comparison](diagrams/post-action-surface-rediscovery/architecture-comparison.svg) +![post-action rediscovery architecture before and after this change](diagrams/post-action-surface-rediscovery/architecture-comparison.png) ## Existing Cua Driver flow From caa8b9f61669abf3c6757b010f6ed617e461df7d Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 22:49:02 +0800 Subject: [PATCH 10/16] refactor(cua-driver): narrow surface rebind data Keep modality and focus as internal resolver facts. Publish only the window identity needed for logical rebinding, and ignore unrelated foreground changes. Co-authored-by: Francesco Bonacci <195596869+f-trycua@users.noreply.github.com> --- libs/cua-driver/contract/manifest.json | 288 ++---------------- .../cua-driver/docs/action-result-contract.md | 2 +- .../architecture-comparison.svg | 195 ------------ .../docs/post-action-surface-rediscovery.md | 136 ++------- .../python/src/cua_driver/__init__.py | 2 - .../python/src/cua_driver/_native_contract.py | 115 ++----- .../crates/cua-driver-contract/src/lib.rs | 15 +- .../crates/cua-driver-contract/src/outputs.rs | 19 +- .../cua-driver-core/src/action_record.rs | 64 ++-- .../installed_app_textedit_macos_test.rs | 1 - .../src/post_action_observer.rs | 92 +----- .../src/native/cua_driver_contract.ts | 50 +-- 12 files changed, 114 insertions(+), 865 deletions(-) delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg diff --git a/libs/cua-driver/contract/manifest.json b/libs/cua-driver/contract/manifest.json index 8bb415826d..c12f26f12e 100644 --- a/libs/cua-driver/contract/manifest.json +++ b/libs/cua-driver/contract/manifest.json @@ -193,18 +193,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -219,9 +207,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -283,18 +269,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -309,9 +283,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -695,18 +667,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -721,9 +681,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -785,18 +743,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -811,9 +757,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -1784,18 +1728,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -1810,9 +1742,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -1874,18 +1804,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -1900,9 +1818,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -2054,18 +1970,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -2080,9 +1984,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -2144,18 +2046,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -2170,9 +2060,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -2505,18 +2393,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -2531,9 +2407,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -2595,18 +2469,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -2621,9 +2483,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -2821,18 +2681,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -2847,9 +2695,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -2911,18 +2757,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -2937,9 +2771,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -3157,18 +2989,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -3183,9 +3003,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -3247,18 +3065,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -3273,9 +3079,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -3740,18 +3544,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -3766,9 +3558,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -3830,18 +3620,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -3856,9 +3634,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, @@ -4188,18 +3964,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -4214,9 +3978,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": [ "object", @@ -4278,18 +4040,6 @@ "app_name": { "type": "string" }, - "kind": { - "enum": [ - "window", - "dialog", - "sheet", - "popover" - ], - "type": "string" - }, - "modal": { - "type": "boolean" - }, "pid": { "format": "int64", "type": "integer" @@ -4304,9 +4054,7 @@ "pid", "window_id", "app_name", - "title", - "kind", - "modal" + "title" ], "type": "object" }, diff --git a/libs/cua-driver/docs/action-result-contract.md b/libs/cua-driver/docs/action-result-contract.md index 5eb7f0fb89..33551ad63b 100644 --- a/libs/cua-driver/docs/action-result-contract.md +++ b/libs/cua-driver/docs/action-result-contract.md @@ -33,7 +33,7 @@ Every successful action returns a closed `structuredContent` object: | `route` | `accessibility`, `synthetic_events`, `global_input`, `dom`, `trusted_input` | | `delivery.mode` | `background`, `foreground`, `not_applicable`, `unknown` | | `evidence[].kind` | `value_readback` | -| `window_change.new_windows[]` | target-scoped, owner-verified `pid`, `window_id`, application, title, surface kind, and modality | +| `window_change.new_windows[]` | target-scoped, owner-verified `pid`, `window_id`, application, and title | | `escalation.target` | `pixel`, `foreground`, `page`, `session`, `rebind` | | `escalation.reason` | `route_unavailable`, `delivery_failed`, `effect_unconfirmed`, `suspected_noop`, `permission_required`, `surface_changed` | diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg deleted file mode 100644 index 7f60096213..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.svg +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - -rediscovery - - -cluster_current - -before this PR - - -cluster_2746 - -#2746 - - -cluster_picu - -pi-computer-use - - -cluster_combined - -this PR - - - -current_before - -global window ids - - - -current_action - -native action - - - -current_before->current_action - - - - - -current_prose - -diagnostic prose only - - - -current_action->current_prose - - - - - -pr_global - -global window diff - - - -pr_json - -legacy JSON adapter - - - -pr_global->pr_json - - - - - -pr_effect - -effect promotion and count-based rebind - - - -pr_json->pr_effect - - - - - -pi_before - -target AX roots - - - -pi_action - -native action - - - -pi_before->pi_action - - - - - -pi_after - -authoritative root diff - - - -pi_action->pi_after - - - - - -pi_resolve - -refresh modal ownership - - - -pi_after->pi_resolve - - - - - -best_observer - -typed target-root observer - - - -best_record - -shared action record - - - -best_observer->best_record - - - - - -best_resolver - -shared candidate validation - - - -best_observer->best_resolver - - - - - -best_action - -typed actuator outcome - - - -best_action->best_record - - - - - -best_public - -closed result preserves effect - - - -best_record->best_public - - - - - -best_resolver->best_public - - - - - diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index 41fb07baa2..9a38573dab 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -1,123 +1,49 @@ # Post-action surface rediscovery -This document records the implementation boundary for issue -[#2238](https://github.com/trycua/cua/issues/2238). The change -keeps Cua Driver's typed action-result contract while replacing its global -window-count heuristic with target-scoped root observation and shared candidate -validation. +Issue [#2238](https://github.com/trycua/cua/issues/2238) describes a stale-target failure: an action opens a sheet, dialog, popover, or Open/Save panel, but the caller remains bound to the blocked parent window. Cua Driver's old macOS detector compared all desktop windows and appended changes to diagnostic prose. It could neither prove causality nor return a stable rebind address. -![post-action rediscovery architecture before and after this change](diagrams/post-action-surface-rediscovery/architecture-comparison.png) +This implementation resolves the structured-rebind rung. It does not add the issue's later privacy-sensitive desktop-frame fallback or inert-resnapshot backstop. -## Existing Cua Driver flow +![Post-action rediscovery architecture before and after this change](diagrams/post-action-surface-rediscovery/architecture-comparison.png) -Cua Driver already protects foreground focus and detects visible native windows, -but the topology survives only in diagnostic text. +## Sources -## What pull request [#2746](https://github.com/trycua/cua/pull/2746) contributes +The implementation retains Cua Driver's native action routes, focus-suppression leases, shared action record, closed result contract, and harness-owned escalation policy. From [#2746](https://github.com/trycua/cua/pull/2746), it retains typed `window_change` metadata, explicit `rebind` advice, and generated Rust, Python, TypeScript, and manifest bindings. -Pull request [#2746](https://github.com/trycua/cua/pull/2746) adds a closed `window_change` record and explicit `rebind` -escalation. Those are useful public semantics and remain in this implementation. -Its detector, however, still treats every new desktop window as action-related, -selects an exact target from list length alone, and promotes any topology change -to confirmed action effect. +The target-root observer adapts `injaneity/pi-computer-use` commit `022a280a377065c95736cc15f684bf1fad46479e`: snapshot the target accessibility domain before dispatch, run the actuator, poll the same domain for a bounded period, and use appeared roots plus modal or focused state to resolve the next interaction surface. -This implementation preserves from -[#2746](https://github.com/trycua/cua/pull/2746): +Cua Driver adds WindowServer ownership verification at its macOS platform boundary. This keeps AppKit Open/Save panels hosted by a separate service process addressable without treating unrelated desktop changes as action results. -- the portable `window_change` record; -- explicit, non-activating `rebind` advice; -- exact target metadata when one replacement surface is validated; -- generated Rust, Python, TypeScript, and manifest parity; and -- rebind-before-pixel, page, foreground, or desktop recovery guidance. +## Flow -It does not preserve the global-window causality inference, effect promotion, or -branch-local JSON mutation. +```text +macOS action decorator + → target accessibility snapshot + → native actuator + → target accessibility diff + → WindowServer ownership proof + → shared candidate resolver + → typed action record + → closed action result +``` -## What is adapted from pi-computer-use +The observer reports facts; the shared resolver chooses policy; the action record accounts for actuator effect. A surface change cannot promote `effect` to `confirmed` because confirmation still requires value readback. -The target-root observer adapts the architecture proven in -`injaneity/pi-computer-use` commit -`022a280a377065c95736cc15f684bf1fad46479e`: +An exact rebind requires one owner-verified candidate that is modal or focused. Otherwise the result retains candidates without inventing a target, and the caller can correlate them with one fresh `list_windows` call. Rebinding itself never activates, raises, captures, or sends input to the new surface. -- capture accessibility roots for the target process before and after an action; -- treat the accessibility snapshot diff as authoritative; -- use accessibility notifications, focus state, and cheap native-window polling - only to end the bounded wait early; -- report appeared, closed, and focused roots with modality facts; and -- refresh target resolution before selecting a visible modal surface. +Windows and Linux currently omit topology instead of substituting an unscoped desktop heuristic. -The design is adapted rather than copied verbatim. Cua Driver keeps its Rust -platform boundary, focus-suppression leases, action execution record, closed -contract vocabulary, and explicit harness-owned escalation policy. +## Removed path -## Combined architecture - -Detection answers what changed. Resolution answers which surface is safe to -bind. Action accounting answers what the actuator proved. None substitutes for -another. - -### Platform observer - -Each platform adapter owns native root discovery and never chooses rebind -policy. macOS diffs target-process accessibility windows and their sheet, -dialog, and popover children, then verifies each mapped CGWindow owner. This -keeps AppKit's out-of-process Open/Save panel service addressable without -scanning unrelated desktop changes. Windows and Linux currently omit topology instead -of substituting an unscoped desktop heuristic; equivalent UIA or AT-SPI -observers can later feed the same core resolver. - -A cross-application handoff becomes exact only when it appears in the target's -AX roots and WindowServer independently verifies the mapped owner. Temporal -proximity alone never creates a candidate. - -### Shared action coordinator - -One macOS action decorator starts observation immediately before dispatch and -finishes it afterward. The core dispatch seam attaches its typed delta to the -action execution record, including partial and failed delivery. Platform tools -do not serialize topology to JSON or ask the legacy adapter to parse it back. - -### Shared resolver - -An exact rebind requires exactly one eligible candidate after validation. The -candidate must be a target-owned blocking modal, a newly focused target-owned -root, or a separately verified cross-application handoff. Otherwise the result -retains candidates and asks the harness to correlate them with `list_windows`. - -Logical rebinding never activates, raises, captures, or sends input to the new -surface. The caller refreshes window-scoped state and restarts with background -delivery. +The implementation removes the global `WindowChangeDetector`, per-tool diagnostic suffix mutations, the dead observation-skip hook, topology transport through legacy JSON, and `window_change` as duplicate effect evidence. One macOS action decorator now owns observation for every affected action tool. ## Contract invariants -- `window_change` is independent from `effect`; topology cannot promote an - unverifiable, partial, suspected-noop, or refused action to confirmed. -- an exact escalation target must also appear in the accompanying topology. -- unrelated desktop windows cannot become target-owned candidates. -- an empty or unchanged delta cannot produce rebind advice. -- partial and failed delivery retain observed topology internally; successful - public action results expose it through the closed contract. -- platform adapters report limitations explicitly instead of substituting a - global desktop heuristic. - -## Recovery ladder - -Use an exact validated target when present. Otherwise correlate the candidates -with `list_windows`; use one request-scoped, privacy-sensitive desktop frame -only when structured correlation fails. After recovery, refresh window-scoped -state and resume background semantic actions. The fallback must not silently -widen persistent capture scope. - -## Validation plan - -Before the pull request is made ready: - -1. contract tests must reject inconsistent rebind targets and effect promotion; -2. shared resolver tests must cover one modal, several candidates, unrelated - windows, cross-app handoff, partial delivery, and malformed producer data; -3. macOS observer tests must cover AX-only sheets and delayed root appearance; -4. the TextEdit Open-panel harness case must consume structured topology, - rebind without activation, and prove the user's foreground app is unchanged; -5. Windows and Linux receive focused contract coverage plus either native - behavior evidence or an explicit limitation; and -6. the complete canonical macOS harness runs once on the stable candidate SHA. +- `window_change` is independent from actuator effect. +- an exact escalation target also appears in `window_change.new_windows`. +- unrelated foreground or desktop changes do not create a surface delta. +- ambiguous candidates never produce an exact target. +- partial and failed delivery retain observed topology internally. +- logical rebinding does not change foreground focus or z-order. + +The canonical TextEdit Open-panel case exercises the foreign panel-service owner, exact rebound addressability, unchanged foreground and z-order, stable cursor position, and input isolation. The complete macOS Lume matrix remains the readiness gate for the final candidate SHA. diff --git a/libs/cua-driver/python/src/cua_driver/__init__.py b/libs/cua-driver/python/src/cua_driver/__init__.py index 8e06329e73..65477f52e6 100644 --- a/libs/cua-driver/python/src/cua_driver/__init__.py +++ b/libs/cua-driver/python/src/cua_driver/__init__.py @@ -58,7 +58,6 @@ ActionEvidenceKind, ActionResult, ActionRoute, - ActionSurfaceKind, ActionWindowChange, ActionWindowTarget, BoundsExpectation, @@ -183,7 +182,6 @@ def _create_private_worker_python_sdk(cls, options): "ActionEvidenceKind", "ActionResult", "ActionRoute", - "ActionSurfaceKind", "ActionWindowChange", "ActionWindowTarget", "BoundsExpectation", diff --git a/libs/cua-driver/python/src/cua_driver/_native_contract.py b/libs/cua-driver/python/src/cua_driver/_native_contract.py index ced7ab90c1..6ee4e2093f 100644 --- a/libs/cua-driver/python/src/cua_driver/_native_contract.py +++ b/libs/cua-driver/python/src/cua_driver/_native_contract.py @@ -1078,101 +1078,22 @@ def lower(value): builder.write(value.encode("utf-8")) return builder.finalize() - - - - - -class ActionSurfaceKind(enum.Enum): - - WINDOW = 0 - - DIALOG = 1 - - SHEET = 2 - - POPOVER = 3 - - - -class _UniffiFfiConverterTypeActionSurfaceKind(_UniffiConverterRustBuffer): - @staticmethod - def read(buf): - variant = buf.read_i32() - if variant == 1: - return ActionSurfaceKind.WINDOW - if variant == 2: - return ActionSurfaceKind.DIALOG - if variant == 3: - return ActionSurfaceKind.SHEET - if variant == 4: - return ActionSurfaceKind.POPOVER - raise InternalError("Raw enum value doesn't match any cases") - - @staticmethod - def check_lower(value): - if value == ActionSurfaceKind.WINDOW: - return - if value == ActionSurfaceKind.DIALOG: - return - if value == ActionSurfaceKind.SHEET: - return - if value == ActionSurfaceKind.POPOVER: - return - raise ValueError(value) - - @staticmethod - def write(value, buf): - if value == ActionSurfaceKind.WINDOW: - buf.write_i32(1) - if value == ActionSurfaceKind.DIALOG: - buf.write_i32(2) - if value == ActionSurfaceKind.SHEET: - buf.write_i32(3) - if value == ActionSurfaceKind.POPOVER: - buf.write_i32(4) - - - -class _UniffiFfiConverterBoolean: - @classmethod - def check_lower(cls, value): - return not not value - - @classmethod - def lower(cls, value): - return 1 if value else 0 - - @staticmethod - def lift(value): - return value != 0 - - @classmethod - def read(cls, buf): - return cls.lift(buf.read_u8()) - - @classmethod - def write(cls, value, buf): - buf.write_u8(value) - @dataclass class ActionWindowTarget: """ A target-owned native interaction root discovered after an action. """ - def __init__(self, *, pid:int, window_id:int, app_name:str, title:str, kind:ActionSurfaceKind, modal:bool): + def __init__(self, *, pid:int, window_id:int, app_name:str, title:str): self.pid = pid self.window_id = window_id self.app_name = app_name self.title = title - self.kind = kind - self.modal = modal def __str__(self): - return "ActionWindowTarget(pid={}, window_id={}, app_name={}, title={}, kind={}, modal={})".format(self.pid, self.window_id, self.app_name, self.title, self.kind, self.modal) + return "ActionWindowTarget(pid={}, window_id={}, app_name={}, title={})".format(self.pid, self.window_id, self.app_name, self.title) def __eq__(self, other): if self.pid != other.pid: return False @@ -1182,10 +1103,6 @@ def __eq__(self, other): return False if self.title != other.title: return False - if self.kind != other.kind: - return False - if self.modal != other.modal: - return False return True class _UniffiFfiConverterTypeActionWindowTarget(_UniffiConverterRustBuffer): @@ -1196,8 +1113,6 @@ def read(buf): window_id=_UniffiFfiConverterUInt64.read(buf), app_name=_UniffiFfiConverterString.read(buf), title=_UniffiFfiConverterString.read(buf), - kind=_UniffiFfiConverterTypeActionSurfaceKind.read(buf), - modal=_UniffiFfiConverterBoolean.read(buf), ) @staticmethod @@ -1206,8 +1121,6 @@ def check_lower(value): _UniffiFfiConverterUInt64.check_lower(value.window_id) _UniffiFfiConverterString.check_lower(value.app_name) _UniffiFfiConverterString.check_lower(value.title) - _UniffiFfiConverterTypeActionSurfaceKind.check_lower(value.kind) - _UniffiFfiConverterBoolean.check_lower(value.modal) @staticmethod def write(value, buf): @@ -1215,8 +1128,6 @@ def write(value, buf): _UniffiFfiConverterUInt64.write(value.window_id, buf) _UniffiFfiConverterString.write(value.app_name, buf) _UniffiFfiConverterString.write(value.title, buf) - _UniffiFfiConverterTypeActionSurfaceKind.write(value.kind, buf) - _UniffiFfiConverterBoolean.write(value.modal, buf) class _UniffiFfiConverterOptionalTypeActionWindowTarget(_UniffiConverterRustBuffer): @classmethod @@ -1579,6 +1490,27 @@ def read(cls, buf): _UniffiFfiConverterTypeActionWindowTarget.read(buf) for i in range(count) ] +class _UniffiFfiConverterBoolean: + @classmethod + def check_lower(cls, value): + return not not value + + @classmethod + def lower(cls, value): + return 1 if value else 0 + + @staticmethod + def lift(value): + return value != 0 + + @classmethod + def read(cls, buf): + return cls.lift(buf.read_u8()) + + @classmethod + def write(cls, value, buf): + buf.write_u8(value) + @dataclass class ActionWindowChange: """ @@ -5556,7 +5488,6 @@ def write(value, buf): "ActionDeliveryMode", "ActionEscalationTarget", "ActionEscalationReason", - "ActionSurfaceKind", "ActionEvidenceKind", "ActionEffect", "ActionRoute", diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs index dde4e312ad..506bd1b6db 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/lib.rs @@ -39,14 +39,13 @@ pub use inputs::{ pub use outputs::{ advertised_output_schema, refusal_envelope_schema, ActionDelivery, ActionDeliveryMode, ActionEffect, ActionEscalation, ActionEscalationReason, ActionEscalationTarget, ActionEvidence, - ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, ActionSurfaceKind, - ActionWindowChange, ActionWindowTarget, ClipboardReadOutput, ClipboardWriteOutput, - CursorMotionOutput, CursorPointOutput, CursorPositionOutput, CursorThemeOutput, - CursorVisualOutput, DesktopStateOutput, EffectiveScope, EndSessionOutput, - GetAgentCursorStateOutput, ListSessionsOutput, ScreenSizeOutput, SessionClientKindOutput, - SessionLifecycleState, SessionOutput, SessionStateOutput, SessionTransportOutput, - SetAgentCursorEnabledOutput, SetAgentCursorMotionOutput, SetAgentCursorThemeOutput, - StartSessionOutput, ToolOutput, + ActionEvidenceKind, ActionResult, ActionResultValidationError, ActionRoute, ActionWindowChange, + ActionWindowTarget, ClipboardReadOutput, ClipboardWriteOutput, CursorMotionOutput, + CursorPointOutput, CursorPositionOutput, CursorThemeOutput, CursorVisualOutput, + DesktopStateOutput, EffectiveScope, EndSessionOutput, GetAgentCursorStateOutput, + ListSessionsOutput, ScreenSizeOutput, SessionClientKindOutput, SessionLifecycleState, + SessionOutput, SessionStateOutput, SessionTransportOutput, SetAgentCursorEnabledOutput, + SetAgentCursorMotionOutput, SetAgentCursorThemeOutput, StartSessionOutput, ToolOutput, }; pub use verification::{ BoundsExpectation, ElementPredicate, ElementSelector, PredicateOutcome, StatePredicate, diff --git a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs index fb209d3c1c..a00fd5baf6 100644 --- a/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs +++ b/libs/cua-driver/rust/crates/cua-driver-contract/src/outputs.rs @@ -449,15 +449,6 @@ pub struct ActionEvidence { pub kind: ActionEvidenceKind, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Enum)] -#[serde(rename_all = "snake_case")] -pub enum ActionSurfaceKind { - Window, - Dialog, - Sheet, - Popover, -} - /// A target-owned native interaction root discovered after an action. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, uniffi::Record)] #[serde(deny_unknown_fields)] @@ -466,8 +457,6 @@ pub struct ActionWindowTarget { pub window_id: u64, pub app_name: String, pub title: String, - pub kind: ActionSurfaceKind, - pub modal: bool, } /// Read-only topology observed for the action's target process. @@ -789,11 +778,7 @@ mod tests { assert_eq!(target["additionalProperties"], false); assert_eq!( target["required"], - json!(["pid", "window_id", "app_name", "title", "kind", "modal"]) - ); - assert_eq!( - target["properties"]["kind"]["enum"], - json!(["window", "dialog", "sheet", "popover"]) + json!(["pid", "window_id", "app_name", "title"]) ); } @@ -906,8 +891,6 @@ mod tests { window_id: 7, app_name: "Editor".into(), title: "Open".into(), - kind: ActionSurfaceKind::Sheet, - modal: true, }; let mut result = confirmed_result(); result.evidence = None; diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs index 1b12522e03..5feaff6371 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/action_record.rs @@ -193,21 +193,12 @@ pub enum EvidenceKind { OperatorObservation, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ActionSurfaceKind { - Window, - Dialog, - Sheet, - Popover, -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct ActionSurfaceTarget { pub pid: i64, pub window_id: u64, pub app_name: String, pub title: String, - pub kind: ActionSurfaceKind, pub modal: bool, } @@ -226,12 +217,6 @@ pub struct ActionSurfaceDelta { pub rebind: Option, } -impl ActionSurfaceDelta { - pub fn changed(&self) -> bool { - self.foreground_changed || !self.new_windows.is_empty() - } -} - /// Apply the same fail-closed exact-target rule to every platform observer. /// Ownership must already be proven by target-scoped native enumeration. pub fn resolve_surface_delta( @@ -257,7 +242,7 @@ pub fn resolve_surface_delta( .iter() .map(|candidate| candidate.target.clone()) .collect(); - if new_windows.is_empty() && !foreground_changed { + if new_windows.is_empty() { return None; } let eligible: Vec = unique @@ -386,9 +371,6 @@ impl ActionExecutionRecord { /// Attach observer facts without changing the actuator's effect or /// overwriting its internal escalation account. pub fn observe_surface_delta(&mut self, delta: ActionSurfaceDelta) { - if !delta.changed() { - return; - } self.surface_delta = Some(delta); } @@ -477,14 +459,10 @@ impl ActionExecutionRecord { escalation: projection .surface_delta .as_ref() - .and_then(|delta| { - delta - .changed() - .then(|| cua_driver_contract::ActionEscalation { - target: cua_driver_contract::ActionEscalationTarget::Rebind, - reason: cua_driver_contract::ActionEscalationReason::SurfaceChanged, - window: delta.rebind.as_ref().map(public_surface_target), - }) + .map(|delta| cua_driver_contract::ActionEscalation { + target: cua_driver_contract::ActionEscalationTarget::Rebind, + reason: cua_driver_contract::ActionEscalationReason::SurfaceChanged, + window: delta.rebind.as_ref().map(public_surface_target), }) .or_else(|| { projection.escalation.map(|escalation| { @@ -674,13 +652,6 @@ fn public_surface_target(target: &ActionSurfaceTarget) -> cua_driver_contract::A window_id: target.window_id, app_name: target.app_name.clone(), title: target.title.clone(), - kind: match target.kind { - ActionSurfaceKind::Window => cua_driver_contract::ActionSurfaceKind::Window, - ActionSurfaceKind::Dialog => cua_driver_contract::ActionSurfaceKind::Dialog, - ActionSurfaceKind::Sheet => cua_driver_contract::ActionSurfaceKind::Sheet, - ActionSurfaceKind::Popover => cua_driver_contract::ActionSurfaceKind::Popover, - }, - modal: target.modal, } } @@ -1365,11 +1336,9 @@ mod tests { window_id: 7, app_name: "Editor".into(), title: "Open".into(), - kind: ActionSurfaceKind::Sheet, modal: true, }; let parent = ActionSurfaceTarget { - kind: ActionSurfaceKind::Window, modal: false, ..target.clone() }; @@ -1381,7 +1350,7 @@ mod tests { focused: false, }, ActionSurfaceCandidate { - target, + target: target.clone(), focused: false, }, ], @@ -1410,6 +1379,27 @@ mod tests { .window_change .and_then(|change| change.new_windows.into_iter().next()) ); + + let second = ActionSurfaceTarget { + window_id: 8, + ..target.clone() + }; + assert!(resolve_surface_delta( + vec![ + ActionSurfaceCandidate { + target, + focused: false, + }, + ActionSurfaceCandidate { + target: second, + focused: true, + }, + ], + false, + ) + .expect("ambiguous surface delta") + .rebind + .is_none()); } #[test] diff --git a/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs b/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs index 4ea6e62481..4384497707 100644 --- a/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs +++ b/libs/cua-driver/rust/crates/cua-driver/tests/installed_app_textedit_macos_test.rs @@ -244,7 +244,6 @@ fn background_open_panel_returns_a_typed_rebind() { panel_pid, pid, "AppKit panel service ownership was not resolved" ); - assert!(candidate["modal"].as_bool().unwrap_or(false)); assert_eq!(opened.structured()["escalation"]["target"], "rebind"); assert_eq!( opened.structured()["escalation"]["window"], diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs index bdcd82fcb4..027363925a 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -11,8 +11,7 @@ use std::time::{Duration, Instant}; use async_trait::async_trait; use core_foundation::base::{CFRelease, CFTypeRef}; use cua_driver_core::action_record::{ - resolve_surface_delta, ActionSurfaceCandidate, ActionSurfaceDelta, ActionSurfaceKind, - ActionSurfaceTarget, + resolve_surface_delta, ActionSurfaceCandidate, ActionSurfaceDelta, ActionSurfaceTarget, }; use cua_driver_core::protocol::ToolResult; use cua_driver_core::tool::{ProtectedResourceOwnership, Tool, ToolDef}; @@ -224,15 +223,6 @@ unsafe fn insert_root( || role == "AXDialog" || subrole.to_ascii_lowercase().contains("dialog") || subrole.to_ascii_lowercase().contains("modal"); - let kind = if role == "AXSheet" { - ActionSurfaceKind::Sheet - } else if role == "AXDialog" || subrole.to_ascii_lowercase().contains("dialog") { - ActionSurfaceKind::Dialog - } else if role == "AXPopover" || subrole.to_ascii_lowercase().contains("popover") { - ActionSurfaceKind::Popover - } else { - ActionSurfaceKind::Window - }; let key = RootKey { window_id: own_window_id, parent_window_id, @@ -246,7 +236,6 @@ unsafe fn insert_root( window_id: u64::from(window_id), app_name: owner_app_name, title, - kind, modal, } }) @@ -280,83 +269,8 @@ fn surface_owner( mod tests { use super::*; - fn root(window_id: u64, modal: bool, focused: bool) -> Root { - let target = ActionSurfaceTarget { - pid: 42, - window_id, - app_name: "Editor".into(), - title: "Dialog".into(), - kind: ActionSurfaceKind::Dialog, - modal, - }; - Root { - target: Some(target), - focused, - } - } - - #[test] - fn exact_rebind_requires_one_validated_candidate() { - let modal = root(7, true, false); - let delta = resolve_surface_delta( - vec![ActionSurfaceCandidate { - target: modal.target.clone().expect("target"), - focused: modal.focused, - }], - false, - ) - .expect("delta"); - assert_eq!( - delta.rebind.as_ref().map(|target| target.window_id), - Some(7) - ); - - let focused = root(8, false, true); - let delta = resolve_surface_delta( - vec![modal, focused] - .into_iter() - .map(|root| ActionSurfaceCandidate { - target: root.target.expect("target"), - focused: root.focused, - }) - .collect(), - false, - ) - .expect("delta"); - assert!(delta.rebind.is_none()); - } - - #[test] - fn verified_panel_service_owner_becomes_the_rebind_pid() { - let windows = vec![crate::windows::WindowInfo { - window_id: 7, - pid: 99, - app_name: "Open and Save Panel Service".into(), - title: "Open".into(), - bounds: crate::windows::WindowBounds { - x: 0.0, - y: 0.0, - width: 640.0, - height: 480.0, - }, - layer: 0, - z_index: 1, - is_on_screen: true, - current_space_id: None, - on_current_space: None, - space_ids: None, - }]; - assert_eq!( - surface_owner(&windows, 42, 7, "TextEdit"), - Some((99, "Open and Save Panel Service".into())) - ); - assert_eq!(surface_owner(&[], 42, 7, "TextEdit"), None); - } - #[test] - fn unrelated_foreground_change_has_no_exact_target() { - let delta = resolve_surface_delta(Vec::new(), true).expect("delta"); - assert!(delta.new_windows.is_empty()); - assert!(delta.rebind.is_none()); + fn unrelated_foreground_change_is_not_a_surface_delta() { + assert_eq!(resolve_surface_delta(Vec::new(), true), None); } } diff --git a/libs/cua-driver/typescript/src/native/cua_driver_contract.ts b/libs/cua-driver/typescript/src/native/cua_driver_contract.ts index 3f489f3750..0a26b4e13e 100644 --- a/libs/cua-driver/typescript/src/native/cua_driver_contract.ts +++ b/libs/cua-driver/typescript/src/native/cua_driver_contract.ts @@ -197,41 +197,6 @@ const stringConverter = (() => { })(); const FfiConverterString = uniffiCreateFfiConverterString(stringConverter); -export enum ActionSurfaceKind { - Window, - Dialog, - Sheet, - Popover -} - -const FfiConverterTypeActionSurfaceKind = (() => { - const ordinalConverter = FfiConverterInt32; - type TypeName = ActionSurfaceKind; - class FFIConverter extends AbstractFfiConverterByteArray { - read(from: RustBuffer): TypeName { - switch (ordinalConverter.read(from)) { - case 1: return ActionSurfaceKind.Window; - case 2: return ActionSurfaceKind.Dialog; - case 3: return ActionSurfaceKind.Sheet; - case 4: return ActionSurfaceKind.Popover; - default: throw new UniffiInternalError.UnexpectedEnumCase(); - } - } - write(value: TypeName, into: RustBuffer): void { - switch (value) { - case ActionSurfaceKind.Window: return ordinalConverter.write(1, into); - case ActionSurfaceKind.Dialog: return ordinalConverter.write(2, into); - case ActionSurfaceKind.Sheet: return ordinalConverter.write(3, into); - case ActionSurfaceKind.Popover: return ordinalConverter.write(4, into); - } - } - allocationSize(value: TypeName): number { - return ordinalConverter.allocationSize(0); - } - } - return new FFIConverter(); -})(); - /** * A target-owned native interaction root discovered after an action. */ @@ -239,9 +204,7 @@ export type ActionWindowTarget = { pid: bigint, windowId: bigint, appName: string, - title: string, - kind: ActionSurfaceKind, - modal: boolean + title: string } /** @@ -268,9 +231,7 @@ const FfiConverterTypeActionWindowTarget = (() => { pid: FfiConverterInt64.read(from), windowId: FfiConverterUInt64.read(from), appName: FfiConverterString.read(from), - title: FfiConverterString.read(from), - kind: FfiConverterTypeActionSurfaceKind.read(from), - modal: FfiConverterBool.read(from) + title: FfiConverterString.read(from) }; } write(value: TypeName, into: RustBuffer): void { @@ -278,16 +239,12 @@ const FfiConverterTypeActionWindowTarget = (() => { FfiConverterUInt64.write(value.windowId, into); FfiConverterString.write(value.appName, into); FfiConverterString.write(value.title, into); - FfiConverterTypeActionSurfaceKind.write(value.kind, into); - FfiConverterBool.write(value.modal, into); } allocationSize(value: TypeName): number { return FfiConverterInt64.allocationSize(value.pid) + FfiConverterUInt64.allocationSize(value.windowId) + FfiConverterString.allocationSize(value.appName) + - FfiConverterString.allocationSize(value.title) + - FfiConverterTypeActionSurfaceKind.allocationSize(value.kind) + - FfiConverterBool.allocationSize(value.modal); + FfiConverterString.allocationSize(value.title); } }; @@ -4020,7 +3977,6 @@ export default Object.freeze({ FfiConverterTypeActionEvidenceKind, FfiConverterTypeActionResult, FfiConverterTypeActionRoute, - FfiConverterTypeActionSurfaceKind, FfiConverterTypeActionTarget, FfiConverterTypeActionWindowChange, FfiConverterTypeActionWindowTarget, From add6a827f7da44faf2ae584e542c092205befaeb Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 23:02:06 +0800 Subject: [PATCH 11/16] perf(cua-driver): use snapshot-authoritative surface polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapt pi-computer-use’s cheap signal and authoritative AX diff while retaining Cua Driver ownership proof, focus suppression, and typed action accounting. --- .../docs/post-action-surface-rediscovery.md | 6 +- .../src/post_action_observer.rs | 218 +++++++++++++----- 2 files changed, 167 insertions(+), 57 deletions(-) diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index 9a38573dab..156deb9663 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -10,9 +10,11 @@ This implementation resolves the structured-rebind rung. It does not add the iss The implementation retains Cua Driver's native action routes, focus-suppression leases, shared action record, closed result contract, and harness-owned escalation policy. From [#2746](https://github.com/trycua/cua/pull/2746), it retains typed `window_change` metadata, explicit `rebind` advice, and generated Rust, Python, TypeScript, and manifest bindings. -The target-root observer adapts `injaneity/pi-computer-use` commit `022a280a377065c95736cc15f684bf1fad46479e`: snapshot the target accessibility domain before dispatch, run the actuator, poll the same domain for a bounded period, and use appeared roots plus modal or focused state to resolve the next interaction surface. +The target-root observer adapts `injaneity/pi-computer-use` commit `022a280a377065c95736cc15f684bf1fad46479e`: snapshot the target accessibility domain before dispatch, use a cheap target-window signature only to end the bounded wait early, then treat one accessibility snapshot diff as authoritative. If the early signal beats the accessibility tree, bounded catch-up retries let it settle. Appeared roots plus modal or focused state resolve the next interaction surface. -Cua Driver adds WindowServer ownership verification at its macOS platform boundary. This keeps AppKit Open/Save panels hosted by a separate service process addressable without treating unrelated desktop changes as action results. +The persistent AXObserver ring is intentionally not copied. That commit established that sheet creation emits no reliable AX notification, so events improve latency for some roots but do not improve correctness. Cua Driver's bounded signature poll preserves the useful early-exit behavior without adding observer lifecycle state. + +Cua Driver adds its existing focus-suppression lease, typed action record, and WindowServer ownership verification at the macOS platform boundary. This keeps AppKit Open/Save panels hosted by a separate service process addressable without treating unrelated desktop changes as action results. `AXSheets` and `AXChildren` are merged because recent macOS versions do not expose sheets consistently through one attribute. ## Flow diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs index 027363925a..60b2ad5fa9 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -5,7 +5,7 @@ //! attaches one typed delta to `ToolResult`. Native notifications and global //! window lists are not used as causality evidence. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant}; use async_trait::async_trait; @@ -19,18 +19,29 @@ use serde_json::Value; use crate::ax::bindings::{ ax_get_window_id, copy_ax_windows, copy_bool_attr, copy_element_array_attr, copy_string_attr, - AXUIElementCreateApplication, AXUIElementRef, AXUIElementSetMessagingTimeout, + element_screen_rect, AXUIElementCreateApplication, AXUIElementRef, + AXUIElementSetMessagingTimeout, }; -const OBSERVATION_TIMEOUT: Duration = Duration::from_millis(1000); -const POLL_INTERVAL: Duration = Duration::from_millis(50); +const OBSERVATION_TIMEOUT: Duration = Duration::from_millis(400); +const POLL_INTERVAL: Duration = Duration::from_millis(30); +const CATCH_UP_INTERVAL: Duration = Duration::from_millis(80); +const CATCH_UP_ATTEMPTS: usize = 3; #[derive(Clone, Debug, Eq, Hash, PartialEq)] -struct RootKey { - window_id: Option, - parent_window_id: Option, - role: String, - subrole: String, +enum RootKey { + Native { + window_id: u32, + role: String, + subrole: String, + }, + Transient { + parent_window_id: Option, + role: String, + subrole: String, + title: String, + frame: Option<[i64; 4]>, + }, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -41,6 +52,12 @@ struct Root { type RootSnapshot = HashMap; +#[derive(Default)] +struct RootObservation { + roots: RootSnapshot, + window_signature: HashSet, +} + pub struct ObservedActionTool { inner: Box, } @@ -128,50 +145,89 @@ impl Tool for ObservedActionTool { fn observe_delta( pid: i32, prior_front: Option, - before: RootSnapshot, + before: RootObservation, ) -> Option { let deadline = Instant::now() + OBSERVATION_TIMEOUT; - loop { - let after = snapshot_roots(pid); - let appeared: Vec<&Root> = after - .iter() - .filter(|(key, _)| !before.contains_key(*key)) - .map(|(_, root)| root) - .collect(); - let foreground_changed = matches!( - (prior_front, crate::apps::frontmost_pid()), - (Some(before), Some(after)) if before != after - ); - if !appeared.is_empty() || foreground_changed { - let candidates = appeared - .into_iter() - .filter_map(|root| { - root.target.clone().map(|target| ActionSurfaceCandidate { - target, - focused: root.focused, - }) - }) - .collect(); - if let Some(delta) = resolve_surface_delta(candidates, foreground_changed) { - return Some(delta); - } + let signaled = loop { + if foreground_changed(prior_front) + || target_window_signature(pid) != before.window_signature + { + break true; } if Instant::now() >= deadline { - return None; + break false; } std::thread::sleep(POLL_INTERVAL); + }; + + let mut delta = diff_roots( + &before.roots, + &snapshot_roots(pid).roots, + foreground_changed(prior_front), + ); + if delta.is_none() && signaled { + for _ in 0..CATCH_UP_ATTEMPTS { + std::thread::sleep(CATCH_UP_INTERVAL); + delta = diff_roots( + &before.roots, + &snapshot_roots(pid).roots, + foreground_changed(prior_front), + ); + if delta.is_some() { + break; + } + } } + delta +} + +fn foreground_changed(prior_front: Option) -> bool { + matches!( + (prior_front, crate::apps::frontmost_pid()), + (Some(before), Some(after)) if before != after + ) +} + +fn diff_roots( + before: &RootSnapshot, + after: &RootSnapshot, + foreground_changed: bool, +) -> Option { + let candidates = after + .iter() + .filter(|(key, _)| !before.contains_key(*key)) + .filter_map(|(_, root)| { + root.target.clone().map(|target| ActionSurfaceCandidate { + target, + focused: root.focused, + }) + }) + .collect(); + resolve_surface_delta(candidates, foreground_changed) } -fn snapshot_roots(pid: i32) -> RootSnapshot { +fn target_window_signature(pid: i32) -> HashSet { + crate::windows::visible_windows() + .into_iter() + .filter(|window| window.pid == pid) + .map(|window| window.window_id) + .collect() +} + +fn snapshot_roots(pid: i32) -> RootObservation { unsafe { let app = AXUIElementCreateApplication(pid); if app.is_null() { - return HashMap::new(); + return RootObservation::default(); } AXUIElementSetMessagingTimeout(app, 0.25); let app_name = crate::apps::get_app_name_for_pid(pid).unwrap_or_default(); let window_server = crate::windows::all_windows(); + let window_signature = window_server + .iter() + .filter(|window| window.pid == pid && window.is_on_screen) + .map(|window| window.window_id) + .collect(); let windows = copy_ax_windows(app); let mut roots = HashMap::new(); for window in windows { @@ -184,24 +240,29 @@ fn snapshot_roots(pid: i32) -> RootSnapshot { &app_name, &window_server, ); - for child in copy_element_array_attr(window, "AXChildren") { - let role = copy_string_attr(child, "AXRole").unwrap_or_default(); - if matches!(role.as_str(), "AXSheet" | "AXDialog" | "AXPopover") { - insert_root( - &mut roots, - child, - parent_window_id, - pid, - &app_name, - &window_server, - ); + for attribute in ["AXSheets", "AXChildren"] { + for child in copy_element_array_attr(window, attribute) { + let role = copy_string_attr(child, "AXRole").unwrap_or_default(); + if matches!(role.as_str(), "AXSheet" | "AXDialog" | "AXPopover") { + insert_root( + &mut roots, + child, + parent_window_id, + pid, + &app_name, + &window_server, + ); + } + CFRelease(child as CFTypeRef); } - CFRelease(child as CFTypeRef); } CFRelease(window as CFTypeRef); } CFRelease(app as CFTypeRef); - roots + RootObservation { + roots, + window_signature, + } } } @@ -223,11 +284,20 @@ unsafe fn insert_root( || role == "AXDialog" || subrole.to_ascii_lowercase().contains("dialog") || subrole.to_ascii_lowercase().contains("modal"); - let key = RootKey { - window_id: own_window_id, - parent_window_id, - role, - subrole, + let key = match own_window_id { + Some(window_id) => RootKey::Native { + window_id, + role, + subrole, + }, + None => RootKey::Transient { + parent_window_id, + role, + subrole, + title: title.clone(), + frame: element_screen_rect(element) + .map(|frame| frame.map(|value| value.round() as i64)), + }, }; let target = effective_window_id.and_then(|window_id| { surface_owner(window_server, pid, window_id, app_name).map(|(owner_pid, owner_app_name)| { @@ -269,6 +339,44 @@ fn surface_owner( mod tests { use super::*; + fn root(window_id: u64, title: &str, modal: bool) -> Root { + Root { + target: Some(ActionSurfaceTarget { + pid: 42, + window_id, + app_name: "Editor".into(), + title: title.into(), + modal, + }), + focused: false, + } + } + + #[test] + fn root_diff_ignores_metadata_changes_and_reports_an_appeared_modal() { + let parent = RootKey::Native { + window_id: 7, + role: "AXWindow".into(), + subrole: "AXStandardWindow".into(), + }; + let before = RootSnapshot::from([(parent.clone(), root(7, "Draft", false))]); + let mut after = RootSnapshot::from([(parent, root(7, "Draft — Edited", false))]); + assert_eq!(diff_roots(&before, &after, false), None); + + let sheet = RootKey::Native { + window_id: 8, + role: "AXSheet".into(), + subrole: String::new(), + }; + after.insert(sheet, root(8, "Open", true)); + assert_eq!( + diff_roots(&before, &after, false) + .and_then(|delta| delta.rebind) + .map(|target| target.window_id), + Some(8) + ); + } + #[test] fn unrelated_foreground_change_is_not_a_surface_delta() { assert_eq!(resolve_surface_delta(Vec::new(), true), None); From 91a68f0128af6f7c6e6cb5a9de4023b3f2e16a9b Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 23:11:24 +0800 Subject: [PATCH 12/16] fix(cua-driver): resolve surface owners after ax discovery --- .../src/post_action_observer.rs | 179 ++++++++++-------- 1 file changed, 100 insertions(+), 79 deletions(-) diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs index 60b2ad5fa9..c8d7999f41 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -44,9 +44,11 @@ enum RootKey { }, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, PartialEq)] struct Root { - target: Option, + window_id: Option, + title: String, + modal: bool, focused: bool, } @@ -160,25 +162,17 @@ fn observe_delta( std::thread::sleep(POLL_INTERVAL); }; - let mut delta = diff_roots( - &before.roots, - &snapshot_roots(pid).roots, - foreground_changed(prior_front), - ); - if delta.is_none() && signaled { + let mut appeared = appeared_roots(&before.roots, &snapshot_roots(pid).roots); + if appeared.is_empty() && signaled { for _ in 0..CATCH_UP_ATTEMPTS { std::thread::sleep(CATCH_UP_INTERVAL); - delta = diff_roots( - &before.roots, - &snapshot_roots(pid).roots, - foreground_changed(prior_front), - ); - if delta.is_some() { + appeared = appeared_roots(&before.roots, &snapshot_roots(pid).roots); + if !appeared.is_empty() { break; } } } - delta + resolve_appeared_roots(pid, &appeared, foreground_changed(prior_front)) } fn foreground_changed(prior_front: Option) -> bool { @@ -188,22 +182,62 @@ fn foreground_changed(prior_front: Option) -> bool { ) } -fn diff_roots( - before: &RootSnapshot, - after: &RootSnapshot, +fn appeared_roots(before: &RootSnapshot, after: &RootSnapshot) -> Vec { + after + .iter() + .filter(|(key, _)| !before.contains_key(*key)) + .map(|(_, root)| root.clone()) + .collect() +} + +fn resolve_appeared_roots( + pid: i32, + roots: &[Root], foreground_changed: bool, ) -> Option { - let candidates = after + if roots.is_empty() { + return None; + } + let app_name = crate::apps::get_app_name_for_pid(pid).unwrap_or_default(); + let mut resolved = resolve_candidates(pid, &app_name, roots, &crate::windows::all_windows()); + for _ in 0..CATCH_UP_ATTEMPTS { + if resolved.len() == roots.len() { + break; + } + std::thread::sleep(CATCH_UP_INTERVAL); + resolved = resolve_candidates(pid, &app_name, roots, &crate::windows::all_windows()); + } + let incomplete = resolved.len() != roots.len(); + let mut delta = resolve_surface_delta(resolved, foreground_changed)?; + if incomplete { + delta.rebind = None; + } + Some(delta) +} + +fn resolve_candidates( + pid: i32, + app_name: &str, + roots: &[Root], + windows: &[crate::windows::WindowInfo], +) -> Vec { + roots .iter() - .filter(|(key, _)| !before.contains_key(*key)) - .filter_map(|(_, root)| { - root.target.clone().map(|target| ActionSurfaceCandidate { - target, + .filter_map(|root| { + let window_id = root.window_id?; + let (owner_pid, owner_app_name) = surface_owner(windows, pid, window_id, app_name)?; + Some(ActionSurfaceCandidate { + target: ActionSurfaceTarget { + pid: i64::from(owner_pid), + window_id: u64::from(window_id), + app_name: owner_app_name, + title: root.title.clone(), + modal: root.modal, + }, focused: root.focused, }) }) - .collect(); - resolve_surface_delta(candidates, foreground_changed) + .collect() } fn target_window_signature(pid: i32) -> HashSet { @@ -221,37 +255,16 @@ fn snapshot_roots(pid: i32) -> RootObservation { return RootObservation::default(); } AXUIElementSetMessagingTimeout(app, 0.25); - let app_name = crate::apps::get_app_name_for_pid(pid).unwrap_or_default(); - let window_server = crate::windows::all_windows(); - let window_signature = window_server - .iter() - .filter(|window| window.pid == pid && window.is_on_screen) - .map(|window| window.window_id) - .collect(); let windows = copy_ax_windows(app); let mut roots = HashMap::new(); for window in windows { let parent_window_id = ax_get_window_id(window); - insert_root( - &mut roots, - window, - parent_window_id, - pid, - &app_name, - &window_server, - ); + insert_root(&mut roots, window, parent_window_id); for attribute in ["AXSheets", "AXChildren"] { for child in copy_element_array_attr(window, attribute) { let role = copy_string_attr(child, "AXRole").unwrap_or_default(); if matches!(role.as_str(), "AXSheet" | "AXDialog" | "AXPopover") { - insert_root( - &mut roots, - child, - parent_window_id, - pid, - &app_name, - &window_server, - ); + insert_root(&mut roots, child, parent_window_id); } CFRelease(child as CFTypeRef); } @@ -261,7 +274,7 @@ fn snapshot_roots(pid: i32) -> RootObservation { CFRelease(app as CFTypeRef); RootObservation { roots, - window_signature, + window_signature: target_window_signature(pid), } } } @@ -270,9 +283,6 @@ unsafe fn insert_root( roots: &mut RootSnapshot, element: AXUIElementRef, parent_window_id: Option, - pid: i32, - app_name: &str, - window_server: &[crate::windows::WindowInfo], ) { let role = copy_string_attr(element, "AXRole").unwrap_or_default(); let subrole = copy_string_attr(element, "AXSubrole").unwrap_or_default(); @@ -299,21 +309,12 @@ unsafe fn insert_root( .map(|frame| frame.map(|value| value.round() as i64)), }, }; - let target = effective_window_id.and_then(|window_id| { - surface_owner(window_server, pid, window_id, app_name).map(|(owner_pid, owner_app_name)| { - ActionSurfaceTarget { - pid: i64::from(owner_pid), - window_id: u64::from(window_id), - app_name: owner_app_name, - title, - modal, - } - }) - }); roots.insert( key, Root { - target, + window_id: effective_window_id, + title, + modal, focused: copy_bool_attr(element, "AXFocused").unwrap_or(false), }, ); @@ -339,15 +340,11 @@ fn surface_owner( mod tests { use super::*; - fn root(window_id: u64, title: &str, modal: bool) -> Root { + fn root(window_id: u32, title: &str, modal: bool) -> Root { Root { - target: Some(ActionSurfaceTarget { - pid: 42, - window_id, - app_name: "Editor".into(), - title: title.into(), - modal, - }), + window_id: Some(window_id), + title: title.into(), + modal, focused: false, } } @@ -361,7 +358,7 @@ mod tests { }; let before = RootSnapshot::from([(parent.clone(), root(7, "Draft", false))]); let mut after = RootSnapshot::from([(parent, root(7, "Draft — Edited", false))]); - assert_eq!(diff_roots(&before, &after, false), None); + assert!(appeared_roots(&before, &after).is_empty()); let sheet = RootKey::Native { window_id: 8, @@ -369,12 +366,36 @@ mod tests { subrole: String::new(), }; after.insert(sheet, root(8, "Open", true)); - assert_eq!( - diff_roots(&before, &after, false) - .and_then(|delta| delta.rebind) - .map(|target| target.window_id), - Some(8) - ); + assert_eq!(appeared_roots(&before, &after), vec![root(8, "Open", true)]); + } + + #[test] + fn owner_resolution_can_follow_an_ax_root_without_guessing() { + let roots = vec![root(8, "Open", true)]; + assert!(resolve_candidates(42, "TextEdit", &roots, &[]).is_empty()); + + let windows = vec![crate::windows::WindowInfo { + window_id: 8, + pid: 99, + app_name: "Open and Save Panel Service".into(), + title: "Open".into(), + bounds: crate::windows::WindowBounds { + x: 0.0, + y: 0.0, + width: 640.0, + height: 480.0, + }, + layer: 0, + z_index: 1, + is_on_screen: true, + current_space_id: None, + on_current_space: None, + space_ids: None, + }]; + let candidates = resolve_candidates(42, "TextEdit", &roots, &windows); + assert_eq!(candidates.len(), 1); + assert_eq!(candidates[0].target.pid, 99); + assert_eq!(candidates[0].target.window_id, 8); } #[test] From ad32a9a960b317c8ed49b02d6d9d9d8ab165b38c Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 23:13:27 +0800 Subject: [PATCH 13/16] docs(cua-driver): explain deferred owner resolution --- libs/cua-driver/docs/post-action-surface-rediscovery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/cua-driver/docs/post-action-surface-rediscovery.md b/libs/cua-driver/docs/post-action-surface-rediscovery.md index 156deb9663..c72f85f439 100644 --- a/libs/cua-driver/docs/post-action-surface-rediscovery.md +++ b/libs/cua-driver/docs/post-action-surface-rediscovery.md @@ -14,7 +14,7 @@ The target-root observer adapts `injaneity/pi-computer-use` commit `022a280a3770 The persistent AXObserver ring is intentionally not copied. That commit established that sheet creation emits no reliable AX notification, so events improve latency for some roots but do not improve correctness. Cua Driver's bounded signature poll preserves the useful early-exit behavior without adding observer lifecycle state. -Cua Driver adds its existing focus-suppression lease, typed action record, and WindowServer ownership verification at the macOS platform boundary. This keeps AppKit Open/Save panels hosted by a separate service process addressable without treating unrelated desktop changes as action results. `AXSheets` and `AXChildren` are merged because recent macOS versions do not expose sheets consistently through one attribute. +Cua Driver adds its existing focus-suppression lease, typed action record, and WindowServer ownership verification at the macOS platform boundary. AX discovery finishes before ownership is sampled; unresolved appeared roots retry only ownership, so a foreign panel cannot be discarded because WindowServer lagged the accessibility tree. An incomplete owner set can return known candidates but never an exact target. `AXSheets` and `AXChildren` are merged because recent macOS versions do not expose sheets consistently through one attribute. ## Flow From a7865b5c56fcb3880053b9dc4050841c2c9f6051 Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 23:33:53 +0800 Subject: [PATCH 14/16] refactor(cua-driver): remove redundant observation work --- .../rust/crates/cua-driver-core/src/tool.rs | 6 ++--- .../crates/platform-linux/src/tools/impl_.rs | 11 +++------- .../src/post_action_observer.rs | 22 +++++++++++-------- .../src/tools/get_desktop_state.rs | 4 +--- .../src/tools/get_window_state.rs | 4 +--- .../crates/platform-macos/src/tools/zoom.rs | 3 +-- .../platform-windows/src/tools/impl_.rs | 11 +++------- 7 files changed, 24 insertions(+), 37 deletions(-) diff --git a/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs b/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs index 94332d02ff..a6e49f63a5 100644 --- a/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs +++ b/libs/cua-driver/rust/crates/cua-driver-core/src/tool.rs @@ -1546,11 +1546,9 @@ impl ToolRegistry { ); } } - if let Some(delta) = result.surface_delta.take() { - if let Some(record) = result.action_record.as_mut() { + if let Some(record) = result.action_record.as_mut() { + if let Some(delta) = result.surface_delta.take() { record.observe_surface_delta(delta); - } else { - result.surface_delta = Some(delta); } } if resolved_name == "launch_app" && result.is_error != Some(true) { 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 7713397b29..b9cde3a419 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 @@ -943,10 +943,8 @@ impl Tool for GetWindowStateTool { ToolResult { content, - is_error: None, structured_content: Some(structured), - action_record: None, - surface_delta: None, + ..Default::default() } } Ok(Err(e)) => ToolResult::error(format!("Capture error: {e}")), @@ -6409,10 +6407,8 @@ impl Tool for GetDesktopStateTool { } ToolResult { content, - is_error: None, structured_content: Some(structured), - action_record: None, - surface_delta: None, + ..Default::default() } } Ok(Err(e)) => ToolResult::error(format!("Capture error: {e}")), @@ -7241,8 +7237,7 @@ impl Tool for ZoomTool { "width": w, "height": h, "format": "jpeg", "mime_type": "image/jpeg" })), - action_record: None, - surface_delta: None, + ..Default::default() } } Ok(Err(e)) => ToolResult::error(format!("Zoom failed: {e}")), diff --git a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs index c8d7999f41..5b03224a10 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/post_action_observer.rs @@ -129,7 +129,7 @@ impl Tool for ObservedActionTool { ) }); - let before = tokio::task::spawn_blocking(move || snapshot_roots(pid)) + let before = tokio::task::spawn_blocking(move || begin_observation(pid)) .await .unwrap_or_default(); let mut result = self.inner.invoke(args).await; @@ -162,11 +162,11 @@ fn observe_delta( std::thread::sleep(POLL_INTERVAL); }; - let mut appeared = appeared_roots(&before.roots, &snapshot_roots(pid).roots); + let mut appeared = appeared_roots(&before.roots, &snapshot_roots(pid)); if appeared.is_empty() && signaled { for _ in 0..CATCH_UP_ATTEMPTS { std::thread::sleep(CATCH_UP_INTERVAL); - appeared = appeared_roots(&before.roots, &snapshot_roots(pid).roots); + appeared = appeared_roots(&before.roots, &snapshot_roots(pid)); if !appeared.is_empty() { break; } @@ -240,6 +240,13 @@ fn resolve_candidates( .collect() } +fn begin_observation(pid: i32) -> RootObservation { + RootObservation { + roots: snapshot_roots(pid), + window_signature: target_window_signature(pid), + } +} + fn target_window_signature(pid: i32) -> HashSet { crate::windows::visible_windows() .into_iter() @@ -248,11 +255,11 @@ fn target_window_signature(pid: i32) -> HashSet { .collect() } -fn snapshot_roots(pid: i32) -> RootObservation { +fn snapshot_roots(pid: i32) -> RootSnapshot { unsafe { let app = AXUIElementCreateApplication(pid); if app.is_null() { - return RootObservation::default(); + return RootSnapshot::default(); } AXUIElementSetMessagingTimeout(app, 0.25); let windows = copy_ax_windows(app); @@ -272,10 +279,7 @@ fn snapshot_roots(pid: i32) -> RootObservation { CFRelease(window as CFTypeRef); } CFRelease(app as CFTypeRef); - RootObservation { - roots, - window_signature: target_window_signature(pid), - } + roots } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs index 126b735ad6..1f324581e7 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_desktop_state.rs @@ -125,10 +125,8 @@ impl Tool for GetDesktopStateTool { ToolResult { content, - is_error: None, structured_content: Some(structured), - action_record: None, - surface_delta: None, + ..Default::default() } } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs index 19cc3696f4..a900804ce3 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/get_window_state.rs @@ -649,10 +649,8 @@ impl Tool for GetWindowStateTool { ); ToolResult { content, - is_error: None, structured_content: Some(structured), - action_record: None, - surface_delta: None, + ..Default::default() } } } diff --git a/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs b/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs index 1e22377bb0..67c3a19c12 100644 --- a/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs +++ b/libs/cua-driver/rust/crates/platform-macos/src/tools/zoom.rs @@ -116,8 +116,7 @@ impl Tool for ZoomTool { "width": w, "height": h, "format": "jpeg", "mime_type": "image/jpeg" })), - action_record: None, - surface_delta: None, + ..Default::default() } } Ok(Err(e)) => ToolResult::error(format!("Zoom failed: {e}")), 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 3de83088b0..0743f1aac8 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 @@ -1506,10 +1506,8 @@ impl Tool for GetWindowStateTool { ToolResult { content, - is_error: None, structured_content: Some(structured), - action_record: None, - surface_delta: None, + ..Default::default() } } Err(e) => ToolResult::error(format!("Error: {e}")), @@ -7471,10 +7469,8 @@ impl Tool for GetDesktopStateTool { ToolResult { content, - is_error: None, structured_content: Some(structured), - action_record: None, - surface_delta: None, + ..Default::default() } } } @@ -8426,8 +8422,7 @@ impl Tool for ZoomTool { "width": w, "height": h, "format": "jpeg", "mime_type": "image/jpeg" })), - action_record: None, - surface_delta: None, + ..Default::default() } } Ok(Err(e)) => ToolResult::error(format!("Zoom failed: {e}")), From bf85705d1df3ad23ea696923a32c61101150d27f Mon Sep 17 00:00:00 2001 From: Zane Chee Date: Tue, 25 Aug 2026 23:40:10 +0800 Subject: [PATCH 15/16] docs(cua-driver): keep rediscovery rationale with the code --- .../cua-driver/docs/action-result-contract.md | 4 +- .../architecture-comparison.dot | 43 --------------- .../architecture-comparison.png | Bin 118125 -> 0 bytes .../docs/post-action-surface-rediscovery.md | 51 ------------------ .../src/post_action_observer.rs | 2 + 5 files changed, 3 insertions(+), 97 deletions(-) delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot delete mode 100644 libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.png delete mode 100644 libs/cua-driver/docs/post-action-surface-rediscovery.md diff --git a/libs/cua-driver/docs/action-result-contract.md b/libs/cua-driver/docs/action-result-contract.md index 33551ad63b..2084d0a13e 100644 --- a/libs/cua-driver/docs/action-result-contract.md +++ b/libs/cua-driver/docs/action-result-contract.md @@ -8,9 +8,7 @@ Cua Driver 0.15 separates two facts that earlier releases mixed together: `satisfied`, `unsatisfied`, or `unknown`. The driver reports these facts. The agent harness owns task meaning, visual -reading, stop/retry decisions, and movement through the action ladder. The -post-action observer and rebind boundary are specified in -[`post-action-surface-rediscovery.md`](post-action-surface-rediscovery.md). +reading, stop/retry decisions, and movement through the action ladder. ## MCP action result diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot deleted file mode 100644 index a11d630f0e..0000000000 --- a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.dot +++ /dev/null @@ -1,43 +0,0 @@ -digraph rediscovery { - graph [rankdir=TB, bgcolor="#ffffff", fontcolor="#1f2328", pad="0.2", nodesep="0.25", ranksep="0.35", compound=true]; - node [shape=box, style="rounded,filled", fillcolor="#f6f8fa", color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=11, margin="0.12,0.08"]; - edge [color="#57606a", fontcolor="#1f2328", fontname="Arial", fontsize=9, arrowsize=0.65]; - - subgraph cluster_current { - label="before this PR"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; - current_before [label="global window ids"]; - current_action [label="native action"]; - current_prose [label="diagnostic prose only"]; - current_before -> current_action -> current_prose; - } - - subgraph cluster_2746 { - label="#2746"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; - pr_global [label="global window diff"]; - pr_json [label="legacy JSON adapter"]; - pr_effect [label="effect promotion and count-based rebind"]; - pr_global -> pr_json -> pr_effect; - } - - subgraph cluster_picu { - label="pi-computer-use"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; - pi_before [label="target AX roots"]; - pi_action [label="native action"]; - pi_after [label="authoritative root diff"]; - pi_resolve [label="refresh modal ownership"]; - pi_before -> pi_action -> pi_after -> pi_resolve; - } - - subgraph cluster_combined { - label="this PR"; fontcolor="#1f2328"; color="#8c959f"; style="dashed"; - best_observer [label="typed target-root observer"]; - best_action [label="typed actuator outcome"]; - best_record [label="shared action record"]; - best_resolver [label="shared candidate validation"]; - best_public [label="closed result preserves effect"]; - best_observer -> best_record; - best_action -> best_record; - best_observer -> best_resolver -> best_public; - best_record -> best_public; - } -} diff --git a/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.png b/libs/cua-driver/docs/diagrams/post-action-surface-rediscovery/architecture-comparison.png deleted file mode 100644 index 896de0cabf9123f6fd1f029c5aca05e92fc8fdf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118125 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