Skip to content

Commit 92c83d2

Browse files
abrichrclaude
andauthored
fix: add coordinate clamping and drag safety to prevent fail-safe triggers (#74)
- Add _clamp_pixel_coords() to keep mouse 5px from screen edges - Apply clamping in _translate_click_action (element and coordinate paths) - Fix drag handler: skip drags with None or all-zero coordinates - Apply clamping to drag start/end coordinates Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c340d20 commit 92c83d2

1 file changed

Lines changed: 41 additions & 9 deletions

File tree

  • openadapt_evals/adapters/waa

openadapt_evals/adapters/waa/live.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,24 @@ def _dismiss_notifications(self, requests_module) -> None:
11821182
pass # Best-effort; don't fail reset if notification kill fails
11831183
logger.debug("Dismissed system notifications")
11841184

1185+
def _clamp_pixel_coords(self, x: int, y: int) -> tuple[int, int]:
1186+
"""Clamp pixel coordinates to a safe margin from screen edges.
1187+
1188+
Prevents PyAutoGUI fail-safe by keeping the mouse at least 5px from
1189+
any screen corner. If both coordinates are 0, the action would
1190+
target the top-left corner -- the most common fail-safe trigger.
1191+
1192+
Returns:
1193+
Clamped (x, y) tuple.
1194+
"""
1195+
screen_w, screen_h = self._actual_screen_size or (
1196+
self.config.screen_width, self.config.screen_height,
1197+
)
1198+
margin = 5
1199+
x = max(margin, min(x, screen_w - margin))
1200+
y = max(margin, min(y, screen_h - margin))
1201+
return x, y
1202+
11851203
def _translate_action(self, action: BenchmarkAction) -> str | None:
11861204
"""Translate BenchmarkAction to element-based command for WAA's Computer.
11871205
@@ -1242,29 +1260,41 @@ def _translate_action(self, action: BenchmarkAction) -> str | None:
12421260
return f"import pyautogui; pyautogui.scroll({clicks})"
12431261

12441262
if action.type == "drag":
1245-
# Get start position
1246-
start_x, start_y = 0, 0
1263+
# Get start position -- skip drag entirely if no coordinates
1264+
start_x, start_y = None, None
12471265
if action.target_node_id is not None:
12481266
elem_id = str(action.target_node_id)
12491267
if elem_id in self._current_rects:
12501268
rect = self._current_rects[elem_id]
12511269
start_x = (rect[0] + rect[2]) // 2
12521270
start_y = (rect[1] + rect[3]) // 2
1253-
elif action.x is not None and action.y is not None:
1271+
if start_x is None and action.x is not None and action.y is not None:
12541272
screen_w, screen_h = self._actual_screen_size or (self.config.screen_width, self.config.screen_height)
12551273
start_x = action.x if not isinstance(action.x, float) or action.x > 1 else int(action.x * screen_w)
12561274
start_y = action.y if not isinstance(action.y, float) or action.y > 1 else int(action.y * screen_h)
12571275

12581276
# Get end position
12591277
screen_w, screen_h = self._actual_screen_size or (self.config.screen_width, self.config.screen_height)
1260-
end_x = action.end_x or 0
1261-
end_y = action.end_y or 0
1262-
if isinstance(end_x, float) and 0 <= end_x <= 1:
1278+
end_x = action.end_x
1279+
end_y = action.end_y
1280+
if end_x is not None and isinstance(end_x, float) and 0 <= end_x <= 1:
12631281
end_x = int(end_x * screen_w)
1264-
if isinstance(end_y, float) and 0 <= end_y <= 1:
1282+
if end_y is not None and isinstance(end_y, float) and 0 <= end_y <= 1:
12651283
end_y = int(end_y * screen_h)
12661284

1267-
return f"import pyautogui; pyautogui.moveTo({int(start_x)}, {int(start_y)}); pyautogui.drag({int(end_x - start_x)}, {int(end_y - start_y)}, duration=0.5)"
1285+
# Skip drag if coordinates are missing or both are at origin
1286+
if start_x is None or end_x is None or end_y is None:
1287+
logger.warning("Drag action missing coordinates, skipping")
1288+
return "pass # drag skipped: missing coordinates"
1289+
if int(start_x) == 0 and int(start_y) == 0 and int(end_x) == 0 and int(end_y) == 0:
1290+
logger.warning("Drag action has all-zero coordinates, skipping")
1291+
return "pass # drag skipped: all-zero coordinates"
1292+
1293+
# Clamp to safe margin
1294+
start_x, start_y = self._clamp_pixel_coords(int(start_x), int(start_y))
1295+
end_x, end_y = self._clamp_pixel_coords(int(end_x), int(end_y))
1296+
1297+
return f"import pyautogui; pyautogui.moveTo({start_x}, {start_y}); pyautogui.drag({end_x - start_x}, {end_y - start_y}, duration=0.5)"
12681298

12691299
logger.warning(f"Unknown action type: {action.type}")
12701300
return None
@@ -1294,6 +1324,7 @@ def _translate_click_action(self, action: BenchmarkAction, click_method: str) ->
12941324
rect = self._current_rects[elem_id]
12951325
cx = (rect[0] + rect[2]) // 2
12961326
cy = (rect[1] + rect[3]) // 2
1327+
cx, cy = self._clamp_pixel_coords(cx, cy)
12971328
return f"import pyautogui; pyautogui.{pyautogui_method}({cx}, {cy})"
12981329
else:
12991330
logger.warning(f"Element ID '{elem_id}' not found in rects, falling back to coordinates")
@@ -1315,7 +1346,8 @@ def _translate_click_action(self, action: BenchmarkAction, click_method: str) ->
13151346
if isinstance(y, float) and 0 <= y <= 1:
13161347
y = int(y * screen_h)
13171348

1318-
return f"import pyautogui; pyautogui.{pyautogui_method}({int(x)}, {int(y)})"
1349+
x, y = self._clamp_pixel_coords(int(x), int(y))
1350+
return f"import pyautogui; pyautogui.{pyautogui_method}({x}, {y})"
13191351

13201352
def _translate_key_action(self, action: BenchmarkAction) -> str:
13211353
"""Translate key press action using pyautogui (no grounding needed)."""

0 commit comments

Comments
 (0)