44Actions v1 - click, type, press
55"""
66
7+ import asyncio
78import time
89
910from .browser import AsyncSentienceBrowser , SentienceBrowser
1011from .browser_evaluator import BrowserEvaluator
12+ from .cursor_policy import CursorPolicy , build_human_cursor_path
1113from .models import ActionResult , BBox , Snapshot
1214from .sentience_methods import SentienceMethod
1315from .snapshot import snapshot , snapshot_async
@@ -18,6 +20,7 @@ def click( # noqa: C901
1820 element_id : int ,
1921 use_mouse : bool = True ,
2022 take_snapshot : bool = False ,
23+ cursor_policy : CursorPolicy | None = None ,
2124) -> ActionResult :
2225 """
2326 Click an element by ID using hybrid approach (mouse simulation by default)
@@ -37,6 +40,7 @@ def click( # noqa: C901
3740
3841 start_time = time .time ()
3942 url_before = browser .page .url
43+ cursor_meta : dict | None = None
4044
4145 if use_mouse :
4246 # Hybrid approach: Get element bbox from snapshot, calculate center, use mouse.click()
@@ -52,9 +56,49 @@ def click( # noqa: C901
5256 # Calculate center of element bbox
5357 center_x = element .bbox .x + element .bbox .width / 2
5458 center_y = element .bbox .y + element .bbox .height / 2
55- # Use Playwright's native mouse click for realistic simulation
59+ # Optional: human-like cursor movement (opt-in)
5660 try :
57- browser .page .mouse .click (center_x , center_y )
61+ if cursor_policy is not None and cursor_policy .mode == "human" :
62+ # Best-effort cursor state on browser instance
63+ pos = getattr (browser , "_sentience_cursor_pos" , None )
64+ if not isinstance (pos , tuple ) or len (pos ) != 2 :
65+ try :
66+ vp = browser .page .viewport_size or {}
67+ pos = (
68+ float (vp .get ("width" , 0 )) / 2.0 ,
69+ float (vp .get ("height" , 0 )) / 2.0 ,
70+ )
71+ except Exception :
72+ pos = (0.0 , 0.0 )
73+
74+ cursor_meta = build_human_cursor_path (
75+ start = (float (pos [0 ]), float (pos [1 ])),
76+ target = (float (center_x ), float (center_y )),
77+ policy = cursor_policy ,
78+ )
79+ pts = cursor_meta .get ("path" , [])
80+ steps = int (cursor_meta .get ("steps" ) or max (1 , len (pts )))
81+ duration_ms = int (cursor_meta .get ("duration_ms" ) or 0 )
82+ per_step_s = (
83+ (duration_ms / max (1 , len (pts ))) / 1000.0 if duration_ms > 0 else 0.0
84+ )
85+ for p in pts :
86+ browser .page .mouse .move (float (p ["x" ]), float (p ["y" ]))
87+ if per_step_s > 0 :
88+ time .sleep (per_step_s )
89+ pause_ms = int (cursor_meta .get ("pause_before_click_ms" ) or 0 )
90+ if pause_ms > 0 :
91+ time .sleep (pause_ms / 1000.0 )
92+ browser .page .mouse .click (center_x , center_y )
93+ setattr (
94+ browser , "_sentience_cursor_pos" , (float (center_x ), float (center_y ))
95+ )
96+ else :
97+ # Default behavior (no regression)
98+ browser .page .mouse .click (center_x , center_y )
99+ setattr (
100+ browser , "_sentience_cursor_pos" , (float (center_x ), float (center_y ))
101+ )
58102 success = True
59103 except Exception :
60104 # If navigation happens, mouse.click might fail, but that's OK
@@ -122,6 +166,7 @@ def click( # noqa: C901
122166 outcome = outcome ,
123167 url_changed = url_changed ,
124168 snapshot_after = snapshot_after ,
169+ cursor = cursor_meta ,
125170 error = (
126171 None
127172 if success
@@ -414,6 +459,7 @@ def click_rect(
414459 highlight : bool = True ,
415460 highlight_duration : float = 2.0 ,
416461 take_snapshot : bool = False ,
462+ cursor_policy : CursorPolicy | None = None ,
417463) -> ActionResult :
418464 """
419465 Click at the center of a rectangle using Playwright's native mouse simulation.
@@ -469,6 +515,7 @@ def click_rect(
469515 # Calculate center of rectangle
470516 center_x = x + w / 2
471517 center_y = y + h / 2
518+ cursor_meta : dict | None = None
472519
473520 # Show highlight before clicking (if enabled)
474521 if highlight :
@@ -479,7 +526,35 @@ def click_rect(
479526 # Use Playwright's native mouse click for realistic simulation
480527 # This triggers hover, focus, mousedown, mouseup sequences
481528 try :
529+ if cursor_policy is not None and cursor_policy .mode == "human" :
530+ pos = getattr (browser , "_sentience_cursor_pos" , None )
531+ if not isinstance (pos , tuple ) or len (pos ) != 2 :
532+ try :
533+ vp = browser .page .viewport_size or {}
534+ pos = (float (vp .get ("width" , 0 )) / 2.0 , float (vp .get ("height" , 0 )) / 2.0 )
535+ except Exception :
536+ pos = (0.0 , 0.0 )
537+
538+ cursor_meta = build_human_cursor_path (
539+ start = (float (pos [0 ]), float (pos [1 ])),
540+ target = (float (center_x ), float (center_y )),
541+ policy = cursor_policy ,
542+ )
543+ pts = cursor_meta .get ("path" , [])
544+ duration_ms_move = int (cursor_meta .get ("duration_ms" ) or 0 )
545+ per_step_s = (
546+ (duration_ms_move / max (1 , len (pts ))) / 1000.0 if duration_ms_move > 0 else 0.0
547+ )
548+ for p in pts :
549+ browser .page .mouse .move (float (p ["x" ]), float (p ["y" ]))
550+ if per_step_s > 0 :
551+ time .sleep (per_step_s )
552+ pause_ms = int (cursor_meta .get ("pause_before_click_ms" ) or 0 )
553+ if pause_ms > 0 :
554+ time .sleep (pause_ms / 1000.0 )
555+
482556 browser .page .mouse .click (center_x , center_y )
557+ setattr (browser , "_sentience_cursor_pos" , (float (center_x ), float (center_y )))
483558 success = True
484559 except Exception as e :
485560 success = False
@@ -512,6 +587,7 @@ def click_rect(
512587 outcome = outcome ,
513588 url_changed = url_changed ,
514589 snapshot_after = snapshot_after ,
590+ cursor = cursor_meta ,
515591 error = (
516592 None
517593 if success
@@ -531,6 +607,7 @@ async def click_async(
531607 element_id : int ,
532608 use_mouse : bool = True ,
533609 take_snapshot : bool = False ,
610+ cursor_policy : CursorPolicy | None = None ,
534611) -> ActionResult :
535612 """
536613 Click an element by ID using hybrid approach (async)
@@ -549,6 +626,7 @@ async def click_async(
549626
550627 start_time = time .time ()
551628 url_before = browser .page .url
629+ cursor_meta : dict | None = None
552630
553631 if use_mouse :
554632 try :
@@ -563,7 +641,44 @@ async def click_async(
563641 center_x = element .bbox .x + element .bbox .width / 2
564642 center_y = element .bbox .y + element .bbox .height / 2
565643 try :
566- await browser .page .mouse .click (center_x , center_y )
644+ if cursor_policy is not None and cursor_policy .mode == "human" :
645+ pos = getattr (browser , "_sentience_cursor_pos" , None )
646+ if not isinstance (pos , tuple ) or len (pos ) != 2 :
647+ try :
648+ vp = browser .page .viewport_size or {}
649+ pos = (
650+ float (vp .get ("width" , 0 )) / 2.0 ,
651+ float (vp .get ("height" , 0 )) / 2.0 ,
652+ )
653+ except Exception :
654+ pos = (0.0 , 0.0 )
655+
656+ cursor_meta = build_human_cursor_path (
657+ start = (float (pos [0 ]), float (pos [1 ])),
658+ target = (float (center_x ), float (center_y )),
659+ policy = cursor_policy ,
660+ )
661+ pts = cursor_meta .get ("path" , [])
662+ duration_ms = int (cursor_meta .get ("duration_ms" ) or 0 )
663+ per_step_s = (
664+ (duration_ms / max (1 , len (pts ))) / 1000.0 if duration_ms > 0 else 0.0
665+ )
666+ for p in pts :
667+ await browser .page .mouse .move (float (p ["x" ]), float (p ["y" ]))
668+ if per_step_s > 0 :
669+ await asyncio .sleep (per_step_s )
670+ pause_ms = int (cursor_meta .get ("pause_before_click_ms" ) or 0 )
671+ if pause_ms > 0 :
672+ await asyncio .sleep (pause_ms / 1000.0 )
673+ await browser .page .mouse .click (center_x , center_y )
674+ setattr (
675+ browser , "_sentience_cursor_pos" , (float (center_x ), float (center_y ))
676+ )
677+ else :
678+ await browser .page .mouse .click (center_x , center_y )
679+ setattr (
680+ browser , "_sentience_cursor_pos" , (float (center_x ), float (center_y ))
681+ )
567682 success = True
568683 except Exception :
569684 success = True
@@ -640,6 +755,7 @@ async def click_async(
640755 outcome = outcome ,
641756 url_changed = url_changed ,
642757 snapshot_after = snapshot_after ,
758+ cursor = cursor_meta ,
643759 error = (
644760 None
645761 if success
@@ -922,6 +1038,7 @@ async def click_rect_async(
9221038 highlight : bool = True ,
9231039 highlight_duration : float = 2.0 ,
9241040 take_snapshot : bool = False ,
1041+ cursor_policy : CursorPolicy | None = None ,
9251042) -> ActionResult :
9261043 """
9271044 Click at the center of a rectangle (async)
@@ -968,6 +1085,7 @@ async def click_rect_async(
9681085 # Calculate center of rectangle
9691086 center_x = x + w / 2
9701087 center_y = y + h / 2
1088+ cursor_meta : dict | None = None
9711089
9721090 # Show highlight before clicking
9731091 if highlight :
@@ -976,7 +1094,35 @@ async def click_rect_async(
9761094
9771095 # Use Playwright's native mouse click
9781096 try :
1097+ if cursor_policy is not None and cursor_policy .mode == "human" :
1098+ pos = getattr (browser , "_sentience_cursor_pos" , None )
1099+ if not isinstance (pos , tuple ) or len (pos ) != 2 :
1100+ try :
1101+ vp = browser .page .viewport_size or {}
1102+ pos = (float (vp .get ("width" , 0 )) / 2.0 , float (vp .get ("height" , 0 )) / 2.0 )
1103+ except Exception :
1104+ pos = (0.0 , 0.0 )
1105+
1106+ cursor_meta = build_human_cursor_path (
1107+ start = (float (pos [0 ]), float (pos [1 ])),
1108+ target = (float (center_x ), float (center_y )),
1109+ policy = cursor_policy ,
1110+ )
1111+ pts = cursor_meta .get ("path" , [])
1112+ duration_ms_move = int (cursor_meta .get ("duration_ms" ) or 0 )
1113+ per_step_s = (
1114+ (duration_ms_move / max (1 , len (pts ))) / 1000.0 if duration_ms_move > 0 else 0.0
1115+ )
1116+ for p in pts :
1117+ await browser .page .mouse .move (float (p ["x" ]), float (p ["y" ]))
1118+ if per_step_s > 0 :
1119+ await asyncio .sleep (per_step_s )
1120+ pause_ms = int (cursor_meta .get ("pause_before_click_ms" ) or 0 )
1121+ if pause_ms > 0 :
1122+ await asyncio .sleep (pause_ms / 1000.0 )
1123+
9791124 await browser .page .mouse .click (center_x , center_y )
1125+ setattr (browser , "_sentience_cursor_pos" , (float (center_x ), float (center_y )))
9801126 success = True
9811127 except Exception as e :
9821128 success = False
@@ -1009,6 +1155,7 @@ async def click_rect_async(
10091155 outcome = outcome ,
10101156 url_changed = url_changed ,
10111157 snapshot_after = snapshot_after ,
1158+ cursor = cursor_meta ,
10121159 error = (
10131160 None
10141161 if success
0 commit comments