@wong2kim/wmux 1.1.2 → 2.0.1
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.
- package/README.md +137 -86
- package/dist/cli/cli/commands/system.js +12 -1
- package/dist/cli/shared/rpc.js +6 -0
- package/dist/cli/shared/types.js +108 -4
- package/dist/mcp/mcp/index.js +12 -1
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +193 -86
- package/dist/mcp/mcp/playwright/anti-detection.js +12 -7
- package/dist/mcp/mcp/playwright/security.js +29 -0
- package/dist/mcp/mcp/playwright/tools/extraction.js +3 -3
- package/dist/mcp/mcp/playwright/tools/file.js +4 -4
- package/dist/mcp/mcp/playwright/tools/inspection.js +93 -29
- package/dist/mcp/mcp/playwright/tools/interaction.js +207 -137
- package/dist/mcp/mcp/playwright/tools/navigation.js +37 -14
- package/dist/mcp/mcp/playwright/tools/state.js +4 -4
- package/dist/mcp/mcp/playwright/tools/utility.js +2 -2
- package/dist/mcp/mcp/playwright/tools/wait.js +10 -2
- package/dist/mcp/shared/rpc.js +6 -0
- package/dist/mcp/shared/types.js +108 -4
- package/package.json +6 -5
|
@@ -5,6 +5,8 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const PlaywrightEngine_1 = require("../PlaywrightEngine");
|
|
6
6
|
const snapshot_1 = require("../snapshot");
|
|
7
7
|
const anti_detection_1 = require("../anti-detection");
|
|
8
|
+
const security_1 = require("../security");
|
|
9
|
+
const wmux_client_1 = require("../../wmux-client");
|
|
8
10
|
// Optional surfaceId schema reused across tools
|
|
9
11
|
const optionalSurfaceId = zod_1.z
|
|
10
12
|
.string()
|
|
@@ -126,13 +128,53 @@ function registerInspectionTools(server) {
|
|
|
126
128
|
surfaceId: optionalSurfaceId,
|
|
127
129
|
}, async ({ format, surfaceId }) => {
|
|
128
130
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
// Try Playwright for full snapshot
|
|
132
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
133
|
+
if (page) {
|
|
134
|
+
const snapshot = await (0, snapshot_1.generateSnapshot)(page, { format: format ?? 'ai' });
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: 'text', text: snapshot }],
|
|
137
|
+
};
|
|
132
138
|
}
|
|
133
|
-
|
|
139
|
+
// Fallback: extract page structure via RPC evaluation
|
|
140
|
+
// Tags interactive elements with data-wmux-ref so interaction tools can resolve them
|
|
141
|
+
const result = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
142
|
+
expression: `(() => {
|
|
143
|
+
const sel = 'a[href], button, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="tab"], [contenteditable="true"]';
|
|
144
|
+
const interactives = [...document.querySelectorAll(sel)].slice(0, 100);
|
|
145
|
+
interactives.forEach((el, i) => el.setAttribute('data-wmux-ref', String(i)));
|
|
146
|
+
const title = document.title;
|
|
147
|
+
const url = location.href;
|
|
148
|
+
const lines = ['Page: ' + title, 'URL: ' + url, ''];
|
|
149
|
+
document.querySelectorAll('h1,h2,h3').forEach(h => {
|
|
150
|
+
lines.push(h.tagName + ': ' + (h.textContent || '').trim().substring(0, 80));
|
|
151
|
+
});
|
|
152
|
+
lines.push('', 'Interactive elements (use ref number for click/fill/type):');
|
|
153
|
+
interactives.forEach((el, i) => {
|
|
154
|
+
const tag = el.tagName.toLowerCase();
|
|
155
|
+
const role = el.getAttribute('role') || '';
|
|
156
|
+
const text = (el.textContent || '').trim().substring(0, 60);
|
|
157
|
+
const label = el.getAttribute('aria-label') || '';
|
|
158
|
+
const name = el.getAttribute('name') || '';
|
|
159
|
+
const type = el.getAttribute('type') || '';
|
|
160
|
+
const placeholder = el.getAttribute('placeholder') || '';
|
|
161
|
+
const href = el.getAttribute('href') || '';
|
|
162
|
+
let desc = ' [ref=' + i + '] ' + tag;
|
|
163
|
+
if (type) desc += '[type=' + type + ']';
|
|
164
|
+
if (role) desc += '[role=' + role + ']';
|
|
165
|
+
if (name) desc += ' name="' + name + '"';
|
|
166
|
+
if (label) desc += ' "' + label + '"';
|
|
167
|
+
else if (text) desc += ' "' + text + '"';
|
|
168
|
+
if (placeholder) desc += ' placeholder="' + placeholder + '"';
|
|
169
|
+
if (href) desc += ' -> ' + href.substring(0, 60);
|
|
170
|
+
lines.push(desc);
|
|
171
|
+
});
|
|
172
|
+
return lines.join('\\n');
|
|
173
|
+
})()`,
|
|
174
|
+
...(surfaceId && { surfaceId }),
|
|
175
|
+
});
|
|
134
176
|
return {
|
|
135
|
-
content: [{ type: 'text', text:
|
|
177
|
+
content: [{ type: 'text', text: result.value }],
|
|
136
178
|
};
|
|
137
179
|
}
|
|
138
180
|
catch (error) {
|
|
@@ -146,7 +188,7 @@ function registerInspectionTools(server) {
|
|
|
146
188
|
// -----------------------------------------------------------------------
|
|
147
189
|
// browser_screenshot
|
|
148
190
|
// -----------------------------------------------------------------------
|
|
149
|
-
server.tool('browser_screenshot', 'Take a screenshot of the current page or a specific element. Returns the image as base64-encoded PNG.', {
|
|
191
|
+
server.tool('browser_screenshot', 'Take a screenshot of the current page or a specific element. Returns the image as base64-encoded PNG. Requires browser_open to be called first to establish a connection, even if a browser panel is already visible.', {
|
|
150
192
|
fullPage: zod_1.z
|
|
151
193
|
.boolean()
|
|
152
194
|
.optional()
|
|
@@ -158,27 +200,30 @@ function registerInspectionTools(server) {
|
|
|
158
200
|
surfaceId: optionalSurfaceId,
|
|
159
201
|
}, async ({ fullPage, ref, surfaceId }) => {
|
|
160
202
|
try {
|
|
161
|
-
|
|
162
|
-
if (!page) {
|
|
163
|
-
throw new Error('No browser page available. Call browser_open first.');
|
|
164
|
-
}
|
|
165
|
-
let buffer;
|
|
203
|
+
// Try Playwright for element-level screenshots (ref)
|
|
166
204
|
if (ref) {
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
|
|
205
|
+
const page = await engine.getPage(surfaceId);
|
|
206
|
+
if (page) {
|
|
207
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
208
|
+
if (!el) {
|
|
209
|
+
throw new Error(`Could not resolve ref="${ref}" to an element.`);
|
|
210
|
+
}
|
|
211
|
+
const buffer = (await el.screenshot());
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'image', data: buffer.toString('base64'), mimeType: 'image/png' }],
|
|
214
|
+
};
|
|
170
215
|
}
|
|
171
|
-
buffer = (await el.screenshot());
|
|
172
216
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
217
|
+
// Use RPC for fast, reliable screenshots (bypasses Playwright CDP discovery)
|
|
218
|
+
const result = await (0, wmux_client_1.sendRpc)('browser.screenshot', {
|
|
219
|
+
...(surfaceId && { surfaceId }),
|
|
220
|
+
...(fullPage && { fullPage }),
|
|
221
|
+
});
|
|
177
222
|
return {
|
|
178
223
|
content: [
|
|
179
224
|
{
|
|
180
225
|
type: 'image',
|
|
181
|
-
data:
|
|
226
|
+
data: result.data,
|
|
182
227
|
mimeType: 'image/png',
|
|
183
228
|
},
|
|
184
229
|
],
|
|
@@ -200,12 +245,31 @@ function registerInspectionTools(server) {
|
|
|
200
245
|
surfaceId: optionalSurfaceId,
|
|
201
246
|
}, async ({ expression, surfaceId }) => {
|
|
202
247
|
try {
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
|
|
248
|
+
const warnings = (0, security_1.detectDangerousPatterns)(expression);
|
|
249
|
+
if (warnings.length > 0) {
|
|
250
|
+
console.warn(`[browser_evaluate] Dangerous patterns detected: ${warnings.join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
let result;
|
|
253
|
+
// Try Playwright first for gesture-aware evaluation
|
|
254
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
255
|
+
if (page) {
|
|
256
|
+
result = await (0, anti_detection_1.evaluateWithGesture)(page, expression);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Fallback: RPC evaluation via main process webContents
|
|
260
|
+
const rpcResult = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
261
|
+
expression,
|
|
262
|
+
...(surfaceId && { surfaceId }),
|
|
263
|
+
});
|
|
264
|
+
result = rpcResult.value;
|
|
265
|
+
}
|
|
266
|
+
const text = typeof result === 'string' ? result : (JSON.stringify(result, null, 2) ?? 'undefined');
|
|
267
|
+
if (warnings.length > 0) {
|
|
268
|
+
const warningText = `\u26A0 Security warning: expression contains potentially dangerous patterns: ${warnings.join(', ')}. Exercise caution with untrusted input.\n\n`;
|
|
269
|
+
return {
|
|
270
|
+
content: [{ type: 'text', text: warningText + text }],
|
|
271
|
+
};
|
|
206
272
|
}
|
|
207
|
-
const result = await (0, anti_detection_1.evaluateWithGesture)(page, expression);
|
|
208
|
-
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
209
273
|
return {
|
|
210
274
|
content: [{ type: 'text', text: text ?? 'undefined' }],
|
|
211
275
|
};
|
|
@@ -235,7 +299,7 @@ function registerInspectionTools(server) {
|
|
|
235
299
|
try {
|
|
236
300
|
const page = await engine.getPage(surfaceId);
|
|
237
301
|
if (!page) {
|
|
238
|
-
throw new Error('No browser page available. Call browser_open first.');
|
|
302
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
239
303
|
}
|
|
240
304
|
const key = surfaceKey(surfaceId);
|
|
241
305
|
ensureConsoleListener(page, key);
|
|
@@ -280,7 +344,7 @@ function registerInspectionTools(server) {
|
|
|
280
344
|
try {
|
|
281
345
|
const page = await engine.getPage(surfaceId);
|
|
282
346
|
if (!page) {
|
|
283
|
-
throw new Error('No browser page available. Call browser_open first.');
|
|
347
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
284
348
|
}
|
|
285
349
|
const key = surfaceKey(surfaceId);
|
|
286
350
|
ensureNetworkListener(page, key);
|
|
@@ -320,7 +384,7 @@ function registerInspectionTools(server) {
|
|
|
320
384
|
try {
|
|
321
385
|
const page = await engine.getPage(surfaceId);
|
|
322
386
|
if (!page) {
|
|
323
|
-
throw new Error('No browser page available. Call browser_open first.');
|
|
387
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
324
388
|
}
|
|
325
389
|
const key = surfaceKey(surfaceId);
|
|
326
390
|
ensureNetworkListener(page, key);
|
|
@@ -370,7 +434,7 @@ function registerInspectionTools(server) {
|
|
|
370
434
|
try {
|
|
371
435
|
const page = await engine.getPage(surfaceId);
|
|
372
436
|
if (!page) {
|
|
373
|
-
throw new Error('No browser page available. Call browser_open first.');
|
|
437
|
+
throw new Error('No browser page available. Call browser_open with a URL first to establish a CDP connection (required even if a browser panel is already visible).');
|
|
374
438
|
}
|
|
375
439
|
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
376
440
|
if (!el) {
|
|
@@ -6,6 +6,7 @@ const PlaywrightEngine_1 = require("../PlaywrightEngine");
|
|
|
6
6
|
const snapshot_1 = require("../snapshot");
|
|
7
7
|
const dom_intelligence_1 = require("../dom-intelligence");
|
|
8
8
|
const human_typing_1 = require("../human-typing");
|
|
9
|
+
const wmux_client_1 = require("../../wmux-client");
|
|
9
10
|
// Optional surfaceId schema reused across tools
|
|
10
11
|
const optionalSurfaceId = zod_1.z
|
|
11
12
|
.string()
|
|
@@ -15,6 +16,46 @@ const REF_NOT_FOUND_HINT = 'Element with ref={ref} not found. Run browser_snapsh
|
|
|
15
16
|
function refNotFound(ref) {
|
|
16
17
|
return REF_NOT_FOUND_HINT.replace('{ref}', ref);
|
|
17
18
|
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// RPC-based interaction helpers (used when Playwright page is unavailable)
|
|
21
|
+
// These resolve elements via data-wmux-ref attributes set by browser_snapshot.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
async function rpcEval(expression, surfaceId) {
|
|
24
|
+
const result = await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
25
|
+
expression,
|
|
26
|
+
...(surfaceId && { surfaceId }),
|
|
27
|
+
});
|
|
28
|
+
return result.value;
|
|
29
|
+
}
|
|
30
|
+
async function rpcClick(ref, surfaceId, _double) {
|
|
31
|
+
// Use CDP click: first get element coordinates via JS, then dispatch mouse events
|
|
32
|
+
await (0, wmux_client_1.sendRpc)('browser.click.cdp', {
|
|
33
|
+
selector: `[data-wmux-ref="${ref}"]`,
|
|
34
|
+
...(surfaceId && { surfaceId }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function rpcFill(ref, value, surfaceId) {
|
|
38
|
+
// Click on the element first to focus it
|
|
39
|
+
await rpcClick(ref, surfaceId);
|
|
40
|
+
// Small delay for focus
|
|
41
|
+
await new Promise(r => setTimeout(r, 100));
|
|
42
|
+
// Select all existing text
|
|
43
|
+
await (0, wmux_client_1.sendRpc)('browser.evaluate', {
|
|
44
|
+
expression: `document.execCommand('selectAll')`,
|
|
45
|
+
...(surfaceId && { surfaceId }),
|
|
46
|
+
});
|
|
47
|
+
// Type the new value via CDP Input.insertText (handles CJK, React controlled inputs)
|
|
48
|
+
await (0, wmux_client_1.sendRpc)('browser.type.cdp', {
|
|
49
|
+
text: value,
|
|
50
|
+
...(surfaceId && { surfaceId }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function rpcPressKey(key, surfaceId) {
|
|
54
|
+
await (0, wmux_client_1.sendRpc)('browser.press.cdp', {
|
|
55
|
+
key,
|
|
56
|
+
...(surfaceId && { surfaceId }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
18
59
|
/**
|
|
19
60
|
* Register interaction-related MCP tools on the given server.
|
|
20
61
|
*
|
|
@@ -46,52 +87,43 @@ function registerInteractionTools(server) {
|
|
|
46
87
|
surfaceId: optionalSurfaceId,
|
|
47
88
|
}, async ({ ref, smartRef, double, surfaceId }) => {
|
|
48
89
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
// Try Playwright first
|
|
91
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
92
|
+
if (page) {
|
|
93
|
+
if (smartRef !== undefined) {
|
|
94
|
+
const selector = (0, dom_intelligence_1.getLocatorByRef)(smartRef);
|
|
95
|
+
if (!selector) {
|
|
96
|
+
throw new Error(`Element with smartRef=${smartRef} not found. Run browser_smart_snapshot to get current refs.`);
|
|
97
|
+
}
|
|
98
|
+
const locator = page.locator(selector);
|
|
99
|
+
if (double)
|
|
100
|
+
await locator.dblclick();
|
|
101
|
+
else
|
|
102
|
+
await locator.click();
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: 'text', text: `Clicked${double ? ' (double)' : ''} element smartRef=${smartRef}` }],
|
|
105
|
+
};
|
|
65
106
|
}
|
|
107
|
+
if (!ref)
|
|
108
|
+
throw new Error('Either ref or smartRef must be provided.');
|
|
109
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
110
|
+
if (!el)
|
|
111
|
+
throw new Error(refNotFound(ref));
|
|
112
|
+
if (double)
|
|
113
|
+
await el.dblclick();
|
|
114
|
+
else
|
|
115
|
+
await el.click();
|
|
66
116
|
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text',
|
|
70
|
-
text: `Clicked${double ? ' (double)' : ''} element smartRef=${smartRef}`,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
117
|
+
content: [{ type: 'text', text: `Clicked${double ? ' (double)' : ''} element ref=${ref}` }],
|
|
73
118
|
};
|
|
74
119
|
}
|
|
75
|
-
|
|
120
|
+
// RPC fallback
|
|
121
|
+
if (!ref && smartRef === undefined)
|
|
76
122
|
throw new Error('Either ref or smartRef must be provided.');
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!el) {
|
|
80
|
-
throw new Error(refNotFound(ref));
|
|
81
|
-
}
|
|
82
|
-
if (double) {
|
|
83
|
-
await el.dblclick();
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
await el.click();
|
|
87
|
-
}
|
|
123
|
+
const resolvedRef = ref ?? String(smartRef);
|
|
124
|
+
await rpcClick(resolvedRef, surfaceId, double);
|
|
88
125
|
return {
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: 'text',
|
|
92
|
-
text: `Clicked${double ? ' (double)' : ''} element ref=${ref}`,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
126
|
+
content: [{ type: 'text', text: `Clicked${double ? ' (double)' : ''} element ref=${resolvedRef}` }],
|
|
95
127
|
};
|
|
96
128
|
}
|
|
97
129
|
catch (error) {
|
|
@@ -119,24 +151,26 @@ function registerInteractionTools(server) {
|
|
|
119
151
|
surfaceId: optionalSurfaceId,
|
|
120
152
|
}, async ({ ref, text, submit, humanlike, surfaceId }) => {
|
|
121
153
|
try {
|
|
122
|
-
const page = await engine.getPage(surfaceId);
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
154
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
155
|
+
if (page) {
|
|
156
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
157
|
+
if (!el)
|
|
158
|
+
throw new Error(refNotFound(ref));
|
|
159
|
+
if (humanlike) {
|
|
160
|
+
await el.click();
|
|
161
|
+
await (0, human_typing_1.typeHumanlike)(page, '', text);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await el.fill(text);
|
|
165
|
+
}
|
|
166
|
+
if (submit)
|
|
167
|
+
await page.keyboard.press('Enter');
|
|
134
168
|
}
|
|
135
169
|
else {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
170
|
+
// RPC fallback
|
|
171
|
+
await rpcFill(ref, text, surfaceId);
|
|
172
|
+
if (submit)
|
|
173
|
+
await rpcPressKey('Enter', surfaceId);
|
|
140
174
|
}
|
|
141
175
|
return {
|
|
142
176
|
content: [
|
|
@@ -168,20 +202,27 @@ function registerInteractionTools(server) {
|
|
|
168
202
|
surfaceId: optionalSurfaceId,
|
|
169
203
|
}, async ({ fields, surfaceId }) => {
|
|
170
204
|
try {
|
|
171
|
-
const page = await engine.getPage(surfaceId);
|
|
172
|
-
if (!page) {
|
|
173
|
-
throw new Error('No browser page available. Call browser_open first.');
|
|
174
|
-
}
|
|
205
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
175
206
|
let filled = 0;
|
|
176
207
|
const errors = [];
|
|
177
208
|
for (const field of fields) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
209
|
+
try {
|
|
210
|
+
if (page) {
|
|
211
|
+
const el = await (0, snapshot_1.resolveRef)(page, field.ref);
|
|
212
|
+
if (!el) {
|
|
213
|
+
errors.push(refNotFound(field.ref));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
await el.fill(field.value);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await rpcFill(field.ref, field.value, surfaceId);
|
|
220
|
+
}
|
|
221
|
+
filled++;
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
182
225
|
}
|
|
183
|
-
await el.fill(field.value);
|
|
184
|
-
filled++;
|
|
185
226
|
}
|
|
186
227
|
let resultText = `Filled ${filled}/${fields.length} field(s).`;
|
|
187
228
|
if (errors.length > 0) {
|
|
@@ -210,11 +251,13 @@ function registerInteractionTools(server) {
|
|
|
210
251
|
surfaceId: optionalSurfaceId,
|
|
211
252
|
}, async ({ key, surfaceId }) => {
|
|
212
253
|
try {
|
|
213
|
-
const page = await engine.getPage(surfaceId);
|
|
214
|
-
if (
|
|
215
|
-
|
|
254
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
255
|
+
if (page) {
|
|
256
|
+
await page.keyboard.press(key);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
await rpcPressKey(key, surfaceId);
|
|
216
260
|
}
|
|
217
|
-
await page.keyboard.press(key);
|
|
218
261
|
return {
|
|
219
262
|
content: [{ type: 'text', text: `Pressed key: ${key}` }],
|
|
220
263
|
};
|
|
@@ -235,19 +278,28 @@ function registerInteractionTools(server) {
|
|
|
235
278
|
surfaceId: optionalSurfaceId,
|
|
236
279
|
}, async ({ ref, surfaceId }) => {
|
|
237
280
|
try {
|
|
238
|
-
const page = await engine.getPage(surfaceId);
|
|
239
|
-
if (
|
|
240
|
-
|
|
281
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
282
|
+
if (page) {
|
|
283
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
284
|
+
if (!el)
|
|
285
|
+
throw new Error(refNotFound(ref));
|
|
286
|
+
await el.hover();
|
|
241
287
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
288
|
+
else {
|
|
289
|
+
// RPC fallback: dispatch mouseover event
|
|
290
|
+
const val = await rpcEval(`(() => {
|
|
291
|
+
const el = document.querySelector('[data-wmux-ref="${ref}"]');
|
|
292
|
+
if (!el) return 'not_found';
|
|
293
|
+
el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
294
|
+
el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
|
|
295
|
+
el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
|
296
|
+
return 'ok';
|
|
297
|
+
})()`, surfaceId);
|
|
298
|
+
if (val === 'not_found')
|
|
299
|
+
throw new Error(refNotFound(ref));
|
|
245
300
|
}
|
|
246
|
-
await el.hover();
|
|
247
301
|
return {
|
|
248
|
-
content: [
|
|
249
|
-
{ type: 'text', text: `Hovered over element ref=${ref}` },
|
|
250
|
-
],
|
|
302
|
+
content: [{ type: 'text', text: `Hovered over element ref=${ref}` }],
|
|
251
303
|
};
|
|
252
304
|
}
|
|
253
305
|
catch (error) {
|
|
@@ -269,40 +321,49 @@ function registerInteractionTools(server) {
|
|
|
269
321
|
surfaceId: optionalSurfaceId,
|
|
270
322
|
}, async ({ sourceRef, targetRef, surfaceId }) => {
|
|
271
323
|
try {
|
|
272
|
-
const page = await engine.getPage(surfaceId);
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
324
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
325
|
+
if (page) {
|
|
326
|
+
const sourceEl = await (0, snapshot_1.resolveRef)(page, sourceRef);
|
|
327
|
+
if (!sourceEl)
|
|
328
|
+
throw new Error(refNotFound(sourceRef));
|
|
329
|
+
const targetEl = await (0, snapshot_1.resolveRef)(page, targetRef);
|
|
330
|
+
if (!targetEl)
|
|
331
|
+
throw new Error(refNotFound(targetRef));
|
|
332
|
+
const sourceBox = await sourceEl.boundingBox();
|
|
333
|
+
const targetBox = await targetEl.boundingBox();
|
|
334
|
+
if (!sourceBox || !targetBox) {
|
|
335
|
+
throw new Error('Could not determine bounding box for source or target element.');
|
|
336
|
+
}
|
|
337
|
+
const sourceX = sourceBox.x + sourceBox.width / 2;
|
|
338
|
+
const sourceY = sourceBox.y + sourceBox.height / 2;
|
|
339
|
+
const targetX = targetBox.x + targetBox.width / 2;
|
|
340
|
+
const targetY = targetBox.y + targetBox.height / 2;
|
|
341
|
+
await page.mouse.move(sourceX, sourceY);
|
|
342
|
+
await page.mouse.down();
|
|
343
|
+
await page.mouse.move(targetX, targetY, { steps: 10 });
|
|
344
|
+
await page.mouse.up();
|
|
283
345
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
346
|
+
else {
|
|
347
|
+
// RPC fallback: simplified drag via JS events
|
|
348
|
+
const val = await rpcEval(`(() => {
|
|
349
|
+
const src = document.querySelector('[data-wmux-ref="${sourceRef}"]');
|
|
350
|
+
const tgt = document.querySelector('[data-wmux-ref="${targetRef}"]');
|
|
351
|
+
if (!src) return 'source_not_found';
|
|
352
|
+
if (!tgt) return 'target_not_found';
|
|
353
|
+
const dt = new DataTransfer();
|
|
354
|
+
src.dispatchEvent(new DragEvent('dragstart', { bubbles: true, dataTransfer: dt }));
|
|
355
|
+
tgt.dispatchEvent(new DragEvent('dragover', { bubbles: true, dataTransfer: dt }));
|
|
356
|
+
tgt.dispatchEvent(new DragEvent('drop', { bubbles: true, dataTransfer: dt }));
|
|
357
|
+
src.dispatchEvent(new DragEvent('dragend', { bubbles: true, dataTransfer: dt }));
|
|
358
|
+
return 'ok';
|
|
359
|
+
})()`, surfaceId);
|
|
360
|
+
if (val === 'source_not_found')
|
|
361
|
+
throw new Error(refNotFound(sourceRef));
|
|
362
|
+
if (val === 'target_not_found')
|
|
363
|
+
throw new Error(refNotFound(targetRef));
|
|
289
364
|
}
|
|
290
|
-
// Perform drag from center of source to center of target
|
|
291
|
-
const sourceX = sourceBox.x + sourceBox.width / 2;
|
|
292
|
-
const sourceY = sourceBox.y + sourceBox.height / 2;
|
|
293
|
-
const targetX = targetBox.x + targetBox.width / 2;
|
|
294
|
-
const targetY = targetBox.y + targetBox.height / 2;
|
|
295
|
-
await page.mouse.move(sourceX, sourceY);
|
|
296
|
-
await page.mouse.down();
|
|
297
|
-
await page.mouse.move(targetX, targetY, { steps: 10 });
|
|
298
|
-
await page.mouse.up();
|
|
299
365
|
return {
|
|
300
|
-
content: [
|
|
301
|
-
{
|
|
302
|
-
type: 'text',
|
|
303
|
-
text: `Dragged element ref=${sourceRef} to ref=${targetRef}`,
|
|
304
|
-
},
|
|
305
|
-
],
|
|
366
|
+
content: [{ type: 'text', text: `Dragged element ref=${sourceRef} to ref=${targetRef}` }],
|
|
306
367
|
};
|
|
307
368
|
}
|
|
308
369
|
catch (error) {
|
|
@@ -324,22 +385,28 @@ function registerInteractionTools(server) {
|
|
|
324
385
|
surfaceId: optionalSurfaceId,
|
|
325
386
|
}, async ({ ref, values, surfaceId }) => {
|
|
326
387
|
try {
|
|
327
|
-
const page = await engine.getPage(surfaceId);
|
|
328
|
-
if (
|
|
329
|
-
|
|
388
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
389
|
+
if (page) {
|
|
390
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
391
|
+
if (!el)
|
|
392
|
+
throw new Error(refNotFound(ref));
|
|
393
|
+
await el.selectOption(values);
|
|
330
394
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
395
|
+
else {
|
|
396
|
+
const escapedValues = JSON.stringify(values);
|
|
397
|
+
const val = await rpcEval(`(() => {
|
|
398
|
+
const el = document.querySelector('[data-wmux-ref="${ref}"]');
|
|
399
|
+
if (!el || el.tagName !== 'SELECT') return 'not_found';
|
|
400
|
+
const vals = ${escapedValues};
|
|
401
|
+
[...el.options].forEach(o => { o.selected = vals.includes(o.value); });
|
|
402
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
403
|
+
return 'ok';
|
|
404
|
+
})()`, surfaceId);
|
|
405
|
+
if (val === 'not_found')
|
|
406
|
+
throw new Error(refNotFound(ref));
|
|
334
407
|
}
|
|
335
|
-
await el.selectOption(values);
|
|
336
408
|
return {
|
|
337
|
-
content: [
|
|
338
|
-
{
|
|
339
|
-
type: 'text',
|
|
340
|
-
text: `Selected value(s) [${values.join(', ')}] in element ref=${ref}`,
|
|
341
|
-
},
|
|
342
|
-
],
|
|
409
|
+
content: [{ type: 'text', text: `Selected value(s) [${values.join(', ')}] in element ref=${ref}` }],
|
|
343
410
|
};
|
|
344
411
|
}
|
|
345
412
|
catch (error) {
|
|
@@ -358,22 +425,25 @@ function registerInteractionTools(server) {
|
|
|
358
425
|
surfaceId: optionalSurfaceId,
|
|
359
426
|
}, async ({ ref, surfaceId }) => {
|
|
360
427
|
try {
|
|
361
|
-
const page = await engine.getPage(surfaceId);
|
|
362
|
-
if (
|
|
363
|
-
|
|
428
|
+
const page = await engine.getPage(surfaceId).catch(() => null);
|
|
429
|
+
if (page) {
|
|
430
|
+
const el = await (0, snapshot_1.resolveRef)(page, ref);
|
|
431
|
+
if (!el)
|
|
432
|
+
throw new Error(refNotFound(ref));
|
|
433
|
+
await el.scrollIntoViewIfNeeded();
|
|
364
434
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
435
|
+
else {
|
|
436
|
+
const val = await rpcEval(`(() => {
|
|
437
|
+
const el = document.querySelector('[data-wmux-ref="${ref}"]');
|
|
438
|
+
if (!el) return 'not_found';
|
|
439
|
+
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
440
|
+
return 'ok';
|
|
441
|
+
})()`, surfaceId);
|
|
442
|
+
if (val === 'not_found')
|
|
443
|
+
throw new Error(refNotFound(ref));
|
|
368
444
|
}
|
|
369
|
-
await el.scrollIntoViewIfNeeded();
|
|
370
445
|
return {
|
|
371
|
-
content: [
|
|
372
|
-
{
|
|
373
|
-
type: 'text',
|
|
374
|
-
text: `Scrolled element ref=${ref} into view`,
|
|
375
|
-
},
|
|
376
|
-
],
|
|
446
|
+
content: [{ type: 'text', text: `Scrolled element ref=${ref} into view` }],
|
|
377
447
|
};
|
|
378
448
|
}
|
|
379
449
|
catch (error) {
|