@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.
@@ -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
- const page = await engine.getPage(surfaceId);
130
- if (!page) {
131
- throw new Error('No browser page available. Call browser_open first.');
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
- const snapshot = await (0, snapshot_1.generateSnapshot)(page, { format: format ?? 'ai' });
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: snapshot }],
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
- const page = await engine.getPage(surfaceId);
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 el = await (0, snapshot_1.resolveRef)(page, ref);
168
- if (!el) {
169
- throw new Error(`Could not resolve ref="${ref}" to an element.`);
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
- else {
174
- buffer = (await page.screenshot({ fullPage: fullPage ?? false }));
175
- }
176
- const base64 = buffer.toString('base64');
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: base64,
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 page = await engine.getPage(surfaceId);
204
- if (!page) {
205
- throw new Error('No browser page available. Call browser_open first.');
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
- const page = await engine.getPage(surfaceId);
50
- if (!page) {
51
- throw new Error('No browser page available. Call browser_open first.');
52
- }
53
- if (smartRef !== undefined) {
54
- // Use dom-intelligence ref resolution
55
- const selector = (0, dom_intelligence_1.getLocatorByRef)(smartRef);
56
- if (!selector) {
57
- throw new Error(`Element with smartRef=${smartRef} not found. Run browser_smart_snapshot to get current refs.`);
58
- }
59
- const locator = page.locator(selector);
60
- if (double) {
61
- await locator.dblclick();
62
- }
63
- else {
64
- await locator.click();
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
- if (!ref) {
120
+ // RPC fallback
121
+ if (!ref && smartRef === undefined)
76
122
  throw new Error('Either ref or smartRef must be provided.');
77
- }
78
- const el = await (0, snapshot_1.resolveRef)(page, ref);
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 (!page) {
124
- throw new Error('No browser page available. Call browser_open first.');
125
- }
126
- const el = await (0, snapshot_1.resolveRef)(page, ref);
127
- if (!el) {
128
- throw new Error(refNotFound(ref));
129
- }
130
- if (humanlike) {
131
- // Focus the element first, then use human-like typing via keyboard
132
- await el.click();
133
- await (0, human_typing_1.typeHumanlike)(page, '', text);
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
- await el.fill(text);
137
- }
138
- if (submit) {
139
- await page.keyboard.press('Enter');
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
- const el = await (0, snapshot_1.resolveRef)(page, field.ref);
179
- if (!el) {
180
- errors.push(refNotFound(field.ref));
181
- continue;
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 (!page) {
215
- throw new Error('No browser page available. Call browser_open first.');
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 (!page) {
240
- throw new Error('No browser page available. Call browser_open first.');
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
- const el = await (0, snapshot_1.resolveRef)(page, ref);
243
- if (!el) {
244
- throw new Error(refNotFound(ref));
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 (!page) {
274
- throw new Error('No browser page available. Call browser_open first.');
275
- }
276
- const sourceEl = await (0, snapshot_1.resolveRef)(page, sourceRef);
277
- if (!sourceEl) {
278
- throw new Error(refNotFound(sourceRef));
279
- }
280
- const targetEl = await (0, snapshot_1.resolveRef)(page, targetRef);
281
- if (!targetEl) {
282
- throw new Error(refNotFound(targetRef));
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
- // Get bounding boxes for source and target
285
- const sourceBox = await sourceEl.boundingBox();
286
- const targetBox = await targetEl.boundingBox();
287
- if (!sourceBox || !targetBox) {
288
- throw new Error('Could not determine bounding box for source or target element.');
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 (!page) {
329
- throw new Error('No browser page available. Call browser_open first.');
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
- const el = await (0, snapshot_1.resolveRef)(page, ref);
332
- if (!el) {
333
- throw new Error(refNotFound(ref));
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 (!page) {
363
- throw new Error('No browser page available. Call browser_open first.');
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
- const el = await (0, snapshot_1.resolveRef)(page, ref);
366
- if (!el) {
367
- throw new Error(refNotFound(ref));
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) {