Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions README/WHATS_NEW_zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 本次更新 — AutoControl

## 本次更新 (2026-06-23) — 等待窗口标题(正则)

阻塞直到窗口标题符合正则(或消失)。完整参考:[`docs/source/Zh/doc/new_features/v126_features_doc.rst`](../docs/source/Zh/doc/new_features/v126_features_doc.rst)。

- **`wait_until_window_title`**(`AC_wait_window_title`):`wait_for_window` 以子字符串比对且仅等*出现*;`wait_until_window_closed` 为子字符串消失。本功能默认以正则表达式比对(`regex=False` 改子字符串),并可等待标题消失(`present=False`)——例如等标签页导览至 `r".*— Checkout$"`。标题来源可注入、无头可测。

## 本次更新 (2026-06-23) — 表格 / 网格单元格定位

依(行、列)从单元格边界框定位表格单元格。完整参考:[`docs/source/Zh/doc/new_features/v125_features_doc.rst`](../docs/source/Zh/doc/new_features/v125_features_doc.rst)。
Expand Down
6 changes: 6 additions & 0 deletions README/WHATS_NEW_zh-TW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 本次更新 — AutoControl

## 本次更新 (2026-06-23) — 等待視窗標題(正則)

阻塞直到視窗標題符合正則(或消失)。完整參考:[`docs/source/Zh/doc/new_features/v126_features_doc.rst`](../docs/source/Zh/doc/new_features/v126_features_doc.rst)。

- **`wait_until_window_title`**(`AC_wait_window_title`):`wait_for_window` 以子字串比對且僅等*出現*;`wait_until_window_closed` 為子字串消失。本功能預設以正則表達式比對(`regex=False` 改子字串),並可等待標題消失(`present=False`)——例如等分頁導覽至 `r".*— Checkout$"`。標題來源可注入、無頭可測。

## 本次更新 (2026-06-23) — 表格 / 格線儲存格定位

依(列、欄)從儲存格邊界框定位表格儲存格。完整參考:[`docs/source/Zh/doc/new_features/v125_features_doc.rst`](../docs/source/Zh/doc/new_features/v125_features_doc.rst)。
Expand Down
6 changes: 6 additions & 0 deletions WHATS_NEW.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# What's New — AutoControl

## What's new (2026-06-23) — Wait for Window Title (Regex)

Block until a window title matches a regex (or vanishes). Full reference: [`docs/source/Eng/doc/new_features/v126_features_doc.rst`](docs/source/Eng/doc/new_features/v126_features_doc.rst).

- **`wait_until_window_title`** (`AC_wait_window_title`): `wait_for_window` matches a title substring and only waits for *appear*; `wait_until_window_closed` is substring vanish. This matches a regular expression by default (`regex=False` for substring) and can wait for the title to vanish (`present=False`) — e.g. wait for a tab to navigate to `r".*— Checkout$"`. Injectable title source, headless-testable.

## What's new (2026-06-23) — Grid / Table Cell Addressing

Address a table cell by (row, column) from cell bounding boxes. Full reference: [`docs/source/Eng/doc/new_features/v125_features_doc.rst`](docs/source/Eng/doc/new_features/v125_features_doc.rst).
Expand Down
35 changes: 35 additions & 0 deletions docs/source/Eng/doc/new_features/v126_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Wait for Window Title (Regex)
=============================

``wait_for_window`` matches a window title by *substring* and only waits for it to
*appear*; ``wait_until_window_closed`` is the substring vanish. Neither supports a
regular-expression title or "wait until the active window's title matches P" —
e.g. waiting for a browser tab to finish navigating to ``r".*— Checkout$"``. This
adds a regex title wait to the ``smart_waits`` family.

The title source is injectable, so the loop is headless-testable without real
windows. Imports no ``PySide6``.

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

.. code-block:: python

from je_auto_control import wait_until_window_title

wait_until_window_title(r".*— Checkout$", timeout_s=20) # tab navigated
wait_until_window_title("Updating", present=False) # dialog gone
wait_until_window_title("Checkout", regex=False) # substring mode

By default ``pattern`` is a regular expression (``re.search``); pass
``regex=False`` for a plain substring test. ``present=False`` waits for the title
to *vanish*. The result is a ``WaitOutcome`` (``succeeded`` / ``reason`` /
``elapsed_s`` / ``samples_taken``); ``title_lister`` is injectable for tests.

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

``AC_wait_window_title`` takes ``pattern`` plus ``present`` / ``regex`` /
``timeout_s`` / ``poll_interval_s`` and returns the ``WaitOutcome`` dict. It is
exposed as the MCP tool ``ac_wait_window_title`` and as a Script Builder command
under **Flow**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v123_features_doc
doc/new_features/v124_features_doc
doc/new_features/v125_features_doc
doc/new_features/v126_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
30 changes: 30 additions & 0 deletions docs/source/Zh/doc/new_features/v126_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
等待視窗標題(正則)
==================

``wait_for_window`` 以*子字串*比對視窗標題且僅等待其*出現*;``wait_until_window_closed`` 為子字串消失。兩者都
不支援正則表達式標題或「等到使用中視窗標題符合 P」——例如等待瀏覽器分頁導覽至 ``r".*— Checkout$"``。本功能
為 ``smart_waits`` 家族加入正則標題等待。

標題來源可注入,因此迴圈可在無真實視窗下做無頭測試。不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import wait_until_window_title

wait_until_window_title(r".*— Checkout$", timeout_s=20) # 分頁已導覽
wait_until_window_title("Updating", present=False) # 對話框消失
wait_until_window_title("Checkout", regex=False) # 子字串模式

預設 ``pattern`` 為正則表達式(``re.search``);傳入 ``regex=False`` 改用純子字串比對。``present=False`` 等待
標題*消失*。結果為 ``WaitOutcome``(``succeeded`` / ``reason`` / ``elapsed_s`` / ``samples_taken``);
``title_lister`` 可注入以供測試。

執行器命令
----------

``AC_wait_window_title`` 接受 ``pattern`` 以及 ``present`` / ``regex`` / ``timeout_s`` / ``poll_interval_s``,
並回傳 ``WaitOutcome`` dict。它以 MCP 工具 ``ac_wait_window_title`` 以及 Script Builder 中 **Flow** 分類下的命令
提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v123_features_doc
doc/new_features/v124_features_doc
doc/new_features/v125_features_doc
doc/new_features/v126_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
4 changes: 2 additions & 2 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@
wait_until_file, wait_until_gone, wait_until_image_gone,
wait_until_pixel_changes, wait_until_port, wait_until_process,
wait_until_region_idle, wait_until_screen_stable, wait_until_text_gone,
wait_until_window_closed,
wait_until_window_closed, wait_until_window_title,
)
# Visual regression (golden-image comparison)
from je_auto_control.utils.visual_regression import (
Expand Down Expand Up @@ -1261,7 +1261,7 @@ def start_autocontrol_gui(*args, **kwargs):
"wait_until_clipboard_changes", "wait_until_window_closed",
"wait_until_file", "wait_until_port", "wait_until_process",
"wait_until_gone", "wait_until_image_gone", "wait_until_text_gone",
"wait_until_color",
"wait_until_color", "wait_until_window_title",
# Visual regression + state machine
"take_golden", "compare_to_golden", "image_difference",
"DiffResult", "MaskRegion",
Expand Down
12 changes: 12 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,18 @@ def _add_flow_specs(specs: List[CommandSpec]) -> None:
),
description="Block until a colour fills (or leaves) a screen region.",
))
specs.append(CommandSpec(
"AC_wait_window_title", "Flow", "Wait for Window Title",
fields=(
FieldSpec("pattern", FieldType.STRING, placeholder="Checkout$"),
FieldSpec("present", FieldType.BOOL, optional=True, default=True),
FieldSpec("regex", FieldType.BOOL, optional=True, default=True),
FieldSpec("timeout_s", FieldType.FLOAT, optional=True, default=10.0),
FieldSpec("poll_interval_s", FieldType.FLOAT, optional=True,
default=0.2, min_value=0.01),
),
description="Block until a window title matches a regex (or vanishes).",
))
specs.append(CommandSpec(
"AC_loop", "Flow", "Loop (N times)",
fields=(
Expand Down
12 changes: 12 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,17 @@ def _wait_text_gone(text: str, timeout_s: float = 10.0,
).to_dict()


def _wait_window_title(pattern: str, present: bool = True, regex: bool = True,
timeout_s: float = 10.0,
poll_interval_s: float = 0.2) -> Dict[str, Any]:
"""Executor adapter: wait for a window title (regex) to appear / vanish."""
from je_auto_control.utils.smart_waits import wait_until_window_title
return wait_until_window_title(
pattern, present=bool(present), regex=bool(regex),
timeout_s=float(timeout_s), poll_interval_s=float(poll_interval_s),
).to_dict()


def _wait_color(target_rgb: Any, region: Any = None,
tolerance: int = 10, min_fraction: float = 0.5,
present: bool = True, timeout_s: float = 10.0,
Expand Down Expand Up @@ -5080,6 +5091,7 @@ def __init__(self):
"AC_wait_image_gone": _wait_image_gone,
"AC_wait_text_gone": _wait_text_gone,
"AC_wait_color": _wait_color,
"AC_wait_window_title": _wait_window_title,
"AC_wait_window_closed": _wait_window_closed,

# Cost telemetry (LLM token + USD tracking)
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -1323,6 +1323,21 @@ def smart_wait_tools() -> List[MCPTool]:
handler=h.wait_color,
annotations=READ_ONLY,
),
MCPTool(
name="ac_wait_window_title",
description=("Block until a window title matches 'pattern' (a regex "
"by default; regex=False for substring) — or vanishes "
"with present=False. e.g. r'.*— Checkout$'."),
input_schema=schema({
"pattern": {"type": "string"},
"present": {"type": "boolean"},
"regex": {"type": "boolean"},
"timeout_s": {"type": "number"},
"poll_interval_s": {"type": "number"}},
required=["pattern"]),
handler=h.wait_window_title,
annotations=READ_ONLY,
),
MCPTool(
name="ac_wait_screen_stable",
description=("Block until the screen stops moving (consecutive "
Expand Down
7 changes: 7 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2465,6 +2465,13 @@ def wait_color(target_rgb, region=None, tolerance=10, min_fraction=0.5,
present, timeout_s, poll_interval_s)


def wait_window_title(pattern, present=True, regex=True, timeout_s=10.0,
poll_interval_s=0.2):
from je_auto_control.utils.executor.action_executor import _wait_window_title
return _wait_window_title(pattern, present, regex, timeout_s,
poll_interval_s)


def wait_for_file(path: str, timeout_s: float = 30.0,
poll_interval_s: float = 0.25,
stable_for_s: float = 1.0,
Expand Down
21 changes: 11 additions & 10 deletions je_auto_control/utils/smart_waits/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@
"""
from je_auto_control.utils.smart_waits.waits import (
ClipboardReader, FileStatReader, Frame, PortConnector, ProcessLister,
ScreenSampler, WaitOutcome, WindowFinder, wait_until_clipboard_changes,
wait_until_color, wait_until_file, wait_until_gone, wait_until_image_gone,
wait_until_pixel_changes, wait_until_port, wait_until_process,
wait_until_region_idle, wait_until_screen_stable, wait_until_text_gone,
wait_until_window_closed,
ScreenSampler, WaitOutcome, WindowFinder, WindowTitleLister,
wait_until_clipboard_changes, wait_until_color, wait_until_file,
wait_until_gone, wait_until_image_gone, wait_until_pixel_changes,
wait_until_port, wait_until_process, wait_until_region_idle,
wait_until_screen_stable, wait_until_text_gone, wait_until_window_closed,
wait_until_window_title,
)


__all__ = [
"ClipboardReader", "FileStatReader", "Frame", "PortConnector",
"ProcessLister", "ScreenSampler", "WaitOutcome", "WindowFinder",
"wait_until_clipboard_changes", "wait_until_color", "wait_until_file",
"wait_until_gone", "wait_until_image_gone", "wait_until_pixel_changes",
"wait_until_port", "wait_until_process", "wait_until_region_idle",
"wait_until_screen_stable", "wait_until_text_gone",
"wait_until_window_closed",
"WindowTitleLister", "wait_until_clipboard_changes", "wait_until_color",
"wait_until_file", "wait_until_gone", "wait_until_image_gone",
"wait_until_pixel_changes", "wait_until_port", "wait_until_process",
"wait_until_region_idle", "wait_until_screen_stable", "wait_until_text_gone",
"wait_until_window_closed", "wait_until_window_title",
]
47 changes: 47 additions & 0 deletions je_auto_control/utils/smart_waits/waits.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"""
from __future__ import annotations

import re
import time
from dataclasses import asdict, dataclass
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
Expand Down Expand Up @@ -238,6 +239,52 @@ def _default_window_finder(title: str, case_sensitive: bool) -> bool:
return find_window(title, case_sensitive=case_sensitive) is not None


WindowTitleLister = Callable[[], List[str]]


def _title_matches(titles: List[str], pattern: str,
compiled: Optional["re.Pattern"]) -> bool:
if compiled is not None:
return any(compiled.search(title) for title in titles)
return any(pattern in title for title in titles)


def wait_until_window_title(pattern: str, *, present: bool = True,
regex: bool = True, timeout_s: float = 10.0,
poll_interval_s: float = 0.2,
title_lister: Optional[WindowTitleLister] = None
) -> WaitOutcome:
"""Wait until a window whose title matches ``pattern`` appears (or vanishes).

Unlike ``wait_for_window`` (substring, appear only) this matches a regular
expression by default (``regex=False`` falls back to a substring test) and
can wait for the title to *vanish* with ``present=False`` — e.g. wait for a
browser tab to navigate to ``r".*— Checkout$"``. ``title_lister() -> [titles]``
is injectable for tests.
"""
if timeout_s <= 0:
raise ValueError(_TIMEOUT_POSITIVE)
if poll_interval_s <= 0:
raise ValueError(_POLL_POSITIVE)
titles_of = title_lister or _default_title_lister
compiled = re.compile(pattern) if regex else None
started = time.monotonic()
deadline = started + float(timeout_s)
samples = 0
while time.monotonic() < deadline:
samples += 1
if _title_matches(titles_of(), pattern, compiled) == bool(present):
return _finish(True, "window title condition met", started, samples)
time.sleep(float(poll_interval_s))
return _finish(False, "timeout while waiting for window title",
started, samples)


def _default_title_lister() -> List[str]:
from je_auto_control.wrapper.auto_control_window import list_windows
return [title for _id, title in list_windows()]


FileStatReader = Callable[[str], Optional[int]]


Expand Down
67 changes: 67 additions & 0 deletions test/unit_test/headless/test_wait_window_title_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Headless tests for wait-until-window-title. No Qt."""
import je_auto_control as ac
from je_auto_control.utils.smart_waits import (
WaitOutcome, wait_until_window_title,
)


def test_regex_title_appears():
# title list flips to include a match on the 2nd poll
state = {"polls": 0}

def lister():
state["polls"] += 1
return ["Editor"] if state["polls"] < 2 else ["Shop — Checkout"]

outcome = wait_until_window_title(r".*— Checkout$", timeout_s=5.0,
poll_interval_s=0.001, title_lister=lister)
assert isinstance(outcome, WaitOutcome) and outcome.succeeded is True


def test_substring_mode():
outcome = wait_until_window_title(
"Checkout", regex=False, timeout_s=5.0, poll_interval_s=0.001,
title_lister=lambda: ["My Shop Checkout Page"])
assert outcome.succeeded is True


def test_wait_for_vanish():
outcome = wait_until_window_title(
"Loading", present=False, timeout_s=5.0, poll_interval_s=0.001,
title_lister=lambda: ["Done"])
assert outcome.succeeded is True # no title matches -> vanished


def test_timeout_when_never_matches():
outcome = wait_until_window_title(
"Nope", timeout_s=0.03, poll_interval_s=0.001,
title_lister=lambda: ["Editor", "Browser"])
assert outcome.succeeded is False and "timeout" in outcome.reason


def test_validation():
for bad in ({"timeout_s": 0}, {"poll_interval_s": 0}):
try:
wait_until_window_title("x", title_lister=lambda: [], **bad)
except ValueError:
pass
else: # pragma: no cover
raise AssertionError("expected ValueError")


# --- wiring ---------------------------------------------------------------

def test_wiring():
known = ac.executor.known_commands()
assert "AC_wait_window_title" in set(known)
from je_auto_control.utils.mcp_server.tools import build_default_tool_registry
names = {t.name for t in build_default_tool_registry()}
assert "ac_wait_window_title" in names
from je_auto_control.gui.script_builder.command_schema import _build_specs
specs = {s.command for s in _build_specs()}
assert "AC_wait_window_title" in specs


def test_facade_exports():
assert hasattr(ac, "wait_until_window_title")
assert "wait_until_window_title" in ac.__all__
Loading