@weavetab/mcp 2.5.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/AGENTS.md +48 -0
  2. package/ARCHITECTURE.md +590 -0
  3. package/LICENSE +631 -0
  4. package/README.md +909 -0
  5. package/SECURITY.md +196 -0
  6. package/TROUBLESHOOTING.md +365 -0
  7. package/dist/anticaptcha/detector.d.ts +49 -0
  8. package/dist/anticaptcha/detector.js +465 -0
  9. package/dist/anticaptcha/integration.d.ts +36 -0
  10. package/dist/anticaptcha/integration.js +123 -0
  11. package/dist/anticaptcha/solver.d.ts +27 -0
  12. package/dist/anticaptcha/solver.js +552 -0
  13. package/dist/audit/logger.d.ts +13 -0
  14. package/dist/audit/logger.js +177 -0
  15. package/dist/audit/telemetry.d.ts +36 -0
  16. package/dist/audit/telemetry.js +162 -0
  17. package/dist/cdp/bridge.d.ts +16 -0
  18. package/dist/cdp/bridge.js +826 -0
  19. package/dist/cdp/confirm.d.ts +26 -0
  20. package/dist/cdp/confirm.js +79 -0
  21. package/dist/cdp/connector.d.ts +37 -0
  22. package/dist/cdp/connector.js +661 -0
  23. package/dist/cdp/extension.d.ts +36 -0
  24. package/dist/cdp/extension.js +118 -0
  25. package/dist/cdp/finder.d.ts +1 -0
  26. package/dist/cdp/finder.js +2 -0
  27. package/dist/cdp/helpers.d.ts +11 -0
  28. package/dist/cdp/helpers.js +195 -0
  29. package/dist/cdp/label_cache.d.ts +12 -0
  30. package/dist/cdp/label_cache.js +51 -0
  31. package/dist/cdp/lock.d.ts +33 -0
  32. package/dist/cdp/lock.js +206 -0
  33. package/dist/cdp/omni_broker.d.ts +17 -0
  34. package/dist/cdp/omni_broker.js +119 -0
  35. package/dist/cdp/profiles.d.ts +1 -0
  36. package/dist/cdp/profiles.js +87 -0
  37. package/dist/cdp/pruner.d.ts +41 -0
  38. package/dist/cdp/pruner.js +263 -0
  39. package/dist/cdp/ref_cache.d.ts +101 -0
  40. package/dist/cdp/ref_cache.js +234 -0
  41. package/dist/cdp/retry.d.ts +5 -0
  42. package/dist/cdp/retry.js +39 -0
  43. package/dist/cdp/retry_enhanced.d.ts +44 -0
  44. package/dist/cdp/retry_enhanced.js +102 -0
  45. package/dist/cdp/security.d.ts +50 -0
  46. package/dist/cdp/security.js +237 -0
  47. package/dist/cdp/verify.d.ts +39 -0
  48. package/dist/cdp/verify.js +143 -0
  49. package/dist/cdp/walker.d.ts +91 -0
  50. package/dist/cdp/walker.js +896 -0
  51. package/dist/cdp/walker.test.d.ts +2 -0
  52. package/dist/cdp/walker.test.js +172 -0
  53. package/dist/cdp/websocket_transport.d.ts +15 -0
  54. package/dist/cdp/websocket_transport.js +118 -0
  55. package/dist/cli/plugin.d.ts +9 -0
  56. package/dist/cli/plugin.js +1134 -0
  57. package/dist/cli/updater.d.ts +1 -0
  58. package/dist/cli/updater.js +72 -0
  59. package/dist/config/loader.d.ts +120 -0
  60. package/dist/config/loader.js +704 -0
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.js +1248 -0
  63. package/dist/intelligence/hints.d.ts +11 -0
  64. package/dist/intelligence/hints.js +39 -0
  65. package/dist/intelligence/intent.d.ts +33 -0
  66. package/dist/intelligence/intent.js +171 -0
  67. package/dist/intelligence/sessionMemory.d.ts +66 -0
  68. package/dist/intelligence/sessionMemory.js +284 -0
  69. package/dist/intelligence/sessionMemory.test.d.ts +2 -0
  70. package/dist/intelligence/sessionMemory.test.js +62 -0
  71. package/dist/intelligence/trail.d.ts +64 -0
  72. package/dist/intelligence/trail.js +233 -0
  73. package/dist/intelligence/trail.test.d.ts +2 -0
  74. package/dist/intelligence/trail.test.js +110 -0
  75. package/dist/intelligence/vision.d.ts +24 -0
  76. package/dist/intelligence/vision.js +151 -0
  77. package/dist/lifecycle/mission_manager.d.ts +16 -0
  78. package/dist/lifecycle/mission_manager.js +44 -0
  79. package/dist/overlay/bubble.d.ts +16 -0
  80. package/dist/overlay/bubble.js +37 -0
  81. package/dist/overlay/ghost.d.ts +35 -0
  82. package/dist/overlay/ghost.js +508 -0
  83. package/dist/overlay/injector.d.ts +22 -0
  84. package/dist/overlay/injector.js +198 -0
  85. package/dist/plugin/interface.d.ts +134 -0
  86. package/dist/plugin/interface.js +1 -0
  87. package/dist/plugin/loader.d.ts +18 -0
  88. package/dist/plugin/loader.js +155 -0
  89. package/dist/plugin/registry.d.ts +50 -0
  90. package/dist/plugin/registry.js +234 -0
  91. package/dist/security/blacklist.d.ts +24 -0
  92. package/dist/security/blacklist.js +80 -0
  93. package/dist/security/filter.d.ts +2 -0
  94. package/dist/security/filter.js +20 -0
  95. package/dist/security/guard.d.ts +8 -0
  96. package/dist/security/guard.js +35 -0
  97. package/dist/security/ratelimit.d.ts +11 -0
  98. package/dist/security/ratelimit.js +57 -0
  99. package/dist/security/secrets.d.ts +75 -0
  100. package/dist/security/secrets.js +365 -0
  101. package/dist/sensors/actionability.d.ts +81 -0
  102. package/dist/sensors/actionability.js +303 -0
  103. package/dist/sensors/guard.d.ts +25 -0
  104. package/dist/sensors/guard.js +168 -0
  105. package/dist/sensors/mutations.d.ts +23 -0
  106. package/dist/sensors/mutations.js +93 -0
  107. package/dist/sensors/network.d.ts +67 -0
  108. package/dist/sensors/network.js +193 -0
  109. package/dist/sensors/operation_monitor.d.ts +80 -0
  110. package/dist/sensors/operation_monitor.js +203 -0
  111. package/dist/sensors/pacing.d.ts +44 -0
  112. package/dist/sensors/pacing.js +111 -0
  113. package/dist/sensors/thoughts.d.ts +2 -0
  114. package/dist/sensors/thoughts.js +59 -0
  115. package/dist/sensors/wait.d.ts +12 -0
  116. package/dist/sensors/wait.js +44 -0
  117. package/dist/server.d.ts +7 -0
  118. package/dist/server.js +1687 -0
  119. package/dist/state/agent.d.ts +36 -0
  120. package/dist/state/agent.js +101 -0
  121. package/dist/state/cache.d.ts +16 -0
  122. package/dist/state/cache.js +92 -0
  123. package/dist/state/cache.test.d.ts +2 -0
  124. package/dist/state/cache.test.js +76 -0
  125. package/dist/state/delta.d.ts +71 -0
  126. package/dist/state/delta.js +234 -0
  127. package/dist/state/delta.test.d.ts +2 -0
  128. package/dist/state/delta.test.js +279 -0
  129. package/dist/state/groups.d.ts +15 -0
  130. package/dist/state/groups.js +80 -0
  131. package/dist/state/session.d.ts +30 -0
  132. package/dist/state/session.js +155 -0
  133. package/dist/tools/accessibility.d.ts +14 -0
  134. package/dist/tools/accessibility.js +32 -0
  135. package/dist/tools/ask.d.ts +14 -0
  136. package/dist/tools/ask.js +49 -0
  137. package/dist/tools/automation.d.ts +36 -0
  138. package/dist/tools/automation.js +218 -0
  139. package/dist/tools/browser_detect.d.ts +13 -0
  140. package/dist/tools/browser_detect.js +73 -0
  141. package/dist/tools/burst.d.ts +35 -0
  142. package/dist/tools/burst.js +344 -0
  143. package/dist/tools/canvas.d.ts +25 -0
  144. package/dist/tools/canvas.js +132 -0
  145. package/dist/tools/captcha.d.ts +38 -0
  146. package/dist/tools/captcha.js +209 -0
  147. package/dist/tools/click.d.ts +36 -0
  148. package/dist/tools/click.js +561 -0
  149. package/dist/tools/click_and_wait.d.ts +33 -0
  150. package/dist/tools/click_and_wait.js +149 -0
  151. package/dist/tools/clipboard.d.ts +29 -0
  152. package/dist/tools/clipboard.js +279 -0
  153. package/dist/tools/console.d.ts +21 -0
  154. package/dist/tools/console.js +107 -0
  155. package/dist/tools/cookies.d.ts +67 -0
  156. package/dist/tools/cookies.js +121 -0
  157. package/dist/tools/design_extract.d.ts +109 -0
  158. package/dist/tools/design_extract.js +641 -0
  159. package/dist/tools/dialog.d.ts +24 -0
  160. package/dist/tools/dialog.js +109 -0
  161. package/dist/tools/drag.d.ts +15 -0
  162. package/dist/tools/drag.js +29 -0
  163. package/dist/tools/emulation.d.ts +33 -0
  164. package/dist/tools/emulation.js +69 -0
  165. package/dist/tools/eval.d.ts +20 -0
  166. package/dist/tools/eval.js +92 -0
  167. package/dist/tools/fill.d.ts +41 -0
  168. package/dist/tools/fill.js +667 -0
  169. package/dist/tools/find.d.ts +30 -0
  170. package/dist/tools/find.js +60 -0
  171. package/dist/tools/geo.d.ts +19 -0
  172. package/dist/tools/geo.js +29 -0
  173. package/dist/tools/github/analyze.d.ts +4 -0
  174. package/dist/tools/github/analyze.js +113 -0
  175. package/dist/tools/github/cache.d.ts +21 -0
  176. package/dist/tools/github/cache.js +28 -0
  177. package/dist/tools/github/index.d.ts +4 -0
  178. package/dist/tools/github/index.js +4 -0
  179. package/dist/tools/github/issues.d.ts +9 -0
  180. package/dist/tools/github/issues.js +88 -0
  181. package/dist/tools/github/pr.d.ts +5 -0
  182. package/dist/tools/github/pr.js +88 -0
  183. package/dist/tools/github/rateLimiter.d.ts +4 -0
  184. package/dist/tools/github/rateLimiter.js +46 -0
  185. package/dist/tools/github/read.d.ts +34 -0
  186. package/dist/tools/github/read.js +74 -0
  187. package/dist/tools/github/repoContext.d.ts +20 -0
  188. package/dist/tools/github/repoContext.js +129 -0
  189. package/dist/tools/highlight.d.ts +20 -0
  190. package/dist/tools/highlight.js +140 -0
  191. package/dist/tools/hover.d.ts +19 -0
  192. package/dist/tools/hover.js +57 -0
  193. package/dist/tools/inspect.d.ts +17 -0
  194. package/dist/tools/inspect.js +125 -0
  195. package/dist/tools/integration.test.d.ts +2 -0
  196. package/dist/tools/integration.test.js +96 -0
  197. package/dist/tools/key.d.ts +25 -0
  198. package/dist/tools/key.js +173 -0
  199. package/dist/tools/macro_compiler.d.ts +7 -0
  200. package/dist/tools/macro_compiler.js +82 -0
  201. package/dist/tools/mouse.d.ts +21 -0
  202. package/dist/tools/mouse.js +62 -0
  203. package/dist/tools/navigate.d.ts +16 -0
  204. package/dist/tools/navigate.js +188 -0
  205. package/dist/tools/network_intercept.d.ts +17 -0
  206. package/dist/tools/network_intercept.js +151 -0
  207. package/dist/tools/office/index.d.ts +9 -0
  208. package/dist/tools/office/index.js +7 -0
  209. package/dist/tools/office/list.d.ts +15 -0
  210. package/dist/tools/office/list.js +41 -0
  211. package/dist/tools/office/parse.d.ts +20 -0
  212. package/dist/tools/office/parse.js +172 -0
  213. package/dist/tools/pdf.d.ts +22 -0
  214. package/dist/tools/pdf.js +42 -0
  215. package/dist/tools/performance.d.ts +12 -0
  216. package/dist/tools/performance.js +47 -0
  217. package/dist/tools/performance_test.d.ts +70 -0
  218. package/dist/tools/performance_test.js +128 -0
  219. package/dist/tools/plan.d.ts +39 -0
  220. package/dist/tools/plan.js +161 -0
  221. package/dist/tools/pointer.d.ts +35 -0
  222. package/dist/tools/pointer.js +522 -0
  223. package/dist/tools/read.d.ts +30 -0
  224. package/dist/tools/read.js +488 -0
  225. package/dist/tools/recording.d.ts +45 -0
  226. package/dist/tools/recording.js +124 -0
  227. package/dist/tools/response.d.ts +129 -0
  228. package/dist/tools/response.js +5 -0
  229. package/dist/tools/scrape.d.ts +24 -0
  230. package/dist/tools/scrape.js +140 -0
  231. package/dist/tools/screenshot.d.ts +40 -0
  232. package/dist/tools/screenshot.js +79 -0
  233. package/dist/tools/scroll.d.ts +23 -0
  234. package/dist/tools/scroll.js +196 -0
  235. package/dist/tools/select.d.ts +13 -0
  236. package/dist/tools/select.js +217 -0
  237. package/dist/tools/snapshot.d.ts +27 -0
  238. package/dist/tools/snapshot.js +99 -0
  239. package/dist/tools/storage.d.ts +18 -0
  240. package/dist/tools/storage.js +29 -0
  241. package/dist/tools/tabs.d.ts +77 -0
  242. package/dist/tools/tabs.js +342 -0
  243. package/dist/tools/thought.d.ts +16 -0
  244. package/dist/tools/thought.js +79 -0
  245. package/dist/tools/type.d.ts +65 -0
  246. package/dist/tools/type.js +1061 -0
  247. package/dist/tools/type_secret.d.ts +23 -0
  248. package/dist/tools/type_secret.js +229 -0
  249. package/dist/tools/upload.d.ts +19 -0
  250. package/dist/tools/upload.js +103 -0
  251. package/dist/tools/video_compiler.d.ts +24 -0
  252. package/dist/tools/video_compiler.js +199 -0
  253. package/dist/tools/viewport.d.ts +44 -0
  254. package/dist/tools/viewport.js +230 -0
  255. package/dist/tools/wait.d.ts +19 -0
  256. package/dist/tools/wait.js +202 -0
  257. package/dist/tools/wait_for.d.ts +23 -0
  258. package/dist/tools/wait_for.js +30 -0
  259. package/dist/tools/windows.d.ts +22 -0
  260. package/dist/tools/windows.js +57 -0
  261. package/dist/ui/index.d.ts +1 -0
  262. package/dist/ui/index.js +12 -0
  263. package/docs/DEV_MODE.md +68 -0
  264. package/docs/DOCUMENTATION.md +532 -0
  265. package/docs/PLUGINS.md +538 -0
  266. package/docs/TOOLS.md +104 -0
  267. package/docs/dev_plugin.md +310 -0
  268. package/ext-dist/background.js +467 -0
  269. package/ext-dist/content.js +3089 -0
  270. package/ext-dist/dashboard.html +583 -0
  271. package/ext-dist/hud/hud.js +73 -0
  272. package/ext-dist/hud/index.html +20 -0
  273. package/ext-dist/manifest.json +77 -0
  274. package/ext-dist/sidepanel/sidepanel.js +25 -0
  275. package/ext-dist/sidepanel.html +547 -0
  276. package/ext-dist/styles.css +218 -0
  277. package/icon.png +0 -0
  278. package/mcp.json +796 -0
  279. package/package.json +113 -0
  280. package/server.json +23 -0
  281. package/smithery.yaml +11 -0
@@ -0,0 +1,3089 @@
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._canvas) {
168
+ __weaveCursor._canvas.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
169
+ }
170
+ }
171
+
172
+ const hudEl = document.getElementById('Weavetab-hud');
173
+ if (hudEl) {
174
+ hudEl.style.display = 'none';
175
+ }
176
+
177
+ const hudHost = document.querySelector('weavetab-host');
178
+ if (hudHost) {
179
+ hudHost.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
180
+ }
181
+
182
+ const indicatorEl = document.getElementById('wt-controlled-indicator') || document.querySelector('.wt-controlled-indicator') || document.getElementById('__Weavetab_controlled_indicator');
183
+ if (indicatorEl) {
184
+ indicatorEl.style.display = __wt_masterVisualsEnabled ? 'block' : 'none';
185
+ }
186
+
187
+ if (!__wt_masterVisualsEnabled) {
188
+ if (typeof clearXRay === 'function') clearXRay();
189
+ document.querySelectorAll('.Weavetab-ripple, .wt-xray-spotlight, .wt-bubble, .__Weavetab_xray').forEach(function(el){
190
+ el.remove();
191
+ });
192
+ }
193
+ }
194
+
195
+ try {
196
+ browserAPI.storage.local.get(['wt_master_visuals'], function(res) {
197
+ if (res && res.wt_master_visuals === false) {
198
+ __wt_applyMasterVisuals(false);
199
+ } else {
200
+ __wt_applyMasterVisuals(true);
201
+ }
202
+ });
203
+
204
+ browserAPI.storage.onChanged.addListener(function(changes, area) {
205
+ if (area === 'local' && changes.wt_master_visuals !== undefined) {
206
+ __wt_applyMasterVisuals(changes.wt_master_visuals.newValue !== false);
207
+ }
208
+ });
209
+ } catch(e) {}
210
+
211
+ // Ghost CDP cursor relay from background
212
+ browserAPI.runtime.onMessage.addListener(function(msg) {
213
+ if (msg.type === 'WT_TOGGLE_MASTER_VISUALS') {
214
+ __wt_applyMasterVisuals(msg.enabled);
215
+ }
216
+ if (msg.type === 'GHOST_CURSOR_MOVE' && iframe && iframe.contentWindow) {
217
+ iframe.contentWindow.postMessage({ type: 'RT_CURSOR_MOVE', x: msg.x, y: msg.y, phase: msg.phase }, '*');
218
+ }
219
+ if (msg.type === 'TOOL_CALL_EVENT' && iframe && iframe.contentWindow) {
220
+ iframe.contentWindow.postMessage({ type: 'TOOL_CALL_EVENT', payload: msg.payload }, '*');
221
+ }
222
+ // Relay sidepanel open/close state to HUD iframe so it can gate browser_thought pill
223
+ if (msg.type === 'WT_SIDEPANEL_STATE' && iframe && iframe.contentWindow) {
224
+ iframe.contentWindow.postMessage({ type: 'WT_SIDEPANEL_STATE', open: msg.open }, '*');
225
+ }
226
+ // Background tells us to suppress the thought pill for this specific event
227
+ if (msg.type === 'WT_SUPPRESS_THOUGHT_HUD' && iframe && iframe.contentWindow) {
228
+ iframe.contentWindow.postMessage({ type: 'WT_SUPPRESS_THOUGHT_HUD' }, '*');
229
+ }
230
+ // Scripted pill toggle from sidepanel settings — relay to HUD iframe
231
+ if (msg.type === 'WT_SCRIPTED_PILL_TOGGLE' && iframe && iframe.contentWindow) {
232
+ iframe.contentWindow.postMessage({ type: 'WT_SCRIPTED_PILL_TOGGLE', enabled: msg.enabled }, '*');
233
+ }
234
+ // Handle incoming user notes sent from the sidepanel input
235
+ if (msg.type === 'WT_USER_NOTE_INJECT' || msg.type === 'WT_USER_NOTE') {
236
+ window.__wt_chat_queue = window.__wt_chat_queue || [];
237
+ let formattedMsg = msg.text || '';
238
+ if (msg.snipe) {
239
+ formattedMsg = `[USER AREA ANNOTATION ATTACHED (${msg.snipe.mode})]: ` + formattedMsg;
240
+ }
241
+ window.__wt_chat_queue.push(formattedMsg);
242
+
243
+ // Unblock pending ask_bridge polling loops immediately
244
+ window.__wt_ask_bridge = window.__wt_ask_bridge || {};
245
+ if (window.__wt_ask_bridge.resolve) {
246
+ window.__wt_ask_bridge.resolve({
247
+ success: true,
248
+ response: formattedMsg,
249
+ status: 'answered'
250
+ });
251
+ window.__wt_ask_bridge = {};
252
+ }
253
+
254
+ // Display on-page glassmorphism notification banner if master visuals are enabled
255
+ if (window.__wt_masterVisualsEnabled !== false) {
256
+ __wt_showUserNoteVisualBanner(formattedMsg);
257
+ }
258
+ }
259
+ });
260
+
261
+ function __wt_showUserNoteVisualBanner(text) {
262
+ let banner = document.getElementById('wt-user-note-banner');
263
+ if (!banner) {
264
+ banner = document.createElement('div');
265
+ banner.id = 'wt-user-note-banner';
266
+ banner.setAttribute('data-wt-ignore', 'true');
267
+ banner.style.cssText = `
268
+ position: fixed; top: 16px; right: 20px; z-index: 2147483647;
269
+ background: rgba(24, 24, 27, 0.94); backdrop-filter: blur(12px);
270
+ border: 1px solid #38bdf8; color: #f4f4f5; font-weight: 500;
271
+ font-size: 11px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
272
+ padding: 8px 16px; border-radius: 20px;
273
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6), 0 0 14px rgba(56, 189, 248, 0.4);
274
+ display: flex; align-items: center; gap: 8px;
275
+ transition: opacity 0.3s ease, transform 0.3s ease;
276
+ opacity: 0; transform: translateY(-10px); pointer-events: none;
277
+ `;
278
+ document.documentElement.appendChild(banner);
279
+ }
280
+ const cleanText = text.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').slice(0, 90);
281
+ 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>`;
282
+ requestAnimationFrame(() => {
283
+ banner.style.opacity = '1';
284
+ banner.style.transform = 'translateY(0)';
285
+ });
286
+ setTimeout(() => {
287
+ banner.style.opacity = '0';
288
+ banner.style.transform = 'translateY(-10px)';
289
+ setTimeout(() => banner.remove(), 300);
290
+ }, 4000);
291
+ }
292
+
293
+ // State persistence bridge: listen for state changes from iframe
294
+ window.addEventListener('message', function(e) {
295
+ // Forward relevant events to the extension background/sidepanel
296
+ if (e.data && (
297
+ e.data.type === 'WT_TOOL_CALL' ||
298
+ e.data.type === 'WT_PLAN_UPDATE' ||
299
+ e.data.type === 'WT_THOUGHT' ||
300
+ e.data.type === 'TOOL_CALL_EVENT' ||
301
+ e.data.type === 'Weavetab_CONFIG' ||
302
+ e.data.type === 'WT_CONFIG_UPDATE'
303
+ )) {
304
+ __wt_safeSendMessage(e.data);
305
+ }
306
+
307
+
308
+ if (e.origin !== browserAPI.runtime.getURL('').slice(0, -1)) return;
309
+ if (e.data.type === 'HUD_STATE_CHANGED') {
310
+ hudState = { ...hudState, ...e.data.state };
311
+ browserAPI.storage.local.set({ 'wt-hud-state': hudState });
312
+ }
313
+ });
314
+
315
+ // Listen for toggle command from background
316
+ browserAPI.runtime.onMessage.addListener(function(msg) {
317
+ console.log('[Weavetab Content] Received message:', msg.type);
318
+ if (msg.type === 'TOGGLE_HUD' && iframe && iframe.contentWindow) {
319
+ iframe.contentWindow.postMessage({ type: 'TOGGLE_HUD' }, '*');
320
+ }
321
+ if (msg.type === 'TOGGLE_DEBUG' && iframe && iframe.contentWindow) {
322
+ console.log('[Weavetab Content] Forwarding TOGGLE_DEBUG to iframe');
323
+ iframe.contentWindow.postMessage({ type: 'TOGGLE_DEBUG' }, '*');
324
+ }
325
+ });
326
+
327
+ // Initial mount
328
+ createMount();
329
+ })();
330
+
331
+ // ── UNIFIED VISUAL SYSTEM INITIALIZATION ─────────────────────────
332
+ // Ensures all visual components initialize in correct order and persist across reloads
333
+ window.__wt_visualsReady = false;
334
+ window.__wt_visualComponents = {
335
+ cursor: false,
336
+ feedback: false,
337
+ debug: false
338
+ };
339
+ window.__wt_zIndexCounter = 2147483640; // Start near max z-index
340
+
341
+ function __wt_initVisuals() {
342
+ if (window.__wt_visualsReady) return;
343
+
344
+ // Initialize cursor first
345
+ if (!window.__wt_visualComponents.cursor && window.__weaveCursor_instance) {
346
+ window.__wt_visualComponents.cursor = true;
347
+ }
348
+
349
+ // Check for WeaveTab OS panel (todo list + chat)
350
+ if (!window.__wt_visualComponents.feedback && document.getElementById('wt-main-container')) {
351
+ window.__wt_visualComponents.feedback = true;
352
+ }
353
+
354
+ // Check for debug overlay
355
+ if (!window.__wt_visualComponents.debug && document.getElementById('wt-debug-overlay')) {
356
+ window.__wt_visualComponents.debug = true;
357
+ }
358
+
359
+ // Mark as ready when cursor and panel are initialized
360
+ if (window.__wt_visualComponents.cursor && window.__wt_visualComponents.feedback) {
361
+ window.__wt_visualsReady = true;
362
+ }
363
+ }
364
+
365
+ // Health check every 5 seconds to recover missing components
366
+ setInterval(function() {
367
+ __wt_initVisuals();
368
+ __wt_recoverVisuals();
369
+ }, 5000);
370
+
371
+ // Visual recovery - detect and fix missing/broken components
372
+ function __wt_recoverVisuals() {
373
+ // Recover cursor if missing
374
+ if (window.__wt_visualComponents && window.__wt_visualComponents.cursor && !document.getElementById('__Weavetab_cursor_host')) {
375
+ console.warn('[Weavetab] Cursor host missing, reinitializing...');
376
+ if (window.__weaveCursor_instance && window.__weaveCursor_instance.destroy) {
377
+ window.__weaveCursor_instance.destroy();
378
+ }
379
+ __weaveCursor = new WeaveCursor();
380
+ window.__weaveCursor_instance = __weaveCursor;
381
+ window.__wt_visualComponents.cursor = true;
382
+ }
383
+
384
+ // Recover WeaveTab OS panel if missing
385
+ if (window.__wt_visualComponents.feedback && !document.getElementById('wt-main-container')) {
386
+ console.warn('[Weavetab] WeaveTab OS panel missing, this is handled by page reload');
387
+ }
388
+
389
+ // Fix z-index conflicts
390
+ var wtElements = document.querySelectorAll('[data-wt-ignore]');
391
+ wtElements.forEach(function(el) {
392
+ if (el.style.zIndex && parseInt(el.style.zIndex) < 2147483640) {
393
+ el.style.zIndex = ++window.__wt_zIndexCounter;
394
+ }
395
+ });
396
+ }
397
+
398
+ // ── STALE ELEMENT CLEANUP ─────────────────────────────────────
399
+ // Removes old UI elements from previous server/extension sessions.
400
+ // Observer stays active for the full page lifetime (SPA-safe).
401
+ (function cleanupOldUI() {
402
+ var OLD_IDS = [
403
+ 'wt-ghost-cursor','wt-ghost-bubble','wt-ask-panel','wt-cur-canvas',
404
+ 'wt-ask-container','wt-msg-panel','wt-ask-title','wt-msg-bar','wt-msg-pill'
405
+ ];
406
+ function removeOld() {
407
+ for (var i = 0; i < OLD_IDS.length; i++) {
408
+ var el = document.getElementById(OLD_IDS[i]);
409
+ if (el) el.remove();
410
+ }
411
+ }
412
+ removeOld();
413
+ // Persistent observer — never disconnects so SPA re-renders stay clean
414
+ var obs = new MutationObserver(function() { removeOld(); });
415
+ if (document.documentElement) {
416
+ obs.observe(document.documentElement, { childList: true, subtree: true });
417
+ } else {
418
+ document.addEventListener('DOMContentLoaded', function() {
419
+ if (document.documentElement) obs.observe(document.documentElement, { childList: true, subtree: true });
420
+ });
421
+ }
422
+ })();
423
+
424
+ // ── UI DESIGN SYSTEM (inject once, styles.css handles visuals) ─
425
+ (function injectWeavetabUI() {
426
+ if (document.getElementById('wt-ui-styles')) return;
427
+ // Guard marker so we only run once
428
+ const m = document.createElement('meta');
429
+ m.id = 'wt-ui-styles';
430
+ (document.head || document.documentElement || document.body).appendChild(m);
431
+ })();
432
+
433
+ // ── THOUGHT BUBBLE ─────────────────────────────────────────────
434
+ let __wt_activeBubble = null;
435
+ let __wt_bubbleTimer = null;
436
+ let __wt_typewriterTimer = null;
437
+ let __wt_feedbackSticky = false;
438
+ let __wt_thoughtBirth = 0;
439
+ let __wt_thoughtQueue = [];
440
+ let __wt_lastRemoveTime = 0;
441
+ let __wt_toolActive = false;
442
+
443
+ function __wt_fadeOutBubble(callback) {
444
+ if (!__wt_activeBubble) { if (callback) callback(); return; }
445
+ var bub = __wt_activeBubble;
446
+ bub.style.transition = 'opacity 0.15s ease-out, transform 0.15s ease-out';
447
+ bub.style.opacity = '0';
448
+ bub.style.transform = 'translateX(-50%) translateY(6px)';
449
+ setTimeout(function() {
450
+ bub.remove();
451
+ if (callback) callback();
452
+ }, 150);
453
+ }
454
+
455
+ function __wt_destroyBubble() {
456
+ if (__wt_bubbleTimer) { clearTimeout(__wt_bubbleTimer); __wt_bubbleTimer = null; }
457
+ if (__wt_typewriterTimer) { clearInterval(__wt_typewriterTimer); __wt_typewriterTimer = null; }
458
+ var current = document.querySelector('.wt-bubble[data-wt-thought]');
459
+ if (current) {
460
+ current.style.transition = 'opacity 0.15s ease-out, transform 0.15s ease-out';
461
+ current.style.opacity = '0';
462
+ current.style.transform = 'translateX(-50%) translateY(6px)';
463
+ setTimeout(function() {
464
+ if (current && current.parentNode) current.remove();
465
+ __wt_activeBubble = null;
466
+ }, 150);
467
+ } else {
468
+ __wt_activeBubble = null;
469
+ }
470
+ __wt_lastRemoveTime = Date.now();
471
+ }
472
+
473
+ function __wt_removeThought() {
474
+ if (__wt_feedbackSticky) return;
475
+ if (__wt_thoughtBirth && Date.now() - __wt_thoughtBirth < 300) return;
476
+ __wt_destroyBubble();
477
+ }
478
+
479
+ function __wt_showThought(text, options) {
480
+ // On-page thought bubbles removed — all tool calls/thoughts display inside sidepanel logs
481
+ return;
482
+ }
483
+
484
+ function __wt_renderThought(text, options) {
485
+ // On-page thought bubbles removed — all tool calls/thoughts display inside sidepanel logs
486
+ return;
487
+ }
488
+
489
+ function __wt_processThoughtQueue() {
490
+ if (__wt_thoughtQueue.length > 0) {
491
+ var next = __wt_thoughtQueue.shift();
492
+ setTimeout(function() { __wt_renderThought(next.text, next.options); }, 200);
493
+ }
494
+ }
495
+
496
+ // ── QUESTION INPUT (merged into thought bubble system) ────────
497
+ let __wt_askContainer = null;
498
+ let __wt_askTimer = null;
499
+ let __wt_askBridge = null;
500
+
501
+ function __wt_showQuestion(text, inputType, timeoutSeconds) {
502
+ __wt_removeAsk();
503
+ __wt_feedbackSticky = true;
504
+ var ts = timeoutSeconds || 120;
505
+
506
+ // DOM bridge
507
+ __wt_askBridge = document.getElementById('__wt_ask_bridge');
508
+ if (!__wt_askBridge) {
509
+ __wt_askBridge = document.createElement('div');
510
+ __wt_askBridge.id = '__wt_ask_bridge';
511
+ __wt_askBridge.style.display = 'none';
512
+ document.body.appendChild(__wt_askBridge);
513
+ }
514
+ __wt_askBridge.dataset.status = 'pending';
515
+ __wt_askBridge.dataset.result = '';
516
+
517
+ // Update HUD to "waiting..." so the top bar doesn't show question text twice
518
+ updateHUD('waiting for your input...');
519
+
520
+ // Render green thought bubble (same as sticky feedback — 0ms delay)
521
+ __wt_renderThought(text, { active: false, sticky: true });
522
+
523
+ // Countdown timer
524
+ var remaining = ts;
525
+ __wt_askTimer = setInterval(function() {
526
+ remaining--;
527
+ if (remaining <= 0) {
528
+ clearInterval(__wt_askTimer);
529
+ __wt_askTimer = null;
530
+ if (__wt_askBridge) { __wt_askBridge.dataset.status = 'timeout'; __wt_askBridge.dataset.result = ''; }
531
+ __wt_removeAsk();
532
+ }
533
+ }, 1000);
534
+
535
+ // Smooth input row — delays slightly so bubble appears first, then input slides up
536
+ setTimeout(function() {
537
+ if (!__wt_askBridge) return;
538
+
539
+ var row = document.createElement('div');
540
+ row.setAttribute('data-wt-ignore', '');
541
+ row.style.cssText = 'position:fixed;bottom:16px;left:50%;transform:translateX(-50%) translateY(8px);z-index:2147483640;display:flex;align-items:center;gap:0;pointer-events:auto;opacity:0;transition:opacity 0.25s ease-out, transform 0.25s ease-out;';
542
+
543
+ var input;
544
+ if (inputType === 'paragraph') {
545
+ input = document.createElement('textarea');
546
+ input.rows = 3;
547
+ input.style.cssText = 'min-width:280px;max-width:480px;font-size:11px;padding:6px 8px;border-radius:4px 0 0 4px;background:#042f2e;color:#e4e4e7;border:1px solid #059669;border-right:none;outline:none;resize:vertical;font-family:"IBM Plex Mono",monospace;';
548
+ input.placeholder = 'Type your response...';
549
+ } else {
550
+ input = document.createElement('input');
551
+ input.style.cssText = 'min-width:200px;font-size:11px;padding:4px 8px;border-radius:4px 0 0 4px;background:#042f2e;color:#e4e4e7;border:1px solid #059669;border-right:none;outline:none;caret-color:#10b981;font-family:"IBM Plex Mono",monospace;';
552
+ input.placeholder = 'Your answer...';
553
+ }
554
+
555
+ var btn = document.createElement('button');
556
+ btn.style.cssText = 'padding:4px 10px;background:#059670;border:1px solid #059670;border-left:none;border-radius:0 4px 4px 0;color:#fff;cursor:pointer;font-size:10px;font-family:"IBM Plex Mono",monospace;';
557
+ btn.textContent = 'Send';
558
+
559
+ row.appendChild(input);
560
+ row.appendChild(btn);
561
+ document.body.appendChild(row);
562
+ __wt_askContainer = row;
563
+
564
+ // Fade in
565
+ requestAnimationFrame(function() {
566
+ row.style.opacity = '1';
567
+ row.style.transform = 'translateX(-50%) translateY(0)';
568
+ });
569
+
570
+ setTimeout(function() { input.focus(); }, 150);
571
+
572
+ function submit() {
573
+ if (__wt_askBridge) { __wt_askBridge.dataset.status = 'submitted'; __wt_askBridge.dataset.result = input.value; }
574
+ __wt_removeAsk();
575
+ }
576
+ function cancel() {
577
+ if (__wt_askBridge) { __wt_askBridge.dataset.status = 'cancelled'; __wt_askBridge.dataset.result = ''; }
578
+ __wt_removeAsk();
579
+ }
580
+
581
+ btn.onclick = submit;
582
+ input.onkeydown = function(e) {
583
+ if (e.key === 'Enter') {
584
+ if (inputType === 'paragraph' && !e.ctrlKey && !e.metaKey) return;
585
+ submit();
586
+ }
587
+ if (e.key === 'Escape') cancel();
588
+ };
589
+ }, 400);
590
+ }
591
+
592
+ function __wt_removeAsk() {
593
+ if (__wt_askTimer) { clearInterval(__wt_askTimer); __wt_askTimer = null; }
594
+ if (__wt_askContainer) { __wt_askContainer.remove(); __wt_askContainer = null; }
595
+ __wt_askBridge = null;
596
+ __wt_feedbackSticky = false;
597
+ clearTimeout(window.__wt_feedbackStickyTimer);
598
+ __wt_thoughtQueue = [];
599
+ __wt_destroyBubble();
600
+ }
601
+
602
+ // ── SHARED OVERLAY CONTAINER ────────────────────────────────────
603
+ const __wt_shadowStyles = `
604
+ .Weavetab-ripple {
605
+ width: 40px; height: 40px; margin: -20px 0 0 -20px; border-radius: 50%;
606
+ background: rgba(79,142,247,0.35); border: 2px solid #4F8EF7;
607
+ animation: __wt_ripple 0.55s ease-out forwards;
608
+ }
609
+ @keyframes __wt_ripple { from { transform: scale(0.3); opacity: 1; } to { transform: scale(2.2); opacity: 0; } }
610
+ @keyframes __wt_xray_pulse {
611
+ 0% { opacity: 0; transform: scale(0.94); }
612
+ 18% { opacity: 1; transform: scale(1.02); }
613
+ 70% { opacity: 0.85; transform: scale(1); }
614
+ 100% { opacity: 0; transform: scale(0.97); }
615
+ }
616
+ `;
617
+
618
+ function getWeavetabRoot() {
619
+ let host = document.getElementById('Weavetab-root');
620
+ if (!host) {
621
+ host = document.createElement('div');
622
+ host.id = 'Weavetab-root';
623
+ host.setAttribute('data-wt-ignore', '');
624
+ Object.assign(host.style, {
625
+ position: 'fixed', left: '0', top: '0',
626
+ width: '0', height: '0',
627
+ zIndex: '2147483640', pointerEvents: 'none',
628
+ });
629
+ var root = document.documentElement || document.body;
630
+ if (root) root.appendChild(host);
631
+ }
632
+ // Reuse existing shadow root (open mode, set by CDP injector or ourselves)
633
+ if (host.shadowRoot) {
634
+ return host.shadowRoot;
635
+ }
636
+ // Guard against NotSupportedError when another context already attached a closed shadow root
637
+ try {
638
+ const sr = host.attachShadow({ mode: 'open' });
639
+ const style = document.createElement('style');
640
+ style.textContent = __wt_shadowStyles;
641
+ sr.appendChild(style);
642
+ return sr;
643
+ } catch (e) {
644
+ // FIX: fallback must be invisible and non-blocking so it can't
645
+ // cover page content or cause opacity bleeding on sites like example.com.
646
+ const fallback = document.createElement('div');
647
+ fallback.setAttribute('data-wt-ignore', '');
648
+ fallback.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;pointer-events:none;overflow:visible;z-index:2147483640;';
649
+ __wt_safeAppendToBody(fallback);
650
+ return fallback;
651
+ }
652
+ }
653
+
654
+ // ── COORDINATE ORACLE ───────────────────────────────────────────
655
+ function shadowQuery(selector) {
656
+ function searchRoot(root) {
657
+ const found = root.querySelector(selector);
658
+ if (found) return found;
659
+
660
+ for (const el of root.querySelectorAll('*')) {
661
+ if (el.shadowRoot) {
662
+ const result = searchRoot(el.shadowRoot);
663
+ if (result) return result;
664
+ }
665
+ }
666
+ return null;
667
+ }
668
+ return searchRoot(document);
669
+ }
670
+ window.__Weavetab = {
671
+ version: '2.5.0',
672
+ suppressVisuals: false,
673
+ shadowQuery: shadowQuery,
674
+
675
+ // ── CDP Co-Processor Helpers ──────────────────────────────────────────────
676
+ // Call these via Runtime.evaluate instead of doing expensive CDP-side work.
677
+ // All return plain JSON-serializable values.
678
+
679
+ /** Scroll position, viewport size, and page dimensions in one call. */
680
+ getScrollInfo: function() {
681
+ var vv = window.visualViewport || {};
682
+ var body = document.body || document.documentElement;
683
+ return {
684
+ scrollX: Math.round(window.scrollX),
685
+ scrollY: Math.round(window.scrollY),
686
+ viewportW: Math.round(vv.width || window.innerWidth),
687
+ viewportH: Math.round(vv.height || window.innerHeight),
688
+ pageW: Math.round(body.scrollWidth),
689
+ pageH: Math.round(body.scrollHeight),
690
+ maxScrollX: Math.round(Math.max(0, body.scrollWidth - window.innerWidth)),
691
+ maxScrollY: Math.round(Math.max(0, body.scrollHeight - window.innerHeight)),
692
+ scrollRatio: body.scrollHeight > 0 ? +(window.scrollY / body.scrollHeight).toFixed(3) : 0
693
+ };
694
+ },
695
+
696
+ /**
697
+ * Wait until no DOM mutations occur for `quietMs` milliseconds.
698
+ * Resolves to true when stable, false on timeout (default 5000ms).
699
+ * CDP calls this and awaits — replaces dumb sleep() calls.
700
+ */
701
+ waitForStable: function(quietMs, timeoutMs) {
702
+ quietMs = quietMs || 300;
703
+ timeoutMs = timeoutMs || 5000;
704
+ return new Promise(function(resolve) {
705
+ var timer = null;
706
+ var deadline = setTimeout(function() {
707
+ obs.disconnect();
708
+ resolve(false);
709
+ }, timeoutMs);
710
+ function reset() {
711
+ if (timer) clearTimeout(timer);
712
+ timer = setTimeout(function() {
713
+ clearTimeout(deadline);
714
+ obs.disconnect();
715
+ resolve(true);
716
+ }, quietMs);
717
+ }
718
+ var obs = new MutationObserver(reset);
719
+ obs.observe(document.documentElement, { childList: true, subtree: true, attributes: true, characterData: true });
720
+ reset(); // start the quiet timer immediately
721
+ });
722
+ },
723
+
724
+ /**
725
+ * Resolve a data-wt-id directly from the live index — much faster than
726
+ * doing a full accessibility tree walk.
727
+ * Returns { id, x, y, rect, tag, label, viewport, isVisible } or null.
728
+ */
729
+ getElementByWtId: function(id) {
730
+ var idx = window.__wt_liveIndex;
731
+ if (!idx) return null;
732
+ var entry = null;
733
+ for (var i = 0; i < idx.length; i++) {
734
+ if (idx[i].id === String(id)) { entry = idx[i]; break; }
735
+ }
736
+ if (!entry) return null;
737
+ // Refresh coordinates from DOM if element still exists
738
+ var el = document.querySelector('[data-wt-id="' + entry.id + '"]');
739
+ if (el) {
740
+ var r = el.getBoundingClientRect();
741
+ entry.x = Math.round(r.left + r.width / 2);
742
+ entry.y = Math.round(r.top + r.height / 2);
743
+ entry.rect = { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) };
744
+ }
745
+ return entry;
746
+ },
747
+
748
+ /**
749
+ * Dump all form field current values — lets agent verify form state cheaply.
750
+ * Returns array of { name, id, type, value, tag }.
751
+ */
752
+ getFormValues: function() {
753
+ var results = [];
754
+ var els = document.querySelectorAll('input, select, textarea');
755
+ for (var i = 0; i < els.length; i++) {
756
+ var el = els[i];
757
+ if (el.hasAttribute('data-wt-ignore')) continue;
758
+ var val = el.type === 'checkbox' || el.type === 'radio' ? el.checked : el.value;
759
+ results.push({
760
+ name: el.name || el.id || null,
761
+ id: el.id || null,
762
+ wtId: el.getAttribute('data-wt-id') || null,
763
+ type: el.type || el.tagName.toLowerCase(),
764
+ tag: el.tagName.toLowerCase(),
765
+ value: val,
766
+ placeholder: el.placeholder || null
767
+ });
768
+ }
769
+ return results;
770
+ },
771
+
772
+ /**
773
+ * Check visibility of a selector — uses the live IntersectionObserver data
774
+ * maintained by the MutationObserver. Much faster than a full DOM query.
775
+ * Returns { found, visible, occluded, x, y, rect } or null.
776
+ */
777
+ checkVisible: function(selector) {
778
+ var el = document.querySelector(selector) || shadowQuery(selector);
779
+ if (!el) return { found: false };
780
+ var r = el.getBoundingClientRect();
781
+ var vv = window.visualViewport || {};
782
+ var vw = vv.width || window.innerWidth;
783
+ var vh = vv.height || window.innerHeight;
784
+ var inViewport = r.width > 0 && r.height > 0 && r.top < vh && r.bottom > 0 && r.left < vw && r.right > 0;
785
+ var topEl = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2);
786
+ var occluded = topEl !== el && !el.contains(topEl);
787
+ return {
788
+ found: true,
789
+ visible: inViewport,
790
+ occluded: occluded,
791
+ x: Math.round(r.left + r.width / 2),
792
+ y: Math.round(r.top + r.height / 2),
793
+ rect: { left: Math.round(r.left), top: Math.round(r.top), width: Math.round(r.width), height: Math.round(r.height) }
794
+ };
795
+ },
796
+
797
+ /**
798
+ * Return the live element index, optionally filtered.
799
+ * filter: 'visible' | 'all' (default 'visible')
800
+ * Much faster than browser_map for simple lookups.
801
+ */
802
+ getLiveIndex: function(filter) {
803
+ var idx = window.__wt_liveIndex || [];
804
+ if (filter === 'all') return idx;
805
+ // Default: visible viewport only
806
+ return idx.filter(function(e) { return e.viewport === 'visible' || e.isVisible; });
807
+ },
808
+
809
+ /**
810
+ * Pop and return the next user feedback message from the queue.
811
+ * MCP calls this between tool calls to check for user interrupts.
812
+ * Returns { text, ts, tabId } or null if queue is empty.
813
+ * Non-destructive — call popUserFeedback() to consume, peekUserFeedback() to inspect.
814
+ */
815
+ popUserFeedback: function() {
816
+ // Read from localStorage queue (written by submitFeedback + persisted by background)
817
+ try {
818
+ var raw = localStorage.getItem('__wt_chat_queue_persist');
819
+ if (!raw) return null;
820
+ var arr = JSON.parse(raw);
821
+ if (!arr || !arr.length) return null;
822
+ var item = arr.shift();
823
+ localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(arr));
824
+ return item;
825
+ } catch(e) { return null; }
826
+ },
827
+
828
+ /**
829
+ * Check if a blocking dialog (alert/confirm/prompt) is pending on this tab.
830
+ * Agent should check this before clicking if it suspects a dialog might block.
831
+ * Returns { pending: bool, ts: number } — reads from extension storage via background relay.
832
+ */
833
+ getAlertState: function() {
834
+ // Sync check via a DOM marker set by alert wrapper
835
+ return { pending: !!(window.__wt_alertPending), ts: Date.now() };
836
+ },
837
+ };
838
+ window.__Weavetab_shadowQuery = shadowQuery;
839
+
840
+ function isOccluded(el) {
841
+ const rect = el.getBoundingClientRect();
842
+ const cx = rect.left + rect.width / 2;
843
+ const cy = rect.top + rect.height / 2;
844
+ const topEl = document.elementFromPoint(cx, cy);
845
+ return topEl !== el && !el.contains(topEl);
846
+ }
847
+
848
+ window.__Weavetab_getCoords = function(selector) {
849
+ const el = document.querySelector(selector) || shadowQuery(selector);
850
+
851
+ if (!el) return null;
852
+
853
+ const rect = el.getBoundingClientRect();
854
+ const vv = window.visualViewport;
855
+
856
+ return {
857
+ x: rect.left + rect.width / 2 + vv.offsetLeft,
858
+ y: rect.top + rect.height / 2 + vv.offsetTop,
859
+ raw: { left: rect.left, top: rect.top, w: rect.width, h: rect.height },
860
+ occluded: isOccluded(el),
861
+ visible: rect.width > 0 && rect.height > 0
862
+ && rect.top < vv.height && rect.bottom > 0
863
+ };
864
+ };
865
+
866
+ function getStableSelector(el) {
867
+ if (el.id) return `#${el.id}`;
868
+ const path = [];
869
+ while (el.parentNode) {
870
+ let index = 1;
871
+ let sibling = el.previousElementSibling;
872
+ while (sibling) {
873
+ if (sibling.tagName === el.tagName) index++;
874
+ sibling = sibling.previousElementSibling;
875
+ }
876
+ path.unshift(`${el.tagName.toLowerCase()}:nth-of-type(${index})`);
877
+ el = el.parentNode;
878
+ }
879
+ return path.join(' > ');
880
+ }
881
+
882
+ window.__Weavetab_fullMap = function() {
883
+ const map = [];
884
+ let idCounter = 1;
885
+ function crawl(root, depth = 0) {
886
+ for (const el of root.querySelectorAll(
887
+ 'a,button,input,select,textarea,[role],[onclick],[tabindex]'
888
+ )) {
889
+ if (el.hasAttribute('data-wt-ignore')) continue;
890
+ const rect = el.getBoundingClientRect();
891
+ // Always include form elements (input/textarea/select/button) regardless of rect size
892
+ // — canvas-heavy pages can occlude them and getBoundingClientRect returns 0
893
+ const isEssential = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(el.tagName);
894
+ if (rect.width === 0 && !isEssential) continue;
895
+ let id = el.getAttribute('data-wt-id');
896
+ if (!id) {
897
+ id = String(idCounter++);
898
+ el.setAttribute('data-wt-id', id);
899
+ }
900
+ map.push({
901
+ id: id,
902
+ tag: el.tagName.toLowerCase(),
903
+ text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0,80),
904
+ role: el.getAttribute('role'),
905
+ selector: `[data-wt-id="${id}"]`,
906
+ coords: { x: rect.left + rect.width/2, y: rect.top + rect.height/2 },
907
+ inShadow: depth > 0
908
+ });
909
+ if (el.shadowRoot) crawl(el.shadowRoot, depth + 1);
910
+ }
911
+ }
912
+ crawl(document);
913
+ return map;
914
+ };
915
+
916
+ // ── CONSOLE BUFFERING ───────────────────────────────────────────
917
+ const __consoleBuffer = [];
918
+ let __consoleCursor = 0;
919
+
920
+ ['log','warn','error','info','debug'].forEach(level => {
921
+ const orig = console[level];
922
+ console[level] = (...args) => {
923
+ __consoleBuffer.push({ level, time: Date.now(), text: args.join(' ') });
924
+ orig.apply(console, args);
925
+ };
926
+ });
927
+
928
+ window.__Weavetab_readConsole = function({ since = 'last', level = null } = {}) {
929
+ const from = since === 'last' ? __consoleCursor : 0;
930
+ const entries = __consoleBuffer.slice(from);
931
+ __consoleCursor = __consoleBuffer.length;
932
+ return level ? entries.filter(e => e.level === level) : entries;
933
+ };
934
+
935
+ // ── PREMIUM WEAVE CURSOR (Phase 5.1 / 5.3) ─────────────────────
936
+ // Shadow-DOM-isolated, canvas-based, with cognitive-load speed scaling
937
+ // and state-driven visual transitions: idle, moving, reading, typing, clicking, thinking
938
+ class WeaveCursor {
939
+ constructor() {
940
+ // Attach a shadow host that sits above all page content
941
+ this._host = document.createElement('div');
942
+ this._host.id = '__Weavetab_cursor_host';
943
+ Object.assign(this._host.style, {
944
+ position: 'fixed',
945
+ top: '0',
946
+ left: '0',
947
+ width: '0',
948
+ height: '0',
949
+ zIndex: '2147483640',
950
+ pointerEvents: 'none',
951
+ overflow: 'visible',
952
+ });
953
+
954
+ const shadow = this._host.attachShadow({ mode: 'closed' });
955
+
956
+ // Canvas layer
957
+ this._canvas = document.createElement('canvas');
958
+ this._canvas.width = 64;
959
+ this._canvas.height = 64;
960
+ Object.assign(this._canvas.style, {
961
+ position: 'fixed',
962
+ top: '0',
963
+ left: '0',
964
+ width: '64px',
965
+ height: '64px',
966
+ pointerEvents: 'none',
967
+ transform: 'translate(-9999px,-9999px)',
968
+ willChange: 'transform',
969
+ transition: 'opacity 0.15s ease-out',
970
+ });
971
+ shadow.appendChild(this._canvas);
972
+
973
+ this._ctx = this._canvas.getContext('2d');
974
+ this._x = -9999;
975
+ this._y = -9999;
976
+ this._targetX = -9999;
977
+ this._targetY = -9999;
978
+ this._lerpX = -9999;
979
+ this._lerpY = -9999;
980
+ this._state = 'idle';
981
+ this._visible = false;
982
+ this._animFrame = null;
983
+ this._lastMoveTime = 0;
984
+ this._lastTime = performance.now();
985
+ this._waypointQueue = [];
986
+ this._playingQueue = false;
987
+
988
+ this._hotX = 0;
989
+ this._hotY = 0;
990
+
991
+ // Sharp precision arrow pointer path (tip at 0,0)
992
+ this._outerPath = new Path2D("M 0 0 L 0 16 L 4.2 12.2 L 7.5 19 L 10.2 17.6 L 6.8 10.8 L 11.8 10.8 Z");
993
+ this._innerPath = new Path2D("M 1.2 2.5 L 1.2 13.5 L 3.8 10.8 L 6.6 16.5 L 8.2 15.6 L 5.4 9.8 L 9.8 9.8 Z");
994
+
995
+ // Vivid linear gradient (#06B6D4 -> #3B82F6)
996
+ this._blueGrad = this._ctx.createLinearGradient(0, 0, 12, 19);
997
+ this._blueGrad.addColorStop(0, '#06B6D4');
998
+ this._blueGrad.addColorStop(1, '#3B82F6');
999
+
1000
+ __wt_safeAppendToBody(this._host);
1001
+ this._startLoop();
1002
+ }
1003
+
1004
+ destroy() {
1005
+ if (this._animFrame) {
1006
+ cancelAnimationFrame(this._animFrame);
1007
+ this._animFrame = null;
1008
+ }
1009
+ if (this._host && this._host.parentNode) {
1010
+ this._host.parentNode.removeChild(this._host);
1011
+ }
1012
+ }
1013
+
1014
+ setState(state) {
1015
+ this._state = state || 'idle';
1016
+ }
1017
+
1018
+ setVisible(visible) {
1019
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) {
1020
+ this._visible = false;
1021
+ this._canvas.style.opacity = '0';
1022
+ return;
1023
+ }
1024
+ this._visible = visible;
1025
+ this._canvas.style.opacity = visible ? '1' : '0';
1026
+ }
1027
+
1028
+ moveTo(x, y) {
1029
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1030
+ if (this._playingQueue) return;
1031
+ this._targetX = x;
1032
+ this._targetY = y;
1033
+ if (this._lerpX === -9999) { this._lerpX = x; this._lerpY = y; }
1034
+ this._lastMoveTime = Date.now();
1035
+ if (!this._visible) {
1036
+ this._visible = true;
1037
+ this._canvas.style.opacity = '1';
1038
+ }
1039
+ if (!this._animFrame) {
1040
+ this._startLoop();
1041
+ }
1042
+ }
1043
+
1044
+ setWaypoints(waypoints, durationMs) {
1045
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1046
+ this._waypointQueue = waypoints;
1047
+ this._playingQueue = true;
1048
+ this._waypointStartTime = performance.now();
1049
+ this._waypointDuration = durationMs;
1050
+ if (waypoints.length > 0 && this._lerpX === -9999) {
1051
+ this._lerpX = waypoints[0].x;
1052
+ this._lerpY = waypoints[0].y;
1053
+ }
1054
+ if (!this._visible) { this._visible = true; this._canvas.style.opacity = '1'; }
1055
+ if (!this._animFrame) this._startLoop();
1056
+ }
1057
+
1058
+ _startLoop() {
1059
+ this._idleFrames = 0;
1060
+ this._lastTime = performance.now();
1061
+ const draw = (currentTime) => {
1062
+ const dt = Math.min((currentTime - this._lastTime) / 1000, 0.1);
1063
+ this._lastTime = currentTime;
1064
+
1065
+ if (this._playingQueue && this._waypointQueue.length > 0) {
1066
+ const elapsed = currentTime - this._waypointStartTime;
1067
+ const progress = Math.min(elapsed / this._waypointDuration, 1);
1068
+ const idx = Math.min(Math.floor(progress * this._waypointQueue.length), this._waypointQueue.length - 1);
1069
+ const pt = this._waypointQueue[idx];
1070
+ if (pt) {
1071
+ this._targetX = pt.x;
1072
+ this._targetY = pt.y;
1073
+ this._lastMoveTime = Date.now();
1074
+ }
1075
+ if (progress >= 1) {
1076
+ this._playingQueue = false;
1077
+ this._waypointQueue = [];
1078
+ }
1079
+ }
1080
+
1081
+ // Smooth lerp (k = 14)
1082
+ const lerpFactor = 1 - Math.exp(-14 * dt);
1083
+ if (this._targetX !== -9999) {
1084
+ this._lerpX += (this._targetX - this._lerpX) * lerpFactor;
1085
+ this._lerpY += (this._targetY - this._lerpY) * lerpFactor;
1086
+ if (Math.abs(this._targetX - this._lerpX) < 0.3) this._lerpX = this._targetX;
1087
+ if (Math.abs(this._targetY - this._lerpY) < 0.3) this._lerpY = this._targetY;
1088
+ }
1089
+
1090
+ const hasRecentMove = (Date.now() - this._lastMoveTime) < 1200;
1091
+ if (hasRecentMove && (typeof __wt_masterVisualsEnabled === 'undefined' || __wt_masterVisualsEnabled)) {
1092
+ if (!this._visible) {
1093
+ this._visible = true;
1094
+ this._canvas.style.opacity = '1';
1095
+ }
1096
+ this._idleFrames = 0;
1097
+ } else {
1098
+ this._idleFrames++;
1099
+ if (this._idleFrames > 90 || !__wt_masterVisualsEnabled) {
1100
+ if (this._visible) {
1101
+ this._visible = false;
1102
+ this._canvas.style.opacity = '0';
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ this._render();
1108
+ this._animFrame = requestAnimationFrame(draw);
1109
+ };
1110
+ this._animFrame = requestAnimationFrame(draw);
1111
+ }
1112
+
1113
+ _render() {
1114
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) {
1115
+ this._canvas.style.opacity = '0';
1116
+ return;
1117
+ }
1118
+
1119
+ const { _ctx: ctx, _lerpX: x, _lerpY: y } = this;
1120
+
1121
+ this._canvas.style.transform = `translate(${x - this._hotX}px, ${y - this._hotY}px)`;
1122
+ ctx.clearRect(0, 0, 64, 64);
1123
+
1124
+ ctx.save();
1125
+ ctx.translate(2, 2);
1126
+
1127
+ // Sharp outer stroke — White
1128
+ ctx.fillStyle = '#FFFFFF';
1129
+ ctx.strokeStyle = '#FFFFFF';
1130
+ ctx.lineWidth = 2;
1131
+ ctx.lineJoin = 'miter';
1132
+ ctx.miterLimit = 3;
1133
+ ctx.fill(this._outerPath);
1134
+ ctx.stroke(this._outerPath);
1135
+
1136
+ // Sharp inner core — Gradient
1137
+ ctx.fillStyle = this._blueGrad;
1138
+ ctx.fill(this._innerPath);
1139
+
1140
+ ctx.restore();
1141
+ }
1142
+ }
1143
+
1144
+ // Instantiate the premium cursor
1145
+ let __weaveCursor = { setState: function(){}, moveTo: function(){}, destroy: function(){}, setVisible: function(){}, setWaypoints: function(){} };
1146
+ window.__weaveCursor_instance = __weaveCursor;
1147
+
1148
+ function moveCursor(x, y, state) {
1149
+ // Cursor visuals disabled
1150
+ }
1151
+
1152
+
1153
+ // Click ripple (kept as page-level effect, not inside shadow DOM)
1154
+ function ripple(x, y) {
1155
+ if (typeof __wt_masterVisualsEnabled !== 'undefined' && !__wt_masterVisualsEnabled) return;
1156
+ const el = document.createElement('div');
1157
+ el.className = 'Weavetab-ripple';
1158
+ el.setAttribute('data-wt-ignore', '');
1159
+ el.style.cssText = `position:fixed;left:${x}px;top:${y}px;pointer-events:none;z-index:2147483646`;
1160
+ getWeavetabRoot().appendChild(el);
1161
+ el.addEventListener('animationend', () => el.remove());
1162
+ }
1163
+
1164
+ // ── X-RAY SPOTLIGHT OVERLAY (Phase 5.2) ────────────────────────
1165
+ // Flashes glowing halos around top interactive elements when the
1166
+ // semantic map is scanned (GET_SEMANTIC_MAP message).
1167
+ let __xrayActive = false;
1168
+ const __xrayOverlays = [];
1169
+
1170
+ function clearXRay() {
1171
+ for (const el of __xrayOverlays) el.remove();
1172
+ __xrayOverlays.length = 0;
1173
+ __xrayActive = false;
1174
+ }
1175
+
1176
+ function activateXRay(elements) {
1177
+ clearXRay();
1178
+ __xrayActive = true;
1179
+
1180
+ // Colour spectrum for visual richness
1181
+ const colours = [
1182
+ '#4F8EF7', '#7C3AED', '#10B981', '#F59E0B',
1183
+ '#EF4444', '#06B6D4', '#F97316', '#8B5CF6',
1184
+ ];
1185
+
1186
+ // Architecture change: Use live index for visual overlay
1187
+ // The live index maintains up-to-date coordinates and visibility info
1188
+ const liveIndex = window.__wt_liveIndex || [];
1189
+ const top = elements.slice(0, 12); // highlight top 12 nodes
1190
+
1191
+ top.forEach((item, i) => {
1192
+ // Try to get element from live index first for accurate coordinates
1193
+ const liveEntry = liveIndex.find(e => e.id === item.id || e.selector === item.selector);
1194
+
1195
+ let rect;
1196
+ let el;
1197
+
1198
+ if (liveEntry && liveEntry.rect) {
1199
+ // Use coordinates from live index (more accurate, already computed)
1200
+ rect = liveEntry.rect;
1201
+ el = document.querySelector(item.selector) || shadowQuery(item.selector);
1202
+ } else {
1203
+ // Fallback to querying DOM
1204
+ el = document.querySelector(item.selector) || shadowQuery(item.selector);
1205
+ if (!el) return;
1206
+ rect = el.getBoundingClientRect();
1207
+ }
1208
+
1209
+ if (rect.width === 0 || rect.height === 0) return;
1210
+
1211
+ const colour = colours[i % colours.length];
1212
+ const halo = document.createElement('div');
1213
+ halo.className = '__Weavetab_xray';
1214
+ halo.setAttribute('data-wt-ignore', '');
1215
+
1216
+ Object.assign(halo.style, {
1217
+ position: 'fixed',
1218
+ left: `${rect.left - 4}px`,
1219
+ top: `${rect.top - 4}px`,
1220
+ width: `${rect.width + 8}px`,
1221
+ height: `${rect.height + 8}px`,
1222
+ border: `2px solid ${colour}`,
1223
+ borderRadius: '6px',
1224
+ boxShadow: `0 0 12px 4px ${colour}88, inset 0 0 8px ${colour}33`,
1225
+ background: `${colour}0d`,
1226
+ pointerEvents: 'none',
1227
+ zIndex: '2147483645',
1228
+ animation: '__wt_xray_pulse 1.4s ease-in-out forwards',
1229
+ animationDelay: `${i * 55}ms`,
1230
+ });
1231
+
1232
+ // Rank label badge
1233
+ const badge = document.createElement('span');
1234
+ Object.assign(badge.style, {
1235
+ position: 'absolute',
1236
+ top: '-10px',
1237
+ left: '4px',
1238
+ background: colour,
1239
+ color: '#fff',
1240
+ fontSize: '9px',
1241
+ fontFamily: 'system-ui, sans-serif',
1242
+ fontWeight: '700',
1243
+ padding: '1px 5px',
1244
+ borderRadius: '3px',
1245
+ lineHeight: '14px',
1246
+ whiteSpace: 'nowrap',
1247
+ letterSpacing: '0.03em',
1248
+ boxShadow: `0 1px 4px ${colour}99`,
1249
+ });
1250
+ const label = (item.text || item.role || item.tag || '').slice(0, 22) || `#${i + 1}`;
1251
+ badge.textContent = `${i + 1} ${label}`;
1252
+ halo.appendChild(badge);
1253
+
1254
+ getWeavetabRoot().appendChild(halo);
1255
+ __xrayOverlays.push(halo);
1256
+ });
1257
+
1258
+ // Auto-clear after 2.8 s
1259
+ setTimeout(clearXRay, 2800);
1260
+ }
1261
+
1262
+ // Inject keyframe animation for X-Ray
1263
+ (function injectXRayStyles() {
1264
+ if (document.getElementById('__Weavetab_xray_styles')) return;
1265
+ const style = document.createElement('style');
1266
+ style.id = '__Weavetab_xray_styles';
1267
+ style.textContent = `
1268
+ @keyframes __wt_xray_pulse {
1269
+ 0% { opacity: 0; transform: scale(0.94); }
1270
+ 18% { opacity: 1; transform: scale(1.02); }
1271
+ 70% { opacity: 0.85; transform: scale(1); }
1272
+ 100% { opacity: 0; transform: scale(0.97); }
1273
+ }
1274
+ .Weavetab-ripple {
1275
+ width: 40px; height: 40px;
1276
+ margin: -20px 0 0 -20px;
1277
+ border-radius: 50%;
1278
+ background: rgba(79,142,247,0.35);
1279
+ border: 2px solid #4F8EF7;
1280
+ animation: __wt_ripple 0.55s ease-out forwards;
1281
+ }
1282
+ @keyframes __wt_ripple {
1283
+ from { transform: scale(0.3); opacity: 1; }
1284
+ to { transform: scale(2.2); opacity: 0; }
1285
+ }
1286
+ @keyframes wt-reading-pulse {
1287
+ 0% { opacity: 0; box-shadow: inset 0 0 0 rgba(59,130,246,0); }
1288
+ 15% { opacity: 1; box-shadow: inset 0 0 60px rgba(59,130,246,0.15); }
1289
+ 85% { opacity: 1; box-shadow: inset 0 0 60px rgba(59,130,246,0.15); }
1290
+ 100% { opacity: 0; box-shadow: inset 0 0 0 rgba(59,130,246,0); }
1291
+ }
1292
+ `;
1293
+ (document.head || document.documentElement).appendChild(style);
1294
+ })();
1295
+
1296
+ // ── READING GLOW ────────────────────────────────────────────────
1297
+ let __wt_readingGlow = null;
1298
+
1299
+ function __wt_showReadingGlow() {
1300
+ if (__wt_readingGlow) return;
1301
+ var el = document.createElement('div');
1302
+ el.id = 'wt-reading-glow';
1303
+ el.setAttribute('data-wt-ignore', '');
1304
+ 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;';
1305
+ __wt_safeAppendToBody(el);
1306
+ __wt_readingGlow = el;
1307
+ setTimeout(__wt_hideReadingGlow, 3500);
1308
+ }
1309
+
1310
+ function __wt_hideReadingGlow() {
1311
+ if (__wt_readingGlow) { __wt_readingGlow.remove(); __wt_readingGlow = null; }
1312
+ }
1313
+
1314
+ // ── SEMANTIC DOM MAP ────────────────────────────────────────────
1315
+ // Architecture change: Passive/extension-side ID assignment with live index
1316
+ // The MutationObserver continuously assigns data-wt-id to interactive elements
1317
+ // and maintains a live index that CDP can read lazily
1318
+
1319
+ // Live index - always up-to-date, accessible via window.__wt_liveIndex
1320
+ window.__wt_liveIndex = [];
1321
+ window.__wt_nextId = 1;
1322
+
1323
+ // IntersectionObserver for real occlusion detection (not just display:none)
1324
+ const intersectionObserver = new IntersectionObserver((entries) => {
1325
+ for (const entry of entries) {
1326
+ const el = entry.target;
1327
+ const id = el.getAttribute('data-wt-id');
1328
+ if (id) {
1329
+ const indexEntry = window.__wt_liveIndex.find(e => e.id === id);
1330
+ if (indexEntry) {
1331
+ indexEntry.isVisible = entry.isIntersecting;
1332
+ indexEntry.intersectionRatio = entry.intersectionRatio;
1333
+ }
1334
+ }
1335
+ }
1336
+ }, { threshold: [0, 0.25, 0.5, 0.75, 1.0] });
1337
+
1338
+ // Function to assign data-wt-id to an element and add to live index
1339
+ function assignIdAndIndex(el) {
1340
+ if (el.hasAttribute('data-wt-ignore')) return;
1341
+ if (el.hasAttribute('data-wt-id')) return; // Already assigned
1342
+
1343
+ const id = String(window.__wt_nextId++);
1344
+ el.setAttribute('data-wt-id', id);
1345
+ el.__wt_id = id;
1346
+
1347
+ const rect = el.getBoundingClientRect();
1348
+ const inViewport = (
1349
+ rect.bottom > 0 && rect.right > 0 &&
1350
+ rect.top < window.innerHeight && rect.left < window.innerWidth
1351
+ );
1352
+ const viewportPos = inViewport ? "visible" : (rect.top >= window.innerHeight ? "below" : "above");
1353
+
1354
+ // Check for event listeners
1355
+ const hasClickListener = el.onclick !== null ||
1356
+ el.addEventListener !== undefined &&
1357
+ (el._listeners?.click?.length > 0);
1358
+
1359
+ const indexEntry = {
1360
+ id: id,
1361
+ tag: el.tagName.toLowerCase(),
1362
+ role: el.getAttribute('role') || el.tagName.toLowerCase(),
1363
+ label: el.getAttribute('aria-label') || el.textContent?.trim().slice(0, 60) || '',
1364
+ placeholder: el.getAttribute('placeholder') || '',
1365
+ selector: getStableSelector(el),
1366
+ x: Math.round(rect.left + rect.width / 2),
1367
+ y: Math.round(rect.top + rect.height / 2),
1368
+ viewport: viewportPos,
1369
+ href: el.href || null,
1370
+ isVisible: inViewport,
1371
+ intersectionRatio: inViewport ? 1.0 : 0.0,
1372
+ hasClickListener: hasClickListener,
1373
+ rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height }
1374
+ };
1375
+
1376
+ window.__wt_liveIndex.push(indexEntry);
1377
+
1378
+ // Observe for intersection changes
1379
+ try {
1380
+ intersectionObserver.observe(el);
1381
+ } catch (e) {
1382
+ // Element might not be observable
1383
+ }
1384
+ }
1385
+
1386
+ // Function to update an existing entry in the live index
1387
+ function updateIndexEntry(el) {
1388
+ const id = el.getAttribute('data-wt-id');
1389
+ if (!id) return;
1390
+
1391
+ const indexEntry = window.__wt_liveIndex.find(e => e.id === id);
1392
+ if (!indexEntry) return;
1393
+
1394
+ const rect = el.getBoundingClientRect();
1395
+ const inViewport = (
1396
+ rect.bottom > 0 && rect.right > 0 &&
1397
+ rect.top < window.innerHeight && rect.left < window.innerWidth
1398
+ );
1399
+ const viewportPos = inViewport ? "visible" : (rect.top >= window.innerHeight ? "below" : "above");
1400
+
1401
+ indexEntry.x = Math.round(rect.left + rect.width / 2);
1402
+ indexEntry.y = Math.round(rect.top + rect.height / 2);
1403
+ indexEntry.viewport = viewportPos;
1404
+ indexEntry.isVisible = inViewport;
1405
+ indexEntry.rect = { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
1406
+ indexEntry.label = el.getAttribute('aria-label') || el.textContent?.trim().slice(0, 60) || '';
1407
+ }
1408
+
1409
+ // Function to remove an entry from the live index
1410
+ function removeIndexEntry(el) {
1411
+ const id = el.getAttribute('data-wt-id');
1412
+ if (!id) return;
1413
+
1414
+ const index = window.__wt_liveIndex.findIndex(e => e.id === id);
1415
+ if (index !== -1) {
1416
+ window.__wt_liveIndex.splice(index, 1);
1417
+ }
1418
+
1419
+ try {
1420
+ intersectionObserver.unobserve(el);
1421
+ } catch (e) {
1422
+ // Element might not be observable
1423
+ }
1424
+ }
1425
+
1426
+ // MutationObserver for continuous ID assignment and index maintenance
1427
+ const idAssignmentObserver = new MutationObserver((mutations) => {
1428
+ for (const mutation of mutations) {
1429
+ // Handle added nodes
1430
+ for (const node of mutation.addedNodes) {
1431
+ if (node.nodeType === Node.ELEMENT_NODE) {
1432
+ // Check if the node itself is interactive
1433
+ 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"])';
1434
+ if (node.matches && node.matches(interactiveSelector)) {
1435
+ assignIdAndIndex(node);
1436
+ }
1437
+ // Check descendants
1438
+ const descendants = node.querySelectorAll ? node.querySelectorAll(interactiveSelector) : [];
1439
+ for (const desc of descendants) {
1440
+ assignIdAndIndex(desc);
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ // Handle removed nodes
1446
+ for (const node of mutation.removedNodes) {
1447
+ if (node.nodeType === Node.ELEMENT_NODE) {
1448
+ if (node.getAttribute && node.getAttribute('data-wt-id')) {
1449
+ removeIndexEntry(node);
1450
+ }
1451
+ const descendants = node.querySelectorAll ? node.querySelectorAll('[data-wt-id]') : [];
1452
+ for (const desc of descendants) {
1453
+ removeIndexEntry(desc);
1454
+ }
1455
+ }
1456
+ }
1457
+
1458
+ // Handle attribute changes (position, visibility, etc.)
1459
+ if (mutation.type === 'attributes' && mutation.target.getAttribute) {
1460
+ const target = mutation.target;
1461
+ if (target.getAttribute('data-wt-id')) {
1462
+ updateIndexEntry(target);
1463
+ }
1464
+ }
1465
+ }
1466
+ });
1467
+
1468
+ // Start observing when DOM is ready
1469
+ function startIdAssignment() {
1470
+ 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"])';
1471
+
1472
+ // Assign IDs to existing interactive elements
1473
+ const existing = document.querySelectorAll(interactiveSelector);
1474
+ for (const el of existing) {
1475
+ assignIdAndIndex(el);
1476
+ }
1477
+
1478
+ // Start observing for changes
1479
+ if (document.body) {
1480
+ idAssignmentObserver.observe(document.body, {
1481
+ childList: true,
1482
+ subtree: true,
1483
+ attributes: true,
1484
+ attributeFilter: ['style', 'class', 'hidden', 'aria-hidden']
1485
+ });
1486
+ } else {
1487
+ document.addEventListener('DOMContentLoaded', () => {
1488
+ const existing = document.querySelectorAll(interactiveSelector);
1489
+ for (const el of existing) {
1490
+ assignIdAndIndex(el);
1491
+ }
1492
+ idAssignmentObserver.observe(document.body, {
1493
+ childList: true,
1494
+ subtree: true,
1495
+ attributes: true,
1496
+ attributeFilter: ['style', 'class', 'hidden', 'aria-hidden']
1497
+ });
1498
+ });
1499
+ }
1500
+ }
1501
+
1502
+ // Start the ID assignment system
1503
+ if (document.readyState === 'loading') {
1504
+ document.addEventListener('DOMContentLoaded', startIdAssignment);
1505
+ } else {
1506
+ startIdAssignment();
1507
+ }
1508
+
1509
+ // Legacy buildSemanticMap function for backward compatibility
1510
+ // Now reads from the live index instead of rebuilding
1511
+ let cachedMap = null;
1512
+ const legacyObserver = new MutationObserver(() => {
1513
+ cachedMap = null;
1514
+ });
1515
+ // FIX: document.body may be null at document_start; defer observe until body exists
1516
+ if (document.body) {
1517
+ legacyObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
1518
+ } else {
1519
+ document.addEventListener('DOMContentLoaded', function() {
1520
+ if (document.body) legacyObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] });
1521
+ }, { once: true });
1522
+ }
1523
+
1524
+ function buildSemanticMap() {
1525
+ if (cachedMap) return cachedMap;
1526
+
1527
+ // Read from the live index maintained by the MutationObserver
1528
+ cachedMap = window.__wt_liveIndex.map(entry => ({
1529
+ id: entry.id,
1530
+ tag: entry.tag,
1531
+ role: entry.role,
1532
+ label: entry.label,
1533
+ placeholder: entry.placeholder,
1534
+ selector: entry.selector,
1535
+ x: entry.x,
1536
+ y: entry.y,
1537
+ viewport: entry.viewport,
1538
+ href: entry.href,
1539
+ coords: { x: entry.x, y: entry.y }
1540
+ }));
1541
+
1542
+ return cachedMap;
1543
+ }
1544
+
1545
+ // ── HUD ─────────────────────────────────────────────────────────
1546
+ const hudHistory = [];
1547
+ const hud = (() => {
1548
+ const el = document.createElement('div');
1549
+ el.id = 'Weavetab-hud';
1550
+ el.setAttribute('data-wt-ignore', '');
1551
+ Object.assign(el.style, {
1552
+ position: 'fixed', bottom: '24px', right: '24px',
1553
+ transform: 'translateY(8px)',
1554
+ color: '#a1a1aa',
1555
+ font: '11px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif',
1556
+ borderRadius: '8px',
1557
+ opacity: '0', pointerEvents: 'none',
1558
+ zIndex: '2147483640',
1559
+ maxWidth: '400px',
1560
+ display: 'flex', flexDirection: 'column-reverse', gap: '4px',
1561
+ transition: 'opacity 0.2s, transform 0.2s',
1562
+ textAlign: 'right',
1563
+ });
1564
+ __wt_safeAppendToBody(el);
1565
+ return el;
1566
+ })();
1567
+
1568
+ function updateHUD(action, detail) {
1569
+ if (window.self !== window.top) return;
1570
+ // Tool call entries display strictly inside the Sidepanel TOOL CALLS section.
1571
+ // Suppress on-page bottom-right HUD popup overlay.
1572
+ if (hud) {
1573
+ hud.style.display = 'none';
1574
+ hud.style.opacity = '0';
1575
+ }
1576
+ }
1577
+
1578
+ // ── ACTIVE TAB INDICATOR ─────────────────────────────────────────
1579
+ class ControlledTabIndicator {
1580
+ constructor() {
1581
+ this._el = document.createElement('div');
1582
+ this._el.id = '__Weavetab_controlled_indicator';
1583
+ this._el.setAttribute('data-wt-ignore', '');
1584
+ Object.assign(this._el.style, {
1585
+ position: 'fixed',
1586
+ top: '0',
1587
+ left: '0',
1588
+ right: '0',
1589
+ height: '3px',
1590
+ zIndex: '2147483646',
1591
+ background: '#3B82F6',
1592
+ boxShadow: '0 0 12px #3B82F6, 0 0 4px #60a5fa',
1593
+ opacity: '0',
1594
+ transition: 'opacity 0.25s ease-in-out',
1595
+ pointerEvents: 'none',
1596
+ });
1597
+ // Animated shimmer
1598
+ this._shimmer = document.createElement('div');
1599
+ Object.assign(this._shimmer.style, {
1600
+ position: 'absolute',
1601
+ top: '0', left: '-60%', width: '60%', height: '100%',
1602
+ background: 'linear-gradient(90deg,transparent,rgba(255,255,255,0.45),transparent)',
1603
+ animation: 'wt-shimmer 1.6s infinite',
1604
+ });
1605
+ // Inject animation keyframes into shadow root
1606
+ var style = document.createElement('style');
1607
+ style.textContent = '@keyframes wt-shimmer{0%{left:-60%}100%{left:120%}}';
1608
+ try { getWeavetabRoot().appendChild(style); } catch(e) {}
1609
+ this._el.appendChild(this._shimmer);
1610
+ getWeavetabRoot().appendChild(this._el);
1611
+ this._hideTimer = null;
1612
+ this._persistent = false;
1613
+ }
1614
+
1615
+ // Show persistently (while agent is weaving)
1616
+ showPersistent(color) {
1617
+ this._persistent = true;
1618
+ clearTimeout(this._hideTimer);
1619
+ if (color) {
1620
+ this._el.style.background = color;
1621
+ this._el.style.boxShadow = '0 0 12px ' + color + ', 0 0 4px ' + color;
1622
+ }
1623
+ this._el.style.opacity = '1';
1624
+ this._shimmer.style.display = 'block';
1625
+ }
1626
+
1627
+ // Show briefly (1.5s flash for single actions)
1628
+ show(color) {
1629
+ if (this._persistent) return; // Don't override persistent mode
1630
+ if (color) this._el.style.background = color;
1631
+ this._el.style.opacity = '1';
1632
+ clearTimeout(this._hideTimer);
1633
+ this._hideTimer = setTimeout(() => this.hide(), 1500);
1634
+ }
1635
+
1636
+ hide() {
1637
+ this._persistent = false;
1638
+ this._el.style.opacity = '0';
1639
+ this._shimmer.style.display = 'none';
1640
+ }
1641
+
1642
+ destroy() {
1643
+ clearTimeout(this._hideTimer);
1644
+ this._el.remove();
1645
+ }
1646
+ }
1647
+
1648
+ const controlledIndicator = new ControlledTabIndicator();
1649
+
1650
+ // ── MESSAGE BRIDGE ──────────────────────────────────────────────
1651
+ // Shared handler for both CustomEvent (MAIN world dispatch) and
1652
+ // postMessage (cross-world: MAIN → ISOLATED) communication channels.
1653
+ var __wt_lastCursor = { x: -9999, y: -9999 };
1654
+ var __wt_processedMsgIds = new Set();
1655
+
1656
+ function __wt_handleWeavetabMsg(msg) {
1657
+ if (!msg) return;
1658
+ if (Array.isArray(msg)) { msg.forEach(m => __wt_handleWeavetabMsg(m)); return; }
1659
+ if (msg._id) { if (__wt_processedMsgIds.has(msg._id)) return; __wt_processedMsgIds.add(msg._id); if (__wt_processedMsgIds.size > 100) __wt_processedMsgIds.clear(); }
1660
+
1661
+ if (msg.type === 'Weavetab_STATE') {
1662
+ if (msg.cursor) {
1663
+ __wt_lastCursor = { x: msg.cursor.x, y: msg.cursor.y };
1664
+ if (msg.cursor.waypoints && msg.cursor.duration_ms) {
1665
+ __weaveCursor.setWaypoints(msg.cursor.waypoints, msg.cursor.duration_ms);
1666
+ }
1667
+ moveCursor(msg.cursor.x, msg.cursor.y, msg.state);
1668
+ }
1669
+ else if (msg.state) { moveCursor(__wt_lastCursor.x, __wt_lastCursor.y, msg.state); }
1670
+ if (msg.ripple) ripple(msg.ripple.x, msg.ripple.y);
1671
+
1672
+ if (msg.userFeedback === true && msg.feedbackText) {
1673
+ __wt_feedbackSticky = true;
1674
+ clearTimeout(window.__wt_feedbackStickyTimer);
1675
+ window.__wt_feedbackStickyTimer = setTimeout(function(){ __wt_feedbackSticky = false; }, 6000);
1676
+ } else if (msg.userFeedback === false) {
1677
+ setTimeout(function(){ __wt_feedbackSticky = false; }, 2000);
1678
+ }
1679
+
1680
+ // stickyFeedback=true: fired by Weavetab_logs when agent answers a user question.
1681
+ // Pin the bubble for 10s so tool chatter (moving to..., typing...) cannot override it.
1682
+ if (msg.stickyFeedback === true && msg.hud && msg.hud.action) {
1683
+ __wt_thoughtQueue = [];
1684
+ clearTimeout(window.__wt_feedbackStickyTimer);
1685
+
1686
+ if (msg.ask) {
1687
+ window.__wt_feedbackStickyTimer = setTimeout(function(){
1688
+ __wt_feedbackSticky = false;
1689
+ __wt_removeAsk();
1690
+ }, (msg.ask.timeout || 120) * 1000 + 2000);
1691
+ __wt_showQuestion(msg.hud.action, msg.ask.input || 'sentence', msg.ask.timeout || 120);
1692
+ } else {
1693
+ __wt_feedbackSticky = true;
1694
+ window.__wt_feedbackStickyTimer = setTimeout(function(){
1695
+ __wt_feedbackSticky = false;
1696
+ __wt_destroyBubble();
1697
+ }, 10000);
1698
+ __wt_renderThought(msg.hud.action, { active: false, sticky: true });
1699
+ if (msg.hud) updateHUD(msg.hud.action, msg.hud.detail);
1700
+ }
1701
+ if (msg.cursor) {
1702
+ __wt_lastCursor = { x: msg.cursor.x, y: msg.cursor.y };
1703
+ if (msg.cursor.waypoints && msg.cursor.duration_ms) {
1704
+ __weaveCursor.setWaypoints(msg.cursor.waypoints, msg.cursor.duration_ms);
1705
+ }
1706
+ moveCursor(msg.cursor.x, msg.cursor.y, msg.state);
1707
+ }
1708
+ else if (msg.state) { moveCursor(__wt_lastCursor.x, __wt_lastCursor.y, msg.state); }
1709
+ return;
1710
+ }
1711
+
1712
+ if (msg.hud) {
1713
+ updateHUD(msg.hud.action, msg.hud.detail);
1714
+ var a = (msg.hud.action || '').trim();
1715
+ if (a === 'reading the page...') {
1716
+ __wt_showReadingGlow();
1717
+ __wt_showThought('Reading the page...', { active: true });
1718
+ __weaveCursor.setState('reading');
1719
+ } else if (a === 'Mission Running') {
1720
+ // Tool call is starting — switch to eval animation and show tool bubble
1721
+ __wt_toolActive = true;
1722
+ __weaveCursor.setState('eval');
1723
+ __wt_showThought('Executing tool...', { active: true });
1724
+ } else if (a.startsWith('[OK]')) {
1725
+ // Tool completed successfully — revert to idle and instantly hide bubble
1726
+ __weaveCursor.setState('idle');
1727
+ __wt_toolActive = false;
1728
+ if (!__wt_feedbackSticky) {
1729
+ __wt_destroyBubble(); // Instant removal
1730
+ __wt_processThoughtQueue();
1731
+ }
1732
+ } else if (a.startsWith('[ERR]')) {
1733
+ // Tool errored — show stuck cursor briefly
1734
+ __weaveCursor.setState('stuck');
1735
+ __wt_toolActive = false;
1736
+ setTimeout(function(){ __weaveCursor.setState('idle'); }, 3000);
1737
+ if (!__wt_feedbackSticky) {
1738
+ __wt_destroyBubble(); // Instant removal
1739
+ __wt_processThoughtQueue();
1740
+ }
1741
+ } else if (a && a.length > 3) {
1742
+ __wt_hideReadingGlow();
1743
+ if (!__wt_feedbackSticky) {
1744
+ __wt_showThought(a, { active: true });
1745
+ }
1746
+ } else {
1747
+ if (!__wt_feedbackSticky) {
1748
+ __wt_hideReadingGlow();
1749
+ __wt_destroyBubble(); // Instant removal
1750
+ __wt_processThoughtQueue();
1751
+ }
1752
+ }
1753
+ }
1754
+ if (msg.darkMode !== undefined) document.body.classList.toggle('Weavetab-dark', msg.darkMode);
1755
+ if (msg.cursor || msg.hud) {
1756
+ controlledIndicator.show('#3B82F6');
1757
+ }
1758
+ }
1759
+
1760
+ // Handle plan updates via sendToExtension channel
1761
+ if (msg.type === 'Weavetab_STATE' && msg.plan) {
1762
+ const p = msg.plan;
1763
+ if (p.action === 'hide') {
1764
+ if (window.__wt_plan) window.__wt_plan('hide', '', []);
1765
+ } else {
1766
+ if (window.__wt_plan) window.__wt_plan(p.action || 'create', p.goal || '', p.tasks || []);
1767
+ }
1768
+ }
1769
+
1770
+
1771
+ if (msg.type === 'WT_USER_NOTE_INJECT') {
1772
+ window.__wt_user_notes = window.__wt_user_notes || [];
1773
+ window.__wt_user_notes.push({ text: msg.text, time: Date.now() });
1774
+ }
1775
+
1776
+ if (msg.type === 'GET_SEMANTIC_MAP') {
1777
+ const map = window.__Weavetab_fullMap();
1778
+ if (window.__wt_user_notes && window.__wt_user_notes.length > 0) {
1779
+ // Keep notes for 2 minutes
1780
+ window.__wt_user_notes = window.__wt_user_notes.filter(n => Date.now() - n.time < 120000);
1781
+ const notesMap = window.__wt_user_notes.map((note, i) => ({
1782
+ id: `user-msg-${i}`,
1783
+ role: 'alert',
1784
+ tag: 'div',
1785
+ text: `URGENT MESSAGE FROM USER: "${note.text}"`,
1786
+ coords: [0, 0, 100, 100],
1787
+ interactive: false
1788
+ }));
1789
+ map.unshift(...notesMap);
1790
+ }
1791
+ activateXRay(map.filter(m => m.coords && m.text).slice(0, 12));
1792
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: map } }));
1793
+ }
1794
+
1795
+ if (msg.type === 'RESOLVE_EXTENSION_URL') {
1796
+ var url = '';
1797
+ try { url = browserAPI.runtime.getURL((msg.page || 'dashboard') + '.html'); } catch(e) {}
1798
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: url } }));
1799
+ }
1800
+
1801
+ if (msg.type === 'Weavetab_CONFIG') {
1802
+ window.__wt_devMode = !!msg.devMode;
1803
+ window.dispatchEvent(new CustomEvent('wt-config-sync'));
1804
+ __wt_safeSendMessage(msg);
1805
+ }
1806
+
1807
+ if (msg.type === 'Weavetab_GROUP_STATE') {
1808
+ __wt_safeSendMessage(msg);
1809
+ // Only ever blue dot — no yellow. Idle state = no dot.
1810
+ const dotColor = (msg.state === 'idle') ? null : '#3B82F6';
1811
+ if (dotColor) {
1812
+ controlledIndicator.show(dotColor);
1813
+ }
1814
+ updateFaviconDot(msg.state, msg.isActive);
1815
+ // Parked tabs: clear thought bubble and reading glow
1816
+ if (msg.isActive === false) {
1817
+ __wt_removeThought();
1818
+ __wt_hideReadingGlow();
1819
+ }
1820
+ if (msg.state === 'idle') {
1821
+ __wt_hideReadingGlow();
1822
+ __wt_destroyBubble();
1823
+ __wt_feedbackSticky = false;
1824
+ __wt_processThoughtQueue();
1825
+ restoreOriginalFavicon();
1826
+ // Hide the persistent indicator bar when idle
1827
+ controlledIndicator.hide();
1828
+ __weaveCursor.setState('idle');
1829
+ } else if (msg.state === 'weaving') {
1830
+ // Show persistent blue bar while agent works
1831
+ controlledIndicator.showPersistent('#3B82F6');
1832
+ __weaveCursor.setState('eval');
1833
+ } else if (msg.state === 'stuck') {
1834
+ // Still show blue bar (not red) when stuck
1835
+ controlledIndicator.showPersistent('#3B82F6');
1836
+ __weaveCursor.setState('stuck');
1837
+ }
1838
+ }
1839
+
1840
+
1841
+ if (msg.type === 'Weavetab_THOUGHT') {
1842
+ __wt_showThought(msg.text, { active: msg.active });
1843
+ }
1844
+
1845
+ if (msg.type === 'Weavetab_ASK') {
1846
+ // Legacy support — route to new unified question system
1847
+ console.log('[Weavetab] Received ASK request (legacy):', msg.question);
1848
+ __wt_showQuestion(msg.question, 'sentence', msg.timeout || 120);
1849
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-out', { detail: { id: msg.id, data: { status: 'shown' } } }));
1850
+ }
1851
+
1852
+ if (msg.type === 'Weavetab_CLEAR_THOUGHT') {
1853
+ __wt_removeThought();
1854
+ __wt_hideReadingGlow();
1855
+ __wt_processThoughtQueue();
1856
+ }
1857
+
1858
+ if (msg.type === 'Weavetab_RELOAD_EXTENSION') {
1859
+ __wt_safeSendMessage({ type: 'RELOAD_EXTENSION' });
1860
+ }
1861
+ }
1862
+
1863
+ // Channel 1: CustomEvent from MAIN world (used by fallback + sendToExtension)
1864
+ window.addEventListener('Weavetab-msg-in', (e) => {
1865
+ __wt_handleWeavetabMsg(e.detail);
1866
+ });
1867
+
1868
+ // Channel 2: postMessage from MAIN world (cross-world MV3 bridge)
1869
+ window.addEventListener('message', (event) => {
1870
+ if (event.data && event.data.type === 'Weavetab-msg-in' && event.data.data) {
1871
+ __wt_handleWeavetabMsg(event.data.data);
1872
+ }
1873
+ // ── User feedback relay: WeaveTab OS panel → background storage queue ────
1874
+ // When the user types in the chat panel and hits Send, submitFeedback() fires
1875
+ // a __WT_FEEDBACK postMessage. We relay it to the background so it lands in
1876
+ // chrome.storage.local and window.__Weavetab.popUserFeedback() can return it.
1877
+ if (event.data && event.data.type === '__WT_FEEDBACK' && event.data.text) {
1878
+ try {
1879
+ if (browserAPI.runtime && browserAPI.runtime.id) {
1880
+ browserAPI.runtime.sendMessage({ type: 'USER_FEEDBACK', text: event.data.text }).catch(function(){});
1881
+ }
1882
+ } catch(e) {}
1883
+ }
1884
+ });
1885
+
1886
+ // ── FAVICON STATUS DOT INDICATOR ───────────────────────────────
1887
+ let wtFaviconObserver = null;
1888
+
1889
+ let __wt_faviconStaleTimer = null;
1890
+
1891
+ function updateFaviconDot(state, isActive) {
1892
+ if (__wt_faviconStaleTimer) { clearTimeout(__wt_faviconStaleTimer); __wt_faviconStaleTimer = null; }
1893
+
1894
+ if (state === 'idle' || !state || isActive === false) {
1895
+ if (wtFaviconObserver) {
1896
+ wtFaviconObserver.disconnect();
1897
+ wtFaviconObserver = null;
1898
+ }
1899
+ restoreOriginalFavicon();
1900
+ return;
1901
+ }
1902
+
1903
+ // Always use blue — never red or yellow for favicon dot
1904
+ const color = '#3B82F6';
1905
+
1906
+ if (!wtFaviconObserver) {
1907
+ wtFaviconObserver = new MutationObserver(() => {
1908
+ wtFaviconObserver.disconnect();
1909
+ applyFaviconDot(color);
1910
+ wtFaviconObserver.observe(document.head, { childList: true, subtree: true });
1911
+ });
1912
+ } else {
1913
+ wtFaviconObserver.disconnect();
1914
+ }
1915
+
1916
+ applyFaviconDot(color);
1917
+ wtFaviconObserver.observe(document.head, { childList: true, subtree: true });
1918
+
1919
+ // Set stale timer: clear dot + hide glow if no message for 30s
1920
+ __wt_faviconStaleTimer = setTimeout(function() {
1921
+ restoreOriginalFavicon();
1922
+ __wt_hideReadingGlow();
1923
+ __wt_destroyBubble();
1924
+ }, 30000);
1925
+ }
1926
+
1927
+ function restoreOriginalFavicon() {
1928
+ const customLink = document.getElementById('wt-custom-favicon');
1929
+ if (customLink) customLink.remove();
1930
+
1931
+ const links = document.querySelectorAll('link[rel*="icon"]');
1932
+ links.forEach(link => {
1933
+ if (link.dataset.wtOrigHref) {
1934
+ link.href = link.dataset.wtOrigHref;
1935
+ delete link.dataset.wtOrigHref;
1936
+ }
1937
+ });
1938
+ }
1939
+
1940
+ function applyFaviconDot(color) {
1941
+ const links = Array.from(document.querySelectorAll('link[rel*="icon"]')).filter(l => l.id !== 'wt-custom-favicon');
1942
+ let originalHref = '';
1943
+
1944
+ if (links.length > 0) {
1945
+ const link = links[0];
1946
+ if (!link.dataset.wtOrigHref) {
1947
+ link.dataset.wtOrigHref = link.href;
1948
+ }
1949
+ originalHref = link.dataset.wtOrigHref;
1950
+ } else {
1951
+ originalHref = '/favicon.ico';
1952
+ }
1953
+
1954
+ const canvas = document.createElement('canvas');
1955
+ canvas.width = 32;
1956
+ canvas.height = 32;
1957
+ const ctx = canvas.getContext('2d');
1958
+
1959
+ const img = new Image();
1960
+ img.crossOrigin = 'anonymous';
1961
+ img.onload = () => {
1962
+ try {
1963
+ ctx.drawImage(img, 0, 0, 32, 32);
1964
+ drawDot(ctx, color);
1965
+ setFaviconHref(canvas.toDataURL('image/png'));
1966
+ } catch (e) {
1967
+ drawFallbackIcon(ctx, color);
1968
+ setFaviconHref(canvas.toDataURL('image/png'));
1969
+ }
1970
+ };
1971
+ img.onerror = () => {
1972
+ drawFallbackIcon(ctx, color);
1973
+ setFaviconHref(canvas.toDataURL('image/png'));
1974
+ };
1975
+ img.src = originalHref;
1976
+ }
1977
+
1978
+ function drawDot(ctx, color) {
1979
+ // Solid dot
1980
+ ctx.beginPath();
1981
+ ctx.arc(24, 24, 5.5, 0, 2 * Math.PI);
1982
+ ctx.fillStyle = color;
1983
+ ctx.fill();
1984
+ }
1985
+
1986
+ function drawFallbackIcon(ctx, color) {
1987
+ ctx.clearRect(0, 0, 32, 32);
1988
+ ctx.beginPath();
1989
+ ctx.arc(16, 16, 11, 0, 2 * Math.PI);
1990
+ ctx.fillStyle = '#1E1F22';
1991
+ ctx.fill();
1992
+ ctx.strokeStyle = '#3C3F45';
1993
+ ctx.lineWidth = 1.5;
1994
+ ctx.stroke();
1995
+
1996
+ ctx.beginPath();
1997
+ ctx.arc(16, 16, 6, 0, 2 * Math.PI);
1998
+ ctx.fillStyle = '#7C3AED';
1999
+ ctx.fill();
2000
+
2001
+ drawDot(ctx, color);
2002
+ }
2003
+
2004
+ function setFaviconHref(dataUrl) {
2005
+ let customLink = document.getElementById('wt-custom-favicon');
2006
+ if (!customLink) {
2007
+ customLink = document.createElement('link');
2008
+ customLink.id = 'wt-custom-favicon';
2009
+ customLink.rel = 'icon';
2010
+ customLink.type = 'image/png';
2011
+ (document.head || document.documentElement).appendChild(customLink);
2012
+ }
2013
+ customLink.href = dataUrl;
2014
+
2015
+ const links = document.querySelectorAll('link[rel*="icon"]');
2016
+ links.forEach(link => {
2017
+ if (link.id !== 'wt-custom-favicon') {
2018
+ if (!link.dataset.wtOrigHref) {
2019
+ link.dataset.wtOrigHref = link.href;
2020
+ }
2021
+ link.href = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
2022
+ }
2023
+ });
2024
+ }
2025
+
2026
+
2027
+
2028
+ // ── PERSISTENT MESSAGING PANEL (WeaveTab OS v2.5.0) ───────────────
2029
+ // Design inspired by designs/elements.html
2030
+ (function initWeaveTabUI() {
2031
+ if (window.self !== window.top) return;
2032
+ if (document.getElementById('wt-main-container')) return;
2033
+
2034
+ const version = "2.5.0";
2035
+ const isBeta = true;
2036
+
2037
+ // ── CSS Injection ─────────────────────────────────────────────
2038
+ const style = document.createElement('style');
2039
+ style.id = 'wt-ui-styles';
2040
+ style.textContent = `
2041
+ /* ── Sidepanel Tokens ────────────────────────────────────── */
2042
+ :host, .wt-sp-root { all: initial; }
2043
+ .wt-sp-root, .wt-sp-root * { box-sizing: border-box; margin: 0; padding: 0; }
2044
+
2045
+ .wt-sp-root {
2046
+ --sp-bg: #ffffff;
2047
+ --sp-bg-panel: #f9f9fb;
2048
+ --sp-bg-item: #f4f4f5;
2049
+ --sp-bg-hover: #e8e8ec;
2050
+ --sp-border: #e4e4e7;
2051
+ --sp-border-hi: #d4d4d8;
2052
+ --sp-text: #18181b;
2053
+ --sp-text-secondary: #52525b;
2054
+ --sp-text-muted: #a1a1aa;
2055
+ --sp-blue: #3b82f6;
2056
+ --sp-blue-light: #60a5fa;
2057
+ --sp-green: #16a34a;
2058
+ --sp-green-bg: #f0fdf4;
2059
+ --sp-red: #ef4444;
2060
+ --sp-red-bg: #fef2f2;
2061
+ --sp-yellow: #f59e0b;
2062
+ --sp-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 4px 12px rgba(0,0,0,0.06);
2063
+ --sp-font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif;
2064
+ --sp-font-mono: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
2065
+ }
2066
+
2067
+ .wt-sp-root[data-theme='dark'] {
2068
+ --sp-bg: #09090b;
2069
+ --sp-bg-panel: #111113;
2070
+ --sp-bg-item: #1c1c1f;
2071
+ --sp-bg-hover: #27272a;
2072
+ --sp-border: #27272a;
2073
+ --sp-border-hi: #3f3f46;
2074
+ --sp-text: #fafafa;
2075
+ --sp-text-secondary: #a1a1aa;
2076
+ --sp-text-muted: #52525b;
2077
+ --sp-green-bg: #052e16;
2078
+ --sp-red-bg: #450a0a;
2079
+ --sp-shadow: 0 1px 3px rgba(0,0,0,0.4), 0 4px 12px rgba(0,0,0,0.3);
2080
+ }
2081
+
2082
+ /* ── Panel Shell ─────────────────────────────────────────── */
2083
+ .wt-sp-root {
2084
+ position: fixed;
2085
+ top: 0; bottom: 0; right: 0;
2086
+ width: 360px;
2087
+ display: none !important;
2088
+ flex-direction: column;
2089
+ background: var(--sp-bg);
2090
+ border-left: 1px solid var(--sp-border);
2091
+ z-index: 2147483646;
2092
+ pointer-events: auto;
2093
+ font-family: var(--sp-font);
2094
+ color: var(--sp-text);
2095
+
2096
+ transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
2097
+ overflow: hidden;
2098
+ }
2099
+
2100
+ .wt-sp-root.minimized {
2101
+ transform: translateX(100%);
2102
+ pointer-events: none;
2103
+ }
2104
+
2105
+ /* ── Header ──────────────────────────────────────────────── */
2106
+ .wt-sp-header {
2107
+ display: flex;
2108
+ justify-content: space-between;
2109
+ align-items: center;
2110
+ padding: 10px 14px;
2111
+ border-bottom: 1px solid var(--sp-border);
2112
+ flex-shrink: 0;
2113
+ -webkit-app-region: drag;
2114
+ }
2115
+
2116
+ .wt-sp-brand {
2117
+ font-size: 13px;
2118
+ font-weight: 600;
2119
+ color: var(--sp-text);
2120
+ letter-spacing: -0.01em;
2121
+ }
2122
+
2123
+ .wt-sp-controls {
2124
+ display: flex;
2125
+ align-items: center;
2126
+ gap: 2px;
2127
+ -webkit-app-region: no-drag;
2128
+ }
2129
+
2130
+ .wt-sp-ctrl-btn {
2131
+ width: 28px; height: 28px;
2132
+ display: flex; align-items: center; justify-content: center;
2133
+ background: transparent;
2134
+ border: none;
2135
+ border-radius: 6px;
2136
+ color: var(--sp-text-muted);
2137
+ cursor: pointer;
2138
+ transition: background 0.15s, color 0.15s;
2139
+ font-size: 14px;
2140
+ }
2141
+ .wt-sp-ctrl-btn:hover { background: var(--sp-bg-hover); color: var(--sp-text); }
2142
+
2143
+ /* ── Badge Row ───────────────────────────────────────────── */
2144
+ .wt-sp-badges {
2145
+ display: flex;
2146
+ align-items: center;
2147
+ gap: 6px;
2148
+ padding: 6px 14px;
2149
+ border-bottom: 1px solid var(--sp-border);
2150
+ flex-shrink: 0;
2151
+ }
2152
+
2153
+ .wt-sp-badge {
2154
+ font-size: 9px;
2155
+ font-weight: 600;
2156
+ padding: 2px 6px;
2157
+ border-radius: 4px;
2158
+ text-transform: uppercase;
2159
+ letter-spacing: 0.03em;
2160
+ }
2161
+ .wt-sp-badge-dev { background: var(--sp-blue); color: #fff; }
2162
+ .wt-sp-badge-beta { background: var(--sp-yellow); color: #fff; }
2163
+
2164
+ /* ── Body (scrollable) ───────────────────────────────────── */
2165
+ .wt-sp-body {
2166
+ flex: 1;
2167
+ overflow-y: auto;
2168
+ display: flex;
2169
+ flex-direction: column;
2170
+ }
2171
+
2172
+ .wt-sp-body::-webkit-scrollbar { width: 5px; }
2173
+ .wt-sp-body::-webkit-scrollbar-track { background: transparent; }
2174
+ .wt-sp-body::-webkit-scrollbar-thumb { background: var(--sp-border-hi); border-radius: 3px; }
2175
+
2176
+ /* ── Section ─────────────────────────────────────────────── */
2177
+ .wt-sp-section {
2178
+ border-bottom: 1px solid var(--sp-border);
2179
+ }
2180
+
2181
+ .wt-sp-section-hdr {
2182
+ display: flex;
2183
+ align-items: center;
2184
+ gap: 6px;
2185
+ padding: 8px 14px;
2186
+ font-size: 11px;
2187
+ font-weight: 600;
2188
+ color: var(--sp-text-secondary);
2189
+ text-transform: uppercase;
2190
+ letter-spacing: 0.04em;
2191
+ cursor: pointer;
2192
+ user-select: none;
2193
+ transition: color 0.15s;
2194
+ }
2195
+ .wt-sp-section-hdr:hover { color: var(--sp-text); }
2196
+
2197
+ .wt-sp-chevron {
2198
+ font-size: 10px;
2199
+ transition: transform 0.2s ease;
2200
+ display: inline-block;
2201
+ color: var(--sp-text-muted);
2202
+ }
2203
+ .wt-sp-section.closed .wt-sp-chevron { transform: rotate(-90deg); }
2204
+
2205
+ .wt-sp-section-body {
2206
+ overflow: hidden;
2207
+ transition: max-height 0.25s cubic-bezier(0.32, 0.72, 0, 1);
2208
+ max-height: 500px;
2209
+ }
2210
+ .wt-sp-section.closed .wt-sp-section-body { max-height: 0; }
2211
+
2212
+ .wt-sp-section-grow { flex: 1; display: flex; flex-direction: column; }
2213
+ .wt-sp-section-grow .wt-sp-section-body { flex: 1; overflow-y: auto; max-height: none; }
2214
+
2215
+ /* ── Tasks ────────────────────────────────────────────────── */
2216
+ .wt-sp-tasks {
2217
+ display: flex;
2218
+ flex-direction: column;
2219
+ padding: 0 14px 10px;
2220
+ gap: 2px;
2221
+ }
2222
+
2223
+ .wt-sp-task {
2224
+ display: flex;
2225
+ align-items: flex-start;
2226
+ gap: 8px;
2227
+ padding: 6px 8px;
2228
+ border-radius: 6px;
2229
+ font-size: 12px;
2230
+ line-height: 1.4;
2231
+ cursor: pointer;
2232
+ transition: background 0.15s;
2233
+ }
2234
+ .wt-sp-task:hover { background: var(--sp-bg-hover); }
2235
+
2236
+ .wt-sp-task.done { color: var(--sp-green); }
2237
+ .wt-sp-task.active { color: var(--sp-text); background: var(--sp-bg-item); border-left: 2px solid var(--sp-blue); padding-left: 6px; }
2238
+ .wt-sp-task.pending { color: var(--sp-text-muted); }
2239
+
2240
+ .wt-sp-task-icon { flex-shrink: 0; width: 16px; text-align: center; font-size: 11px; margin-top: 1px; }
2241
+ .wt-sp-task.done .wt-sp-task-icon { color: var(--sp-green); }
2242
+ .wt-sp-task.active .wt-sp-task-icon { color: var(--sp-blue); }
2243
+
2244
+ .wt-sp-task-desc { flex: 1; word-break: break-word; }
2245
+ .wt-sp-task.done .wt-sp-task-desc { text-decoration: line-through; opacity: 0.7; }
2246
+
2247
+ .wt-sp-task-empty {
2248
+ font-size: 11px;
2249
+ color: var(--sp-text-muted);
2250
+ padding: 8px;
2251
+ text-align: center;
2252
+ }
2253
+
2254
+ /* ── Activity Log ────────────────────────────────────────── */
2255
+ .wt-sp-log {
2256
+ display: flex;
2257
+ flex-direction: column;
2258
+ gap: 1px;
2259
+ padding: 0 14px 10px;
2260
+ font-family: var(--sp-font-mono);
2261
+ }
2262
+
2263
+ .wt-sp-log-entry {
2264
+ display: flex;
2265
+ align-items: flex-start;
2266
+ gap: 8px;
2267
+ padding: 5px 8px;
2268
+ border-radius: 4px;
2269
+ font-size: 11px;
2270
+ line-height: 1.4;
2271
+ transition: background 0.1s;
2272
+ }
2273
+ .wt-sp-log-entry:hover { background: var(--sp-bg-hover); }
2274
+
2275
+ .wt-sp-log-icon { flex-shrink: 0; width: 14px; text-align: center; margin-top: 1px; font-size: 10px; }
2276
+ .wt-sp-log-icon.running { color: var(--sp-blue); }
2277
+ .wt-sp-log-icon.done { color: var(--sp-green); }
2278
+ .wt-sp-log-icon.error { color: var(--sp-red); }
2279
+
2280
+ .wt-sp-log-name {
2281
+ font-weight: 500;
2282
+ color: var(--sp-text);
2283
+ white-space: nowrap;
2284
+ overflow: hidden;
2285
+ text-overflow: ellipsis;
2286
+ max-width: 140px;
2287
+ }
2288
+
2289
+ .wt-sp-log-badge {
2290
+ font-size: 9px;
2291
+ font-weight: 600;
2292
+ padding: 1px 5px;
2293
+ border-radius: 3px;
2294
+ text-transform: uppercase;
2295
+ letter-spacing: 0.02em;
2296
+ flex-shrink: 0;
2297
+ }
2298
+ .wt-sp-log-badge.running { background: rgba(59,130,246,0.12); color: var(--sp-blue); }
2299
+ .wt-sp-log-badge.done { background: rgba(22,163,74,0.12); color: var(--sp-green); }
2300
+ .wt-sp-log-badge.error { background: rgba(239,68,68,0.12); color: var(--sp-red); }
2301
+ .wt-sp-log-badge.info { background: var(--sp-bg-item); color: var(--sp-text-muted); }
2302
+
2303
+ .wt-sp-log-time {
2304
+ font-size: 10px;
2305
+ color: var(--sp-text-muted);
2306
+ margin-left: auto;
2307
+ flex-shrink: 0;
2308
+ }
2309
+
2310
+ .wt-sp-log-empty {
2311
+ font-size: 11px;
2312
+ color: var(--sp-text-muted);
2313
+ padding: 8px;
2314
+ text-align: center;
2315
+ font-family: var(--sp-font);
2316
+ }
2317
+
2318
+ /* ── Chat ─────────────────────────────────────────────────── */
2319
+ .wt-sp-chat {
2320
+ padding: 10px 14px;
2321
+ border-top: 1px solid var(--sp-border);
2322
+ flex-shrink: 0;
2323
+ }
2324
+
2325
+ .wt-sp-chat-row {
2326
+ display: flex;
2327
+ gap: 6px;
2328
+ align-items: flex-end;
2329
+ }
2330
+
2331
+ .wt-sp-chat-input {
2332
+ flex: 1;
2333
+ padding: 8px 12px;
2334
+ background: var(--sp-bg-item);
2335
+ border: 1px solid var(--sp-border);
2336
+ border-radius: 8px;
2337
+ color: var(--sp-text);
2338
+ font-family: var(--sp-font);
2339
+ font-size: 12px;
2340
+ outline: none;
2341
+ resize: none;
2342
+ min-height: 36px;
2343
+ max-height: 100px;
2344
+ transition: border-color 0.15s;
2345
+ line-height: 1.4;
2346
+ }
2347
+ .wt-sp-chat-input::placeholder { color: var(--sp-text-muted); }
2348
+ .wt-sp-chat-input:focus { border-color: var(--sp-blue); }
2349
+
2350
+ .wt-sp-send-btn {
2351
+ width: 36px; height: 36px;
2352
+ display: flex; align-items: center; justify-content: center;
2353
+ background: var(--sp-blue);
2354
+ border: none;
2355
+ border-radius: 8px;
2356
+ cursor: pointer;
2357
+ flex-shrink: 0;
2358
+ transition: background 0.15s, transform 0.1s;
2359
+ }
2360
+ .wt-sp-send-btn:hover { background: #2563eb; }
2361
+ .wt-sp-send-btn:active { transform: scale(0.95); }
2362
+ .wt-sp-send-btn.sent { background: var(--sp-green); }
2363
+ .wt-sp-send-btn svg { width: 16px; height: 16px; stroke: #fff; fill: none; }
2364
+
2365
+ /* ── Debug Section ───────────────────────────────────────── */
2366
+ .wt-sp-debug-area {
2367
+ padding: 0 14px 10px;
2368
+ position: relative;
2369
+ min-height: 200px;
2370
+ }
2371
+
2372
+ .wt-sp-f3-btn {
2373
+ margin-left: auto;
2374
+ font-size: 9px;
2375
+ font-weight: 600;
2376
+ padding: 2px 6px;
2377
+ border-radius: 4px;
2378
+ background: var(--sp-bg-item);
2379
+ border: 1px solid var(--sp-border);
2380
+ color: var(--sp-text-muted);
2381
+ cursor: pointer;
2382
+ text-transform: uppercase;
2383
+ transition: all 0.15s;
2384
+ }
2385
+ .wt-sp-f3-btn:hover { color: var(--sp-text); border-color: var(--sp-border-hi); }
2386
+
2387
+ /* ── System Icon (minimized state) ───────────────────────── */
2388
+ #wt-system-icon {
2389
+ position: fixed;
2390
+ bottom: 20px;
2391
+ right: 20px;
2392
+ width: 40px;
2393
+ height: 40px;
2394
+ background: var(--sp-bg);
2395
+ border: 1px solid var(--sp-border);
2396
+ border-radius: 10px;
2397
+ display: none;
2398
+ justify-content: center;
2399
+ align-items: center;
2400
+ cursor: pointer;
2401
+ z-index: 2147483640;
2402
+ box-shadow: var(--sp-shadow);
2403
+ transition: background 0.15s, transform 0.15s;
2404
+ pointer-events: auto;
2405
+ }
2406
+ #wt-system-icon:hover { background: var(--sp-bg-hover); transform: scale(1.05); }
2407
+ #wt-system-icon svg { width: 18px; height: 18px; fill: var(--sp-text-muted); }
2408
+
2409
+ /* ── Utilities ────────────────────────────────────────────── */
2410
+ .wt-sp-root .hidden, .wt-sp-root.hidden, .wt-hidden { display: none !important; }
2411
+
2412
+ @keyframes wt-sp-spin {
2413
+ from { transform: rotate(0deg); }
2414
+ to { transform: rotate(360deg); }
2415
+ }
2416
+ .wt-sp-spinner {
2417
+ display: inline-block;
2418
+ width: 10px; height: 10px;
2419
+ border: 1.5px solid var(--sp-border-hi);
2420
+ border-top-color: var(--sp-blue);
2421
+ border-radius: 50%;
2422
+ animation: wt-sp-spin 0.8s linear infinite;
2423
+ }
2424
+ `;
2425
+ (document.head || document.documentElement).appendChild(style);
2426
+
2427
+ // ── HTML Construction ─────────────────────────────────────────
2428
+ const container = document.createElement('div');
2429
+ container.id = 'wt-main-container';
2430
+ container.className = 'wt-sp-root';
2431
+ container.setAttribute('data-wt-ignore', 'true');
2432
+ container.setAttribute('data-theme', 'dark');
2433
+
2434
+ container.innerHTML = `
2435
+ <div class="wt-sp-header">
2436
+ <span class="wt-sp-brand">Weavetab</span>
2437
+ <div class="wt-sp-controls">
2438
+ <button class="wt-sp-ctrl-btn" id="wt-theme-toggle" title="Toggle theme">
2439
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
2440
+ </button>
2441
+ <button class="wt-sp-ctrl-btn" id="wt-minimize-btn" title="Minimize">
2442
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>
2443
+ </button>
2444
+ </div>
2445
+ </div>
2446
+ <div class="wt-sp-badges">
2447
+ <span class="wt-sp-badge wt-sp-badge-dev hidden" id="wt-badge-dev">DEV</span>
2448
+ ${isBeta ? '<span class="wt-sp-badge wt-sp-badge-beta">BETA</span>' : ''}
2449
+ </div>
2450
+ <div class="wt-sp-body">
2451
+ <div class="wt-sp-section" id="wt-sec-tasks">
2452
+ <div class="wt-sp-section-hdr" id="wt-header-tasks">
2453
+ <span class="wt-sp-chevron">›</span>
2454
+ <span>Tasks</span>
2455
+ </div>
2456
+ <div class="wt-sp-section-body">
2457
+ <div class="wt-sp-tasks" id="wt-todo-list">
2458
+ <div class="wt-sp-task-empty">No active mission</div>
2459
+ </div>
2460
+ </div>
2461
+ </div>
2462
+
2463
+ <div class="wt-sp-section wt-sp-section-grow" id="wt-sec-logs">
2464
+ <div class="wt-sp-section-hdr" id="wt-header-logs">
2465
+ <span class="wt-sp-chevron">›</span>
2466
+ <span>Activity</span>
2467
+ </div>
2468
+ <div class="wt-sp-section-body">
2469
+ <div class="wt-sp-log" id="wt-log-container">
2470
+ <div class="wt-sp-log-empty">Waiting for activity...</div>
2471
+ </div>
2472
+ </div>
2473
+ </div>
2474
+
2475
+ <div class="wt-sp-section closed" id="wt-sec-debug">
2476
+ <div class="wt-sp-section-hdr" id="wt-header-debug">
2477
+ <span class="wt-sp-chevron">›</span>
2478
+ <span>Debug</span>
2479
+ </div>
2480
+ <div class="wt-sp-section-body">
2481
+ <div class="wt-sp-debug-area" id="wt-debug-content-area"></div>
2482
+ </div>
2483
+ </div>
2484
+ </div>
2485
+
2486
+ <div class="wt-sp-chat">
2487
+ <div class="wt-sp-chat-row">
2488
+ <textarea class="wt-sp-chat-input" id="wt-msg-input" placeholder="Send feedback..." rows="1"></textarea>
2489
+ <button class="wt-sp-send-btn" id="wt-send-btn">
2490
+ <svg viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
2491
+ </button>
2492
+ </div>
2493
+ </div>
2494
+ `;
2495
+ __wt_safeAppendToBody(container);
2496
+
2497
+ const systemIcon = document.createElement('div');
2498
+ systemIcon.id = 'wt-system-icon';
2499
+ systemIcon.title = 'Open Weavetab';
2500
+ systemIcon.innerHTML = `
2501
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2502
+ <path d="M4 4h16v16H4V4zm2 2v12h12V6H6zm2 3h3v2H8V9zm4 4h4v2h-4v-2z" />
2503
+ </svg>
2504
+ `;
2505
+ __wt_safeAppendToBody(systemIcon);
2506
+
2507
+ // ── PERSISTENT ACTIVITY LOG ──────────────────────────────────
2508
+ // Logs never clear until browser close — append-only like Claude Chrome
2509
+ window.__wt_logEntries = [];
2510
+ const logContainer = document.getElementById('wt-log-container');
2511
+
2512
+ function __wt_addLogEntry(name, status, detail) {
2513
+ const entry = {
2514
+ name: name || 'unknown',
2515
+ status: status || 'info',
2516
+ detail: detail || '',
2517
+ time: Date.now()
2518
+ };
2519
+ window.__wt_logEntries.push(entry);
2520
+ __wt_renderLogEntry(entry);
2521
+ // Auto-scroll to bottom
2522
+ if (logContainer) {
2523
+ logContainer.scrollTop = logContainer.scrollHeight;
2524
+ }
2525
+ }
2526
+
2527
+ function __wt_renderLogEntry(entry) {
2528
+ if (!logContainer) return;
2529
+ // Remove empty placeholder on first real entry
2530
+ const empty = logContainer.querySelector('.wt-sp-log-empty');
2531
+ if (empty) empty.remove();
2532
+
2533
+ const el = document.createElement('div');
2534
+ el.className = 'wt-sp-log-entry';
2535
+
2536
+ const iconClass = entry.status === 'done' ? 'done' : entry.status === 'error' ? 'error' : entry.status === 'running' ? 'running' : '';
2537
+ const icon = entry.status === 'running' ? '<span class="wt-sp-spinner"></span>'
2538
+ : entry.status === 'done' ? '&#10003;'
2539
+ : entry.status === 'error' ? '&#10007;'
2540
+ : '&#9679;';
2541
+
2542
+ const badge = entry.status === 'running' ? 'RUNNING'
2543
+ : entry.status === 'done' ? 'OK'
2544
+ : entry.status === 'error' ? 'ERR'
2545
+ : '';
2546
+
2547
+ const timeStr = new Date(entry.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
2548
+
2549
+ el.innerHTML = `
2550
+ <span class="wt-sp-log-icon ${iconClass}">${icon}</span>
2551
+ <span class="wt-sp-log-name">${__wt_escapeHtml(entry.name)}</span>
2552
+ ${badge ? `<span class="wt-sp-log-badge ${entry.status}">${badge}</span>` : ''}
2553
+ <span class="wt-sp-log-time">${timeStr}</span>
2554
+ `;
2555
+
2556
+ if (entry.detail) {
2557
+ const detailEl = document.createElement('div');
2558
+ detailEl.style.cssText = 'width:100%;font-size:10px;color:var(--sp-text-muted);white-space:pre-wrap;word-break:break-all;margin-top:2px;';
2559
+ detailEl.textContent = entry.detail;
2560
+ el.appendChild(detailEl);
2561
+ }
2562
+
2563
+ logContainer.appendChild(el);
2564
+ }
2565
+
2566
+ function __wt_escapeHtml(str) {
2567
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2568
+ }
2569
+
2570
+ // ── THEME SYSTEM ──────────────────────────────────────────────
2571
+ function __wt_applyTheme(theme) {
2572
+ container.setAttribute('data-theme', theme);
2573
+ document.getElementById('wt-system-icon') && document.getElementById('wt-system-icon').setAttribute('data-theme', theme);
2574
+ try { browserAPI.storage.local.set({ wt_theme: theme }); } catch(e) {}
2575
+ }
2576
+
2577
+ function __wt_toggleTheme() {
2578
+ const current = container.getAttribute('data-theme');
2579
+ __wt_applyTheme(current === 'dark' ? 'light' : 'dark');
2580
+ }
2581
+
2582
+ // Restore persisted theme
2583
+ try {
2584
+ browserAPI.storage.local.get(['wt_theme'], function(res) {
2585
+ if (res.wt_theme) {
2586
+ __wt_applyTheme(res.wt_theme);
2587
+ }
2588
+ });
2589
+ } catch(e) {}
2590
+
2591
+
2592
+
2593
+ // ── HUD STATUS INTERCEPTION (for activity log) ────────────────
2594
+ // Capture Weavetab_STATE hud updates and add to log
2595
+ window.addEventListener('message', function(e) {
2596
+ if (e.data && e.data.type === 'Weavetab-msg-in' && e.data.data && e.data.data.type === 'Weavetab_STATE' && e.data.data.hud) {
2597
+ const hud = e.data.data.hud;
2598
+ if (hud.action && hud.action.length > 2) {
2599
+ __wt_addLogEntry(hud.action, 'info', hud.detail || '');
2600
+ }
2601
+ }
2602
+ });
2603
+
2604
+
2605
+
2606
+ // ── Panel State Persistence ────────────────────────────────────
2607
+ // Sidepanel always opens on browser launch (background clears state on startup).
2608
+ // Persists minimized state across page navigations within a session.
2609
+ function minimizePanel() {
2610
+ container.classList.add('minimized');
2611
+ systemIcon.style.display = 'flex';
2612
+ try { browserAPI.storage.local.set({ wt_panel_minimized: true }); } catch(e) {}
2613
+ }
2614
+
2615
+ function restorePanel() {
2616
+ systemIcon.style.display = 'none';
2617
+ container.classList.remove('minimized');
2618
+ try { browserAPI.storage.local.set({ wt_panel_minimized: false }); } catch(e) {}
2619
+ }
2620
+
2621
+ // Restore minimized state from storage (respects user's last action)
2622
+ try {
2623
+ browserAPI.storage.local.get(['wt_panel_minimized'], function(res) {
2624
+ if (res.wt_panel_minimized) {
2625
+ container.classList.add('minimized');
2626
+ systemIcon.style.display = 'flex';
2627
+ }
2628
+ });
2629
+ } catch(e) {}
2630
+
2631
+ function toggleSection(id) {
2632
+ document.getElementById(id).classList.toggle('closed');
2633
+ }
2634
+
2635
+
2636
+
2637
+ function syncDevMode() {
2638
+ const active = !!window.__wt_devMode;
2639
+ const devBadge = document.getElementById('wt-badge-dev');
2640
+
2641
+ if (devBadge) devBadge.classList.toggle('hidden', !active);
2642
+ }
2643
+
2644
+ window.addEventListener('wt-config-sync', syncDevMode);
2645
+
2646
+ container.querySelector('#wt-minimize-btn').onclick = minimizePanel;
2647
+ systemIcon.onclick = restorePanel;
2648
+ container.querySelector('#wt-theme-toggle').onclick = __wt_toggleTheme;
2649
+ container.querySelector('#wt-header-tasks').onclick = () => toggleSection('wt-sec-tasks');
2650
+ container.querySelector('#wt-header-logs').onclick = () => toggleSection('wt-sec-logs');
2651
+ container.querySelector('#wt-header-debug').onclick = () => toggleSection('wt-sec-debug');
2652
+
2653
+
2654
+ syncDevMode();
2655
+
2656
+ const msgInput = container.querySelector('#wt-msg-input');
2657
+ const sendBtn = container.querySelector('#wt-send-btn');
2658
+
2659
+ // Auto-resize textarea
2660
+ msgInput.addEventListener('input', function() {
2661
+ this.style.height = 'auto';
2662
+ this.style.height = Math.min(this.scrollHeight, 100) + 'px';
2663
+ });
2664
+
2665
+ // ── Plan Rendering Integration ───────────────────────────────
2666
+ function renderPlan(action, goal, tasks) {
2667
+ const list = document.getElementById('wt-todo-list');
2668
+ if (!list) return;
2669
+
2670
+ if (action === 'hide' || (!goal && (!tasks || tasks.length === 0))) {
2671
+ list.innerHTML = '<div class="wt-sp-task-empty">No active mission</div>';
2672
+ return;
2673
+ }
2674
+
2675
+ list.innerHTML = '';
2676
+ if (goal) {
2677
+ const goalEl = document.createElement('div');
2678
+ goalEl.className = 'wt-sp-task active';
2679
+ goalEl.style.cssText = 'margin-bottom:4px;font-weight:500;';
2680
+ goalEl.innerHTML = '<span class="wt-sp-task-icon">&#9654;</span><span class="wt-sp-task-desc">' + __wt_escapeHtml(goal) + '</span>';
2681
+ list.appendChild(goalEl);
2682
+ }
2683
+ if (tasks && tasks.length > 0) {
2684
+ tasks.forEach(task => {
2685
+ const item = document.createElement('div');
2686
+ const statusClass = task.status === 'done' ? 'done' : (task.status === 'in-progress' ? 'active' : 'pending');
2687
+ const icon = task.status === 'done' ? '&#10003;' : (task.status === 'in-progress' ? '<span class="wt-sp-spinner"></span>' : '&#9675;');
2688
+
2689
+ item.className = 'wt-sp-task ' + statusClass;
2690
+ item.setAttribute('data-task-id', task.id);
2691
+ item.innerHTML = '<span class="wt-sp-task-icon">' + icon + '</span><span class="wt-sp-task-desc">' + __wt_escapeHtml(task.description) + '</span>';
2692
+
2693
+ list.appendChild(item);
2694
+ });
2695
+ }
2696
+
2697
+ // Log the plan update
2698
+ __wt_addLogEntry('Plan updated', 'info', (tasks ? tasks.length : 0) + ' tasks');
2699
+ }
2700
+
2701
+ window.__wt_plan = renderPlan;
2702
+
2703
+ window.addEventListener('message', (e) => {
2704
+ if (e.data && e.data.type === '__WT_PLAN') {
2705
+ renderPlan(e.data.action, e.data.goal, e.data.tasks);
2706
+ }
2707
+ syncDevMode();
2708
+ });
2709
+
2710
+ setInterval(syncDevMode, 2000);
2711
+
2712
+ try {
2713
+ browserAPI.storage.local.get(['wt_plan_collapsed'], function(res) {});
2714
+ } catch(e) {}
2715
+
2716
+ // Persist draft
2717
+ msgInput.addEventListener('input', function() {
2718
+ try { browserAPI.storage.local.set({ wt_chat_draft: msgInput.value }); } catch(e) {}
2719
+ });
2720
+
2721
+ // Submit
2722
+ function submitFeedback() {
2723
+ var text = msgInput.value.trim();
2724
+ if (!text) return;
2725
+
2726
+ window.postMessage({
2727
+ type: '__WT_FEEDBACK',
2728
+ text: text
2729
+ }, '*');
2730
+
2731
+ window.dispatchEvent(new CustomEvent('Weavetab-msg-in', {
2732
+ detail: {
2733
+ type: 'USER_FEEDBACK',
2734
+ text: text
2735
+ }
2736
+ }));
2737
+
2738
+ try {
2739
+ var existing = JSON.parse(localStorage.getItem('__wt_chat_queue_persist') || '[]');
2740
+ existing.push({ text: text, ts: Date.now() });
2741
+ if (existing.length > 20) existing.splice(0, existing.length - 20);
2742
+ localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(existing));
2743
+ } catch(e) {}
2744
+
2745
+ __wt_addLogEntry('Feedback sent', 'done', text.length > 60 ? text.substring(0, 60) + '...' : text);
2746
+
2747
+ msgInput.value = '';
2748
+ msgInput.style.height = 'auto';
2749
+ try { browserAPI.storage.local.remove('wt_chat_draft'); } catch(e) {}
2750
+ sendBtn.classList.add('sent');
2751
+ setTimeout(function(){ sendBtn.classList.remove('sent'); }, 1200);
2752
+ }
2753
+
2754
+ sendBtn.onclick = submitFeedback;
2755
+ msgInput.addEventListener('keydown', function(e) {
2756
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitFeedback(); }
2757
+ if (e.key === 'Escape') { minimizePanel(); }
2758
+ });
2759
+
2760
+ })();
2761
+
2762
+ // ── GEMINI-STYLE AREA SNIPE & DIRECT ANNOTATION ENGINE ─────────────────────
2763
+ (function initAreaSnipeEngine() {
2764
+ let isSnipeActive = false;
2765
+ let currentSnipeMode = 'dom_target';
2766
+ let overlayEl = null;
2767
+ let marqueeBox = null;
2768
+ let sizeBadge = null;
2769
+ let headerPill = null;
2770
+ let startX = 0, startY = 0;
2771
+ let isDragging = false;
2772
+
2773
+ function createOverlay(mode) {
2774
+ if (overlayEl) overlayEl.remove();
2775
+ currentSnipeMode = mode || 'dom_target';
2776
+
2777
+ overlayEl = document.createElement('div');
2778
+ overlayEl.id = 'wt-snipe-overlay';
2779
+ overlayEl.setAttribute('data-wt-ignore', 'true');
2780
+ overlayEl.style.cssText = `
2781
+ position: fixed; inset: 0; z-index: 2147483647;
2782
+ background: rgba(0, 0, 0, 0.35); backdrop-filter: blur(2px);
2783
+ cursor: crosshair; user-select: none;
2784
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
2785
+ `;
2786
+
2787
+ headerPill = document.createElement('div');
2788
+ headerPill.style.cssText = `
2789
+ position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
2790
+ background: #18181b; border: 1px solid #3f3f46; color: #f4f4f5;
2791
+ font-size: 11px; font-weight: 500; padding: 6px 16px; border-radius: 20px;
2792
+ box-shadow: 0 4px 20px rgba(0,0,0,0.6); pointer-events: none;
2793
+ display: flex; align-items: center; gap: 8px; z-index: 2147483647;
2794
+ `;
2795
+ const modeLabel = currentSnipeMode === 'visual_crop' ? 'Vision Crop' : 'DOM Target';
2796
+ 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>`;
2797
+
2798
+ // Marquee selection box matching img1 Gemini/YouTube Chrome UI style
2799
+ marqueeBox = document.createElement('div');
2800
+ marqueeBox.style.cssText = `
2801
+ position: fixed; border: 1px solid rgba(255, 255, 255, 0.45);
2802
+ border-radius: 12px;
2803
+ box-shadow: 0 0 24px 3px rgba(147, 197, 253, 0.45), inset 0 0 20px rgba(147, 197, 253, 0.2);
2804
+ pointer-events: auto; display: none; z-index: 2147483647;
2805
+ background: rgba(0, 0, 0, 0.1);
2806
+ `;
2807
+
2808
+ // Four thick white rounded corner reticles (matching img1)
2809
+ const corners = [
2810
+ 'top: -2px; left: -2px; border-top: 4px solid #ffffff; border-left: 4px solid #ffffff; border-top-left-radius: 10px;',
2811
+ 'top: -2px; right: -2px; border-top: 4px solid #ffffff; border-right: 4px solid #ffffff; border-top-right-radius: 10px;',
2812
+ 'bottom: -2px; left: -2px; border-bottom: 4px solid #ffffff; border-left: 4px solid #ffffff; border-bottom-left-radius: 10px;',
2813
+ 'bottom: -2px; right: -2px; border-bottom: 4px solid #ffffff; border-right: 4px solid #ffffff; border-bottom-right-radius: 10px;'
2814
+ ];
2815
+ corners.forEach(styleCss => {
2816
+ const c = document.createElement('div');
2817
+ c.style.cssText = `position: absolute; width: 18px; height: 18px; pointer-events: none; ${styleCss}`;
2818
+ marqueeBox.appendChild(c);
2819
+ });
2820
+
2821
+ // Circular close X button on top-right corner
2822
+ const closeBtn = document.createElement('button');
2823
+ closeBtn.style.cssText = `
2824
+ position: absolute; top: -11px; right: -11px;
2825
+ width: 22px; height: 22px; border-radius: 50%;
2826
+ background: rgba(24, 24, 27, 0.95); border: 1px solid rgba(255, 255, 255, 0.25);
2827
+ color: #f4f4f5; cursor: pointer; display: flex; align-items: center; justify-content: center;
2828
+ box-shadow: 0 4px 12px rgba(0,0,0,0.6); z-index: 2147483647; transition: transform 0.15s, background 0.15s;
2829
+ `;
2830
+ 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>`;
2831
+ closeBtn.onclick = function(e) {
2832
+ e.stopPropagation();
2833
+ destroyOverlay();
2834
+ };
2835
+ marqueeBox.appendChild(closeBtn);
2836
+
2837
+ sizeBadge = document.createElement('div');
2838
+ sizeBadge.style.cssText = `
2839
+ position: absolute; bottom: -28px; left: 50%; transform: translateX(-50%);
2840
+ background: rgba(15, 23, 42, 0.9); backdrop-filter: blur(8px); color: #e2e8f0; font-weight: 600;
2841
+ font-size: 10px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; padding: 2px 8px; border-radius: 12px;
2842
+ border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 4px 12px rgba(0,0,0,0.5); pointer-events: none;
2843
+ white-space: nowrap;
2844
+ `;
2845
+ marqueeBox.appendChild(sizeBadge);
2846
+
2847
+ overlayEl.appendChild(headerPill);
2848
+ overlayEl.appendChild(marqueeBox);
2849
+ document.documentElement.appendChild(overlayEl);
2850
+
2851
+ isSnipeActive = true;
2852
+ }
2853
+
2854
+ function destroyOverlay() {
2855
+ if (overlayEl) {
2856
+ overlayEl.remove();
2857
+ overlayEl = null;
2858
+ }
2859
+ isSnipeActive = false;
2860
+ isDragging = false;
2861
+ }
2862
+
2863
+ function handleMouseDown(e) {
2864
+ if (!isSnipeActive || e.button !== 0) return;
2865
+ e.preventDefault();
2866
+ e.stopPropagation();
2867
+
2868
+ isDragging = true;
2869
+ startX = e.clientX;
2870
+ startY = e.clientY;
2871
+
2872
+ marqueeBox.style.left = startX + 'px';
2873
+ marqueeBox.style.top = startY + 'px';
2874
+ marqueeBox.style.width = '0px';
2875
+ marqueeBox.style.height = '0px';
2876
+ marqueeBox.style.display = 'block';
2877
+ }
2878
+
2879
+ function handleMouseMove(e) {
2880
+ if (!isSnipeActive || !isDragging) return;
2881
+ e.preventDefault();
2882
+ e.stopPropagation();
2883
+
2884
+ const currentX = e.clientX;
2885
+ const currentY = e.clientY;
2886
+
2887
+ const left = Math.min(startX, currentX);
2888
+ const top = Math.min(startY, currentY);
2889
+ const width = Math.abs(currentX - startX);
2890
+ const height = Math.abs(currentY - startY);
2891
+
2892
+ marqueeBox.style.left = left + 'px';
2893
+ marqueeBox.style.top = top + 'px';
2894
+ marqueeBox.style.width = width + 'px';
2895
+ marqueeBox.style.height = height + 'px';
2896
+
2897
+ sizeBadge.textContent = `${width} × ${height} px`;
2898
+ }
2899
+
2900
+ function handleMouseUp(e) {
2901
+ if (!isSnipeActive || !isDragging) return;
2902
+ e.preventDefault();
2903
+ e.stopPropagation();
2904
+
2905
+ isDragging = false;
2906
+ const currentX = e.clientX;
2907
+ const currentY = e.clientY;
2908
+
2909
+ const left = Math.min(startX, currentX);
2910
+ const top = Math.min(startY, currentY);
2911
+ const width = Math.abs(currentX - startX);
2912
+ const height = Math.abs(currentY - startY);
2913
+
2914
+ if (width < 10 || height < 10) {
2915
+ destroyOverlay();
2916
+ return;
2917
+ }
2918
+
2919
+ if (currentSnipeMode === 'visual_crop') {
2920
+ captureVisualCrop(left, top, width, height);
2921
+ } else {
2922
+ captureDOMTargets(left, top, width, height);
2923
+ }
2924
+
2925
+ destroyOverlay();
2926
+ }
2927
+
2928
+ function handleKeyDown(e) {
2929
+ if (e.key === 'Escape' && isSnipeActive) {
2930
+ destroyOverlay();
2931
+ }
2932
+ if (e.altKey && (e.key === 's' || e.key === 'S')) {
2933
+ e.preventDefault();
2934
+ createOverlay('dom_target');
2935
+ }
2936
+ }
2937
+
2938
+ window.addEventListener('mousedown', handleMouseDown, true);
2939
+ window.addEventListener('mousemove', handleMouseMove, true);
2940
+ window.addEventListener('mouseup', handleMouseUp, true);
2941
+ window.addEventListener('keydown', handleKeyDown, true);
2942
+
2943
+ // ── Mode 1: DOM Element Intersector ──────────────────────────────────────
2944
+ function captureDOMTargets(left, top, width, height) {
2945
+ const right = left + width;
2946
+ const bottom = top + height;
2947
+ const elements = [];
2948
+
2949
+ const allEls = document.querySelectorAll('button, input, select, textarea, a, h1, h2, h3, h4, h5, h6, p, table, tr, td, th, div, span, [role]');
2950
+ allEls.forEach((el) => {
2951
+ if (el.hasAttribute('data-wt-ignore') || el.closest('#wt-snipe-overlay')) return;
2952
+ const r = el.getBoundingClientRect();
2953
+ if (r.width === 0 || r.height === 0) return;
2954
+
2955
+ const overlaps = !(r.right < left || r.left > right || r.bottom < top || r.top > bottom);
2956
+ if (overlaps) {
2957
+ const text = (el.innerText || el.value || el.getAttribute('aria-label') || '').trim().slice(0, 100);
2958
+ if (text || ['INPUT', 'BUTTON', 'A', 'SELECT', 'TEXTAREA'].includes(el.tagName)) {
2959
+ elements.push({
2960
+ tag: el.tagName.toLowerCase(),
2961
+ selector: getStableSelector(el),
2962
+ ref: el.getAttribute('data-wt-id') || null,
2963
+ text: text,
2964
+ role: el.getAttribute('role') || null,
2965
+ rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }
2966
+ });
2967
+ }
2968
+ }
2969
+ });
2970
+
2971
+ const uniqueElements = elements.slice(0, 15);
2972
+
2973
+ __wt_safeSendMessage({
2974
+ type: 'WT_SNIPE_COMPLETED',
2975
+ payload: {
2976
+ mode: 'dom_target',
2977
+ elements: uniqueElements,
2978
+ boundingRect: { x: left, y: top, width, height }
2979
+ }
2980
+ });
2981
+ }
2982
+
2983
+ // ── Mode 2: Visual WebP Crop ─────────────────────────────────────────────
2984
+ function captureVisualCrop(left, top, width, height) {
2985
+ try {
2986
+ const canvas = document.createElement('canvas');
2987
+ canvas.width = width;
2988
+ canvas.height = height;
2989
+ const ctx = canvas.getContext('2d');
2990
+
2991
+ const htmlContent = document.documentElement.outerHTML;
2992
+ const dataSvg = `
2993
+ <svg xmlns="http://www.w3.org/2000/svg" width="${window.innerWidth}" height="${window.innerHeight}">
2994
+ <foreignObject width="100%" height="100%">
2995
+ <div xmlns="http://www.w3.org/1999/xhtml">
2996
+ ${htmlContent}
2997
+ </div>
2998
+ </foreignObject>
2999
+ </svg>
3000
+ `;
3001
+
3002
+ const img = new Image();
3003
+ const blob = new Blob([dataSvg], { type: 'image/svg+xml;charset=utf-8' });
3004
+ const url = URL.createObjectURL(blob);
3005
+
3006
+ img.onload = function() {
3007
+ ctx.drawImage(img, left, top, width, height, 0, 0, width, height);
3008
+ URL.revokeObjectURL(url);
3009
+ const webpBase64 = canvas.toDataURL('image/webp', 0.75);
3010
+
3011
+ __wt_safeSendMessage({
3012
+ type: 'WT_SNIPE_COMPLETED',
3013
+ payload: {
3014
+ mode: 'visual_crop',
3015
+ cropImage: webpBase64,
3016
+ boundingRect: { x: left, y: top, width, height }
3017
+ }
3018
+ });
3019
+ };
3020
+ img.onerror = function() {
3021
+ captureDOMTargets(left, top, width, height);
3022
+ };
3023
+ img.src = url;
3024
+ } catch(e) {
3025
+ captureDOMTargets(left, top, width, height);
3026
+ }
3027
+ }
3028
+
3029
+ // Listen for start snipe message from sidepanel/background
3030
+ browserAPI.runtime.onMessage.addListener((msg) => {
3031
+ if (msg.type === 'WT_START_SNIPE') {
3032
+ createOverlay(msg.mode || 'dom_target');
3033
+ }
3034
+ if (msg.type === 'WT_SNIPE_INJECT' && msg.entry) {
3035
+ const noteStr = typeof msg.entry === 'string' ? msg.entry : JSON.stringify(msg.entry);
3036
+ try {
3037
+ if (!window.__wt_chat_queue) window.__wt_chat_queue = [];
3038
+ window.__wt_chat_queue.push(noteStr);
3039
+ } catch(e) {}
3040
+
3041
+ try {
3042
+ const existing = JSON.parse(localStorage.getItem('__wt_chat_queue_persist') || '[]');
3043
+ existing.push(msg.entry);
3044
+ localStorage.setItem('__wt_chat_queue_persist', JSON.stringify(existing));
3045
+ } catch(e) {}
3046
+
3047
+ const bridge = document.getElementById('__wt_ask_bridge');
3048
+ if (bridge) {
3049
+ bridge.dataset.status = 'submitted';
3050
+ bridge.dataset.result = msg.entry.text || 'Annotation submitted';
3051
+ }
3052
+ }
3053
+ });
3054
+
3055
+ })();
3056
+
3057
+ // ── ALERT / ACTION-REQUIRED TAB INDICATOR ─────────────────────
3058
+ (function initAlertDetector() {
3059
+ if (window.self !== window.top) return;
3060
+ // Expose on window so window.__Weavetab.getAlertState() can read it from CDP MAIN world
3061
+ window.__wt_alertPending = false;
3062
+ var _alert = window.alert;
3063
+ window.alert = function(msg) {
3064
+ window.__wt_alertPending = true;
3065
+ __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
3066
+ var r = _alert.apply(this, arguments);
3067
+ window.__wt_alertPending = false;
3068
+ __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
3069
+ return r;
3070
+ };
3071
+ var _confirm = window.confirm;
3072
+ window.confirm = function(msg) {
3073
+ window.__wt_alertPending = true;
3074
+ __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
3075
+ var r = _confirm.apply(this, arguments);
3076
+ window.__wt_alertPending = false;
3077
+ __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
3078
+ return r;
3079
+ };
3080
+ var _prompt = window.prompt;
3081
+ window.prompt = function(msg, def) {
3082
+ window.__wt_alertPending = true;
3083
+ __wt_safeSendMessage({ type: 'TAB_ALERT_WAITING' });
3084
+ var r = _prompt.apply(this, arguments);
3085
+ window.__wt_alertPending = false;
3086
+ __wt_safeSendMessage({ type: 'TAB_ALERT_CLEARED' });
3087
+ return r;
3088
+ };
3089
+ })();