Skip to content
Open
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
48 changes: 48 additions & 0 deletions examples/press_key.py
Original file line number Diff line number Diff line change
@@ -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); PressKeyEventItem is optional.
await window.press_key(
events=[
{"type": "keyDown", "code": "KeyA", "key": "a"},
{"type": "keyUp", "code": "KeyA", "key": "a"},
],
)

await window.press_key(
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())
22 changes: 22 additions & 0 deletions packages/narada-core/src/narada_core/actions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,8 +419,30 @@ class UserApprovalResponse(BaseModel):
approved: bool


class RecordedKeyModifiers(TypedDict, total=False):
ctrl: bool
shift: bool
alt: bool
meta: bool


class PressKeyEventItem(BaseModel):
type: Literal["keyDown", "keyUp", "press"] = "keyDown" # noqa: A003
code: str
key: str | None = None
modifiers: RecordedKeyModifiers | None = None


class PressKeyRequest(BaseModel):
"""Wire payload: key events for the extension to replay via ``debuggerPress``."""

name: Literal["press_key"] = "press_key"
events: list[PressKeyEventItem]


type ExtensionActionRequest = (
AgenticSelectorRequest
| PressKeyRequest
| AgenticMouseActionRequest
| CloseWindowRequest
| GoToUrlRequest
Expand Down
3 changes: 2 additions & 1 deletion packages/narada-pyodide/src/narada/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from narada_core.actions.models import CriticResult
from narada_core.actions.models import CriticResult, PressKeyEventItem
from narada_core.errors import (
NaradaError,
NaradaTimeoutError,
Expand Down Expand Up @@ -27,6 +27,7 @@
"CloudBrowserWindow",
"CriticConfig",
"CriticResult",
"PressKeyEventItem",
"download_file",
"File",
"LocalBrowserWindow",
Expand Down
41 changes: 41 additions & 0 deletions packages/narada-pyodide/src/narada/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
TYPE_CHECKING,
Any,
Literal,
Mapping,
Optional,
Sequence,
TypeVar,
cast,
overload,
Expand Down Expand Up @@ -43,6 +45,8 @@
GetUrlRequest,
GetUrlResponse,
GoToUrlRequest,
PressKeyEventItem,
PressKeyRequest,
PrintMessageRequest,
PromptForUserInputRequest,
PromptForUserInputResponse,
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -725,6 +731,41 @@ async def agentic_selector(

return result

async def press_key(
self,
*,
events: Sequence[PressKeyEventItem | 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:`PressKeyEventItem` instances or plain mappings (e.g. dicts from
``json.loads``) with the same keys::

await window.press_key(events=[
{"type": "keyDown", "code": "KeyA", "key": "a"},
{"type": "keyUp", "code": "KeyA", "key": "a"},
])
"""

if not events:
raise ValueError("press_key requires a non-empty events= sequence")

normalized: list[PressKeyEventItem] = [
event
if isinstance(event, PressKeyEventItem)
else PressKeyEventItem.model_validate(event)
for event in events
]

await self._run_extension_action(
PressKeyRequest(events=normalized),
timeout=timeout,
)

async def agentic_mouse_action(
self,
*,
Expand Down
3 changes: 2 additions & 1 deletion packages/narada/src/narada/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from narada_core.actions.models import CriticResult
from narada_core.actions.models import CriticResult, PressKeyEventItem
from narada_core.errors import (
NaradaError,
NaradaExtensionMissingError,
Expand Down Expand Up @@ -30,6 +30,7 @@
"CloudBrowserWindow",
"CriticConfig",
"CriticResult",
"PressKeyEventItem",
"download_file",
"File",
"LocalBrowserWindow",
Expand Down
38 changes: 38 additions & 0 deletions packages/narada/src/narada/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Any,
Literal,
Mapping,
Sequence,
TypedDict,
TypeGuard,
TypeVar,
Expand Down Expand Up @@ -44,6 +45,8 @@
GetUrlRequest,
GetUrlResponse,
GoToUrlRequest,
PressKeyEventItem,
PressKeyRequest,
PrintMessageRequest,
PromptForUserInputRequest,
PromptForUserInputResponse,
Expand Down Expand Up @@ -696,6 +699,41 @@ async def agentic_selector(

return result

async def press_key(
self,
*,
events: Sequence[PressKeyEventItem | 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:`PressKeyEventItem` instances or plain mappings (e.g. dicts parsed
from JSON) with the same keys::

await window.press_key(events=[
{"type": "keyDown", "code": "KeyA", "key": "a"},
{"type": "keyUp", "code": "KeyA", "key": "a"},
])
"""

if not events:
raise ValueError("press_key requires a non-empty events= sequence")

normalized: list[PressKeyEventItem] = [
event
if isinstance(event, PressKeyEventItem)
else PressKeyEventItem.model_validate(event)
for event in events
]

await self._run_extension_action(
PressKeyRequest(events=normalized),
timeout=timeout,
)

async def agentic_mouse_action(
self,
*,
Expand Down
Loading