mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
fix(computer-use): remove per-app approval prompts
This commit is contained in:
+33
-1
@@ -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)
|
||||
|
||||
@@ -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
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user