fix(computer-use): remove per-app approval prompts

This commit is contained in:
Relakkes Yang
2026-09-05 03:09:15 +08:00
parent d93d8248c9
commit 90a4a11d28
31 changed files with 670 additions and 2395 deletions
+33 -1
View File
@@ -250,7 +250,8 @@ class TestMutatingCommandsAreGuarded(unittest.TestCase):
"""Coordinate actions must not jump while mouse animation is enabled."""
source = _win_source()
self.assertIn("def _move_cursor_to", source)
self.assertIn("1 - (1 - progress) ** 3", source)
self.assertIn("def _spring_cursor_path", source)
self.assertNotIn("1 - (1 - progress) ** 3", source)
dispatcher = source.index("def main()")
for command in ("click", "drag", "move_mouse", "scroll"):
marker = f'if command == "{command}":'
@@ -262,6 +263,28 @@ class TestMutatingCommandsAreGuarded(unittest.TestCase):
f"{command} must honor the mouse-animation gate",
)
@unittest.skipUnless(IS_WINDOWS, "requires the Windows helper module")
def test_spring_cursor_path_starts_smoothly_and_lands_exactly(self):
spec = importlib.util.spec_from_file_location("win_helper_motion", WIN_HELPER)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
points = module._spring_cursor_path(0, 0, 1000, 500)
self.assertGreater(len(points), 12)
self.assertEqual(points[-1], (1000, 500))
first_distance = (points[0][0] ** 2 + points[0][1] ** 2) ** 0.5
total_distance = (1000 ** 2 + 500 ** 2) ** 0.5
self.assertLess(first_distance / total_distance, 0.10)
self.assertTrue(all(a != b for a, b in zip(points, points[1:])))
def test_drag_reuses_the_shared_cursor_motion(self):
source = _win_source()
dispatcher = source.index('if command == "drag":')
body = source[dispatcher:source.index('if command == "move_mouse":', dispatcher)]
self.assertGreaterEqual(body.count("_move_cursor_to("), 2)
self.assertNotIn("for step in range", body)
def test_helper_uses_per_monitor_dpi_coordinates(self):
source = _win_source()
self.assertIn("DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2", source)
@@ -577,6 +600,15 @@ class TestCursorBadge(unittest.TestCase):
self.assertIn("WindowFromPoint", source)
self.assertIn("GetForegroundWindow", source)
def test_overlay_activity_does_not_hide_before_animated_motion(self):
source = CURSOR_BADGE.read_text(encoding="utf-8")
start = source.index(" def _on_activity")
end = source.index(" def _read_parent", start)
body = source[start:end]
self.assertNotIn("ShowWindow", body)
self.assertIn("GetCursorPos", body)
self.assertIn("self._requested_visible = self._target_pid is not None", body)
def test_overlay_readiness_precedes_the_first_action(self):
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn('print("READY", flush=True)', source)
+10 -6
View File
@@ -569,14 +569,18 @@ class VirtualCursorOverlay:
with self._lock:
if point is not None:
# Hide for the target lookup. A click-through layered window can
# still be returned by WindowFromPoint on some Windows builds.
if self.hwnd and self._shown:
user32.ShowWindow(self.hwnd, SW_HIDE)
self._shown = False
# Keep the cursor visible between related actions. Hiding here
# made every movement reappear only after the first injected
# frame, which looked like a jump into the middle of the path.
# `_pid_at_point` already skips this transparent overlay.
self._destination = point
self._target_pid = target_pid or self._pid_at_point(point)
self._requested_visible = False # reveal on injected motion
if self._agent_position is None:
current = POINT()
if user32.GetCursorPos(ctypes.byref(current)):
self._agent_position = (int(current.x), int(current.y))
self._requested_visible = self._target_pid is not None
self._hidden_for_user = False
elif target_pid is not None:
self._target_pid = target_pid
+54 -21
View File
@@ -1196,30 +1196,70 @@ def _absolute_mouse_move(x: int, y: int) -> _INPUT:
)
def _spring_cursor_path(
start_x: int,
start_y: int,
target_x: int,
target_y: int,
) -> list[tuple[int, int]]:
"""Sample the same damped-spring motion used by the macOS cursor."""
distance = ((target_x - start_x) ** 2 + (target_y - start_y) ** 2) ** 0.5
if distance < 2:
return [(target_x, target_y)]
# CursorMotionState.swift uses k=196 and a damping ratio of 0.85. A
# 60-Hz fixed step gives Windows the same zero-velocity start and gentle
# settle while keeping physical-pointer actions bounded.
frame_interval = 1.0 / 60.0
stiffness = 196.0
damping = 2.0 * 0.85 * stiffness ** 0.5
max_duration = min(0.45, max(0.20, distance / 3000.0))
sample_count = max(1, round(max_duration / frame_interval))
pos_x, pos_y = float(start_x), float(start_y)
vel_x = vel_y = 0.0
points: list[tuple[int, int]] = []
for _ in range(sample_count):
vel_x += (stiffness * (target_x - pos_x) - damping * vel_x) * frame_interval
vel_y += (stiffness * (target_y - pos_y) - damping * vel_y) * frame_interval
pos_x += vel_x * frame_interval
pos_y += vel_y * frame_interval
point = (round(pos_x), round(pos_y))
if not points or point != points[-1]:
points.append(point)
remaining = ((target_x - pos_x) ** 2 + (target_y - pos_y) ** 2) ** 0.5
speed = (vel_x ** 2 + vel_y ** 2) ** 0.5
if remaining < 0.5 and speed < 6.0:
break
target = (target_x, target_y)
if not points or points[-1] != target:
points.append(target)
return points
def _move_cursor_to(x: int, y: int, animate: bool) -> None:
"""Move the shared pointer along a short eased path for overlay parity."""
"""Move the shared pointer with the macOS virtual-cursor spring."""
current = wintypes.POINT()
if not _user32.GetCursorPos(ctypes.byref(current)):
_send_inputs([_absolute_mouse_move(x, y)])
return
start_x, start_y = int(current.x), int(current.y)
distance = ((x - start_x) ** 2 + (y - start_y) ** 2) ** 0.5
if not animate or distance < 2:
if not animate:
_send_inputs([_absolute_mouse_move(x, y)])
return
# 100-260ms is long enough to read as motion without slowing the agent.
duration = min(0.26, max(0.10, distance / 4000.0))
steps = max(6, min(18, round(duration * 60)))
for step in range(1, steps + 1):
progress = step / steps
eased = 1 - (1 - progress) ** 3
next_x = round(start_x + (x - start_x) * eased)
next_y = round(start_y + (y - start_y) * eased)
points = _spring_cursor_path(start_x, start_y, x, y)
started = time.perf_counter()
for index, (next_x, next_y) in enumerate(points):
_send_inputs([_absolute_mouse_move(next_x, next_y)])
if step < steps:
time.sleep(duration / steps)
if index < len(points) - 1:
deadline = started + (index + 1) / 60.0
delay = deadline - time.perf_counter()
if delay > 0:
time.sleep(delay)
_VIRTUAL_KEYS = {
@@ -1859,14 +1899,7 @@ def main() -> int:
animate = bool(payload.get("animate", True))
_move_cursor_to(start_x, start_y, animate)
_send_inputs([_mouse_input(MOUSEEVENTF_LEFTDOWN)])
duration = 0.18 if animate else 0
for step in range(1, 13):
_send_inputs([_absolute_mouse_move(
round(start_x + (target_x - start_x) * step / 12),
round(start_y + (target_y - start_y) * step / 12),
)])
if duration and step < 12:
time.sleep(duration / 12)
_move_cursor_to(target_x, target_y, animate)
_send_inputs([_mouse_input(MOUSEEVENTF_LEFTUP)])
return _finish(lease, True)
if command == "move_mouse":