@weavetab/mcp 2.5.0-beta.0

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 (281) hide show
  1. package/AGENTS.md +48 -0
  2. package/ARCHITECTURE.md +590 -0
  3. package/LICENSE +631 -0
  4. package/README.md +909 -0
  5. package/SECURITY.md +196 -0
  6. package/TROUBLESHOOTING.md +365 -0
  7. package/dist/anticaptcha/detector.d.ts +49 -0
  8. package/dist/anticaptcha/detector.js +465 -0
  9. package/dist/anticaptcha/integration.d.ts +36 -0
  10. package/dist/anticaptcha/integration.js +123 -0
  11. package/dist/anticaptcha/solver.d.ts +27 -0
  12. package/dist/anticaptcha/solver.js +552 -0
  13. package/dist/audit/logger.d.ts +13 -0
  14. package/dist/audit/logger.js +177 -0
  15. package/dist/audit/telemetry.d.ts +36 -0
  16. package/dist/audit/telemetry.js +162 -0
  17. package/dist/cdp/bridge.d.ts +16 -0
  18. package/dist/cdp/bridge.js +826 -0
  19. package/dist/cdp/confirm.d.ts +26 -0
  20. package/dist/cdp/confirm.js +79 -0
  21. package/dist/cdp/connector.d.ts +37 -0
  22. package/dist/cdp/connector.js +661 -0
  23. package/dist/cdp/extension.d.ts +36 -0
  24. package/dist/cdp/extension.js +118 -0
  25. package/dist/cdp/finder.d.ts +1 -0
  26. package/dist/cdp/finder.js +2 -0
  27. package/dist/cdp/helpers.d.ts +11 -0
  28. package/dist/cdp/helpers.js +195 -0
  29. package/dist/cdp/label_cache.d.ts +12 -0
  30. package/dist/cdp/label_cache.js +51 -0
  31. package/dist/cdp/lock.d.ts +33 -0
  32. package/dist/cdp/lock.js +206 -0
  33. package/dist/cdp/omni_broker.d.ts +17 -0
  34. package/dist/cdp/omni_broker.js +119 -0
  35. package/dist/cdp/profiles.d.ts +1 -0
  36. package/dist/cdp/profiles.js +87 -0
  37. package/dist/cdp/pruner.d.ts +41 -0
  38. package/dist/cdp/pruner.js +263 -0
  39. package/dist/cdp/ref_cache.d.ts +101 -0
  40. package/dist/cdp/ref_cache.js +234 -0
  41. package/dist/cdp/retry.d.ts +5 -0
  42. package/dist/cdp/retry.js +39 -0
  43. package/dist/cdp/retry_enhanced.d.ts +44 -0
  44. package/dist/cdp/retry_enhanced.js +102 -0
  45. package/dist/cdp/security.d.ts +50 -0
  46. package/dist/cdp/security.js +237 -0
  47. package/dist/cdp/verify.d.ts +39 -0
  48. package/dist/cdp/verify.js +143 -0
  49. package/dist/cdp/walker.d.ts +91 -0
  50. package/dist/cdp/walker.js +896 -0
  51. package/dist/cdp/walker.test.d.ts +2 -0
  52. package/dist/cdp/walker.test.js +172 -0
  53. package/dist/cdp/websocket_transport.d.ts +15 -0
  54. package/dist/cdp/websocket_transport.js +118 -0
  55. package/dist/cli/plugin.d.ts +9 -0
  56. package/dist/cli/plugin.js +1134 -0
  57. package/dist/cli/updater.d.ts +1 -0
  58. package/dist/cli/updater.js +72 -0
  59. package/dist/config/loader.d.ts +120 -0
  60. package/dist/config/loader.js +704 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +1248 -0
  63. package/dist/intelligence/hints.d.ts +11 -0
  64. package/dist/intelligence/hints.js +39 -0
  65. package/dist/intelligence/intent.d.ts +33 -0
  66. package/dist/intelligence/intent.js +171 -0
  67. package/dist/intelligence/sessionMemory.d.ts +66 -0
  68. package/dist/intelligence/sessionMemory.js +284 -0
  69. package/dist/intelligence/sessionMemory.test.d.ts +2 -0
  70. package/dist/intelligence/sessionMemory.test.js +62 -0
  71. package/dist/intelligence/trail.d.ts +64 -0
  72. package/dist/intelligence/trail.js +233 -0
  73. package/dist/intelligence/trail.test.d.ts +2 -0
  74. package/dist/intelligence/trail.test.js +110 -0
  75. package/dist/intelligence/vision.d.ts +24 -0
  76. package/dist/intelligence/vision.js +151 -0
  77. package/dist/lifecycle/mission_manager.d.ts +16 -0
  78. package/dist/lifecycle/mission_manager.js +44 -0
  79. package/dist/overlay/bubble.d.ts +16 -0
  80. package/dist/overlay/bubble.js +37 -0
  81. package/dist/overlay/ghost.d.ts +35 -0
  82. package/dist/overlay/ghost.js +508 -0
  83. package/dist/overlay/injector.d.ts +22 -0
  84. package/dist/overlay/injector.js +198 -0
  85. package/dist/plugin/interface.d.ts +134 -0
  86. package/dist/plugin/interface.js +1 -0
  87. package/dist/plugin/loader.d.ts +18 -0
  88. package/dist/plugin/loader.js +155 -0
  89. package/dist/plugin/registry.d.ts +50 -0
  90. package/dist/plugin/registry.js +234 -0
  91. package/dist/security/blacklist.d.ts +24 -0
  92. package/dist/security/blacklist.js +80 -0
  93. package/dist/security/filter.d.ts +2 -0
  94. package/dist/security/filter.js +20 -0
  95. package/dist/security/guard.d.ts +8 -0
  96. package/dist/security/guard.js +35 -0
  97. package/dist/security/ratelimit.d.ts +11 -0
  98. package/dist/security/ratelimit.js +57 -0
  99. package/dist/security/secrets.d.ts +75 -0
  100. package/dist/security/secrets.js +365 -0
  101. package/dist/sensors/actionability.d.ts +81 -0
  102. package/dist/sensors/actionability.js +303 -0
  103. package/dist/sensors/guard.d.ts +25 -0
  104. package/dist/sensors/guard.js +168 -0
  105. package/dist/sensors/mutations.d.ts +23 -0
  106. package/dist/sensors/mutations.js +93 -0
  107. package/dist/sensors/network.d.ts +67 -0
  108. package/dist/sensors/network.js +193 -0
  109. package/dist/sensors/operation_monitor.d.ts +80 -0
  110. package/dist/sensors/operation_monitor.js +203 -0
  111. package/dist/sensors/pacing.d.ts +44 -0
  112. package/dist/sensors/pacing.js +111 -0
  113. package/dist/sensors/thoughts.d.ts +2 -0
  114. package/dist/sensors/thoughts.js +59 -0
  115. package/dist/sensors/wait.d.ts +12 -0
  116. package/dist/sensors/wait.js +44 -0
  117. package/dist/server.d.ts +7 -0
  118. package/dist/server.js +1687 -0
  119. package/dist/state/agent.d.ts +36 -0
  120. package/dist/state/agent.js +101 -0
  121. package/dist/state/cache.d.ts +16 -0
  122. package/dist/state/cache.js +92 -0
  123. package/dist/state/cache.test.d.ts +2 -0
  124. package/dist/state/cache.test.js +76 -0
  125. package/dist/state/delta.d.ts +71 -0
  126. package/dist/state/delta.js +234 -0
  127. package/dist/state/delta.test.d.ts +2 -0
  128. package/dist/state/delta.test.js +279 -0
  129. package/dist/state/groups.d.ts +15 -0
  130. package/dist/state/groups.js +80 -0
  131. package/dist/state/session.d.ts +30 -0
  132. package/dist/state/session.js +155 -0
  133. package/dist/tools/accessibility.d.ts +14 -0
  134. package/dist/tools/accessibility.js +32 -0
  135. package/dist/tools/ask.d.ts +14 -0
  136. package/dist/tools/ask.js +49 -0
  137. package/dist/tools/automation.d.ts +36 -0
  138. package/dist/tools/automation.js +218 -0
  139. package/dist/tools/browser_detect.d.ts +13 -0
  140. package/dist/tools/browser_detect.js +73 -0
  141. package/dist/tools/burst.d.ts +35 -0
  142. package/dist/tools/burst.js +344 -0
  143. package/dist/tools/canvas.d.ts +25 -0
  144. package/dist/tools/canvas.js +132 -0
  145. package/dist/tools/captcha.d.ts +38 -0
  146. package/dist/tools/captcha.js +209 -0
  147. package/dist/tools/click.d.ts +36 -0
  148. package/dist/tools/click.js +561 -0
  149. package/dist/tools/click_and_wait.d.ts +33 -0
  150. package/dist/tools/click_and_wait.js +149 -0
  151. package/dist/tools/clipboard.d.ts +29 -0
  152. package/dist/tools/clipboard.js +279 -0
  153. package/dist/tools/console.d.ts +21 -0
  154. package/dist/tools/console.js +107 -0
  155. package/dist/tools/cookies.d.ts +67 -0
  156. package/dist/tools/cookies.js +121 -0
  157. package/dist/tools/design_extract.d.ts +109 -0
  158. package/dist/tools/design_extract.js +641 -0
  159. package/dist/tools/dialog.d.ts +24 -0
  160. package/dist/tools/dialog.js +109 -0
  161. package/dist/tools/drag.d.ts +15 -0
  162. package/dist/tools/drag.js +29 -0
  163. package/dist/tools/emulation.d.ts +33 -0
  164. package/dist/tools/emulation.js +69 -0
  165. package/dist/tools/eval.d.ts +20 -0
  166. package/dist/tools/eval.js +92 -0
  167. package/dist/tools/fill.d.ts +41 -0
  168. package/dist/tools/fill.js +667 -0
  169. package/dist/tools/find.d.ts +30 -0
  170. package/dist/tools/find.js +60 -0
  171. package/dist/tools/geo.d.ts +19 -0
  172. package/dist/tools/geo.js +29 -0
  173. package/dist/tools/github/analyze.d.ts +4 -0
  174. package/dist/tools/github/analyze.js +113 -0
  175. package/dist/tools/github/cache.d.ts +21 -0
  176. package/dist/tools/github/cache.js +28 -0
  177. package/dist/tools/github/index.d.ts +4 -0
  178. package/dist/tools/github/index.js +4 -0
  179. package/dist/tools/github/issues.d.ts +9 -0
  180. package/dist/tools/github/issues.js +88 -0
  181. package/dist/tools/github/pr.d.ts +5 -0
  182. package/dist/tools/github/pr.js +88 -0
  183. package/dist/tools/github/rateLimiter.d.ts +4 -0
  184. package/dist/tools/github/rateLimiter.js +46 -0
  185. package/dist/tools/github/read.d.ts +34 -0
  186. package/dist/tools/github/read.js +74 -0
  187. package/dist/tools/github/repoContext.d.ts +20 -0
  188. package/dist/tools/github/repoContext.js +129 -0
  189. package/dist/tools/highlight.d.ts +20 -0
  190. package/dist/tools/highlight.js +140 -0
  191. package/dist/tools/hover.d.ts +19 -0
  192. package/dist/tools/hover.js +57 -0
  193. package/dist/tools/inspect.d.ts +17 -0
  194. package/dist/tools/inspect.js +125 -0
  195. package/dist/tools/integration.test.d.ts +2 -0
  196. package/dist/tools/integration.test.js +96 -0
  197. package/dist/tools/key.d.ts +25 -0
  198. package/dist/tools/key.js +173 -0
  199. package/dist/tools/macro_compiler.d.ts +7 -0
  200. package/dist/tools/macro_compiler.js +82 -0
  201. package/dist/tools/mouse.d.ts +21 -0
  202. package/dist/tools/mouse.js +62 -0
  203. package/dist/tools/navigate.d.ts +16 -0
  204. package/dist/tools/navigate.js +188 -0
  205. package/dist/tools/network_intercept.d.ts +17 -0
  206. package/dist/tools/network_intercept.js +151 -0
  207. package/dist/tools/office/index.d.ts +9 -0
  208. package/dist/tools/office/index.js +7 -0
  209. package/dist/tools/office/list.d.ts +15 -0
  210. package/dist/tools/office/list.js +41 -0
  211. package/dist/tools/office/parse.d.ts +20 -0
  212. package/dist/tools/office/parse.js +172 -0
  213. package/dist/tools/pdf.d.ts +22 -0
  214. package/dist/tools/pdf.js +42 -0
  215. package/dist/tools/performance.d.ts +12 -0
  216. package/dist/tools/performance.js +47 -0
  217. package/dist/tools/performance_test.d.ts +70 -0
  218. package/dist/tools/performance_test.js +128 -0
  219. package/dist/tools/plan.d.ts +39 -0
  220. package/dist/tools/plan.js +161 -0
  221. package/dist/tools/pointer.d.ts +35 -0
  222. package/dist/tools/pointer.js +522 -0
  223. package/dist/tools/read.d.ts +30 -0
  224. package/dist/tools/read.js +488 -0
  225. package/dist/tools/recording.d.ts +45 -0
  226. package/dist/tools/recording.js +124 -0
  227. package/dist/tools/response.d.ts +129 -0
  228. package/dist/tools/response.js +5 -0
  229. package/dist/tools/scrape.d.ts +24 -0
  230. package/dist/tools/scrape.js +140 -0
  231. package/dist/tools/screenshot.d.ts +40 -0
  232. package/dist/tools/screenshot.js +79 -0
  233. package/dist/tools/scroll.d.ts +23 -0
  234. package/dist/tools/scroll.js +196 -0
  235. package/dist/tools/select.d.ts +13 -0
  236. package/dist/tools/select.js +217 -0
  237. package/dist/tools/snapshot.d.ts +27 -0
  238. package/dist/tools/snapshot.js +99 -0
  239. package/dist/tools/storage.d.ts +18 -0
  240. package/dist/tools/storage.js +29 -0
  241. package/dist/tools/tabs.d.ts +77 -0
  242. package/dist/tools/tabs.js +342 -0
  243. package/dist/tools/thought.d.ts +16 -0
  244. package/dist/tools/thought.js +79 -0
  245. package/dist/tools/type.d.ts +65 -0
  246. package/dist/tools/type.js +1061 -0
  247. package/dist/tools/type_secret.d.ts +23 -0
  248. package/dist/tools/type_secret.js +229 -0
  249. package/dist/tools/upload.d.ts +19 -0
  250. package/dist/tools/upload.js +103 -0
  251. package/dist/tools/video_compiler.d.ts +24 -0
  252. package/dist/tools/video_compiler.js +199 -0
  253. package/dist/tools/viewport.d.ts +44 -0
  254. package/dist/tools/viewport.js +230 -0
  255. package/dist/tools/wait.d.ts +19 -0
  256. package/dist/tools/wait.js +202 -0
  257. package/dist/tools/wait_for.d.ts +23 -0
  258. package/dist/tools/wait_for.js +30 -0
  259. package/dist/tools/windows.d.ts +22 -0
  260. package/dist/tools/windows.js +57 -0
  261. package/dist/ui/index.d.ts +1 -0
  262. package/dist/ui/index.js +12 -0
  263. package/docs/DEV_MODE.md +68 -0
  264. package/docs/DOCUMENTATION.md +532 -0
  265. package/docs/PLUGINS.md +538 -0
  266. package/docs/TOOLS.md +104 -0
  267. package/docs/dev_plugin.md +310 -0
  268. package/ext-dist/background.js +467 -0
  269. package/ext-dist/content.js +3089 -0
  270. package/ext-dist/dashboard.html +583 -0
  271. package/ext-dist/hud/hud.js +73 -0
  272. package/ext-dist/hud/index.html +20 -0
  273. package/ext-dist/manifest.json +77 -0
  274. package/ext-dist/sidepanel/sidepanel.js +25 -0
  275. package/ext-dist/sidepanel.html +547 -0
  276. package/ext-dist/styles.css +218 -0
  277. package/icon.png +0 -0
  278. package/mcp.json +796 -0
  279. package/package.json +113 -0
  280. package/server.json +23 -0
  281. package/smithery.yaml +11 -0
@@ -0,0 +1,896 @@
1
+ import { logDebug } from "../audit/logger.js";
2
+ import { SessionSecretTracker } from "../security/secrets.js";
3
+ // ─── Public types ─────────────────────────────────────────────────────────────
4
+ export const WT_REGISTRY_INIT_EXPR = `
5
+ window.__weavetab = window.__weavetab || {};
6
+ window.__weavetab.registry = window.__weavetab.registry || new WeakMap();
7
+ `;
8
+ const viewportCache = new Map();
9
+ export function clearViewportCache(targetId) {
10
+ viewportCache.delete(targetId);
11
+ }
12
+ // ─── Per-navigation fallback ID counter (legacy — kept for external callers) ──
13
+ // NOTE: This counter is NO LONGER used for collision dedup — see walkPassGlobalRefCounter.
14
+ // Kept only so callers of resetFallbackIdCounter() don't break.
15
+ let wtFallbackIdCounter = 1;
16
+ export function resetFallbackIdCounter() {
17
+ wtFallbackIdCounter = 1;
18
+ }
19
+ // ─── Global atomic walk-pass ref counter (Bug #5 fix) ────────────────────────
20
+ // A single monotonically-incrementing integer shared across ALL walk passes,
21
+ // ALL frames, ALL tabs, and ALL tools in the process lifetime.
22
+ // It is NEVER reset — only ever incremented — so no two elements across any
23
+ // code path can ever receive the same numeric suffix in their ref ID.
24
+ //
25
+ // Design invariants:
26
+ // • walkPassGlobalRefCounter ticks forward on every fallback ID assignment.
27
+ // • walkPassSeenIds is the authoritative dedup registry for one top-level
28
+ // buildActionMap / buildAXTreeMap call (cross-frame scope).
29
+ // • beginWalkPass() is called ONCE at the top of each public entry point.
30
+ // • The JS collision counter injected into in-page code is seeded with
31
+ // walkPassGlobalRefCounter so iframe B's counter starts where iframe A ended.
32
+ let walkPassGlobalRefCounter = 1;
33
+ let walkPassSeenIds = new Map();
34
+ /**
35
+ * Must be called at the start of every buildActionMap / buildAXTreeMap
36
+ * invocation. Returns the counter base captured for this pass so the
37
+ * in-page JS can be seeded with it.
38
+ */
39
+ function beginWalkPass() {
40
+ walkPassSeenIds = new Map();
41
+ return walkPassGlobalRefCounter;
42
+ }
43
+ /**
44
+ * Reserves the next N IDs from the global counter and returns the first.
45
+ * Thread-safe within a single Node.js event-loop tick (synchronous).
46
+ */
47
+ function allocateRefIds(count) {
48
+ const base = walkPassGlobalRefCounter;
49
+ walkPassGlobalRefCounter += Math.max(count, 1);
50
+ return base;
51
+ }
52
+ /**
53
+ * Deduplicates a ref ID within the current walk pass (cross-frame scope).
54
+ * If `id` is already taken, appends _2, _3, … until unique, then records it.
55
+ * Uses walkPassSeenIds so collisions from different iframes are caught.
56
+ */
57
+ function dedupeRefId(id) {
58
+ if (!walkPassSeenIds.has(id)) {
59
+ walkPassSeenIds.set(id, 1);
60
+ return id;
61
+ }
62
+ const next = walkPassSeenIds.get(id) + 1;
63
+ walkPassSeenIds.set(id, next);
64
+ // Append _N suffix; recursively check in case _N itself is taken
65
+ return dedupeRefId(`${id}_${next}`);
66
+ }
67
+ // ─── Shadow DOM reference cache (Phase 2a) ───────────────────────────────────
68
+ // Caches whether a page has discovered shadow DOM content per targetId.
69
+ // Avoids the overhead of a two-phase (flat → deep) query on every browser_map.
70
+ const shadowDomCache = new Map();
71
+ export function setShadowDomCache(targetId, hasShadow) {
72
+ shadowDomCache.set(targetId, hasShadow);
73
+ }
74
+ export function clearShadowDomCache(targetId) {
75
+ shadowDomCache.delete(targetId);
76
+ }
77
+ // ─── Main walker ─────────────────────────────────────────────────────────────
78
+ // Shared helper that identifies WeaveTab's own overlay/UI elements.
79
+ // Applied in both buildActionMap (DOM walker path) and buildAXTreeMap (AX tree
80
+ // path) so overlay elements never appear in delta output regardless of code path.
81
+ function isWeaveTabOverlay(label, selector) {
82
+ const lbl = label?.toLowerCase().trim() || '';
83
+ const sel = selector?.toLowerCase() || '';
84
+ const WT_LABELS = new Set([
85
+ 'chat with weavetab', 'weavetab', 'wt-msg-input', 'wt-msg-send',
86
+ 'wt-msg-panel', 'wt-ghost-cursor', 'wt-ghost-bubble', 'weavetab-tactical-overlay',
87
+ 'wt-shadow-host', 'wt-chat-icon', 'wt-chat-panel', 'wt-chat-input',
88
+ 'f3', 'toggle theme', 'minimize', 'send feedback...'
89
+ ]);
90
+ if (WT_LABELS.has(lbl))
91
+ return true;
92
+ if (/^(chat with weavetab|weavetab hud|weave.*tab.*chat|send feedback|redirect, add a note, change goal|\u2191$|wt-(chat|shadow))/i.test(lbl))
93
+ return true;
94
+ if (sel.includes('wt-shadow') || sel.includes('data-wt-ignore') || sel.startsWith('#wt-'))
95
+ return true;
96
+ return false;
97
+ }
98
+ export async function buildActionMap(session, options = {}, targetId = "default") {
99
+ // ── Begin a new walk pass — resets per-pass dedup registry ─────────────────
100
+ const passCounterBase = beginWalkPass();
101
+ const url = options.url ?? (await session.Page.getFrameTree()).frameTree.frame.url;
102
+ const title = options.title ?? "";
103
+ // Inherit sticky pruning state from session if not explicitly set in options
104
+ if (options.prune === undefined) {
105
+ const { sessionRegistry } = await import("../state/session.js");
106
+ const client = sessionRegistry.getAll().find(c => c.targetId === targetId);
107
+ if (client?.pruneActive) {
108
+ options.prune = true;
109
+ }
110
+ }
111
+ const { getFromExtension } = await import("./helpers.js");
112
+ let extensionMap = await getFromExtension(session, { type: 'GET_SEMANTIC_MAP' });
113
+ // Validate the extension result — reject if any element has undefined ID
114
+ if (extensionMap && Array.isArray(extensionMap)) {
115
+ const invalidCount = extensionMap.filter((el) => el.id === undefined || el.id === null || el.id === '').length;
116
+ if (invalidCount > 0) {
117
+ logDebug(`[Walker] Extension returned ${invalidCount}/${extensionMap.length} elements with invalid IDs, forcing CDP fallback`);
118
+ extensionMap = null;
119
+ }
120
+ }
121
+ // Phase 0.2: DOM ID verification — confirm extension IDs are actually written as
122
+ // data-wt-id attributes in the live DOM. If the majority are missing the extension
123
+ // built a map but never applied the attributes; fall through to CDP fallback.
124
+ if (extensionMap && Array.isArray(extensionMap) && extensionMap.length > 0) {
125
+ const sampleSize = Math.min(5, extensionMap.length);
126
+ const sampleIds = extensionMap.slice(0, sampleSize).map((el) => el.id).filter(Boolean);
127
+ if (sampleIds.length > 0) {
128
+ try {
129
+ const { result: domVerify } = await session.Runtime.evaluate({
130
+ expression: `(function(ids) {
131
+ return ids.filter(function(id) {
132
+ return !document.querySelector('[data-wt-id="' + id + '"]');
133
+ }).length;
134
+ })(${JSON.stringify(sampleIds)})`,
135
+ returnByValue: true
136
+ });
137
+ const missingCount = domVerify?.value ?? sampleIds.length;
138
+ if (missingCount > Math.ceil(sampleIds.length / 2)) {
139
+ logDebug(`[Walker] Extension ID DOM verification: ${missingCount}/${sampleIds.length} IDs not found in DOM \u2014 falling through to CDP fallback`);
140
+ extensionMap = null;
141
+ }
142
+ }
143
+ catch { /* non-fatal \u2014 keep extensionMap as-is and proceed */ }
144
+ }
145
+ }
146
+ if (!extensionMap || !Array.isArray(extensionMap)) {
147
+ // Architecture change: Try to read from extension's live index first
148
+ // The extension now maintains window.__wt_liveIndex via continuous MutationObserver
149
+ const { result: liveIndexResult } = await session.Runtime.evaluate({
150
+ expression: `(function() {
151
+ if (typeof window.__wt_liveIndex !== 'undefined' && Array.isArray(window.__wt_liveIndex)) {
152
+ return JSON.stringify({
153
+ elements: window.__wt_liveIndex,
154
+ source: 'live-index'
155
+ });
156
+ }
157
+ return JSON.stringify({ elements: null, source: 'unavailable' });
158
+ })()`,
159
+ returnByValue: true
160
+ });
161
+ if (liveIndexResult && liveIndexResult.value) {
162
+ try {
163
+ const parsed = JSON.parse(liveIndexResult.value);
164
+ if (parsed.source === 'live-index' && parsed.elements && Array.isArray(parsed.elements)) {
165
+ logDebug(`[Walker] Successfully read ${parsed.elements.length} elements from extension live index`);
166
+ extensionMap = parsed.elements;
167
+ }
168
+ }
169
+ catch (e) {
170
+ logDebug(`[Walker] Failed to parse live index result: ${e}`);
171
+ }
172
+ }
173
+ }
174
+ if (!extensionMap || !Array.isArray(extensionMap)) {
175
+ // Fallback: inject a script to get interactable elements when extension is missing
176
+ // Hardened with per-element try-catch — one bad node no longer kills the entire map
177
+ // Deep shadow DOM traversal — falls back to recursive shadow-penetrating query when flat query is sparse
178
+ const { result } = await session.Runtime.evaluate({
179
+ expression: `(function() {
180
+ 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"])';
181
+
182
+ // deepQuery has a hard depth limit and a visited-set guard so circular
183
+ // shadow root references can't cause a stack overflow.
184
+ function deepQuery(root, selector, visited, depth) {
185
+ if (!visited) visited = new Set();
186
+ if (depth === undefined) depth = 0;
187
+ if (depth > 10) return []; // hard depth limit
188
+ if (visited.has(root)) return []; // break cycles
189
+ visited.add(root);
190
+ var all = [];
191
+ try {
192
+ var direct = root.querySelectorAll(selector);
193
+ for (var i = 0; i < direct.length; i++) all.push(direct[i]);
194
+ } catch (e) {}
195
+ // TreeWalker — lazy DOM traversal, avoids building a static NodeList of every element
196
+ try {
197
+ var walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
198
+ while (walker.nextNode()) {
199
+ try {
200
+ var el = walker.currentNode;
201
+ if (el.shadowRoot && !visited.has(el.shadowRoot)) {
202
+ var inner = deepQuery(el.shadowRoot, selector, visited, depth + 1);
203
+ for (var k = 0; k < inner.length; k++) all.push(inner[k]);
204
+ }
205
+ } catch (e) {}
206
+ }
207
+ } catch (e) {}
208
+ return all;
209
+ }
210
+
211
+ var hasShadowCache = ${JSON.stringify(!!shadowDomCache.get(targetId))};
212
+ var useDeep = ${JSON.stringify(!!options.deep)} || hasShadowCache;
213
+ var els = [];
214
+
215
+ // Phase 1: flat query (skip when cache confirms shadow DOM exists — deepQuery covers root too)
216
+ if (!hasShadowCache) {
217
+ try {
218
+ var flatEls = document.querySelectorAll(INTERACTIVE_SEL);
219
+ for (var fi = 0; fi < flatEls.length; fi++) els.push(flatEls[fi]);
220
+ } catch (e) {}
221
+ }
222
+
223
+ // Phase 2: deep query if cache hit OR flat results are sparse
224
+ if (useDeep || els.length < 3) {
225
+ try {
226
+ var deepEls = deepQuery(document, INTERACTIVE_SEL);
227
+ // Deduplicate by comparing tagName + label + position fingerprint
228
+ var seen = new Set();
229
+ for (var di = 0; di < els.length; di++) {
230
+ try {
231
+ var r = els[di].getBoundingClientRect();
232
+ seen.add(els[di].tagName + '|' + Math.round(r.left) + '|' + Math.round(r.top));
233
+ } catch (e) {}
234
+ }
235
+ for (var di2 = 0; di2 < deepEls.length; di2++) {
236
+ try {
237
+ var el2 = deepEls[di2];
238
+ var r2 = el2.getBoundingClientRect();
239
+ var key = el2.tagName + '|' + Math.round(r2.left) + '|' + Math.round(r2.top);
240
+ if (!seen.has(key)) {
241
+ els.push(el2);
242
+ seen.add(key);
243
+ }
244
+ } catch (e) {}
245
+ }
246
+ } catch (e) {}
247
+ }
248
+
249
+ // ── Content-addressable IDs — stable across re-renders ────────────────
250
+ // djb2 hash of (tagName + role + trimmed text/label + sibling-index + parent context).
251
+ // Same element always gets the same ID even if the walker runs multiple
252
+ // times. Sequential counter only used as collision fallback.
253
+ function wtHashId(el) {
254
+ var tag = el.tagName || '';
255
+ var role = el.getAttribute('role') || '';
256
+ var text = (el.getAttribute('aria-label') || el.textContent || el.value || el.placeholder || '').slice(0, 60).trim();
257
+ var rect = el.getBoundingClientRect();
258
+ var absX = Math.round(rect.left + (window.scrollX || 0));
259
+ var absY = Math.round(rect.top + (window.scrollY || 0));
260
+ var path = '';
261
+ try {
262
+ var curr = el;
263
+ for(var level = 0; level < 3 && curr; level++) {
264
+ var p = curr.parentElement;
265
+ if (!p) break;
266
+ var idx = 0;
267
+ var siblings = p.children;
268
+ for (var si = 0; si < siblings.length; si++) {
269
+ if (siblings[si] === curr) { idx = si; break; }
270
+ }
271
+ path += '|' + (curr.tagName||'') + '[' + idx + ']';
272
+ curr = p;
273
+ }
274
+ } catch(e) {}
275
+ var raw = tag + '|' + role + '|' + text + '|' + absX + '|' + absY + path;
276
+ var h = 5381;
277
+ for (var ci = 0; ci < raw.length; ci++) {
278
+ h = ((h << 5) + h) ^ raw.charCodeAt(ci);
279
+ h = h >>> 0; // keep unsigned 32-bit
280
+ }
281
+ return 'wt' + h.toString(36);
282
+ }
283
+ var usedHashes = {};
284
+ // Bug #5 fix: seed collision counter from the global walk-pass base so that
285
+ // elements in different iframes processed in the same pass never share a
286
+ // numeric suffix. passCounterBase is injected by the TypeScript host.
287
+ var collisionCounter = ${JSON.stringify(passCounterBase)};
288
+ var _coIdx = 0;
289
+ var results = [];
290
+ for (var i2 = 0; i2 < els.length; i2++) {
291
+ try {
292
+ var el = els[i2];
293
+ if (el.closest('[data-wt-ignore]') !== null) continue;
294
+ var rect = el.getBoundingClientRect();
295
+ if (rect.width === 0 || rect.height === 0 || rect.bottom < 0 || rect.top > window.innerHeight) continue;
296
+ var id = el.getAttribute('data-wt-id');
297
+ if (!id) {
298
+ var hid = wtHashId(el);
299
+ // Resolve collision: if same hash already used, append counter
300
+ if (usedHashes[hid]) {
301
+ hid = hid + '_' + (collisionCounter++);
302
+ }
303
+ usedHashes[hid] = true;
304
+ id = hid;
305
+ el.setAttribute('data-wt-id', id);
306
+ }
307
+ var role = el.tagName.toLowerCase();
308
+ if (role === 'a') role = 'link';
309
+ else if (role === 'input' && (el.type === 'text' || el.type === 'search')) role = 'textbox';
310
+ var frameUrl = '';
311
+ try { if (el.ownerDocument !== document) frameUrl = el.ownerDocument.defaultView.location.href; } catch(e) { frameUrl = 'cross-origin-' + (_coIdx++); }
312
+ var isSecret = el.hasAttribute('data-wt-secret') || (el.tagName === 'INPUT' && el.type === 'password');
313
+ var labelStr = el.getAttribute('aria-label') || el.innerText || el.value || el.placeholder || '';
314
+ // For radio/checkbox, serialize checked state as 'true'/'false' so the agent
315
+ // can see which option is selected without having to scrape the DOM separately.
316
+ var elType = (el.type || '').toLowerCase();
317
+ var elRole = (el.getAttribute('role') || el.tagName.toLowerCase());
318
+ var isCheckable = elType === 'radio' || elType === 'checkbox' ||
319
+ elRole === 'radio' || elRole === 'checkbox';
320
+ var isOption = el.tagName === 'OPTION';
321
+ var valStr;
322
+ if (isCheckable) {
323
+ valStr = el.checked ? 'true' : 'false';
324
+ } else if (isOption) {
325
+ valStr = el.selected ? 'true' : 'false';
326
+ } else {
327
+ valStr = el.value !== undefined ? String(el.value) : undefined;
328
+ }
329
+ var txtContent = (el.textContent || '').slice(0, 200);
330
+
331
+ if (isSecret) {
332
+ var rawAria = el.getAttribute('aria-label') || '';
333
+ var rawPlaceholder = el.placeholder || '';
334
+ labelStr = rawAria || rawPlaceholder || '[REDACTED SECRET FIELD]';
335
+ if (valStr !== undefined) valStr = '[REDACTED]';
336
+ txtContent = '[REDACTED]';
337
+ }
338
+
339
+ // Capture structural idiom (e.g. aria-controls, aria-owns, etc.)
340
+ var idiom = '';
341
+ var CONTROLS_ATTRS = ['aria-controls', 'aria-owns', 'data-target', 'data-bs-target'];
342
+ for (var ca = 0; ca < CONTROLS_ATTRS.length; ca++) {
343
+ if (el.hasAttribute(CONTROLS_ATTRS[ca])) {
344
+ idiom = CONTROLS_ATTRS[ca];
345
+ break;
346
+ }
347
+ }
348
+ results.push({
349
+ id: String(id),
350
+ role: el.getAttribute('role') || role,
351
+ label: labelStr,
352
+ value: valStr,
353
+ x: Math.round(rect.left + rect.width / 2),
354
+ y: Math.round(rect.top + rect.height / 2),
355
+ selector: '[data-wt-id="' + id + '"]',
356
+ href: el.href,
357
+ textContent: txtContent,
358
+ className: el.className || '',
359
+ frameUrl: frameUrl,
360
+ structuralIdiom: idiom || undefined
361
+ });
362
+ } catch (e) {
363
+ // Silently skip elements that cause errors (cross-origin SVG, detached nodes, etc.)
364
+ }
365
+ }
366
+ return JSON.stringify({ elements: results, shadowFound: results.length > 0 && typeof flatEls !== 'undefined' && els.length > flatEls.length });
367
+ })()`,
368
+ returnByValue: true
369
+ });
370
+ if (result && result.value) {
371
+ try {
372
+ var parsed = JSON.parse(result.value);
373
+ // New format: { elements: [...], shadowFound: boolean }
374
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.elements) {
375
+ extensionMap = parsed.elements;
376
+ if (parsed.shadowFound) {
377
+ shadowDomCache.set(targetId, true);
378
+ }
379
+ }
380
+ else {
381
+ // Old format: bare array
382
+ extensionMap = parsed;
383
+ }
384
+ }
385
+ catch (e) {
386
+ extensionMap = [];
387
+ }
388
+ }
389
+ }
390
+ if (!extensionMap || !Array.isArray(extensionMap)) {
391
+ return { url, title, elements: [], targetId };
392
+ }
393
+ // Accept both extension format ({tag, text, coords}) and fallback format ({role, label, x, y})
394
+ const mappedElements = extensionMap.map((el) => ({
395
+ id: `w:${el.id}`,
396
+ role: el.role || el.tag || 'generic',
397
+ label: el.label || el.text || el.placeholder || '',
398
+ value: el.value,
399
+ group: 'main_content',
400
+ fingerprint: `${el.role || el.tag || 'generic'}:${el.label || el.text || ''}:${el.id}`,
401
+ depth: 0,
402
+ x: el.x ?? (el.coords ? el.coords.x : undefined),
403
+ y: el.y ?? (el.coords ? el.coords.y : undefined),
404
+ viewport: el.viewport,
405
+ href: el.href,
406
+ selector: el.selector,
407
+ backendNodeId: undefined,
408
+ textContent: el.textContent || undefined,
409
+ className: el.className || el.attributes?.class || '',
410
+ structuralIdiom: el.structuralIdiom || undefined,
411
+ frameUrl: el.frameUrl || undefined,
412
+ }));
413
+ // Post-mapping sanitizer — catch any w:undefined/w:null and reassign.
414
+ // Uses a stable hash derived from the element's fingerprint, coordinates, and
415
+ // index rather than an incrementing global counter (which would yield unstable
416
+ // IDs across map calls).
417
+ let invalidIdCount = 0;
418
+ const usedFallbackHashes = new Set();
419
+ for (let i = 0; i < mappedElements.length; i++) {
420
+ if (!mappedElements[i].id || mappedElements[i].id === 'w:undefined' || mappedElements[i].id === 'w:null' || mappedElements[i].id === 'w:') {
421
+ const baseStr = `${mappedElements[i].role}|${mappedElements[i].label}|${mappedElements[i].x}|${mappedElements[i].y}`;
422
+ let h = 5381;
423
+ for (let c = 0; c < baseStr.length; c++) {
424
+ h = ((h << 5) + h) ^ baseStr.charCodeAt(c);
425
+ h = h >>> 0;
426
+ }
427
+ let hash = h.toString(36);
428
+ if (usedFallbackHashes.has(hash)) {
429
+ let col = 1;
430
+ while (usedFallbackHashes.has(`${hash}_${col}`))
431
+ col++;
432
+ hash = `${hash}_${col}`;
433
+ }
434
+ usedFallbackHashes.add(hash);
435
+ mappedElements[i].id = `w:f${hash}`; // 'f' for fallback
436
+ mappedElements[i].fingerprint = `${mappedElements[i].role}:${mappedElements[i].label}:fb-${hash}`;
437
+ invalidIdCount++;
438
+ }
439
+ }
440
+ if (invalidIdCount > 0) {
441
+ logDebug(`[Walker] Post-mapping sanitizer fixed ${invalidIdCount} element(s) with stable fallback IDs`);
442
+ }
443
+ // ── Bug #5 fix: global cross-frame dedup using walkPassSeenIds ───────────────
444
+ // walkPassSeenIds is reset once per buildActionMap call (see beginWalkPass()).
445
+ // It spans ALL frames in this pass — not just this mappedElements array — so a
446
+ // hash collision between an element in the main frame and one in an iframe is
447
+ // caught and disambiguated here rather than silently producing two `w:X` refs.
448
+ {
449
+ for (const el of mappedElements) {
450
+ const original = el.id;
451
+ const deduped = dedupeRefId(el.id);
452
+ if (deduped !== original) {
453
+ el.id = deduped;
454
+ // Patch data-wt-id selector so click/type tools can still resolve the element.
455
+ if (el.selector && el.selector.includes('data-wt-id')) {
456
+ el.selector = el.selector.replace(/data-wt-id="[^"]+"/, `data-wt-id="${el.id.replace('w:', '')}"`);
457
+ }
458
+ logDebug(`[Walker] Bug#5 cross-frame dedup: ${original} → ${el.id}`);
459
+ }
460
+ }
461
+ }
462
+ // ── AI-blind: strip Weavetab overlay elements from the map ─────────────────
463
+ // The chat panel, ghost cursor, HUD, and overlay UI are for the USER only.
464
+ // The AI must never see, click, or type into them.
465
+ // Uses shared isWeaveTabOverlay() helper — also applied in buildAXTreeMap.
466
+ const visibleElements = mappedElements.filter((el) => {
467
+ // Use shared isWeaveTabOverlay helper for consistent filtering
468
+ if (isWeaveTabOverlay(el.label, el.selector))
469
+ return false;
470
+ // Defense-in-depth: skip elements with empty labels and non-interactive roles
471
+ // Catches overlay elements that slipped through (e.g. plain divs with onclick)
472
+ const lbl = el.label?.toLowerCase().trim() || '';
473
+ if (!lbl && !el.value && !el.placeholder && !el.href &&
474
+ (el.role === 'generic' || el.role === '' || el.role === 'div'))
475
+ return false;
476
+ return true;
477
+ });
478
+ // ──────────────────────────────────────────────────────────────────────────
479
+ // ── Semantic DOM Pruning (optional, opt-in via prune:true) ─────────────────
480
+ // Prune = one-shot compression of THIS snapshot before returning.
481
+ // Delta = diffs across time (previous call → now). They are complementary.
482
+ // NOTE: sequenced after data-wt-id assignment. Do not enable
483
+ // as default until that approach is confirmed stable.
484
+ let prunedElements = visibleElements;
485
+ if (options.prune) {
486
+ try {
487
+ const { loadDomainProfile } = await import('../intelligence/sessionMemory.js');
488
+ const hostname = new URL(url).hostname;
489
+ const profile = await loadDomainProfile(hostname);
490
+ const structuralIdioms = profile?.memories
491
+ .filter(m => m.state !== "deprecated" && m.fingerprint?.structuralIdiom)
492
+ .map(m => m.fingerprint.structuralIdiom) || [];
493
+ // Deduplicate idioms
494
+ const uniqueIdioms = Array.from(new Set(structuralIdioms));
495
+ const { pruneSnapshot } = await import('./pruner.js');
496
+ prunedElements = await pruneSnapshot(session, visibleElements, targetId, uniqueIdioms);
497
+ }
498
+ catch (e) {
499
+ logDebug(`[Walker] Pruner threw — falling back to unpruned list: ${e}`);
500
+ prunedElements = visibleElements;
501
+ }
502
+ }
503
+ // Scope filter — restrict to elements within a CSS selector
504
+ let scopedElements = prunedElements;
505
+ if (options.scope) {
506
+ const { result: scopeResult } = await session.Runtime.evaluate({
507
+ expression: `(function(sel) {
508
+ const el = document.querySelector(sel);
509
+ if (!el) return null;
510
+ const r = el.getBoundingClientRect();
511
+ const ids = [...el.querySelectorAll('[data-wt-id]')].map(e => e.getAttribute('data-wt-id')).filter(Boolean);
512
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, ids: ids };
513
+ })(${JSON.stringify(options.scope)})`,
514
+ returnByValue: true,
515
+ }).catch(() => ({ result: { value: null } }));
516
+ const scopeData = scopeResult?.value;
517
+ if (scopeData) {
518
+ const validIds = new Set(scopeData.ids);
519
+ scopedElements = visibleElements.filter(el => {
520
+ if (validIds.has(el.id.replace('w:', '')))
521
+ return true;
522
+ if (el.x !== undefined && el.y !== undefined) {
523
+ return el.x >= scopeData.left && el.x <= scopeData.right &&
524
+ el.y >= scopeData.top && el.y <= scopeData.bottom;
525
+ }
526
+ return false;
527
+ });
528
+ }
529
+ }
530
+ // Query filter
531
+ let filtered = scopedElements;
532
+ if (options.query) {
533
+ const q = options.query.toLowerCase();
534
+ filtered = filtered.filter((el) => el.label.toLowerCase().includes(q) ||
535
+ el.type?.toLowerCase().includes(q) ||
536
+ el.role.toLowerCase().includes(q) ||
537
+ el.id.toLowerCase().includes(q));
538
+ }
539
+ // Limit
540
+ if (options.limit && filtered.length > options.limit) {
541
+ filtered = filtered.slice(0, options.limit);
542
+ }
543
+ return { url, title, elements: filtered, targetId };
544
+ }
545
+ // ─── toRefEntries — converts ActionElements to RefEntry for delta tracking ───
546
+ export function toRefEntries(elements) {
547
+ return elements.map((el) => ({
548
+ id: el.id,
549
+ role: el.role,
550
+ label: el.label,
551
+ type: el.type,
552
+ href: el.href,
553
+ disabled: el.disabled,
554
+ value: el.value,
555
+ fingerprint: el.fingerprint,
556
+ }));
557
+ }
558
+ export function findAllMatches(elements, query) {
559
+ if (!query || !query.trim())
560
+ return [];
561
+ // Helper to normalize element label by stripping ellipsis/punctuation
562
+ const n = (str) => (str || "").toLowerCase().replace(/[\.\u2026\s]+/g, ' ').trim();
563
+ const q = n(query);
564
+ const matches = [];
565
+ const addMatch = (el) => {
566
+ if (el && !matches.find(m => m.id === el.id))
567
+ matches.push(el);
568
+ };
569
+ // 1. Exact match (highest priority)
570
+ elements.forEach(el => { if (n(el.label) === q)
571
+ addMatch(el); });
572
+ // 2. placeholder text
573
+ elements.forEach(el => { if (n(el.placeholder) === q)
574
+ addMatch(el); });
575
+ // 3. data-testid
576
+ elements.forEach(el => { if (n(el.testId) === q)
577
+ addMatch(el); });
578
+ // 4. visible text content / partial label match
579
+ elements.forEach(el => { if (n(el.label).includes(q))
580
+ addMatch(el); });
581
+ // 4.5 fuzzy partial match (split words)
582
+ const words = q.split(/\s+/);
583
+ if (words.length > 1) {
584
+ elements.forEach(el => { if (words.every(w => n(el.label).includes(w)))
585
+ addMatch(el); });
586
+ }
587
+ // 5. type match (fallback)
588
+ elements.forEach(el => { if (el.type === query)
589
+ addMatch(el); });
590
+ return matches;
591
+ }
592
+ export function findBestMatch(elements, query) {
593
+ const matches = findAllMatches(elements, query);
594
+ return matches.length > 0 ? matches[0] : undefined;
595
+ }
596
+ // ─── Markdown table formatter (Rule 11) ──────────────────────────────────────
597
+ export function toMarkdownTable(elements, url, title, hint) {
598
+ const lines = [];
599
+ if (hint)
600
+ lines.push(`> ${hint}\n`);
601
+ lines.push(`**${title || url}** — ${url}`);
602
+ lines.push("");
603
+ let minDepth = 999;
604
+ for (const el of elements) {
605
+ if (el.depth < minDepth)
606
+ minDepth = el.depth;
607
+ }
608
+ for (const el of elements) {
609
+ const indent = " ".repeat(Math.max(0, el.depth - minDepth));
610
+ const roleTag = `(${formatRole(el.role, el.type)})`;
611
+ const ref = el.id ? ` [${el.id}]` : "";
612
+ const value = el.value ? ` value="${el.value}"` : "";
613
+ // Strip everything non-interactive if it doesn't have text
614
+ if (!el.id && !el.label.trim())
615
+ continue;
616
+ lines.push(`${indent}${roleTag} "${el.label}"${value}${ref}`);
617
+ }
618
+ return lines.join("\n");
619
+ }
620
+ function formatRole(role, type) {
621
+ const roleMap = {
622
+ button: "Btn", link: "Lnk", textbox: "Input", searchbox: "Search",
623
+ combobox: "Select", checkbox: "Chk", radio: "Radio", listbox: "List",
624
+ menuitem: "Menu", slider: "Slider", tab: "Tab", generic: "Icon",
625
+ heading: "H", paragraph: "P", StaticText: "Text", text: "Text"
626
+ };
627
+ const base = roleMap[role] ?? role;
628
+ return type ? `${base}/${type}` : base;
629
+ }
630
+ export function safeSelector(el) {
631
+ if (!el)
632
+ return undefined;
633
+ if (el.selector)
634
+ return el.selector;
635
+ return undefined;
636
+ }
637
+ export async function resolveByAria(session, query) {
638
+ if (!query || !query.trim())
639
+ return null;
640
+ const resp = await session.Runtime.evaluate({
641
+ expression: `(function(q) {
642
+ var INTERACTIVE = 'a[href], button, input:not([type="hidden"]), textarea, select,' +
643
+ '[role="button"],[role="link"],[role="textbox"],[role="checkbox"],' +
644
+ '[role="radio"],[role="combobox"],[role="searchbox"],[role="menuitem"],' +
645
+ '[role="tab"],[role="option"],[tabindex]:not([tabindex="-1"])';
646
+
647
+ function norm(s) { return (s || '').toLowerCase().replace(/[.\\u2026\\s]+/g, ' ').trim(); }
648
+ var qLow = norm(q);
649
+
650
+ // Score: prefer leaf elements (no interactive child = score 0, has child = score 10)
651
+ function score(el) {
652
+ try { return el.querySelector(INTERACTIVE) !== null ? 10 : 0; } catch(e) { return 0; }
653
+ }
654
+ function best(list) {
655
+ if (!list || !list.length) return null;
656
+ var arr = Array.prototype.slice.call(list);
657
+ arr.sort(function(a,b){ return score(a)-score(b); });
658
+ return arr[0];
659
+ }
660
+ function makeResult(el) {
661
+ if (!el) return null;
662
+ var r = el.getBoundingClientRect();
663
+ if (!r || r.width === 0 || r.height === 0) return null;
664
+ if (r.bottom < 0 || r.top > window.innerHeight) return null;
665
+ var sel = '';
666
+ var wtId = el.getAttribute('data-wt-id');
667
+ if (wtId) {
668
+ sel = '[data-wt-id="' + wtId.replace(/"/g, '\\\\"') + '"]';
669
+ } else if (el.id) {
670
+ try { sel = '#' + CSS.escape(el.id); } catch(e) { sel = '#' + el.id; }
671
+ } else {
672
+ var aria = el.getAttribute('aria-label');
673
+ if (aria) sel = el.tagName.toLowerCase() + '[aria-label="' + aria.replace(/"/g, '\\\\"') + '"]';
674
+ }
675
+ if (!sel) {
676
+ var tmp = 'wt-tmp-' + Math.random().toString(36).slice(2,8);
677
+ el.setAttribute('data-wt-id', tmp);
678
+ sel = '[data-wt-id="' + tmp + '"]';
679
+ }
680
+ return {
681
+ label: el.getAttribute('aria-label') || (el.textContent||'').trim().slice(0,80) || q,
682
+ x: Math.round(r.left + r.width/2),
683
+ y: Math.round(r.top + r.height/2),
684
+ selector: sel
685
+ };
686
+ }
687
+
688
+ var all, found, i, hit;
689
+
690
+ // 1. Exact aria-label
691
+ all = document.querySelectorAll('[aria-label]');
692
+ found = [];
693
+ for (i=0;i<all.length;i++) {
694
+ if (norm(all[i].getAttribute('aria-label'))===qLow) found.push(all[i]);
695
+ }
696
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
697
+
698
+ // 2. Exact innerText on interactive elements
699
+ all = document.querySelectorAll(INTERACTIVE);
700
+ found = [];
701
+ for (i=0;i<all.length;i++) {
702
+ var t = norm(all[i].innerText||all[i].textContent);
703
+ if (t===qLow) found.push(all[i]);
704
+ }
705
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
706
+
707
+ // 3. Placeholder
708
+ all = document.querySelectorAll('[placeholder]');
709
+ found = [];
710
+ for (i=0;i<all.length;i++) {
711
+ if (norm(all[i].getAttribute('placeholder'))===qLow) found.push(all[i]);
712
+ }
713
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
714
+
715
+ // 4. title / alt
716
+ all = document.querySelectorAll('[title],[alt]');
717
+ found = [];
718
+ for (i=0;i<all.length;i++) {
719
+ var v = norm(all[i].getAttribute('title')||all[i].getAttribute('alt'));
720
+ if (v===qLow) found.push(all[i]);
721
+ }
722
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
723
+
724
+ // 5. Fuzzy partial innerText
725
+ all = document.querySelectorAll(INTERACTIVE);
726
+ found = [];
727
+ for (i=0;i<all.length;i++) {
728
+ var t5 = norm(all[i].innerText||all[i].textContent);
729
+ if (t5.indexOf(qLow)>=0 && t5.length < qLow.length*4) found.push(all[i]);
730
+ }
731
+ if (found.length) { hit=makeResult(best(found)); if(hit) return hit; }
732
+
733
+ return null;
734
+ })(${JSON.stringify(query)})`,
735
+ returnByValue: true,
736
+ }).catch(() => null);
737
+ const val = resp?.result?.value;
738
+ if (!val)
739
+ return null;
740
+ return { id: "aria-fallback", label: val.label, x: val.x, y: val.y, selector: val.selector };
741
+ }
742
+ // axTreeCache is URL-keyed with an insertion-order LRU cap of 50 entries so it
743
+ // doesn't grow without bound during long sessions visiting many URLs.
744
+ const AX_TREE_CACHE_MAX = 50;
745
+ const axTreeCache = new Map();
746
+ export function clearAXTreeCache(targetId) {
747
+ // axTreeCache is URL-keyed; targetId param unused but kept for API consistency
748
+ axTreeCache.clear();
749
+ }
750
+ function axTreeCacheSet(url, map) {
751
+ if (axTreeCache.size >= AX_TREE_CACHE_MAX && !axTreeCache.has(url)) {
752
+ const firstKey = axTreeCache.keys().next().value;
753
+ if (firstKey !== undefined)
754
+ axTreeCache.delete(firstKey);
755
+ }
756
+ axTreeCache.set(url, map);
757
+ }
758
+ // ─── buildAXTreeMap ───────────────────────────────────────────────────────────
759
+ // Uses CDP Accessibility.getFullAXTree() to build an ActionMap.
760
+ // Auto-called by browserMap when DOM-based walker returns sparse or unlabeled-heavy results.
761
+ // Agent can also force via browser_map({ ax_tree: true }).
762
+ const INTERACTIVE_AX_ROLES = new Set([
763
+ 'button', 'link', 'textbox', 'combobox', 'checkbox', 'radio',
764
+ 'slider', 'tab', 'menuitem', 'searchbox', 'switch', 'spinbutton',
765
+ 'treeitem', 'listbox', 'option', 'menuitemcheckbox', 'menuitemradio',
766
+ 'scrollbar', 'tabpanel', 'tree', 'treegrid', 'alert', 'dialog',
767
+ 'tooltip', 'status', 'timer',
768
+ ]);
769
+ // These are skipped — they add noise without actionable info.
770
+ // Role names must be lowercase because the code lowercases the role string
771
+ // before checking the Set. PascalCase names like 'InlineTextBox' or 'StaticText'
772
+ // would never match and let noise elements leak into every delta response.
773
+ const NOISE_AX_ROLES = new Set([
774
+ 'generic', 'inlinetextbox', 'statictext', 'paragraph', 'heading',
775
+ 'list', 'listitem', 'none', 'presentation', 'row', 'cell',
776
+ 'grid', 'group', 'region', 'document', 'rootwebarea',
777
+ ]);
778
+ export async function buildAXTreeMap(session, _options = {}, targetId = "default") {
779
+ // ── Bug #5 fix: begin a new walk pass for AX tree mode ───────────────────────
780
+ // beginWalkPass() resets walkPassSeenIds so the cross-frame dedup registry is
781
+ // fresh for this call. allocateRefIds() reserves a block of IDs from the
782
+ // global monotonic counter so AX sequential IDs never collide with DOM-walker
783
+ // IDs produced in the same session.
784
+ beginWalkPass();
785
+ const { frameTree } = await session.Page.getFrameTree();
786
+ const url = frameTree.frame.url;
787
+ const title = frameTree.frame.name ?? url ?? "";
788
+ // Check cache
789
+ const cached = axTreeCache.get(url);
790
+ if (cached && cached.elements.length > 0) {
791
+ return cached;
792
+ }
793
+ // Enable accessibility domain
794
+ try {
795
+ await session.Accessibility.enable();
796
+ }
797
+ catch { /* already enabled */ }
798
+ let nodes = [];
799
+ try {
800
+ const result = await session.Accessibility.getFullAXTree();
801
+ nodes = result?.nodes ?? [];
802
+ }
803
+ catch (e) {
804
+ return { url, title, elements: [], targetId };
805
+ }
806
+ // Filter to interactive, non-ignored nodes
807
+ const candidates = nodes.filter((n) => {
808
+ if (n.ignored)
809
+ return false;
810
+ const role = (n.role?.value || '').toLowerCase();
811
+ if (!role || NOISE_AX_ROLES.has(role))
812
+ return false;
813
+ return INTERACTIVE_AX_ROLES.has(role) || n.name?.value || n.value?.value;
814
+ });
815
+ // ── Bug #5 fix: allocate a contiguous block of global IDs for this AX pass ──
816
+ // allocateRefIds(candidates.length) advances walkPassGlobalRefCounter by the
817
+ // number of nodes we are about to process, guaranteeing that every w:N ID
818
+ // emitted here is globally unique for the lifetime of the server process.
819
+ const axIdBase = allocateRefIds(candidates.length);
820
+ let axIdCursor = axIdBase;
821
+ const elements = [];
822
+ const backendIdsToResolve = [];
823
+ for (const node of candidates) {
824
+ const role = (node.role?.value || 'generic').toLowerCase();
825
+ let label = node.name?.value || node.description?.value || '';
826
+ // Extract value: for radio/checkbox use the checked property from AX tree.
827
+ // AX tree stores this as node.checked.value ('true'|'false'|'mixed') or
828
+ // node.selected.value ('true'|'false'). Fallback to node.value for text inputs.
829
+ let value;
830
+ if (role === 'radio' || role === 'checkbox' || role === 'menuitemcheckbox' || role === 'menuitemradio') {
831
+ const checked = node.properties?.find((p) => p.name === 'checked')?.value?.value;
832
+ value = checked === true || checked === 'true' ? 'true' : 'false';
833
+ }
834
+ else if (role === 'option') {
835
+ const selected = node.properties?.find((p) => p.name === 'selected')?.value?.value;
836
+ value = selected === true || selected === 'true' ? 'true' : 'false';
837
+ }
838
+ else {
839
+ value = node.value?.value !== undefined ? String(node.value.value) : undefined;
840
+ }
841
+ const isSecretRole = role === 'password' || node.name?.value?.toLowerCase().includes('password');
842
+ if (isSecretRole || (node.backendDOMNodeId !== undefined && SessionSecretTracker.isNodeSecret(targetId, node.backendDOMNodeId))) {
843
+ label = '[REDACTED]';
844
+ value = '[REDACTED]';
845
+ }
846
+ // Use globally-unique sequential ID (Bug #5 fix: was local idCounter = 1).
847
+ const id = dedupeRefId(`w:${axIdCursor++}`);
848
+ elements.push({
849
+ id,
850
+ role,
851
+ label,
852
+ value,
853
+ backendNodeId: node.backendDOMNodeId,
854
+ group: 'main_content',
855
+ fingerprint: `ax:${role}:${label || ''}:${axIdCursor}`,
856
+ depth: 0,
857
+ });
858
+ if (node.backendDOMNodeId !== undefined) {
859
+ backendIdsToResolve.push(node.backendDOMNodeId);
860
+ }
861
+ }
862
+ // Filter out WeaveTab overlay elements from AX tree results. Without this,
863
+ // every delta call returned ~50 extra elements from WeaveTab's own chat panel,
864
+ // HUD, and overlay UI injected into the page DOM.
865
+ const filteredElements = elements.filter(el => !isWeaveTabOverlay(el.label, el.selector));
866
+ // Batch-resolve bounding rects for backendNodeIds
867
+ if (backendIdsToResolve.length > 0) {
868
+ try {
869
+ const { nodeIds } = await session.DOM.pushNodesByBackendIdsToFrontend({
870
+ backendNodeIds: backendIdsToResolve,
871
+ });
872
+ if (nodeIds) {
873
+ await Promise.all(nodeIds.map(async (nodeId, i) => {
874
+ try {
875
+ const { model } = await session.DOM.getBoxModel({ nodeId });
876
+ if (model?.content) {
877
+ const content = model.content;
878
+ const cx = Math.round((content[0] + content[4]) / 2);
879
+ const cy = Math.round((content[1] + content[5]) / 2);
880
+ const el = elements.find(e => e.backendNodeId === backendIdsToResolve[i]);
881
+ if (el) {
882
+ el.x = cx;
883
+ el.y = cy;
884
+ }
885
+ }
886
+ }
887
+ catch { /* element may be detached — skip */ }
888
+ }));
889
+ }
890
+ }
891
+ catch { /* coordinate resolution best-effort */ }
892
+ }
893
+ const map = { url, title, elements: filteredElements, targetId };
894
+ axTreeCacheSet(url, map);
895
+ return map;
896
+ }