From 989bc2c29cef95bb7e2c09c7f23cd27a63c81599 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Tue, 12 May 2026 18:36:22 +0300 Subject: [PATCH 1/2] =?UTF-8?q?Press=20Keys:=20SDK=20dispatch=5Fkey=5Feven?= =?UTF-8?q?t(events=3D=E2=80=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/press_key.py | 48 +++++++++++++++++++ .../src/narada_core/actions/models.py | 22 +++++++++ .../narada-pyodide/src/narada/__init__.py | 3 +- packages/narada-pyodide/src/narada/window.py | 41 ++++++++++++++++ packages/narada/src/narada/__init__.py | 3 +- packages/narada/src/narada/window.py | 38 +++++++++++++++ 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 examples/press_key.py diff --git a/examples/press_key.py b/examples/press_key.py new file mode 100644 index 0000000..f832ab3 --- /dev/null +++ b/examples/press_key.py @@ -0,0 +1,48 @@ +import asyncio + +from narada import Narada + + +async def main() -> None: + async with Narada() as narada: + window = await narada.open_and_initialize_browser_window() + + await window.go_to_url( + url="https://w3c.github.io/uievents/tools/key-event-viewer.html", timeout=60 + ) + + # Dict items are accepted (same shape as JSON); DispatchKeyEventItem is optional. + await window.dispatch_key_event( + events=[ + {"type": "keyDown", "code": "KeyA", "key": "a"}, + {"type": "keyUp", "code": "KeyA", "key": "a"}, + ], + ) + + await window.dispatch_key_event( + events=[ + { + "type": "keyDown", + "code": "ShiftLeft", + "key": "Shift", + "modifiers": {"shift": True}, + }, + { + "type": "keyDown", + "code": "KeyA", + "key": "A", + "modifiers": {"shift": True}, + }, + { + "type": "keyUp", + "code": "KeyA", + "key": "A", + "modifiers": {"shift": True}, + }, + {"type": "keyUp", "code": "ShiftLeft", "key": "Shift"}, + ], + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index 25b5cbc..bad1ac4 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -419,8 +419,30 @@ class UserApprovalResponse(BaseModel): approved: bool +class RecordedKeyModifiers(TypedDict, total=False): + ctrl: bool + shift: bool + alt: bool + meta: bool + + +class DispatchKeyEventItem(BaseModel): + type: Literal["keyDown", "keyUp", "press"] = "keyDown" # noqa: A003 + code: str + key: str | None = None + modifiers: RecordedKeyModifiers | None = None + + +class DispatchKeyEventRequest(BaseModel): + """Wire payload: key events for the extension to replay via ``debuggerPress``.""" + + name: Literal["dispatch_key_event"] = "dispatch_key_event" + events: list[DispatchKeyEventItem] + + type ExtensionActionRequest = ( AgenticSelectorRequest + | DispatchKeyEventRequest | AgenticMouseActionRequest | CloseWindowRequest | GoToUrlRequest diff --git a/packages/narada-pyodide/src/narada/__init__.py b/packages/narada-pyodide/src/narada/__init__.py index 1c5c77b..5bba9ad 100644 --- a/packages/narada-pyodide/src/narada/__init__.py +++ b/packages/narada-pyodide/src/narada/__init__.py @@ -1,4 +1,4 @@ -from narada_core.actions.models import CriticResult +from narada_core.actions.models import CriticResult, DispatchKeyEventItem from narada_core.errors import ( NaradaError, NaradaTimeoutError, @@ -27,6 +27,7 @@ "CloudBrowserWindow", "CriticConfig", "CriticResult", + "DispatchKeyEventItem", "download_file", "File", "LocalBrowserWindow", diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 4c6809c..0bb1a14 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -11,7 +11,9 @@ TYPE_CHECKING, Any, Literal, + Mapping, Optional, + Sequence, TypeVar, cast, overload, @@ -32,6 +34,8 @@ AgentUsage, CloseWindowRequest, CriticResult, + DispatchKeyEventItem, + DispatchKeyEventRequest, ExtensionActionRequest, ExtensionActionResponse, GetFullHtmlRequest, @@ -695,6 +699,7 @@ async def agentic_selector( action: AgenticSelectorAction, selectors: AgenticSelectors, fallback_operator_query: str, + nth_match: Optional[str] = None, # Larger default timeout because Operator can take a bit to run. timeout: int | None = 300, ) -> AgenticSelectorResponse: @@ -715,6 +720,7 @@ async def agentic_selector( action=action, selectors=selectors, fallback_operator_query=fallback_operator_query, + nth_match=nth_match, ), response_model, timeout=timeout, @@ -725,6 +731,41 @@ async def agentic_selector( return result + async def dispatch_key_event( + self, + *, + events: Sequence[DispatchKeyEventItem | Mapping[str, Any]], + timeout: int | None = 60, + ) -> None: + """Send keyboard events on the active tab (Chrome debugger). + + Each item uses ``type`` of ``\"keyDown\"``, ``\"keyUp\"``, or ``\"press\"``. + ``code`` is required per item; ``key`` and ``modifiers`` are optional. + + Items may be :class:`DispatchKeyEventItem` instances or plain mappings (e.g. dicts from + ``json.loads``) with the same keys:: + + await window.dispatch_key_event(events=[ + {"type": "keyDown", "code": "KeyA", "key": "a"}, + {"type": "keyUp", "code": "KeyA", "key": "a"}, + ]) + """ + + if not events: + raise ValueError("dispatch_key_event requires a non-empty events= sequence") + + normalized: list[DispatchKeyEventItem] = [ + event + if isinstance(event, DispatchKeyEventItem) + else DispatchKeyEventItem.model_validate(event) + for event in events + ] + + await self._run_extension_action( + DispatchKeyEventRequest(events=normalized), + timeout=timeout, + ) + async def agentic_mouse_action( self, *, diff --git a/packages/narada/src/narada/__init__.py b/packages/narada/src/narada/__init__.py index 719f28d..466aae5 100644 --- a/packages/narada/src/narada/__init__.py +++ b/packages/narada/src/narada/__init__.py @@ -1,4 +1,4 @@ -from narada_core.actions.models import CriticResult +from narada_core.actions.models import CriticResult, DispatchKeyEventItem from narada_core.errors import ( NaradaError, NaradaExtensionMissingError, @@ -30,6 +30,7 @@ "CloudBrowserWindow", "CriticConfig", "CriticResult", + "DispatchKeyEventItem", "download_file", "File", "LocalBrowserWindow", diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index c577951..83f5744 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -13,6 +13,7 @@ Any, Literal, Mapping, + Sequence, TypedDict, TypeGuard, TypeVar, @@ -33,6 +34,8 @@ AgentUsage, CloseWindowRequest, CriticResult, + DispatchKeyEventItem, + DispatchKeyEventRequest, ExtensionActionRequest, ExtensionActionResponse, GetFullHtmlRequest, @@ -696,6 +699,41 @@ async def agentic_selector( return result + async def dispatch_key_event( + self, + *, + events: Sequence[DispatchKeyEventItem | Mapping[str, Any]], + timeout: int | None = 60, + ) -> None: + """Send keyboard events on the active tab (Chrome debugger). + + Each item uses ``type`` of ``\"keyDown\"``, ``\"keyUp\"``, or ``\"press\"`` (Playwright/CDP + semantics). ``code`` is required per item; ``key`` and ``modifiers`` are optional. + + Items may be :class:`DispatchKeyEventItem` instances or plain mappings (e.g. dicts parsed + from JSON) with the same keys:: + + await window.dispatch_key_event(events=[ + {"type": "keyDown", "code": "KeyA", "key": "a"}, + {"type": "keyUp", "code": "KeyA", "key": "a"}, + ]) + """ + + if not events: + raise ValueError("dispatch_key_event requires a non-empty events= sequence") + + normalized: list[DispatchKeyEventItem] = [ + event + if isinstance(event, DispatchKeyEventItem) + else DispatchKeyEventItem.model_validate(event) + for event in events + ] + + await self._run_extension_action( + DispatchKeyEventRequest(events=normalized), + timeout=timeout, + ) + async def agentic_mouse_action( self, *, From 01e444429a69b04348f912c321daf50f13e334e1 Mon Sep 17 00:00:00 2001 From: Pavlo Haidar Date: Tue, 12 May 2026 21:09:56 +0300 Subject: [PATCH 2/2] rename --- examples/press_key.py | 6 ++--- .../src/narada_core/actions/models.py | 10 ++++----- .../narada-pyodide/src/narada/__init__.py | 4 ++-- packages/narada-pyodide/src/narada/window.py | 22 +++++++++---------- packages/narada/src/narada/__init__.py | 4 ++-- packages/narada/src/narada/window.py | 22 +++++++++---------- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/examples/press_key.py b/examples/press_key.py index f832ab3..a63dd5d 100644 --- a/examples/press_key.py +++ b/examples/press_key.py @@ -11,15 +11,15 @@ async def main() -> None: url="https://w3c.github.io/uievents/tools/key-event-viewer.html", timeout=60 ) - # Dict items are accepted (same shape as JSON); DispatchKeyEventItem is optional. - await window.dispatch_key_event( + # Dict items are accepted (same shape as JSON); PressKeyEventItem is optional. + await window.press_key( events=[ {"type": "keyDown", "code": "KeyA", "key": "a"}, {"type": "keyUp", "code": "KeyA", "key": "a"}, ], ) - await window.dispatch_key_event( + await window.press_key( events=[ { "type": "keyDown", diff --git a/packages/narada-core/src/narada_core/actions/models.py b/packages/narada-core/src/narada_core/actions/models.py index bad1ac4..a838c31 100644 --- a/packages/narada-core/src/narada_core/actions/models.py +++ b/packages/narada-core/src/narada_core/actions/models.py @@ -426,23 +426,23 @@ class RecordedKeyModifiers(TypedDict, total=False): meta: bool -class DispatchKeyEventItem(BaseModel): +class PressKeyEventItem(BaseModel): type: Literal["keyDown", "keyUp", "press"] = "keyDown" # noqa: A003 code: str key: str | None = None modifiers: RecordedKeyModifiers | None = None -class DispatchKeyEventRequest(BaseModel): +class PressKeyRequest(BaseModel): """Wire payload: key events for the extension to replay via ``debuggerPress``.""" - name: Literal["dispatch_key_event"] = "dispatch_key_event" - events: list[DispatchKeyEventItem] + name: Literal["press_key"] = "press_key" + events: list[PressKeyEventItem] type ExtensionActionRequest = ( AgenticSelectorRequest - | DispatchKeyEventRequest + | PressKeyRequest | AgenticMouseActionRequest | CloseWindowRequest | GoToUrlRequest diff --git a/packages/narada-pyodide/src/narada/__init__.py b/packages/narada-pyodide/src/narada/__init__.py index 5bba9ad..44ed030 100644 --- a/packages/narada-pyodide/src/narada/__init__.py +++ b/packages/narada-pyodide/src/narada/__init__.py @@ -1,4 +1,4 @@ -from narada_core.actions.models import CriticResult, DispatchKeyEventItem +from narada_core.actions.models import CriticResult, PressKeyEventItem from narada_core.errors import ( NaradaError, NaradaTimeoutError, @@ -27,7 +27,7 @@ "CloudBrowserWindow", "CriticConfig", "CriticResult", - "DispatchKeyEventItem", + "PressKeyEventItem", "download_file", "File", "LocalBrowserWindow", diff --git a/packages/narada-pyodide/src/narada/window.py b/packages/narada-pyodide/src/narada/window.py index 0bb1a14..6b9a4fc 100644 --- a/packages/narada-pyodide/src/narada/window.py +++ b/packages/narada-pyodide/src/narada/window.py @@ -34,8 +34,6 @@ AgentUsage, CloseWindowRequest, CriticResult, - DispatchKeyEventItem, - DispatchKeyEventRequest, ExtensionActionRequest, ExtensionActionResponse, GetFullHtmlRequest, @@ -47,6 +45,8 @@ GetUrlRequest, GetUrlResponse, GoToUrlRequest, + PressKeyEventItem, + PressKeyRequest, PrintMessageRequest, PromptForUserInputRequest, PromptForUserInputResponse, @@ -731,10 +731,10 @@ async def agentic_selector( return result - async def dispatch_key_event( + async def press_key( self, *, - events: Sequence[DispatchKeyEventItem | Mapping[str, Any]], + events: Sequence[PressKeyEventItem | Mapping[str, Any]], timeout: int | None = 60, ) -> None: """Send keyboard events on the active tab (Chrome debugger). @@ -742,27 +742,27 @@ async def dispatch_key_event( Each item uses ``type`` of ``\"keyDown\"``, ``\"keyUp\"``, or ``\"press\"``. ``code`` is required per item; ``key`` and ``modifiers`` are optional. - Items may be :class:`DispatchKeyEventItem` instances or plain mappings (e.g. dicts from + Items may be :class:`PressKeyEventItem` instances or plain mappings (e.g. dicts from ``json.loads``) with the same keys:: - await window.dispatch_key_event(events=[ + await window.press_key(events=[ {"type": "keyDown", "code": "KeyA", "key": "a"}, {"type": "keyUp", "code": "KeyA", "key": "a"}, ]) """ if not events: - raise ValueError("dispatch_key_event requires a non-empty events= sequence") + raise ValueError("press_key requires a non-empty events= sequence") - normalized: list[DispatchKeyEventItem] = [ + normalized: list[PressKeyEventItem] = [ event - if isinstance(event, DispatchKeyEventItem) - else DispatchKeyEventItem.model_validate(event) + if isinstance(event, PressKeyEventItem) + else PressKeyEventItem.model_validate(event) for event in events ] await self._run_extension_action( - DispatchKeyEventRequest(events=normalized), + PressKeyRequest(events=normalized), timeout=timeout, ) diff --git a/packages/narada/src/narada/__init__.py b/packages/narada/src/narada/__init__.py index 466aae5..8f7a0fb 100644 --- a/packages/narada/src/narada/__init__.py +++ b/packages/narada/src/narada/__init__.py @@ -1,4 +1,4 @@ -from narada_core.actions.models import CriticResult, DispatchKeyEventItem +from narada_core.actions.models import CriticResult, PressKeyEventItem from narada_core.errors import ( NaradaError, NaradaExtensionMissingError, @@ -30,7 +30,7 @@ "CloudBrowserWindow", "CriticConfig", "CriticResult", - "DispatchKeyEventItem", + "PressKeyEventItem", "download_file", "File", "LocalBrowserWindow", diff --git a/packages/narada/src/narada/window.py b/packages/narada/src/narada/window.py index 83f5744..270fe5d 100644 --- a/packages/narada/src/narada/window.py +++ b/packages/narada/src/narada/window.py @@ -34,8 +34,6 @@ AgentUsage, CloseWindowRequest, CriticResult, - DispatchKeyEventItem, - DispatchKeyEventRequest, ExtensionActionRequest, ExtensionActionResponse, GetFullHtmlRequest, @@ -47,6 +45,8 @@ GetUrlRequest, GetUrlResponse, GoToUrlRequest, + PressKeyEventItem, + PressKeyRequest, PrintMessageRequest, PromptForUserInputRequest, PromptForUserInputResponse, @@ -699,10 +699,10 @@ async def agentic_selector( return result - async def dispatch_key_event( + async def press_key( self, *, - events: Sequence[DispatchKeyEventItem | Mapping[str, Any]], + events: Sequence[PressKeyEventItem | Mapping[str, Any]], timeout: int | None = 60, ) -> None: """Send keyboard events on the active tab (Chrome debugger). @@ -710,27 +710,27 @@ async def dispatch_key_event( Each item uses ``type`` of ``\"keyDown\"``, ``\"keyUp\"``, or ``\"press\"`` (Playwright/CDP semantics). ``code`` is required per item; ``key`` and ``modifiers`` are optional. - Items may be :class:`DispatchKeyEventItem` instances or plain mappings (e.g. dicts parsed + Items may be :class:`PressKeyEventItem` instances or plain mappings (e.g. dicts parsed from JSON) with the same keys:: - await window.dispatch_key_event(events=[ + await window.press_key(events=[ {"type": "keyDown", "code": "KeyA", "key": "a"}, {"type": "keyUp", "code": "KeyA", "key": "a"}, ]) """ if not events: - raise ValueError("dispatch_key_event requires a non-empty events= sequence") + raise ValueError("press_key requires a non-empty events= sequence") - normalized: list[DispatchKeyEventItem] = [ + normalized: list[PressKeyEventItem] = [ event - if isinstance(event, DispatchKeyEventItem) - else DispatchKeyEventItem.model_validate(event) + if isinstance(event, PressKeyEventItem) + else PressKeyEventItem.model_validate(event) for event in events ] await self._run_extension_action( - DispatchKeyEventRequest(events=normalized), + PressKeyRequest(events=normalized), timeout=timeout, )