wu-framework 1.1.6 → 1.1.8

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 (90) hide show
  1. package/README.md +511 -977
  2. package/dist/wu-framework.cjs.js +3 -1
  3. package/dist/wu-framework.cjs.js.map +1 -0
  4. package/dist/wu-framework.dev.js +7533 -2761
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +3 -0
  7. package/dist/wu-framework.esm.js.map +1 -0
  8. package/dist/wu-framework.umd.js +3 -1
  9. package/dist/wu-framework.umd.js.map +1 -0
  10. package/integrations/astro/README.md +127 -0
  11. package/integrations/astro/WuApp.astro +63 -0
  12. package/integrations/astro/WuShell.astro +39 -0
  13. package/integrations/astro/index.js +68 -0
  14. package/integrations/astro/package.json +38 -0
  15. package/integrations/astro/types.d.ts +53 -0
  16. package/package.json +94 -74
  17. package/src/adapters/angular/ai.js +30 -0
  18. package/src/adapters/angular/index.d.ts +154 -0
  19. package/src/adapters/angular/index.js +932 -0
  20. package/src/adapters/angular.d.ts +3 -154
  21. package/src/adapters/angular.js +3 -813
  22. package/src/adapters/index.js +35 -24
  23. package/src/adapters/lit/ai.js +20 -0
  24. package/src/adapters/lit/index.d.ts +120 -0
  25. package/src/adapters/lit/index.js +721 -0
  26. package/src/adapters/lit.d.ts +3 -120
  27. package/src/adapters/lit.js +3 -726
  28. package/src/adapters/preact/ai.js +33 -0
  29. package/src/adapters/preact/index.d.ts +108 -0
  30. package/src/adapters/preact/index.js +661 -0
  31. package/src/adapters/preact.d.ts +3 -108
  32. package/src/adapters/preact.js +3 -665
  33. package/src/adapters/react/ai.js +135 -0
  34. package/src/adapters/react/index.d.ts +246 -0
  35. package/src/adapters/react/index.js +689 -0
  36. package/src/adapters/react.d.ts +3 -212
  37. package/src/adapters/react.js +3 -513
  38. package/src/adapters/shared.js +64 -0
  39. package/src/adapters/solid/ai.js +32 -0
  40. package/src/adapters/solid/index.d.ts +101 -0
  41. package/src/adapters/solid/index.js +586 -0
  42. package/src/adapters/solid.d.ts +3 -101
  43. package/src/adapters/solid.js +3 -591
  44. package/src/adapters/svelte/ai.js +31 -0
  45. package/src/adapters/svelte/index.d.ts +166 -0
  46. package/src/adapters/svelte/index.js +798 -0
  47. package/src/adapters/svelte.d.ts +3 -166
  48. package/src/adapters/svelte.js +3 -803
  49. package/src/adapters/vanilla/ai.js +30 -0
  50. package/src/adapters/vanilla/index.d.ts +179 -0
  51. package/src/adapters/vanilla/index.js +785 -0
  52. package/src/adapters/vanilla.d.ts +3 -179
  53. package/src/adapters/vanilla.js +3 -791
  54. package/src/adapters/vue/ai.js +52 -0
  55. package/src/adapters/vue/index.d.ts +299 -0
  56. package/src/adapters/vue/index.js +608 -0
  57. package/src/adapters/vue.d.ts +3 -299
  58. package/src/adapters/vue.js +3 -611
  59. package/src/ai/wu-ai-actions.js +261 -0
  60. package/src/ai/wu-ai-browser.js +663 -0
  61. package/src/ai/wu-ai-context.js +332 -0
  62. package/src/ai/wu-ai-conversation.js +554 -0
  63. package/src/ai/wu-ai-permissions.js +381 -0
  64. package/src/ai/wu-ai-provider.js +605 -0
  65. package/src/ai/wu-ai-schema.js +225 -0
  66. package/src/ai/wu-ai-triggers.js +396 -0
  67. package/src/ai/wu-ai.js +474 -0
  68. package/src/core/wu-app.js +50 -8
  69. package/src/core/wu-cache.js +1 -1
  70. package/src/core/wu-core.js +645 -677
  71. package/src/core/wu-html-parser.js +121 -211
  72. package/src/core/wu-iframe-sandbox.js +328 -0
  73. package/src/core/wu-mcp-bridge.js +647 -0
  74. package/src/core/wu-overrides.js +510 -0
  75. package/src/core/wu-prefetch.js +414 -0
  76. package/src/core/wu-proxy-sandbox.js +398 -75
  77. package/src/core/wu-sandbox.js +86 -268
  78. package/src/core/wu-script-executor.js +79 -182
  79. package/src/core/wu-snapshot-sandbox.js +149 -106
  80. package/src/core/wu-strategies.js +13 -0
  81. package/src/core/wu-style-bridge.js +0 -2
  82. package/src/index.js +139 -665
  83. package/dist/wu-framework.hex.js +0 -23
  84. package/dist/wu-framework.min.js +0 -1
  85. package/dist/wu-framework.obf.js +0 -1
  86. package/scripts/build-protected.js +0 -366
  87. package/scripts/build.js +0 -212
  88. package/scripts/rollup-plugin-hex.js +0 -143
  89. package/src/core/wu-registry.js +0 -60
  90. package/src/core/wu-sandbox-pool.js +0 -390
@@ -0,0 +1,647 @@
1
+ /**
2
+ * WU-MCP Bridge (Browser Side)
3
+ *
4
+ * Connects to the wu-mcp-server via WebSocket and executes
5
+ * commands using wu.* APIs. This is the "eyes and hands" of
6
+ * the MCP server inside the browser.
7
+ *
8
+ * @example
9
+ * // Auto-connect (called by wu.mcp.connect())
10
+ * import { createMcpBridge } from './wu-mcp-bridge.js';
11
+ *
12
+ * const bridge = createMcpBridge(wuInstance);
13
+ * bridge.connect('ws://localhost:3100');
14
+ */
15
+
16
+ /**
17
+ * Create the MCP bridge for a Wu instance.
18
+ *
19
+ * @param {object} wu - The Wu Framework instance (window.wu)
20
+ * @returns {object} Bridge API: { connect, disconnect, isConnected }
21
+ */
22
+ export function createMcpBridge(wu) {
23
+ let ws = null;
24
+ let reconnectTimer = null;
25
+ let reconnectAttempts = 0;
26
+ const MAX_RECONNECT_ATTEMPTS = 10;
27
+ const RECONNECT_DELAY = 2000;
28
+
29
+ // Event log for wu_list_events
30
+ const eventLog = [];
31
+ const MAX_EVENT_LOG = 200;
32
+
33
+ // Console log capture
34
+ const consoleLog = [];
35
+ const MAX_CONSOLE_LOG = 500;
36
+
37
+ // Network log capture
38
+ const networkLog = [];
39
+ const MAX_NETWORK_LOG = 300;
40
+
41
+ // Capture events for history
42
+ if (wu.eventBus) {
43
+ wu.eventBus.on('*', (event) => {
44
+ eventLog.push({
45
+ name: event.name,
46
+ data: event.data,
47
+ timestamp: event.timestamp || Date.now(),
48
+ source: event.source || 'unknown',
49
+ });
50
+ if (eventLog.length > MAX_EVENT_LOG) eventLog.shift();
51
+ });
52
+ }
53
+
54
+ // Capture console messages
55
+ _interceptConsole();
56
+
57
+ // Capture network requests (fetch + XMLHttpRequest)
58
+ _interceptNetwork();
59
+
60
+ // ── Command handlers ──
61
+
62
+ const handlers = {
63
+ status() {
64
+ return {
65
+ connected: true,
66
+ framework: 'wu-framework',
67
+ apps: _getAppList(),
68
+ storeKeys: wu.store ? Object.keys(wu.store.get('') || {}) : [],
69
+ actionsCount: wu.ai?._actions ? Object.keys(wu.ai._actions).length : 0,
70
+ eventLogSize: eventLog.length,
71
+ };
72
+ },
73
+
74
+ list_apps() {
75
+ return _getAppList();
76
+ },
77
+
78
+ navigate({ route }) {
79
+ if (!route) return { error: 'Route is required' };
80
+ if (wu.eventBus) {
81
+ wu.eventBus.emit('shell:navigate', { route });
82
+ }
83
+ if (wu.store) {
84
+ wu.store.set('currentPath', route);
85
+ }
86
+ return { navigated: route };
87
+ },
88
+
89
+ mount_app({ appName, container }) {
90
+ if (!appName) return { error: 'appName is required' };
91
+ try {
92
+ if (wu.mount) {
93
+ wu.mount(appName, container);
94
+ return { mounted: appName, container };
95
+ }
96
+ return { error: 'wu.mount not available' };
97
+ } catch (err) {
98
+ return { error: err.message };
99
+ }
100
+ },
101
+
102
+ unmount_app({ appName }) {
103
+ if (!appName) return { error: 'appName is required' };
104
+ try {
105
+ if (wu.unmount) {
106
+ wu.unmount(appName);
107
+ return { unmounted: appName };
108
+ }
109
+ return { error: 'wu.unmount not available' };
110
+ } catch (err) {
111
+ return { error: err.message };
112
+ }
113
+ },
114
+
115
+ get_state({ path }) {
116
+ if (!wu.store) return { error: 'wu.store not available' };
117
+ const value = wu.store.get(path || '');
118
+ return { path: path || '(root)', value };
119
+ },
120
+
121
+ set_state({ path, value }) {
122
+ if (!wu.store) return { error: 'wu.store not available' };
123
+ if (!path) return { error: 'path is required' };
124
+ wu.store.set(path, value);
125
+ return { path, value, updated: true };
126
+ },
127
+
128
+ emit_event({ event, data }) {
129
+ if (!wu.eventBus) return { error: 'wu.eventBus not available' };
130
+ if (!event) return { error: 'event name is required' };
131
+ wu.eventBus.emit(event, data);
132
+ return { emitted: event, data };
133
+ },
134
+
135
+ list_events({ limit = 20 }) {
136
+ return eventLog.slice(-limit);
137
+ },
138
+
139
+ list_actions() {
140
+ if (!wu.ai?._actions) return { actions: [], note: 'wu.ai not initialized or no actions registered' };
141
+ const actions = Object.entries(wu.ai._actions).map(([name, def]) => ({
142
+ name,
143
+ description: def.description || '',
144
+ parameters: def.parameters ? Object.keys(def.parameters) : [],
145
+ }));
146
+ return { actions, count: actions.length };
147
+ },
148
+
149
+ async execute_action({ action, params }) {
150
+ if (!wu.ai) return { error: 'wu.ai not available' };
151
+ if (!action) return { error: 'action name is required' };
152
+
153
+ try {
154
+ // Try to execute via wu.ai action system
155
+ if (wu.ai._actions && wu.ai._actions[action]) {
156
+ const handler = wu.ai._actions[action].handler;
157
+ const result = await handler(params || {}, {
158
+ emit: (e, d) => wu.eventBus?.emit(e, d),
159
+ setState: (p, v) => wu.store?.set(p, v),
160
+ getState: (p) => wu.store?.get(p),
161
+ });
162
+ return { action, result };
163
+ }
164
+ return { error: `Action "${action}" not found` };
165
+ } catch (err) {
166
+ return { error: err.message };
167
+ }
168
+ },
169
+
170
+ snapshot({ appName }) {
171
+ try {
172
+ const target = appName
173
+ ? document.querySelector(`[data-wu-app="${appName}"]`) || document.querySelector(`#wu-app-${appName}`)
174
+ : document.body;
175
+
176
+ if (!target) return { error: `App "${appName}" not found in DOM` };
177
+
178
+ const tree = _buildA11yTree(target, 0, 5);
179
+ return {
180
+ app: appName || '(page)',
181
+ snapshot: tree,
182
+ timestamp: Date.now(),
183
+ };
184
+ } catch (err) {
185
+ return { error: err.message };
186
+ }
187
+ },
188
+
189
+ console({ level = 'all', limit = 50 }) {
190
+ const filtered = level === 'all'
191
+ ? consoleLog
192
+ : consoleLog.filter((m) => m.level === level);
193
+ return filtered.slice(-limit);
194
+ },
195
+
196
+ async screenshot({ selector, quality = 0.8 }) {
197
+ try {
198
+ const target = selector
199
+ ? document.querySelector(selector)
200
+ : document.documentElement;
201
+
202
+ if (!target) return { error: `Element not found: ${selector}` };
203
+
204
+ const rect = target.getBoundingClientRect();
205
+ const w = Math.ceil(Math.min(rect.width || window.innerWidth, 1920));
206
+ const h = Math.ceil(Math.min(rect.height || window.innerHeight, 1080));
207
+
208
+ // Clone target and inline all computed styles for accurate rendering
209
+ const clone = target.cloneNode(true);
210
+ _inlineComputedStyles(target, clone);
211
+
212
+ // Serialize to XHTML (required for SVG foreignObject)
213
+ const serializer = new XMLSerializer();
214
+ const xhtml = serializer.serializeToString(clone);
215
+
216
+ // Build SVG with foreignObject containing the styled DOM
217
+ const svgStr = [
218
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">`,
219
+ '<foreignObject width="100%" height="100%">',
220
+ `<div xmlns="http://www.w3.org/1999/xhtml" style="width:${w}px;height:${h}px;overflow:hidden;">`,
221
+ xhtml,
222
+ '</div>',
223
+ '</foreignObject>',
224
+ '</svg>',
225
+ ].join('');
226
+
227
+ const svgBlob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
228
+ const url = URL.createObjectURL(svgBlob);
229
+
230
+ // Render SVG to Canvas
231
+ const dataUrl = await new Promise((resolve) => {
232
+ const img = new Image();
233
+ img.onload = () => {
234
+ const canvas = document.createElement('canvas');
235
+ canvas.width = w;
236
+ canvas.height = h;
237
+ const ctx = canvas.getContext('2d');
238
+ ctx.drawImage(img, 0, 0);
239
+ URL.revokeObjectURL(url);
240
+ resolve(canvas.toDataURL('image/png', quality));
241
+ };
242
+ img.onerror = () => {
243
+ URL.revokeObjectURL(url);
244
+ resolve(null);
245
+ };
246
+ img.src = url;
247
+ });
248
+
249
+ if (!dataUrl) return { error: 'Canvas rendering failed' };
250
+
251
+ // Return base64 without the data:image/png;base64, prefix
252
+ const base64 = dataUrl.split(',')[1];
253
+ return {
254
+ selector: selector || '(page)',
255
+ width: w,
256
+ height: h,
257
+ format: 'png',
258
+ base64,
259
+ sizeKB: Math.round((base64.length * 3) / 4 / 1024),
260
+ timestamp: Date.now(),
261
+ };
262
+ } catch (err) {
263
+ return { error: err.message };
264
+ }
265
+ },
266
+
267
+ click({ selector, text }) {
268
+ try {
269
+ let el = null;
270
+
271
+ if (selector) {
272
+ el = document.querySelector(selector);
273
+ }
274
+
275
+ // Fallback: find by visible text content
276
+ if (!el && text) {
277
+ const candidates = document.querySelectorAll('button, a, [role="button"], input[type="submit"], [data-click], label');
278
+ for (const candidate of candidates) {
279
+ if (candidate.textContent?.trim().toLowerCase().includes(text.toLowerCase())) {
280
+ el = candidate;
281
+ break;
282
+ }
283
+ }
284
+ }
285
+
286
+ if (!el) return { error: `Element not found: ${selector || `text="${text}"`}` };
287
+
288
+ // Scroll into view and click
289
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
290
+ el.click();
291
+
292
+ const tag = el.tagName?.toLowerCase();
293
+ const id = el.id ? `#${el.id}` : '';
294
+ const cls = el.className && typeof el.className === 'string' ? `.${el.className.split(' ')[0]}` : '';
295
+ return {
296
+ clicked: `${tag}${id}${cls}`,
297
+ text: el.textContent?.trim().slice(0, 80) || '',
298
+ rect: el.getBoundingClientRect().toJSON(),
299
+ };
300
+ } catch (err) {
301
+ return { error: err.message };
302
+ }
303
+ },
304
+
305
+ type({ selector, text, clear = false, submit = false }) {
306
+ try {
307
+ if (!selector) return { error: 'selector is required' };
308
+ if (text === undefined) return { error: 'text is required' };
309
+
310
+ const el = document.querySelector(selector);
311
+ if (!el) return { error: `Element not found: ${selector}` };
312
+
313
+ // Focus the element
314
+ el.focus();
315
+
316
+ // Clear existing value if requested
317
+ if (clear) {
318
+ el.value = '';
319
+ el.dispatchEvent(new Event('input', { bubbles: true }));
320
+ }
321
+
322
+ // Set value and fire events (works with React, Vue, etc.)
323
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
324
+ window.HTMLInputElement.prototype, 'value'
325
+ )?.set || Object.getOwnPropertyDescriptor(
326
+ window.HTMLTextAreaElement.prototype, 'value'
327
+ )?.set;
328
+
329
+ if (nativeInputValueSetter) {
330
+ nativeInputValueSetter.call(el, clear ? text : el.value + text);
331
+ } else {
332
+ el.value = clear ? text : el.value + text;
333
+ }
334
+
335
+ // Dispatch events that frameworks listen to
336
+ el.dispatchEvent(new Event('input', { bubbles: true }));
337
+ el.dispatchEvent(new Event('change', { bubbles: true }));
338
+
339
+ // Submit form if requested
340
+ if (submit) {
341
+ const form = el.closest('form');
342
+ if (form) {
343
+ form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
344
+ } else {
345
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true }));
346
+ }
347
+ }
348
+
349
+ return {
350
+ selector,
351
+ typed: text,
352
+ currentValue: el.value?.slice(0, 200),
353
+ submitted: submit,
354
+ };
355
+ } catch (err) {
356
+ return { error: err.message };
357
+ }
358
+ },
359
+
360
+ network({ method, status, limit = 50 }) {
361
+ let filtered = networkLog;
362
+ if (method) filtered = filtered.filter((r) => r.method.toUpperCase() === method.toUpperCase());
363
+ if (status) {
364
+ if (status === 'error') {
365
+ filtered = filtered.filter((r) => r.status === 0 || r.status >= 400);
366
+ } else {
367
+ filtered = filtered.filter((r) => String(r.status).startsWith(String(status)));
368
+ }
369
+ }
370
+ return {
371
+ requests: filtered.slice(-limit),
372
+ total: networkLog.length,
373
+ filtered: filtered.length,
374
+ };
375
+ },
376
+
377
+ eval({ expression }) {
378
+ try {
379
+ // eslint-disable-next-line no-eval
380
+ const result = eval(expression);
381
+ return {
382
+ expression,
383
+ result: typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result),
384
+ type: typeof result,
385
+ };
386
+ } catch (err) {
387
+ return { error: err.message, expression };
388
+ }
389
+ },
390
+ };
391
+
392
+ // ── WebSocket connection ──
393
+
394
+ function connect(url = 'ws://localhost:19100') {
395
+ if (ws && ws.readyState <= 1) {
396
+ console.warn('[wu-mcp-bridge] Already connected or connecting');
397
+ return;
398
+ }
399
+
400
+ try {
401
+ ws = new WebSocket(url);
402
+
403
+ ws.onopen = () => {
404
+ console.log('[wu-mcp-bridge] Connected to wu-mcp-server');
405
+ reconnectAttempts = 0;
406
+ };
407
+
408
+ ws.onmessage = async (event) => {
409
+ try {
410
+ const msg = JSON.parse(event.data);
411
+ const { id, command, params } = msg;
412
+
413
+ if (!id || !command) {
414
+ console.warn('[wu-mcp-bridge] Invalid message:', msg);
415
+ return;
416
+ }
417
+
418
+ const handler = handlers[command];
419
+ if (!handler) {
420
+ _respond(id, null, `Unknown command: ${command}`);
421
+ return;
422
+ }
423
+
424
+ try {
425
+ const result = await handler(params || {});
426
+ _respond(id, result);
427
+ } catch (err) {
428
+ _respond(id, null, err.message);
429
+ }
430
+ } catch (err) {
431
+ console.error('[wu-mcp-bridge] Failed to handle message:', err);
432
+ }
433
+ };
434
+
435
+ ws.onclose = () => {
436
+ console.log('[wu-mcp-bridge] Disconnected');
437
+ ws = null;
438
+ _scheduleReconnect(url);
439
+ };
440
+
441
+ ws.onerror = () => {
442
+ // onclose will fire after this
443
+ };
444
+ } catch (err) {
445
+ console.error('[wu-mcp-bridge] Connection failed:', err.message);
446
+ _scheduleReconnect(url);
447
+ }
448
+ }
449
+
450
+ function disconnect() {
451
+ if (reconnectTimer) {
452
+ clearTimeout(reconnectTimer);
453
+ reconnectTimer = null;
454
+ }
455
+ reconnectAttempts = MAX_RECONNECT_ATTEMPTS; // prevent reconnect
456
+ if (ws) {
457
+ ws.close();
458
+ ws = null;
459
+ }
460
+ }
461
+
462
+ function isConnected() {
463
+ return ws !== null && ws.readyState === 1;
464
+ }
465
+
466
+ // ── Private helpers ──
467
+
468
+ function _respond(id, result, error) {
469
+ if (!ws || ws.readyState !== 1) return;
470
+ const msg = error ? { id, error } : { id, result };
471
+ ws.send(JSON.stringify(msg));
472
+ }
473
+
474
+ function _scheduleReconnect(url) {
475
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
476
+ reconnectAttempts++;
477
+ const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5);
478
+ reconnectTimer = setTimeout(() => connect(url), delay);
479
+ }
480
+
481
+ function _getAppList() {
482
+ // Try to get app info from wu internals
483
+ const apps = [];
484
+
485
+ if (wu._apps) {
486
+ for (const [name, app] of Object.entries(wu._apps)) {
487
+ apps.push({
488
+ name,
489
+ mounted: app.mounted || app.isMounted || false,
490
+ url: app.url || app.info?.url || '',
491
+ status: app.status || app.info?.status || 'unknown',
492
+ });
493
+ }
494
+ }
495
+
496
+ // Fallback: scan DOM for wu-app elements
497
+ if (apps.length === 0) {
498
+ document.querySelectorAll('[data-wu-app]').forEach((el) => {
499
+ apps.push({
500
+ name: el.getAttribute('data-wu-app'),
501
+ mounted: true,
502
+ container: `#${el.id || '(no-id)'}`,
503
+ });
504
+ });
505
+ }
506
+
507
+ return apps;
508
+ }
509
+
510
+ function _buildA11yTree(el, depth, maxDepth) {
511
+ if (depth > maxDepth || !el) return '';
512
+
513
+ const indent = ' '.repeat(depth);
514
+ const tag = el.tagName?.toLowerCase() || '';
515
+ const role = el.getAttribute?.('role') || '';
516
+ const ariaLabel = el.getAttribute?.('aria-label') || '';
517
+ const text = el.childNodes?.length === 1 && el.childNodes[0].nodeType === 3
518
+ ? el.textContent?.trim().slice(0, 80) : '';
519
+
520
+ let line = `${indent}<${tag}`;
521
+ if (el.id) line += ` id="${el.id}"`;
522
+ if (role) line += ` role="${role}"`;
523
+ if (ariaLabel) line += ` aria-label="${ariaLabel}"`;
524
+ if (el.className && typeof el.className === 'string') {
525
+ const cls = el.className.trim().slice(0, 60);
526
+ if (cls) line += ` class="${cls}"`;
527
+ }
528
+ line += '>';
529
+ if (text) line += ` "${text}"`;
530
+
531
+ let result = line + '\n';
532
+
533
+ // Traverse into shadow DOM if present
534
+ const root = el.shadowRoot || el;
535
+ const children = root.children || [];
536
+
537
+ for (let i = 0; i < children.length && i < 50; i++) {
538
+ result += _buildA11yTree(children[i], depth + 1, maxDepth);
539
+ }
540
+
541
+ return result;
542
+ }
543
+
544
+ function _interceptConsole() {
545
+ const levels = ['log', 'warn', 'error'];
546
+ for (const level of levels) {
547
+ const original = console[level];
548
+ console[level] = (...args) => {
549
+ consoleLog.push({
550
+ level,
551
+ message: args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '),
552
+ timestamp: Date.now(),
553
+ });
554
+ if (consoleLog.length > MAX_CONSOLE_LOG) consoleLog.shift();
555
+ original.apply(console, args);
556
+ };
557
+ }
558
+ }
559
+
560
+ function _interceptNetwork() {
561
+ // ── Intercept fetch() ──
562
+ const originalFetch = window.fetch;
563
+ window.fetch = async function (...args) {
564
+ const start = Date.now();
565
+ const req = args[0];
566
+ const url = typeof req === 'string' ? req : req?.url || '';
567
+ const method = args[1]?.method || (req?.method) || 'GET';
568
+
569
+ try {
570
+ const response = await originalFetch.apply(window, args);
571
+ const size = parseInt(response.headers?.get('content-length') || '0', 10);
572
+ networkLog.push({
573
+ type: 'fetch', method: method.toUpperCase(), url,
574
+ status: response.status, statusText: response.statusText,
575
+ duration: Date.now() - start, size, timestamp: start,
576
+ });
577
+ if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
578
+ return response;
579
+ } catch (err) {
580
+ networkLog.push({
581
+ type: 'fetch', method: method.toUpperCase(), url,
582
+ status: 0, error: err.message,
583
+ duration: Date.now() - start, timestamp: start,
584
+ });
585
+ if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
586
+ throw err;
587
+ }
588
+ };
589
+
590
+ // ── Intercept XMLHttpRequest ──
591
+ const origOpen = XMLHttpRequest.prototype.open;
592
+ const origSend = XMLHttpRequest.prototype.send;
593
+
594
+ XMLHttpRequest.prototype.open = function (method, url, ...rest) {
595
+ this._wuMcp = { method: (method || 'GET').toUpperCase(), url: String(url), start: null };
596
+ return origOpen.call(this, method, url, ...rest);
597
+ };
598
+
599
+ XMLHttpRequest.prototype.send = function (...args) {
600
+ if (this._wuMcp) {
601
+ this._wuMcp.start = Date.now();
602
+ this.addEventListener('loadend', () => {
603
+ networkLog.push({
604
+ type: 'xhr',
605
+ method: this._wuMcp.method,
606
+ url: this._wuMcp.url,
607
+ status: this.status,
608
+ statusText: this.statusText,
609
+ duration: Date.now() - this._wuMcp.start,
610
+ size: parseInt(this.getResponseHeader('content-length') || '0', 10),
611
+ timestamp: this._wuMcp.start,
612
+ });
613
+ if (networkLog.length > MAX_NETWORK_LOG) networkLog.shift();
614
+ });
615
+ }
616
+ return origSend.apply(this, args);
617
+ };
618
+ }
619
+
620
+ function _inlineComputedStyles(source, clone) {
621
+ // Copy computed styles from source to clone for accurate Canvas rendering
622
+ const sourceStyle = window.getComputedStyle(source);
623
+ const important = ['color', 'background', 'background-color', 'font-family',
624
+ 'font-size', 'font-weight', 'border', 'border-radius', 'padding', 'margin',
625
+ 'display', 'flex-direction', 'align-items', 'justify-content', 'gap',
626
+ 'width', 'height', 'max-width', 'max-height', 'overflow', 'opacity',
627
+ 'box-shadow', 'text-align', 'line-height', 'position', 'top', 'left',
628
+ 'right', 'bottom', 'z-index', 'transform', 'visibility'];
629
+
630
+ for (const prop of important) {
631
+ try {
632
+ const val = sourceStyle.getPropertyValue(prop);
633
+ if (val) clone.style?.setProperty(prop, val);
634
+ } catch (_) { /* skip */ }
635
+ }
636
+
637
+ // Recurse into children (limit depth for performance)
638
+ const sourceChildren = source.children || [];
639
+ const cloneChildren = clone.children || [];
640
+ const max = Math.min(sourceChildren.length, cloneChildren.length, 200);
641
+ for (let i = 0; i < max; i++) {
642
+ _inlineComputedStyles(sourceChildren[i], cloneChildren[i]);
643
+ }
644
+ }
645
+
646
+ return { connect, disconnect, isConnected };
647
+ }