@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
@@ -1,2527 +1,2527 @@
1
- const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
2
- // Register this tab with background (skip about:blank and chrome:// pages)
3
- if (location.href !== 'about:blank' && !location.protocol.startsWith('chrome')) {
4
- try { browserAPI.runtime.sendMessage({ type: 'REGISTER_TAB' }).catch(function(){}); } catch(e) {}
5
- }
6
-
7
- // ── STABLE EXTENSION MARKER ────────────────────────────────────────
8
- // Unconditionally injected at document root so MCP server can detect the extension reliably.
9
- if (!document.getElementById('wt-extension-marker')) {
10
- const marker = document.createElement('meta');
11
- marker.id = 'wt-extension-marker';
12
- marker.setAttribute('name', 'weavetab-extension');
13
- if (document.head) {
14
- document.head.appendChild(marker);
15
- } else if (document.documentElement) {
16
- document.documentElement.appendChild(marker);
17
- }
18
- }
19
-
20
- // ── SAFE MESSAGE BRIDGE ────────────────────────────────────────
21
- // Wraps browserAPI.runtime.sendMessage to survive "Extension context invalidated"
22
- // errors that occur when the extension is reloaded/updated mid-session.
23
- var __wt_extAlive = true;
24
- function __wt_safeSendMessage(msg) {
25
- try {
26
- if (!browserAPI.runtime || !browserAPI.runtime.id) { __wt_extAlive = false; return; }
27
- browserAPI.runtime.sendMessage(msg).catch(function(){ __wt_extAlive = false; });
28
- } catch(e) {
29
- __wt_extAlive = false;
30
- }
31
- }
32
-
33
- function __wt_safeAppendToBody(el) {
34
- if (document.body) {
35
- document.body.appendChild(el);
36
- } else {
37
- document.addEventListener('DOMContentLoaded', function() {
38
- if (document.body) document.body.appendChild(el);
39
- }, { once: true });
40
- // Backup for SPAs that might fire DOMContentLoaded early or clear body
41
- const obs = new MutationObserver((m, observer) => {
42
- if (document.body) {
43
- if (!el.parentNode || !document.body.contains(el)) document.body.appendChild(el);
44
- observer.disconnect();
45
- }
46
- });
47
- obs.observe(document.documentElement, { childList: true });
48
- }
49
- }
50
-
51
- // ── HUD MOUNT LAYER (Phase 1: Closed Shadow DOM + Iframe Architecture) ─────
52
- // Reload-proof, agent-invisible, real-time visual system
53
- (function mountHUD() {
54
- // Guard: exit gracefully on chrome:// pages (content scripts blocked)
55
- if (location.protocol.startsWith('chrome')) return;
56
-
57
- let host = null;
58
- let shadow = null;
59
- let iframe = null;
60
- let hudState = {};
61
-
62
- // Create mount point with closed shadow DOM
63
- function createMount() {
64
- if (host && document.documentElement.contains(host)) return;
65
-
66
- host = document.createElement('weavetab-host');
67
- host.setAttribute('data-wt-internal', 'true');
68
- host.setAttribute('aria-hidden', 'true');
69
- host.style.cssText = 'all:initial !important;position:fixed !important;left:0 !important;top:0 !important;width:0 !important;height:0 !important;z-index:2147483647 !important;pointer-events:none !important;background:transparent !important;opacity:1 !important;transform:none !important;filter:none !important;';
70
- __wt_safeAppendToBody(host);
71
-
72
- // Closed shadow root — ref lives ONLY here, never on window
73
- shadow = host.attachShadow({ mode: 'closed' });
74
-
75
- iframe = document.createElement('iframe');
76
- iframe.src = browserAPI.runtime.getURL('hud/index.html');
77
- iframe.id = 'wt-hud-iframe';
78
- iframe.setAttribute('allowtransparency', 'true');
79
- // FIX: Start hidden. Expand only after receiving state to prevent white flash.
80
- Object.assign(iframe.style, {
81
- position: 'fixed',
82
- top: '0',
83
- left: '0',
84
- width: '0',
85
- height: '0',
86
- border: 'none',
87
- background: 'transparent',
88
- pointerEvents: 'none',
89
- zIndex: '2147483647',
90
- visibility: 'hidden',
91
- opacity: '0',
92
- colorScheme: 'light dark'
93
- });
94
- shadow.appendChild(iframe);
95
-
96
- // REHYDRATE: send state to iframe, but DO NOT make visible yet to avoid white flash.
97
- iframe.onload = function() {
98
- browserAPI.storage.local.get('wt-hud-state', function(result) {
99
- hudState = result['wt-hud-state'] || {};
100
- if (iframe.contentWindow) {
101
- iframe.contentWindow.postMessage({ type: 'REHYDRATE', state: hudState }, browserAPI.runtime.getURL(''));
102
- }
103
- });
104
- };
105
-
106
- // Listen for HUD state changes from the iframe to toggle visibility
107
- window.addEventListener('message', function(e) {
108
- if (e.data && e.data.type === 'HUD_STATE_CHANGED' && e.source === iframe.contentWindow) {
109
- const visible = e.data.state && e.data.state.hudVisible !== false;
110
- if (visible) {
111
- iframe.style.width = '100vw';
112
- iframe.style.height = '100vh';
113
- iframe.style.visibility = 'visible';
114
- iframe.style.opacity = '1';
115
- } else {
116
- iframe.style.width = '0';
117
- iframe.style.height = '0';
118
- iframe.style.visibility = 'hidden';
119
- iframe.style.opacity = '0';
120
- }
121
- }
122
- });
123
- }
124
-
125
- // Navigation guard: re-mount if host is removed (SPA/bfcache safe)
126
- const observer = new MutationObserver(function() {
127
- if (!document.documentElement.contains(host)) {
128
- createMount();
129
- }
130
- if (window.__wt_visualComponents && window.__wt_visualComponents.cursor && !document.getElementById('__Weavetab_cursor_host')) {
131
- if (typeof __wt_recoverVisuals === 'function') __wt_recoverVisuals();
132
- }
133
- });
134
- observer.observe(document.documentElement, { childList: true });
135
-
136
- // Real-time event relays (Agent only)
137
- // Removed physical mousemove, click, and keydown listeners to prevent lag and interference with agent visuals.
138
-
139
- let scrollTicking = false;
140
- document.addEventListener('scroll', function() {
141
- if (!scrollTicking && iframe && iframe.contentWindow) {
142
- window.requestAnimationFrame(function() {
143
- iframe.contentWindow.postMessage({
144
- type: 'RT_SCROLL',
145
- y: window.scrollY,
146
- ratio: window.scrollY / document.body.scrollHeight
147
- }, '*');
148
- scrollTicking = false;
149
- });
150
- scrollTicking = true;
151
- }
152
- }, { passive: true });
153
-
154
- // ── MASTER VISUALS TOGGLE SYSTEM ───────────────────────────
155
- let __wt_masterVisualsEnabled = true;
156
-
157
- function __wt_applyMasterVisuals(enabled) {
158
- __wt_masterVisualsEnabled = !!enabled;
159
- window.__wt_masterVisualsEnabled = __wt_masterVisualsEnabled;
160
-
161
- const cursorHost = document.getElementById('__Weavetab_cursor_host');
162
- if (cursorHost) {
163
- cursorHost.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
164
- }
165
- if (typeof __weaveCursor !== 'undefined' && __weaveCursor) {
166
- __weaveCursor.setVisible(__wt_masterVisualsEnabled);
167
- if (__weaveCursor._cursor) {
168
- __weaveCursor._cursor.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
169
- } else if (__weaveCursor._canvas) {
170
- __weaveCursor._canvas.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
171
- }
172
- }
173
-
174
- const hudEl = document.getElementById('Weavetab-hud');
175
- if (hudEl) {
176
- hudEl.style.display = 'none';
177
- }
178
-
179
- const hudHost = document.querySelector('weavetab-host');
180
- if (hudHost) {
181
- hudHost.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
182
- }
183
-
184
- const indicatorEl = document.getElementById('wt-controlled-indicator') || document.querySelector('.wt-controlled-indicator') || document.getElementById('__Weavetab_controlled_indicator');
185
- if (indicatorEl) {
186
- indicatorEl.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
187
- }
188
-
189
- if (!__wt_masterVisualsEnabled) {
190
- if (typeof clearXRay === 'function') clearXRay();
191
- document.querySelectorAll('.Weavetab-ripple, .wt-xray-spotlight, .wt-bubble, .__Weavetab_xray').forEach(function(el){
192
- el.remove();
193
- });
194
- }
195
- }
196
-
197
- try {
198
- browserAPI.storage.local.get(['wt_master_visuals'], function(res) {
199
- if (res && res.wt_master_visuals === false) {
200
- __wt_applyMasterVisuals(false);
201
- } else {
202
- __wt_applyMasterVisuals(true);
203
- }
204
- });
205
-
206
- browserAPI.storage.onChanged.addListener(function(changes, area) {
207
- if (area === 'local' && changes.wt_master_visuals !== undefined) {
208
- __wt_applyMasterVisuals(changes.wt_master_visuals.newValue !== false);
209
- }
210
- });
211
- } catch(e) {}
212
-
213
- // Ghost CDP cursor relay from background
214
- browserAPI.runtime.onMessage.addListener(function(msg) {
215
- if (msg.type === 'WT_TOGGLE_MASTER_VISUALS') {
216
- __wt_applyMasterVisuals(msg.enabled);
217
- }
218
- if (msg.type === 'GHOST_CURSOR_MOVE') {
219
- if (typeof msg.x === 'number' && typeof msg.y === 'number') {
220
- moveCursor(msg.x, msg.y, msg.phase);
221
- }
222
- if (iframe && iframe.contentWindow) {
223
- iframe.contentWindow.postMessage({ type: 'RT_CURSOR_MOVE', x: msg.x, y: msg.y, phase: msg.phase }, '*');
224
- }
225
- }
226
- if (msg.type === 'TOOL_CALL_EVENT' && iframe && iframe.contentWindow) {
227
- iframe.contentWindow.postMessage({ type: 'TOOL_CALL_EVENT', payload: msg.payload }, '*');
228
- }
229
- // Scripted pill toggle — relay to HUD iframe
230
- if (msg.type === 'WT_SCRIPTED_PILL_TOGGLE' && iframe && iframe.contentWindow) {
231
- iframe.contentWindow.postMessage({ type: 'WT_SCRIPTED_PILL_TOGGLE', enabled: msg.enabled }, '*');
232
- }
233
- // Handle agent thoughts in middle-down HUD
234
- if (msg.type === 'WT_THOUGHT' || msg.type === 'Weavetab_THOUGHT') {
235
- __wt_showThought(msg.text, msg);
236
- }
237
- if (msg.type === 'WT_CLEAR_THOUGHT' || msg.type === 'Weavetab_CLEAR_THOUGHT') {
238
- __wt_removeThought();
239
- }
240
- // Handle incoming user notes from the extension
241
- if (msg.type === 'WT_USER_NOTE_INJECT' || msg.type === 'WT_USER_NOTE') {
242
- window.__wt_chat_queue = window.__wt_chat_queue || [];
243
- let formattedMsg = msg.text || '';
244
- if (msg.snipe) {
245
- formattedMsg = `[USER AREA ANNOTATION ATTACHED (${msg.snipe.mode})]: ` + formattedMsg;
246
- }
247
- window.__wt_chat_queue.push(formattedMsg);
248
-
249
- // Unblock pending ask_bridge polling loops immediately
250
- window.__wt_ask_bridge = window.__wt_ask_bridge || {};
251
- if (window.__wt_ask_bridge.resolve) {
252
- window.__wt_ask_bridge.resolve({
253
- success: true,
254
- response: formattedMsg,
255
- status: 'answered'
256
- });
257
- window.__wt_ask_bridge = {};
258
- }
259
-
260
- // Display on-page glassmorphism notification banner if master visuals are enabled
261
- if (window.__wt_masterVisualsEnabled !== false) {
262
- __wt_showUserNoteVisualBanner(formattedMsg);
263
- }
264
- }
265
- });
266
-
267
- function __wt_showUserNoteVisualBanner(text) {
268
- let banner = document.getElementById('wt-user-note-banner');
269
- if (!banner) {
270
- banner = document.createElement('div');
271
- banner.id = 'wt-user-note-banner';
272
- banner.setAttribute('data-wt-ignore', 'true');
273
- banner.style.cssText = `
274
- position: fixed; top: 16px; right: 20px; z-index: 2147483647;
275
- background: rgba(24, 24, 27, 0.94); backdrop-filter: blur(12px);
276
- border: 1px solid #38bdf8; color: #f4f4f5; font-weight: 500;
277
- font-size: 11px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
278
- padding: 8px 16px; border-radius: 20px;
279
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 14px rgba(56, 189, 248, 0.4);
280
- display: flex; align-items: center; gap: 8px;
281
- transition: opacity 0.3s ease, transform 0.3s ease;
282
- opacity: 0; transform: translateY(-10px); pointer-events: none;
283
- `;
284
- document.documentElement.appendChild(banner);
285
- }
286
- const cleanText = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').slice(0, 90);
287
- banner.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#38bdf8" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span style="color:#38bdf8;font-weight:600;">Message Received:</span> <span>${cleanText}</span>`;
288
- requestAnimationFrame(() => {
289
- banner.style.opacity = '1';
290
- banner.style.transform = 'translateY(0)';
291
- });
292
- setTimeout(() => {
293
- banner.style.opacity = '0';
294
- banner.style.transform = 'translateY(-10px)';
295
- setTimeout(() => banner.remove(), 300);
296
- }, 4000);
297
- }
298
-
299
- // State persistence bridge: listen for state changes from iframe
300
- window.addEventListener('message', function(e) {
301
- // Forward relevant events to the extension background
302
- if (e.data && (
303
- e.data.type === 'WT_TOOL_CALL' ||
304
- e.data.type === 'WT_PLAN_UPDATE' ||
305
- e.data.type === 'WT_THOUGHT' ||
306
- e.data.type === 'TOOL_CALL_EVENT' ||
307
- e.data.type === 'Weavetab_CONFIG' ||
308
- e.data.type === 'WT_CONFIG_UPDATE'
309
- )) {
310
- __wt_safeSendMessage(e.data);
311
- }
312
-
313
-
314
- if (e.origin !== browserAPI.runtime.getURL('').slice(0, -1)) return;
315
- if (e.data.type === 'HUD_STATE_CHANGED') {
316
- hudState = { ...hudState, ...e.data.state };
317
- browserAPI.storage.local.set({ 'wt-hud-state': hudState });
318
- }
319
- });
320
-
321
- // Listen for toggle command from background
322
- browserAPI.runtime.onMessage.addListener(function(msg) {
323
- console.log('[Weavetab Content] Received message:', msg.type);
324
- if (msg.type === 'TOGGLE_HUD' && iframe && iframe.contentWindow) {
325
- iframe.contentWindow.postMessage({ type: 'TOGGLE_HUD' }, '*');
326
- }
327
- if (msg.type === 'TOGGLE_DEBUG' && iframe && iframe.contentWindow) {
328
- console.log('[Weavetab Content] Forwarding TOGGLE_DEBUG to iframe');
329
- iframe.contentWindow.postMessage({ type: 'TOGGLE_DEBUG' }, '*');
330
- }
331
- });
332
-
333
- // Initial mount
334
- createMount();
335
- })();
336
-
337
- // ── UNIFIED VISUAL SYSTEM INITIALIZATION ─────────────────────────
338
- // Ensures all visual components initialize in correct order and persist across reloads
339
- window.__wt_visualsReady = false;
340
- window.__wt_visualComponents = {
341
- cursor: false,
342
- feedback: false,
343
- debug: false
344
- };
345
- window.__wt_zIndexCounter = 2147483640; // Start near max z-index
346
-
347
- function __wt_initVisuals() {
348
- if (window.__wt_visualsReady) return;
349
-
350
- // Initialize cursor first
351
- if (!window.__wt_visualComponents.cursor && window.__weaveCursor_instance) {
352
- window.__wt_visualComponents.cursor = true;
353
- }
354
-
355
- // Check for WeaveTab OS panel (todo list + chat)
356
- if (!window.__wt_visualComponents.feedback && document.getElementById('wt-main-container')) {
357
- window.__wt_visualComponents.feedback = true;
358
- }
359
-
360
- // Check for debug overlay
361
- if (!window.__wt_visualComponents.debug && document.getElementById('wt-debug-overlay')) {
362
- window.__wt_visualComponents.debug = true;
363
- }
364
-
365
- // Mark as ready when cursor and panel are initialized
366
- if (window.__wt_visualComponents.cursor && window.__wt_visualComponents.feedback) {
367
- window.__wt_visualsReady = true;
368
- }
369
- }
370
-
371
- // Health check every 5 seconds to recover missing components
372
- setInterval(function() {
373
- __wt_initVisuals();
374
- __wt_recoverVisuals();
375
- }, 5000);
376
-
377
- // Visual recovery - detect and fix missing/broken components
378
- function __wt_recoverVisuals() {
379
- // Recover cursor if missing
380
- if (window.__wt_visualComponents && window.__wt_visualComponents.cursor && !document.getElementById('__Weavetab_cursor_host')) {
381
- console.warn('[Weavetab] Cursor host missing, reinitializing...');
382
- if (window.__weaveCursor_instance && window.__weaveCursor_instance.destroy) {
383
- window.__weaveCursor_instance.destroy();
384
- }
385
- __weaveCursor = new WeaveCursor();
386
- window.__weaveCursor_instance = __weaveCursor;
387
- window.__wt_visualComponents.cursor = true;
388
- }
389
-
390
- // Recover WeaveTab OS panel if missing
391
- if (window.__wt_visualComponents.feedback && !document.getElementById('wt-main-container')) {
392
- console.warn('[Weavetab] WeaveTab OS panel missing, this is handled by page reload');
393
- }
394
-
395
- // Fix z-index conflicts
396
- var wtElements = document.querySelectorAll('[data-wt-ignore]');
397
- wtElements.forEach(function(el) {
398
- if (el.style.zIndex && parseInt(el.style.zIndex) < 2147483640) {
399
- el.style.zIndex = ++window.__wt_zIndexCounter;
400
- }
401
- });
402
- }
403
-
404
- // ── STALE ELEMENT CLEANUP ─────────────────────────────────────
405
- // Removes old UI elements from previous server/extension sessions.
406
- // Observer stays active for the full page lifetime (SPA-safe).
407
- (function cleanupOldUI() {
408
- var OLD_IDS = [
409
- 'wt-ghost-cursor','wt-ghost-bubble','wt-ask-panel','wt-cur-canvas',
410
- 'wt-ask-container','wt-msg-panel','wt-ask-title','wt-msg-bar','wt-msg-pill'
411
- ];
412
- function removeOld() {
413
- for (var i = 0; i < OLD_IDS.length; i++) {
414
- var el = document.getElementById(OLD_IDS[i]);
415
- if (el) el.remove();
416
- }
417
- }
418
- removeOld();
419
- // Persistent observer — never disconnects so SPA re-renders stay clean
420
- var obs = new MutationObserver(function() { removeOld(); });
421
- if (document.documentElement) {
422
- obs.observe(document.documentElement, { childList: true, subtree: true });
423
- } else {
424
- document.addEventListener('DOMContentLoaded', function() {
425
- if (document.documentElement) obs.observe(document.documentElement, { childList: true, subtree: true });
426
- });
427
- }
428
- })();
429
-
430
- // ── UI DESIGN SYSTEM (inject once, styles.css handles visuals) ─
431
- (function injectWeavetabUI() {
432
- if (document.getElementById('wt-ui-styles')) return;
433
- // Guard marker so we only run once
434
- const m = document.createElement('meta');
435
- m.id = 'wt-ui-styles';
436
- (document.head || document.documentElement || document.body).appendChild(m);
437
- })();
438
-
439
- // ── THOUGHT BUBBLE ─────────────────────────────────────────────
440
- let __wt_activeBubble = null;
441
- let __wt_bubbleTimer = null;
442
- let __wt_typewriterTimer = null;
443
- let __wt_feedbackSticky = false;
444
- let __wt_thoughtBirth = 0;
445
- let __wt_thoughtQueue = [];
446
- let __wt_lastRemoveTime = 0;
447
- let __wt_toolActive = false;
448
-
449
- function __wt_fadeOutBubble(callback) {
450
- if (!__wt_activeBubble) { if (callback) callback(); return; }
451
- var bub = __wt_activeBubble;
452
- bub.style.transition = 'opacity 0.15s ease-out, transform 0.15s ease-out';
453
- bub.style.opacity = '0';
454
- bub.style.transform = 'translateX(-50%) translateY(6px)';
455
- setTimeout(function() {
456
- bub.remove();
457
- if (callback) callback();
458
- }, 150);
459
- }
460
-
461
- function __wt_destroyBubble() {
462
- if (__wt_bubbleTimer) { clearTimeout(__wt_bubbleTimer); __wt_bubbleTimer = null; }
463
- if (__wt_typewriterTimer) { clearInterval(__wt_typewriterTimer); __wt_typewriterTimer = null; }
464
- var current = document.querySelector('.wt-bubble[data-wt-thought]');
465
- if (current) {
466
- current.style.transition = 'opacity 0.15s ease-out, transform 0.15s ease-out';
467
- current.style.opacity = '0';
468
- current.style.transform = 'translateX(-50%) translateY(6px)';
469
- setTimeout(function() {
470
- if (current && current.parentNode) current.remove();
471
- __wt_activeBubble = null;
472
- }, 150);
473
- } else {
474
- __wt_activeBubble = null;
475
- }
476
- __wt_lastRemoveTime = Date.now();
477
- }
478
-
479
- // ── MIDDLE-DOWN THOUGHT & PRESENCE HUD ─────────────────────────────
480
- let __wt_thoughtHudEl = null;
481
- let __wt_thoughtHudTimer = null;
482
- let __wt_thoughtHudCountdown = null;
483
-
484
- function __wt_highlightAnchor(anchor) {
485
- if (!anchor) return;
486
- try {
487
- let sel = anchor;
488
- if (anchor.startsWith('w:')) {
489
- const stripped = anchor.slice(2);
490
- sel = `[data-wt-id="${stripped}"]`;
491
- }
492
- const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
493
- if (!el) return;
494
- const r = el.getBoundingClientRect();
495
- if (r.width === 0 && r.height === 0) return;
496
-
497
- const ring = document.createElement('div');
498
- ring.setAttribute('data-wt-ignore', 'true');
499
- ring.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;border:1.5px solid #38bdf8;box-shadow:0 0 16px rgba(56,189,248,0.5);border-radius:4px;z-index:2147483646;pointer-events:none;animation:wt-consider-pulse 1.2s ease-in-out infinite;`;
500
- (document.getElementById('Weavetab-root')?.shadowRoot || document.body || document.documentElement).appendChild(ring);
501
- setTimeout(() => {
502
- ring.style.transition = 'opacity 0.25s ease-out';
503
- ring.style.opacity = '0';
504
- setTimeout(() => ring.remove(), 260);
505
- }, 2400);
506
- } catch(e) {}
507
- }
508
-
509
- function __wt_removeThought() {
510
- if (__wt_thoughtHudTimer) {
511
- clearTimeout(__wt_thoughtHudTimer);
512
- __wt_thoughtHudTimer = null;
513
- }
514
- if (__wt_thoughtHudCountdown) {
515
- clearInterval(__wt_thoughtHudCountdown);
516
- __wt_thoughtHudCountdown = null;
517
- }
518
- if (__wt_thoughtHudEl) {
519
- const el = __wt_thoughtHudEl;
520
- __wt_thoughtHudEl = null;
521
- el.style.opacity = '0';
522
- el.style.transform = 'translateX(-50%) translateY(10px)';
523
- setTimeout(() => {
524
- if (el.parentNode) el.remove();
525
- }, 220);
526
- }
527
- __wt_feedbackSticky = false;
528
- clearTimeout(window.__wt_feedbackStickyTimer);
529
- __wt_destroyBubble();
530
- }
531
-
532
- function __wt_showThought(text, options) {
533
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
534
- if (!text || typeof text !== 'string') return;
535
-
536
- const opts = options || {};
537
- const isQuestion = opts.action === 'question' || opts.kind === 'question' || !!opts.ask;
538
- const anchor = opts.anchor || '';
539
-
540
- // Contextual visual anchor highlight
541
- if (anchor) {
542
- __wt_highlightAnchor(anchor);
543
- }
544
-
545
- // Clear pending timers
546
- if (__wt_thoughtHudTimer) {
547
- clearTimeout(__wt_thoughtHudTimer);
548
- __wt_thoughtHudTimer = null;
549
- }
550
- if (__wt_thoughtHudCountdown) {
551
- clearInterval(__wt_thoughtHudCountdown);
552
- __wt_thoughtHudCountdown = null;
553
- }
554
-
555
- let hud = __wt_thoughtHudEl;
556
- if (!hud || !hud.parentNode) {
557
- hud = document.createElement('div');
558
- hud.id = '__wt_thought_hud';
559
- hud.setAttribute('data-wt-ignore', 'true');
560
- hud.style.cssText = `
561
- position: fixed;
562
- bottom: 24px;
563
- left: 50%;
564
- transform: translateX(-50%) translateY(8px);
565
- z-index: 2147483647;
566
- width: 320px;
567
- max-width: calc(100vw - 32px);
568
- background: #0b0d12;
569
- border: 1px solid #1e222b;
570
- border-radius: 4px;
571
- padding: 10px 12px;
572
- box-shadow: 0 10px 30px rgba(0,0,0,0.8), 0 0 1px rgba(255,255,255,0.08);
573
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
574
- color: #f3f4f6;
575
- pointer-events: auto;
576
- opacity: 0;
577
- transition: opacity 0.18s cubic-bezier(0.16, 1, 0.3, 1), transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
578
- box-sizing: border-box;
579
- `;
580
- (document.getElementById('Weavetab-root')?.shadowRoot || document.body || document.documentElement).appendChild(hud);
581
- __wt_thoughtHudEl = hud;
582
- }
583
-
584
- const cleanText = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
585
- const anchorBadge = anchor ? `<span style="color:#2b70ff;font-size:10px;font-family:monospace;font-weight:600;margin-left:4px;">@${anchor.replace(/&/g,'&amp;')}</span>` : '';
586
-
587
- if (isQuestion) {
588
- const timeoutSec = (opts.timeout || (opts.ask && opts.ask.timeout) || 20);
589
- hud.innerHTML = `
590
- <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:8px;">
591
- <div style="font-size:12px;line-height:1.4;color:#ffffff;font-weight:500;word-break:break-word;">
592
- ${cleanText}${anchorBadge}
593
- </div>
594
- <button id="wt-hud-close" title="Dismiss" style="background:transparent;border:none;color:#555d6d;font-size:12px;cursor:pointer;padding:0 2px;line-height:1;margin-top:1px;">✕</button>
595
- </div>
596
- <div style="display:flex;align-items:center;gap:6px;">
597
- <input id="wt-hud-input" type="text" placeholder="Type answer..." style="height:28px;background:#14161f;border:1px solid #252a36;border-radius:3px;padding:0 8px;font-size:12px;color:#fff;outline:none;flex:1;min-width:0;caret-color:#2b70ff;font-family:inherit;box-sizing:border-box;" />
598
- <button id="wt-hud-submit-btn" title="Send" style="width:28px;height:28px;background:#2b70ff;border:none;border-radius:3px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;padding:0;box-sizing:border-box;">
599
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
600
- </button>
601
- <span id="wt-hud-countdown" style="font-size:10px;color:#64748b;font-family:monospace;margin-left:2px;min-width:20px;text-align:right;">${timeoutSec}s</span>
602
- </div>
603
- `;
604
- } else {
605
- // Feedback / observation mode — ultra compact single row
606
- hud.innerHTML = `
607
- <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
608
- <div style="display:flex;align-items:center;gap:7px;min-width:0;">
609
- <span style="display:inline-block;width:5px;height:5px;border-radius:50%;background:#2b70ff;box-shadow:0 0 6px #2b70ff;flex-shrink:0;"></span>
610
- <span style="font-size:12px;line-height:1.4;color:#f3f4f6;word-break:break-word;font-weight:450;">${cleanText}${anchorBadge}</span>
611
- </div>
612
- <button id="wt-hud-close" title="Dismiss" style="background:transparent;border:none;color:#555d6d;font-size:12px;cursor:pointer;padding:0 2px;line-height:1;flex-shrink:0;">✕</button>
613
- </div>
614
- `;
615
- }
616
-
617
- // Animate in
618
- requestAnimationFrame(() => {
619
- if (hud) {
620
- hud.style.opacity = '1';
621
- hud.style.transform = 'translateX(-50%) translateY(0)';
622
- }
623
- });
624
-
625
- // Wire close button
626
- const closeBtn = hud.querySelector('#wt-hud-close');
627
- if (closeBtn) {
628
- closeBtn.addEventListener('click', () => {
629
- __wt_removeThought();
630
- });
631
- }
632
-
633
- if (isQuestion) {
634
- let askBridge = document.getElementById('__wt_ask_bridge');
635
- if (!askBridge) {
636
- askBridge = document.createElement('div');
637
- askBridge.id = '__wt_ask_bridge';
638
- askBridge.style.display = 'none';
639
- document.body.appendChild(askBridge);
640
- }
641
- askBridge.dataset.status = 'pending';
642
- askBridge.dataset.result = '';
643
-
644
- const input = hud.querySelector('#wt-hud-input');
645
- const submitBtn = hud.querySelector('#wt-hud-submit-btn');
646
- const countdownSpan = hud.querySelector('#wt-hud-countdown');
647
-
648
- let remaining = opts.timeout || (opts.ask && opts.ask.timeout) || 20;
649
-
650
- __wt_thoughtHudCountdown = setInterval(() => {
651
- remaining--;
652
- if (countdownSpan) countdownSpan.textContent = `${remaining}s`;
653
- if (remaining <= 0) {
654
- clearInterval(__wt_thoughtHudCountdown);
655
- __wt_thoughtHudCountdown = null;
656
- if (askBridge) {
657
- askBridge.dataset.status = 'timeout';
658
- askBridge.dataset.result = '';
659
- }
660
- __wt_removeThought();
661
- }
662
- }, 1000);
663
-
664
- const submit = () => {
665
- const val = input ? input.value.trim() : '';
666
- if (askBridge) {
667
- askBridge.dataset.status = 'submitted';
668
- askBridge.dataset.result = val;
669
- }
670
- if (window.__wt_ask_bridge && window.__wt_ask_bridge.resolve) {
671
- window.__wt_ask_bridge.resolve({ success: true, response: val, status: 'answered' });
672
- window.__wt_ask_bridge = {};
673
- }
674
- __wt_removeThought();
675
- };
676
-
677
- if (submitBtn) submitBtn.addEventListener('click', submit);
678
- if (input) {
679
- input.addEventListener('focus', () => { input.style.borderColor = '#2b70ff'; });
680
- input.addEventListener('blur', () => { input.style.borderColor = '#252a36'; });
681
- input.addEventListener('keydown', (e) => {
682
- if (e.key === 'Enter') submit();
683
- if (e.key === 'Escape') __wt_removeThought();
684
- });
685
- setTimeout(() => { if (input) input.focus(); }, 80);
686
- }
687
- } else {
688
- // Auto-fade timer for feedback
689
- const autoDuration = Math.max(3800, Math.min(8000, text.length * 50));
690
- __wt_thoughtHudTimer = setTimeout(() => {
691
- __wt_removeThought();
692
- }, autoDuration);
693
- }
694
- }
695
-
696
- function __wt_renderThought(text, options) {
697
- __wt_showThought(text, options);
698
- }
699
-
700
- function __wt_processThoughtQueue() {
701
- if (__wt_thoughtQueue.length > 0) {
702
- var next = __wt_thoughtQueue.shift();
703
- setTimeout(function() { __wt_showThought(next.text, next.options); }, 200);
704
- }
705
- }
706
-
707
- function __wt_showQuestion(text, inputType, timeoutSeconds) {
708
- __wt_showThought(text, { action: 'question', timeout: timeoutSeconds || 20 });
709
- }
710
-
711
- function __wt_removeAsk() {
712
- __wt_removeThought();
713
- }
714
-
715
- // ── SHARED OVERLAY CONTAINER ────────────────────────────────────
716
- const __wt_shadowStyles = `
717
- .Weavetab-ripple {
718
- width: 40px; height: 40px; margin: -20px 0 0 -20px; border-radius: 50%;
719
- background: rgba(79,142,247,0.35); border: 2px solid #4F8EF7;
720
- animation: __wt_ripple 0.55s ease-out forwards;
721
- }
722
- @keyframes __wt_ripple { from { transform: scale(0.3); opacity: 1; } to { transform: scale(2.2); opacity: 0; } }
723
- @keyframes __wt_xray_pulse {
724
- 0% { opacity: 0; transform: scale(0.94); }
725
- 18% { opacity: 1; transform: scale(1.02); }
726
- 70% { opacity: 0.85; transform: scale(1); }
727
- 100% { opacity: 0; transform: scale(0.97); }
728
- }
729
- `;
730
-
731
- function getWeavetabRoot() {
732
- let host = document.getElementById('Weavetab-root');
733
- if (!host) {
734
- host = document.createElement('div');
735
- host.id = 'Weavetab-root';
736
- host.setAttribute('data-wt-ignore', '');
737
- Object.assign(host.style, {
738
- position: 'fixed', left: '0', top: '0',
739
- width: '0', height: '0',
740
- zIndex: '2147483640', pointerEvents: 'none',
741
- });
742
- var root = document.documentElement || document.body;
743
- if (root) root.appendChild(host);
744
- }
745
- // Reuse existing shadow root (open mode, set by CDP injector or ourselves)
746
- if (host.shadowRoot) {
747
- return host.shadowRoot;
748
- }
749
- // Guard against NotSupportedError when another context already attached a closed shadow root
750
- try {
751
- const sr = host.attachShadow({ mode: 'open' });
752
- const style = document.createElement('style');
753
- style.textContent = __wt_shadowStyles;
754
- sr.appendChild(style);
755
- return sr;
756
- } catch (e) {
757
- // FIX: fallback must be invisible and non-blocking so it can't
758
- // cover page content or cause opacity bleeding on sites like example.com.
759
- const fallback = document.createElement('div');
760
- fallback.setAttribute('data-wt-ignore', '');
761
- fallback.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;pointer-events:none;overflow:visible;z-index:2147483640;';
762
- __wt_safeAppendToBody(fallback);
763
- return fallback;
764
- }
765
- }
766
-
767
- // ── COORDINATE ORACLE ───────────────────────────────────────────
768
- function shadowQuery(selector) {
769
- function searchRoot(root) {
770
- const found = root.querySelector(selector);
771
- if (found) return found;
772
-
773
- for (const el of root.querySelectorAll('*')) {
774
- if (el.shadowRoot) {
775
- const result = searchRoot(el.shadowRoot);
776
- if (result) return result;
777
- }
778
- }
779
- return null;
780
- }
781
- return searchRoot(document);
782
- }
783
- window.__Weavetab = {
784
- version: '2.5.0',
785
- suppressVisuals: false,
786
- shadowQuery: shadowQuery,
787
-
788
- // ── CDP Co-Processor Helpers ──────────────────────────────────────────────
789
- // Call these via Runtime.evaluate instead of doing expensive CDP-side work.
790
- // All return plain JSON-serializable values.
791
-
792
- /** Scroll position, viewport size, and page dimensions in one call. */
793
- getScrollInfo: function() {
794
- var vv = window.visualViewport || {};
795
- var body = document.body || document.documentElement;
796
- return {
797
- scrollX: Math.round(window.scrollX),
798
- scrollY: Math.round(window.scrollY),
799
- viewportW: Math.round(vv.width || window.innerWidth),
800
- viewportH: Math.round(vv.height || window.innerHeight),
801
- pageW: Math.round(body.scrollWidth),
802
- pageH: Math.round(body.scrollHeight),
803
- maxScrollX: Math.round(Math.max(0, body.scrollWidth - window.innerWidth)),
804
- maxScrollY: Math.round(Math.max(0, body.scrollHeight - window.innerHeight)),
805
- scrollRatio: body.scrollHeight > 0 ? +(window.scrollY / body.scrollHeight).toFixed(3) : 0
806
- };
807
- },
808
-
809
- /**
810
- * Wait until no DOM mutations occur for `quietMs` milliseconds.
811
- * Resolves to true when stable, false on timeout (default 5000ms).
812
- * CDP calls this and awaits — replaces dumb sleep() calls.
813
- */
814
- waitForStable: function(quietMs, timeoutMs) {
815
- quietMs = quietMs || 300;
816
- timeoutMs = timeoutMs || 5000;
817
- return new Promise(function(resolve) {
818
- var timer = null;
819
- var deadline = setTimeout(function() {
820
- obs.disconnect();
821
- resolve(false);
822
- }, timeoutMs);
823
- function reset() {
824
- if (timer) clearTimeout(timer);
825
- timer = setTimeout(function() {
826
- clearTimeout(deadline);
827
- obs.disconnect();
828
- resolve(true);
829
- }, quietMs);
830
- }
831
- var obs = new MutationObserver(reset);
832
- obs.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });
833
- reset(); // start the quiet timer immediately
834
- });
835
- },
836
-
837
- /**
838
- * Resolve a data-wt-id directly from the live index — much faster than
839
- * doing a full accessibility tree walk.
840
- * Returns { id, x, y, rect, tag, label, viewport, isVisible } or null.
841
- */
842
- getElementByWtId: function(id) {
843
- var idx = window.__wt_liveIndex;
844
- if (!idx) return null;
845
- var entry = null;
846
- for (var i = 0; i < idx.length; i++) {
847
- if (idx[i].id === String(id)) { entry = idx[i]; break; }
848
- }
849
- if (!entry) return null;
850
- // Refresh coordinates from DOM if element still exists
851
- var el = document.querySelector('[data-wt-id="' + entry.id + '"]');
852
- if (el) {
853
- var r = el.getBoundingClientRect();
854
- entry.x = Math.round(r.left + r.width / 2);
855
- entry.y = Math.round(r.top + r.height / 2);
856
- entry.rect = { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) };
857
- }
858
- return entry;
859
- },
860
-
861
- /**
862
- * Dump all form field current values — lets agent verify form state cheaply.
863
- * Returns array of { name, id, type, value, tag }.
864
- */
865
- getFormValues: function() {
866
- var results = [];
867
- var els = document.querySelectorAll('input, select, textarea');
868
- for (var i = 0; i < els.length; i++) {
869
- var el = els[i];
870
- if (el.hasAttribute('data-wt-ignore')) continue;
871
- var val = el.type === 'checkbox' || el.type === 'radio' ? el.checked : el.value;
872
- results.push({
873
- name: el.name || el.id || null,
874
- id: el.id || null,
875
- wtId: el.getAttribute('data-wt-id') || null,
876
- type: el.type || el.tagName.toLowerCase(),
877
- tag: el.tagName.toLowerCase(),
878
- value: val,
879
- placeholder: el.placeholder || null
880
- });
881
- }
882
- return results;
883
- },
884
-
885
- /**
886
- * Check visibility of a selector — uses the live IntersectionObserver data
887
- * maintained by the MutationObserver. Much faster than a full DOM query.
888
- * Returns { found, visible, occluded, x, y, rect } or null.
889
- */
890
- checkVisible: function(selector) {
891
- var el = document.querySelector(selector) || shadowQuery(selector);
892
- if (!el) return { found: false };
893
- var r = el.getBoundingClientRect();
894
- var vv = window.visualViewport || {};
895
- var vw = vv.width || window.innerWidth;
896
- var vh = vv.height || window.innerHeight;
897
- var inViewport = r.width > 0 && r.height > 0 && r.top < vh && r.bottom > 0 && r.left < vw && r.right > 0;
898
- var topEl = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
899
- var occluded = topEl !== el && !el.contains(topEl);
900
- return {
901
- found: true,
902
- visible: inViewport,
903
- occluded: occluded,
904
- x: Math.round(r.left + r.width / 2),
905
- y: Math.round(r.top + r.height / 2),
906
- rect: { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) }
907
- };
908
- },
909
-
910
- /**
911
- * Return the live element index, optionally filtered.
912
- * filter: 'visible' | 'all' (default 'visible')
913
- * Much faster than browser_map for simple lookups.
914
- */
915
- getLiveIndex: function(filter) {
916
- var idx = window.__wt_liveIndex || [];
917
- if (filter === 'all') return idx;
918
- // Default: visible viewport only
919
- return idx.filter(function(e) { return e.viewport === 'visible' || e.isVisible; });
920
- },
921
-
922
- /**
923
- * Pop and return the next user feedback message from the queue.
924
- * MCP calls this between tool calls to check for user interrupts.
925
- * Returns { text, ts, tabId } or null if queue is empty.
926
- * Non-destructive — call popUserFeedback() to consume, peekUserFeedback() to inspect.
927
- */
928
- popUserFeedback: function() {
929
- // Read from localStorage queue (written by submitFeedback + persisted by background)
930
- try {
931
- var raw = localStorage.getItem('__wt_chat_queue_persist');
932
- if (!raw) return null;
933
- var arr = JSON.parse(raw);
934
- if (!arr || !arr.length) return null;
935
- var item = arr.shift();
936
- localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(arr));
937
- return item;
938
- } catch(e) { return null; }
939
- },
940
-
941
- /**
942
- * Check if a blocking dialog (alert/confirm/prompt) is pending on this tab.
943
- * Agent should check this before clicking if it suspects a dialog might block.
944
- * Returns { pending: bool, ts: number } — reads from extension storage via background relay.
945
- */
946
- getAlertState: function() {
947
- // Sync check via a DOM marker set by alert wrapper
948
- return { pending: !!(window.__wt_alertPending), ts: Date.now() };
949
- },
950
- };
951
- window.__Weavetab_shadowQuery = shadowQuery;
952
-
953
- function isOccluded(el) {
954
- const rect = el.getBoundingClientRect();
955
- const cx = rect.left + rect.width / 2;
956
- const cy = rect.top + rect.height / 2;
957
- const topEl = document.elementFromPoint(cx, cy);
958
- return topEl !== el && !el.contains(topEl);
959
- }
960
-
961
- window.__Weavetab_getCoords = function(selector) {
962
- const el = document.querySelector(selector) || shadowQuery(selector);
963
-
964
- if (!el) return null;
965
-
966
- const rect = el.getBoundingClientRect();
967
- const vv = window.visualViewport;
968
-
969
- return {
970
- x: rect.left + rect.width / 2 + vv.offsetLeft,
971
- y: rect.top + rect.height / 2 + vv.offsetTop,
972
- raw: { left: rect.left, top: rect.top, w: rect.width, h: rect.height },
973
- occluded: isOccluded(el),
974
- visible: rect.width > 0 && rect.height > 0
975
- && rect.top < vv.height && rect.bottom > 0
976
- };
977
- };
978
-
979
- function getStableSelector(el) {
980
- if (el.id) return `#${el.id}`;
981
- const path = [];
982
- while (el.parentNode) {
983
- let index = 1;
984
- let sibling = el.previousElementSibling;
985
- while (sibling) {
986
- if (sibling.tagName === el.tagName) index++;
987
- sibling = sibling.previousElementSibling;
988
- }
989
- path.unshift(`${el.tagName.toLowerCase()}:nth-of-type(${index})`);
990
- el = el.parentNode;
991
- }
992
- return path.join(' > ');
993
- }
994
-
995
- window.__Weavetab_fullMap = function() {
996
- const map = [];
997
- let idCounter = 1;
998
- function crawl(root, depth = 0) {
999
- for (const el of root.querySelectorAll(
1000
- 'a,button,input,select,textarea,[role],[onclick],[tabindex]'
1001
- )) {
1002
- if (el.hasAttribute('data-wt-ignore')) continue;
1003
- const rect = el.getBoundingClientRect();
1004
- // Always include form elements (input/textarea/select/button) regardless of rect size
1005
- // — canvas-heavy pages can occlude them and getBoundingClientRect returns 0
1006
- const isEssential = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(el.tagName);
1007
- if (rect.width === 0 && !isEssential) continue;
1008
- let id = el.getAttribute('data-wt-id');
1009
- if (!id) {
1010
- id = String(idCounter++);
1011
- el.setAttribute('data-wt-id', id);
1012
- }
1013
- map.push({
1014
- id: id,
1015
- tag: el.tagName.toLowerCase(),
1016
- text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0,80),
1017
- role: el.getAttribute('role'),
1018
- selector: `[data-wt-id="${id}"]`,
1019
- coords: { x: rect.left + rect.width/2, y: rect.top + rect.height/2 },
1020
- inShadow: depth > 0
1021
- });
1022
- if (el.shadowRoot) crawl(el.shadowRoot, depth + 1);
1023
- }
1024
- }
1025
- crawl(document);
1026
- return map;
1027
- };
1028
-
1029
- // ── CONSOLE BUFFERING ───────────────────────────────────────────
1030
- const __consoleBuffer = [];
1031
- let __consoleCursor = 0;
1032
-
1033
- ['log','warn','error','info','debug'].forEach(level => {
1034
- const orig = console[level];
1035
- console[level] = (...args) => {
1036
- __consoleBuffer.push({ level, time: Date.now(), text: args.join(' ') });
1037
- orig.apply(console, args);
1038
- };
1039
- });
1040
-
1041
- window.__Weavetab_readConsole = function({ since = 'last', level = null } = {}) {
1042
- const from = since === 'last' ? __consoleCursor : 0;
1043
- const entries = __consoleBuffer.slice(from);
1044
- __consoleCursor = __consoleBuffer.length;
1045
- return level ? entries.filter(e => e.level === level) : entries;
1046
- };
1047
-
1048
- // ── BLUE SVG WEAVE CURSOR ───────────────────────────────────────
1049
- // Shadow-DOM-isolated, hardware-accelerated SVG cursor with smooth interpolation
1050
- class WeaveCursor {
1051
- constructor() {
1052
- // Attach a shadow host that sits above all page content
1053
- this._host = document.createElement('div');
1054
- this._host.id = '__Weavetab_cursor_host';
1055
- this._host.setAttribute('data-wt-ignore', 'true');
1056
- Object.assign(this._host.style, {
1057
- position: 'fixed',
1058
- top: '0',
1059
- left: '0',
1060
- width: '0',
1061
- height: '0',
1062
- zIndex: '2147483647',
1063
- pointerEvents: 'none',
1064
- overflow: 'visible',
1065
- });
1066
-
1067
- const shadow = this._host.attachShadow({ mode: 'closed' });
1068
-
1069
- // Liquid trail canvas inside shadow root (zero DOM reflows)
1070
- this._trailCanvas = document.createElement('canvas');
1071
- this._trailCanvas.setAttribute('data-wt-ignore', 'true');
1072
- Object.assign(this._trailCanvas.style, {
1073
- position: 'fixed',
1074
- top: '0',
1075
- left: '0',
1076
- width: '100vw',
1077
- height: '100vh',
1078
- pointerEvents: 'none',
1079
- zIndex: '2147483645',
1080
- overflow: 'visible',
1081
- });
1082
- this._trailCanvas.width = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1920;
1083
- this._trailCanvas.height = typeof window !== 'undefined' && window.innerHeight ? window.innerHeight : 1080;
1084
- shadow.appendChild(this._trailCanvas);
1085
- this._trailCtx = this._trailCanvas.getContext('2d');
1086
- this._trailPoints = [];
1087
- this._lastTrailRecord = 0;
1088
-
1089
- window.addEventListener('resize', () => {
1090
- if (this._trailCanvas) {
1091
- this._trailCanvas.width = window.innerWidth;
1092
- this._trailCanvas.height = window.innerHeight;
1093
- }
1094
- });
1095
-
1096
- // Hotspot aligned with the top-left pointer tip (3.0, 5.0 in 32x32 viewBox)
1097
- this._hotX = 3.0;
1098
- this._hotY = 5.0;
1099
- this._scale = 0.4;
1100
-
1101
- const initX = typeof window !== 'undefined' && window.innerWidth ? Math.round(window.innerWidth / 2) : 640;
1102
- const initY = typeof window !== 'undefined' && window.innerHeight ? Math.round(window.innerHeight / 2) : 400;
1103
-
1104
- // Exact vector SVG cursor matching user design
1105
- this._cursor = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1106
- this._cursor.setAttribute('class', 'cursor');
1107
- this._cursor.setAttribute('viewBox', '0 0 32 32');
1108
- this._cursor.setAttribute('width', '32');
1109
- this._cursor.setAttribute('height', '32');
1110
- this._cursor.setAttribute('aria-hidden', 'true');
1111
- Object.assign(this._cursor.style, {
1112
- position: 'fixed',
1113
- top: '0',
1114
- left: '0',
1115
- width: '32px',
1116
- height: '32px',
1117
- display: 'block',
1118
- transformOrigin: `${this._hotX}px ${this._hotY}px`,
1119
- transform: `translate3d(${initX - this._hotX}px, ${initY - this._hotY}px, 0) scale(${this._scale})`,
1120
- overflow: 'visible',
1121
- pointerEvents: 'none',
1122
- willChange: 'transform',
1123
- transition: 'opacity 0.2s ease-out',
1124
- opacity: '1',
1125
- zIndex: '2147483647',
1126
- filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.35))',
1127
- });
1128
-
1129
- const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1130
- path.setAttribute('class', 'cursor-shape');
1131
- path.setAttribute('d', 'M 6.1,4.7 C 4.5,4.1 3.0,5.0 3.0,6.7 C 3.0,7.1 3.1,7.5 3.3,8.0 L 10.2,27.0 C 10.8,28.7 12.0,29.6 13.6,29.5 C 15.1,29.4 16.0,28.5 16.5,27.0 L 19.4,18.8 L 25.9,17.0 C 27.7,16.5 28.9,15.2 28.9,13.6 C 28.9,12.1 28.0,10.9 26.4,10.3 Z');
1132
- path.setAttribute('fill', '#1268ff');
1133
- path.setAttribute('stroke', '#ffffff');
1134
- path.setAttribute('stroke-width', '3.1');
1135
- path.setAttribute('stroke-linejoin', 'round');
1136
- path.setAttribute('stroke-linecap', 'round');
1137
-
1138
- this._cursor.appendChild(path);
1139
- shadow.appendChild(this._cursor);
1140
-
1141
- // Alias _canvas to _cursor for backward compatibility
1142
- this._canvas = this._cursor;
1143
-
1144
- this._x = initX;
1145
- this._y = initY;
1146
- this._targetX = initX;
1147
- this._targetY = initY;
1148
- this._lerpX = initX;
1149
- this._lerpY = initY;
1150
- this._state = 'idle';
1151
- this._visible = true;
1152
- this._animFrame = null;
1153
- this._lastTime = performance.now();
1154
- this._waypointQueue = [];
1155
- this._playingQueue = false;
1156
-
1157
- __wt_safeAppendToBody(this._host);
1158
- this._startLoop();
1159
- }
1160
-
1161
- destroy() {
1162
- if (this._animFrame) {
1163
- cancelAnimationFrame(this._animFrame);
1164
- this._animFrame = null;
1165
- }
1166
- if (this._host && this._host.parentNode) {
1167
- this._host.parentNode.removeChild(this._host);
1168
- }
1169
- }
1170
-
1171
- setState(state) {
1172
- this._state = state || 'idle';
1173
- }
1174
-
1175
- setVisible(visible) {
1176
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) {
1177
- this._visible = false;
1178
- this._cursor.style.opacity = '0';
1179
- if (this._trailCtx && this._trailCanvas) {
1180
- this._trailCtx.clearRect(0, 0, this._trailCanvas.width, this._trailCanvas.height);
1181
- }
1182
- return;
1183
- }
1184
- this._visible = visible;
1185
- this._cursor.style.opacity = visible ? '1' : '0';
1186
- }
1187
-
1188
- moveTo(x, y) {
1189
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1190
- if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) return;
1191
- this._targetX = x;
1192
- this._targetY = y;
1193
- if (this._lerpX === -9999) {
1194
- this._lerpX = x;
1195
- this._lerpY = y;
1196
- }
1197
- if (!this._visible) {
1198
- this._visible = true;
1199
- this._cursor.style.opacity = '1';
1200
- }
1201
- if (!this._animFrame) {
1202
- this._startLoop();
1203
- }
1204
- }
1205
-
1206
- setWaypoints(waypoints, durationMs) {
1207
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1208
- if (!waypoints || !waypoints.length) return;
1209
- this._waypointQueue = waypoints;
1210
- this._playingQueue = true;
1211
- this._waypointStartTime = performance.now();
1212
- this._waypointDuration = durationMs || (waypoints.length * 16);
1213
- if (waypoints.length > 0 && this._lerpX === -9999) {
1214
- this._lerpX = waypoints[0].x;
1215
- this._lerpY = waypoints[0].y;
1216
- }
1217
- if (!this._visible) {
1218
- this._visible = true;
1219
- this._cursor.style.opacity = '1';
1220
- }
1221
- if (!this._animFrame) this._startLoop();
1222
- }
1223
-
1224
- _startLoop() {
1225
- this._lastTime = performance.now();
1226
- const draw = (currentTime) => {
1227
- const dt = Math.min((currentTime - this._lastTime) / 1000, 0.1);
1228
- this._lastTime = currentTime;
1229
-
1230
- if (this._playingQueue && this._waypointQueue.length > 0) {
1231
- const elapsed = currentTime - this._waypointStartTime;
1232
- const progress = Math.max(0, Math.min(elapsed / (this._waypointDuration || 1), 1));
1233
- const totalSegments = this._waypointQueue.length - 1;
1234
-
1235
- if (totalSegments <= 0) {
1236
- this._targetX = this._waypointQueue[0].x;
1237
- this._targetY = this._waypointQueue[0].y;
1238
- } else {
1239
- const floatIdx = progress * totalSegments;
1240
- const i0 = Math.floor(floatIdx);
1241
- const i1 = Math.min(i0 + 1, totalSegments);
1242
- const t = floatIdx - i0;
1243
-
1244
- const p0 = this._waypointQueue[i0];
1245
- const p1 = this._waypointQueue[i1];
1246
- if (p0 && p1) {
1247
- this._targetX = p0.x + (p1.x - p0.x) * t;
1248
- this._targetY = p0.y + (p1.y - p0.y) * t;
1249
- }
1250
- }
1251
-
1252
- if (progress >= 1) {
1253
- this._playingQueue = false;
1254
- this._waypointQueue = [];
1255
- }
1256
- }
1257
-
1258
- // Smooth exponential lerp (k = 24 for fluid human-smooth tracking)
1259
- const lerpFactor = 1 - Math.exp(-24 * dt);
1260
- if (this._targetX !== -9999) {
1261
- this._lerpX += (this._targetX - this._lerpX) * lerpFactor;
1262
- this._lerpY += (this._targetY - this._lerpY) * lerpFactor;
1263
- if (Math.abs(this._targetX - this._lerpX) < 0.1) this._lerpX = this._targetX;
1264
- if (Math.abs(this._targetY - this._lerpY) < 0.1) this._lerpY = this._targetY;
1265
-
1266
- // "always show in browser" - stays visible once loaded and active
1267
- if (typeof __wt_masterVisualsEnabled === 'undefined' || __wt_masterVisualsEnabled) {
1268
- if (!this._visible) {
1269
- this._visible = true;
1270
- this._cursor.style.opacity = '1';
1271
- }
1272
- } else {
1273
- if (this._visible) {
1274
- this._visible = false;
1275
- this._cursor.style.opacity = '0';
1276
- }
1277
- }
1278
-
1279
- this._render(currentTime);
1280
- }
1281
-
1282
- this._animFrame = requestAnimationFrame(draw);
1283
- };
1284
- if (this._animFrame) cancelAnimationFrame(this._animFrame);
1285
- this._animFrame = requestAnimationFrame(draw);
1286
- }
1287
-
1288
- _render(currentTime) {
1289
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) {
1290
- this._cursor.style.opacity = '0';
1291
- if (this._trailCtx && this._trailCanvas) {
1292
- this._trailCtx.clearRect(0, 0, this._trailCanvas.width, this._trailCanvas.height);
1293
- }
1294
- return;
1295
- }
1296
- const x = this._lerpX;
1297
- const y = this._lerpY;
1298
- if (x === -9999 || y === -9999) return;
1299
-
1300
- const now = currentTime || performance.now();
1301
-
1302
- // 1. Trail decay tracking
1303
- const dx = this._targetX - this._lerpX;
1304
- const dy = this._targetY - this._lerpY;
1305
- const moveDist = Math.sqrt(dx * dx + dy * dy);
1306
-
1307
- if (moveDist > 1.8 && now - this._lastTrailRecord > 22) {
1308
- this._trailPoints.push({ x, y, birth: now });
1309
- this._lastTrailRecord = now;
1310
- if (this._trailPoints.length > 14) this._trailPoints.shift();
1311
- }
1312
-
1313
- // Render decaying trail
1314
- if (this._trailCtx && this._trailCanvas) {
1315
- this._trailCtx.clearRect(0, 0, this._trailCanvas.width, this._trailCanvas.height);
1316
- for (let i = this._trailPoints.length - 1; i >= 0; i--) {
1317
- const pt = this._trailPoints[i];
1318
- const age = (now - pt.birth) / 260;
1319
- if (age >= 1) {
1320
- this._trailPoints.splice(i, 1);
1321
- continue;
1322
- }
1323
- const alpha = (1 - age) * 0.45;
1324
- const radius = Math.max(1, (1 - age) * 2.8);
1325
- this._trailCtx.beginPath();
1326
- this._trailCtx.arc(pt.x, pt.y, radius, 0, Math.PI * 2);
1327
- this._trailCtx.fillStyle = `rgba(18, 104, 255, ${alpha})`;
1328
- this._trailCtx.shadowColor = '#1268ff';
1329
- this._trailCtx.shadowBlur = 4;
1330
- this._trailCtx.fill();
1331
- }
1332
- }
1333
-
1334
- // 2. Cursor scale and tactile micro-physics
1335
- let targetScale = this._scale;
1336
- if (this._state === 'clicking') {
1337
- targetScale = this._scale * 0.82; // Tactile compression on click
1338
- } else if (this._state === 'hover') {
1339
- targetScale = this._scale * 1.08; // Interactive focus
1340
- } else if (this._state === 'idle') {
1341
- targetScale = this._scale + Math.sin(now * 0.003) * 0.012; // Alive breathing oscillation
1342
- }
1343
-
1344
- // 3. Dynamic velocity tilt (natural motion lag)
1345
- const tilt = Math.max(-5, Math.min(5, dx * 0.06));
1346
-
1347
- this._cursor.style.transform = `translate3d(${x - this._hotX}px, ${y - this._hotY}px, 0) rotate(${tilt}deg) scale(${targetScale})`;
1348
- }
1349
- }
1350
-
1351
- // Instantiate the active cursor
1352
- let __weaveCursor = new WeaveCursor();
1353
- window.__weaveCursor_instance = __weaveCursor;
1354
-
1355
- function moveCursor(x, y, state) {
1356
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1357
- if (__weaveCursor && typeof __weaveCursor.moveTo === 'function') {
1358
- if (typeof x === 'number' && typeof y === 'number' && x >= 0 && y >= 0) {
1359
- __weaveCursor.moveTo(x, y);
1360
- }
1361
- if (state) __weaveCursor.setState(state);
1362
- }
1363
- }
1364
-
1365
- // Enhanced twin action ripples radiating from click target
1366
- function ripple(x, y) {
1367
- if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1368
- const host = document.createElement('div');
1369
- host.className = 'Weavetab-ripple-host';
1370
- host.setAttribute('data-wt-ignore', 'true');
1371
- host.style.cssText = `position:fixed;left:${x}px;top:${y}px;pointer-events:none;z-index:2147483646;transform:translate(-50%, -50%);`;
1372
-
1373
- const w1 = document.createElement('div');
1374
- w1.style.cssText = 'position:absolute;top:50%;left:50%;width:8px;height:8px;margin:-4px 0 0 -4px;border:2px solid #1268ff;border-radius:50%;box-shadow:0 0 12px rgba(18,104,255,0.7);animation:__wt_ripple_expand 0.42s cubic-bezier(0.1, 0.8, 0.3, 1) forwards;pointer-events:none;';
1375
-
1376
- const w2 = document.createElement('div');
1377
- w2.style.cssText = 'position:absolute;top:50%;left:50%;width:8px;height:8px;margin:-4px 0 0 -4px;border:1.5px solid #60a5fa;border-radius:50%;animation:__wt_ripple_expand 0.52s 0.08s cubic-bezier(0.1, 0.8, 0.3, 1) forwards;pointer-events:none;';
1378
-
1379
- host.appendChild(w1);
1380
- host.appendChild(w2);
1381
- (document.getElementById('Weavetab-root')?.shadowRoot || document.body || document.documentElement).appendChild(host);
1382
- setTimeout(() => host.remove(), 620);
1383
- }
1384
-
1385
- // ── X-RAY SPOTLIGHT OVERLAY (Phase 5.2) ────────────────────────
1386
- // Flashes glowing halos around top interactive elements when the
1387
- // semantic map is scanned (GET_SEMANTIC_MAP message).
1388
- let __xrayActive = false;
1389
- const __xrayOverlays = [];
1390
-
1391
- function clearXRay() {
1392
- for (const el of __xrayOverlays) el.remove();
1393
- __xrayOverlays.length = 0;
1394
- __xrayActive = false;
1395
- }
1396
-
1397
- function activateXRay(elements) {
1398
- clearXRay();
1399
- __xrayActive = true;
1400
-
1401
- // Colour spectrum for visual richness
1402
- const colours = [
1403
- '#4F8EF7', '#7C3AED', '#10B981', '#F59E0B',
1404
- '#EF4444', '#06B6D4', '#F97316', '#8B5CF6',
1405
- ];
1406
-
1407
- // Architecture change: Use live index for visual overlay
1408
- // The live index maintains up-to-date coordinates and visibility info
1409
- const liveIndex = window.__wt_liveIndex || [];
1410
- const top = elements.slice(0, 12); // highlight top 12 nodes
1411
-
1412
- top.forEach((item, i) => {
1413
- // Try to get element from live index first for accurate coordinates
1414
- const liveEntry = liveIndex.find(e => e.id === item.id || e.selector === item.selector);
1415
-
1416
- let rect;
1417
- let el;
1418
-
1419
- if (liveEntry && liveEntry.rect) {
1420
- // Use coordinates from live index (more accurate, already computed)
1421
- rect = liveEntry.rect;
1422
- el = document.querySelector(item.selector) || shadowQuery(item.selector);
1423
- } else {
1424
- // Fallback to querying DOM
1425
- el = document.querySelector(item.selector) || shadowQuery(item.selector);
1426
- if (!el) return;
1427
- rect = el.getBoundingClientRect();
1428
- }
1429
-
1430
- if (rect.width === 0 || rect.height === 0) return;
1431
-
1432
- const colour = colours[i % colours.length];
1433
- const halo = document.createElement('div');
1434
- halo.className = '__Weavetab_xray';
1435
- halo.setAttribute('data-wt-ignore', '');
1436
-
1437
- Object.assign(halo.style, {
1438
- position: 'fixed',
1439
- left: `${rect.left - 4}px`,
1440
- top: `${rect.top - 4}px`,
1441
- width: `${rect.width + 8}px`,
1442
- height: `${rect.height + 8}px`,
1443
- border: `2px solid ${colour}`,
1444
- borderRadius: '6px',
1445
- boxShadow: `0 0 12px 4px ${colour}88, inset 0 0 8px ${colour}33`,
1446
- background: `${colour}0d`,
1447
- pointerEvents: 'none',
1448
- zIndex: '2147483645',
1449
- animation: '__wt_xray_pulse 1.4s ease-in-out forwards',
1450
- animationDelay: `${i * 55}ms`,
1451
- });
1452
-
1453
- // Rank label badge
1454
- const badge = document.createElement('span');
1455
- Object.assign(badge.style, {
1456
- position: 'absolute',
1457
- top: '-10px',
1458
- left: '4px',
1459
- background: colour,
1460
- color: '#fff',
1461
- fontSize: '9px',
1462
- fontFamily: 'system-ui, sans-serif',
1463
- fontWeight: '700',
1464
- padding: '1px 5px',
1465
- borderRadius: '3px',
1466
- lineHeight: '14px',
1467
- whiteSpace: 'nowrap',
1468
- letterSpacing: '0.03em',
1469
- boxShadow: `0 1px 4px ${colour}99`,
1470
- });
1471
- const label = (item.text || item.role || item.tag || '').slice(0, 22) || `#${i + 1}`;
1472
- badge.textContent = `${i + 1} ${label}`;
1473
- halo.appendChild(badge);
1474
-
1475
- getWeavetabRoot().appendChild(halo);
1476
- __xrayOverlays.push(halo);
1477
- });
1478
-
1479
- // Auto-clear after 2.8 s
1480
- setTimeout(clearXRay, 2800);
1481
- }
1482
-
1483
- // Inject keyframe animation for X-Ray
1484
- (function injectXRayStyles() {
1485
- if (document.getElementById('__Weavetab_xray_styles')) return;
1486
- const style = document.createElement('style');
1487
- style.id = '__Weavetab_xray_styles';
1488
- style.textContent = `
1489
- @keyframes __wt_xray_pulse {
1490
- 0% { opacity: 0; transform: scale(0.94); }
1491
- 18% { opacity: 1; transform: scale(1.02); }
1492
- 70% { opacity: 0.85; transform: scale(1); }
1493
- 100% { opacity: 0; transform: scale(0.97); }
1494
- }
1495
- .Weavetab-ripple {
1496
- width: 40px; height: 40px;
1497
- margin: -20px 0 0 -20px;
1498
- border-radius: 50%;
1499
- background: rgba(79,142,247,0.35);
1500
- border: 2px solid #4F8EF7;
1501
- animation: __wt_ripple 0.55s ease-out forwards;
1502
- }
1503
- @keyframes __wt_ripple {
1504
- from { transform: scale(0.3); opacity: 1; }
1505
- to { transform: scale(2.2); opacity: 0; }
1506
- }
1507
- @keyframes wt-reading-pulse {
1508
- 0% { opacity: 0; box-shadow: inset 0 0 0 rgba(59,130,246,0); }
1509
- 15% { opacity: 1; box-shadow: inset 0 0 60px rgba(59,130,246,0.15); }
1510
- 85% { opacity: 1; box-shadow: inset 0 0 60px rgba(59,130,246,0.15); }
1511
- 100% { opacity: 0; box-shadow: inset 0 0 0 rgba(59,130,246,0); }
1512
- }
1513
- `;
1514
- (document.head || document.documentElement).appendChild(style);
1515
- })();
1516
-
1517
- // ── READING GLOW ────────────────────────────────────────────────
1518
- let __wt_readingGlow = null;
1519
-
1520
- function __wt_showReadingGlow() {
1521
- if (__wt_readingGlow) return;
1522
- var el = document.createElement('div');
1523
- el.id = 'wt-reading-glow';
1524
- el.setAttribute('data-wt-ignore', '');
1525
- el.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483646;border:4px solid;border-image:linear-gradient(135deg, #3b82f6, #8b5cf6, #10b981) 1;animation:wt-reading-pulse 3.5s ease-in-out forwards;';
1526
- __wt_safeAppendToBody(el);
1527
- __wt_readingGlow = el;
1528
- setTimeout(__wt_hideReadingGlow, 3500);
1529
- }
1530
-
1531
- function __wt_hideReadingGlow() {
1532
- if (__wt_readingGlow) { __wt_readingGlow.remove(); __wt_readingGlow = null; }
1533
- }
1534
-
1535
- // ── SEMANTIC DOM MAP ────────────────────────────────────────────
1536
- // Architecture change: Passive/extension-side ID assignment with live index
1537
- // The MutationObserver continuously assigns data-wt-id to interactive elements
1538
- // and maintains a live index that CDP can read lazily
1539
-
1540
- // Live index - always up-to-date, accessible via window.__wt_liveIndex
1541
- window.__wt_liveIndex = [];
1542
- window.__wt_nextId = 1;
1543
-
1544
- // IntersectionObserver for real occlusion detection (not just display:none)
1545
- const intersectionObserver = new IntersectionObserver((entries) => {
1546
- for (const entry of entries) {
1547
- const el = entry.target;
1548
- const id = el.getAttribute('data-wt-id');
1549
- if (id) {
1550
- const indexEntry = window.__wt_liveIndex.find(e => e.id === id);
1551
- if (indexEntry) {
1552
- indexEntry.isVisible = entry.isIntersecting;
1553
- indexEntry.intersectionRatio = entry.intersectionRatio;
1554
- }
1555
- }
1556
- }
1557
- }, { threshold: [0, 0.25, 0.5, 0.75, 1.0] });
1558
-
1559
- // Function to assign data-wt-id to an element and add to live index
1560
- function assignIdAndIndex(el) {
1561
- if (el.hasAttribute('data-wt-ignore')) return;
1562
- if (el.hasAttribute('data-wt-id')) return; // Already assigned
1563
-
1564
- const id = String(window.__wt_nextId++);
1565
- el.setAttribute('data-wt-id', id);
1566
- el.__wt_id = id;
1567
-
1568
- const rect = el.getBoundingClientRect();
1569
- const inViewport = (
1570
- rect.bottom > 0 && rect.right > 0 &&
1571
- rect.top < window.innerHeight && rect.left < window.innerWidth
1572
- );
1573
- const viewportPos = inViewport ? "visible" : (rect.top >= window.innerHeight ? "below" : "above");
1574
-
1575
- // Check for event listeners
1576
- const hasClickListener = el.onclick !== null ||
1577
- el.addEventListener !== undefined &&
1578
- (el._listeners?.click?.length > 0);
1579
-
1580
- const indexEntry = {
1581
- id: id,
1582
- tag: el.tagName.toLowerCase(),
1583
- role: el.getAttribute('role') || el.tagName.toLowerCase(),
1584
- label: el.getAttribute('aria-label') || el.textContent?.trim().slice(0, 60) || '',
1585
- placeholder: el.getAttribute('placeholder') || '',
1586
- selector: getStableSelector(el),
1587
- x: Math.round(rect.left + rect.width / 2),
1588
- y: Math.round(rect.top + rect.height / 2),
1589
- viewport: viewportPos,
1590
- href: el.href || null,
1591
- isVisible: inViewport,
1592
- intersectionRatio: inViewport ? 1.0 : 0.0,
1593
- hasClickListener: hasClickListener,
1594
- rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
1595
- };
1596
-
1597
- window.__wt_liveIndex.push(indexEntry);
1598
-
1599
- // Observe for intersection changes
1600
- try {
1601
- intersectionObserver.observe(el);
1602
- } catch (e) {
1603
- // Element might not be observable
1604
- }
1605
- }
1606
-
1607
- // Function to update an existing entry in the live index
1608
- function updateIndexEntry(el) {
1609
- const id = el.getAttribute('data-wt-id');
1610
- if (!id) return;
1611
-
1612
- const indexEntry = window.__wt_liveIndex.find(e => e.id === id);
1613
- if (!indexEntry) return;
1614
-
1615
- const rect = el.getBoundingClientRect();
1616
- const inViewport = (
1617
- rect.bottom > 0 && rect.right > 0 &&
1618
- rect.top < window.innerHeight && rect.left < window.innerWidth
1619
- );
1620
- const viewportPos = inViewport ? "visible" : (rect.top >= window.innerHeight ? "below" : "above");
1621
-
1622
- indexEntry.x = Math.round(rect.left + rect.width / 2);
1623
- indexEntry.y = Math.round(rect.top + rect.height / 2);
1624
- indexEntry.viewport = viewportPos;
1625
- indexEntry.isVisible = inViewport;
1626
- indexEntry.rect = { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
1627
- indexEntry.label = el.getAttribute('aria-label') || el.textContent?.trim().slice(0, 60) || '';
1628
- }
1629
-
1630
- // Function to remove an entry from the live index
1631
- function removeIndexEntry(el) {
1632
- const id = el.getAttribute('data-wt-id');
1633
- if (!id) return;
1634
-
1635
- const index = window.__wt_liveIndex.findIndex(e => e.id === id);
1636
- if (index !== -1) {
1637
- window.__wt_liveIndex.splice(index, 1);
1638
- }
1639
-
1640
- try {
1641
- intersectionObserver.unobserve(el);
1642
- } catch (e) {
1643
- // Element might not be observable
1644
- }
1645
- }
1646
-
1647
- // MutationObserver for continuous ID assignment and index maintenance
1648
- const idAssignmentObserver = new MutationObserver((mutations) => {
1649
- for (const mutation of mutations) {
1650
- // Handle added nodes
1651
- for (const node of mutation.addedNodes) {
1652
- if (node.nodeType === Node.ELEMENT_NODE) {
1653
- // Check if the node itself is interactive
1654
- const interactiveSelector = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
1655
- if (node.matches && node.matches(interactiveSelector)) {
1656
- assignIdAndIndex(node);
1657
- }
1658
- // Check descendants
1659
- const descendants = node.querySelectorAll ? node.querySelectorAll(interactiveSelector) : [];
1660
- for (const desc of descendants) {
1661
- assignIdAndIndex(desc);
1662
- }
1663
- }
1664
- }
1665
-
1666
- // Handle removed nodes
1667
- for (const node of mutation.removedNodes) {
1668
- if (node.nodeType === Node.ELEMENT_NODE) {
1669
- if (node.getAttribute && node.getAttribute('data-wt-id')) {
1670
- removeIndexEntry(node);
1671
- }
1672
- const descendants = node.querySelectorAll ? node.querySelectorAll('[data-wt-id]') : [];
1673
- for (const desc of descendants) {
1674
- removeIndexEntry(desc);
1675
- }
1676
- }
1677
- }
1678
-
1679
- // Handle attribute changes (position, visibility, etc.)
1680
- if (mutation.type === 'attributes' && mutation.target.getAttribute) {
1681
- const target = mutation.target;
1682
- if (target.getAttribute('data-wt-id')) {
1683
- updateIndexEntry(target);
1684
- }
1685
- }
1686
- }
1687
- });
1688
-
1689
- // Start observing when DOM is ready
1690
- function startIdAssignment() {
1691
- const interactiveSelector = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
1692
-
1693
- // Assign IDs to existing interactive elements
1694
- const existing = document.querySelectorAll(interactiveSelector);
1695
- for (const el of existing) {
1696
- assignIdAndIndex(el);
1697
- }
1698
-
1699
- // Start observing for changes
1700
- if (document.body) {
1701
- idAssignmentObserver.observe(document.body, {
1702
- childList: true,
1703
- subtree: true,
1704
- attributes: true,
1705
- attributeFilter: ['style', 'class', 'hidden', 'aria-hidden']
1706
- });
1707
- } else {
1708
- document.addEventListener('DOMContentLoaded', () => {
1709
- const existing = document.querySelectorAll(interactiveSelector);
1710
- for (const el of existing) {
1711
- assignIdAndIndex(el);
1712
- }
1713
- idAssignmentObserver.observe(document.body, {
1714
- childList: true,
1715
- subtree: true,
1716
- attributes: true,
1717
- attributeFilter: ['style', 'class', 'hidden', 'aria-hidden']
1718
- });
1719
- });
1720
- }
1721
- }
1722
-
1723
- // Start the ID assignment system
1724
- if (document.readyState === 'loading') {
1725
- document.addEventListener('DOMContentLoaded', startIdAssignment);
1726
- } else {
1727
- startIdAssignment();
1728
- }
1729
-
1730
- // Legacy buildSemanticMap function for backward compatibility
1731
- // Now reads from the live index instead of rebuilding
1732
- let cachedMap = null;
1733
- const legacyObserver = new MutationObserver(() => {
1734
- cachedMap = null;
1735
- });
1736
- // FIX: document.body may be null at document_start; defer observe until body exists
1737
- if (document.body) {
1738
- legacyObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
1739
- } else {
1740
- document.addEventListener('DOMContentLoaded', function() {
1741
- if (document.body) legacyObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
1742
- }, { once: true });
1743
- }
1744
-
1745
- function buildSemanticMap() {
1746
- if (cachedMap) return cachedMap;
1747
-
1748
- // Read from the live index maintained by the MutationObserver
1749
- cachedMap = window.__wt_liveIndex.map(entry => ({
1750
- id: entry.id,
1751
- tag: entry.tag,
1752
- role: entry.role,
1753
- label: entry.label,
1754
- placeholder: entry.placeholder,
1755
- selector: entry.selector,
1756
- x: entry.x,
1757
- y: entry.y,
1758
- viewport: entry.viewport,
1759
- href: entry.href,
1760
- coords: { x: entry.x, y: entry.y }
1761
- }));
1762
-
1763
- return cachedMap;
1764
- }
1765
-
1766
- // ── HUD ─────────────────────────────────────────────────────────
1767
- const hudHistory = [];
1768
- const hud = (() => {
1769
- const el = document.createElement('div');
1770
- el.id = 'Weavetab-hud';
1771
- el.setAttribute('data-wt-ignore', '');
1772
- Object.assign(el.style, {
1773
- position: 'fixed', bottom: '24px', right: '24px',
1774
- transform: 'translateY(8px)',
1775
- color: '#a1a1aa',
1776
- font: '11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
1777
- borderRadius: '8px',
1778
- opacity: '0', pointerEvents: 'none',
1779
- zIndex: '2147483640',
1780
- maxWidth: '400px',
1781
- display: 'flex', flexDirection: 'column-reverse', gap: '4px',
1782
- transition: 'opacity 0.2s, transform 0.2s',
1783
- textAlign: 'right',
1784
- });
1785
- __wt_safeAppendToBody(el);
1786
- return el;
1787
- })();
1788
-
1789
- function updateHUD(action, detail) {
1790
- if (window.self !== window.top) return;
1791
- // Tool call entries display strictly inside the Sidepanel TOOL CALLS section.
1792
- // Suppress on-page bottom-right HUD popup overlay.
1793
- if (hud) {
1794
- hud.style.display = 'none';
1795
- hud.style.opacity = '0';
1796
- }
1797
- }
1798
-
1799
- // ── ACTIVE TAB INDICATOR ─────────────────────────────────────────
1800
- class ControlledTabIndicator {
1801
- constructor() {
1802
- this._el = document.createElement('div');
1803
- this._el.id = '__Weavetab_controlled_indicator';
1804
- this._el.setAttribute('data-wt-ignore', '');
1805
- Object.assign(this._el.style, {
1806
- position: 'fixed',
1807
- top: '0',
1808
- left: '0',
1809
- right: '0',
1810
- height: '3px',
1811
- zIndex: '2147483646',
1812
- background: '#3B82F6',
1813
- boxShadow: '0 0 12px #3B82F6, 0 0 4px #60a5fa',
1814
- opacity: '0',
1815
- transition: 'opacity 0.25s ease-in-out',
1816
- pointerEvents: 'none',
1817
- });
1818
- // Animated shimmer
1819
- this._shimmer = document.createElement('div');
1820
- Object.assign(this._shimmer.style, {
1821
- position: 'absolute',
1822
- top: '0', left: '-60%', width: '60%', height: '100%',
1823
- background: 'linear-gradient(90deg,transparent,rgba(255,255,255,0.45),transparent)',
1824
- animation: 'wt-shimmer 1.6s infinite',
1825
- });
1826
- // Inject animation keyframes into shadow root
1827
- var style = document.createElement('style');
1828
- style.textContent = '@keyframes wt-shimmer{0%{left:-60%}100%{left:120%}}';
1829
- try { getWeavetabRoot().appendChild(style); } catch(e) {}
1830
- this._el.appendChild(this._shimmer);
1831
- getWeavetabRoot().appendChild(this._el);
1832
- this._hideTimer = null;
1833
- this._persistent = false;
1834
- }
1835
-
1836
- // Show persistently (while agent is weaving)
1837
- showPersistent(color) {
1838
- this._persistent = true;
1839
- clearTimeout(this._hideTimer);
1840
- if (color) {
1841
- this._el.style.background = color;
1842
- this._el.style.boxShadow = '0 0 12px ' + color + ', 0 0 4px ' + color;
1843
- }
1844
- this._el.style.opacity = '1';
1845
- this._shimmer.style.display = 'block';
1846
- }
1847
-
1848
- // Show briefly (1.5s flash for single actions)
1849
- show(color) {
1850
- if (this._persistent) return; // Don't override persistent mode
1851
- if (color) this._el.style.background = color;
1852
- this._el.style.opacity = '1';
1853
- clearTimeout(this._hideTimer);
1854
- this._hideTimer = setTimeout(() => this.hide(), 1500);
1855
- }
1856
-
1857
- hide() {
1858
- this._persistent = false;
1859
- this._el.style.opacity = '0';
1860
- this._shimmer.style.display = 'none';
1861
- }
1862
-
1863
- destroy() {
1864
- clearTimeout(this._hideTimer);
1865
- this._el.remove();
1866
- }
1867
- }
1868
-
1869
- const controlledIndicator = new ControlledTabIndicator();
1870
-
1871
- // ── MESSAGE BRIDGE ──────────────────────────────────────────────
1872
- // Shared handler for both CustomEvent (MAIN world dispatch) and
1873
- // postMessage (cross-world: MAIN → ISOLATED) communication channels.
1874
- var __wt_lastCursor = { x: -9999, y: -9999 };
1875
- var __wt_processedMsgIds = new Set();
1876
-
1877
- function __wt_handleWeavetabMsg(msg) {
1878
- if (!msg) return;
1879
- if (Array.isArray(msg)) { msg.forEach(m => __wt_handleWeavetabMsg(m)); return; }
1880
- if (msg._id) { if (__wt_processedMsgIds.has(msg._id)) return; __wt_processedMsgIds.add(msg._id); if (__wt_processedMsgIds.size > 100) __wt_processedMsgIds.clear(); }
1881
-
1882
- if (msg.type === 'Weavetab_STATE') {
1883
- if (msg.cursor) {
1884
- __wt_lastCursor = { x: msg.cursor.x, y: msg.cursor.y };
1885
- if (msg.cursor.waypoints && msg.cursor.duration_ms) {
1886
- __weaveCursor.setWaypoints(msg.cursor.waypoints, msg.cursor.duration_ms);
1887
- }
1888
- moveCursor(msg.cursor.x, msg.cursor.y, msg.state);
1889
- }
1890
- else if (msg.state) { moveCursor(__wt_lastCursor.x, __wt_lastCursor.y, msg.state); }
1891
- if (msg.ripple) ripple(msg.ripple.x, msg.ripple.y);
1892
-
1893
- if (msg.userFeedback === true && msg.feedbackText) {
1894
- __wt_feedbackSticky = true;
1895
- clearTimeout(window.__wt_feedbackStickyTimer);
1896
- window.__wt_feedbackStickyTimer = setTimeout(function(){ __wt_feedbackSticky = false; }, 6000);
1897
- } else if (msg.userFeedback === false) {
1898
- setTimeout(function(){ __wt_feedbackSticky = false; }, 2000);
1899
- }
1900
-
1901
- if (msg.hud) {
1902
- updateHUD(msg.hud.action, msg.hud.detail);
1903
- var a = (msg.hud.action || '').trim();
1904
- if (a === 'reading the page...') {
1905
- __weaveCursor.setState('reading');
1906
- } else if (a === 'Mission Running') {
1907
- __wt_toolActive = true;
1908
- __weaveCursor.setState('eval');
1909
- } else if (a.startsWith('[OK]')) {
1910
- __weaveCursor.setState('idle');
1911
- __wt_toolActive = false;
1912
- } else if (a.startsWith('[ERR]')) {
1913
- __weaveCursor.setState('stuck');
1914
- __wt_toolActive = false;
1915
- setTimeout(function(){ __weaveCursor.setState('idle'); }, 3000);
1916
- }
1917
- }
1918
- if (msg.cursor || msg.hud) {
1919
- controlledIndicator.show('#3B82F6');
1920
- }
1921
- }
1922
-
1923
- // Pure thought event from browser_thoughts tool only
1924
- if (msg.type === 'WT_THOUGHT' || msg.type === 'Weavetab_THOUGHT') {
1925
- __wt_showThought(msg.text, msg);
1926
- }
1927
- if (msg.type === 'WT_CLEAR_THOUGHT' || msg.type === 'Weavetab_CLEAR_THOUGHT') {
1928
- __wt_removeThought();
1929
- }
1930
-
1931
- // Handle plan updates via sendToExtension channel
1932
- if (msg.type === 'Weavetab_STATE' && msg.plan) {
1933
- const p = msg.plan;
1934
- if (p.action === 'hide') {
1935
- if (window.__wt_plan) window.__wt_plan('hide', '', []);
1936
- } else {
1937
- if (window.__wt_plan) window.__wt_plan(p.action || 'create', p.goal || '', p.tasks || []);
1938
- }
1939
- }
1940
-
1941
-
1942
- if (msg.type === 'WT_USER_NOTE_INJECT') {
1943
- window.__wt_user_notes = window.__wt_user_notes || [];
1944
- window.__wt_user_notes.push({ text: msg.text, time: Date.now() });
1945
- }
1946
-
1947
- if (msg.type === 'GET_SEMANTIC_MAP') {
1948
- const map = window.__Weavetab_fullMap();
1949
- if (window.__wt_user_notes && window.__wt_user_notes.length > 0) {
1950
- // Keep notes for 2 minutes
1951
- window.__wt_user_notes = window.__wt_user_notes.filter(n => Date.now() - n.time < 120000);
1952
- const notesMap = window.__wt_user_notes.map((note, i) => ({
1953
- id: `user-msg-${i}`,
1954
- role: 'alert',
1955
- tag: 'div',
1956
- text: `URGENT MESSAGE FROM USER: "${note.text}"`,
1957
- coords: [0, 0, 100, 100],
1958
- interactive: false
1959
- }));
1960
- map.unshift(...notesMap);
1961
- }
1962
- activateXRay(map.filter(m => m.coords && m.text).slice(0, 12));
1963
- window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: map } }));
1964
- }
1965
-
1966
- if (msg.type === 'RESOLVE_EXTENSION_URL') {
1967
- var url = '';
1968
- try { url = browserAPI.runtime.getURL((msg.page || 'dashboard') + '.html'); } catch(e) {}
1969
- window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: url } }));
1970
- }
1971
-
1972
- if (msg.type === 'Weavetab_CONFIG') {
1973
- window.__wt_devMode = !!msg.devMode;
1974
- window.dispatchEvent(new CustomEvent('wt-config-sync'));
1975
- __wt_safeSendMessage(msg);
1976
- }
1977
-
1978
- if (msg.type === 'Weavetab_GROUP_STATE') {
1979
- __wt_safeSendMessage(msg);
1980
- // Only ever blue dot — no yellow. Idle state = no dot.
1981
- const dotColor = (msg.state === 'idle') ? null : '#3B82F6';
1982
- if (dotColor) {
1983
- controlledIndicator.show(dotColor);
1984
- }
1985
- updateFaviconDot(msg.state, msg.isActive);
1986
- // Parked tabs: clear thought bubble and reading glow
1987
- if (msg.isActive === false) {
1988
- __wt_removeThought();
1989
- __wt_hideReadingGlow();
1990
- }
1991
- if (msg.state === 'idle') {
1992
- __wt_hideReadingGlow();
1993
- __wt_destroyBubble();
1994
- __wt_feedbackSticky = false;
1995
- __wt_processThoughtQueue();
1996
- restoreOriginalFavicon();
1997
- // Hide the persistent indicator bar when idle
1998
- controlledIndicator.hide();
1999
- __weaveCursor.setState('idle');
2000
- } else if (msg.state === 'weaving') {
2001
- // Show persistent blue bar while agent works
2002
- controlledIndicator.showPersistent('#3B82F6');
2003
- __weaveCursor.setState('eval');
2004
- } else if (msg.state === 'stuck') {
2005
- // Still show blue bar (not red) when stuck
2006
- controlledIndicator.showPersistent('#3B82F6');
2007
- __weaveCursor.setState('stuck');
2008
- }
2009
- }
2010
-
2011
-
2012
- if (msg.type === 'Weavetab_THOUGHT') {
2013
- __wt_showThought(msg.text, { active: msg.active });
2014
- }
2015
-
2016
- if (msg.type === 'Weavetab_ASK') {
2017
- // Legacy support — route to new unified question system
2018
- console.log('[Weavetab] Received ASK request (legacy):', msg.question);
2019
- __wt_showQuestion(msg.question, 'sentence', msg.timeout || 120);
2020
- window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: { status: 'shown' } } }));
2021
- }
2022
-
2023
- if (msg.type === 'Weavetab_CLEAR_THOUGHT') {
2024
- __wt_removeThought();
2025
- __wt_hideReadingGlow();
2026
- __wt_processThoughtQueue();
2027
- }
2028
-
2029
- if (msg.type === 'Weavetab_RELOAD_EXTENSION') {
2030
- __wt_safeSendMessage({ type: 'RELOAD_EXTENSION' });
2031
- }
2032
- }
2033
-
2034
- // Channel 1: CustomEvent from MAIN world (used by fallback + sendToExtension)
2035
- window.addEventListener('Weavetab-msg-in', (e) => {
2036
- __wt_handleWeavetabMsg(e.detail);
2037
- });
2038
-
2039
- // Channel 2: postMessage from MAIN world (cross-world MV3 bridge)
2040
- window.addEventListener('message', (event) => {
2041
- if (event.data && event.data.type === 'Weavetab-msg-in' && event.data.data) {
2042
- __wt_handleWeavetabMsg(event.data.data);
2043
- }
2044
- // ── User feedback relay: WeaveTab OS panel → background storage queue ────
2045
- // When the user types in the chat panel and hits Send, submitFeedback() fires
2046
- // a __WT_FEEDBACK postMessage. We relay it to the background so it lands in
2047
- // chrome.storage.local and window.__Weavetab.popUserFeedback() can return it.
2048
- if (event.data && event.data.type === '__WT_FEEDBACK' && event.data.text) {
2049
- try {
2050
- if (browserAPI.runtime && browserAPI.runtime.id) {
2051
- browserAPI.runtime.sendMessage({ type: 'USER_FEEDBACK', text: event.data.text }).catch(function(){});
2052
- }
2053
- } catch(e) {}
2054
- }
2055
- });
2056
-
2057
- // ── FAVICON STATUS DOT INDICATOR ───────────────────────────────
2058
- let wtFaviconObserver = null;
2059
-
2060
- let __wt_faviconStaleTimer = null;
2061
-
2062
- function updateFaviconDot(state, isActive) {
2063
- if (__wt_faviconStaleTimer) { clearTimeout(__wt_faviconStaleTimer); __wt_faviconStaleTimer = null; }
2064
-
2065
- if (state === 'idle' || !state || isActive === false) {
2066
- if (wtFaviconObserver) {
2067
- wtFaviconObserver.disconnect();
2068
- wtFaviconObserver = null;
2069
- }
2070
- restoreOriginalFavicon();
2071
- return;
2072
- }
2073
-
2074
- // Always use blue — never red or yellow for favicon dot
2075
- const color = '#3B82F6';
2076
-
2077
- if (!wtFaviconObserver) {
2078
- wtFaviconObserver = new MutationObserver(() => {
2079
- wtFaviconObserver.disconnect();
2080
- applyFaviconDot(color);
2081
- wtFaviconObserver.observe(document.head, { childList: true, subtree: true });
2082
- });
2083
- } else {
2084
- wtFaviconObserver.disconnect();
2085
- }
2086
-
2087
- applyFaviconDot(color);
2088
- wtFaviconObserver.observe(document.head, { childList: true, subtree: true });
2089
-
2090
- // Set stale timer: clear dot + hide glow if no message for 30s
2091
- __wt_faviconStaleTimer = setTimeout(function() {
2092
- restoreOriginalFavicon();
2093
- __wt_hideReadingGlow();
2094
- __wt_destroyBubble();
2095
- }, 30000);
2096
- }
2097
-
2098
- function restoreOriginalFavicon() {
2099
- const customLink = document.getElementById('wt-custom-favicon');
2100
- if (customLink) customLink.remove();
2101
-
2102
- const links = document.querySelectorAll('link[rel*="icon"]');
2103
- links.forEach(link => {
2104
- if (link.dataset.wtOrigHref) {
2105
- link.href = link.dataset.wtOrigHref;
2106
- delete link.dataset.wtOrigHref;
2107
- }
2108
- });
2109
- }
2110
-
2111
- function applyFaviconDot(color) {
2112
- const links = Array.from(document.querySelectorAll('link[rel*="icon"]')).filter(l => l.id !== 'wt-custom-favicon');
2113
- let originalHref = '';
2114
-
2115
- if (links.length > 0) {
2116
- const link = links[0];
2117
- if (!link.dataset.wtOrigHref) {
2118
- link.dataset.wtOrigHref = link.href;
2119
- }
2120
- originalHref = link.dataset.wtOrigHref;
2121
- } else {
2122
- originalHref = '/favicon.ico';
2123
- }
2124
-
2125
- const canvas = document.createElement('canvas');
2126
- canvas.width = 32;
2127
- canvas.height = 32;
2128
- const ctx = canvas.getContext('2d');
2129
-
2130
- const img = new Image();
2131
- img.crossOrigin = 'anonymous';
2132
- img.onload = () => {
2133
- try {
2134
- ctx.drawImage(img, 0, 0, 32, 32);
2135
- drawDot(ctx, color);
2136
- setFaviconHref(canvas.toDataURL('image/png'));
2137
- } catch (e) {
2138
- drawFallbackIcon(ctx, color);
2139
- setFaviconHref(canvas.toDataURL('image/png'));
2140
- }
2141
- };
2142
- img.onerror = () => {
2143
- drawFallbackIcon(ctx, color);
2144
- setFaviconHref(canvas.toDataURL('image/png'));
2145
- };
2146
- img.src = originalHref;
2147
- }
2148
-
2149
- function drawDot(ctx, color) {
2150
- // Solid dot
2151
- ctx.beginPath();
2152
- ctx.arc(24, 24, 5.5, 0, 2 * Math.PI);
2153
- ctx.fillStyle = color;
2154
- ctx.fill();
2155
- }
2156
-
2157
- function drawFallbackIcon(ctx, color) {
2158
- ctx.clearRect(0, 0, 32, 32);
2159
- ctx.beginPath();
2160
- ctx.arc(16, 16, 11, 0, 2 * Math.PI);
2161
- ctx.fillStyle = '#1E1F22';
2162
- ctx.fill();
2163
- ctx.strokeStyle = '#3C3F45';
2164
- ctx.lineWidth = 1.5;
2165
- ctx.stroke();
2166
-
2167
- ctx.beginPath();
2168
- ctx.arc(16, 16, 6, 0, 2 * Math.PI);
2169
- ctx.fillStyle = '#7C3AED';
2170
- ctx.fill();
2171
-
2172
- drawDot(ctx, color);
2173
- }
2174
-
2175
- function setFaviconHref(dataUrl) {
2176
- let customLink = document.getElementById('wt-custom-favicon');
2177
- if (!customLink) {
2178
- customLink = document.createElement('link');
2179
- customLink.id = 'wt-custom-favicon';
2180
- customLink.rel = 'icon';
2181
- customLink.type = 'image/png';
2182
- (document.head || document.documentElement).appendChild(customLink);
2183
- }
2184
- customLink.href = dataUrl;
2185
-
2186
- const links = document.querySelectorAll('link[rel*="icon"]');
2187
- links.forEach(link => {
2188
- if (link.id !== 'wt-custom-favicon') {
2189
- if (!link.dataset.wtOrigHref) {
2190
- link.dataset.wtOrigHref = link.href;
2191
- }
2192
- link.href = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
2193
- }
2194
- });
2195
- }
2196
-
2197
-
2198
-
2199
- // ── SIDE PANEL REMOVED (Replaced by pure middle-down Thought HUD) ──
2200
-
2201
- (function initAreaSnipeEngine() {
2202
- let isSnipeActive = false;
2203
- let currentSnipeMode = 'dom_target';
2204
- let overlayEl = null;
2205
- let marqueeBox = null;
2206
- let sizeBadge = null;
2207
- let headerPill = null;
2208
- let startX = 0, startY = 0;
2209
- let isDragging = false;
2210
-
2211
- function createOverlay(mode) {
2212
- if (overlayEl) overlayEl.remove();
2213
- currentSnipeMode = mode || 'dom_target';
2214
-
2215
- overlayEl = document.createElement('div');
2216
- overlayEl.id = 'wt-snipe-overlay';
2217
- overlayEl.setAttribute('data-wt-ignore', 'true');
2218
- overlayEl.style.cssText = `
2219
- position: fixed; inset: 0; z-index: 2147483647;
2220
- background: rgba(0, 0, 0, 0.35); backdrop-filter: blur(2px);
2221
- cursor: crosshair; user-select: none;
2222
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2223
- `;
2224
-
2225
- headerPill = document.createElement('div');
2226
- headerPill.style.cssText = `
2227
- position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
2228
- background: #18181b; border: 1px solid #3f3f46; color: #f4f4f5;
2229
- font-size: 11px; font-weight: 500; padding: 6px 16px; border-radius: 20px;
2230
- box-shadow: 0 4px 20px rgba(0,0,0,0.6); pointer-events: none;
2231
- display: flex; align-items: center; gap: 8px; z-index: 2147483647;
2232
- `;
2233
- const modeLabel = currentSnipeMode === 'visual_crop' ? 'Vision Crop' : 'DOM Target';
2234
- headerPill.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#38bdf8" stroke-width="2"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><rect x="7" y="7" width="10" height="10" rx="1"/></svg>Drag box to select area</span> <span style="color:#06b6d4;font-weight:600;">[${modeLabel}]</span> <span style="color:#71717a;">(Esc to cancel)</span>`;
2235
-
2236
- // Marquee selection box matching img1 Gemini/YouTube Chrome UI style
2237
- marqueeBox = document.createElement('div');
2238
- marqueeBox.style.cssText = `
2239
- position: fixed; border: 1px solid rgba(255, 255, 255, 0.45);
2240
- border-radius: 12px;
2241
- box-shadow: 0 0 24px 3px rgba(147, 197, 253, 0.45), inset 0 0 20px rgba(147, 197, 253, 0.2);
2242
- pointer-events: auto; display: none; z-index: 2147483647;
2243
- background: rgba(0, 0, 0, 0.1);
2244
- `;
2245
-
2246
- // Four thick white rounded corner reticles (matching img1)
2247
- const corners = [
2248
- 'top: -2px; left: -2px; border-top: 4px solid #ffffff; border-left: 4px solid #ffffff; border-top-left-radius: 10px;',
2249
- 'top: -2px; right: -2px; border-top: 4px solid #ffffff; border-right: 4px solid #ffffff; border-top-right-radius: 10px;',
2250
- 'bottom: -2px; left: -2px; border-bottom: 4px solid #ffffff; border-left: 4px solid #ffffff; border-bottom-left-radius: 10px;',
2251
- 'bottom: -2px; right: -2px; border-bottom: 4px solid #ffffff; border-right: 4px solid #ffffff; border-bottom-right-radius: 10px;'
2252
- ];
2253
- corners.forEach(styleCss => {
2254
- const c = document.createElement('div');
2255
- c.style.cssText = `position: absolute; width: 18px; height: 18px; pointer-events: none; ${styleCss}`;
2256
- marqueeBox.appendChild(c);
2257
- });
2258
-
2259
- // Circular close X button on top-right corner
2260
- const closeBtn = document.createElement('button');
2261
- closeBtn.style.cssText = `
2262
- position: absolute; top: -11px; right: -11px;
2263
- width: 22px; height: 22px; border-radius: 50%;
2264
- background: rgba(24, 24, 27, 0.95); border: 1px solid rgba(255, 255, 255, 0.25);
2265
- color: #f4f4f5; cursor: pointer; display: flex; align-items: center; justify-content: center;
2266
- box-shadow: 0 4px 12px rgba(0,0,0,0.6); z-index: 2147483647; transition: transform 0.15s, background 0.15s;
2267
- `;
2268
- closeBtn.innerHTML = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
2269
- closeBtn.onclick = function(e) {
2270
- e.stopPropagation();
2271
- destroyOverlay();
2272
- };
2273
- marqueeBox.appendChild(closeBtn);
2274
-
2275
- sizeBadge = document.createElement('div');
2276
- sizeBadge.style.cssText = `
2277
- position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%);
2278
- background: rgba(15, 23, 42, 0.9); backdrop-filter: blur(8px); color: #e2e8f0; font-weight: 600;
2279
- font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 2px 8px; border-radius: 12px;
2280
- border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none;
2281
- white-space: nowrap;
2282
- `;
2283
- marqueeBox.appendChild(sizeBadge);
2284
-
2285
- overlayEl.appendChild(headerPill);
2286
- overlayEl.appendChild(marqueeBox);
2287
- document.documentElement.appendChild(overlayEl);
2288
-
2289
- isSnipeActive = true;
2290
- }
2291
-
2292
- function destroyOverlay() {
2293
- if (overlayEl) {
2294
- overlayEl.remove();
2295
- overlayEl = null;
2296
- }
2297
- isSnipeActive = false;
2298
- isDragging = false;
2299
- }
2300
-
2301
- function handleMouseDown(e) {
2302
- if (!isSnipeActive || e.button !== 0) return;
2303
- e.preventDefault();
2304
- e.stopPropagation();
2305
-
2306
- isDragging = true;
2307
- startX = e.clientX;
2308
- startY = e.clientY;
2309
-
2310
- marqueeBox.style.left = startX + 'px';
2311
- marqueeBox.style.top = startY + 'px';
2312
- marqueeBox.style.width = '0px';
2313
- marqueeBox.style.height = '0px';
2314
- marqueeBox.style.display = 'block';
2315
- }
2316
-
2317
- function handleMouseMove(e) {
2318
- if (!isSnipeActive || !isDragging) return;
2319
- e.preventDefault();
2320
- e.stopPropagation();
2321
-
2322
- const currentX = e.clientX;
2323
- const currentY = e.clientY;
2324
-
2325
- const left = Math.min(startX, currentX);
2326
- const top = Math.min(startY, currentY);
2327
- const width = Math.abs(currentX - startX);
2328
- const height = Math.abs(currentY - startY);
2329
-
2330
- marqueeBox.style.left = left + 'px';
2331
- marqueeBox.style.top = top + 'px';
2332
- marqueeBox.style.width = width + 'px';
2333
- marqueeBox.style.height = height + 'px';
2334
-
2335
- sizeBadge.textContent = `${width} × ${height} px`;
2336
- }
2337
-
2338
- function handleMouseUp(e) {
2339
- if (!isSnipeActive || !isDragging) return;
2340
- e.preventDefault();
2341
- e.stopPropagation();
2342
-
2343
- isDragging = false;
2344
- const currentX = e.clientX;
2345
- const currentY = e.clientY;
2346
-
2347
- const left = Math.min(startX, currentX);
2348
- const top = Math.min(startY, currentY);
2349
- const width = Math.abs(currentX - startX);
2350
- const height = Math.abs(currentY - startY);
2351
-
2352
- if (width < 10 || height < 10) {
2353
- destroyOverlay();
2354
- return;
2355
- }
2356
-
2357
- if (currentSnipeMode === 'visual_crop') {
2358
- captureVisualCrop(left, top, width, height);
2359
- } else {
2360
- captureDOMTargets(left, top, width, height);
2361
- }
2362
-
2363
- destroyOverlay();
2364
- }
2365
-
2366
- function handleKeyDown(e) {
2367
- if (e.key === 'Escape' && isSnipeActive) {
2368
- destroyOverlay();
2369
- }
2370
- if (e.altKey && (e.key === 's' || e.key === 'S')) {
2371
- e.preventDefault();
2372
- createOverlay('dom_target');
2373
- }
2374
- }
2375
-
2376
- window.addEventListener('mousedown', handleMouseDown, true);
2377
- window.addEventListener('mousemove', handleMouseMove, true);
2378
- window.addEventListener('mouseup', handleMouseUp, true);
2379
- window.addEventListener('keydown', handleKeyDown, true);
2380
-
2381
- // ── Mode 1: DOM Element Intersector ──────────────────────────────────────
2382
- function captureDOMTargets(left, top, width, height) {
2383
- const right = left + width;
2384
- const bottom = top + height;
2385
- const elements = [];
2386
-
2387
- const allEls = document.querySelectorAll('button, input, select, textarea, a, h1, h2, h3, h4, h5, h6, p, table, tr, td, th, div, span, [role]');
2388
- allEls.forEach((el) => {
2389
- if (el.hasAttribute('data-wt-ignore') || el.closest('#wt-snipe-overlay')) return;
2390
- const r = el.getBoundingClientRect();
2391
- if (r.width === 0 || r.height === 0) return;
2392
-
2393
- const overlaps = !(r.right < left || r.left > right || r.bottom < top || r.top > bottom);
2394
- if (overlaps) {
2395
- const text = (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0, 100);
2396
- if (text || ['INPUT', 'BUTTON', 'A', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
2397
- elements.push({
2398
- tag: el.tagName.toLowerCase(),
2399
- selector: getStableSelector(el),
2400
- ref: el.getAttribute('data-wt-id') || null,
2401
- text: text,
2402
- role: el.getAttribute('role') || null,
2403
- rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }
2404
- });
2405
- }
2406
- }
2407
- });
2408
-
2409
- const uniqueElements = elements.slice(0, 15);
2410
-
2411
- __wt_safeSendMessage({
2412
- type: 'WT_SNIPE_COMPLETED',
2413
- payload: {
2414
- mode: 'dom_target',
2415
- elements: uniqueElements,
2416
- boundingRect: { x: left, y: top, width, height }
2417
- }
2418
- });
2419
- }
2420
-
2421
- // ── Mode 2: Visual WebP Crop ─────────────────────────────────────────────
2422
- function captureVisualCrop(left, top, width, height) {
2423
- try {
2424
- const canvas = document.createElement('canvas');
2425
- canvas.width = width;
2426
- canvas.height = height;
2427
- const ctx = canvas.getContext('2d');
2428
-
2429
- const htmlContent = document.documentElement.outerHTML;
2430
- const dataSvg = `
2431
- <svg xmlns="http://www.w3.org/2000/svg" width="${window.innerWidth}" height="${window.innerHeight}">
2432
- <foreignObject width="100%" height="100%">
2433
- <div xmlns="http://www.w3.org/1999/xhtml">
2434
- ${htmlContent}
2435
- </div>
2436
- </foreignObject>
2437
- </svg>
2438
- `;
2439
-
2440
- const img = new Image();
2441
- const blob = new Blob([dataSvg], { type: 'image/svg+xml;charset=utf-8' });
2442
- const url = URL.createObjectURL(blob);
2443
-
2444
- img.onload = function() {
2445
- ctx.drawImage(img, left, top, width, height, 0, 0, width, height);
2446
- URL.revokeObjectURL(url);
2447
- const webpBase64 = canvas.toDataURL('image/webp', 0.75);
2448
-
2449
- __wt_safeSendMessage({
2450
- type: 'WT_SNIPE_COMPLETED',
2451
- payload: {
2452
- mode: 'visual_crop',
2453
- cropImage: webpBase64,
2454
- boundingRect: { x: left, y: top, width, height }
2455
- }
2456
- });
2457
- };
2458
- img.onerror = function() {
2459
- captureDOMTargets(left, top, width, height);
2460
- };
2461
- img.src = url;
2462
- } catch(e) {
2463
- captureDOMTargets(left, top, width, height);
2464
- }
2465
- }
2466
-
2467
- // Listen for snipe trigger from background
2468
- browserAPI.runtime.onMessage.addListener((msg) => {
2469
- if (msg.type === 'WT_START_SNIPE') {
2470
- createOverlay(msg.mode || 'dom_target');
2471
- }
2472
- if (msg.type === 'WT_SNIPE_INJECT' && msg.entry) {
2473
- const noteStr = typeof msg.entry === 'string' ? msg.entry : JSON.stringify(msg.entry);
2474
- try {
2475
- if (!window.__wt_chat_queue) window.__wt_chat_queue = [];
2476
- window.__wt_chat_queue.push(noteStr);
2477
- } catch(e) {}
2478
-
2479
- try {
2480
- const existing = JSON.parse(localStorage.getItem('__wt_chat_queue_persist') || '[]');
2481
- existing.push(msg.entry);
2482
- localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(existing));
2483
- } catch(e) {}
2484
-
2485
- const bridge = document.getElementById('__wt_ask_bridge');
2486
- if (bridge) {
2487
- bridge.dataset.status = 'submitted';
2488
- bridge.dataset.result = msg.entry.text || 'Annotation submitted';
2489
- }
2490
- }
2491
- });
2492
-
2493
- })();
2494
-
2495
- // ── ALERT / ACTION-REQUIRED TAB INDICATOR ─────────────────────
2496
- (function initAlertDetector() {
2497
- if (window.self !== window.top) return;
2498
- // Expose on window so window.__Weavetab.getAlertState() can read it from CDP MAIN world
2499
- window.__wt_alertPending = false;
2500
- var _alert = window.alert;
2501
- window.alert = function(msg) {
2502
- window.__wt_alertPending = true;
2503
- __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
2504
- var r = _alert.apply(this, arguments);
2505
- window.__wt_alertPending = false;
2506
- __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
2507
- return r;
2508
- };
2509
- var _confirm = window.confirm;
2510
- window.confirm = function(msg) {
2511
- window.__wt_alertPending = true;
2512
- __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
2513
- var r = _confirm.apply(this, arguments);
2514
- window.__wt_alertPending = false;
2515
- __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
2516
- return r;
2517
- };
2518
- var _prompt = window.prompt;
2519
- window.prompt = function(msg, def) {
2520
- window.__wt_alertPending = true;
2521
- __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
2522
- var r = _prompt.apply(this, arguments);
2523
- window.__wt_alertPending = false;
2524
- __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
2525
- return r;
2526
- };
2527
- })();
1
+ const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
2
+ // Register this tab with background (skip about:blank and chrome:// pages)
3
+ if (location.href !== 'about:blank' && !location.protocol.startsWith('chrome')) {
4
+ try { browserAPI.runtime.sendMessage({ type: 'REGISTER_TAB' }).catch(function(){}); } catch(e) {}
5
+ }
6
+
7
+ // ── STABLE EXTENSION MARKER ────────────────────────────────────────
8
+ // Unconditionally injected at document root so MCP server can detect the extension reliably.
9
+ if (!document.getElementById('wt-extension-marker')) {
10
+ const marker = document.createElement('meta');
11
+ marker.id = 'wt-extension-marker';
12
+ marker.setAttribute('name', 'weavetab-extension');
13
+ if (document.head) {
14
+ document.head.appendChild(marker);
15
+ } else if (document.documentElement) {
16
+ document.documentElement.appendChild(marker);
17
+ }
18
+ }
19
+
20
+ // ── SAFE MESSAGE BRIDGE ────────────────────────────────────────
21
+ // Wraps browserAPI.runtime.sendMessage to survive "Extension context invalidated"
22
+ // errors that occur when the extension is reloaded/updated mid-session.
23
+ var __wt_extAlive = true;
24
+ function __wt_safeSendMessage(msg) {
25
+ try {
26
+ if (!browserAPI.runtime || !browserAPI.runtime.id) { __wt_extAlive = false; return; }
27
+ browserAPI.runtime.sendMessage(msg).catch(function(){ __wt_extAlive = false; });
28
+ } catch(e) {
29
+ __wt_extAlive = false;
30
+ }
31
+ }
32
+
33
+ function __wt_safeAppendToBody(el) {
34
+ if (document.body) {
35
+ document.body.appendChild(el);
36
+ } else {
37
+ document.addEventListener('DOMContentLoaded', function() {
38
+ if (document.body) document.body.appendChild(el);
39
+ }, { once: true });
40
+ // Backup for SPAs that might fire DOMContentLoaded early or clear body
41
+ const obs = new MutationObserver((m, observer) => {
42
+ if (document.body) {
43
+ if (!el.parentNode || !document.body.contains(el)) document.body.appendChild(el);
44
+ observer.disconnect();
45
+ }
46
+ });
47
+ obs.observe(document.documentElement, { childList: true });
48
+ }
49
+ }
50
+
51
+ // ── HUD MOUNT LAYER (Phase 1: Closed Shadow DOM + Iframe Architecture) ─────
52
+ // Reload-proof, agent-invisible, real-time visual system
53
+ (function mountHUD() {
54
+ // Guard: exit gracefully on chrome:// pages (content scripts blocked)
55
+ if (location.protocol.startsWith('chrome')) return;
56
+
57
+ let host = null;
58
+ let shadow = null;
59
+ let iframe = null;
60
+ let hudState = {};
61
+
62
+ // Create mount point with closed shadow DOM
63
+ function createMount() {
64
+ if (host && document.documentElement.contains(host)) return;
65
+
66
+ host = document.createElement('weavetab-host');
67
+ host.setAttribute('data-wt-internal', 'true');
68
+ host.setAttribute('aria-hidden', 'true');
69
+ host.style.cssText = 'all:initial !important;position:fixed !important;left:0 !important;top:0 !important;width:0 !important;height:0 !important;z-index:2147483647 !important;pointer-events:none !important;background:transparent !important;opacity:1 !important;transform:none !important;filter:none !important;';
70
+ __wt_safeAppendToBody(host);
71
+
72
+ // Closed shadow root — ref lives ONLY here, never on window
73
+ shadow = host.attachShadow({ mode: 'closed' });
74
+
75
+ iframe = document.createElement('iframe');
76
+ iframe.src = browserAPI.runtime.getURL('hud/index.html');
77
+ iframe.id = 'wt-hud-iframe';
78
+ iframe.setAttribute('allowtransparency', 'true');
79
+ // FIX: Start hidden. Expand only after receiving state to prevent white flash.
80
+ Object.assign(iframe.style, {
81
+ position: 'fixed',
82
+ top: '0',
83
+ left: '0',
84
+ width: '0',
85
+ height: '0',
86
+ border: 'none',
87
+ background: 'transparent',
88
+ pointerEvents: 'none',
89
+ zIndex: '2147483647',
90
+ visibility: 'hidden',
91
+ opacity: '0',
92
+ colorScheme: 'light dark'
93
+ });
94
+ shadow.appendChild(iframe);
95
+
96
+ // REHYDRATE: send state to iframe, but DO NOT make visible yet to avoid white flash.
97
+ iframe.onload = function() {
98
+ browserAPI.storage.local.get('wt-hud-state', function(result) {
99
+ hudState = result['wt-hud-state'] || {};
100
+ if (iframe.contentWindow) {
101
+ iframe.contentWindow.postMessage({ type: 'REHYDRATE', state: hudState }, browserAPI.runtime.getURL(''));
102
+ }
103
+ });
104
+ };
105
+
106
+ // Listen for HUD state changes from the iframe to toggle visibility
107
+ window.addEventListener('message', function(e) {
108
+ if (e.data && e.data.type === 'HUD_STATE_CHANGED' && e.source === iframe.contentWindow) {
109
+ const visible = e.data.state && e.data.state.hudVisible !== false;
110
+ if (visible) {
111
+ iframe.style.width = '100vw';
112
+ iframe.style.height = '100vh';
113
+ iframe.style.visibility = 'visible';
114
+ iframe.style.opacity = '1';
115
+ } else {
116
+ iframe.style.width = '0';
117
+ iframe.style.height = '0';
118
+ iframe.style.visibility = 'hidden';
119
+ iframe.style.opacity = '0';
120
+ }
121
+ }
122
+ });
123
+ }
124
+
125
+ // Navigation guard: re-mount if host is removed (SPA/bfcache safe)
126
+ const observer = new MutationObserver(function() {
127
+ if (!document.documentElement.contains(host)) {
128
+ createMount();
129
+ }
130
+ if (window.__wt_visualComponents && window.__wt_visualComponents.cursor && !document.getElementById('__Weavetab_cursor_host')) {
131
+ if (typeof __wt_recoverVisuals === 'function') __wt_recoverVisuals();
132
+ }
133
+ });
134
+ observer.observe(document.documentElement, { childList: true });
135
+
136
+ // Real-time event relays (Agent only)
137
+ // Removed physical mousemove, click, and keydown listeners to prevent lag and interference with agent visuals.
138
+
139
+ let scrollTicking = false;
140
+ document.addEventListener('scroll', function() {
141
+ if (!scrollTicking && iframe && iframe.contentWindow) {
142
+ window.requestAnimationFrame(function() {
143
+ iframe.contentWindow.postMessage({
144
+ type: 'RT_SCROLL',
145
+ y: window.scrollY,
146
+ ratio: window.scrollY / document.body.scrollHeight
147
+ }, '*');
148
+ scrollTicking = false;
149
+ });
150
+ scrollTicking = true;
151
+ }
152
+ }, { passive: true });
153
+
154
+ // ── MASTER VISUALS TOGGLE SYSTEM ───────────────────────────
155
+ let __wt_masterVisualsEnabled = true;
156
+
157
+ function __wt_applyMasterVisuals(enabled) {
158
+ __wt_masterVisualsEnabled = !!enabled;
159
+ window.__wt_masterVisualsEnabled = __wt_masterVisualsEnabled;
160
+
161
+ const cursorHost = document.getElementById('__Weavetab_cursor_host');
162
+ if (cursorHost) {
163
+ cursorHost.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
164
+ }
165
+ if (typeof __weaveCursor !== 'undefined' && __weaveCursor) {
166
+ __weaveCursor.setVisible(__wt_masterVisualsEnabled);
167
+ if (__weaveCursor._cursor) {
168
+ __weaveCursor._cursor.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
169
+ } else if (__weaveCursor._canvas) {
170
+ __weaveCursor._canvas.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
171
+ }
172
+ }
173
+
174
+ const hudEl = document.getElementById('Weavetab-hud');
175
+ if (hudEl) {
176
+ hudEl.style.display = 'none';
177
+ }
178
+
179
+ const hudHost = document.querySelector('weavetab-host');
180
+ if (hudHost) {
181
+ hudHost.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
182
+ }
183
+
184
+ const indicatorEl = document.getElementById('wt-controlled-indicator') || document.querySelector('.wt-controlled-indicator') || document.getElementById('__Weavetab_controlled_indicator');
185
+ if (indicatorEl) {
186
+ indicatorEl.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
187
+ }
188
+
189
+ if (!__wt_masterVisualsEnabled) {
190
+ if (typeof clearXRay === 'function') clearXRay();
191
+ document.querySelectorAll('.Weavetab-ripple, .wt-xray-spotlight, .wt-bubble, .__Weavetab_xray').forEach(function(el){
192
+ el.remove();
193
+ });
194
+ }
195
+ }
196
+
197
+ try {
198
+ browserAPI.storage.local.get(['wt_master_visuals'], function(res) {
199
+ if (res && res.wt_master_visuals === false) {
200
+ __wt_applyMasterVisuals(false);
201
+ } else {
202
+ __wt_applyMasterVisuals(true);
203
+ }
204
+ });
205
+
206
+ browserAPI.storage.onChanged.addListener(function(changes, area) {
207
+ if (area === 'local' && changes.wt_master_visuals !== undefined) {
208
+ __wt_applyMasterVisuals(changes.wt_master_visuals.newValue !== false);
209
+ }
210
+ });
211
+ } catch(e) {}
212
+
213
+ // Ghost CDP cursor relay from background
214
+ browserAPI.runtime.onMessage.addListener(function(msg) {
215
+ if (msg.type === 'WT_TOGGLE_MASTER_VISUALS') {
216
+ __wt_applyMasterVisuals(msg.enabled);
217
+ }
218
+ if (msg.type === 'GHOST_CURSOR_MOVE') {
219
+ if (typeof msg.x === 'number' && typeof msg.y === 'number') {
220
+ moveCursor(msg.x, msg.y, msg.phase);
221
+ }
222
+ if (iframe && iframe.contentWindow) {
223
+ iframe.contentWindow.postMessage({ type: 'RT_CURSOR_MOVE', x: msg.x, y: msg.y, phase: msg.phase }, '*');
224
+ }
225
+ }
226
+ if (msg.type === 'TOOL_CALL_EVENT' && iframe && iframe.contentWindow) {
227
+ iframe.contentWindow.postMessage({ type: 'TOOL_CALL_EVENT', payload: msg.payload }, '*');
228
+ }
229
+ // Scripted pill toggle — relay to HUD iframe
230
+ if (msg.type === 'WT_SCRIPTED_PILL_TOGGLE' && iframe && iframe.contentWindow) {
231
+ iframe.contentWindow.postMessage({ type: 'WT_SCRIPTED_PILL_TOGGLE', enabled: msg.enabled }, '*');
232
+ }
233
+ // Handle agent thoughts in middle-down HUD
234
+ if (msg.type === 'WT_THOUGHT' || msg.type === 'Weavetab_THOUGHT') {
235
+ __wt_showThought(msg.text, msg);
236
+ }
237
+ if (msg.type === 'WT_CLEAR_THOUGHT' || msg.type === 'Weavetab_CLEAR_THOUGHT') {
238
+ __wt_removeThought();
239
+ }
240
+ // Handle incoming user notes from the extension
241
+ if (msg.type === 'WT_USER_NOTE_INJECT' || msg.type === 'WT_USER_NOTE') {
242
+ window.__wt_chat_queue = window.__wt_chat_queue || [];
243
+ let formattedMsg = msg.text || '';
244
+ if (msg.snipe) {
245
+ formattedMsg = `[USER AREA ANNOTATION ATTACHED (${msg.snipe.mode})]: ` + formattedMsg;
246
+ }
247
+ window.__wt_chat_queue.push(formattedMsg);
248
+
249
+ // Unblock pending ask_bridge polling loops immediately
250
+ window.__wt_ask_bridge = window.__wt_ask_bridge || {};
251
+ if (window.__wt_ask_bridge.resolve) {
252
+ window.__wt_ask_bridge.resolve({
253
+ success: true,
254
+ response: formattedMsg,
255
+ status: 'answered'
256
+ });
257
+ window.__wt_ask_bridge = {};
258
+ }
259
+
260
+ // Display on-page glassmorphism notification banner if master visuals are enabled
261
+ if (window.__wt_masterVisualsEnabled !== false) {
262
+ __wt_showUserNoteVisualBanner(formattedMsg);
263
+ }
264
+ }
265
+ });
266
+
267
+ function __wt_showUserNoteVisualBanner(text) {
268
+ let banner = document.getElementById('wt-user-note-banner');
269
+ if (!banner) {
270
+ banner = document.createElement('div');
271
+ banner.id = 'wt-user-note-banner';
272
+ banner.setAttribute('data-wt-ignore', 'true');
273
+ banner.style.cssText = `
274
+ position: fixed; top: 16px; right: 20px; z-index: 2147483647;
275
+ background: rgba(24, 24, 27, 0.94); backdrop-filter: blur(12px);
276
+ border: 1px solid #38bdf8; color: #f4f4f5; font-weight: 500;
277
+ font-size: 11px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
278
+ padding: 8px 16px; border-radius: 20px;
279
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 14px rgba(56, 189, 248, 0.4);
280
+ display: flex; align-items: center; gap: 8px;
281
+ transition: opacity 0.3s ease, transform 0.3s ease;
282
+ opacity: 0; transform: translateY(-10px); pointer-events: none;
283
+ `;
284
+ document.documentElement.appendChild(banner);
285
+ }
286
+ const cleanText = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').slice(0, 90);
287
+ banner.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#38bdf8" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span style="color:#38bdf8;font-weight:600;">Message Received:</span> <span>${cleanText}</span>`;
288
+ requestAnimationFrame(() => {
289
+ banner.style.opacity = '1';
290
+ banner.style.transform = 'translateY(0)';
291
+ });
292
+ setTimeout(() => {
293
+ banner.style.opacity = '0';
294
+ banner.style.transform = 'translateY(-10px)';
295
+ setTimeout(() => banner.remove(), 300);
296
+ }, 4000);
297
+ }
298
+
299
+ // State persistence bridge: listen for state changes from iframe
300
+ window.addEventListener('message', function(e) {
301
+ // Forward relevant events to the extension background
302
+ if (e.data && (
303
+ e.data.type === 'WT_TOOL_CALL' ||
304
+ e.data.type === 'WT_PLAN_UPDATE' ||
305
+ e.data.type === 'WT_THOUGHT' ||
306
+ e.data.type === 'TOOL_CALL_EVENT' ||
307
+ e.data.type === 'Weavetab_CONFIG' ||
308
+ e.data.type === 'WT_CONFIG_UPDATE'
309
+ )) {
310
+ __wt_safeSendMessage(e.data);
311
+ }
312
+
313
+
314
+ if (e.origin !== browserAPI.runtime.getURL('').slice(0, -1)) return;
315
+ if (e.data.type === 'HUD_STATE_CHANGED') {
316
+ hudState = { ...hudState, ...e.data.state };
317
+ browserAPI.storage.local.set({ 'wt-hud-state': hudState });
318
+ }
319
+ });
320
+
321
+ // Listen for toggle command from background
322
+ browserAPI.runtime.onMessage.addListener(function(msg) {
323
+ console.log('[Weavetab Content] Received message:', msg.type);
324
+ if (msg.type === 'TOGGLE_HUD' && iframe && iframe.contentWindow) {
325
+ iframe.contentWindow.postMessage({ type: 'TOGGLE_HUD' }, '*');
326
+ }
327
+ if (msg.type === 'TOGGLE_DEBUG' && iframe && iframe.contentWindow) {
328
+ console.log('[Weavetab Content] Forwarding TOGGLE_DEBUG to iframe');
329
+ iframe.contentWindow.postMessage({ type: 'TOGGLE_DEBUG' }, '*');
330
+ }
331
+ });
332
+
333
+ // Initial mount
334
+ createMount();
335
+ })();
336
+
337
+ // ── UNIFIED VISUAL SYSTEM INITIALIZATION ─────────────────────────
338
+ // Ensures all visual components initialize in correct order and persist across reloads
339
+ window.__wt_visualsReady = false;
340
+ window.__wt_visualComponents = {
341
+ cursor: false,
342
+ feedback: false,
343
+ debug: false
344
+ };
345
+ window.__wt_zIndexCounter = 2147483640; // Start near max z-index
346
+
347
+ function __wt_initVisuals() {
348
+ if (window.__wt_visualsReady) return;
349
+
350
+ // Initialize cursor first
351
+ if (!window.__wt_visualComponents.cursor && window.__weaveCursor_instance) {
352
+ window.__wt_visualComponents.cursor = true;
353
+ }
354
+
355
+ // Check for WeaveTab OS panel (todo list + chat)
356
+ if (!window.__wt_visualComponents.feedback && document.getElementById('wt-main-container')) {
357
+ window.__wt_visualComponents.feedback = true;
358
+ }
359
+
360
+ // Check for debug overlay
361
+ if (!window.__wt_visualComponents.debug && document.getElementById('wt-debug-overlay')) {
362
+ window.__wt_visualComponents.debug = true;
363
+ }
364
+
365
+ // Mark as ready when cursor and panel are initialized
366
+ if (window.__wt_visualComponents.cursor && window.__wt_visualComponents.feedback) {
367
+ window.__wt_visualsReady = true;
368
+ }
369
+ }
370
+
371
+ // Health check every 5 seconds to recover missing components
372
+ setInterval(function() {
373
+ __wt_initVisuals();
374
+ __wt_recoverVisuals();
375
+ }, 5000);
376
+
377
+ // Visual recovery - detect and fix missing/broken components
378
+ function __wt_recoverVisuals() {
379
+ // Recover cursor if missing
380
+ if (window.__wt_visualComponents && window.__wt_visualComponents.cursor && !document.getElementById('__Weavetab_cursor_host')) {
381
+ console.warn('[Weavetab] Cursor host missing, reinitializing...');
382
+ if (window.__weaveCursor_instance && window.__weaveCursor_instance.destroy) {
383
+ window.__weaveCursor_instance.destroy();
384
+ }
385
+ __weaveCursor = new WeaveCursor();
386
+ window.__weaveCursor_instance = __weaveCursor;
387
+ window.__wt_visualComponents.cursor = true;
388
+ }
389
+
390
+ // Recover WeaveTab OS panel if missing
391
+ if (window.__wt_visualComponents.feedback && !document.getElementById('wt-main-container')) {
392
+ console.warn('[Weavetab] WeaveTab OS panel missing, this is handled by page reload');
393
+ }
394
+
395
+ // Fix z-index conflicts
396
+ var wtElements = document.querySelectorAll('[data-wt-ignore]');
397
+ wtElements.forEach(function(el) {
398
+ if (el.style.zIndex && parseInt(el.style.zIndex) < 2147483640) {
399
+ el.style.zIndex = ++window.__wt_zIndexCounter;
400
+ }
401
+ });
402
+ }
403
+
404
+ // ── STALE ELEMENT CLEANUP ─────────────────────────────────────
405
+ // Removes old UI elements from previous server/extension sessions.
406
+ // Observer stays active for the full page lifetime (SPA-safe).
407
+ (function cleanupOldUI() {
408
+ var OLD_IDS = [
409
+ 'wt-ghost-cursor','wt-ghost-bubble','wt-ask-panel','wt-cur-canvas',
410
+ 'wt-ask-container','wt-msg-panel','wt-ask-title','wt-msg-bar','wt-msg-pill'
411
+ ];
412
+ function removeOld() {
413
+ for (var i = 0; i < OLD_IDS.length; i++) {
414
+ var el = document.getElementById(OLD_IDS[i]);
415
+ if (el) el.remove();
416
+ }
417
+ }
418
+ removeOld();
419
+ // Persistent observer — never disconnects so SPA re-renders stay clean
420
+ var obs = new MutationObserver(function() { removeOld(); });
421
+ if (document.documentElement) {
422
+ obs.observe(document.documentElement, { childList: true, subtree: true });
423
+ } else {
424
+ document.addEventListener('DOMContentLoaded', function() {
425
+ if (document.documentElement) obs.observe(document.documentElement, { childList: true, subtree: true });
426
+ });
427
+ }
428
+ })();
429
+
430
+ // ── UI DESIGN SYSTEM (inject once, styles.css handles visuals) ─
431
+ (function injectWeavetabUI() {
432
+ if (document.getElementById('wt-ui-styles')) return;
433
+ // Guard marker so we only run once
434
+ const m = document.createElement('meta');
435
+ m.id = 'wt-ui-styles';
436
+ (document.head || document.documentElement || document.body).appendChild(m);
437
+ })();
438
+
439
+ // ── THOUGHT BUBBLE ─────────────────────────────────────────────
440
+ let __wt_activeBubble = null;
441
+ let __wt_bubbleTimer = null;
442
+ let __wt_typewriterTimer = null;
443
+ let __wt_feedbackSticky = false;
444
+ let __wt_thoughtBirth = 0;
445
+ let __wt_thoughtQueue = [];
446
+ let __wt_lastRemoveTime = 0;
447
+ let __wt_toolActive = false;
448
+
449
+ function __wt_fadeOutBubble(callback) {
450
+ if (!__wt_activeBubble) { if (callback) callback(); return; }
451
+ var bub = __wt_activeBubble;
452
+ bub.style.transition = 'opacity 0.15s ease-out, transform 0.15s ease-out';
453
+ bub.style.opacity = '0';
454
+ bub.style.transform = 'translateX(-50%) translateY(6px)';
455
+ setTimeout(function() {
456
+ bub.remove();
457
+ if (callback) callback();
458
+ }, 150);
459
+ }
460
+
461
+ function __wt_destroyBubble() {
462
+ if (__wt_bubbleTimer) { clearTimeout(__wt_bubbleTimer); __wt_bubbleTimer = null; }
463
+ if (__wt_typewriterTimer) { clearInterval(__wt_typewriterTimer); __wt_typewriterTimer = null; }
464
+ var current = document.querySelector('.wt-bubble[data-wt-thought]');
465
+ if (current) {
466
+ current.style.transition = 'opacity 0.15s ease-out, transform 0.15s ease-out';
467
+ current.style.opacity = '0';
468
+ current.style.transform = 'translateX(-50%) translateY(6px)';
469
+ setTimeout(function() {
470
+ if (current && current.parentNode) current.remove();
471
+ __wt_activeBubble = null;
472
+ }, 150);
473
+ } else {
474
+ __wt_activeBubble = null;
475
+ }
476
+ __wt_lastRemoveTime = Date.now();
477
+ }
478
+
479
+ // ── MIDDLE-DOWN THOUGHT & PRESENCE HUD ─────────────────────────────
480
+ let __wt_thoughtHudEl = null;
481
+ let __wt_thoughtHudTimer = null;
482
+ let __wt_thoughtHudCountdown = null;
483
+
484
+ function __wt_highlightAnchor(anchor) {
485
+ if (!anchor) return;
486
+ try {
487
+ let sel = anchor;
488
+ if (anchor.startsWith('w:')) {
489
+ const stripped = anchor.slice(2);
490
+ sel = `[data-wt-id="${stripped}"]`;
491
+ }
492
+ const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
493
+ if (!el) return;
494
+ const r = el.getBoundingClientRect();
495
+ if (r.width === 0 && r.height === 0) return;
496
+
497
+ const ring = document.createElement('div');
498
+ ring.setAttribute('data-wt-ignore', 'true');
499
+ ring.style.cssText = `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;border:1.5px solid #38bdf8;box-shadow:0 0 16px rgba(56,189,248,0.5);border-radius:4px;z-index:2147483646;pointer-events:none;animation:wt-consider-pulse 1.2s ease-in-out infinite;`;
500
+ (document.getElementById('Weavetab-root')?.shadowRoot || document.body || document.documentElement).appendChild(ring);
501
+ setTimeout(() => {
502
+ ring.style.transition = 'opacity 0.25s ease-out';
503
+ ring.style.opacity = '0';
504
+ setTimeout(() => ring.remove(), 260);
505
+ }, 2400);
506
+ } catch(e) {}
507
+ }
508
+
509
+ function __wt_removeThought() {
510
+ if (__wt_thoughtHudTimer) {
511
+ clearTimeout(__wt_thoughtHudTimer);
512
+ __wt_thoughtHudTimer = null;
513
+ }
514
+ if (__wt_thoughtHudCountdown) {
515
+ clearInterval(__wt_thoughtHudCountdown);
516
+ __wt_thoughtHudCountdown = null;
517
+ }
518
+ if (__wt_thoughtHudEl) {
519
+ const el = __wt_thoughtHudEl;
520
+ __wt_thoughtHudEl = null;
521
+ el.style.opacity = '0';
522
+ el.style.transform = 'translateX(-50%) translateY(10px)';
523
+ setTimeout(() => {
524
+ if (el.parentNode) el.remove();
525
+ }, 220);
526
+ }
527
+ __wt_feedbackSticky = false;
528
+ clearTimeout(window.__wt_feedbackStickyTimer);
529
+ __wt_destroyBubble();
530
+ }
531
+
532
+ function __wt_showThought(text, options) {
533
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
534
+ if (!text || typeof text !== 'string') return;
535
+
536
+ const opts = options || {};
537
+ const isQuestion = opts.action === 'question' || opts.kind === 'question' || !!opts.ask;
538
+ const anchor = opts.anchor || '';
539
+
540
+ // Contextual visual anchor highlight
541
+ if (anchor) {
542
+ __wt_highlightAnchor(anchor);
543
+ }
544
+
545
+ // Clear pending timers
546
+ if (__wt_thoughtHudTimer) {
547
+ clearTimeout(__wt_thoughtHudTimer);
548
+ __wt_thoughtHudTimer = null;
549
+ }
550
+ if (__wt_thoughtHudCountdown) {
551
+ clearInterval(__wt_thoughtHudCountdown);
552
+ __wt_thoughtHudCountdown = null;
553
+ }
554
+
555
+ let hud = __wt_thoughtHudEl;
556
+ if (!hud || !hud.parentNode) {
557
+ hud = document.createElement('div');
558
+ hud.id = '__wt_thought_hud';
559
+ hud.setAttribute('data-wt-ignore', 'true');
560
+ hud.style.cssText = `
561
+ position: fixed;
562
+ bottom: 24px;
563
+ left: 50%;
564
+ transform: translateX(-50%) translateY(8px);
565
+ z-index: 2147483647;
566
+ width: 320px;
567
+ max-width: calc(100vw - 32px);
568
+ background: #0b0d12;
569
+ border: 1px solid #1e222b;
570
+ border-radius: 4px;
571
+ padding: 10px 12px;
572
+ box-shadow: 0 10px 30px rgba(0,0,0,0.8), 0 0 1px rgba(255,255,255,0.08);
573
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
574
+ color: #f3f4f6;
575
+ pointer-events: auto;
576
+ opacity: 0;
577
+ transition: opacity 0.18s cubic-bezier(0.16, 1, 0.3, 1), transform 0.18s cubic-bezier(0.16, 1, 0.3, 1);
578
+ box-sizing: border-box;
579
+ `;
580
+ (document.getElementById('Weavetab-root')?.shadowRoot || document.body || document.documentElement).appendChild(hud);
581
+ __wt_thoughtHudEl = hud;
582
+ }
583
+
584
+ const cleanText = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
585
+ const anchorBadge = anchor ? `<span style="color:#2b70ff;font-size:10px;font-family:monospace;font-weight:600;margin-left:4px;">@${anchor.replace(/&/g,'&amp;')}</span>` : '';
586
+
587
+ if (isQuestion) {
588
+ const timeoutSec = (opts.timeout || (opts.ask && opts.ask.timeout) || 20);
589
+ hud.innerHTML = `
590
+ <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:8px;">
591
+ <div style="font-size:12px;line-height:1.4;color:#ffffff;font-weight:500;word-break:break-word;">
592
+ ${cleanText}${anchorBadge}
593
+ </div>
594
+ <button id="wt-hud-close" title="Dismiss" style="background:transparent;border:none;color:#555d6d;font-size:12px;cursor:pointer;padding:0 2px;line-height:1;margin-top:1px;">✕</button>
595
+ </div>
596
+ <div style="display:flex;align-items:center;gap:6px;">
597
+ <input id="wt-hud-input" type="text" placeholder="Type answer..." style="height:28px;background:#14161f;border:1px solid #252a36;border-radius:3px;padding:0 8px;font-size:12px;color:#fff;outline:none;flex:1;min-width:0;caret-color:#2b70ff;font-family:inherit;box-sizing:border-box;" />
598
+ <button id="wt-hud-submit-btn" title="Send" style="width:28px;height:28px;background:#2b70ff;border:none;border-radius:3px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;padding:0;box-sizing:border-box;">
599
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
600
+ </button>
601
+ <span id="wt-hud-countdown" style="font-size:10px;color:#64748b;font-family:monospace;margin-left:2px;min-width:20px;text-align:right;">${timeoutSec}s</span>
602
+ </div>
603
+ `;
604
+ } else {
605
+ // Feedback / observation mode — ultra compact single row
606
+ hud.innerHTML = `
607
+ <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
608
+ <div style="display:flex;align-items:center;gap:7px;min-width:0;">
609
+ <span style="display:inline-block;width:5px;height:5px;border-radius:50%;background:#2b70ff;box-shadow:0 0 6px #2b70ff;flex-shrink:0;"></span>
610
+ <span style="font-size:12px;line-height:1.4;color:#f3f4f6;word-break:break-word;font-weight:450;">${cleanText}${anchorBadge}</span>
611
+ </div>
612
+ <button id="wt-hud-close" title="Dismiss" style="background:transparent;border:none;color:#555d6d;font-size:12px;cursor:pointer;padding:0 2px;line-height:1;flex-shrink:0;">✕</button>
613
+ </div>
614
+ `;
615
+ }
616
+
617
+ // Animate in
618
+ requestAnimationFrame(() => {
619
+ if (hud) {
620
+ hud.style.opacity = '1';
621
+ hud.style.transform = 'translateX(-50%) translateY(0)';
622
+ }
623
+ });
624
+
625
+ // Wire close button
626
+ const closeBtn = hud.querySelector('#wt-hud-close');
627
+ if (closeBtn) {
628
+ closeBtn.addEventListener('click', () => {
629
+ __wt_removeThought();
630
+ });
631
+ }
632
+
633
+ if (isQuestion) {
634
+ let askBridge = document.getElementById('__wt_ask_bridge');
635
+ if (!askBridge) {
636
+ askBridge = document.createElement('div');
637
+ askBridge.id = '__wt_ask_bridge';
638
+ askBridge.style.display = 'none';
639
+ document.body.appendChild(askBridge);
640
+ }
641
+ askBridge.dataset.status = 'pending';
642
+ askBridge.dataset.result = '';
643
+
644
+ const input = hud.querySelector('#wt-hud-input');
645
+ const submitBtn = hud.querySelector('#wt-hud-submit-btn');
646
+ const countdownSpan = hud.querySelector('#wt-hud-countdown');
647
+
648
+ let remaining = opts.timeout || (opts.ask && opts.ask.timeout) || 20;
649
+
650
+ __wt_thoughtHudCountdown = setInterval(() => {
651
+ remaining--;
652
+ if (countdownSpan) countdownSpan.textContent = `${remaining}s`;
653
+ if (remaining <= 0) {
654
+ clearInterval(__wt_thoughtHudCountdown);
655
+ __wt_thoughtHudCountdown = null;
656
+ if (askBridge) {
657
+ askBridge.dataset.status = 'timeout';
658
+ askBridge.dataset.result = '';
659
+ }
660
+ __wt_removeThought();
661
+ }
662
+ }, 1000);
663
+
664
+ const submit = () => {
665
+ const val = input ? input.value.trim() : '';
666
+ if (askBridge) {
667
+ askBridge.dataset.status = 'submitted';
668
+ askBridge.dataset.result = val;
669
+ }
670
+ if (window.__wt_ask_bridge && window.__wt_ask_bridge.resolve) {
671
+ window.__wt_ask_bridge.resolve({ success: true, response: val, status: 'answered' });
672
+ window.__wt_ask_bridge = {};
673
+ }
674
+ __wt_removeThought();
675
+ };
676
+
677
+ if (submitBtn) submitBtn.addEventListener('click', submit);
678
+ if (input) {
679
+ input.addEventListener('focus', () => { input.style.borderColor = '#2b70ff'; });
680
+ input.addEventListener('blur', () => { input.style.borderColor = '#252a36'; });
681
+ input.addEventListener('keydown', (e) => {
682
+ if (e.key === 'Enter') submit();
683
+ if (e.key === 'Escape') __wt_removeThought();
684
+ });
685
+ setTimeout(() => { if (input) input.focus(); }, 80);
686
+ }
687
+ } else {
688
+ // Auto-fade timer for feedback
689
+ const autoDuration = Math.max(3800, Math.min(8000, text.length * 50));
690
+ __wt_thoughtHudTimer = setTimeout(() => {
691
+ __wt_removeThought();
692
+ }, autoDuration);
693
+ }
694
+ }
695
+
696
+ function __wt_renderThought(text, options) {
697
+ __wt_showThought(text, options);
698
+ }
699
+
700
+ function __wt_processThoughtQueue() {
701
+ if (__wt_thoughtQueue.length > 0) {
702
+ var next = __wt_thoughtQueue.shift();
703
+ setTimeout(function() { __wt_showThought(next.text, next.options); }, 200);
704
+ }
705
+ }
706
+
707
+ function __wt_showQuestion(text, inputType, timeoutSeconds) {
708
+ __wt_showThought(text, { action: 'question', timeout: timeoutSeconds || 20 });
709
+ }
710
+
711
+ function __wt_removeAsk() {
712
+ __wt_removeThought();
713
+ }
714
+
715
+ // ── SHARED OVERLAY CONTAINER ────────────────────────────────────
716
+ const __wt_shadowStyles = `
717
+ .Weavetab-ripple {
718
+ width: 40px; height: 40px; margin: -20px 0 0 -20px; border-radius: 50%;
719
+ background: rgba(79,142,247,0.35); border: 2px solid #4F8EF7;
720
+ animation: __wt_ripple 0.55s ease-out forwards;
721
+ }
722
+ @keyframes __wt_ripple { from { transform: scale(0.3); opacity: 1; } to { transform: scale(2.2); opacity: 0; } }
723
+ @keyframes __wt_xray_pulse {
724
+ 0% { opacity: 0; transform: scale(0.94); }
725
+ 18% { opacity: 1; transform: scale(1.02); }
726
+ 70% { opacity: 0.85; transform: scale(1); }
727
+ 100% { opacity: 0; transform: scale(0.97); }
728
+ }
729
+ `;
730
+
731
+ function getWeavetabRoot() {
732
+ let host = document.getElementById('Weavetab-root');
733
+ if (!host) {
734
+ host = document.createElement('div');
735
+ host.id = 'Weavetab-root';
736
+ host.setAttribute('data-wt-ignore', '');
737
+ Object.assign(host.style, {
738
+ position: 'fixed', left: '0', top: '0',
739
+ width: '0', height: '0',
740
+ zIndex: '2147483640', pointerEvents: 'none',
741
+ });
742
+ var root = document.documentElement || document.body;
743
+ if (root) root.appendChild(host);
744
+ }
745
+ // Reuse existing shadow root (open mode, set by CDP injector or ourselves)
746
+ if (host.shadowRoot) {
747
+ return host.shadowRoot;
748
+ }
749
+ // Guard against NotSupportedError when another context already attached a closed shadow root
750
+ try {
751
+ const sr = host.attachShadow({ mode: 'open' });
752
+ const style = document.createElement('style');
753
+ style.textContent = __wt_shadowStyles;
754
+ sr.appendChild(style);
755
+ return sr;
756
+ } catch (e) {
757
+ // FIX: fallback must be invisible and non-blocking so it can't
758
+ // cover page content or cause opacity bleeding on sites like example.com.
759
+ const fallback = document.createElement('div');
760
+ fallback.setAttribute('data-wt-ignore', '');
761
+ fallback.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;pointer-events:none;overflow:visible;z-index:2147483640;';
762
+ __wt_safeAppendToBody(fallback);
763
+ return fallback;
764
+ }
765
+ }
766
+
767
+ // ── COORDINATE ORACLE ───────────────────────────────────────────
768
+ function shadowQuery(selector) {
769
+ function searchRoot(root) {
770
+ const found = root.querySelector(selector);
771
+ if (found) return found;
772
+
773
+ for (const el of root.querySelectorAll('*')) {
774
+ if (el.shadowRoot) {
775
+ const result = searchRoot(el.shadowRoot);
776
+ if (result) return result;
777
+ }
778
+ }
779
+ return null;
780
+ }
781
+ return searchRoot(document);
782
+ }
783
+ window.__Weavetab = {
784
+ version: '2.5.0',
785
+ suppressVisuals: false,
786
+ shadowQuery: shadowQuery,
787
+
788
+ // ── CDP Co-Processor Helpers ──────────────────────────────────────────────
789
+ // Call these via Runtime.evaluate instead of doing expensive CDP-side work.
790
+ // All return plain JSON-serializable values.
791
+
792
+ /** Scroll position, viewport size, and page dimensions in one call. */
793
+ getScrollInfo: function() {
794
+ var vv = window.visualViewport || {};
795
+ var body = document.body || document.documentElement;
796
+ return {
797
+ scrollX: Math.round(window.scrollX),
798
+ scrollY: Math.round(window.scrollY),
799
+ viewportW: Math.round(vv.width || window.innerWidth),
800
+ viewportH: Math.round(vv.height || window.innerHeight),
801
+ pageW: Math.round(body.scrollWidth),
802
+ pageH: Math.round(body.scrollHeight),
803
+ maxScrollX: Math.round(Math.max(0, body.scrollWidth - window.innerWidth)),
804
+ maxScrollY: Math.round(Math.max(0, body.scrollHeight - window.innerHeight)),
805
+ scrollRatio: body.scrollHeight > 0 ? +(window.scrollY / body.scrollHeight).toFixed(3) : 0
806
+ };
807
+ },
808
+
809
+ /**
810
+ * Wait until no DOM mutations occur for `quietMs` milliseconds.
811
+ * Resolves to true when stable, false on timeout (default 5000ms).
812
+ * CDP calls this and awaits — replaces dumb sleep() calls.
813
+ */
814
+ waitForStable: function(quietMs, timeoutMs) {
815
+ quietMs = quietMs || 300;
816
+ timeoutMs = timeoutMs || 5000;
817
+ return new Promise(function(resolve) {
818
+ var timer = null;
819
+ var deadline = setTimeout(function() {
820
+ obs.disconnect();
821
+ resolve(false);
822
+ }, timeoutMs);
823
+ function reset() {
824
+ if (timer) clearTimeout(timer);
825
+ timer = setTimeout(function() {
826
+ clearTimeout(deadline);
827
+ obs.disconnect();
828
+ resolve(true);
829
+ }, quietMs);
830
+ }
831
+ var obs = new MutationObserver(reset);
832
+ obs.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });
833
+ reset(); // start the quiet timer immediately
834
+ });
835
+ },
836
+
837
+ /**
838
+ * Resolve a data-wt-id directly from the live index — much faster than
839
+ * doing a full accessibility tree walk.
840
+ * Returns { id, x, y, rect, tag, label, viewport, isVisible } or null.
841
+ */
842
+ getElementByWtId: function(id) {
843
+ var idx = window.__wt_liveIndex;
844
+ if (!idx) return null;
845
+ var entry = null;
846
+ for (var i = 0; i < idx.length; i++) {
847
+ if (idx[i].id === String(id)) { entry = idx[i]; break; }
848
+ }
849
+ if (!entry) return null;
850
+ // Refresh coordinates from DOM if element still exists
851
+ var el = document.querySelector('[data-wt-id="' + entry.id + '"]');
852
+ if (el) {
853
+ var r = el.getBoundingClientRect();
854
+ entry.x = Math.round(r.left + r.width / 2);
855
+ entry.y = Math.round(r.top + r.height / 2);
856
+ entry.rect = { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) };
857
+ }
858
+ return entry;
859
+ },
860
+
861
+ /**
862
+ * Dump all form field current values — lets agent verify form state cheaply.
863
+ * Returns array of { name, id, type, value, tag }.
864
+ */
865
+ getFormValues: function() {
866
+ var results = [];
867
+ var els = document.querySelectorAll('input, select, textarea');
868
+ for (var i = 0; i < els.length; i++) {
869
+ var el = els[i];
870
+ if (el.hasAttribute('data-wt-ignore')) continue;
871
+ var val = el.type === 'checkbox' || el.type === 'radio' ? el.checked : el.value;
872
+ results.push({
873
+ name: el.name || el.id || null,
874
+ id: el.id || null,
875
+ wtId: el.getAttribute('data-wt-id') || null,
876
+ type: el.type || el.tagName.toLowerCase(),
877
+ tag: el.tagName.toLowerCase(),
878
+ value: val,
879
+ placeholder: el.placeholder || null
880
+ });
881
+ }
882
+ return results;
883
+ },
884
+
885
+ /**
886
+ * Check visibility of a selector — uses the live IntersectionObserver data
887
+ * maintained by the MutationObserver. Much faster than a full DOM query.
888
+ * Returns { found, visible, occluded, x, y, rect } or null.
889
+ */
890
+ checkVisible: function(selector) {
891
+ var el = document.querySelector(selector) || shadowQuery(selector);
892
+ if (!el) return { found: false };
893
+ var r = el.getBoundingClientRect();
894
+ var vv = window.visualViewport || {};
895
+ var vw = vv.width || window.innerWidth;
896
+ var vh = vv.height || window.innerHeight;
897
+ var inViewport = r.width > 0 && r.height > 0 && r.top < vh && r.bottom > 0 && r.left < vw && r.right > 0;
898
+ var topEl = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
899
+ var occluded = topEl !== el && !el.contains(topEl);
900
+ return {
901
+ found: true,
902
+ visible: inViewport,
903
+ occluded: occluded,
904
+ x: Math.round(r.left + r.width / 2),
905
+ y: Math.round(r.top + r.height / 2),
906
+ rect: { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) }
907
+ };
908
+ },
909
+
910
+ /**
911
+ * Return the live element index, optionally filtered.
912
+ * filter: 'visible' | 'all' (default 'visible')
913
+ * Much faster than browser_map for simple lookups.
914
+ */
915
+ getLiveIndex: function(filter) {
916
+ var idx = window.__wt_liveIndex || [];
917
+ if (filter === 'all') return idx;
918
+ // Default: visible viewport only
919
+ return idx.filter(function(e) { return e.viewport === 'visible' || e.isVisible; });
920
+ },
921
+
922
+ /**
923
+ * Pop and return the next user feedback message from the queue.
924
+ * MCP calls this between tool calls to check for user interrupts.
925
+ * Returns { text, ts, tabId } or null if queue is empty.
926
+ * Non-destructive — call popUserFeedback() to consume, peekUserFeedback() to inspect.
927
+ */
928
+ popUserFeedback: function() {
929
+ // Read from localStorage queue (written by submitFeedback + persisted by background)
930
+ try {
931
+ var raw = localStorage.getItem('__wt_chat_queue_persist');
932
+ if (!raw) return null;
933
+ var arr = JSON.parse(raw);
934
+ if (!arr || !arr.length) return null;
935
+ var item = arr.shift();
936
+ localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(arr));
937
+ return item;
938
+ } catch(e) { return null; }
939
+ },
940
+
941
+ /**
942
+ * Check if a blocking dialog (alert/confirm/prompt) is pending on this tab.
943
+ * Agent should check this before clicking if it suspects a dialog might block.
944
+ * Returns { pending: bool, ts: number } — reads from extension storage via background relay.
945
+ */
946
+ getAlertState: function() {
947
+ // Sync check via a DOM marker set by alert wrapper
948
+ return { pending: !!(window.__wt_alertPending), ts: Date.now() };
949
+ },
950
+ };
951
+ window.__Weavetab_shadowQuery = shadowQuery;
952
+
953
+ function isOccluded(el) {
954
+ const rect = el.getBoundingClientRect();
955
+ const cx = rect.left + rect.width / 2;
956
+ const cy = rect.top + rect.height / 2;
957
+ const topEl = document.elementFromPoint(cx, cy);
958
+ return topEl !== el && !el.contains(topEl);
959
+ }
960
+
961
+ window.__Weavetab_getCoords = function(selector) {
962
+ const el = document.querySelector(selector) || shadowQuery(selector);
963
+
964
+ if (!el) return null;
965
+
966
+ const rect = el.getBoundingClientRect();
967
+ const vv = window.visualViewport;
968
+
969
+ return {
970
+ x: rect.left + rect.width / 2 + vv.offsetLeft,
971
+ y: rect.top + rect.height / 2 + vv.offsetTop,
972
+ raw: { left: rect.left, top: rect.top, w: rect.width, h: rect.height },
973
+ occluded: isOccluded(el),
974
+ visible: rect.width > 0 && rect.height > 0
975
+ && rect.top < vv.height && rect.bottom > 0
976
+ };
977
+ };
978
+
979
+ function getStableSelector(el) {
980
+ if (el.id) return `#${el.id}`;
981
+ const path = [];
982
+ while (el.parentNode) {
983
+ let index = 1;
984
+ let sibling = el.previousElementSibling;
985
+ while (sibling) {
986
+ if (sibling.tagName === el.tagName) index++;
987
+ sibling = sibling.previousElementSibling;
988
+ }
989
+ path.unshift(`${el.tagName.toLowerCase()}:nth-of-type(${index})`);
990
+ el = el.parentNode;
991
+ }
992
+ return path.join(' > ');
993
+ }
994
+
995
+ window.__Weavetab_fullMap = function() {
996
+ const map = [];
997
+ let idCounter = 1;
998
+ function crawl(root, depth = 0) {
999
+ for (const el of root.querySelectorAll(
1000
+ 'a,button,input,select,textarea,[role],[onclick],[tabindex]'
1001
+ )) {
1002
+ if (el.hasAttribute('data-wt-ignore')) continue;
1003
+ const rect = el.getBoundingClientRect();
1004
+ // Always include form elements (input/textarea/select/button) regardless of rect size
1005
+ // — canvas-heavy pages can occlude them and getBoundingClientRect returns 0
1006
+ const isEssential = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(el.tagName);
1007
+ if (rect.width === 0 && !isEssential) continue;
1008
+ let id = el.getAttribute('data-wt-id');
1009
+ if (!id) {
1010
+ id = String(idCounter++);
1011
+ el.setAttribute('data-wt-id', id);
1012
+ }
1013
+ map.push({
1014
+ id: id,
1015
+ tag: el.tagName.toLowerCase(),
1016
+ text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0,80),
1017
+ role: el.getAttribute('role'),
1018
+ selector: `[data-wt-id="${id}"]`,
1019
+ coords: { x: rect.left + rect.width/2, y: rect.top + rect.height/2 },
1020
+ inShadow: depth > 0
1021
+ });
1022
+ if (el.shadowRoot) crawl(el.shadowRoot, depth + 1);
1023
+ }
1024
+ }
1025
+ crawl(document);
1026
+ return map;
1027
+ };
1028
+
1029
+ // ── CONSOLE BUFFERING ───────────────────────────────────────────
1030
+ const __consoleBuffer = [];
1031
+ let __consoleCursor = 0;
1032
+
1033
+ ['log','warn','error','info','debug'].forEach(level => {
1034
+ const orig = console[level];
1035
+ console[level] = (...args) => {
1036
+ __consoleBuffer.push({ level, time: Date.now(), text: args.join(' ') });
1037
+ orig.apply(console, args);
1038
+ };
1039
+ });
1040
+
1041
+ window.__Weavetab_readConsole = function({ since = 'last', level = null } = {}) {
1042
+ const from = since === 'last' ? __consoleCursor : 0;
1043
+ const entries = __consoleBuffer.slice(from);
1044
+ __consoleCursor = __consoleBuffer.length;
1045
+ return level ? entries.filter(e => e.level === level) : entries;
1046
+ };
1047
+
1048
+ // ── BLUE SVG WEAVE CURSOR ───────────────────────────────────────
1049
+ // Shadow-DOM-isolated, hardware-accelerated SVG cursor with smooth interpolation
1050
+ class WeaveCursor {
1051
+ constructor() {
1052
+ // Attach a shadow host that sits above all page content
1053
+ this._host = document.createElement('div');
1054
+ this._host.id = '__Weavetab_cursor_host';
1055
+ this._host.setAttribute('data-wt-ignore', 'true');
1056
+ Object.assign(this._host.style, {
1057
+ position: 'fixed',
1058
+ top: '0',
1059
+ left: '0',
1060
+ width: '0',
1061
+ height: '0',
1062
+ zIndex: '2147483647',
1063
+ pointerEvents: 'none',
1064
+ overflow: 'visible',
1065
+ });
1066
+
1067
+ const shadow = this._host.attachShadow({ mode: 'closed' });
1068
+
1069
+ // Liquid trail canvas inside shadow root (zero DOM reflows)
1070
+ this._trailCanvas = document.createElement('canvas');
1071
+ this._trailCanvas.setAttribute('data-wt-ignore', 'true');
1072
+ Object.assign(this._trailCanvas.style, {
1073
+ position: 'fixed',
1074
+ top: '0',
1075
+ left: '0',
1076
+ width: '100vw',
1077
+ height: '100vh',
1078
+ pointerEvents: 'none',
1079
+ zIndex: '2147483645',
1080
+ overflow: 'visible',
1081
+ });
1082
+ this._trailCanvas.width = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1920;
1083
+ this._trailCanvas.height = typeof window !== 'undefined' && window.innerHeight ? window.innerHeight : 1080;
1084
+ shadow.appendChild(this._trailCanvas);
1085
+ this._trailCtx = this._trailCanvas.getContext('2d');
1086
+ this._trailPoints = [];
1087
+ this._lastTrailRecord = 0;
1088
+
1089
+ window.addEventListener('resize', () => {
1090
+ if (this._trailCanvas) {
1091
+ this._trailCanvas.width = window.innerWidth;
1092
+ this._trailCanvas.height = window.innerHeight;
1093
+ }
1094
+ });
1095
+
1096
+ // Hotspot aligned with the top-left pointer tip (3.0, 5.0 in 32x32 viewBox)
1097
+ this._hotX = 3.0;
1098
+ this._hotY = 5.0;
1099
+ this._scale = 0.4;
1100
+
1101
+ const initX = typeof window !== 'undefined' && window.innerWidth ? Math.round(window.innerWidth / 2) : 640;
1102
+ const initY = typeof window !== 'undefined' && window.innerHeight ? Math.round(window.innerHeight / 2) : 400;
1103
+
1104
+ // Exact vector SVG cursor matching user design
1105
+ this._cursor = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1106
+ this._cursor.setAttribute('class', 'cursor');
1107
+ this._cursor.setAttribute('viewBox', '0 0 32 32');
1108
+ this._cursor.setAttribute('width', '32');
1109
+ this._cursor.setAttribute('height', '32');
1110
+ this._cursor.setAttribute('aria-hidden', 'true');
1111
+ Object.assign(this._cursor.style, {
1112
+ position: 'fixed',
1113
+ top: '0',
1114
+ left: '0',
1115
+ width: '32px',
1116
+ height: '32px',
1117
+ display: 'block',
1118
+ transformOrigin: `${this._hotX}px ${this._hotY}px`,
1119
+ transform: `translate3d(${initX - this._hotX}px, ${initY - this._hotY}px, 0) scale(${this._scale})`,
1120
+ overflow: 'visible',
1121
+ pointerEvents: 'none',
1122
+ willChange: 'transform',
1123
+ transition: 'opacity 0.2s ease-out',
1124
+ opacity: '1',
1125
+ zIndex: '2147483647',
1126
+ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.35))',
1127
+ });
1128
+
1129
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1130
+ path.setAttribute('class', 'cursor-shape');
1131
+ path.setAttribute('d', 'M 6.1,4.7 C 4.5,4.1 3.0,5.0 3.0,6.7 C 3.0,7.1 3.1,7.5 3.3,8.0 L 10.2,27.0 C 10.8,28.7 12.0,29.6 13.6,29.5 C 15.1,29.4 16.0,28.5 16.5,27.0 L 19.4,18.8 L 25.9,17.0 C 27.7,16.5 28.9,15.2 28.9,13.6 C 28.9,12.1 28.0,10.9 26.4,10.3 Z');
1132
+ path.setAttribute('fill', '#1268ff');
1133
+ path.setAttribute('stroke', '#ffffff');
1134
+ path.setAttribute('stroke-width', '3.1');
1135
+ path.setAttribute('stroke-linejoin', 'round');
1136
+ path.setAttribute('stroke-linecap', 'round');
1137
+
1138
+ this._cursor.appendChild(path);
1139
+ shadow.appendChild(this._cursor);
1140
+
1141
+ // Alias _canvas to _cursor for backward compatibility
1142
+ this._canvas = this._cursor;
1143
+
1144
+ this._x = initX;
1145
+ this._y = initY;
1146
+ this._targetX = initX;
1147
+ this._targetY = initY;
1148
+ this._lerpX = initX;
1149
+ this._lerpY = initY;
1150
+ this._state = 'idle';
1151
+ this._visible = true;
1152
+ this._animFrame = null;
1153
+ this._lastTime = performance.now();
1154
+ this._waypointQueue = [];
1155
+ this._playingQueue = false;
1156
+
1157
+ __wt_safeAppendToBody(this._host);
1158
+ this._startLoop();
1159
+ }
1160
+
1161
+ destroy() {
1162
+ if (this._animFrame) {
1163
+ cancelAnimationFrame(this._animFrame);
1164
+ this._animFrame = null;
1165
+ }
1166
+ if (this._host && this._host.parentNode) {
1167
+ this._host.parentNode.removeChild(this._host);
1168
+ }
1169
+ }
1170
+
1171
+ setState(state) {
1172
+ this._state = state || 'idle';
1173
+ }
1174
+
1175
+ setVisible(visible) {
1176
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) {
1177
+ this._visible = false;
1178
+ this._cursor.style.opacity = '0';
1179
+ if (this._trailCtx && this._trailCanvas) {
1180
+ this._trailCtx.clearRect(0, 0, this._trailCanvas.width, this._trailCanvas.height);
1181
+ }
1182
+ return;
1183
+ }
1184
+ this._visible = visible;
1185
+ this._cursor.style.opacity = visible ? '1' : '0';
1186
+ }
1187
+
1188
+ moveTo(x, y) {
1189
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1190
+ if (typeof x !== 'number' || typeof y !== 'number' || isNaN(x) || isNaN(y)) return;
1191
+ this._targetX = x;
1192
+ this._targetY = y;
1193
+ if (this._lerpX === -9999) {
1194
+ this._lerpX = x;
1195
+ this._lerpY = y;
1196
+ }
1197
+ if (!this._visible) {
1198
+ this._visible = true;
1199
+ this._cursor.style.opacity = '1';
1200
+ }
1201
+ if (!this._animFrame) {
1202
+ this._startLoop();
1203
+ }
1204
+ }
1205
+
1206
+ setWaypoints(waypoints, durationMs) {
1207
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1208
+ if (!waypoints || !waypoints.length) return;
1209
+ this._waypointQueue = waypoints;
1210
+ this._playingQueue = true;
1211
+ this._waypointStartTime = performance.now();
1212
+ this._waypointDuration = durationMs || (waypoints.length * 16);
1213
+ if (waypoints.length > 0 && this._lerpX === -9999) {
1214
+ this._lerpX = waypoints[0].x;
1215
+ this._lerpY = waypoints[0].y;
1216
+ }
1217
+ if (!this._visible) {
1218
+ this._visible = true;
1219
+ this._cursor.style.opacity = '1';
1220
+ }
1221
+ if (!this._animFrame) this._startLoop();
1222
+ }
1223
+
1224
+ _startLoop() {
1225
+ this._lastTime = performance.now();
1226
+ const draw = (currentTime) => {
1227
+ const dt = Math.min((currentTime - this._lastTime) / 1000, 0.1);
1228
+ this._lastTime = currentTime;
1229
+
1230
+ if (this._playingQueue && this._waypointQueue.length > 0) {
1231
+ const elapsed = currentTime - this._waypointStartTime;
1232
+ const progress = Math.max(0, Math.min(elapsed / (this._waypointDuration || 1), 1));
1233
+ const totalSegments = this._waypointQueue.length - 1;
1234
+
1235
+ if (totalSegments <= 0) {
1236
+ this._targetX = this._waypointQueue[0].x;
1237
+ this._targetY = this._waypointQueue[0].y;
1238
+ } else {
1239
+ const floatIdx = progress * totalSegments;
1240
+ const i0 = Math.floor(floatIdx);
1241
+ const i1 = Math.min(i0 + 1, totalSegments);
1242
+ const t = floatIdx - i0;
1243
+
1244
+ const p0 = this._waypointQueue[i0];
1245
+ const p1 = this._waypointQueue[i1];
1246
+ if (p0 && p1) {
1247
+ this._targetX = p0.x + (p1.x - p0.x) * t;
1248
+ this._targetY = p0.y + (p1.y - p0.y) * t;
1249
+ }
1250
+ }
1251
+
1252
+ if (progress >= 1) {
1253
+ this._playingQueue = false;
1254
+ this._waypointQueue = [];
1255
+ }
1256
+ }
1257
+
1258
+ // Smooth exponential lerp (k = 24 for fluid human-smooth tracking)
1259
+ const lerpFactor = 1 - Math.exp(-24 * dt);
1260
+ if (this._targetX !== -9999) {
1261
+ this._lerpX += (this._targetX - this._lerpX) * lerpFactor;
1262
+ this._lerpY += (this._targetY - this._lerpY) * lerpFactor;
1263
+ if (Math.abs(this._targetX - this._lerpX) < 0.1) this._lerpX = this._targetX;
1264
+ if (Math.abs(this._targetY - this._lerpY) < 0.1) this._lerpY = this._targetY;
1265
+
1266
+ // "always show in browser" - stays visible once loaded and active
1267
+ if (typeof __wt_masterVisualsEnabled === 'undefined' || __wt_masterVisualsEnabled) {
1268
+ if (!this._visible) {
1269
+ this._visible = true;
1270
+ this._cursor.style.opacity = '1';
1271
+ }
1272
+ } else {
1273
+ if (this._visible) {
1274
+ this._visible = false;
1275
+ this._cursor.style.opacity = '0';
1276
+ }
1277
+ }
1278
+
1279
+ this._render(currentTime);
1280
+ }
1281
+
1282
+ this._animFrame = requestAnimationFrame(draw);
1283
+ };
1284
+ if (this._animFrame) cancelAnimationFrame(this._animFrame);
1285
+ this._animFrame = requestAnimationFrame(draw);
1286
+ }
1287
+
1288
+ _render(currentTime) {
1289
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) {
1290
+ this._cursor.style.opacity = '0';
1291
+ if (this._trailCtx && this._trailCanvas) {
1292
+ this._trailCtx.clearRect(0, 0, this._trailCanvas.width, this._trailCanvas.height);
1293
+ }
1294
+ return;
1295
+ }
1296
+ const x = this._lerpX;
1297
+ const y = this._lerpY;
1298
+ if (x === -9999 || y === -9999) return;
1299
+
1300
+ const now = currentTime || performance.now();
1301
+
1302
+ // 1. Trail decay tracking
1303
+ const dx = this._targetX - this._lerpX;
1304
+ const dy = this._targetY - this._lerpY;
1305
+ const moveDist = Math.sqrt(dx * dx + dy * dy);
1306
+
1307
+ if (moveDist > 1.8 && now - this._lastTrailRecord > 22) {
1308
+ this._trailPoints.push({ x, y, birth: now });
1309
+ this._lastTrailRecord = now;
1310
+ if (this._trailPoints.length > 14) this._trailPoints.shift();
1311
+ }
1312
+
1313
+ // Render decaying trail
1314
+ if (this._trailCtx && this._trailCanvas) {
1315
+ this._trailCtx.clearRect(0, 0, this._trailCanvas.width, this._trailCanvas.height);
1316
+ for (let i = this._trailPoints.length - 1; i >= 0; i--) {
1317
+ const pt = this._trailPoints[i];
1318
+ const age = (now - pt.birth) / 260;
1319
+ if (age >= 1) {
1320
+ this._trailPoints.splice(i, 1);
1321
+ continue;
1322
+ }
1323
+ const alpha = (1 - age) * 0.45;
1324
+ const radius = Math.max(1, (1 - age) * 2.8);
1325
+ this._trailCtx.beginPath();
1326
+ this._trailCtx.arc(pt.x, pt.y, radius, 0, Math.PI * 2);
1327
+ this._trailCtx.fillStyle = `rgba(18, 104, 255, ${alpha})`;
1328
+ this._trailCtx.shadowColor = '#1268ff';
1329
+ this._trailCtx.shadowBlur = 4;
1330
+ this._trailCtx.fill();
1331
+ }
1332
+ }
1333
+
1334
+ // 2. Cursor scale and tactile micro-physics
1335
+ let targetScale = this._scale;
1336
+ if (this._state === 'clicking') {
1337
+ targetScale = this._scale * 0.82; // Tactile compression on click
1338
+ } else if (this._state === 'hover') {
1339
+ targetScale = this._scale * 1.08; // Interactive focus
1340
+ } else if (this._state === 'idle') {
1341
+ targetScale = this._scale + Math.sin(now * 0.003) * 0.012; // Alive breathing oscillation
1342
+ }
1343
+
1344
+ // 3. Dynamic velocity tilt (natural motion lag)
1345
+ const tilt = Math.max(-5, Math.min(5, dx * 0.06));
1346
+
1347
+ this._cursor.style.transform = `translate3d(${x - this._hotX}px, ${y - this._hotY}px, 0) rotate(${tilt}deg) scale(${targetScale})`;
1348
+ }
1349
+ }
1350
+
1351
+ // Instantiate the active cursor
1352
+ let __weaveCursor = new WeaveCursor();
1353
+ window.__weaveCursor_instance = __weaveCursor;
1354
+
1355
+ function moveCursor(x, y, state) {
1356
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1357
+ if (__weaveCursor && typeof __weaveCursor.moveTo === 'function') {
1358
+ if (typeof x === 'number' && typeof y === 'number' && x >= 0 && y >= 0) {
1359
+ __weaveCursor.moveTo(x, y);
1360
+ }
1361
+ if (state) __weaveCursor.setState(state);
1362
+ }
1363
+ }
1364
+
1365
+ // Enhanced twin action ripples radiating from click target
1366
+ function ripple(x, y) {
1367
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1368
+ const host = document.createElement('div');
1369
+ host.className = 'Weavetab-ripple-host';
1370
+ host.setAttribute('data-wt-ignore', 'true');
1371
+ host.style.cssText = `position:fixed;left:${x}px;top:${y}px;pointer-events:none;z-index:2147483646;transform:translate(-50%, -50%);`;
1372
+
1373
+ const w1 = document.createElement('div');
1374
+ w1.style.cssText = 'position:absolute;top:50%;left:50%;width:8px;height:8px;margin:-4px 0 0 -4px;border:2px solid #1268ff;border-radius:50%;box-shadow:0 0 12px rgba(18,104,255,0.7);animation:__wt_ripple_expand 0.42s cubic-bezier(0.1, 0.8, 0.3, 1) forwards;pointer-events:none;';
1375
+
1376
+ const w2 = document.createElement('div');
1377
+ w2.style.cssText = 'position:absolute;top:50%;left:50%;width:8px;height:8px;margin:-4px 0 0 -4px;border:1.5px solid #60a5fa;border-radius:50%;animation:__wt_ripple_expand 0.52s 0.08s cubic-bezier(0.1, 0.8, 0.3, 1) forwards;pointer-events:none;';
1378
+
1379
+ host.appendChild(w1);
1380
+ host.appendChild(w2);
1381
+ (document.getElementById('Weavetab-root')?.shadowRoot || document.body || document.documentElement).appendChild(host);
1382
+ setTimeout(() => host.remove(), 620);
1383
+ }
1384
+
1385
+ // ── X-RAY SPOTLIGHT OVERLAY (Phase 5.2) ────────────────────────
1386
+ // Flashes glowing halos around top interactive elements when the
1387
+ // semantic map is scanned (GET_SEMANTIC_MAP message).
1388
+ let __xrayActive = false;
1389
+ const __xrayOverlays = [];
1390
+
1391
+ function clearXRay() {
1392
+ for (const el of __xrayOverlays) el.remove();
1393
+ __xrayOverlays.length = 0;
1394
+ __xrayActive = false;
1395
+ }
1396
+
1397
+ function activateXRay(elements) {
1398
+ clearXRay();
1399
+ __xrayActive = true;
1400
+
1401
+ // Colour spectrum for visual richness
1402
+ const colours = [
1403
+ '#4F8EF7', '#7C3AED', '#10B981', '#F59E0B',
1404
+ '#EF4444', '#06B6D4', '#F97316', '#8B5CF6',
1405
+ ];
1406
+
1407
+ // Architecture change: Use live index for visual overlay
1408
+ // The live index maintains up-to-date coordinates and visibility info
1409
+ const liveIndex = window.__wt_liveIndex || [];
1410
+ const top = elements.slice(0, 12); // highlight top 12 nodes
1411
+
1412
+ top.forEach((item, i) => {
1413
+ // Try to get element from live index first for accurate coordinates
1414
+ const liveEntry = liveIndex.find(e => e.id === item.id || e.selector === item.selector);
1415
+
1416
+ let rect;
1417
+ let el;
1418
+
1419
+ if (liveEntry && liveEntry.rect) {
1420
+ // Use coordinates from live index (more accurate, already computed)
1421
+ rect = liveEntry.rect;
1422
+ el = document.querySelector(item.selector) || shadowQuery(item.selector);
1423
+ } else {
1424
+ // Fallback to querying DOM
1425
+ el = document.querySelector(item.selector) || shadowQuery(item.selector);
1426
+ if (!el) return;
1427
+ rect = el.getBoundingClientRect();
1428
+ }
1429
+
1430
+ if (rect.width === 0 || rect.height === 0) return;
1431
+
1432
+ const colour = colours[i % colours.length];
1433
+ const halo = document.createElement('div');
1434
+ halo.className = '__Weavetab_xray';
1435
+ halo.setAttribute('data-wt-ignore', '');
1436
+
1437
+ Object.assign(halo.style, {
1438
+ position: 'fixed',
1439
+ left: `${rect.left - 4}px`,
1440
+ top: `${rect.top - 4}px`,
1441
+ width: `${rect.width + 8}px`,
1442
+ height: `${rect.height + 8}px`,
1443
+ border: `2px solid ${colour}`,
1444
+ borderRadius: '6px',
1445
+ boxShadow: `0 0 12px 4px ${colour}88, inset 0 0 8px ${colour}33`,
1446
+ background: `${colour}0d`,
1447
+ pointerEvents: 'none',
1448
+ zIndex: '2147483645',
1449
+ animation: '__wt_xray_pulse 1.4s ease-in-out forwards',
1450
+ animationDelay: `${i * 55}ms`,
1451
+ });
1452
+
1453
+ // Rank label badge
1454
+ const badge = document.createElement('span');
1455
+ Object.assign(badge.style, {
1456
+ position: 'absolute',
1457
+ top: '-10px',
1458
+ left: '4px',
1459
+ background: colour,
1460
+ color: '#fff',
1461
+ fontSize: '9px',
1462
+ fontFamily: 'system-ui, sans-serif',
1463
+ fontWeight: '700',
1464
+ padding: '1px 5px',
1465
+ borderRadius: '3px',
1466
+ lineHeight: '14px',
1467
+ whiteSpace: 'nowrap',
1468
+ letterSpacing: '0.03em',
1469
+ boxShadow: `0 1px 4px ${colour}99`,
1470
+ });
1471
+ const label = (item.text || item.role || item.tag || '').slice(0, 22) || `#${i + 1}`;
1472
+ badge.textContent = `${i + 1} ${label}`;
1473
+ halo.appendChild(badge);
1474
+
1475
+ getWeavetabRoot().appendChild(halo);
1476
+ __xrayOverlays.push(halo);
1477
+ });
1478
+
1479
+ // Auto-clear after 2.8 s
1480
+ setTimeout(clearXRay, 2800);
1481
+ }
1482
+
1483
+ // Inject keyframe animation for X-Ray
1484
+ (function injectXRayStyles() {
1485
+ if (document.getElementById('__Weavetab_xray_styles')) return;
1486
+ const style = document.createElement('style');
1487
+ style.id = '__Weavetab_xray_styles';
1488
+ style.textContent = `
1489
+ @keyframes __wt_xray_pulse {
1490
+ 0% { opacity: 0; transform: scale(0.94); }
1491
+ 18% { opacity: 1; transform: scale(1.02); }
1492
+ 70% { opacity: 0.85; transform: scale(1); }
1493
+ 100% { opacity: 0; transform: scale(0.97); }
1494
+ }
1495
+ .Weavetab-ripple {
1496
+ width: 40px; height: 40px;
1497
+ margin: -20px 0 0 -20px;
1498
+ border-radius: 50%;
1499
+ background: rgba(79,142,247,0.35);
1500
+ border: 2px solid #4F8EF7;
1501
+ animation: __wt_ripple 0.55s ease-out forwards;
1502
+ }
1503
+ @keyframes __wt_ripple {
1504
+ from { transform: scale(0.3); opacity: 1; }
1505
+ to { transform: scale(2.2); opacity: 0; }
1506
+ }
1507
+ @keyframes wt-reading-pulse {
1508
+ 0% { opacity: 0; box-shadow: inset 0 0 0 rgba(59,130,246,0); }
1509
+ 15% { opacity: 1; box-shadow: inset 0 0 60px rgba(59,130,246,0.15); }
1510
+ 85% { opacity: 1; box-shadow: inset 0 0 60px rgba(59,130,246,0.15); }
1511
+ 100% { opacity: 0; box-shadow: inset 0 0 0 rgba(59,130,246,0); }
1512
+ }
1513
+ `;
1514
+ (document.head || document.documentElement).appendChild(style);
1515
+ })();
1516
+
1517
+ // ── READING GLOW ────────────────────────────────────────────────
1518
+ let __wt_readingGlow = null;
1519
+
1520
+ function __wt_showReadingGlow() {
1521
+ if (__wt_readingGlow) return;
1522
+ var el = document.createElement('div');
1523
+ el.id = 'wt-reading-glow';
1524
+ el.setAttribute('data-wt-ignore', '');
1525
+ el.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483646;border:4px solid;border-image:linear-gradient(135deg, #3b82f6, #8b5cf6, #10b981) 1;animation:wt-reading-pulse 3.5s ease-in-out forwards;';
1526
+ __wt_safeAppendToBody(el);
1527
+ __wt_readingGlow = el;
1528
+ setTimeout(__wt_hideReadingGlow, 3500);
1529
+ }
1530
+
1531
+ function __wt_hideReadingGlow() {
1532
+ if (__wt_readingGlow) { __wt_readingGlow.remove(); __wt_readingGlow = null; }
1533
+ }
1534
+
1535
+ // ── SEMANTIC DOM MAP ────────────────────────────────────────────
1536
+ // Architecture change: Passive/extension-side ID assignment with live index
1537
+ // The MutationObserver continuously assigns data-wt-id to interactive elements
1538
+ // and maintains a live index that CDP can read lazily
1539
+
1540
+ // Live index - always up-to-date, accessible via window.__wt_liveIndex
1541
+ window.__wt_liveIndex = [];
1542
+ window.__wt_nextId = 1;
1543
+
1544
+ // IntersectionObserver for real occlusion detection (not just display:none)
1545
+ const intersectionObserver = new IntersectionObserver((entries) => {
1546
+ for (const entry of entries) {
1547
+ const el = entry.target;
1548
+ const id = el.getAttribute('data-wt-id');
1549
+ if (id) {
1550
+ const indexEntry = window.__wt_liveIndex.find(e => e.id === id);
1551
+ if (indexEntry) {
1552
+ indexEntry.isVisible = entry.isIntersecting;
1553
+ indexEntry.intersectionRatio = entry.intersectionRatio;
1554
+ }
1555
+ }
1556
+ }
1557
+ }, { threshold: [0, 0.25, 0.5, 0.75, 1.0] });
1558
+
1559
+ // Function to assign data-wt-id to an element and add to live index
1560
+ function assignIdAndIndex(el) {
1561
+ if (el.hasAttribute('data-wt-ignore')) return;
1562
+ if (el.hasAttribute('data-wt-id')) return; // Already assigned
1563
+
1564
+ const id = String(window.__wt_nextId++);
1565
+ el.setAttribute('data-wt-id', id);
1566
+ el.__wt_id = id;
1567
+
1568
+ const rect = el.getBoundingClientRect();
1569
+ const inViewport = (
1570
+ rect.bottom > 0 && rect.right > 0 &&
1571
+ rect.top < window.innerHeight && rect.left < window.innerWidth
1572
+ );
1573
+ const viewportPos = inViewport ? "visible" : (rect.top >= window.innerHeight ? "below" : "above");
1574
+
1575
+ // Check for event listeners
1576
+ const hasClickListener = el.onclick !== null ||
1577
+ el.addEventListener !== undefined &&
1578
+ (el._listeners?.click?.length > 0);
1579
+
1580
+ const indexEntry = {
1581
+ id: id,
1582
+ tag: el.tagName.toLowerCase(),
1583
+ role: el.getAttribute('role') || el.tagName.toLowerCase(),
1584
+ label: el.getAttribute('aria-label') || el.textContent?.trim().slice(0, 60) || '',
1585
+ placeholder: el.getAttribute('placeholder') || '',
1586
+ selector: getStableSelector(el),
1587
+ x: Math.round(rect.left + rect.width / 2),
1588
+ y: Math.round(rect.top + rect.height / 2),
1589
+ viewport: viewportPos,
1590
+ href: el.href || null,
1591
+ isVisible: inViewport,
1592
+ intersectionRatio: inViewport ? 1.0 : 0.0,
1593
+ hasClickListener: hasClickListener,
1594
+ rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
1595
+ };
1596
+
1597
+ window.__wt_liveIndex.push(indexEntry);
1598
+
1599
+ // Observe for intersection changes
1600
+ try {
1601
+ intersectionObserver.observe(el);
1602
+ } catch (e) {
1603
+ // Element might not be observable
1604
+ }
1605
+ }
1606
+
1607
+ // Function to update an existing entry in the live index
1608
+ function updateIndexEntry(el) {
1609
+ const id = el.getAttribute('data-wt-id');
1610
+ if (!id) return;
1611
+
1612
+ const indexEntry = window.__wt_liveIndex.find(e => e.id === id);
1613
+ if (!indexEntry) return;
1614
+
1615
+ const rect = el.getBoundingClientRect();
1616
+ const inViewport = (
1617
+ rect.bottom > 0 && rect.right > 0 &&
1618
+ rect.top < window.innerHeight && rect.left < window.innerWidth
1619
+ );
1620
+ const viewportPos = inViewport ? "visible" : (rect.top >= window.innerHeight ? "below" : "above");
1621
+
1622
+ indexEntry.x = Math.round(rect.left + rect.width / 2);
1623
+ indexEntry.y = Math.round(rect.top + rect.height / 2);
1624
+ indexEntry.viewport = viewportPos;
1625
+ indexEntry.isVisible = inViewport;
1626
+ indexEntry.rect = { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
1627
+ indexEntry.label = el.getAttribute('aria-label') || el.textContent?.trim().slice(0, 60) || '';
1628
+ }
1629
+
1630
+ // Function to remove an entry from the live index
1631
+ function removeIndexEntry(el) {
1632
+ const id = el.getAttribute('data-wt-id');
1633
+ if (!id) return;
1634
+
1635
+ const index = window.__wt_liveIndex.findIndex(e => e.id === id);
1636
+ if (index !== -1) {
1637
+ window.__wt_liveIndex.splice(index, 1);
1638
+ }
1639
+
1640
+ try {
1641
+ intersectionObserver.unobserve(el);
1642
+ } catch (e) {
1643
+ // Element might not be observable
1644
+ }
1645
+ }
1646
+
1647
+ // MutationObserver for continuous ID assignment and index maintenance
1648
+ const idAssignmentObserver = new MutationObserver((mutations) => {
1649
+ for (const mutation of mutations) {
1650
+ // Handle added nodes
1651
+ for (const node of mutation.addedNodes) {
1652
+ if (node.nodeType === Node.ELEMENT_NODE) {
1653
+ // Check if the node itself is interactive
1654
+ const interactiveSelector = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
1655
+ if (node.matches && node.matches(interactiveSelector)) {
1656
+ assignIdAndIndex(node);
1657
+ }
1658
+ // Check descendants
1659
+ const descendants = node.querySelectorAll ? node.querySelectorAll(interactiveSelector) : [];
1660
+ for (const desc of descendants) {
1661
+ assignIdAndIndex(desc);
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ // Handle removed nodes
1667
+ for (const node of mutation.removedNodes) {
1668
+ if (node.nodeType === Node.ELEMENT_NODE) {
1669
+ if (node.getAttribute && node.getAttribute('data-wt-id')) {
1670
+ removeIndexEntry(node);
1671
+ }
1672
+ const descendants = node.querySelectorAll ? node.querySelectorAll('[data-wt-id]') : [];
1673
+ for (const desc of descendants) {
1674
+ removeIndexEntry(desc);
1675
+ }
1676
+ }
1677
+ }
1678
+
1679
+ // Handle attribute changes (position, visibility, etc.)
1680
+ if (mutation.type === 'attributes' && mutation.target.getAttribute) {
1681
+ const target = mutation.target;
1682
+ if (target.getAttribute('data-wt-id')) {
1683
+ updateIndexEntry(target);
1684
+ }
1685
+ }
1686
+ }
1687
+ });
1688
+
1689
+ // Start observing when DOM is ready
1690
+ function startIdAssignment() {
1691
+ const interactiveSelector = 'a, button, input, select, textarea, [role="button"], [role="link"], [role="textbox"], [role="checkbox"], [role="radio"], [role="combobox"], [role="searchbox"], [role="menuitem"], [role="tab"], [tabindex]:not([tabindex="-1"])';
1692
+
1693
+ // Assign IDs to existing interactive elements
1694
+ const existing = document.querySelectorAll(interactiveSelector);
1695
+ for (const el of existing) {
1696
+ assignIdAndIndex(el);
1697
+ }
1698
+
1699
+ // Start observing for changes
1700
+ if (document.body) {
1701
+ idAssignmentObserver.observe(document.body, {
1702
+ childList: true,
1703
+ subtree: true,
1704
+ attributes: true,
1705
+ attributeFilter: ['style', 'class', 'hidden', 'aria-hidden']
1706
+ });
1707
+ } else {
1708
+ document.addEventListener('DOMContentLoaded', () => {
1709
+ const existing = document.querySelectorAll(interactiveSelector);
1710
+ for (const el of existing) {
1711
+ assignIdAndIndex(el);
1712
+ }
1713
+ idAssignmentObserver.observe(document.body, {
1714
+ childList: true,
1715
+ subtree: true,
1716
+ attributes: true,
1717
+ attributeFilter: ['style', 'class', 'hidden', 'aria-hidden']
1718
+ });
1719
+ });
1720
+ }
1721
+ }
1722
+
1723
+ // Start the ID assignment system
1724
+ if (document.readyState === 'loading') {
1725
+ document.addEventListener('DOMContentLoaded', startIdAssignment);
1726
+ } else {
1727
+ startIdAssignment();
1728
+ }
1729
+
1730
+ // Legacy buildSemanticMap function for backward compatibility
1731
+ // Now reads from the live index instead of rebuilding
1732
+ let cachedMap = null;
1733
+ const legacyObserver = new MutationObserver(() => {
1734
+ cachedMap = null;
1735
+ });
1736
+ // FIX: document.body may be null at document_start; defer observe until body exists
1737
+ if (document.body) {
1738
+ legacyObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
1739
+ } else {
1740
+ document.addEventListener('DOMContentLoaded', function() {
1741
+ if (document.body) legacyObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
1742
+ }, { once: true });
1743
+ }
1744
+
1745
+ function buildSemanticMap() {
1746
+ if (cachedMap) return cachedMap;
1747
+
1748
+ // Read from the live index maintained by the MutationObserver
1749
+ cachedMap = window.__wt_liveIndex.map(entry => ({
1750
+ id: entry.id,
1751
+ tag: entry.tag,
1752
+ role: entry.role,
1753
+ label: entry.label,
1754
+ placeholder: entry.placeholder,
1755
+ selector: entry.selector,
1756
+ x: entry.x,
1757
+ y: entry.y,
1758
+ viewport: entry.viewport,
1759
+ href: entry.href,
1760
+ coords: { x: entry.x, y: entry.y }
1761
+ }));
1762
+
1763
+ return cachedMap;
1764
+ }
1765
+
1766
+ // ── HUD ─────────────────────────────────────────────────────────
1767
+ const hudHistory = [];
1768
+ const hud = (() => {
1769
+ const el = document.createElement('div');
1770
+ el.id = 'Weavetab-hud';
1771
+ el.setAttribute('data-wt-ignore', '');
1772
+ Object.assign(el.style, {
1773
+ position: 'fixed', bottom: '24px', right: '24px',
1774
+ transform: 'translateY(8px)',
1775
+ color: '#a1a1aa',
1776
+ font: '11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
1777
+ borderRadius: '8px',
1778
+ opacity: '0', pointerEvents: 'none',
1779
+ zIndex: '2147483640',
1780
+ maxWidth: '400px',
1781
+ display: 'flex', flexDirection: 'column-reverse', gap: '4px',
1782
+ transition: 'opacity 0.2s, transform 0.2s',
1783
+ textAlign: 'right',
1784
+ });
1785
+ __wt_safeAppendToBody(el);
1786
+ return el;
1787
+ })();
1788
+
1789
+ function updateHUD(action, detail) {
1790
+ if (window.self !== window.top) return;
1791
+ // Tool call entries display strictly inside the Sidepanel TOOL CALLS section.
1792
+ // Suppress on-page bottom-right HUD popup overlay.
1793
+ if (hud) {
1794
+ hud.style.display = 'none';
1795
+ hud.style.opacity = '0';
1796
+ }
1797
+ }
1798
+
1799
+ // ── ACTIVE TAB INDICATOR ─────────────────────────────────────────
1800
+ class ControlledTabIndicator {
1801
+ constructor() {
1802
+ this._el = document.createElement('div');
1803
+ this._el.id = '__Weavetab_controlled_indicator';
1804
+ this._el.setAttribute('data-wt-ignore', '');
1805
+ Object.assign(this._el.style, {
1806
+ position: 'fixed',
1807
+ top: '0',
1808
+ left: '0',
1809
+ right: '0',
1810
+ height: '3px',
1811
+ zIndex: '2147483646',
1812
+ background: '#3B82F6',
1813
+ boxShadow: '0 0 12px #3B82F6, 0 0 4px #60a5fa',
1814
+ opacity: '0',
1815
+ transition: 'opacity 0.25s ease-in-out',
1816
+ pointerEvents: 'none',
1817
+ });
1818
+ // Animated shimmer
1819
+ this._shimmer = document.createElement('div');
1820
+ Object.assign(this._shimmer.style, {
1821
+ position: 'absolute',
1822
+ top: '0', left: '-60%', width: '60%', height: '100%',
1823
+ background: 'linear-gradient(90deg,transparent,rgba(255,255,255,0.45),transparent)',
1824
+ animation: 'wt-shimmer 1.6s infinite',
1825
+ });
1826
+ // Inject animation keyframes into shadow root
1827
+ var style = document.createElement('style');
1828
+ style.textContent = '@keyframes wt-shimmer{0%{left:-60%}100%{left:120%}}';
1829
+ try { getWeavetabRoot().appendChild(style); } catch(e) {}
1830
+ this._el.appendChild(this._shimmer);
1831
+ getWeavetabRoot().appendChild(this._el);
1832
+ this._hideTimer = null;
1833
+ this._persistent = false;
1834
+ }
1835
+
1836
+ // Show persistently (while agent is weaving)
1837
+ showPersistent(color) {
1838
+ this._persistent = true;
1839
+ clearTimeout(this._hideTimer);
1840
+ if (color) {
1841
+ this._el.style.background = color;
1842
+ this._el.style.boxShadow = '0 0 12px ' + color + ', 0 0 4px ' + color;
1843
+ }
1844
+ this._el.style.opacity = '1';
1845
+ this._shimmer.style.display = 'block';
1846
+ }
1847
+
1848
+ // Show briefly (1.5s flash for single actions)
1849
+ show(color) {
1850
+ if (this._persistent) return; // Don't override persistent mode
1851
+ if (color) this._el.style.background = color;
1852
+ this._el.style.opacity = '1';
1853
+ clearTimeout(this._hideTimer);
1854
+ this._hideTimer = setTimeout(() => this.hide(), 1500);
1855
+ }
1856
+
1857
+ hide() {
1858
+ this._persistent = false;
1859
+ this._el.style.opacity = '0';
1860
+ this._shimmer.style.display = 'none';
1861
+ }
1862
+
1863
+ destroy() {
1864
+ clearTimeout(this._hideTimer);
1865
+ this._el.remove();
1866
+ }
1867
+ }
1868
+
1869
+ const controlledIndicator = new ControlledTabIndicator();
1870
+
1871
+ // ── MESSAGE BRIDGE ──────────────────────────────────────────────
1872
+ // Shared handler for both CustomEvent (MAIN world dispatch) and
1873
+ // postMessage (cross-world: MAIN → ISOLATED) communication channels.
1874
+ var __wt_lastCursor = { x: -9999, y: -9999 };
1875
+ var __wt_processedMsgIds = new Set();
1876
+
1877
+ function __wt_handleWeavetabMsg(msg) {
1878
+ if (!msg) return;
1879
+ if (Array.isArray(msg)) { msg.forEach(m => __wt_handleWeavetabMsg(m)); return; }
1880
+ if (msg._id) { if (__wt_processedMsgIds.has(msg._id)) return; __wt_processedMsgIds.add(msg._id); if (__wt_processedMsgIds.size > 100) __wt_processedMsgIds.clear(); }
1881
+
1882
+ if (msg.type === 'Weavetab_STATE') {
1883
+ if (msg.cursor) {
1884
+ __wt_lastCursor = { x: msg.cursor.x, y: msg.cursor.y };
1885
+ if (msg.cursor.waypoints && msg.cursor.duration_ms) {
1886
+ __weaveCursor.setWaypoints(msg.cursor.waypoints, msg.cursor.duration_ms);
1887
+ }
1888
+ moveCursor(msg.cursor.x, msg.cursor.y, msg.state);
1889
+ }
1890
+ else if (msg.state) { moveCursor(__wt_lastCursor.x, __wt_lastCursor.y, msg.state); }
1891
+ if (msg.ripple) ripple(msg.ripple.x, msg.ripple.y);
1892
+
1893
+ if (msg.userFeedback === true && msg.feedbackText) {
1894
+ __wt_feedbackSticky = true;
1895
+ clearTimeout(window.__wt_feedbackStickyTimer);
1896
+ window.__wt_feedbackStickyTimer = setTimeout(function(){ __wt_feedbackSticky = false; }, 6000);
1897
+ } else if (msg.userFeedback === false) {
1898
+ setTimeout(function(){ __wt_feedbackSticky = false; }, 2000);
1899
+ }
1900
+
1901
+ if (msg.hud) {
1902
+ updateHUD(msg.hud.action, msg.hud.detail);
1903
+ var a = (msg.hud.action || '').trim();
1904
+ if (a === 'reading the page...') {
1905
+ __weaveCursor.setState('reading');
1906
+ } else if (a === 'Mission Running') {
1907
+ __wt_toolActive = true;
1908
+ __weaveCursor.setState('eval');
1909
+ } else if (a.startsWith('[OK]')) {
1910
+ __weaveCursor.setState('idle');
1911
+ __wt_toolActive = false;
1912
+ } else if (a.startsWith('[ERR]')) {
1913
+ __weaveCursor.setState('stuck');
1914
+ __wt_toolActive = false;
1915
+ setTimeout(function(){ __weaveCursor.setState('idle'); }, 3000);
1916
+ }
1917
+ }
1918
+ if (msg.cursor || msg.hud) {
1919
+ controlledIndicator.show('#3B82F6');
1920
+ }
1921
+ }
1922
+
1923
+ // Pure thought event from browser_thoughts tool only
1924
+ if (msg.type === 'WT_THOUGHT' || msg.type === 'Weavetab_THOUGHT') {
1925
+ __wt_showThought(msg.text, msg);
1926
+ }
1927
+ if (msg.type === 'WT_CLEAR_THOUGHT' || msg.type === 'Weavetab_CLEAR_THOUGHT') {
1928
+ __wt_removeThought();
1929
+ }
1930
+
1931
+ // Handle plan updates via sendToExtension channel
1932
+ if (msg.type === 'Weavetab_STATE' && msg.plan) {
1933
+ const p = msg.plan;
1934
+ if (p.action === 'hide') {
1935
+ if (window.__wt_plan) window.__wt_plan('hide', '', []);
1936
+ } else {
1937
+ if (window.__wt_plan) window.__wt_plan(p.action || 'create', p.goal || '', p.tasks || []);
1938
+ }
1939
+ }
1940
+
1941
+
1942
+ if (msg.type === 'WT_USER_NOTE_INJECT') {
1943
+ window.__wt_user_notes = window.__wt_user_notes || [];
1944
+ window.__wt_user_notes.push({ text: msg.text, time: Date.now() });
1945
+ }
1946
+
1947
+ if (msg.type === 'GET_SEMANTIC_MAP') {
1948
+ const map = window.__Weavetab_fullMap();
1949
+ if (window.__wt_user_notes && window.__wt_user_notes.length > 0) {
1950
+ // Keep notes for 2 minutes
1951
+ window.__wt_user_notes = window.__wt_user_notes.filter(n => Date.now() - n.time < 120000);
1952
+ const notesMap = window.__wt_user_notes.map((note, i) => ({
1953
+ id: `user-msg-${i}`,
1954
+ role: 'alert',
1955
+ tag: 'div',
1956
+ text: `URGENT MESSAGE FROM USER: "${note.text}"`,
1957
+ coords: [0, 0, 100, 100],
1958
+ interactive: false
1959
+ }));
1960
+ map.unshift(...notesMap);
1961
+ }
1962
+ activateXRay(map.filter(m => m.coords && m.text).slice(0, 12));
1963
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: map } }));
1964
+ }
1965
+
1966
+ if (msg.type === 'RESOLVE_EXTENSION_URL') {
1967
+ var url = '';
1968
+ try { url = browserAPI.runtime.getURL((msg.page || 'dashboard') + '.html'); } catch(e) {}
1969
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: url } }));
1970
+ }
1971
+
1972
+ if (msg.type === 'Weavetab_CONFIG') {
1973
+ window.__wt_devMode = !!msg.devMode;
1974
+ window.dispatchEvent(new CustomEvent('wt-config-sync'));
1975
+ __wt_safeSendMessage(msg);
1976
+ }
1977
+
1978
+ if (msg.type === 'Weavetab_GROUP_STATE') {
1979
+ __wt_safeSendMessage(msg);
1980
+ // Only ever blue dot — no yellow. Idle state = no dot.
1981
+ const dotColor = (msg.state === 'idle') ? null : '#3B82F6';
1982
+ if (dotColor) {
1983
+ controlledIndicator.show(dotColor);
1984
+ }
1985
+ updateFaviconDot(msg.state, msg.isActive);
1986
+ // Parked tabs: clear thought bubble and reading glow
1987
+ if (msg.isActive === false) {
1988
+ __wt_removeThought();
1989
+ __wt_hideReadingGlow();
1990
+ }
1991
+ if (msg.state === 'idle') {
1992
+ __wt_hideReadingGlow();
1993
+ __wt_destroyBubble();
1994
+ __wt_feedbackSticky = false;
1995
+ __wt_processThoughtQueue();
1996
+ restoreOriginalFavicon();
1997
+ // Hide the persistent indicator bar when idle
1998
+ controlledIndicator.hide();
1999
+ __weaveCursor.setState('idle');
2000
+ } else if (msg.state === 'weaving') {
2001
+ // Show persistent blue bar while agent works
2002
+ controlledIndicator.showPersistent('#3B82F6');
2003
+ __weaveCursor.setState('eval');
2004
+ } else if (msg.state === 'stuck') {
2005
+ // Still show blue bar (not red) when stuck
2006
+ controlledIndicator.showPersistent('#3B82F6');
2007
+ __weaveCursor.setState('stuck');
2008
+ }
2009
+ }
2010
+
2011
+
2012
+ if (msg.type === 'Weavetab_THOUGHT') {
2013
+ __wt_showThought(msg.text, { active: msg.active });
2014
+ }
2015
+
2016
+ if (msg.type === 'Weavetab_ASK') {
2017
+ // Legacy support — route to new unified question system
2018
+ console.log('[Weavetab] Received ASK request (legacy):', msg.question);
2019
+ __wt_showQuestion(msg.question, 'sentence', msg.timeout || 120);
2020
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: { status: 'shown' } } }));
2021
+ }
2022
+
2023
+ if (msg.type === 'Weavetab_CLEAR_THOUGHT') {
2024
+ __wt_removeThought();
2025
+ __wt_hideReadingGlow();
2026
+ __wt_processThoughtQueue();
2027
+ }
2028
+
2029
+ if (msg.type === 'Weavetab_RELOAD_EXTENSION') {
2030
+ __wt_safeSendMessage({ type: 'RELOAD_EXTENSION' });
2031
+ }
2032
+ }
2033
+
2034
+ // Channel 1: CustomEvent from MAIN world (used by fallback + sendToExtension)
2035
+ window.addEventListener('Weavetab-msg-in', (e) => {
2036
+ __wt_handleWeavetabMsg(e.detail);
2037
+ });
2038
+
2039
+ // Channel 2: postMessage from MAIN world (cross-world MV3 bridge)
2040
+ window.addEventListener('message', (event) => {
2041
+ if (event.data && event.data.type === 'Weavetab-msg-in' && event.data.data) {
2042
+ __wt_handleWeavetabMsg(event.data.data);
2043
+ }
2044
+ // ── User feedback relay: WeaveTab OS panel → background storage queue ────
2045
+ // When the user types in the chat panel and hits Send, submitFeedback() fires
2046
+ // a __WT_FEEDBACK postMessage. We relay it to the background so it lands in
2047
+ // chrome.storage.local and window.__Weavetab.popUserFeedback() can return it.
2048
+ if (event.data && event.data.type === '__WT_FEEDBACK' && event.data.text) {
2049
+ try {
2050
+ if (browserAPI.runtime && browserAPI.runtime.id) {
2051
+ browserAPI.runtime.sendMessage({ type: 'USER_FEEDBACK', text: event.data.text }).catch(function(){});
2052
+ }
2053
+ } catch(e) {}
2054
+ }
2055
+ });
2056
+
2057
+ // ── FAVICON STATUS DOT INDICATOR ───────────────────────────────
2058
+ let wtFaviconObserver = null;
2059
+
2060
+ let __wt_faviconStaleTimer = null;
2061
+
2062
+ function updateFaviconDot(state, isActive) {
2063
+ if (__wt_faviconStaleTimer) { clearTimeout(__wt_faviconStaleTimer); __wt_faviconStaleTimer = null; }
2064
+
2065
+ if (state === 'idle' || !state || isActive === false) {
2066
+ if (wtFaviconObserver) {
2067
+ wtFaviconObserver.disconnect();
2068
+ wtFaviconObserver = null;
2069
+ }
2070
+ restoreOriginalFavicon();
2071
+ return;
2072
+ }
2073
+
2074
+ // Always use blue — never red or yellow for favicon dot
2075
+ const color = '#3B82F6';
2076
+
2077
+ if (!wtFaviconObserver) {
2078
+ wtFaviconObserver = new MutationObserver(() => {
2079
+ wtFaviconObserver.disconnect();
2080
+ applyFaviconDot(color);
2081
+ wtFaviconObserver.observe(document.head, { childList: true, subtree: true });
2082
+ });
2083
+ } else {
2084
+ wtFaviconObserver.disconnect();
2085
+ }
2086
+
2087
+ applyFaviconDot(color);
2088
+ wtFaviconObserver.observe(document.head, { childList: true, subtree: true });
2089
+
2090
+ // Set stale timer: clear dot + hide glow if no message for 30s
2091
+ __wt_faviconStaleTimer = setTimeout(function() {
2092
+ restoreOriginalFavicon();
2093
+ __wt_hideReadingGlow();
2094
+ __wt_destroyBubble();
2095
+ }, 30000);
2096
+ }
2097
+
2098
+ function restoreOriginalFavicon() {
2099
+ const customLink = document.getElementById('wt-custom-favicon');
2100
+ if (customLink) customLink.remove();
2101
+
2102
+ const links = document.querySelectorAll('link[rel*="icon"]');
2103
+ links.forEach(link => {
2104
+ if (link.dataset.wtOrigHref) {
2105
+ link.href = link.dataset.wtOrigHref;
2106
+ delete link.dataset.wtOrigHref;
2107
+ }
2108
+ });
2109
+ }
2110
+
2111
+ function applyFaviconDot(color) {
2112
+ const links = Array.from(document.querySelectorAll('link[rel*="icon"]')).filter(l => l.id !== 'wt-custom-favicon');
2113
+ let originalHref = '';
2114
+
2115
+ if (links.length > 0) {
2116
+ const link = links[0];
2117
+ if (!link.dataset.wtOrigHref) {
2118
+ link.dataset.wtOrigHref = link.href;
2119
+ }
2120
+ originalHref = link.dataset.wtOrigHref;
2121
+ } else {
2122
+ originalHref = '/favicon.ico';
2123
+ }
2124
+
2125
+ const canvas = document.createElement('canvas');
2126
+ canvas.width = 32;
2127
+ canvas.height = 32;
2128
+ const ctx = canvas.getContext('2d');
2129
+
2130
+ const img = new Image();
2131
+ img.crossOrigin = 'anonymous';
2132
+ img.onload = () => {
2133
+ try {
2134
+ ctx.drawImage(img, 0, 0, 32, 32);
2135
+ drawDot(ctx, color);
2136
+ setFaviconHref(canvas.toDataURL('image/png'));
2137
+ } catch (e) {
2138
+ drawFallbackIcon(ctx, color);
2139
+ setFaviconHref(canvas.toDataURL('image/png'));
2140
+ }
2141
+ };
2142
+ img.onerror = () => {
2143
+ drawFallbackIcon(ctx, color);
2144
+ setFaviconHref(canvas.toDataURL('image/png'));
2145
+ };
2146
+ img.src = originalHref;
2147
+ }
2148
+
2149
+ function drawDot(ctx, color) {
2150
+ // Solid dot
2151
+ ctx.beginPath();
2152
+ ctx.arc(24, 24, 5.5, 0, 2 * Math.PI);
2153
+ ctx.fillStyle = color;
2154
+ ctx.fill();
2155
+ }
2156
+
2157
+ function drawFallbackIcon(ctx, color) {
2158
+ ctx.clearRect(0, 0, 32, 32);
2159
+ ctx.beginPath();
2160
+ ctx.arc(16, 16, 11, 0, 2 * Math.PI);
2161
+ ctx.fillStyle = '#1E1F22';
2162
+ ctx.fill();
2163
+ ctx.strokeStyle = '#3C3F45';
2164
+ ctx.lineWidth = 1.5;
2165
+ ctx.stroke();
2166
+
2167
+ ctx.beginPath();
2168
+ ctx.arc(16, 16, 6, 0, 2 * Math.PI);
2169
+ ctx.fillStyle = '#7C3AED';
2170
+ ctx.fill();
2171
+
2172
+ drawDot(ctx, color);
2173
+ }
2174
+
2175
+ function setFaviconHref(dataUrl) {
2176
+ let customLink = document.getElementById('wt-custom-favicon');
2177
+ if (!customLink) {
2178
+ customLink = document.createElement('link');
2179
+ customLink.id = 'wt-custom-favicon';
2180
+ customLink.rel = 'icon';
2181
+ customLink.type = 'image/png';
2182
+ (document.head || document.documentElement).appendChild(customLink);
2183
+ }
2184
+ customLink.href = dataUrl;
2185
+
2186
+ const links = document.querySelectorAll('link[rel*="icon"]');
2187
+ links.forEach(link => {
2188
+ if (link.id !== 'wt-custom-favicon') {
2189
+ if (!link.dataset.wtOrigHref) {
2190
+ link.dataset.wtOrigHref = link.href;
2191
+ }
2192
+ link.href = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
2193
+ }
2194
+ });
2195
+ }
2196
+
2197
+
2198
+
2199
+ // ── SIDE PANEL REMOVED (Replaced by pure middle-down Thought HUD) ──
2200
+
2201
+ (function initAreaSnipeEngine() {
2202
+ let isSnipeActive = false;
2203
+ let currentSnipeMode = 'dom_target';
2204
+ let overlayEl = null;
2205
+ let marqueeBox = null;
2206
+ let sizeBadge = null;
2207
+ let headerPill = null;
2208
+ let startX = 0, startY = 0;
2209
+ let isDragging = false;
2210
+
2211
+ function createOverlay(mode) {
2212
+ if (overlayEl) overlayEl.remove();
2213
+ currentSnipeMode = mode || 'dom_target';
2214
+
2215
+ overlayEl = document.createElement('div');
2216
+ overlayEl.id = 'wt-snipe-overlay';
2217
+ overlayEl.setAttribute('data-wt-ignore', 'true');
2218
+ overlayEl.style.cssText = `
2219
+ position: fixed; inset: 0; z-index: 2147483647;
2220
+ background: rgba(0, 0, 0, 0.35); backdrop-filter: blur(2px);
2221
+ cursor: crosshair; user-select: none;
2222
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2223
+ `;
2224
+
2225
+ headerPill = document.createElement('div');
2226
+ headerPill.style.cssText = `
2227
+ position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
2228
+ background: #18181b; border: 1px solid #3f3f46; color: #f4f4f5;
2229
+ font-size: 11px; font-weight: 500; padding: 6px 16px; border-radius: 20px;
2230
+ box-shadow: 0 4px 20px rgba(0,0,0,0.6); pointer-events: none;
2231
+ display: flex; align-items: center; gap: 8px; z-index: 2147483647;
2232
+ `;
2233
+ const modeLabel = currentSnipeMode === 'visual_crop' ? 'Vision Crop' : 'DOM Target';
2234
+ headerPill.innerHTML = `<span style="display:inline-flex;align-items:center;gap:6px;"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#38bdf8" stroke-width="2"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><rect x="7" y="7" width="10" height="10" rx="1"/></svg>Drag box to select area</span> <span style="color:#06b6d4;font-weight:600;">[${modeLabel}]</span> <span style="color:#71717a;">(Esc to cancel)</span>`;
2235
+
2236
+ // Marquee selection box matching img1 Gemini/YouTube Chrome UI style
2237
+ marqueeBox = document.createElement('div');
2238
+ marqueeBox.style.cssText = `
2239
+ position: fixed; border: 1px solid rgba(255, 255, 255, 0.45);
2240
+ border-radius: 12px;
2241
+ box-shadow: 0 0 24px 3px rgba(147, 197, 253, 0.45), inset 0 0 20px rgba(147, 197, 253, 0.2);
2242
+ pointer-events: auto; display: none; z-index: 2147483647;
2243
+ background: rgba(0, 0, 0, 0.1);
2244
+ `;
2245
+
2246
+ // Four thick white rounded corner reticles (matching img1)
2247
+ const corners = [
2248
+ 'top: -2px; left: -2px; border-top: 4px solid #ffffff; border-left: 4px solid #ffffff; border-top-left-radius: 10px;',
2249
+ 'top: -2px; right: -2px; border-top: 4px solid #ffffff; border-right: 4px solid #ffffff; border-top-right-radius: 10px;',
2250
+ 'bottom: -2px; left: -2px; border-bottom: 4px solid #ffffff; border-left: 4px solid #ffffff; border-bottom-left-radius: 10px;',
2251
+ 'bottom: -2px; right: -2px; border-bottom: 4px solid #ffffff; border-right: 4px solid #ffffff; border-bottom-right-radius: 10px;'
2252
+ ];
2253
+ corners.forEach(styleCss => {
2254
+ const c = document.createElement('div');
2255
+ c.style.cssText = `position: absolute; width: 18px; height: 18px; pointer-events: none; ${styleCss}`;
2256
+ marqueeBox.appendChild(c);
2257
+ });
2258
+
2259
+ // Circular close X button on top-right corner
2260
+ const closeBtn = document.createElement('button');
2261
+ closeBtn.style.cssText = `
2262
+ position: absolute; top: -11px; right: -11px;
2263
+ width: 22px; height: 22px; border-radius: 50%;
2264
+ background: rgba(24, 24, 27, 0.95); border: 1px solid rgba(255, 255, 255, 0.25);
2265
+ color: #f4f4f5; cursor: pointer; display: flex; align-items: center; justify-content: center;
2266
+ box-shadow: 0 4px 12px rgba(0,0,0,0.6); z-index: 2147483647; transition: transform 0.15s, background 0.15s;
2267
+ `;
2268
+ closeBtn.innerHTML = `<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
2269
+ closeBtn.onclick = function(e) {
2270
+ e.stopPropagation();
2271
+ destroyOverlay();
2272
+ };
2273
+ marqueeBox.appendChild(closeBtn);
2274
+
2275
+ sizeBadge = document.createElement('div');
2276
+ sizeBadge.style.cssText = `
2277
+ position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%);
2278
+ background: rgba(15, 23, 42, 0.9); backdrop-filter: blur(8px); color: #e2e8f0; font-weight: 600;
2279
+ font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 2px 8px; border-radius: 12px;
2280
+ border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none;
2281
+ white-space: nowrap;
2282
+ `;
2283
+ marqueeBox.appendChild(sizeBadge);
2284
+
2285
+ overlayEl.appendChild(headerPill);
2286
+ overlayEl.appendChild(marqueeBox);
2287
+ document.documentElement.appendChild(overlayEl);
2288
+
2289
+ isSnipeActive = true;
2290
+ }
2291
+
2292
+ function destroyOverlay() {
2293
+ if (overlayEl) {
2294
+ overlayEl.remove();
2295
+ overlayEl = null;
2296
+ }
2297
+ isSnipeActive = false;
2298
+ isDragging = false;
2299
+ }
2300
+
2301
+ function handleMouseDown(e) {
2302
+ if (!isSnipeActive || e.button !== 0) return;
2303
+ e.preventDefault();
2304
+ e.stopPropagation();
2305
+
2306
+ isDragging = true;
2307
+ startX = e.clientX;
2308
+ startY = e.clientY;
2309
+
2310
+ marqueeBox.style.left = startX + 'px';
2311
+ marqueeBox.style.top = startY + 'px';
2312
+ marqueeBox.style.width = '0px';
2313
+ marqueeBox.style.height = '0px';
2314
+ marqueeBox.style.display = 'block';
2315
+ }
2316
+
2317
+ function handleMouseMove(e) {
2318
+ if (!isSnipeActive || !isDragging) return;
2319
+ e.preventDefault();
2320
+ e.stopPropagation();
2321
+
2322
+ const currentX = e.clientX;
2323
+ const currentY = e.clientY;
2324
+
2325
+ const left = Math.min(startX, currentX);
2326
+ const top = Math.min(startY, currentY);
2327
+ const width = Math.abs(currentX - startX);
2328
+ const height = Math.abs(currentY - startY);
2329
+
2330
+ marqueeBox.style.left = left + 'px';
2331
+ marqueeBox.style.top = top + 'px';
2332
+ marqueeBox.style.width = width + 'px';
2333
+ marqueeBox.style.height = height + 'px';
2334
+
2335
+ sizeBadge.textContent = `${width} × ${height} px`;
2336
+ }
2337
+
2338
+ function handleMouseUp(e) {
2339
+ if (!isSnipeActive || !isDragging) return;
2340
+ e.preventDefault();
2341
+ e.stopPropagation();
2342
+
2343
+ isDragging = false;
2344
+ const currentX = e.clientX;
2345
+ const currentY = e.clientY;
2346
+
2347
+ const left = Math.min(startX, currentX);
2348
+ const top = Math.min(startY, currentY);
2349
+ const width = Math.abs(currentX - startX);
2350
+ const height = Math.abs(currentY - startY);
2351
+
2352
+ if (width < 10 || height < 10) {
2353
+ destroyOverlay();
2354
+ return;
2355
+ }
2356
+
2357
+ if (currentSnipeMode === 'visual_crop') {
2358
+ captureVisualCrop(left, top, width, height);
2359
+ } else {
2360
+ captureDOMTargets(left, top, width, height);
2361
+ }
2362
+
2363
+ destroyOverlay();
2364
+ }
2365
+
2366
+ function handleKeyDown(e) {
2367
+ if (e.key === 'Escape' && isSnipeActive) {
2368
+ destroyOverlay();
2369
+ }
2370
+ if (e.altKey && (e.key === 's' || e.key === 'S')) {
2371
+ e.preventDefault();
2372
+ createOverlay('dom_target');
2373
+ }
2374
+ }
2375
+
2376
+ window.addEventListener('mousedown', handleMouseDown, true);
2377
+ window.addEventListener('mousemove', handleMouseMove, true);
2378
+ window.addEventListener('mouseup', handleMouseUp, true);
2379
+ window.addEventListener('keydown', handleKeyDown, true);
2380
+
2381
+ // ── Mode 1: DOM Element Intersector ──────────────────────────────────────
2382
+ function captureDOMTargets(left, top, width, height) {
2383
+ const right = left + width;
2384
+ const bottom = top + height;
2385
+ const elements = [];
2386
+
2387
+ const allEls = document.querySelectorAll('button, input, select, textarea, a, h1, h2, h3, h4, h5, h6, p, table, tr, td, th, div, span, [role]');
2388
+ allEls.forEach((el) => {
2389
+ if (el.hasAttribute('data-wt-ignore') || el.closest('#wt-snipe-overlay')) return;
2390
+ const r = el.getBoundingClientRect();
2391
+ if (r.width === 0 || r.height === 0) return;
2392
+
2393
+ const overlaps = !(r.right < left || r.left > right || r.bottom < top || r.top > bottom);
2394
+ if (overlaps) {
2395
+ const text = (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0, 100);
2396
+ if (text || ['INPUT', 'BUTTON', 'A', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
2397
+ elements.push({
2398
+ tag: el.tagName.toLowerCase(),
2399
+ selector: getStableSelector(el),
2400
+ ref: el.getAttribute('data-wt-id') || null,
2401
+ text: text,
2402
+ role: el.getAttribute('role') || null,
2403
+ rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }
2404
+ });
2405
+ }
2406
+ }
2407
+ });
2408
+
2409
+ const uniqueElements = elements.slice(0, 15);
2410
+
2411
+ __wt_safeSendMessage({
2412
+ type: 'WT_SNIPE_COMPLETED',
2413
+ payload: {
2414
+ mode: 'dom_target',
2415
+ elements: uniqueElements,
2416
+ boundingRect: { x: left, y: top, width, height }
2417
+ }
2418
+ });
2419
+ }
2420
+
2421
+ // ── Mode 2: Visual WebP Crop ─────────────────────────────────────────────
2422
+ function captureVisualCrop(left, top, width, height) {
2423
+ try {
2424
+ const canvas = document.createElement('canvas');
2425
+ canvas.width = width;
2426
+ canvas.height = height;
2427
+ const ctx = canvas.getContext('2d');
2428
+
2429
+ const htmlContent = document.documentElement.outerHTML;
2430
+ const dataSvg = `
2431
+ <svg xmlns="http://www.w3.org/2000/svg" width="${window.innerWidth}" height="${window.innerHeight}">
2432
+ <foreignObject width="100%" height="100%">
2433
+ <div xmlns="http://www.w3.org/1999/xhtml">
2434
+ ${htmlContent}
2435
+ </div>
2436
+ </foreignObject>
2437
+ </svg>
2438
+ `;
2439
+
2440
+ const img = new Image();
2441
+ const blob = new Blob([dataSvg], { type: 'image/svg+xml;charset=utf-8' });
2442
+ const url = URL.createObjectURL(blob);
2443
+
2444
+ img.onload = function() {
2445
+ ctx.drawImage(img, left, top, width, height, 0, 0, width, height);
2446
+ URL.revokeObjectURL(url);
2447
+ const webpBase64 = canvas.toDataURL('image/webp', 0.75);
2448
+
2449
+ __wt_safeSendMessage({
2450
+ type: 'WT_SNIPE_COMPLETED',
2451
+ payload: {
2452
+ mode: 'visual_crop',
2453
+ cropImage: webpBase64,
2454
+ boundingRect: { x: left, y: top, width, height }
2455
+ }
2456
+ });
2457
+ };
2458
+ img.onerror = function() {
2459
+ captureDOMTargets(left, top, width, height);
2460
+ };
2461
+ img.src = url;
2462
+ } catch(e) {
2463
+ captureDOMTargets(left, top, width, height);
2464
+ }
2465
+ }
2466
+
2467
+ // Listen for snipe trigger from background
2468
+ browserAPI.runtime.onMessage.addListener((msg) => {
2469
+ if (msg.type === 'WT_START_SNIPE') {
2470
+ createOverlay(msg.mode || 'dom_target');
2471
+ }
2472
+ if (msg.type === 'WT_SNIPE_INJECT' && msg.entry) {
2473
+ const noteStr = typeof msg.entry === 'string' ? msg.entry : JSON.stringify(msg.entry);
2474
+ try {
2475
+ if (!window.__wt_chat_queue) window.__wt_chat_queue = [];
2476
+ window.__wt_chat_queue.push(noteStr);
2477
+ } catch(e) {}
2478
+
2479
+ try {
2480
+ const existing = JSON.parse(localStorage.getItem('__wt_chat_queue_persist') || '[]');
2481
+ existing.push(msg.entry);
2482
+ localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(existing));
2483
+ } catch(e) {}
2484
+
2485
+ const bridge = document.getElementById('__wt_ask_bridge');
2486
+ if (bridge) {
2487
+ bridge.dataset.status = 'submitted';
2488
+ bridge.dataset.result = msg.entry.text || 'Annotation submitted';
2489
+ }
2490
+ }
2491
+ });
2492
+
2493
+ })();
2494
+
2495
+ // ── ALERT / ACTION-REQUIRED TAB INDICATOR ─────────────────────
2496
+ (function initAlertDetector() {
2497
+ if (window.self !== window.top) return;
2498
+ // Expose on window so window.__Weavetab.getAlertState() can read it from CDP MAIN world
2499
+ window.__wt_alertPending = false;
2500
+ var _alert = window.alert;
2501
+ window.alert = function(msg) {
2502
+ window.__wt_alertPending = true;
2503
+ __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
2504
+ var r = _alert.apply(this, arguments);
2505
+ window.__wt_alertPending = false;
2506
+ __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
2507
+ return r;
2508
+ };
2509
+ var _confirm = window.confirm;
2510
+ window.confirm = function(msg) {
2511
+ window.__wt_alertPending = true;
2512
+ __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
2513
+ var r = _confirm.apply(this, arguments);
2514
+ window.__wt_alertPending = false;
2515
+ __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
2516
+ return r;
2517
+ };
2518
+ var _prompt = window.prompt;
2519
+ window.prompt = function(msg, def) {
2520
+ window.__wt_alertPending = true;
2521
+ __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
2522
+ var r = _prompt.apply(this, arguments);
2523
+ window.__wt_alertPending = false;
2524
+ __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
2525
+ return r;
2526
+ };
2527
+ })();