fix(computer-use): align Windows virtual cursor with macOS

This commit is contained in:
Relakkes Yang
2026-09-04 23:35:10 +08:00
parent e28d0a6534
commit d93d8248c9
12 changed files with 1224 additions and 328 deletions
+115 -27
View File
@@ -246,6 +246,28 @@ class TestMutatingCommandsAreGuarded(unittest.TestCase):
self.assertNotIn("key", coords)
self.assertNotIn("type", coords)
def test_pointer_motion_uses_the_animation_gate(self):
"""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)
dispatcher = source.index("def main()")
for command in ("click", "drag", "move_mouse", "scroll"):
marker = f'if command == "{command}":'
start = source.index(marker, dispatcher)
body = source[start:start + 1400]
self.assertIn(
'payload.get("animate", True)',
body,
f"{command} must honor the mouse-animation gate",
)
def test_helper_uses_per_monitor_dpi_coordinates(self):
source = _win_source()
self.assertIn("DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2", source)
self.assertIn("SetProcessDpiAwarenessContext", source)
self.assertIn("GetDpiForMonitor", source)
def test_every_mutating_branch_finalizes_the_lease(self):
"""No mutating branch may answer with a bare json_output.
@@ -438,7 +460,7 @@ class TestDeliveryGuards(unittest.TestCase):
class TestCursorBadge(unittest.TestCase):
"""The Windows badge annotates the real cursor; it does not replace it."""
"""Windows renders the same virtual-cursor language as macOS."""
def test_badge_script_exists(self):
self.assertTrue(CURSOR_BADGE.exists())
@@ -453,16 +475,17 @@ class TestCursorBadge(unittest.TestCase):
source = CURSOR_BADGE.read_text(encoding="utf-8")
tree = ast.parse(source)
create = next(
methods = [
node for node in ast.walk(tree)
if isinstance(node, ast.FunctionDef) and node.name == "create"
)
if isinstance(node, ast.FunctionDef)
and node.name in {"create", "_tick"}
]
# Read the names actually combined into the window's ex-style, not
# merely the ones defined somewhere in the file. A constant can be
# defined and then left out of CreateWindowExW — which is exactly how
# a click-through window quietly becomes a click-eating one.
used = {
n.id for n in ast.walk(create)
n.id for method in methods for n in ast.walk(method)
if isinstance(n, ast.Name)
}
for style in ("WS_EX_LAYERED", "WS_EX_TRANSPARENT",
@@ -473,31 +496,91 @@ class TestCursorBadge(unittest.TestCase):
)
self.assertIn("SW_SHOWNOACTIVATE", used)
def test_badge_does_not_draw_a_second_pointer(self):
"""Windows has one real cursor and SendInput moves it.
Drawing a fake pointer alongside it would show the user two cursors,
one of which is a lie about where the click will land. The macOS design
does not transfer, and the source says so explicitly.
"""
def test_overlay_draws_a_pointer_instead_of_a_text_banner(self):
"""The old 168x30 label was the black bar reported by Windows users."""
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn("annotation", source.lower())
self.assertNotIn("DrawTextW", source)
self.assertIn("ARROW_POINTS", source)
self.assertIn("#0075f2", source.lower())
self.assertIn("GaussianBlur", source)
self.assertIn("UpdateLayeredWindow", source)
def test_badge_fits_the_default_label(self):
tree = ast.parse(CURSOR_BADGE.read_text(encoding="utf-8"))
width_assignment = next(
node for node in tree.body
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id == "BADGE_W"
for target in node.targets)
def test_overlay_uses_the_agent_hotspot_not_a_cursor_offset(self):
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn("HOTSPOT", source)
self.assertNotIn("CURSOR_OFFSET_X", source)
self.assertNotIn("CURSOR_OFFSET_Y", source)
def test_overlay_is_per_monitor_dpi_aware(self):
"""GetCursorPos and window placement must share physical coordinates."""
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn("DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2", source)
self.assertIn("SetProcessDpiAwarenessContext", source)
self.assertIn("GetDpiForMonitor", source)
self.assertIn("GetDpiForWindow", source)
@unittest.skipUnless(IS_WINDOWS, "requires Pillow and the Windows module")
def test_overlay_renders_a_transparent_pointer_frame(self):
spec = importlib.util.spec_from_file_location("win_cursor_badge", CURSOR_BADGE)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
frame = module._render_cursor(1.0, 0.0, None)
self.assertEqual(frame.size, (module.CANVAS_SIZE, module.CANVAS_SIZE))
self.assertEqual(frame.getpixel((0, 0))[3], 0)
hotspot_x, hotspot_y = (round(value) for value in module.HOTSPOT)
hotspot_alpha = max(
frame.getpixel((hotspot_x + dx, hotspot_y + dy))[3]
for dx in range(-1, 2)
for dy in range(-1, 2)
)
self.assertIsInstance(width_assignment.value, ast.Constant)
self.assertGreaterEqual(
width_assignment.value.value,
160,
'the default "Claude is controlling" label must not be clipped',
self.assertGreater(hotspot_alpha, 200)
self.assertLess(
sum(pixel[3] > 8 for pixel in frame.getdata()),
module.CANVAS_SIZE * module.CANVAS_SIZE // 2,
)
@unittest.skipUnless(IS_WINDOWS, "requires Pillow and the Windows module")
def test_overlay_converts_frames_to_premultiplied_bgra(self):
spec = importlib.util.spec_from_file_location("win_cursor_badge", CURSOR_BADGE)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
image = module.Image.new("RGBA", (1, 1), (100, 150, 200, 128))
self.assertEqual(
module._premultiplied_bgra(image),
bytes((100, 75, 50, 128)),
)
def test_overlay_tracks_agent_input_and_not_the_users_pointer(self):
"""A virtual cursor stays at the agent's last point when the user moves."""
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn("WH_MOUSE_LL", source)
self.assertIn("LLMHF_INJECTED", source)
self.assertIn("_agent_position", source)
self.assertNotIn("def _follow_cursor", source)
def test_overlay_and_helper_share_a_session_input_tag(self):
overlay = CURSOR_BADGE.read_text(encoding="utf-8")
helper = _win_source()
env_name = "CC_HAHA_COMPUTER_USE_INPUT_TAG"
self.assertIn(env_name, overlay)
self.assertIn(env_name, helper)
self.assertIn("dwExtraInfo", overlay)
def test_overlay_visibility_is_bound_to_the_controlled_window(self):
"""Never strand the AI pointer over an unrelated covering app."""
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn("targetPid", source)
self.assertIn("WindowFromPoint", source)
self.assertIn("GetForegroundWindow", source)
def test_overlay_readiness_precedes_the_first_action(self):
source = CURSOR_BADGE.read_text(encoding="utf-8")
self.assertIn('print("READY", flush=True)', source)
def test_badge_exits_with_its_parent(self):
"""An orphaned badge is worse than none.
@@ -519,8 +602,7 @@ class TestCursorBadge(unittest.TestCase):
self.assertIs(module.user32.CreateWindowExW.argtypes[3], module.wintypes.DWORD)
self.assertIs(module.user32.DefWindowProcW.restype, module.LRESULT)
for function in (
module.user32.DrawTextW,
module.user32.SetLayeredWindowAttributes,
module.user32.UpdateLayeredWindow,
module.user32.SetWindowPos,
):
self.assertIsNotNone(function.argtypes)
@@ -547,6 +629,8 @@ class TestCursorBadge(unittest.TestCase):
if process.poll() is not None:
assert process.stderr is not None
self.fail(f"badge exited during startup: {process.stderr.read()}")
assert process.stdout is not None
self.assertEqual(process.stdout.readline().strip(), "READY")
assert process.stdin is not None
process.stdin.close()
returncode = process.wait(timeout=5)
@@ -556,6 +640,10 @@ class TestCursorBadge(unittest.TestCase):
if process.poll() is None:
process.kill()
process.wait(timeout=5)
if process.stdout is not None:
process.stdout.close()
if process.stderr is not None:
process.stderr.close()
self.assertEqual(returncode, 0, stderr)
self.assertNotIn("Exception ignored on calling ctypes callback", stderr)