@weavetab/mcp 2.5.0 → 2.5.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.
Files changed (168) hide show
  1. package/AGENTS.md +49 -49
  2. package/ARCHITECTURE.md +590 -590
  3. package/LICENSE +216 -216
  4. package/README.md +269 -269
  5. package/SECURITY.md +207 -207
  6. package/TROUBLESHOOTING.md +373 -365
  7. package/dist/anticaptcha/detector.js +395 -395
  8. package/dist/anticaptcha/solver.js +130 -130
  9. package/dist/cdp/bridge.js +73 -73
  10. package/dist/cdp/connector.js +46 -22
  11. package/dist/cdp/extension.js +34 -34
  12. package/dist/cdp/helpers.js +174 -174
  13. package/dist/cdp/pruner.js +195 -195
  14. package/dist/cdp/ref_cache.js +66 -66
  15. package/dist/cdp/security.js +10 -10
  16. package/dist/cdp/verify.js +57 -57
  17. package/dist/cdp/walker.js +563 -525
  18. package/dist/cli/plugin.js +26 -3
  19. package/dist/config/loader.d.ts +0 -3
  20. package/dist/config/loader.js +0 -9
  21. package/dist/index.js +0 -4
  22. package/dist/intelligence/intent.js +81 -81
  23. package/dist/intelligence/sessionMemory.js +2 -2
  24. package/dist/overlay/ghost.js +22 -22
  25. package/dist/overlay/injector.js +89 -89
  26. package/dist/plugin/extension_bridge.js +54 -54
  27. package/dist/plugin/interface.d.ts +7 -0
  28. package/dist/plugin/loader.js +3 -4
  29. package/dist/plugin/schema.d.ts +11 -0
  30. package/dist/plugin/schema.js +59 -0
  31. package/dist/security/guard.js +18 -18
  32. package/dist/sensors/actionability.js +167 -167
  33. package/dist/sensors/guard.js +29 -29
  34. package/dist/sensors/network.js +15 -17
  35. package/dist/server.d.ts +1 -1
  36. package/dist/server.js +173 -144
  37. package/dist/state/delta.js +6 -7
  38. package/dist/tools/automation.js +85 -85
  39. package/dist/tools/canvas.js +48 -25
  40. package/dist/tools/checkpoint.js +94 -35
  41. package/dist/tools/click.js +52 -41
  42. package/dist/tools/clipboard.js +80 -74
  43. package/dist/tools/design_extract.js +441 -441
  44. package/dist/tools/dialog.js +13 -13
  45. package/dist/tools/emulation.d.ts +2 -0
  46. package/dist/tools/emulation.js +7 -0
  47. package/dist/tools/fill.js +344 -347
  48. package/dist/tools/find.js +78 -52
  49. package/dist/tools/highlight.js +67 -67
  50. package/dist/tools/inspect.js +54 -25
  51. package/dist/tools/key.js +42 -35
  52. package/dist/tools/navigate.d.ts +1 -1
  53. package/dist/tools/navigate.js +35 -35
  54. package/dist/tools/office/parse.js +62 -18
  55. package/dist/tools/pdf.d.ts +9 -0
  56. package/dist/tools/pdf.js +7 -4
  57. package/dist/tools/performance.d.ts +11 -4
  58. package/dist/tools/performance.js +94 -42
  59. package/dist/tools/performance_test.d.ts +2 -2
  60. package/dist/tools/performance_test.js +69 -69
  61. package/dist/tools/pointer.js +116 -116
  62. package/dist/tools/read.js +99 -67
  63. package/dist/tools/recording.js +9 -0
  64. package/dist/tools/scrape.js +146 -146
  65. package/dist/tools/screenshot.d.ts +2 -0
  66. package/dist/tools/screenshot.js +46 -4
  67. package/dist/tools/scroll.d.ts +3 -1
  68. package/dist/tools/scroll.js +13 -15
  69. package/dist/tools/select.d.ts +3 -0
  70. package/dist/tools/select.js +131 -124
  71. package/dist/tools/snapshot.js +8 -6
  72. package/dist/tools/storage.d.ts +5 -1
  73. package/dist/tools/storage.js +16 -0
  74. package/dist/tools/tabs.d.ts +7 -5
  75. package/dist/tools/tabs.js +79 -26
  76. package/dist/tools/thought.js +8 -8
  77. package/dist/tools/type.js +274 -274
  78. package/dist/tools/type_secret.js +23 -23
  79. package/dist/tools/upload.js +3 -3
  80. package/dist/tools/video_compiler.js +9 -0
  81. package/dist/tools/wait.js +35 -35
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/docs/DOCUMENTATION.md +583 -585
  85. package/docs/PLUGINS.md +139 -121
  86. package/docs/TOOLS.md +3 -4
  87. package/docs/architecture/blind-injection.md +83 -83
  88. package/docs/architecture/overview.md +49 -49
  89. package/docs/architecture/security.md +36 -36
  90. package/docs/architecture/transport.md +37 -37
  91. package/docs/cli/cli-commands.md +50 -50
  92. package/docs/cli/cli-flags.md +28 -28
  93. package/docs/cli/cli-scripting.md +35 -35
  94. package/docs/configuration/config-locations.md +23 -23
  95. package/docs/configuration/configuration.md +39 -27
  96. package/docs/configuration/env-variables.md +19 -19
  97. package/docs/configuration/folders.md +62 -55
  98. package/docs/desktop-automation/electron-apps.md +225 -214
  99. package/docs/get-started/installation.md +1 -1
  100. package/docs/get-started/introduction.md +60 -60
  101. package/docs/get-started/quickstart.md +2 -2
  102. package/docs/get-started/setup-wizard.md +27 -27
  103. package/docs/intelligence/site-profiles.md +151 -151
  104. package/docs/tools/tools.md +582 -631
  105. package/docs/troubleshooting/trouble-browser.md +27 -27
  106. package/docs/troubleshooting/trouble-loop.md +31 -31
  107. package/ext-dist/background.js +415 -415
  108. package/ext-dist/content.js +2527 -2527
  109. package/ext-dist/dashboard.html +583 -583
  110. package/ext-dist/hud/hud.js +3 -3
  111. package/ext-dist/hud/index.html +20 -20
  112. package/ext-dist/manifest.json +73 -73
  113. package/ext-dist/styles.css +223 -223
  114. package/mcp.json +796 -805
  115. package/package.json +118 -118
  116. package/server.json +23 -23
  117. package/dist/cdp/walker.test.d.ts +0 -2
  118. package/dist/cdp/walker.test.js +0 -172
  119. package/dist/intelligence/sessionMemory.test.d.ts +0 -2
  120. package/dist/intelligence/sessionMemory.test.js +0 -62
  121. package/dist/intelligence/trail.test.d.ts +0 -2
  122. package/dist/intelligence/trail.test.js +0 -110
  123. package/dist/state/cache.test.d.ts +0 -2
  124. package/dist/state/cache.test.js +0 -76
  125. package/dist/state/delta.test.d.ts +0 -2
  126. package/dist/state/delta.test.js +0 -279
  127. package/dist/state/groups.d.ts +0 -15
  128. package/dist/state/groups.js +0 -80
  129. package/dist/tools/accessibility.d.ts +0 -14
  130. package/dist/tools/accessibility.js +0 -32
  131. package/dist/tools/ask.d.ts +0 -14
  132. package/dist/tools/ask.js +0 -49
  133. package/dist/tools/click_and_wait.d.ts +0 -33
  134. package/dist/tools/click_and_wait.js +0 -149
  135. package/dist/tools/drag.d.ts +0 -15
  136. package/dist/tools/drag.js +0 -29
  137. package/dist/tools/geo.d.ts +0 -19
  138. package/dist/tools/geo.js +0 -29
  139. package/dist/tools/github/analyze.d.ts +0 -4
  140. package/dist/tools/github/analyze.js +0 -113
  141. package/dist/tools/github/cache.d.ts +0 -21
  142. package/dist/tools/github/cache.js +0 -28
  143. package/dist/tools/github/index.d.ts +0 -19
  144. package/dist/tools/github/index.js +0 -40
  145. package/dist/tools/github/issues.d.ts +0 -9
  146. package/dist/tools/github/issues.js +0 -88
  147. package/dist/tools/github/pr.d.ts +0 -5
  148. package/dist/tools/github/pr.js +0 -88
  149. package/dist/tools/github/rateLimiter.d.ts +0 -4
  150. package/dist/tools/github/rateLimiter.js +0 -46
  151. package/dist/tools/github/read.d.ts +0 -34
  152. package/dist/tools/github/read.js +0 -74
  153. package/dist/tools/github/repoContext.d.ts +0 -20
  154. package/dist/tools/github/repoContext.js +0 -129
  155. package/dist/tools/hover.d.ts +0 -19
  156. package/dist/tools/hover.js +0 -57
  157. package/dist/tools/integration.test.d.ts +0 -2
  158. package/dist/tools/integration.test.js +0 -96
  159. package/dist/tools/macro_compiler.d.ts +0 -7
  160. package/dist/tools/macro_compiler.js +0 -82
  161. package/dist/tools/mouse.d.ts +0 -21
  162. package/dist/tools/mouse.js +0 -62
  163. package/dist/tools/response.d.ts +0 -129
  164. package/dist/tools/response.js +0 -5
  165. package/dist/tools/wait_for.d.ts +0 -23
  166. package/dist/tools/wait_for.js +0 -30
  167. package/dist/tools/windows.d.ts +0 -22
  168. package/dist/tools/windows.js +0 -57
@@ -3,9 +3,9 @@ import { SessionSecretTracker } from "../security/secrets.js";
3
3
  import { getStoredRef, getStoredProps } from "../state/delta.js";
4
4
  import { getRawCachedActionMap } from "../state/cache.js";
5
5
  // ─── Public types ─────────────────────────────────────────────────────────────
6
- export const WT_REGISTRY_INIT_EXPR = `
7
- window.__weavetab = window.__weavetab || {};
8
- window.__weavetab.registry = window.__weavetab.registry || new WeakMap();
6
+ export const WT_REGISTRY_INIT_EXPR = `
7
+ window.__weavetab = window.__weavetab || {};
8
+ window.__weavetab.registry = window.__weavetab.registry || new WeakMap();
9
9
  `;
10
10
  const viewportCache = new Map();
11
11
  export function clearViewportCache(targetId) {
@@ -101,7 +101,23 @@ export async function buildActionMap(session, options = {}, targetId = "default"
101
101
  // ── Begin a new walk pass — resets per-pass dedup registry ─────────────────
102
102
  const passCounterBase = beginWalkPass();
103
103
  const url = options.url ?? (await session.Page.getFrameTree()).frameTree.frame.url;
104
- const title = options.title ?? "";
104
+ let title = options.title;
105
+ if (!title) {
106
+ try {
107
+ const evalRes = await session.Runtime.evaluate({
108
+ expression: "document.title",
109
+ returnByValue: true
110
+ });
111
+ if (evalRes?.result?.value) {
112
+ title = String(evalRes.result.value);
113
+ }
114
+ }
115
+ catch { /* ignore */ }
116
+ }
117
+ if (!title) {
118
+ const { sessionRegistry } = await import("../state/session.js");
119
+ title = sessionRegistry.getAll().find(c => c.targetId === targetId)?.title || "";
120
+ }
105
121
  // Inherit sticky pruning state from session if not explicitly set in options
106
122
  if (options.prune === undefined) {
107
123
  const { sessionRegistry } = await import("../state/session.js");
@@ -129,10 +145,10 @@ export async function buildActionMap(session, options = {}, targetId = "default"
129
145
  if (sampleIds.length > 0) {
130
146
  try {
131
147
  const { result: domVerify } = await session.Runtime.evaluate({
132
- expression: `(function(ids) {
133
- return ids.filter(function(id) {
134
- return !document.querySelector('[data-wt-id="' + id + '"]');
135
- }).length;
148
+ expression: `(function(ids) {
149
+ return ids.filter(function(id) {
150
+ return !document.querySelector('[data-wt-id="' + id + '"]');
151
+ }).length;
136
152
  })(${JSON.stringify(sampleIds)})`,
137
153
  returnByValue: true
138
154
  });
@@ -149,14 +165,14 @@ export async function buildActionMap(session, options = {}, targetId = "default"
149
165
  // Architecture change: Try to read from extension's live index first
150
166
  // The extension now maintains window.__wt_liveIndex via continuous MutationObserver
151
167
  const { result: liveIndexResult } = await session.Runtime.evaluate({
152
- expression: `(function() {
153
- if (typeof window.__wt_liveIndex !== 'undefined' && Array.isArray(window.__wt_liveIndex)) {
154
- return JSON.stringify({
155
- elements: window.__wt_liveIndex,
156
- source: 'live-index'
157
- });
158
- }
159
- return JSON.stringify({ elements: null, source: 'unavailable' });
168
+ expression: `(function() {
169
+ if (typeof window.__wt_liveIndex !== 'undefined' && Array.isArray(window.__wt_liveIndex)) {
170
+ return JSON.stringify({
171
+ elements: window.__wt_liveIndex,
172
+ source: 'live-index'
173
+ });
174
+ }
175
+ return JSON.stringify({ elements: null, source: 'unavailable' });
160
176
  })()`,
161
177
  returnByValue: true
162
178
  });
@@ -178,256 +194,257 @@ export async function buildActionMap(session, options = {}, targetId = "default"
178
194
  // Hardened with per-element try-catch — one bad node no longer kills the entire map
179
195
  // Deep shadow DOM traversal — falls back to recursive shadow-penetrating query when flat query is sparse
180
196
  const { result } = await session.Runtime.evaluate({
181
- expression: `(function() {
182
- var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
183
-
184
- // deepQuery has a hard depth limit and a visited-set guard so circular
185
- // shadow root references can't cause a stack overflow.
186
- function deepQuery(root, selector, visited, depth) {
187
- if (!visited) visited = new Set();
188
- if (depth === undefined) depth = 0;
189
- if (depth > 10) return []; // hard depth limit
190
- if (visited.has(root)) return []; // break cycles
191
- visited.add(root);
192
- var all = [];
193
- try {
194
- var direct = root.querySelectorAll(selector);
195
- for (var i = 0; i < direct.length; i++) all.push(direct[i]);
196
- } catch (e) {}
197
- // TreeWalker — lazy DOM traversal, avoids building a static NodeList of every element
198
- try {
199
- var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
200
- while (walker.nextNode()) {
201
- try {
202
- var el = walker.currentNode;
203
- if (el.shadowRoot && !visited.has(el.shadowRoot)) {
204
- var inner = deepQuery(el.shadowRoot, selector, visited, depth + 1);
205
- for (var k = 0; k < inner.length; k++) all.push(inner[k]);
206
- }
207
- } catch (e) {}
208
- }
209
- } catch (e) {}
210
- return all;
211
- }
212
-
213
- var hasShadowCache = ${JSON.stringify(!!shadowDomCache.get(targetId))};
214
- var useDeep = ${JSON.stringify(!!options.deep)} || hasShadowCache;
215
- var els = [];
216
-
217
- // Phase 1: flat query (skip when cache confirms shadow DOM exists — deepQuery covers root too)
218
- if (!hasShadowCache) {
219
- try {
220
- var flatEls = document.querySelectorAll(INTERACTIVE_SEL);
221
- for (var fi = 0; fi < flatEls.length; fi++) els.push(flatEls[fi]);
222
- } catch (e) {}
223
- }
224
-
225
- // Phase 2: deep query if cache hit OR flat results are sparse
226
- if (useDeep || els.length < 3) {
227
- try {
228
- var deepEls = deepQuery(document, INTERACTIVE_SEL);
229
- // Deduplicate by comparing tagName + label + position fingerprint
230
- var seen = new Set();
231
- for (var di = 0; di < els.length; di++) {
232
- try {
233
- var r = els[di].getBoundingClientRect();
234
- seen.add(els[di].tagName + '|' + Math.round(r.left) + '|' + Math.round(r.top));
235
- } catch (e) {}
236
- }
237
- for (var di2 = 0; di2 < deepEls.length; di2++) {
238
- try {
239
- var el2 = deepEls[di2];
240
- var r2 = el2.getBoundingClientRect();
241
- var key = el2.tagName + '|' + Math.round(r2.left) + '|' + Math.round(r2.top);
242
- if (!seen.has(key)) {
243
- els.push(el2);
244
- seen.add(key);
245
- }
246
- } catch (e) {}
247
- }
248
- } catch (e) {}
249
- }
250
-
251
- // ── Content-addressable IDs — stable across re-renders ────────────────
252
- // djb2 hash of (tagName + role + trimmed text/label + sibling-index + parent context).
253
- // Same element always gets the same ID even if the walker runs multiple
254
- // times. Sequential counter only used as collision fallback.
255
- function wtHashId(el) {
256
- var tag = el.tagName || '';
257
- var role = el.getAttribute('role') || '';
258
- var text = (el.getAttribute('aria-label') || el.textContent || el.value || el.placeholder || '').slice(0, 60).trim();
259
- var rect = el.getBoundingClientRect();
260
- var absX = Math.round(rect.left + (window.scrollX || 0));
261
- var absY = Math.round(rect.top + (window.scrollY || 0));
262
- var path = '';
263
- try {
264
- var curr = el;
265
- for(var level = 0; level < 3 && curr; level++) {
266
- var p = curr.parentElement;
267
- if (!p) break;
268
- var idx = 0;
269
- var siblings = p.children;
270
- for (var si = 0; si < siblings.length; si++) {
271
- if (siblings[si] === curr) { idx = si; break; }
272
- }
273
- path += '|' + (curr.tagName||'') + '[' + idx + ']';
274
- curr = p;
275
- }
276
- } catch(e) {}
277
- var raw = tag + '|' + role + '|' + text + '|' + absX + '|' + absY + path;
278
- var h = 5381;
279
- for (var ci = 0; ci < raw.length; ci++) {
280
- h = ((h << 5) + h) ^ raw.charCodeAt(ci);
281
- h = h >>> 0; // keep unsigned 32-bit
282
- }
283
- return 'wt' + h.toString(36);
284
- }
285
- var usedHashes = {};
286
- // Bug #5 fix: seed collision counter from the global walk-pass base so that
287
- // elements in different iframes processed in the same pass never share a
288
- // numeric suffix. passCounterBase is injected by the TypeScript host.
289
- var collisionCounter = ${JSON.stringify(passCounterBase)};
290
- var _coIdx = 0;
291
- var results = [];
292
- for (var i2 = 0; i2 < els.length; i2++) {
293
- try {
294
- var el = els[i2];
295
- if (el.closest('[data-wt-ignore]') !== null) continue;
296
- var rect = el.getBoundingClientRect();
297
- if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
298
- var id = el.getAttribute('data-wt-id');
299
- if (!id) {
300
- var hid = wtHashId(el);
301
- // Resolve collision: if same hash already used, append counter
302
- if (usedHashes[hid]) {
303
- hid = hid + '_' + (collisionCounter++);
304
- }
305
- usedHashes[hid] = true;
306
- id = hid;
307
- el.setAttribute('data-wt-id', id);
308
- }
309
- var role = el.tagName.toLowerCase();
310
- if (role === 'a') role = 'link';
311
- else if (role === 'input' && (el.type === 'text' || el.type === 'search')) role = 'textbox';
312
- var frameUrl = '';
313
- try { if (el.ownerDocument !== document) frameUrl = el.ownerDocument.defaultView.location.href; } catch(e) { frameUrl = 'cross-origin-' + (_coIdx++); }
314
- var secretRegex = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha/i;
315
- var isSecret = el.hasAttribute('data-wt-secret') ||
316
- (el.tagName === 'INPUT' && (
317
- el.type === 'password' ||
318
- secretRegex.test(el.getAttribute('autocomplete') || '') ||
319
- secretRegex.test(el.getAttribute('name') || '') ||
320
- secretRegex.test(el.getAttribute('id') || '') ||
321
- secretRegex.test(el.getAttribute('aria-label') || '')
322
- ));
323
- var labelStr = el.getAttribute('aria-label') || el.innerText || (isSecret ? '' : el.value) || el.placeholder || '';
324
- // For radio/checkbox, serialize checked state as 'true'/'false' so the agent
325
- // can see which option is selected without having to scrape the DOM separately.
326
- var elType = (el.type || '').toLowerCase();
327
- var elRole = (el.getAttribute('role') || el.tagName.toLowerCase());
328
- var isCheckable = elType === 'radio' || elType === 'checkbox' ||
329
- elRole === 'radio' || elRole === 'checkbox';
330
- var isOption = el.tagName === 'OPTION';
331
- var valStr;
332
- if (isCheckable) {
333
- valStr = el.checked ? 'true' : 'false';
334
- } else if (isOption) {
335
- valStr = el.selected ? 'true' : 'false';
336
- } else {
337
- valStr = el.value !== undefined ? String(el.value) : undefined;
338
- }
339
- var txtContent = (el.textContent || '').slice(0, 200);
340
-
341
- if (isSecret) {
342
- var rawAria = el.getAttribute('aria-label') || '';
343
- var rawPlaceholder = el.placeholder || '';
344
- labelStr = rawAria || rawPlaceholder || '[REDACTED SECRET FIELD]';
345
- if (valStr !== undefined) valStr = '[REDACTED]';
346
- txtContent = '[REDACTED]';
347
- }
348
-
349
- // Capture structural idiom (e.g. aria-controls, aria-owns, etc.)
350
- var idiom = '';
351
- var CONTROLS_ATTRS = ['aria-controls', 'aria-owns', 'data-target', 'data-bs-target'];
352
- for (var ca = 0; ca < CONTROLS_ATTRS.length; ca++) {
353
- if (el.hasAttribute(CONTROLS_ATTRS[ca])) {
354
- idiom = CONTROLS_ATTRS[ca];
355
- break;
356
- }
357
- }
358
-
359
- // ── Container context binding ─────────────────────────────────────
360
- // Walk up the DOM to find a meaningful card/row/list-item heading that
361
- // describes this element's entity context, e.g. "Pro Plan ($29/mo)".
362
- var containerCtx = '';
363
- try {
364
- var CARD_SEL = '[class*="card"],[class*="row"],[class*="item"],[class*="product"],[class*="plan"],[class*="entry"],[class*="result"],[class*="tile"],[class*="option"],[data-entity]';
365
- var ancestor = el.parentElement;
366
- var depth3 = 0;
367
- while (ancestor && depth3 < 8) {
368
- if (ancestor.matches && ancestor.matches(CARD_SEL)) {
369
- var heading = ancestor.querySelector('h1,h2,h3,h4,h5,h6,[class*="title"],[class*="name"],[class*="heading"],[class*="label"]');
370
- if (heading) {
371
- var hText = (heading.innerText || heading.textContent || '').trim().slice(0, 60);
372
- if (hText && hText !== labelStr) { containerCtx = hText; break; }
373
- }
374
- var allText = (ancestor.innerText || ancestor.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
375
- if (allText && allText !== labelStr) { containerCtx = allText; break; }
376
- }
377
- ancestor = ancestor.parentElement;
378
- depth3++;
379
- }
380
- } catch(e) {}
381
-
382
- // ── Modal detection ───────────────────────────────────────────────
383
- var inModal = false;
384
- var modalTitleStr = '';
385
- try {
386
- var MODAL_SEL = 'dialog[open],[role="dialog"],[aria-modal="true"],[class*="modal"],[class*="overlay"],[class*="drawer"]';
387
- var modalAnc = el.parentElement;
388
- var md = 0;
389
- while (modalAnc && md < 15) {
390
- if (modalAnc.matches && modalAnc.matches(MODAL_SEL)) {
391
- inModal = true;
392
- var mh = modalAnc.querySelector('h1,h2,h3,h4,[class*="title"]');
393
- if (mh) {
394
- var lbId = modalAnc.getAttribute('aria-labelledby');
395
- var lbEl = lbId ? document.getElementById(lbId) : null;
396
- modalTitleStr = ((lbEl || mh).innerText || (lbEl || mh).textContent || '').trim().slice(0, 60);
397
- }
398
- break;
399
- }
400
- modalAnc = modalAnc.parentElement;
401
- md++;
402
- }
403
- } catch(e) {}
404
-
405
- results.push({
406
- id: String(id),
407
- role: el.getAttribute('role') || role,
408
- type: elType || undefined,
409
- label: labelStr,
410
- placeholder: el.placeholder || undefined,
411
- value: valStr,
412
- x: Math.round(rect.left + rect.width / 2),
413
- y: Math.round(rect.top + rect.height / 2),
414
- selector: '[data-wt-id="' + id + '"]',
415
- href: el.href,
416
- textContent: txtContent,
417
- className: el.className || '',
418
- frameUrl: frameUrl,
419
- structuralIdiom: idiom || undefined,
420
- containerContext: containerCtx || undefined,
421
- isModal: inModal || undefined,
422
- modalTitle: modalTitleStr || undefined,
423
- disabled: (el.disabled || el.getAttribute('aria-disabled') === 'true') || undefined,
424
- checked: isCheckable ? el.checked : undefined,
425
- });
426
- } catch (e) {
427
- // Silently skip elements that cause errors (cross-origin SVG, detached nodes, etc.)
428
- }
429
- }
430
- return JSON.stringify({ elements: results, shadowFound: results.length > 0 && typeof flatEls !== 'undefined' && els.length > flatEls.length });
197
+ expression: `(function() {
198
+ var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
199
+
200
+ // deepQuery has a hard depth limit and a visited-set guard so circular
201
+ // shadow root references can't cause a stack overflow.
202
+ function deepQuery(root, selector, visited, depth) {
203
+ if (!visited) visited = new Set();
204
+ if (depth === undefined) depth = 0;
205
+ if (depth > 10) return []; // hard depth limit
206
+ if (visited.has(root)) return []; // break cycles
207
+ visited.add(root);
208
+ var all = [];
209
+ try {
210
+ var direct = root.querySelectorAll(selector);
211
+ for (var i = 0; i < direct.length; i++) all.push(direct[i]);
212
+ } catch (e) {}
213
+ // TreeWalker — lazy DOM traversal, avoids building a static NodeList of every element
214
+ try {
215
+ var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
216
+ while (walker.nextNode()) {
217
+ try {
218
+ var el = walker.currentNode;
219
+ if (el.shadowRoot && !visited.has(el.shadowRoot)) {
220
+ var inner = deepQuery(el.shadowRoot, selector, visited, depth + 1);
221
+ for (var k = 0; k < inner.length; k++) all.push(inner[k]);
222
+ }
223
+ } catch (e) {}
224
+ }
225
+ } catch (e) {}
226
+ return all;
227
+ }
228
+
229
+ var hasShadowCache = ${JSON.stringify(!!shadowDomCache.get(targetId))};
230
+ var useDeep = ${JSON.stringify(!!options.deep)} || hasShadowCache;
231
+ var els = [];
232
+
233
+ // Phase 1: flat query (skip when cache confirms shadow DOM exists — deepQuery covers root too)
234
+ if (!hasShadowCache) {
235
+ try {
236
+ var flatEls = document.querySelectorAll(INTERACTIVE_SEL);
237
+ for (var fi = 0; fi < flatEls.length; fi++) els.push(flatEls[fi]);
238
+ } catch (e) {}
239
+ }
240
+
241
+ // Phase 2: deep query if cache hit OR flat results are sparse
242
+ if (useDeep || els.length < 3) {
243
+ try {
244
+ var deepEls = deepQuery(document, INTERACTIVE_SEL);
245
+ // Deduplicate by comparing tagName + label + position fingerprint
246
+ var seen = new Set();
247
+ for (var di = 0; di < els.length; di++) {
248
+ try {
249
+ var r = els[di].getBoundingClientRect();
250
+ seen.add(els[di].tagName + '|' + Math.round(r.left) + '|' + Math.round(r.top));
251
+ } catch (e) {}
252
+ }
253
+ for (var di2 = 0; di2 < deepEls.length; di2++) {
254
+ try {
255
+ var el2 = deepEls[di2];
256
+ var r2 = el2.getBoundingClientRect();
257
+ var key = el2.tagName + '|' + Math.round(r2.left) + '|' + Math.round(r2.top);
258
+ if (!seen.has(key)) {
259
+ els.push(el2);
260
+ seen.add(key);
261
+ }
262
+ } catch (e) {}
263
+ }
264
+ } catch (e) {}
265
+ }
266
+
267
+ // ── Content-addressable IDs — stable across re-renders ────────────────
268
+ // djb2 hash of (tagName + role + trimmed text/label + sibling-index + parent context).
269
+ // Same element always gets the same ID even if the walker runs multiple
270
+ // times. Sequential counter only used as collision fallback.
271
+ function wtHashId(el) {
272
+ var tag = el.tagName || '';
273
+ var role = el.getAttribute('role') || '';
274
+ var text = (el.getAttribute('aria-label') || el.textContent || el.value || el.placeholder || '').slice(0, 60).trim();
275
+ var rect = el.getBoundingClientRect();
276
+ var absX = Math.round(rect.left + (window.scrollX || 0));
277
+ var absY = Math.round(rect.top + (window.scrollY || 0));
278
+ var path = '';
279
+ try {
280
+ var curr = el;
281
+ for(var level = 0; level < 3 && curr; level++) {
282
+ var p = curr.parentElement;
283
+ if (!p) break;
284
+ var idx = 0;
285
+ var siblings = p.children;
286
+ for (var si = 0; si < siblings.length; si++) {
287
+ if (siblings[si] === curr) { idx = si; break; }
288
+ }
289
+ path += '|' + (curr.tagName||'') + '[' + idx + ']';
290
+ curr = p;
291
+ }
292
+ } catch(e) {}
293
+ var raw = tag + '|' + role + '|' + text + '|' + absX + '|' + absY + path;
294
+ var h = 5381;
295
+ for (var ci = 0; ci < raw.length; ci++) {
296
+ h = ((h << 5) + h) ^ raw.charCodeAt(ci);
297
+ h = h >>> 0; // keep unsigned 32-bit
298
+ }
299
+ return 'wt' + h.toString(36);
300
+ }
301
+ var usedHashes = {};
302
+ // Bug #5 fix: seed collision counter from the global walk-pass base so that
303
+ // elements in different iframes processed in the same pass never share a
304
+ // numeric suffix. passCounterBase is injected by the TypeScript host.
305
+ var collisionCounter = ${JSON.stringify(passCounterBase)};
306
+ var _coIdx = 0;
307
+ var results = [];
308
+ for (var i2 = 0; i2 < els.length; i2++) {
309
+ try {
310
+ var el = els[i2];
311
+ if (el.closest('[data-wt-ignore]') !== null) continue;
312
+ var rect = el.getBoundingClientRect();
313
+ if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
314
+ var id = el.getAttribute('data-wt-id');
315
+ if (!id) {
316
+ var hid = wtHashId(el);
317
+ // Resolve collision: if same hash already used, append counter
318
+ if (usedHashes[hid]) {
319
+ hid = hid + '_' + (collisionCounter++);
320
+ }
321
+ usedHashes[hid] = true;
322
+ id = hid;
323
+ el.setAttribute('data-wt-id', id);
324
+ }
325
+ var role = el.tagName.toLowerCase();
326
+ if (role === 'a') role = 'link';
327
+ else if (role === 'input' && (el.type === 'text' || el.type === 'search')) role = 'textbox';
328
+ var frameUrl = '';
329
+ try { if (el.ownerDocument !== document) frameUrl = el.ownerDocument.defaultView.location.href; } catch(e) { frameUrl = 'cross-origin-' + (_coIdx++); }
330
+ var secretRegex = /password|passcode|pin|passphrase|secret|token|auth.?key|access.?code|mot.?de.?passe|contrase[nñ]a|kennwort|senha/i;
331
+ var isSecret = el.hasAttribute('data-wt-secret') ||
332
+ (el.tagName === 'INPUT' && (
333
+ el.type === 'password' ||
334
+ secretRegex.test(el.getAttribute('autocomplete') || '') ||
335
+ secretRegex.test(el.getAttribute('name') || '') ||
336
+ secretRegex.test(el.getAttribute('id') || '') ||
337
+ secretRegex.test(el.getAttribute('aria-label') || '')
338
+ ));
339
+ var richPh = el.getAttribute('placeholder') || el.getAttribute('data-placeholder') || el.getAttribute('aria-placeholder') || (el.querySelector ? (el.querySelector('[data-slate-placeholder]')?.textContent || '') : '') || '';
340
+ var labelStr = el.getAttribute('aria-label') || el.innerText || (isSecret ? '' : el.value) || richPh || '';
341
+ // For radio/checkbox, serialize checked state as 'true'/'false' so the agent
342
+ // can see which option is selected without having to scrape the DOM separately.
343
+ var elType = (el.type || '').toLowerCase();
344
+ var elRole = (el.getAttribute('role') || el.tagName.toLowerCase());
345
+ var isCheckable = elType === 'radio' || elType === 'checkbox' ||
346
+ elRole === 'radio' || elRole === 'checkbox';
347
+ var isOption = el.tagName === 'OPTION';
348
+ var valStr;
349
+ if (isCheckable) {
350
+ valStr = el.checked ? 'true' : 'false';
351
+ } else if (isOption) {
352
+ valStr = el.selected ? 'true' : 'false';
353
+ } else {
354
+ valStr = el.value !== undefined ? String(el.value) : undefined;
355
+ }
356
+ var txtContent = (el.textContent || '').slice(0, 200);
357
+
358
+ if (isSecret) {
359
+ var rawAria = el.getAttribute('aria-label') || '';
360
+ var rawPlaceholder = el.placeholder || '';
361
+ labelStr = rawAria || rawPlaceholder || '[REDACTED SECRET FIELD]';
362
+ if (valStr !== undefined) valStr = '[REDACTED]';
363
+ txtContent = '[REDACTED]';
364
+ }
365
+
366
+ // Capture structural idiom (e.g. aria-controls, aria-owns, etc.)
367
+ var idiom = '';
368
+ var CONTROLS_ATTRS = ['aria-controls', 'aria-owns', 'data-target', 'data-bs-target'];
369
+ for (var ca = 0; ca < CONTROLS_ATTRS.length; ca++) {
370
+ if (el.hasAttribute(CONTROLS_ATTRS[ca])) {
371
+ idiom = CONTROLS_ATTRS[ca];
372
+ break;
373
+ }
374
+ }
375
+
376
+ // ── Container context binding ─────────────────────────────────────
377
+ // Walk up the DOM to find a meaningful card/row/list-item heading that
378
+ // describes this element's entity context, e.g. "Pro Plan ($29/mo)".
379
+ var containerCtx = '';
380
+ try {
381
+ var CARD_SEL = '[class*="card"],[class*="row"],[class*="item"],[class*="product"],[class*="plan"],[class*="entry"],[class*="result"],[class*="tile"],[class*="option"],[data-entity]';
382
+ var ancestor = el.parentElement;
383
+ var depth3 = 0;
384
+ while (ancestor && depth3 < 8) {
385
+ if (ancestor.matches && ancestor.matches(CARD_SEL)) {
386
+ var heading = ancestor.querySelector('h1,h2,h3,h4,h5,h6,[class*="title"],[class*="name"],[class*="heading"],[class*="label"]');
387
+ if (heading) {
388
+ var hText = (heading.innerText || heading.textContent || '').trim().slice(0, 60);
389
+ if (hText && hText !== labelStr) { containerCtx = hText; break; }
390
+ }
391
+ var allText = (ancestor.innerText || ancestor.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
392
+ if (allText && allText !== labelStr) { containerCtx = allText; break; }
393
+ }
394
+ ancestor = ancestor.parentElement;
395
+ depth3++;
396
+ }
397
+ } catch(e) {}
398
+
399
+ // ── Modal detection ───────────────────────────────────────────────
400
+ var inModal = false;
401
+ var modalTitleStr = '';
402
+ try {
403
+ var MODAL_SEL = 'dialog[open],[role="dialog"],[aria-modal="true"],[class*="modal"],[class*="overlay"],[class*="drawer"]';
404
+ var modalAnc = el.parentElement;
405
+ var md = 0;
406
+ while (modalAnc && md < 15) {
407
+ if (modalAnc.matches && modalAnc.matches(MODAL_SEL)) {
408
+ inModal = true;
409
+ var mh = modalAnc.querySelector('h1,h2,h3,h4,[class*="title"]');
410
+ if (mh) {
411
+ var lbId = modalAnc.getAttribute('aria-labelledby');
412
+ var lbEl = lbId ? document.getElementById(lbId) : null;
413
+ modalTitleStr = ((lbEl || mh).innerText || (lbEl || mh).textContent || '').trim().slice(0, 60);
414
+ }
415
+ break;
416
+ }
417
+ modalAnc = modalAnc.parentElement;
418
+ md++;
419
+ }
420
+ } catch(e) {}
421
+
422
+ results.push({
423
+ id: String(id),
424
+ role: el.getAttribute('role') || role,
425
+ type: elType || undefined,
426
+ label: labelStr,
427
+ placeholder: el.placeholder || richPh || undefined,
428
+ value: valStr,
429
+ x: Math.round(rect.left + rect.width / 2),
430
+ y: Math.round(rect.top + rect.height / 2),
431
+ selector: '[data-wt-id="' + id + '"]',
432
+ href: el.href,
433
+ textContent: txtContent,
434
+ className: el.className || '',
435
+ frameUrl: frameUrl,
436
+ structuralIdiom: idiom || undefined,
437
+ containerContext: containerCtx || undefined,
438
+ isModal: inModal || undefined,
439
+ modalTitle: modalTitleStr || undefined,
440
+ disabled: (el.disabled || el.getAttribute('aria-disabled') === 'true') || undefined,
441
+ checked: isCheckable ? el.checked : undefined,
442
+ });
443
+ } catch (e) {
444
+ // Silently skip elements that cause errors (cross-origin SVG, detached nodes, etc.)
445
+ }
446
+ }
447
+ return JSON.stringify({ elements: results, shadowFound: results.length > 0 && typeof flatEls !== 'undefined' && els.length > flatEls.length });
431
448
  })()`,
432
449
  returnByValue: true
433
450
  });
@@ -459,31 +476,31 @@ export async function buildActionMap(session, options = {}, targetId = "default"
459
476
  for (const [, childSession] of childSessions.entries()) {
460
477
  try {
461
478
  const { result: childResult } = await childSession.Runtime.evaluate({
462
- expression: `(function() {
463
- var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
464
- var els = document.querySelectorAll(INTERACTIVE_SEL);
465
- var results = [];
466
- for (var i = 0; i < els.length; i++) {
467
- var el = els[i];
468
- var rect = el.getBoundingClientRect();
469
- if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
470
- var id = el.getAttribute('data-wt-id');
471
- if (!id) {
472
- id = 'wv_' + Math.random().toString(36).slice(2, 9);
473
- el.setAttribute('data-wt-id', id);
474
- }
475
- results.push({
476
- id: id,
477
- role: el.getAttribute('role') || el.tagName.toLowerCase(),
478
- label: el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '',
479
- value: el.value,
480
- x: Math.round(rect.left + rect.width / 2),
481
- y: Math.round(rect.top + rect.height / 2),
482
- selector: '[data-wt-id="' + id + '"]',
483
- frameUrl: window.location.href,
484
- });
485
- }
486
- return JSON.stringify(results);
479
+ expression: `(function() {
480
+ var INTERACTIVE_SEL = 'a, button, input, textarea, select, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
481
+ var els = document.querySelectorAll(INTERACTIVE_SEL);
482
+ var results = [];
483
+ for (var i = 0; i < els.length; i++) {
484
+ var el = els[i];
485
+ var rect = el.getBoundingClientRect();
486
+ if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
487
+ var id = el.getAttribute('data-wt-id');
488
+ if (!id) {
489
+ id = 'wv_' + Math.random().toString(36).slice(2, 9);
490
+ el.setAttribute('data-wt-id', id);
491
+ }
492
+ results.push({
493
+ id: id,
494
+ role: el.getAttribute('role') || el.tagName.toLowerCase(),
495
+ label: el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '',
496
+ value: el.value,
497
+ x: Math.round(rect.left + rect.width / 2),
498
+ y: Math.round(rect.top + rect.height / 2),
499
+ selector: '[data-wt-id="' + id + '"]',
500
+ frameUrl: window.location.href,
501
+ });
502
+ }
503
+ return JSON.stringify(results);
487
504
  })()`,
488
505
  returnByValue: true,
489
506
  });
@@ -624,12 +641,12 @@ export async function buildActionMap(session, options = {}, targetId = "default"
624
641
  let scopedElements = prunedElements;
625
642
  if (options.scope) {
626
643
  const { result: scopeResult } = await session.Runtime.evaluate({
627
- expression: `(function(sel) {
628
- const el = document.querySelector(sel);
629
- if (!el) return null;
630
- const r = el.getBoundingClientRect();
631
- const ids = [...el.querySelectorAll('[data-wt-id]')].map(e => e.getAttribute('data-wt-id')).filter(Boolean);
632
- return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, ids: ids };
644
+ expression: `(function(sel) {
645
+ const el = document.querySelector(sel);
646
+ if (!el) return null;
647
+ const r = el.getBoundingClientRect();
648
+ const ids = [...el.querySelectorAll('[data-wt-id]')].map(e => e.getAttribute('data-wt-id')).filter(Boolean);
649
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, ids: ids };
633
650
  })(${JSON.stringify(options.scope)})`,
634
651
  returnByValue: true,
635
652
  }).catch(() => ({ result: { value: null } }));
@@ -697,15 +714,27 @@ export function findAllMatches(elements, query) {
697
714
  // 1. Exact match (highest priority)
698
715
  elements.forEach(el => { if (n(el.label) === q)
699
716
  addMatch(el); });
717
+ // 1.5 Prefix stripped exact match (e.g. searching "general" for "#general" or vice versa)
718
+ const qStripped = q.replace(/^[#@]/, '');
719
+ if (qStripped !== q) {
720
+ elements.forEach(el => {
721
+ const elLabelClean = n(el.label).replace(/^[#@]/, '');
722
+ if (elLabelClean === qStripped)
723
+ addMatch(el);
724
+ });
725
+ }
700
726
  // 2. placeholder text
701
- elements.forEach(el => { if (n(el.placeholder) === q)
727
+ elements.forEach(el => { if (n(el.placeholder) === q || n(el.placeholder) === qStripped)
702
728
  addMatch(el); });
703
729
  // 3. data-testid
704
730
  elements.forEach(el => { if (n(el.testId) === q)
705
731
  addMatch(el); });
706
732
  // 4. visible text content / partial label match
707
- elements.forEach(el => { if (n(el.label).includes(q))
708
- addMatch(el); });
733
+ elements.forEach(el => {
734
+ const lbl = n(el.label);
735
+ if (lbl.includes(q) || (qStripped.length >= 3 && lbl.includes(qStripped)))
736
+ addMatch(el);
737
+ });
709
738
  // 4.5 fuzzy partial match (split words)
710
739
  const words = q.split(/\s+/);
711
740
  if (words.length > 1) {
@@ -766,99 +795,108 @@ export async function resolveByAria(session, query) {
766
795
  if (!query || !query.trim())
767
796
  return null;
768
797
  const resp = await session.Runtime.evaluate({
769
- expression: `(function(q) {
770
- var INTERACTIVE = 'a[href], button, input:not([type="hidden"]), textarea, select,' +
771
- '[role="button"],[role="link"],[role="textbox"],[role="checkbox"],' +
772
- '[role="radio"],[role="combobox"],[role="searchbox"],[role="menuitem"],' +
773
- '[role="tab"],[role="option"],[tabindex]:not([tabindex="-1"])';
774
-
775
- function norm(s) { return (s || '').toLowerCase().replace(/[.\\u2026\\s]+/g, ' ').trim(); }
776
- var qLow = norm(q);
777
-
778
- // Score: prefer leaf elements (no interactive child = score 0, has child = score 10)
779
- function score(el) {
780
- try { return el.querySelector(INTERACTIVE) !== null ? 10 : 0; } catch(e) { return 0; }
781
- }
782
- function best(list) {
783
- if (!list || !list.length) return null;
784
- var arr = Array.prototype.slice.call(list);
785
- arr.sort(function(a,b){ return score(a)-score(b); });
786
- return arr[0];
787
- }
788
- function makeResult(el) {
789
- if (!el) return null;
790
- var r = el.getBoundingClientRect();
791
- if (!r || r.width === 0 || r.height === 0) return null;
792
- if (r.bottom < 0 || r.top > window.innerHeight) return null;
793
- var sel = '';
794
- var wtId = el.getAttribute('data-wt-id');
795
- if (wtId) {
796
- sel = '[data-wt-id="' + wtId.replace(/"/g, '\\\\"') + '"]';
797
- } else if (el.id) {
798
- try { sel = '#' + CSS.escape(el.id); } catch(e) { sel = '#' + el.id; }
799
- } else {
800
- var aria = el.getAttribute('aria-label');
801
- if (aria) sel = el.tagName.toLowerCase() + '[aria-label="' + aria.replace(/"/g, '\\\\"') + '"]';
802
- }
803
- if (!sel) {
804
- var tmp = 'wt-tmp-' + Math.random().toString(36).slice(2,8);
805
- el.setAttribute('data-wt-id', tmp);
806
- sel = '[data-wt-id="' + tmp + '"]';
807
- }
808
- return {
809
- label: el.getAttribute('aria-label') || (el.textContent||'').trim().slice(0,80) || q,
810
- x: Math.round(r.left + r.width/2),
811
- y: Math.round(r.top + r.height/2),
812
- selector: sel
813
- };
814
- }
815
-
816
- var all, found, i, hit;
817
-
818
- // 1. Exact aria-label
819
- all = document.querySelectorAll('[aria-label]');
820
- found = [];
821
- for (i=0;i<all.length;i++) {
822
- if (norm(all[i].getAttribute('aria-label'))===qLow) found.push(all[i]);
823
- }
824
- if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
825
-
826
- // 2. Exact innerText on interactive elements
827
- all = document.querySelectorAll(INTERACTIVE);
828
- found = [];
829
- for (i=0;i<all.length;i++) {
830
- var t = norm(all[i].innerText||all[i].textContent);
831
- if (t===qLow) found.push(all[i]);
832
- }
833
- if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
834
-
835
- // 3. Placeholder
836
- all = document.querySelectorAll('[placeholder]');
837
- found = [];
838
- for (i=0;i<all.length;i++) {
839
- if (norm(all[i].getAttribute('placeholder'))===qLow) found.push(all[i]);
840
- }
841
- if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
842
-
843
- // 4. title / alt
844
- all = document.querySelectorAll('[title],[alt]');
845
- found = [];
846
- for (i=0;i<all.length;i++) {
847
- var v = norm(all[i].getAttribute('title')||all[i].getAttribute('alt'));
848
- if (v===qLow) found.push(all[i]);
849
- }
850
- if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
851
-
852
- // 5. Fuzzy partial innerText
853
- all = document.querySelectorAll(INTERACTIVE);
854
- found = [];
855
- for (i=0;i<all.length;i++) {
856
- var t5 = norm(all[i].innerText||all[i].textContent);
857
- if (t5.indexOf(qLow)>=0 && t5.length < qLow.length*4) found.push(all[i]);
858
- }
859
- if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
860
-
861
- return null;
798
+ expression: `(function(q) {
799
+ var INTERACTIVE = 'a[href], button, input:not([type="hidden"]), textarea, select,' +
800
+ '[role="button"],[role="link"],[role="textbox"],[role="checkbox"],' +
801
+ '[role="radio"],[role="combobox"],[role="searchbox"],[role="menuitem"],' +
802
+ '[role="tab"],[role="option"],[tabindex]:not([tabindex="-1"])';
803
+
804
+ function norm(s) { return (s || '').toLowerCase().replace(/[.\\u2026\\s]+/g, ' ').trim(); }
805
+ var qLow = norm(q);
806
+
807
+ // Score: prefer leaf elements (no interactive child = score 0, has child = score 10)
808
+ function score(el) {
809
+ try { return el.querySelector(INTERACTIVE) !== null ? 10 : 0; } catch(e) { return 0; }
810
+ }
811
+ function best(list) {
812
+ if (!list || !list.length) return null;
813
+ var arr = Array.prototype.slice.call(list);
814
+ arr.sort(function(a,b){ return score(a)-score(b); });
815
+ return arr[0];
816
+ }
817
+ function makeResult(el) {
818
+ if (!el) return null;
819
+ var r = el.getBoundingClientRect();
820
+ if (!r || r.width === 0 || r.height === 0) return null;
821
+ if (r.bottom < 0 || r.top > window.innerHeight) return null;
822
+ var sel = '';
823
+ var wtId = el.getAttribute('data-wt-id');
824
+ if (wtId) {
825
+ sel = '[data-wt-id="' + wtId.replace(/"/g, '\\\\"') + '"]';
826
+ } else if (el.id) {
827
+ try { sel = '#' + CSS.escape(el.id); } catch(e) { sel = '#' + el.id; }
828
+ } else {
829
+ var aria = el.getAttribute('aria-label');
830
+ if (aria) sel = el.tagName.toLowerCase() + '[aria-label="' + aria.replace(/"/g, '\\\\"') + '"]';
831
+ }
832
+ if (!sel) {
833
+ var tmp = 'wt-tmp-' + Math.random().toString(36).slice(2,8);
834
+ el.setAttribute('data-wt-id', tmp);
835
+ sel = '[data-wt-id="' + tmp + '"]';
836
+ }
837
+ return {
838
+ label: el.getAttribute('aria-label') || (el.textContent||'').trim().slice(0,80) || q,
839
+ x: Math.round(r.left + r.width/2),
840
+ y: Math.round(r.top + r.height/2),
841
+ selector: sel
842
+ };
843
+ }
844
+
845
+ var all, found, i, hit;
846
+
847
+ // 1. Exact aria-label
848
+ all = document.querySelectorAll('[aria-label]');
849
+ found = [];
850
+ for (i=0;i<all.length;i++) {
851
+ if (norm(all[i].getAttribute('aria-label'))===qLow) found.push(all[i]);
852
+ }
853
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
854
+
855
+ // 1.5 Partial aria-label match (e.g. "general" in "general (text channel)")
856
+ found = [];
857
+ for (i=0;i<all.length;i++) {
858
+ var aVal = norm(all[i].getAttribute('aria-label'));
859
+ if (aVal.indexOf(qLow)>=0 && aVal.length < qLow.length*4) found.push(all[i]);
860
+ }
861
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
862
+
863
+ // 2. Exact innerText on interactive elements
864
+ all = document.querySelectorAll(INTERACTIVE);
865
+ found = [];
866
+ for (i=0;i<all.length;i++) {
867
+ var t = norm(all[i].innerText||all[i].textContent);
868
+ if (t===qLow) found.push(all[i]);
869
+ }
870
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
871
+
872
+ // 3. Placeholder and rich placeholders (data-placeholder, aria-placeholder, data-slate-placeholder)
873
+ all = document.querySelectorAll('[placeholder],[data-placeholder],[aria-placeholder],[data-slate-placeholder]');
874
+ found = [];
875
+ for (i=0;i<all.length;i++) {
876
+ var phVal = norm(all[i].getAttribute('placeholder') || all[i].getAttribute('data-placeholder') || all[i].getAttribute('aria-placeholder') || all[i].textContent);
877
+ if (phVal===qLow || phVal.indexOf(qLow)>=0) found.push(all[i]);
878
+ }
879
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
880
+
881
+ // 4. title / alt
882
+ all = document.querySelectorAll('[title],[alt]');
883
+ found = [];
884
+ for (i=0;i<all.length;i++) {
885
+ var v = norm(all[i].getAttribute('title')||all[i].getAttribute('alt'));
886
+ if (v===qLow) found.push(all[i]);
887
+ }
888
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
889
+
890
+ // 5. Fuzzy partial innerText
891
+ all = document.querySelectorAll(INTERACTIVE);
892
+ found = [];
893
+ for (i=0;i<all.length;i++) {
894
+ var t5 = norm(all[i].innerText||all[i].textContent);
895
+ if (t5.indexOf(qLow)>=0 && t5.length < qLow.length*4) found.push(all[i]);
896
+ }
897
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
898
+
899
+ return null;
862
900
  })(${JSON.stringify(query)})`,
863
901
  returnByValue: true,
864
902
  }).catch(() => null);
@@ -1070,138 +1108,138 @@ export async function recoverDetachedRef(session, targetId, ref, preferredCandid
1070
1108
  };
1071
1109
  try {
1072
1110
  const { result } = await session.Runtime.evaluate({
1073
- expression: `(function(refId, meta) {
1074
- // 1. Check if node with data-wt-id=refId is already in DOM, connected, and visible
1075
- const existing = document.querySelector('[data-wt-id="' + refId + '"]');
1076
- if (existing && existing.isConnected) {
1077
- const r = existing.getBoundingClientRect();
1078
- if (r.width > 0 && r.height > 0) {
1079
- return {
1080
- success: true,
1081
- strategy: 'connected_existing',
1082
- x: Math.round(r.left + r.width / 2),
1083
- y: Math.round(r.top + r.height / 2),
1084
- label: existing.getAttribute('aria-label') || (existing.innerText || existing.textContent || '').trim() || existing.placeholder || '',
1085
- role: existing.getAttribute('role') || existing.tagName.toLowerCase()
1086
- };
1087
- }
1088
- }
1089
-
1090
- function isValidCandidate(el) {
1091
- if (!el || !el.isConnected) return false;
1092
- const r = el.getBoundingClientRect();
1093
- if (r.width <= 0 || r.height <= 0) return false;
1094
- try {
1095
- const style = window.getComputedStyle(el);
1096
- if (style.visibility === 'hidden' || style.display === 'none') return false;
1097
- } catch(e) {}
1098
- return true;
1099
- }
1100
-
1101
- // 2. Strategy: Stored Selector (if stable CSS selector recorded)
1102
- if (meta.selector && !meta.selector.startsWith('[data-wt-id')) {
1103
- try {
1104
- const el = document.querySelector(meta.selector);
1105
- if (isValidCandidate(el)) {
1106
- el.setAttribute('data-wt-id', refId);
1107
- const r = el.getBoundingClientRect();
1108
- return {
1109
- success: true,
1110
- strategy: 'stored_selector',
1111
- x: Math.round(r.left + r.width / 2),
1112
- y: Math.round(r.top + r.height / 2),
1113
- label: el.getAttribute('aria-label') || (el.innerText || el.textContent || '').trim() || el.placeholder || '',
1114
- role: el.getAttribute('role') || el.tagName.toLowerCase()
1115
- };
1116
- }
1117
- } catch(e) {}
1118
- }
1119
-
1120
- const normLabel = (meta.label || '').toLowerCase().trim();
1121
- const normPlaceholder = (meta.placeholder || '').toLowerCase().trim();
1122
-
1123
- const candidates = Array.from(document.querySelectorAll(
1124
- 'button, a, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="tab"], [tabindex="0"]'
1125
- ));
1126
-
1127
- // 3. Strategy: Exact Label / Text / Aria match
1128
- if (normLabel) {
1129
- for (const c of candidates) {
1130
- if (!isValidCandidate(c)) continue;
1131
- const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
1132
- const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
1133
- const val = (c.value || '').toLowerCase().trim();
1134
- const placeholder = (c.placeholder || '').toLowerCase().trim();
1135
- const name = (c.getAttribute('name') || '').toLowerCase().trim();
1136
-
1137
- if (aria === normLabel || text === normLabel || placeholder === normLabel || name === normLabel || (val && val === normLabel)) {
1138
- c.setAttribute('data-wt-id', refId);
1139
- const r = c.getBoundingClientRect();
1140
- return {
1141
- success: true,
1142
- strategy: 'exact_label',
1143
- x: Math.round(r.left + r.width / 2),
1144
- y: Math.round(r.top + r.height / 2),
1145
- label: c.getAttribute('aria-label') || (c.innerText || c.textContent || '').trim() || c.placeholder || '',
1146
- role: c.getAttribute('role') || c.tagName.toLowerCase()
1147
- };
1148
- }
1149
- }
1150
- }
1151
-
1152
- // 4. Strategy: Placeholder or Name match
1153
- if (normPlaceholder) {
1154
- for (const c of candidates) {
1155
- if (!isValidCandidate(c)) continue;
1156
- const placeholder = (c.placeholder || '').toLowerCase().trim();
1157
- const name = (c.getAttribute('name') || '').toLowerCase().trim();
1158
- if (placeholder === normPlaceholder || name === normPlaceholder) {
1159
- c.setAttribute('data-wt-id', refId);
1160
- const r = c.getBoundingClientRect();
1161
- return {
1162
- success: true,
1163
- strategy: 'placeholder_or_name',
1164
- x: Math.round(r.left + r.width / 2),
1165
- y: Math.round(r.top + r.height / 2),
1166
- label: c.placeholder || c.getAttribute('name') || '',
1167
- role: c.getAttribute('role') || c.tagName.toLowerCase()
1168
- };
1169
- }
1170
- }
1171
- }
1172
-
1173
- // 5. Strategy: Fuzzy Substring Match (if label length >= 3)
1174
- if (normLabel && normLabel.length >= 3) {
1175
- let bestMatch = null;
1176
- let minDiff = Infinity;
1177
- for (const c of candidates) {
1178
- if (!isValidCandidate(c)) continue;
1179
- const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
1180
- const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
1181
- const candStr = aria || text;
1182
- if (candStr && candStr.includes(normLabel)) {
1183
- const diff = candStr.length - normLabel.length;
1184
- if (diff >= 0 && diff < minDiff) {
1185
- minDiff = diff;
1186
- bestMatch = c;
1187
- }
1188
- }
1189
- }
1190
- if (bestMatch) {
1191
- bestMatch.setAttribute('data-wt-id', refId);
1192
- const r = bestMatch.getBoundingClientRect();
1193
- return {
1194
- success: true,
1195
- strategy: 'fuzzy_label',
1196
- x: Math.round(r.left + r.width / 2),
1197
- y: Math.round(r.top + r.height / 2),
1198
- label: bestMatch.getAttribute('aria-label') || (bestMatch.innerText || bestMatch.textContent || '').trim() || bestMatch.placeholder || '',
1199
- role: bestMatch.getAttribute('role') || bestMatch.tagName.toLowerCase()
1200
- };
1201
- }
1202
- }
1203
-
1204
- return { success: false };
1111
+ expression: `(function(refId, meta) {
1112
+ // 1. Check if node with data-wt-id=refId is already in DOM, connected, and visible
1113
+ const existing = document.querySelector('[data-wt-id="' + refId + '"]');
1114
+ if (existing && existing.isConnected) {
1115
+ const r = existing.getBoundingClientRect();
1116
+ if (r.width > 0 && r.height > 0) {
1117
+ return {
1118
+ success: true,
1119
+ strategy: 'connected_existing',
1120
+ x: Math.round(r.left + r.width / 2),
1121
+ y: Math.round(r.top + r.height / 2),
1122
+ label: existing.getAttribute('aria-label') || (existing.innerText || existing.textContent || '').trim() || existing.placeholder || '',
1123
+ role: existing.getAttribute('role') || existing.tagName.toLowerCase()
1124
+ };
1125
+ }
1126
+ }
1127
+
1128
+ function isValidCandidate(el) {
1129
+ if (!el || !el.isConnected) return false;
1130
+ const r = el.getBoundingClientRect();
1131
+ if (r.width <= 0 || r.height <= 0) return false;
1132
+ try {
1133
+ const style = window.getComputedStyle(el);
1134
+ if (style.visibility === 'hidden' || style.display === 'none') return false;
1135
+ } catch(e) {}
1136
+ return true;
1137
+ }
1138
+
1139
+ // 2. Strategy: Stored Selector (if stable CSS selector recorded)
1140
+ if (meta.selector && !meta.selector.startsWith('[data-wt-id')) {
1141
+ try {
1142
+ const el = document.querySelector(meta.selector);
1143
+ if (isValidCandidate(el)) {
1144
+ el.setAttribute('data-wt-id', refId);
1145
+ const r = el.getBoundingClientRect();
1146
+ return {
1147
+ success: true,
1148
+ strategy: 'stored_selector',
1149
+ x: Math.round(r.left + r.width / 2),
1150
+ y: Math.round(r.top + r.height / 2),
1151
+ label: el.getAttribute('aria-label') || (el.innerText || el.textContent || '').trim() || el.placeholder || '',
1152
+ role: el.getAttribute('role') || el.tagName.toLowerCase()
1153
+ };
1154
+ }
1155
+ } catch(e) {}
1156
+ }
1157
+
1158
+ const normLabel = (meta.label || '').toLowerCase().trim();
1159
+ const normPlaceholder = (meta.placeholder || '').toLowerCase().trim();
1160
+
1161
+ const candidates = Array.from(document.querySelectorAll(
1162
+ 'button, a, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="tab"], [tabindex="0"]'
1163
+ ));
1164
+
1165
+ // 3. Strategy: Exact Label / Text / Aria match
1166
+ if (normLabel) {
1167
+ for (const c of candidates) {
1168
+ if (!isValidCandidate(c)) continue;
1169
+ const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
1170
+ const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
1171
+ const val = (c.value || '').toLowerCase().trim();
1172
+ const placeholder = (c.placeholder || '').toLowerCase().trim();
1173
+ const name = (c.getAttribute('name') || '').toLowerCase().trim();
1174
+
1175
+ if (aria === normLabel || text === normLabel || placeholder === normLabel || name === normLabel || (val && val === normLabel)) {
1176
+ c.setAttribute('data-wt-id', refId);
1177
+ const r = c.getBoundingClientRect();
1178
+ return {
1179
+ success: true,
1180
+ strategy: 'exact_label',
1181
+ x: Math.round(r.left + r.width / 2),
1182
+ y: Math.round(r.top + r.height / 2),
1183
+ label: c.getAttribute('aria-label') || (c.innerText || c.textContent || '').trim() || c.placeholder || '',
1184
+ role: c.getAttribute('role') || c.tagName.toLowerCase()
1185
+ };
1186
+ }
1187
+ }
1188
+ }
1189
+
1190
+ // 4. Strategy: Placeholder or Name match
1191
+ if (normPlaceholder) {
1192
+ for (const c of candidates) {
1193
+ if (!isValidCandidate(c)) continue;
1194
+ const placeholder = (c.placeholder || '').toLowerCase().trim();
1195
+ const name = (c.getAttribute('name') || '').toLowerCase().trim();
1196
+ if (placeholder === normPlaceholder || name === normPlaceholder) {
1197
+ c.setAttribute('data-wt-id', refId);
1198
+ const r = c.getBoundingClientRect();
1199
+ return {
1200
+ success: true,
1201
+ strategy: 'placeholder_or_name',
1202
+ x: Math.round(r.left + r.width / 2),
1203
+ y: Math.round(r.top + r.height / 2),
1204
+ label: c.placeholder || c.getAttribute('name') || '',
1205
+ role: c.getAttribute('role') || c.tagName.toLowerCase()
1206
+ };
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ // 5. Strategy: Fuzzy Substring Match (if label length >= 3)
1212
+ if (normLabel && normLabel.length >= 3) {
1213
+ let bestMatch = null;
1214
+ let minDiff = Infinity;
1215
+ for (const c of candidates) {
1216
+ if (!isValidCandidate(c)) continue;
1217
+ const text = ((c.innerText || c.textContent || '')).toLowerCase().trim();
1218
+ const aria = (c.getAttribute('aria-label') || '').toLowerCase().trim();
1219
+ const candStr = aria || text;
1220
+ if (candStr && candStr.includes(normLabel)) {
1221
+ const diff = candStr.length - normLabel.length;
1222
+ if (diff >= 0 && diff < minDiff) {
1223
+ minDiff = diff;
1224
+ bestMatch = c;
1225
+ }
1226
+ }
1227
+ }
1228
+ if (bestMatch) {
1229
+ bestMatch.setAttribute('data-wt-id', refId);
1230
+ const r = bestMatch.getBoundingClientRect();
1231
+ return {
1232
+ success: true,
1233
+ strategy: 'fuzzy_label',
1234
+ x: Math.round(r.left + r.width / 2),
1235
+ y: Math.round(r.top + r.height / 2),
1236
+ label: bestMatch.getAttribute('aria-label') || (bestMatch.innerText || bestMatch.textContent || '').trim() || bestMatch.placeholder || '',
1237
+ role: bestMatch.getAttribute('role') || bestMatch.tagName.toLowerCase()
1238
+ };
1239
+ }
1240
+ }
1241
+
1242
+ return { success: false };
1205
1243
  })(${JSON.stringify(refNumericId)}, ${JSON.stringify(meta)})`,
1206
1244
  returnByValue: true,
1207
1245
  });