control-dom 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
control_dom/main.py ADDED
@@ -0,0 +1,176 @@
1
+ """
2
+ control_dom Tool
3
+ =================
4
+ Evaluates arbitrary JavaScript in a Tauri child webview using
5
+ the in-browser Playwright Page API polyfill (window.__page).
6
+
7
+ The JS code runs inside the target webview's renderer context.
8
+ Console output (console.log / warn / error) is captured and
9
+ returned so the agent can observe results.
10
+
11
+ Parameters:
12
+ js_code – JavaScript source to evaluate. Has full access to
13
+ window.__page and the Playwright polyfill API.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import logging
20
+ import uuid
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from openchadpy.context import console_messages
24
+ from openchadpy.tool_base import ToolBase
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # How long (seconds) to wait for console output after the script fires.
29
+ _DEFAULT_WAIT = 8.0
30
+ # How long (seconds) between console-poll ticks.
31
+ _POLL_INTERVAL = 0.15
32
+
33
+
34
+ class Tool(ToolBase):
35
+ """Evaluate JavaScript inside a Tauri child webview (DOM control)."""
36
+
37
+ name = "control_dom"
38
+ description = (
39
+ "Evaluate JavaScript code inside a Tauri child webview. "
40
+ "The code runs in the page context and has access to "
41
+ "window.__page — a Playwright Page API polyfill — so you can "
42
+ "click, fill, navigate, scrape, wait for selectors, and more. "
43
+ "All console.log / console.error / console.warn output is captured "
44
+ "and returned so you can observe results. "
45
+ "Always write console.log() statements so you can verify actions succeeded. "
46
+ "Prefer window.__page methods over raw DOM when possible."
47
+ )
48
+ input_schema = {
49
+ "type": "object",
50
+ "properties": {
51
+ "js_code": {
52
+ "type": "string",
53
+ "description": (
54
+ "JavaScript code to evaluate in the webview. "
55
+ "Top-level await is supported — wrap async logic directly. "
56
+ "Use window.__page for Playwright-style DOM control. "
57
+ "Use console.log() to emit observable output. "
58
+ "Example:\n"
59
+ " const loc = window.__page.getByRole('button', { name: 'Submit' });\n"
60
+ " await loc.click();\n"
61
+ " console.log('clicked:', await loc.textContent());"
62
+ ),
63
+ },
64
+ "timeout": {
65
+ "type": "number",
66
+ "description": (
67
+ "Seconds to wait for console output after the script runs "
68
+ f"(default: {_DEFAULT_WAIT}). Increase for long-running scripts."
69
+ ),
70
+ },
71
+ },
72
+ "required": ["js_code"],
73
+ }
74
+ allowed_callers = ["direct", "code_execution", "mcp_client"]
75
+
76
+ # ------------------------------------------------------------------
77
+ # helpers
78
+ # ------------------------------------------------------------------
79
+
80
+ def _resolve_target(self, target_webview: Optional[str]) -> str:
81
+ """Return a best-effort webview label."""
82
+ if target_webview:
83
+ return target_webview
84
+ # Fall back to the active tab id formatted as a webview agent label.
85
+ tid = self.tab_id
86
+ if tid and tid != "global":
87
+ return f"webview-agent-{tid}"
88
+ return ""
89
+
90
+ async def _collect_new_console(
91
+ self,
92
+ label: str,
93
+ before_count: int,
94
+ timeout: float,
95
+ ) -> List[Dict[str, Any]]:
96
+ """
97
+ Poll console_messages[label] until new entries appear or timeout.
98
+ Returns the list of new message dicts appended after `before_count`.
99
+ """
100
+ deadline = asyncio.get_event_loop().time() + timeout
101
+ while asyncio.get_event_loop().time() < deadline:
102
+ entries: List[Any] = console_messages.get(label, [])
103
+ if len(entries) > before_count:
104
+ return list(entries[before_count:])
105
+ await asyncio.sleep(_POLL_INTERVAL)
106
+ # Return whatever arrived even if we timed out
107
+ entries = console_messages.get(label, [])
108
+ return list(entries[before_count:])
109
+
110
+ # ------------------------------------------------------------------
111
+ # execute
112
+ # ------------------------------------------------------------------
113
+
114
+ async def execute(self, **kwargs) -> Dict[str, Any]: # noqa: C901
115
+ js_code: str = kwargs.get("js_code", "").strip()
116
+ target_webview: str = self.get_field("Browser ID") or ""
117
+ timeout: float = float(kwargs.get("timeout", _DEFAULT_WAIT))
118
+
119
+ if not js_code:
120
+ return {"error": "js_code is required and must not be empty."}
121
+
122
+ label = self._resolve_target(target_webview or None)
123
+ if not label:
124
+ return {
125
+ "error": (
126
+ "Could not determine target webview. "
127
+ "Please supply target_webview explicitly."
128
+ )
129
+ }
130
+
131
+ # Snapshot current console length so we can detect new messages.
132
+ before_count: int = len(console_messages.get(label, []))
133
+
134
+ # Wrap the user code in an async IIFE so top-level await works
135
+ # and unhandled errors surface as console.error output.
136
+ wrapped = (
137
+ "(async () => { try {\n"
138
+ + js_code
139
+ + "\n} catch (e) { console.error('[control_dom error]', e && (e.stack || e.message || String(e))); } })();"
140
+ )
141
+
142
+ try:
143
+ if not self.event_emitter:
144
+ return {"error": "event_emitter not available (tool not fully initialised)."}
145
+
146
+ await self.event_emitter.emit("eval", {"script": wrapped, "label": label})
147
+ except Exception as exc:
148
+ logger.error(f"[control_dom] emit eval failed: {exc}", exc_info=True)
149
+ return {"error": f"Failed to send script to webview: {exc}"}
150
+
151
+ # Collect console output that arrives within the timeout window.
152
+ new_messages = await self._collect_new_console(label, before_count, timeout)
153
+
154
+ # Build human-readable output lines.
155
+ output_lines: List[str] = []
156
+ has_error = False
157
+ for msg in new_messages:
158
+ mtype = msg.get("type", "log")
159
+ text = msg.get("text", "")
160
+ output_lines.append(f"[{mtype}] {text}")
161
+ if mtype == "error":
162
+ has_error = True
163
+
164
+ result: Dict[str, Any] = {
165
+ "label": label,
166
+ "console": output_lines,
167
+ }
168
+ if has_error:
169
+ result["warning"] = "One or more console.error() calls were recorded."
170
+ if not output_lines:
171
+ result["note"] = (
172
+ "No console output was captured within the timeout window. "
173
+ "Add console.log() calls to your js_code to verify execution."
174
+ )
175
+
176
+ return result
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: control_dom
3
+ Version: 0.1.0
4
+ Summary: Evaluate JavaScript code inside a Tauri child webview.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: openchadpy>=0.1.23
@@ -0,0 +1,6 @@
1
+ control_dom/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ control_dom/main.py,sha256=vdF0mfU5uOq9Bm8RTjBew2o_XOihyLUwISCHeDk6BF0,6787
3
+ control_dom-0.1.0.dist-info/METADATA,sha256=y2PK4zLZakQJTE229mLio-L9AKPZCvksRZokS9c8WB0,217
4
+ control_dom-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
5
+ control_dom-0.1.0.dist-info/top_level.txt,sha256=oAOFPYp3QHhrMjhd255o0pHYLw-Dig_-YpnAc9D04Pw,12
6
+ control_dom-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ control_dom