Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
f98e010
Add check-digit algorithms (Luhn, Verhoeff, Damm, ISO 7064 MOD 97-10)
JE-Chen Jun 22, 2026
390f8ab
Add IMEI and spaced-input Luhn test vectors
JE-Chen Jun 22, 2026
b07b417
Merge pull request #325 from Integration-Automation/feat/checksum-batch
JE-Chen Jun 22, 2026
7139f0d
Add multi-waypoint mouse gestures (move/drag through a polyline)
JE-Chen Jun 22, 2026
cc7cd78
Merge pull request #326 from Integration-Automation/feat/mouse-path-b…
JE-Chen Jun 22, 2026
c4e140d
Add clear-then-type field entry (Playwright fill idiom)
JE-Chen Jun 22, 2026
e3c5042
Merge pull request #327 from Integration-Automation/feat/field-entry-…
JE-Chen Jun 22, 2026
e16f409
Add blocking wait-until-vanish (wait_until_gone / image / text)
JE-Chen Jun 22, 2026
40a4733
Merge pull request #328 from Integration-Automation/feat/wait-gone-batch
JE-Chen Jun 22, 2026
69fee0c
Add hold-key / auto-repeat key input
JE-Chen Jun 22, 2026
95284da
Use pytest.approx + guard list indexing in key_hold tests (S1244/S6466)
JE-Chen Jun 22, 2026
1d25dc9
Merge pull request #329 from Integration-Automation/feat/key-hold-batch
JE-Chen Jun 22, 2026
cc3e2b6
Add relative mouse movement (move by delta)
JE-Chen Jun 22, 2026
e826894
Merge pull request #330 from Integration-Automation/feat/mouse-relati…
JE-Chen Jun 22, 2026
29bfb10
Add wait-for-region-colour (appear/vanish)
JE-Chen Jun 22, 2026
d4dc012
Drop IDE workspace.xml churn from this branch
JE-Chen Jun 22, 2026
a93fa19
Merge pull request #331 from Integration-Automation/feat/wait-color-b…
JE-Chen Jun 22, 2026
008de97
Add Unicode text entry via clipboard (emoji / CJK)
JE-Chen Jun 22, 2026
3cdb9d5
Restore tracked .idea/workspace.xml (keep out of feature diff)
JE-Chen Jun 22, 2026
fcee74e
Merge pull request #332 from Integration-Automation/feat/text-unicode…
JE-Chen Jun 22, 2026
2ed4f86
Add held-modifiers context across an action group
JE-Chen Jun 22, 2026
3be72ea
Merge pull request #333 from Integration-Automation/feat/modifier-sta…
JE-Chen Jun 22, 2026
73994e6
Add anchor ordinal (Nth match) + anchor_locate_all
JE-Chen Jun 22, 2026
649c8b8
Merge pull request #334 from Integration-Automation/feat/anchor-ordin…
JE-Chen Jun 22, 2026
e7f3772
Add grid/table cell addressing by row/col
JE-Chen Jun 22, 2026
fbce852
Merge pull request #335 from Integration-Automation/feat/grid-locator…
JE-Chen Jun 22, 2026
cb5bb71
Add wait-for-window-title regex (appear/vanish)
JE-Chen Jun 22, 2026
7901e62
Merge pull request #336 from Integration-Automation/feat/wait-window-…
JE-Chen Jun 22, 2026
c69c5c3
Add confidence-returning template matching (score / multi-scale / fin…
JE-Chen Jun 22, 2026
71bc398
Fix S1244 float-eq and use np.nonzero in visual_match (Sonar)
JE-Chen Jun 22, 2026
04d5167
Merge pull request #337 from Integration-Automation/feat/visual-match…
JE-Chen Jun 22, 2026
ded7868
Add locate-on-screen-regions-by-colour
JE-Chen Jun 22, 2026
6b0437c
Merge pull request #338 from Integration-Automation/feat/find-color-r…
JE-Chen Jun 22, 2026
c07b67a
Add masked template matching (ignore background via mask/alpha)
JE-Chen Jun 22, 2026
848c638
Merge pull request #339 from Integration-Automation/feat/masked-templ…
JE-Chen Jun 22, 2026
5b17136
Add SSIM structural comparison (perceptual score + changed regions)
JE-Chen Jun 22, 2026
2fde1cb
Merge pull request #340 from Integration-Automation/feat/ssim-compare…
JE-Chen Jun 22, 2026
4066dcc
Add ORB feature matching (rotation/scale/theme-robust template location)
JE-Chen Jun 22, 2026
980a3a3
Merge pull request #341 from Integration-Automation/feat/feature-matc…
JE-Chen Jun 22, 2026
0a36c35
Add edge/contour shape locator (find rectangles/shapes without a temp…
JE-Chen Jun 22, 2026
b15c51d
Merge pull request #342 from Integration-Automation/feat/shape-locato…
JE-Chen Jun 22, 2026
bc14bdb
Add window tiling/layout geometry planner (halves, grids, cascade)
JE-Chen Jun 22, 2026
9941c44
Fix flaky remote-desktop chat test: wait for viewer registration
JE-Chen Jun 22, 2026
c7594bb
Merge pull request #343 from Integration-Automation/feat/window-layou…
JE-Chen Jun 22, 2026
34a9d95
Add multi-window arrangers (grid / cascade) over the layout planner
JE-Chen Jun 22, 2026
9bad843
Merge pull request #344 from Integration-Automation/feat/window-arran…
JE-Chen Jun 22, 2026
87f88d2
Add image preprocessing pipeline for OCR / template matching
JE-Chen Jun 22, 2026
4f9e0d3
Avoid float-equality checks in deskew (Sonar S1244)
JE-Chen Jun 22, 2026
073782f
Merge pull request #345 from Integration-Automation/feat/preprocess-b…
JE-Chen Jun 22, 2026
5208294
Add multi-monitor / virtual-desktop geometry
JE-Chen Jun 22, 2026
7fa14bd
Avoid float-equality in monitor scale assertion (Sonar S1244)
JE-Chen Jun 22, 2026
e9285f7
Merge pull request #346 from Integration-Automation/feat/monitor-layo…
JE-Chen Jun 22, 2026
a90c7c6
Add actionability gate (visible + stable + enabled + not-occluded)
JE-Chen Jun 22, 2026
def2953
Merge pull request #347 from Integration-Automation/feat/actionabilit…
JE-Chen Jun 22, 2026
a49aed5
Add element-box fusion and reading-order for set-of-marks
JE-Chen Jun 22, 2026
a28c95a
Merge pull request #348 from Integration-Automation/feat/element-pars…
JE-Chen Jun 22, 2026
d3b97ea
Add HSV colour-space segmentation (lighting-robust colour location)
JE-Chen Jun 22, 2026
90ac02e
Dedupe RGB loader and region placeholder (Sonar duplication + S1192)
JE-Chen Jun 22, 2026
63e1bfe
Merge pull request #349 from Integration-Automation/feat/hsv-segment-…
JE-Chen Jun 22, 2026
ec097c9
Add MSER text-region detection (find text without OCR)
JE-Chen Jun 22, 2026
c8b7b73
Merge pull request #350 from Integration-Automation/feat/text-regions…
JE-Chen Jun 22, 2026
e888b1d
Add Hough line / grid / separator detection
JE-Chen Jun 23, 2026
97b549b
Merge pull request #351 from Integration-Automation/feat/edge-lines-b…
JE-Chen Jun 23, 2026
6ed423a
Add retrying value assertions (expect.poll with matchers)
JE-Chen Jun 23, 2026
4c9a2b5
Merge pull request #352 from Integration-Automation/feat/expect-poll-…
JE-Chen Jun 23, 2026
3d392f1
Add composable / filtered candidate locators (chained locator)
JE-Chen Jun 23, 2026
a3ed5f8
Merge pull request #353 from Integration-Automation/feat/locator-chai…
JE-Chen Jun 23, 2026
612da2d
Add rich clipboard (CF_HTML build/parse/get/set)
JE-Chen Jun 23, 2026
1037919
Merge pull request #354 from Integration-Automation/feat/rich-clipboa…
JE-Chen Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions README/WHATS_NEW_zh-CN.md

Large diffs are not rendered by default.

180 changes: 180 additions & 0 deletions README/WHATS_NEW_zh-TW.md

Large diffs are not rendered by default.

180 changes: 180 additions & 0 deletions WHATS_NEW.md

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions docs/source/Eng/doc/new_features/v115_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Check-Digit Algorithms
======================

``pii_text`` detects credit-card and IBAN *shapes* by regex and ``data_quality``
does type / range / regex validation, but nothing actually computes or verifies a
*check digit*. This adds the shared arithmetic engine for the four schemes behind
most real-world identifiers — and the primitive that account-number, card, IBAN,
ISBN and EAN validation build on.

Pure standard library (integer arithmetic; the Verhoeff and Damm tables are small
embedded constants). Every function is pure (string in, bool / str out), so it is
fully deterministic in CI.

Headless API
------------

.. code-block:: python

from je_auto_control import (
luhn_validate, luhn_check_digit,
verhoeff_validate, verhoeff_check_digit,
damm_validate, damm_check_digit,
mod97_10_validate, mod97_10_check_digits,
)

luhn_validate("4111111111111111") # True (credit-card / IMEI)
luhn_check_digit("7992739871") # '3' -> 79927398713
verhoeff_validate("2363") # True (catches transpositions)
damm_check_digit("572") # '4'
mod97_10_validate("3214282912345698765432161182") # True (IBAN engine)

- **Luhn** (mod 10): credit cards, IMEI, many national IDs — catches all
single-digit errors and most adjacent transpositions.
- **Verhoeff** and **Damm**: decimal schemes that catch *all* single-digit and
adjacent-transposition errors (stronger than Luhn).
- **ISO 7064 MOD 97-10**: the two-check-digit scheme behind IBAN and similar.

Each scheme exposes ``*_validate(number)`` (does the value incl. its check digit
verify?) and ``*_check_digit`` / ``*_check_digits`` (what digit(s) to append to a
bare payload?). Non-digit characters are ignored, so spaced/grouped input works.

Executor commands
-----------------

``AC_checksum_validate`` takes a ``scheme`` (``luhn`` / ``verhoeff`` / ``damm`` /
``mod97``) plus a ``number`` and returns ``{valid}``; ``AC_checksum_digit`` returns
``{check_digit}`` for a ``partial``. Both are exposed as MCP tools
(``ac_checksum_validate`` / ``ac_checksum_digit``) and as Script Builder commands
under **Data**.
43 changes: 43 additions & 0 deletions docs/source/Eng/doc/new_features/v116_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Multi-Waypoint Mouse Gestures
=============================

``humanize.humanized_path`` and ``tween_drag`` only interpolate a *single*
start → end hop. Real gestures — signatures, marquee / rubber-band selections,
dragging through several drop targets, shape gestures — need an arbitrary chain
of waypoints, with the button optionally held down across the whole path.

:func:`plan_path` is pure point math (reusing the named easings from
``tween_drag``) and is unit-testable on its own; :func:`move_along_path` and
:func:`drag_path` dispatch through an injectable ``sink`` so the gesture is
tested without real input. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import (
plan_path, move_along_path, drag_path, path_easings,
)

# the eased point list through every waypoint (junctions de-duplicated)
plan_path([(100, 100), (400, 150), (400, 500)], per_segment_steps=20)

move_along_path([(100, 100), (400, 150), (400, 500)]) # hover a polyline
drag_path([(50, 50), (300, 50), (300, 300)], button="mouse_left") # L-drag

``plan_path`` interpolates each consecutive pair with ``per_segment_steps`` eased
steps (``easing`` is any name from ``path_easings()`` — ``linear`` /
``ease_in_out_quad`` / ``ease_out_cubic`` / ``ease_in_cubic``) and does not
duplicate the shared junction points. ``move_along_path`` emits move events
through the path; ``drag_path`` presses at the first waypoint, moves through the
whole path, and releases at the last — for multi-stop drags. Both take a ``sink``
override for headless testing.

Executor commands
-----------------

``AC_move_along_path`` and ``AC_drag_path`` take ``waypoints`` (a JSON
``[[x, y], ...]`` list) plus ``easing`` / ``per_segment_steps`` (and ``button``
for the drag). Both are exposed as MCP tools (``ac_move_along_path`` /
``ac_drag_path``) and as Script Builder commands under **Mouse**.
43 changes: 43 additions & 0 deletions docs/source/Eng/doc/new_features/v117_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Clear-Then-Type Field Entry
===========================

Setting a field's value reliably means *clearing* whatever is there first, then
entering the new text — otherwise automation appends to or corrupts the existing
content. The framework has ``write`` (types, but raises on emoji / CJK / chars
outside the layout table) and ``set_clipboard`` / ``hotkey`` separately, but no
single "focus → clear → set value" primitive and no paste strategy for text that
``write`` cannot type. This adds the Playwright ``fill`` idiom.

:func:`plan_field_set` builds the deterministic op-plan (pure, unit-testable);
:func:`set_field_text` dispatches it through an injectable ``sink`` so it is
tested without real input. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import set_field_text, plan_field_set

set_field_text("new value") # select-all, delete, type
set_field_text("café 🚀", paste=True) # via clipboard (Unicode-safe)
set_field_text("appended", clear="none") # no clear, just type
set_field_text("値", paste=True, modifier="command") # macOS

plan_field_set("hi")
# [{'op': 'hotkey', 'keys': ['ctrl', 'a']},
# {'op': 'key', 'key': 'delete'},
# {'op': 'type', 'text': 'hi'}]

``clear`` is ``"select_all"`` (the ``modifier``+A then Delete clear) or
``"none"``. ``paste=True`` enters the text through the clipboard (``modifier``+V)
— the reliable path for Unicode / emoji / CJK that ``write`` cannot type — rather
than typing key by key. ``modifier`` is the platform command key (``"ctrl"``; use
``"command"`` on macOS). An unknown ``clear`` mode raises ``ValueError``.

Executor commands
-----------------

``AC_set_field_text`` takes ``text`` plus ``clear`` / ``paste`` / ``modifier`` and
returns ``{ops, plan}``. It is exposed as the MCP tool ``ac_set_field_text`` and
as a Script Builder command under **Keyboard**.
41 changes: 41 additions & 0 deletions docs/source/Eng/doc/new_features/v118_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Wait Until Gone (Blocking Vanish Waits)
=======================================

``wait_for_image`` / ``wait_for_text`` block until something *appears*, and the
``observer`` fires async callbacks on vanish — but there was no *blocking* "wait
until this spinner / toast / dialog **disappears** then continue" call for an
image or text. ``wait_until_window_closed`` covers windows only. This adds the
missing vanish waits to the ``smart_waits`` family.

The generic :func:`wait_until_gone` takes any predicate, so its loop is
headless-testable without a real screen; the image / text helpers build that
predicate from the locate functions. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import (
wait_until_gone, wait_until_image_gone, wait_until_text_gone,
)

# generic: wait until any predicate has been falsey
wait_until_gone(lambda: spinner_is_visible(), timeout_s=15)

wait_until_image_gone("spinner.png", timeout_s=15) # image left the screen
wait_until_text_gone("Loading...", timeout_s=15) # OCR text disappeared

Each returns a ``WaitOutcome`` (``succeeded`` / ``reason`` / ``elapsed_s`` /
``samples_taken``) — the same result type as the other smart waits. ``gone_for_s``
requires the target to stay absent for that long before succeeding (debounces a
flickering element); ``poll_interval_s`` / ``timeout_s`` bound the loop.

Executor commands
-----------------

``AC_wait_image_gone`` and ``AC_wait_text_gone`` take the target plus
``timeout_s`` / ``poll_interval_s`` / ``gone_for_s`` (and ``detect_threshold`` for
the image) and return the ``WaitOutcome`` dict. Both are exposed as MCP tools
(``ac_wait_image_gone`` / ``ac_wait_text_gone``) and as Script Builder commands
under **Flow**.
39 changes: 39 additions & 0 deletions docs/source/Eng/doc/new_features/v119_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Hold Key / Auto-Repeat
======================

``type_keyboard`` is an instant down+up and ``input_macro.run_sequence`` can
hand-roll a press / wait / release, but there was no primitive for "hold this key
for N seconds" (game movement, hold-to-scroll) or "send it at R presses per
second" (auto-repeat).

:func:`plan_key_hold` builds the deterministic op-plan (pure, unit-testable);
:func:`hold_key` dispatches it through an injectable ``sink`` and ``sleep`` so it
is tested without real input or real waiting. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import hold_key, plan_key_hold

hold_key("key_d", duration_s=1.5) # press, hold 1.5s, release
hold_key("key_down", duration_s=2.0, rate_hz=20) # 40 key events @ 50ms

plan_key_hold("space", 1.0)
# [{'op': 'press', 'key': 'space'},
# {'op': 'wait', 'seconds': 1.0},
# {'op': 'release', 'key': 'space'}]

With ``rate_hz`` unset the key is pressed, held for ``duration_s``, then released.
With ``rate_hz`` set it is sent as ``round(duration_s * rate_hz)`` discrete key
events spaced ``1 / rate_hz`` apart — simulated auto-repeat for movement / scroll
loops. A non-positive duration or rate raises ``ValueError``. ``hold_key`` routes
the ``wait`` steps to ``sleep`` and the key steps to ``sink``, both injectable.

Executor commands
-----------------

``AC_hold_key`` takes ``key`` plus ``duration_s`` and an optional ``rate_hz`` and
returns ``{ops, plan}``. It is exposed as the MCP tool ``ac_hold_key`` and as a
Script Builder command under **Keyboard**.
35 changes: 35 additions & 0 deletions docs/source/Eng/doc/new_features/v120_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Relative Mouse Movement
=======================

The mouse wrapper exposes only absolute ``set_mouse_position`` — there was no
"nudge the pointer by ``(dx, dy)``" (the pynput / PyAutoGUI ``moveRel`` staple),
which relative-pointer / canvas / FPS-style apps and incremental drags need.

:func:`relative_target` is the pure arithmetic (current + delta) and is
unit-testable; :func:`move_mouse_relative` reads the live position and sets the
new one, with both the getter and setter injectable so it is tested without a
real pointer. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import move_mouse_relative, relative_target

move_mouse_relative(-40, 12) # nudge left 40, down 12 from where it is
# {'from': [200, 200], 'to': [160, 212], 'delta': [-40, 12]}

relative_target((100, 100), 10, -5) # (110, 95) — pure, no I/O

``move_mouse_relative`` reads the current position (raising
``AutoControlMouseException`` if it cannot), adds the delta, and moves there.
``get_position`` / ``set_position`` default to the real mouse wrapper but are
injectable for headless tests.

Executor commands
-----------------

``AC_move_mouse_relative`` takes ``dx`` / ``dy`` and returns ``{from, to,
delta}``. It is exposed as the MCP tool ``ac_move_mouse_relative`` and as a
Script Builder command under **Mouse**.
39 changes: 39 additions & 0 deletions docs/source/Eng/doc/new_features/v121_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Wait for Region Colour
======================

``wait_for_pixel`` matches a single point exactly and ``wait_until_pixel_changes``
detects *any* change at one point — neither answers "wait until this status light
turns green", "until the progress bar is mostly filled", or "until the red error
banner is gone". This adds a region-colour wait to the ``smart_waits`` family.

The pixel counting is a pure helper and :func:`wait_until_color` takes an
injectable ``sampler``, so the loop is headless-testable without a real screen.
Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import wait_until_color

# wait until ≥ 60% of the region is (near) green
wait_until_color(region=[10, 10, 210, 40], target_rgb=[0, 200, 0],
tolerance=15, min_fraction=0.6, timeout_s=20)

# wait until a red banner disappears
wait_until_color(region=[0, 0, 800, 60], target_rgb=[200, 0, 0],
present=False, timeout_s=10)

Pixels within ``tolerance`` (per channel) of ``target_rgb`` are counted. With
``present=True`` the wait succeeds once that fraction reaches ``min_fraction``;
with ``present=False`` once it drops below it. The result is a ``WaitOutcome``
(``succeeded`` / ``reason`` / ``elapsed_s`` / ``samples_taken``).

Executor commands
-----------------

``AC_wait_color`` takes ``target_rgb`` (and optional ``region``) as JSON arrays
plus ``tolerance`` / ``min_fraction`` / ``present`` / ``timeout_s`` /
``poll_interval_s``, and returns the ``WaitOutcome`` dict. It is exposed as the
MCP tool ``ac_wait_color`` and as a Script Builder command under **Flow**.
41 changes: 41 additions & 0 deletions docs/source/Eng/doc/new_features/v122_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Unicode Text Entry (Emoji / CJK)
================================

``write`` types through the platform virtual-key table and *raises* on any
character outside it — emoji, CJK, many accented letters — so non-ASCII text entry
was impossible through the normal path. The reliable, cross-platform way to enter
arbitrary Unicode is to put it on the clipboard and paste it.

:func:`plan_paste` builds the deterministic op-plan and :func:`unicode_code_units`
splits text into UTF-16 code units (for a backend that can do
``KEYEVENTF_UNICODE``); both are pure and unit-testable. :func:`type_unicode`
dispatches the paste plan through an injectable ``sink`` so it is tested without
touching the real clipboard. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import type_unicode, plan_paste, unicode_code_units

type_unicode("café 🚀 値") # clipboard set + Ctrl+V
type_unicode("値", modifier="command") # macOS

unicode_code_units("🚀") # [0xD83D, 0xDE80] (surrogate pair)
plan_paste("hi")
# [{'op': 'set_clipboard', 'text': 'hi'},
# {'op': 'hotkey', 'keys': ['ctrl', 'v']}]

``type_unicode`` sets the clipboard to the text and sends the paste hotkey
(``modifier`` defaults to ``"ctrl"``; use ``"command"`` on macOS), so it enters
*any* text regardless of keyboard layout — emoji, CJK, RTL, accented. It returns
the dispatched plan plus the UTF-16 code-unit count. ``unicode_code_units`` is
provided for backends that want to inject code units directly.

Executor commands
-----------------

``AC_type_unicode`` takes ``text`` plus an optional ``modifier`` and returns
``{ops, plan, code_units}``. It is exposed as the MCP tool ``ac_type_unicode`` and
as a Script Builder command under **Keyboard**.
42 changes: 42 additions & 0 deletions docs/source/Eng/doc/new_features/v123_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Held Modifiers Across an Action Group
=====================================

``hotkey`` presses a set of keys and releases them immediately — fine for a
one-shot chord, but there was no way to hold ``ctrl`` (or ``shift``) *down across
several independent actions* (range-select with shift-clicks, ctrl-clicks to
multi-select) and be sure the modifiers are released even if one of those actions
raises.

:func:`plan_with_modifiers` wraps an op-step list with press / release steps and
is pure / unit-testable; :func:`hold_modifiers` is a context manager that presses
on enter and releases (in reverse) on exit — including on exception —
dispatching through an injectable ``sink``. Imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import hold_modifiers, plan_with_modifiers
from je_auto_control import click_mouse

# shift-held range select: every click happens with shift down
with hold_modifiers(["shift"]):
click_mouse("mouse_left", 100, 100)
click_mouse("mouse_left", 100, 300)
# shift is released here — even if a click raised

plan_with_modifiers([{"op": "click"}], ["ctrl", "shift"])
# press ctrl, press shift, click, release shift, release ctrl

Modifiers are pressed in order on entry and released in *reverse* order on exit,
in a ``finally`` block, so a stuck modifier can never leak. ``plan_with_modifiers``
is the pure plan for any op-step list.

Executor commands
-----------------

``AC_with_modifiers`` runs a nested JSON action list while ``modifiers`` (e.g.
``["ctrl"]`` or ``"ctrl+shift"``) are held, releasing them even if an action
fails. It is exposed as the MCP tool ``ac_with_modifiers`` and as a Script
Builder command under **Keyboard**.
Loading
Loading