@dynamicu/chromedebug-mcp 2.2.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.
- package/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- package/src/validation/schemas.js +391 -0
|
@@ -0,0 +1,3022 @@
|
|
|
1
|
+
// ChromeDebug MCP Content Script v2.0
|
|
2
|
+
// Wrapped in IIFE to avoid global scope pollution and duplicate declarations
|
|
3
|
+
(function() {
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
// Check if already loaded
|
|
7
|
+
if (window.chromePilotContentScriptLoaded) {
|
|
8
|
+
console.log('[ChromeDebug MCP v2.0] Content script already loaded, skipping re-injection');
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Early bailout for user-configured restricted sites
|
|
13
|
+
const hostname = window.location.hostname.toLowerCase();
|
|
14
|
+
|
|
15
|
+
// Load user restrictions from storage and check immediately
|
|
16
|
+
chrome.storage.sync.get(['chromePilotRestrictedSites', 'chromePilotAllowedSites', 'chromePilotMode'], (result) => {
|
|
17
|
+
const mode = result.chromePilotMode || 'whitelist'; // 'whitelist' or 'blacklist'
|
|
18
|
+
const restrictedSites = result.chromePilotRestrictedSites || [];
|
|
19
|
+
const allowedSites = result.chromePilotAllowedSites || [];
|
|
20
|
+
|
|
21
|
+
let shouldBlock = false;
|
|
22
|
+
|
|
23
|
+
if (mode === 'blacklist') {
|
|
24
|
+
// Block if hostname matches any restricted site pattern
|
|
25
|
+
shouldBlock = restrictedSites.some(pattern => {
|
|
26
|
+
// Special handling for patterns with port wildcards like "localhost:*"
|
|
27
|
+
if (pattern.includes(':*')) {
|
|
28
|
+
const basePattern = pattern.replace(':*', '').toLowerCase();
|
|
29
|
+
if (hostname === basePattern) {
|
|
30
|
+
return true; // Match hostname regardless of port
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (pattern.includes('*')) {
|
|
35
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\./g, '\\.'));
|
|
36
|
+
return regex.test(hostname);
|
|
37
|
+
}
|
|
38
|
+
return hostname.includes(pattern.toLowerCase());
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
// Whitelist mode: only allow if hostname matches allowed sites
|
|
42
|
+
const isAllowed = allowedSites.some(pattern => {
|
|
43
|
+
// Special handling for patterns with port wildcards like "localhost:*"
|
|
44
|
+
if (pattern.includes(':*')) {
|
|
45
|
+
const basePattern = pattern.replace(':*', '').toLowerCase();
|
|
46
|
+
if (hostname === basePattern) {
|
|
47
|
+
return true; // Match hostname regardless of port
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (pattern.includes('*')) {
|
|
52
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\./g, '\\.'));
|
|
53
|
+
return regex.test(hostname);
|
|
54
|
+
}
|
|
55
|
+
return hostname.includes(pattern.toLowerCase());
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// For whitelist mode, block if not explicitly allowed (unless empty list)
|
|
59
|
+
shouldBlock = allowedSites.length > 0 && !isAllowed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (shouldBlock) {
|
|
63
|
+
console.log(`[ChromeDebug MCP] Skipping restricted site (${mode} mode):`, hostname);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Continue with normal initialization
|
|
68
|
+
initializeChromePilot();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function initializeChromePilot() {
|
|
72
|
+
window.chromePilotContentScriptLoaded = true;
|
|
73
|
+
|
|
74
|
+
const CHROME_PILOT_VERSION = '2.0.4-BUILD-20250119';
|
|
75
|
+
console.log(`[content.js] Loaded version: ${CHROME_PILOT_VERSION}`);
|
|
76
|
+
|
|
77
|
+
let frameFlashIndicator = null;
|
|
78
|
+
|
|
79
|
+
// Helper function to safely check if extension context is valid
|
|
80
|
+
function isExtensionValid() {
|
|
81
|
+
try {
|
|
82
|
+
return chrome.runtime && chrome.runtime.id;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// Initialize ChromeDebug MCP content script
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
console.log('[ChromeDebug MCP] 🚀 Content script initializing...');
|
|
92
|
+
}, 100);
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* SNAPSHOT FEATURE DISABLED (2025-10-01)
|
|
96
|
+
*
|
|
97
|
+
* Console monitor injection was used for snapshot feature to capture console logs.
|
|
98
|
+
*
|
|
99
|
+
* WHY DISABLED:
|
|
100
|
+
* - Snapshots without console logs are just screenshots (users can do this natively)
|
|
101
|
+
* - Console log capture requires always-on monitoring (privacy concern)
|
|
102
|
+
* - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
|
|
103
|
+
*
|
|
104
|
+
* NOTE: Console logging is still captured during screen recordings via the normal
|
|
105
|
+
* recording system. This code only disabled the on-demand console monitor injection
|
|
106
|
+
* that was used for standalone snapshots.
|
|
107
|
+
*
|
|
108
|
+
* TO RE-ENABLE:
|
|
109
|
+
* 1. Implement privacy-conscious always-on log monitoring
|
|
110
|
+
* 2. Uncomment the code below
|
|
111
|
+
* 3. Re-enable related functions in background.js and popup.js
|
|
112
|
+
*
|
|
113
|
+
* See: SNAPSHOT_FEATURE_DISABLED.md for full explanation
|
|
114
|
+
*/
|
|
115
|
+
/*
|
|
116
|
+
const consoleHistory = [];
|
|
117
|
+
const maxHistorySize = 100;
|
|
118
|
+
let consoleMonitorInjected = false;
|
|
119
|
+
|
|
120
|
+
function injectConsoleMonitor() {
|
|
121
|
+
if (consoleMonitorInjected) {
|
|
122
|
+
console.log('[ChromeDebug] Console monitor already injected');
|
|
123
|
+
return Promise.resolve(true);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const script = document.createElement('script');
|
|
128
|
+
script.src = chrome.runtime.getURL('console-monitor.js');
|
|
129
|
+
|
|
130
|
+
script.onload = function() {
|
|
131
|
+
console.log('[ChromeDebug] Console monitor loaded successfully');
|
|
132
|
+
consoleMonitorInjected = true;
|
|
133
|
+
this.remove();
|
|
134
|
+
resolve(true);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
script.onerror = function() {
|
|
138
|
+
console.error('[ChromeDebug] Failed to load console monitor script');
|
|
139
|
+
this.remove();
|
|
140
|
+
resolve(false);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
(document.head || document.documentElement).appendChild(script);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
window.addEventListener('message', (event) => {
|
|
148
|
+
if (event.source === window && event.data.type === 'CHROME_DEBUG_CONSOLE_LOG') {
|
|
149
|
+
consoleHistory.push(event.data.log);
|
|
150
|
+
|
|
151
|
+
if (consoleHistory.length > maxHistorySize) {
|
|
152
|
+
consoleHistory.shift();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
console.log('[ChromeDebug] Console monitoring ready (will inject on demand)');
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
function getAngularComponentInfo(element) {
|
|
161
|
+
// Try to find Angular debug info
|
|
162
|
+
try {
|
|
163
|
+
// Method 1: ng.probe (Angular 1.x - 8.x in dev mode)
|
|
164
|
+
if (window.ng && window.ng.probe) {
|
|
165
|
+
const debugElement = window.ng.probe(element);
|
|
166
|
+
if (debugElement && debugElement.componentInstance) {
|
|
167
|
+
return {
|
|
168
|
+
componentName: debugElement.componentInstance.constructor.name,
|
|
169
|
+
framework: 'Angular',
|
|
170
|
+
sourceInfo: null // Angular doesn't expose source info easily
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Method 2: __ngContext__ (Angular 9+)
|
|
176
|
+
if (element.__ngContext__) {
|
|
177
|
+
return {
|
|
178
|
+
componentName: 'Angular Component',
|
|
179
|
+
framework: 'Angular 9+',
|
|
180
|
+
sourceInfo: null
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Method 3: Check for Ionic classes
|
|
185
|
+
if (element.tagName && element.tagName.toLowerCase().startsWith('ion-')) {
|
|
186
|
+
return {
|
|
187
|
+
componentName: element.tagName.toLowerCase(),
|
|
188
|
+
framework: 'Ionic',
|
|
189
|
+
sourceInfo: null
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.log('Error detecting Angular component:', e);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getReactComponentInfo(element) {
|
|
200
|
+
// Try to find React Fiber node
|
|
201
|
+
const reactFiberKey = Object.keys(element).find(key =>
|
|
202
|
+
key.startsWith('__reactInternalInstance') ||
|
|
203
|
+
key.startsWith('__reactFiber')
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
if (!reactFiberKey) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const fiber = element[reactFiberKey];
|
|
211
|
+
if (!fiber) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Collect all component info in the tree
|
|
216
|
+
const components = [];
|
|
217
|
+
let currentFiber = fiber;
|
|
218
|
+
|
|
219
|
+
// Debug: Log the first fiber to see what properties are available
|
|
220
|
+
if (fiber) {
|
|
221
|
+
console.log('React Fiber found:', fiber);
|
|
222
|
+
console.log('Fiber properties:', Object.keys(fiber));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
while (currentFiber) {
|
|
226
|
+
// Check if this is a function/class component (not host component)
|
|
227
|
+
if (currentFiber.elementType && typeof currentFiber.elementType === 'function') {
|
|
228
|
+
const componentName = currentFiber.elementType.displayName ||
|
|
229
|
+
currentFiber.elementType.name ||
|
|
230
|
+
'Unknown Component';
|
|
231
|
+
|
|
232
|
+
// Try multiple ways to get source info
|
|
233
|
+
let sourceInfo = null;
|
|
234
|
+
|
|
235
|
+
// Method 1: _debugSource (development builds)
|
|
236
|
+
if (currentFiber._debugSource) {
|
|
237
|
+
sourceInfo = {
|
|
238
|
+
fileName: currentFiber._debugSource.fileName,
|
|
239
|
+
lineNumber: currentFiber._debugSource.lineNumber,
|
|
240
|
+
columnNumber: currentFiber._debugSource.columnNumber
|
|
241
|
+
};
|
|
242
|
+
console.log('Found _debugSource:', sourceInfo);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Method 2: _owner and _source
|
|
246
|
+
if (!sourceInfo && currentFiber._owner && currentFiber._owner._debugSource) {
|
|
247
|
+
sourceInfo = {
|
|
248
|
+
fileName: currentFiber._owner._debugSource.fileName,
|
|
249
|
+
lineNumber: currentFiber._owner._debugSource.lineNumber,
|
|
250
|
+
columnNumber: currentFiber._owner._debugSource.columnNumber
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Method 3: Check the component function itself
|
|
255
|
+
if (!sourceInfo && currentFiber.elementType._source) {
|
|
256
|
+
sourceInfo = {
|
|
257
|
+
fileName: currentFiber.elementType._source.fileName,
|
|
258
|
+
lineNumber: currentFiber.elementType._source.lineNumber,
|
|
259
|
+
columnNumber: currentFiber.elementType._source.columnNumber
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Method 4: Try to extract from stack trace (last resort)
|
|
264
|
+
if (!sourceInfo && currentFiber.elementType.toString) {
|
|
265
|
+
const stackMatch = currentFiber.elementType.toString().match(/at\s+(\S+)\s+\((.+):(\d+):(\d+)\)/);
|
|
266
|
+
if (stackMatch) {
|
|
267
|
+
sourceInfo = {
|
|
268
|
+
fileName: stackMatch[2],
|
|
269
|
+
lineNumber: parseInt(stackMatch[3]),
|
|
270
|
+
columnNumber: parseInt(stackMatch[4])
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
components.push({
|
|
276
|
+
componentName,
|
|
277
|
+
sourceInfo,
|
|
278
|
+
props: currentFiber.memoizedProps,
|
|
279
|
+
key: currentFiber.key,
|
|
280
|
+
type: currentFiber.elementType
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
currentFiber = currentFiber.return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Return the most specific component (first in the list)
|
|
288
|
+
return components.length > 0 ? {
|
|
289
|
+
...components[0],
|
|
290
|
+
componentHierarchy: components.map(c => c.componentName)
|
|
291
|
+
} : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getSelector(element) {
|
|
295
|
+
if (element.id) {
|
|
296
|
+
return `#${element.id}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (element.className && typeof element.className === 'string') {
|
|
300
|
+
const classes = element.className.trim().split(/\s+/).filter(c => !c.startsWith('chrome-pilot'));
|
|
301
|
+
if (classes.length > 0) {
|
|
302
|
+
return `${element.tagName.toLowerCase()}.${classes.join('.')}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const path = [];
|
|
307
|
+
while (element && element.nodeType === Node.ELEMENT_NODE) {
|
|
308
|
+
let selector = element.tagName.toLowerCase();
|
|
309
|
+
|
|
310
|
+
if (element.id) {
|
|
311
|
+
selector = `#${element.id}`;
|
|
312
|
+
path.unshift(selector);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let sibling = element;
|
|
317
|
+
let nth = 1;
|
|
318
|
+
while (sibling.previousElementSibling) {
|
|
319
|
+
sibling = sibling.previousElementSibling;
|
|
320
|
+
if (sibling.tagName === element.tagName) {
|
|
321
|
+
nth++;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (nth > 1) {
|
|
326
|
+
selector += `:nth-of-type(${nth})`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
path.unshift(selector);
|
|
330
|
+
element = element.parentElement;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return path.join(' > ');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function getComponentInfo(element) {
|
|
337
|
+
let currentElement = element;
|
|
338
|
+
let ionicComponent = null;
|
|
339
|
+
|
|
340
|
+
// Check if this is an Ionic component
|
|
341
|
+
if (element.tagName && element.tagName.toLowerCase().startsWith('ion-')) {
|
|
342
|
+
ionicComponent = element.tagName.toLowerCase();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// For Ionic React, we need to look for React components in parent elements
|
|
346
|
+
// because Ionic components are web components that wrap React components
|
|
347
|
+
const maxDepth = 10; // Don't go too far up the tree
|
|
348
|
+
let depth = 0;
|
|
349
|
+
|
|
350
|
+
while (currentElement && depth < maxDepth) {
|
|
351
|
+
// Log for debugging
|
|
352
|
+
if (depth === 0) {
|
|
353
|
+
console.log(`Checking element:`, currentElement);
|
|
354
|
+
console.log(`Element keys:`, Object.keys(currentElement).filter(k => k.includes('react')));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Try to find React info on current element
|
|
358
|
+
const reactInfo = getReactComponentInfo(currentElement);
|
|
359
|
+
if (reactInfo) {
|
|
360
|
+
// If we found React info and also have an Ionic component, combine them
|
|
361
|
+
if (ionicComponent) {
|
|
362
|
+
return {
|
|
363
|
+
...reactInfo,
|
|
364
|
+
framework: 'Ionic React',
|
|
365
|
+
ionicComponent: ionicComponent
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return { ...reactInfo, framework: 'React' };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Move up to parent
|
|
372
|
+
currentElement = currentElement.parentElement;
|
|
373
|
+
depth++;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// If we only found Ionic component (no React info)
|
|
377
|
+
if (ionicComponent) {
|
|
378
|
+
return {
|
|
379
|
+
componentName: ionicComponent,
|
|
380
|
+
framework: 'Ionic',
|
|
381
|
+
sourceInfo: null
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Try Angular as last resort
|
|
386
|
+
const angularInfo = getAngularComponentInfo(element);
|
|
387
|
+
if (angularInfo) {
|
|
388
|
+
return angularInfo;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log('No component info found');
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
// Legacy recording indicator functions removed - now handled by ScreenCaptureVisualFeedback class
|
|
397
|
+
|
|
398
|
+
// Idempotent protection for cleanup function
|
|
399
|
+
let cleanupExecuted = false;
|
|
400
|
+
|
|
401
|
+
function cleanup() {
|
|
402
|
+
// Idempotent protection - only run cleanup once
|
|
403
|
+
if (cleanupExecuted) {
|
|
404
|
+
console.log('[ChromeDebug MCP] Cleanup already executed, skipping');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
cleanupExecuted = true;
|
|
409
|
+
console.log('[ChromeDebug MCP] Executing cleanup...');
|
|
410
|
+
|
|
411
|
+
// Clean up recording indicators and event listeners
|
|
412
|
+
try {
|
|
413
|
+
// Legacy recording indicator cleanup removed - now handled by ScreenCaptureVisualFeedback
|
|
414
|
+
|
|
415
|
+
// Stop workflow recording if active
|
|
416
|
+
if (typeof isWorkflowRecording !== 'undefined' && isWorkflowRecording) {
|
|
417
|
+
console.log('[ChromeDebug MCP] Stopping workflow recording during cleanup');
|
|
418
|
+
if (typeof stopWorkflowRecording === 'function') {
|
|
419
|
+
stopWorkflowRecording();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Stop screen interaction tracking if active
|
|
424
|
+
if (typeof isScreenRecording !== 'undefined' && isScreenRecording) {
|
|
425
|
+
console.log('[ChromeDebug MCP] Stopping screen recording during cleanup');
|
|
426
|
+
if (typeof stopScreenInteractionTracking === 'function') {
|
|
427
|
+
stopScreenInteractionTracking();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log('[ChromeDebug MCP] Cleanup completed successfully');
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.warn('[ChromeDebug MCP] Error during cleanup:', error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
// Only add message listener if extension context is valid
|
|
440
|
+
if (isExtensionValid()) {
|
|
441
|
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
442
|
+
if (request.action === 'getConsoleHistory') {
|
|
443
|
+
// Inject console monitor if not already injected, then return logs
|
|
444
|
+
injectConsoleMonitor().then(() => {
|
|
445
|
+
// Wait a moment for any pending console messages to be captured
|
|
446
|
+
setTimeout(() => {
|
|
447
|
+
sendResponse({ logs: consoleHistory.slice() }); // Return a copy
|
|
448
|
+
}, 100);
|
|
449
|
+
});
|
|
450
|
+
return true; // Keep channel open for async response
|
|
451
|
+
} else if (request.action === 'showRecordingUI') {
|
|
452
|
+
console.log('Received showRecordingUI message');
|
|
453
|
+
showRecordingInterface();
|
|
454
|
+
sendResponse({ success: true });
|
|
455
|
+
return true; // Keep channel open for async response
|
|
456
|
+
} else if (request.action === 'showFrameFlash') {
|
|
457
|
+
showFrameFlash();
|
|
458
|
+
sendResponse({ success: true });
|
|
459
|
+
} else if (request.action === 'recordingComplete') {
|
|
460
|
+
// Show the recording ID in the floating UI
|
|
461
|
+
console.log('Recording complete with ID:', request.recordingId);
|
|
462
|
+
if (recordingOverlay) {
|
|
463
|
+
const statusDiv = document.getElementById('chrome-pilot-recording-status');
|
|
464
|
+
if (statusDiv && request.recordingId) {
|
|
465
|
+
statusDiv.innerHTML = `<strong style="color: #4CAF50;">Recording saved!</strong><br>ID: <code style="background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-family: monospace;">${request.recordingId}</code>`;
|
|
466
|
+
|
|
467
|
+
// Keep the overlay visible for a few seconds to show the ID
|
|
468
|
+
setTimeout(() => {
|
|
469
|
+
if (recordingOverlay) {
|
|
470
|
+
recordingOverlay.remove();
|
|
471
|
+
recordingOverlay = null;
|
|
472
|
+
}
|
|
473
|
+
}, 10000); // Keep visible for 10 seconds
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
sendResponse({ success: true });
|
|
477
|
+
} else if (request.action === 'recordingStarted') {
|
|
478
|
+
// Recording indicator now handled by ScreenCaptureVisualFeedback
|
|
479
|
+
console.log('[ChromeDebug MCP] Received recordingStarted message with scheduled start time:', request.scheduledStartTime);
|
|
480
|
+
// Start tracking screen interactions with scheduled timing
|
|
481
|
+
startScreenInteractionTracking(request.scheduledStartTime, request.sessionId);
|
|
482
|
+
sendResponse({ success: true });
|
|
483
|
+
} else if (request.action === 'recordingStopped') {
|
|
484
|
+
// Recording indicator cleanup now handled by ScreenCaptureVisualFeedback
|
|
485
|
+
console.log('[ChromeDebug MCP] Received recordingStopped message');
|
|
486
|
+
// Stop tracking screen interactions
|
|
487
|
+
stopScreenInteractionTracking();
|
|
488
|
+
sendResponse({ success: true });
|
|
489
|
+
} else if (request.action === 'recordingSessionComplete') {
|
|
490
|
+
// Recording indicator cleanup now handled by ScreenCaptureVisualFeedback
|
|
491
|
+
console.log('[ChromeDebug MCP] Received recordingSessionComplete message');
|
|
492
|
+
// Stop tracking screen interactions
|
|
493
|
+
stopScreenInteractionTracking();
|
|
494
|
+
sendResponse({ success: true });
|
|
495
|
+
} else if (request.action === 'startWorkflowRecording') {
|
|
496
|
+
console.log('[ChromeDebug MCP] Starting workflow recording with settings:', request.screenshotSettings);
|
|
497
|
+
startWorkflowRecording(request.screenshotSettings);
|
|
498
|
+
sendResponse({ success: true });
|
|
499
|
+
} else if (request.action === 'stopWorkflowRecording') {
|
|
500
|
+
console.log('[ChromeDebug MCP] Stopping workflow recording');
|
|
501
|
+
const workflow = stopWorkflowRecording();
|
|
502
|
+
sendResponse({ success: true, workflow: workflow });
|
|
503
|
+
} else if (request.action === 'createRestorePoint') {
|
|
504
|
+
// Create restore point for current workflow
|
|
505
|
+
createRestorePoint(request.actionIndex || workflowActions.length).then(restorePointId => {
|
|
506
|
+
sendResponse({ success: true, restorePointId });
|
|
507
|
+
}).catch(error => {
|
|
508
|
+
sendResponse({ success: false, error: error.message });
|
|
509
|
+
});
|
|
510
|
+
return true; // Keep channel open for async response
|
|
511
|
+
} else if (request.action === 'restoreFromPoint') {
|
|
512
|
+
// Restore from a restore point
|
|
513
|
+
restoreFromPoint(request.restorePointData).then(() => {
|
|
514
|
+
sendResponse({ success: true });
|
|
515
|
+
}).catch(error => {
|
|
516
|
+
sendResponse({ success: false, error: error.message });
|
|
517
|
+
});
|
|
518
|
+
return true; // Keep channel open for async response
|
|
519
|
+
} else if (request.action === 'playWorkflow') {
|
|
520
|
+
// Play a workflow recording
|
|
521
|
+
playWorkflowRecording(request.workflow, request.actions).then(() => {
|
|
522
|
+
sendResponse({ success: true });
|
|
523
|
+
}).catch(error => {
|
|
524
|
+
sendResponse({ success: false, error: error.message });
|
|
525
|
+
});
|
|
526
|
+
return true; // Keep channel open for async response
|
|
527
|
+
} else if (request.action === 'startFullDataRecording') {
|
|
528
|
+
// Start full data recording with user configuration
|
|
529
|
+
console.log('[ChromeDebug MCP] Starting full data recording with config:', request.config);
|
|
530
|
+
startFullDataRecording(request.config).then(() => {
|
|
531
|
+
sendResponse({ success: true });
|
|
532
|
+
}).catch(error => {
|
|
533
|
+
sendResponse({ success: false, error: error.message });
|
|
534
|
+
});
|
|
535
|
+
return true; // Keep channel open for async response
|
|
536
|
+
} else if (request.action === 'stopFullDataRecording') {
|
|
537
|
+
// Stop full data recording
|
|
538
|
+
console.log('[ChromeDebug MCP] Stopping full data recording');
|
|
539
|
+
stopFullDataRecording().then(() => {
|
|
540
|
+
sendResponse({ success: true });
|
|
541
|
+
}).catch(error => {
|
|
542
|
+
sendResponse({ success: false, error: error.message });
|
|
543
|
+
});
|
|
544
|
+
return true; // Keep channel open for async response
|
|
545
|
+
} else if (request.action === 'getFullDataRecordingStats') {
|
|
546
|
+
// Get full data recording statistics
|
|
547
|
+
const stats = getFullDataRecordingStats();
|
|
548
|
+
sendResponse({ success: true, stats: stats });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Helper function to fix CSS selectors that might have issues
|
|
554
|
+
function fixSelector(selector) {
|
|
555
|
+
if (!selector || typeof selector !== 'string') return selector;
|
|
556
|
+
|
|
557
|
+
// Remove pseudo-classes like :hover, :focus, :active that can't be used in querySelector
|
|
558
|
+
let fixedSelector = selector.replace(/:(hover|focus|active|visited|focus-within|focus-visible)/g, '');
|
|
559
|
+
|
|
560
|
+
// Remove any trailing dots
|
|
561
|
+
fixedSelector = fixedSelector.replace(/\.+$/, '');
|
|
562
|
+
|
|
563
|
+
// Remove empty class selectors (consecutive dots)
|
|
564
|
+
fixedSelector = fixedSelector.replace(/\.\./g, '.');
|
|
565
|
+
|
|
566
|
+
// Try to validate the selector
|
|
567
|
+
try {
|
|
568
|
+
// Test if selector is valid
|
|
569
|
+
document.querySelector(fixedSelector);
|
|
570
|
+
return fixedSelector; // It's valid, return the fixed version
|
|
571
|
+
} catch (e) {
|
|
572
|
+
console.warn('[ChromePilot] Invalid selector even after fixes:', fixedSelector, 'Original:', selector);
|
|
573
|
+
// Return the fixed version anyway, as it's our best attempt
|
|
574
|
+
return fixedSelector;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Helper function to wait for page load
|
|
579
|
+
async function waitForPageLoad(timeout = 10000) {
|
|
580
|
+
return new Promise((resolve) => {
|
|
581
|
+
// If document is already loaded, resolve immediately
|
|
582
|
+
if (document.readyState === 'complete') {
|
|
583
|
+
resolve();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let timeoutId;
|
|
588
|
+
const cleanup = () => {
|
|
589
|
+
clearTimeout(timeoutId);
|
|
590
|
+
window.removeEventListener('load', loadHandler);
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const loadHandler = () => {
|
|
594
|
+
cleanup();
|
|
595
|
+
resolve();
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// Set up load listener
|
|
599
|
+
window.addEventListener('load', loadHandler);
|
|
600
|
+
|
|
601
|
+
// Set up timeout
|
|
602
|
+
timeoutId = setTimeout(() => {
|
|
603
|
+
cleanup();
|
|
604
|
+
console.log('[ChromePilot] Page load timeout reached, continuing...');
|
|
605
|
+
resolve();
|
|
606
|
+
}, timeout);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Helper function to wait for element to appear
|
|
611
|
+
async function waitForElement(selector, timeout = 5000) {
|
|
612
|
+
const startTime = Date.now();
|
|
613
|
+
|
|
614
|
+
while (Date.now() - startTime < timeout) {
|
|
615
|
+
const element = document.querySelector(selector);
|
|
616
|
+
if (element) {
|
|
617
|
+
return element;
|
|
618
|
+
}
|
|
619
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Function to play a workflow recording
|
|
626
|
+
async function playWorkflowRecording(workflow, actions) {
|
|
627
|
+
console.log('[ChromePilot] Starting workflow playback with', actions.length, 'actions');
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
// Show playback indicator
|
|
631
|
+
const playbackIndicator = document.createElement('div');
|
|
632
|
+
playbackIndicator.id = 'chrome-pilot-playback-indicator';
|
|
633
|
+
playbackIndicator.style.cssText = `
|
|
634
|
+
position: fixed;
|
|
635
|
+
top: 20px;
|
|
636
|
+
right: 20px;
|
|
637
|
+
background: #4CAF50;
|
|
638
|
+
color: white;
|
|
639
|
+
padding: 12px 24px;
|
|
640
|
+
border-radius: 4px;
|
|
641
|
+
font-family: Arial, sans-serif;
|
|
642
|
+
font-size: 14px;
|
|
643
|
+
z-index: 10000;
|
|
644
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
gap: 10px;
|
|
648
|
+
`;
|
|
649
|
+
playbackIndicator.innerHTML = `
|
|
650
|
+
<div style="
|
|
651
|
+
width: 12px;
|
|
652
|
+
height: 12px;
|
|
653
|
+
border-radius: 50%;
|
|
654
|
+
background: white;
|
|
655
|
+
animation: pulse 1s infinite;
|
|
656
|
+
"></div>
|
|
657
|
+
<span>Playing workflow: <span id="action-counter">0</span>/${actions.length}</span>
|
|
658
|
+
`;
|
|
659
|
+
|
|
660
|
+
// Add animation style
|
|
661
|
+
const style = document.createElement('style');
|
|
662
|
+
style.textContent = `
|
|
663
|
+
@keyframes pulse {
|
|
664
|
+
0% { opacity: 1; }
|
|
665
|
+
50% { opacity: 0.5; }
|
|
666
|
+
100% { opacity: 1; }
|
|
667
|
+
}
|
|
668
|
+
`;
|
|
669
|
+
document.head.appendChild(style);
|
|
670
|
+
document.body.appendChild(playbackIndicator);
|
|
671
|
+
|
|
672
|
+
const actionCounter = document.getElementById('action-counter');
|
|
673
|
+
let executedActions = 0;
|
|
674
|
+
|
|
675
|
+
// Execute each action with proper timing
|
|
676
|
+
for (let i = 0; i < actions.length; i++) {
|
|
677
|
+
const action = actions[i];
|
|
678
|
+
actionCounter.textContent = i + 1;
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
console.log(`[ChromePilot] Executing action ${i + 1}/${actions.length}:`, action.type, 'selector:', action.selector);
|
|
682
|
+
|
|
683
|
+
// Calculate delay based on timestamps
|
|
684
|
+
let delay = 500; // Default delay
|
|
685
|
+
if (i > 0 && action.timestamp && actions[i - 1].timestamp) {
|
|
686
|
+
delay = action.timestamp - actions[i - 1].timestamp;
|
|
687
|
+
// Apply reasonable limits
|
|
688
|
+
delay = Math.min(delay, 10000); // Cap at 10 seconds
|
|
689
|
+
delay = Math.max(delay, 100); // Minimum 100ms
|
|
690
|
+
console.log(`[ChromePilot] Calculated delay: ${delay}ms`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Wait between actions
|
|
694
|
+
if (i > 0) { // Don't delay before first action
|
|
695
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
switch (action.type) {
|
|
699
|
+
case 'click':
|
|
700
|
+
if (action.selector) {
|
|
701
|
+
// Fix selector if needed
|
|
702
|
+
const fixedSelector = fixSelector(action.selector);
|
|
703
|
+
|
|
704
|
+
// Wait for element to appear if it's not immediately available
|
|
705
|
+
let element = document.querySelector(fixedSelector);
|
|
706
|
+
if (!element && action.xpath) {
|
|
707
|
+
// Try XPath as fallback
|
|
708
|
+
console.log('[ChromePilot] Trying XPath fallback:', action.xpath);
|
|
709
|
+
const xpathResult = document.evaluate(action.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
710
|
+
element = xpathResult.singleNodeValue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (!element) {
|
|
714
|
+
console.log('[ChromePilot] Element not found immediately, waiting...');
|
|
715
|
+
element = await waitForElement(fixedSelector, 3000);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (element) {
|
|
719
|
+
// If the element is an SVG or inside a button/link, find the clickable parent
|
|
720
|
+
let clickableElement = element;
|
|
721
|
+
if (element.tagName === 'svg' || element.tagName === 'path' || element.tagName === 'g') {
|
|
722
|
+
// Find parent button or link
|
|
723
|
+
clickableElement = element.closest('button, a') || element;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
console.log('[ChromePilot] Found element:', clickableElement.tagName, 'for selector:', fixedSelector);
|
|
727
|
+
clickableElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
728
|
+
await new Promise(resolve => setTimeout(resolve, 200)); // Wait for scroll
|
|
729
|
+
|
|
730
|
+
// Ensure element is visible and enabled
|
|
731
|
+
const rect = clickableElement.getBoundingClientRect();
|
|
732
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
733
|
+
console.warn('[ChromePilot] Element has zero dimensions, may not be visible');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Try to click
|
|
737
|
+
try {
|
|
738
|
+
clickableElement.click();
|
|
739
|
+
console.log('[ChromePilot] Clicked element successfully');
|
|
740
|
+
} catch (e) {
|
|
741
|
+
console.error('[ChromePilot] Error clicking element:', e);
|
|
742
|
+
// Try alternative click method
|
|
743
|
+
clickableElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// After click, check if navigation might occur
|
|
747
|
+
const currentUrl = window.location.href;
|
|
748
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
749
|
+
|
|
750
|
+
// If URL changed, wait for page load
|
|
751
|
+
if (window.location.href !== currentUrl) {
|
|
752
|
+
console.log('[ChromePilot] Navigation detected after click, waiting for page load...');
|
|
753
|
+
await waitForPageLoad();
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
console.warn('[ChromePilot] Element not found:', action.selector);
|
|
757
|
+
}
|
|
758
|
+
} else if (action.x !== undefined && action.y !== undefined) {
|
|
759
|
+
// Click at coordinates
|
|
760
|
+
const element = document.elementFromPoint(action.x, action.y);
|
|
761
|
+
if (element) {
|
|
762
|
+
element.click();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
|
|
767
|
+
case 'input':
|
|
768
|
+
if (action.selector) {
|
|
769
|
+
// Wait for element to appear if it's not immediately available
|
|
770
|
+
let element = document.querySelector(action.selector);
|
|
771
|
+
if (!element) {
|
|
772
|
+
console.log('[ChromePilot] Input element not found immediately, waiting...');
|
|
773
|
+
element = await waitForElement(action.selector, 3000);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (element) {
|
|
777
|
+
console.log('[ChromePilot] Found input element:', element.tagName, 'type:', element.type, 'for selector:', action.selector);
|
|
778
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
779
|
+
await new Promise(resolve => setTimeout(resolve, 200)); // Wait for scroll
|
|
780
|
+
|
|
781
|
+
// Focus the element
|
|
782
|
+
element.focus();
|
|
783
|
+
|
|
784
|
+
// Clear existing value
|
|
785
|
+
element.value = '';
|
|
786
|
+
|
|
787
|
+
// Set new value
|
|
788
|
+
element.value = action.value || '';
|
|
789
|
+
console.log('[ChromePilot] Set input value to:', action.value);
|
|
790
|
+
|
|
791
|
+
// Trigger events in the right order
|
|
792
|
+
element.dispatchEvent(new Event('focus', { bubbles: true }));
|
|
793
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
794
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
795
|
+
element.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
796
|
+
} else {
|
|
797
|
+
console.warn('[ChromePilot] Input element not found:', action.selector);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
break;
|
|
801
|
+
|
|
802
|
+
case 'keypress':
|
|
803
|
+
if (action.key) {
|
|
804
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: action.key }));
|
|
805
|
+
document.dispatchEvent(new KeyboardEvent('keyup', { key: action.key }));
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
|
|
809
|
+
case 'scroll':
|
|
810
|
+
if (action.deltaY !== undefined) {
|
|
811
|
+
window.scrollBy(0, action.deltaY);
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
|
|
815
|
+
case 'navigation':
|
|
816
|
+
// Navigation will be handled by the browser
|
|
817
|
+
console.log('[ChromePilot] Navigation detected');
|
|
818
|
+
break;
|
|
819
|
+
|
|
820
|
+
default:
|
|
821
|
+
console.warn('[ChromePilot] Unknown action type:', action.type);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
executedActions++;
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.error('[ChromePilot] Error executing action:', error);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Show completion
|
|
831
|
+
playbackIndicator.style.background = '#2196F3';
|
|
832
|
+
playbackIndicator.innerHTML = `
|
|
833
|
+
<span>✓ Workflow completed: ${executedActions}/${actions.length} actions</span>
|
|
834
|
+
`;
|
|
835
|
+
|
|
836
|
+
// Remove indicator after 3 seconds
|
|
837
|
+
setTimeout(() => {
|
|
838
|
+
playbackIndicator.remove();
|
|
839
|
+
style.remove();
|
|
840
|
+
}, 3000);
|
|
841
|
+
|
|
842
|
+
console.log('[ChromePilot] Workflow playback completed');
|
|
843
|
+
|
|
844
|
+
} catch (error) {
|
|
845
|
+
console.error('[ChromePilot] Error during workflow playback:', error);
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Workflow recording variables
|
|
851
|
+
let isWorkflowRecording = false;
|
|
852
|
+
let workflowActions = [];
|
|
853
|
+
let workflowRecordingIndicator = null;
|
|
854
|
+
let workflowScreenshotSettings = null;
|
|
855
|
+
|
|
856
|
+
// Helper function to capture screenshot
|
|
857
|
+
async function captureWorkflowScreenshot() {
|
|
858
|
+
if (!workflowScreenshotSettings || !workflowScreenshotSettings.enabled) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
// Send message to background script to capture screenshot
|
|
864
|
+
const response = await chrome.runtime.sendMessage({
|
|
865
|
+
action: 'captureWorkflowScreenshot',
|
|
866
|
+
settings: workflowScreenshotSettings
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
if (response && response.success) {
|
|
870
|
+
return response.screenshotData;
|
|
871
|
+
}
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.error('[ChromeDebug MCP] Error capturing screenshot:', error);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Helper function to get unique selector for an element
|
|
880
|
+
function getUniqueSelector(element) {
|
|
881
|
+
// Try ID first
|
|
882
|
+
if (element.id) {
|
|
883
|
+
return `#${element.id}`;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// Try unique class combination
|
|
887
|
+
if (element.className && typeof element.className === 'string') {
|
|
888
|
+
const classes = element.className.trim().split(/\s+/)
|
|
889
|
+
.filter(c => !c.startsWith('chrome-pilot'))
|
|
890
|
+
.filter(c => !c.includes(':')) // Filter out pseudo-class artifacts
|
|
891
|
+
.map(c => {
|
|
892
|
+
// Escape special characters in class names
|
|
893
|
+
return c.replace(/([^\w-])/g, '\\$1');
|
|
894
|
+
});
|
|
895
|
+
if (classes.length > 0) {
|
|
896
|
+
const selector = `${element.tagName.toLowerCase()}.${classes.join('.')}`;
|
|
897
|
+
// Check if this selector is unique
|
|
898
|
+
try {
|
|
899
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
900
|
+
return selector;
|
|
901
|
+
}
|
|
902
|
+
} catch (e) {
|
|
903
|
+
// Invalid selector, continue to next method
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Fall back to full path selector
|
|
909
|
+
return getSelector(element);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Helper function to get XPath for an element
|
|
913
|
+
function getXPath(element) {
|
|
914
|
+
if (element.id !== '') {
|
|
915
|
+
return `//*[@id="${element.id}"]`;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (element === document.body) {
|
|
919
|
+
return '/html/body';
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
let ix = 0;
|
|
923
|
+
const siblings = element.parentNode.childNodes;
|
|
924
|
+
|
|
925
|
+
for (let i = 0; i < siblings.length; i++) {
|
|
926
|
+
const sibling = siblings[i];
|
|
927
|
+
if (sibling === element) {
|
|
928
|
+
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
|
|
929
|
+
}
|
|
930
|
+
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
|
931
|
+
ix++;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Create workflow recording indicator
|
|
937
|
+
function createWorkflowRecordingIndicator() {
|
|
938
|
+
if (workflowRecordingIndicator) return;
|
|
939
|
+
|
|
940
|
+
workflowRecordingIndicator = document.createElement('div');
|
|
941
|
+
workflowRecordingIndicator.id = 'chrome-pilot-workflow-indicator';
|
|
942
|
+
workflowRecordingIndicator.innerHTML = `
|
|
943
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
944
|
+
<span style="color: #9c27b0; font-size: 20px; animation: pulse 1.5s infinite;">●</span>
|
|
945
|
+
<span>Recording Workflow...</span>
|
|
946
|
+
<span id="workflow-action-count" style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-size: 12px;">0 actions</span>
|
|
947
|
+
</div>
|
|
948
|
+
`;
|
|
949
|
+
|
|
950
|
+
Object.assign(workflowRecordingIndicator.style, {
|
|
951
|
+
position: 'fixed',
|
|
952
|
+
bottom: '20px',
|
|
953
|
+
left: '20px',
|
|
954
|
+
background: 'rgba(0, 0, 0, 0.9)',
|
|
955
|
+
color: 'white',
|
|
956
|
+
padding: '12px 20px',
|
|
957
|
+
borderRadius: '25px',
|
|
958
|
+
fontSize: '14px',
|
|
959
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif',
|
|
960
|
+
fontWeight: '500',
|
|
961
|
+
zIndex: '2147483647',
|
|
962
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
963
|
+
backdropFilter: 'blur(10px)',
|
|
964
|
+
WebkitBackdropFilter: 'blur(10px)',
|
|
965
|
+
pointerEvents: 'none',
|
|
966
|
+
userSelect: 'none'
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
document.body.appendChild(workflowRecordingIndicator);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function removeWorkflowRecordingIndicator() {
|
|
973
|
+
if (workflowRecordingIndicator) {
|
|
974
|
+
workflowRecordingIndicator.remove();
|
|
975
|
+
workflowRecordingIndicator = null;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function updateWorkflowActionCount() {
|
|
980
|
+
const countEl = document.getElementById('workflow-action-count');
|
|
981
|
+
if (countEl) {
|
|
982
|
+
countEl.textContent = `${workflowActions.length} action${workflowActions.length !== 1 ? 's' : ''}`;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Enhanced click tracking helper functions - Pro Feature
|
|
987
|
+
// Try to import enhanced capture module (only exists in pro build)
|
|
988
|
+
let enhancedCaptureModule = null;
|
|
989
|
+
let captureElementHTML = null;
|
|
990
|
+
let extractEventHandlers = null;
|
|
991
|
+
let captureElementState = null;
|
|
992
|
+
let getEnhancedComponentInfo = null;
|
|
993
|
+
let getPerformanceMetrics = null;
|
|
994
|
+
|
|
995
|
+
// Attempt to load pro module
|
|
996
|
+
(async () => {
|
|
997
|
+
try {
|
|
998
|
+
const module = await import(chrome.runtime.getURL('pro/enhanced-capture.js'));
|
|
999
|
+
enhancedCaptureModule = module;
|
|
1000
|
+
captureElementHTML = module.captureElementHTML;
|
|
1001
|
+
extractEventHandlers = module.extractEventHandlers;
|
|
1002
|
+
captureElementState = module.captureElementState;
|
|
1003
|
+
getEnhancedComponentInfo = module.getEnhancedComponentInfo;
|
|
1004
|
+
getPerformanceMetrics = module.getPerformanceMetrics;
|
|
1005
|
+
console.log('[ChromePilot Pro] Enhanced capture module loaded');
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
console.log('[ChromePilot Free] Enhanced capture not available - using basic mode');
|
|
1008
|
+
// Create stub functions that return empty data for free version
|
|
1009
|
+
captureElementHTML = () => '';
|
|
1010
|
+
extractEventHandlers = () => ({});
|
|
1011
|
+
captureElementState = () => ({});
|
|
1012
|
+
getEnhancedComponentInfo = () => ({});
|
|
1013
|
+
getPerformanceMetrics = () => ({});
|
|
1014
|
+
}
|
|
1015
|
+
})();
|
|
1016
|
+
|
|
1017
|
+
// Helper function to check if enhanced capture is enabled
|
|
1018
|
+
async function shouldEnhanceCapture() {
|
|
1019
|
+
return new Promise((resolve) => {
|
|
1020
|
+
chrome.storage.local.get(['enhancedClickCapture'], (result) => {
|
|
1021
|
+
resolve(result.enhancedClickCapture === true);
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Record click action
|
|
1027
|
+
async function recordClick(event) {
|
|
1028
|
+
if (!isWorkflowRecording) {
|
|
1029
|
+
console.log('[ChromePilot] Click ignored - not recording');
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
console.log('[ChromePilot] Recording click event');
|
|
1034
|
+
|
|
1035
|
+
let element = event.target;
|
|
1036
|
+
|
|
1037
|
+
// If clicking on an SVG element or its children, find the parent button/link
|
|
1038
|
+
if (element.tagName === 'svg' || element.tagName === 'path' || element.tagName === 'g' || element.tagName === 'circle' || element.tagName === 'rect') {
|
|
1039
|
+
const clickableParent = element.closest('button, a, [role="button"], [onclick]');
|
|
1040
|
+
if (clickableParent) {
|
|
1041
|
+
console.log('[ChromePilot] Adjusted click target from', element.tagName, 'to', clickableParent.tagName);
|
|
1042
|
+
element = clickableParent;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const action = {
|
|
1047
|
+
type: 'click',
|
|
1048
|
+
selector: getUniqueSelector(element),
|
|
1049
|
+
xpath: getXPath(element),
|
|
1050
|
+
x: event.clientX,
|
|
1051
|
+
y: event.clientY,
|
|
1052
|
+
timestamp: Date.now()
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// Add text content for buttons and links
|
|
1056
|
+
if (element.tagName === 'BUTTON' || element.tagName === 'A') {
|
|
1057
|
+
action.text = element.textContent.trim();
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Enhanced click tracking data (conditional based on user setting)
|
|
1061
|
+
const enhanceCapture = await shouldEnhanceCapture();
|
|
1062
|
+
if (enhanceCapture) {
|
|
1063
|
+
try {
|
|
1064
|
+
console.log('[ChromePilot] Capturing enhanced click data...');
|
|
1065
|
+
|
|
1066
|
+
// Capture enhanced element data (only include fields with meaningful values)
|
|
1067
|
+
const capturedHTML = captureElementHTML(element);
|
|
1068
|
+
if (capturedHTML) {
|
|
1069
|
+
action.element_html = capturedHTML;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const componentData = getEnhancedComponentInfo(element);
|
|
1073
|
+
if (componentData) {
|
|
1074
|
+
try {
|
|
1075
|
+
JSON.stringify(componentData); // Test if serializable
|
|
1076
|
+
action.component_data = componentData;
|
|
1077
|
+
} catch (e) {
|
|
1078
|
+
console.error('[ChromePilot] Component data cannot be serialized:', e.message);
|
|
1079
|
+
action.component_data = { error: 'Serialization failed', type: typeof componentData };
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const eventHandlers = extractEventHandlers(element);
|
|
1084
|
+
if (eventHandlers) {
|
|
1085
|
+
try {
|
|
1086
|
+
JSON.stringify(eventHandlers); // Test if serializable
|
|
1087
|
+
action.event_handlers = eventHandlers;
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
console.error('[ChromePilot] Event handlers cannot be serialized:', e.message);
|
|
1090
|
+
action.event_handlers = { error: 'Serialization failed', type: typeof eventHandlers };
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const elementState = captureElementState(element);
|
|
1095
|
+
if (elementState) {
|
|
1096
|
+
try {
|
|
1097
|
+
JSON.stringify(elementState); // Test if serializable
|
|
1098
|
+
action.element_state = elementState;
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
console.error('[ChromePilot] Element state cannot be serialized:', e.message);
|
|
1101
|
+
action.element_state = { error: 'Serialization failed', type: typeof elementState };
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const perfMetrics = getPerformanceMetrics();
|
|
1106
|
+
if (perfMetrics) {
|
|
1107
|
+
try {
|
|
1108
|
+
JSON.stringify(perfMetrics); // Test if serializable
|
|
1109
|
+
action.performance_metrics = perfMetrics;
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
console.error('[ChromePilot] Performance metrics cannot be serialized:', e.message);
|
|
1112
|
+
action.performance_metrics = { error: 'Serialization failed', type: typeof perfMetrics };
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
console.log('[ChromePilot] Enhanced click data captured successfully');
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.warn('[ChromePilot] Error capturing enhanced click data:', error);
|
|
1119
|
+
// Continue with basic click recording if enhanced capture fails
|
|
1120
|
+
}
|
|
1121
|
+
} else {
|
|
1122
|
+
console.log('[ChromePilot] Enhanced click capture disabled - using basic capture only');
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Capture screenshot if enabled
|
|
1126
|
+
if (workflowScreenshotSettings && workflowScreenshotSettings.enabled) {
|
|
1127
|
+
// Give a moment for any UI changes to settle
|
|
1128
|
+
setTimeout(async () => {
|
|
1129
|
+
const screenshotData = await captureWorkflowScreenshot();
|
|
1130
|
+
if (screenshotData) {
|
|
1131
|
+
action.screenshot_data = screenshotData;
|
|
1132
|
+
}
|
|
1133
|
+
}, 100);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
workflowActions.push(action);
|
|
1137
|
+
console.log('[ChromePilot] Action recorded, total actions:', workflowActions.length);
|
|
1138
|
+
updateWorkflowActionCount();
|
|
1139
|
+
|
|
1140
|
+
// Visual feedback
|
|
1141
|
+
flashElement(element);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Record input changes
|
|
1145
|
+
async function recordInput(event) {
|
|
1146
|
+
if (!isWorkflowRecording) return;
|
|
1147
|
+
|
|
1148
|
+
const element = event.target;
|
|
1149
|
+
const action = {
|
|
1150
|
+
type: 'input',
|
|
1151
|
+
selector: getUniqueSelector(element),
|
|
1152
|
+
xpath: getXPath(element),
|
|
1153
|
+
value: element.value,
|
|
1154
|
+
timestamp: Date.now()
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// Add input type for better context
|
|
1158
|
+
if (element.type) {
|
|
1159
|
+
action.inputType = element.type;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Add placeholder for context
|
|
1163
|
+
if (element.placeholder) {
|
|
1164
|
+
action.placeholder = element.placeholder;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Capture screenshot if enabled
|
|
1168
|
+
if (workflowScreenshotSettings && workflowScreenshotSettings.enabled) {
|
|
1169
|
+
setTimeout(async () => {
|
|
1170
|
+
const screenshotData = await captureWorkflowScreenshot();
|
|
1171
|
+
if (screenshotData) {
|
|
1172
|
+
action.screenshot_data = screenshotData;
|
|
1173
|
+
}
|
|
1174
|
+
}, 100);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
workflowActions.push(action);
|
|
1178
|
+
updateWorkflowActionCount();
|
|
1179
|
+
|
|
1180
|
+
// Visual feedback
|
|
1181
|
+
flashElement(element);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Visual feedback for recorded actions
|
|
1185
|
+
function flashElement(element) {
|
|
1186
|
+
const originalBorder = element.style.border;
|
|
1187
|
+
const originalOutline = element.style.outline;
|
|
1188
|
+
|
|
1189
|
+
element.style.outline = '3px solid #9c27b0';
|
|
1190
|
+
element.style.outlineOffset = '2px';
|
|
1191
|
+
|
|
1192
|
+
setTimeout(() => {
|
|
1193
|
+
element.style.outline = originalOutline;
|
|
1194
|
+
element.style.outlineOffset = '';
|
|
1195
|
+
}, 300);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Drag tracking variables
|
|
1199
|
+
let isDragging = false;
|
|
1200
|
+
let dragStartElement = null;
|
|
1201
|
+
let dragStartX = 0;
|
|
1202
|
+
let dragStartY = 0;
|
|
1203
|
+
|
|
1204
|
+
// Record drag start
|
|
1205
|
+
function recordMouseDown(event) {
|
|
1206
|
+
if (!isWorkflowRecording) return;
|
|
1207
|
+
|
|
1208
|
+
// Only track left mouse button
|
|
1209
|
+
if (event.button !== 0) return;
|
|
1210
|
+
|
|
1211
|
+
isDragging = true;
|
|
1212
|
+
dragStartElement = event.target;
|
|
1213
|
+
dragStartX = event.clientX;
|
|
1214
|
+
dragStartY = event.clientY;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Record drag end
|
|
1218
|
+
function recordMouseUp(event) {
|
|
1219
|
+
if (!isWorkflowRecording || !isDragging) return;
|
|
1220
|
+
|
|
1221
|
+
const endX = event.clientX;
|
|
1222
|
+
const endY = event.clientY;
|
|
1223
|
+
|
|
1224
|
+
// Only record if there was actual movement
|
|
1225
|
+
const distance = Math.sqrt(Math.pow(endX - dragStartX, 2) + Math.pow(endY - dragStartY, 2));
|
|
1226
|
+
if (distance > 5) { // Minimum 5px movement to count as drag
|
|
1227
|
+
const action = {
|
|
1228
|
+
type: 'drag',
|
|
1229
|
+
startSelector: getUniqueSelector(dragStartElement),
|
|
1230
|
+
startX: dragStartX,
|
|
1231
|
+
startY: dragStartY,
|
|
1232
|
+
endX: endX,
|
|
1233
|
+
endY: endY,
|
|
1234
|
+
distance: Math.round(distance),
|
|
1235
|
+
timestamp: Date.now()
|
|
1236
|
+
};
|
|
1237
|
+
|
|
1238
|
+
// Add element info if dragging specific elements
|
|
1239
|
+
if (dragStartElement.tagName === 'IMG' || dragStartElement.draggable) {
|
|
1240
|
+
action.draggedElement = dragStartElement.tagName.toLowerCase();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
workflowActions.push(action);
|
|
1244
|
+
updateWorkflowActionCount();
|
|
1245
|
+
|
|
1246
|
+
// Visual feedback
|
|
1247
|
+
flashElement(dragStartElement);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
isDragging = false;
|
|
1251
|
+
dragStartElement = null;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Start workflow recording
|
|
1255
|
+
function startWorkflowRecording(screenshotSettings) {
|
|
1256
|
+
isWorkflowRecording = true;
|
|
1257
|
+
workflowActions = [];
|
|
1258
|
+
workflowScreenshotSettings = screenshotSettings;
|
|
1259
|
+
createWorkflowRecordingIndicator();
|
|
1260
|
+
|
|
1261
|
+
console.log('[ChromePilot] Workflow recording started with settings:', screenshotSettings);
|
|
1262
|
+
|
|
1263
|
+
// Start runtime bridge recording for function traces
|
|
1264
|
+
if (window.__chromePilot && window.__chromePilot.startRecording) {
|
|
1265
|
+
window.__chromePilot.startRecording();
|
|
1266
|
+
console.log('[ChromePilot] Started runtime bridge recording for function traces');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Connect to the existing FunctionTracker for workflow recording
|
|
1270
|
+
if (window.ChromePilotTracker && window.ChromePilotTracker._functionTracker) {
|
|
1271
|
+
const tracker = window.ChromePilotTracker._functionTracker;
|
|
1272
|
+
|
|
1273
|
+
// Set up workflow-specific onDataReady callback for function traces
|
|
1274
|
+
const originalOnDataReady = tracker.onDataReady;
|
|
1275
|
+
tracker.onDataReady = (events) => {
|
|
1276
|
+
// Call original callback if it exists (for full data recording)
|
|
1277
|
+
if (originalOnDataReady) {
|
|
1278
|
+
originalOnDataReady(events);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Process events for workflow recording
|
|
1282
|
+
events.forEach(event => {
|
|
1283
|
+
if (event.type === 'execution_trace' && event.function_name) {
|
|
1284
|
+
// Convert execution_trace to workflow function-trace format
|
|
1285
|
+
workflowActions.push({
|
|
1286
|
+
type: 'function-trace',
|
|
1287
|
+
component: event.function_name, // Map function_name to component
|
|
1288
|
+
args: event.arguments || event.arg_summary || 'unknown',
|
|
1289
|
+
timestamp: event.timestamp,
|
|
1290
|
+
stack: event.call_stack || null
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
console.log('[ChromePilot] Function trace captured for workflow:', event.function_name);
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
};
|
|
1297
|
+
|
|
1298
|
+
// Enable function tracking via ChromePilotTracker
|
|
1299
|
+
window.ChromePilotTracker._setRecordingStatus(true);
|
|
1300
|
+
console.log('[ChromePilot] Connected workflow recording to FunctionTracker');
|
|
1301
|
+
} else {
|
|
1302
|
+
console.warn('[ChromePilot] FunctionTracker not available for workflow recording');
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Add event listeners for recording
|
|
1306
|
+
document.addEventListener('click', recordClick, true);
|
|
1307
|
+
document.addEventListener('input', recordInput, true);
|
|
1308
|
+
document.addEventListener('change', recordInput, true);
|
|
1309
|
+
document.addEventListener('mousedown', recordMouseDown, true);
|
|
1310
|
+
document.addEventListener('mouseup', recordMouseUp, true);
|
|
1311
|
+
|
|
1312
|
+
// Function traces are now handled directly via FunctionTracker connection above
|
|
1313
|
+
// Removed phantom message listener that was waiting for messages that never came
|
|
1314
|
+
|
|
1315
|
+
// Track navigation events
|
|
1316
|
+
const currentUrl = window.location.href;
|
|
1317
|
+
window.addEventListener('beforeunload', () => {
|
|
1318
|
+
if (isWorkflowRecording && window.location.href !== currentUrl) {
|
|
1319
|
+
workflowActions.push({
|
|
1320
|
+
type: 'navigation',
|
|
1321
|
+
url: window.location.href,
|
|
1322
|
+
timestamp: Date.now()
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// Stop workflow recording
|
|
1329
|
+
function stopWorkflowRecording() {
|
|
1330
|
+
isWorkflowRecording = false;
|
|
1331
|
+
removeWorkflowRecordingIndicator();
|
|
1332
|
+
|
|
1333
|
+
console.log(`[ChromePilot] Stopping recording. Captured ${workflowActions.length} actions`);
|
|
1334
|
+
|
|
1335
|
+
// Disconnect from FunctionTracker and disable recording
|
|
1336
|
+
if (window.ChromePilotTracker && window.ChromePilotTracker._functionTracker) {
|
|
1337
|
+
// Disable function tracking
|
|
1338
|
+
window.ChromePilotTracker._setRecordingStatus(false);
|
|
1339
|
+
|
|
1340
|
+
// Count function traces captured during workflow recording
|
|
1341
|
+
const functionTraces = workflowActions.filter(action => action.type === 'function-trace');
|
|
1342
|
+
console.log(`[ChromePilot] Function tracking stopped. Captured ${functionTraces.length} function traces`);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// Remove event listeners
|
|
1346
|
+
document.removeEventListener('click', recordClick, true);
|
|
1347
|
+
document.removeEventListener('input', recordInput, true);
|
|
1348
|
+
document.removeEventListener('change', recordInput, true);
|
|
1349
|
+
document.removeEventListener('mousedown', recordMouseDown, true);
|
|
1350
|
+
document.removeEventListener('mouseup', recordMouseUp, true);
|
|
1351
|
+
|
|
1352
|
+
// Reset drag state
|
|
1353
|
+
isDragging = false;
|
|
1354
|
+
dragStartElement = null;
|
|
1355
|
+
|
|
1356
|
+
// Extract function traces from the __chromePilot bridge if available
|
|
1357
|
+
let functionTraces = [];
|
|
1358
|
+
|
|
1359
|
+
// First, get any function traces from the runtime bridge
|
|
1360
|
+
if (window.__chromePilot && window.__chromePilot.stopRecording) {
|
|
1361
|
+
console.log('[ChromeDebug MCP] Stopping runtime bridge recording...');
|
|
1362
|
+
const bridgeTraces = window.__chromePilot.stopRecording();
|
|
1363
|
+
if (Array.isArray(bridgeTraces)) {
|
|
1364
|
+
// Convert bridge traces to workflow action format
|
|
1365
|
+
functionTraces = bridgeTraces.map((trace, index) => ({
|
|
1366
|
+
type: 'function-trace',
|
|
1367
|
+
component: trace.component || 'unknown',
|
|
1368
|
+
args: trace.args || [],
|
|
1369
|
+
stack: trace.stack || '',
|
|
1370
|
+
timestamp: trace.timestamp || Date.now(),
|
|
1371
|
+
selector: '',
|
|
1372
|
+
x: null,
|
|
1373
|
+
y: null,
|
|
1374
|
+
value: null,
|
|
1375
|
+
text: null,
|
|
1376
|
+
placeholder: null,
|
|
1377
|
+
screenshot_data: null
|
|
1378
|
+
}));
|
|
1379
|
+
console.log(`[Chrome Debug] Retrieved ${functionTraces.length} function traces from runtime bridge`);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// Also include any function traces that were added to workflowActions
|
|
1384
|
+
const embeddedTraces = workflowActions.filter(action => action.type === 'function-trace');
|
|
1385
|
+
if (embeddedTraces.length > 0) {
|
|
1386
|
+
console.log(`[Chrome Debug] Found ${embeddedTraces.length} embedded function traces`);
|
|
1387
|
+
functionTraces = [...functionTraces, ...embeddedTraces];
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Return the recorded workflow with proper functionTraces field
|
|
1391
|
+
return {
|
|
1392
|
+
actions: workflowActions,
|
|
1393
|
+
functionTraces: functionTraces // Now includes actual function traces with component details
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Capture DOM snapshot with form values and states
|
|
1398
|
+
function captureDOMSnapshot() {
|
|
1399
|
+
const snapshot = {
|
|
1400
|
+
html: document.documentElement.outerHTML,
|
|
1401
|
+
formData: {},
|
|
1402
|
+
checkboxStates: {},
|
|
1403
|
+
radioStates: {},
|
|
1404
|
+
selectValues: {},
|
|
1405
|
+
textareaValues: {},
|
|
1406
|
+
customElements: [],
|
|
1407
|
+
activeElement: null,
|
|
1408
|
+
modals: []
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// Capture form input values
|
|
1412
|
+
const inputs = document.querySelectorAll('input');
|
|
1413
|
+
inputs.forEach((input, index) => {
|
|
1414
|
+
const selector = getUniqueSelector(input);
|
|
1415
|
+
if (input.type === 'checkbox') {
|
|
1416
|
+
snapshot.checkboxStates[selector] = input.checked;
|
|
1417
|
+
} else if (input.type === 'radio') {
|
|
1418
|
+
snapshot.radioStates[selector] = {
|
|
1419
|
+
checked: input.checked,
|
|
1420
|
+
value: input.value,
|
|
1421
|
+
name: input.name
|
|
1422
|
+
};
|
|
1423
|
+
} else if (input.type !== 'file' && input.type !== 'password') {
|
|
1424
|
+
snapshot.formData[selector] = input.value;
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
// Capture select values
|
|
1429
|
+
const selects = document.querySelectorAll('select');
|
|
1430
|
+
selects.forEach(select => {
|
|
1431
|
+
const selector = getUniqueSelector(select);
|
|
1432
|
+
snapshot.selectValues[selector] = {
|
|
1433
|
+
value: select.value,
|
|
1434
|
+
selectedIndex: select.selectedIndex,
|
|
1435
|
+
options: Array.from(select.options).map(opt => ({
|
|
1436
|
+
value: opt.value,
|
|
1437
|
+
text: opt.text,
|
|
1438
|
+
selected: opt.selected
|
|
1439
|
+
}))
|
|
1440
|
+
};
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
// Capture textarea values
|
|
1444
|
+
const textareas = document.querySelectorAll('textarea');
|
|
1445
|
+
textareas.forEach(textarea => {
|
|
1446
|
+
const selector = getUniqueSelector(textarea);
|
|
1447
|
+
snapshot.textareaValues[selector] = textarea.value;
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// Capture active element
|
|
1451
|
+
if (document.activeElement) {
|
|
1452
|
+
snapshot.activeElement = getUniqueSelector(document.activeElement);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Capture visible modals/dialogs
|
|
1456
|
+
const dialogs = document.querySelectorAll('dialog[open], [role="dialog"]:not([aria-hidden="true"])');
|
|
1457
|
+
dialogs.forEach(dialog => {
|
|
1458
|
+
snapshot.modals.push({
|
|
1459
|
+
selector: getUniqueSelector(dialog),
|
|
1460
|
+
isOpen: true,
|
|
1461
|
+
zIndex: window.getComputedStyle(dialog).zIndex
|
|
1462
|
+
});
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
// Capture custom element states (e.g., web components)
|
|
1466
|
+
const customEls = document.querySelectorAll('[data-state], [aria-expanded], [aria-selected]');
|
|
1467
|
+
customEls.forEach(el => {
|
|
1468
|
+
snapshot.customElements.push({
|
|
1469
|
+
selector: getUniqueSelector(el),
|
|
1470
|
+
attributes: {
|
|
1471
|
+
'data-state': el.getAttribute('data-state'),
|
|
1472
|
+
'aria-expanded': el.getAttribute('aria-expanded'),
|
|
1473
|
+
'aria-selected': el.getAttribute('aria-selected')
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
return snapshot;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Capture storage data
|
|
1482
|
+
async function captureStorageData() {
|
|
1483
|
+
const storage = {
|
|
1484
|
+
localStorage: {},
|
|
1485
|
+
sessionStorage: {},
|
|
1486
|
+
cookies: []
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
// Capture localStorage
|
|
1490
|
+
try {
|
|
1491
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
1492
|
+
const key = localStorage.key(i);
|
|
1493
|
+
storage.localStorage[key] = localStorage.getItem(key);
|
|
1494
|
+
}
|
|
1495
|
+
} catch (e) {
|
|
1496
|
+
console.error('[ChromePilot] Error capturing localStorage:', e);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Capture sessionStorage
|
|
1500
|
+
try {
|
|
1501
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
1502
|
+
const key = sessionStorage.key(i);
|
|
1503
|
+
storage.sessionStorage[key] = sessionStorage.getItem(key);
|
|
1504
|
+
}
|
|
1505
|
+
} catch (e) {
|
|
1506
|
+
console.error('[ChromePilot] Error capturing sessionStorage:', e);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Request cookies from background script
|
|
1510
|
+
try {
|
|
1511
|
+
const response = await chrome.runtime.sendMessage({
|
|
1512
|
+
action: 'getCookies',
|
|
1513
|
+
url: window.location.href
|
|
1514
|
+
});
|
|
1515
|
+
if (response && response.cookies) {
|
|
1516
|
+
storage.cookies = response.cookies;
|
|
1517
|
+
}
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
console.error('[ChromePilot] Error capturing cookies:', e);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
return storage;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Capture console logs (recent ones)
|
|
1526
|
+
function captureConsoleLogs() {
|
|
1527
|
+
// This would be populated by console interception
|
|
1528
|
+
// For now, return empty array - logs are captured separately
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Create restore point
|
|
1533
|
+
async function createRestorePoint(actionIndex) {
|
|
1534
|
+
console.log('[ChromePilot] Creating restore point at action index:', actionIndex);
|
|
1535
|
+
|
|
1536
|
+
try {
|
|
1537
|
+
// Capture all data
|
|
1538
|
+
const domSnapshot = captureDOMSnapshot();
|
|
1539
|
+
const storageData = await captureStorageData();
|
|
1540
|
+
const consoleLogs = captureConsoleLogs();
|
|
1541
|
+
|
|
1542
|
+
const restorePoint = {
|
|
1543
|
+
actionIndex: actionIndex,
|
|
1544
|
+
url: window.location.href,
|
|
1545
|
+
title: document.title,
|
|
1546
|
+
domSnapshot: domSnapshot,
|
|
1547
|
+
scrollX: window.scrollX,
|
|
1548
|
+
scrollY: window.scrollY,
|
|
1549
|
+
localStorage: storageData.localStorage,
|
|
1550
|
+
sessionStorage: storageData.sessionStorage,
|
|
1551
|
+
cookies: storageData.cookies,
|
|
1552
|
+
consoleLogs: consoleLogs,
|
|
1553
|
+
timestamp: Date.now()
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
// Send to background script to store
|
|
1557
|
+
const response = await chrome.runtime.sendMessage({
|
|
1558
|
+
action: 'saveRestorePoint',
|
|
1559
|
+
restorePoint: restorePoint
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
if (response && response.success) {
|
|
1563
|
+
console.log('[ChromePilot] Restore point saved:', response.restorePointId);
|
|
1564
|
+
// Visual feedback
|
|
1565
|
+
showRestorePointNotification('Restore point saved!');
|
|
1566
|
+
return response.restorePointId;
|
|
1567
|
+
} else {
|
|
1568
|
+
console.error('[ChromePilot] Failed to save restore point:', response);
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
console.error('[ChromePilot] Error creating restore point:', error);
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Restore from a restore point
|
|
1578
|
+
async function restoreFromPoint(restorePointData) {
|
|
1579
|
+
console.log('[ChromePilot] Restoring from restore point');
|
|
1580
|
+
|
|
1581
|
+
try {
|
|
1582
|
+
// First check if we need to navigate
|
|
1583
|
+
if (restorePointData.url !== window.location.href) {
|
|
1584
|
+
// Request navigation and wait for it
|
|
1585
|
+
await chrome.runtime.sendMessage({
|
|
1586
|
+
action: 'navigateAndRestore',
|
|
1587
|
+
url: restorePointData.url,
|
|
1588
|
+
restorePointId: restorePointData.id
|
|
1589
|
+
});
|
|
1590
|
+
// Navigation will trigger restore after page loads
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Restore localStorage
|
|
1595
|
+
try {
|
|
1596
|
+
localStorage.clear();
|
|
1597
|
+
Object.entries(restorePointData.localStorage).forEach(([key, value]) => {
|
|
1598
|
+
localStorage.setItem(key, value);
|
|
1599
|
+
});
|
|
1600
|
+
} catch (e) {
|
|
1601
|
+
console.error('[ChromePilot] Error restoring localStorage:', e);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Restore sessionStorage
|
|
1605
|
+
try {
|
|
1606
|
+
sessionStorage.clear();
|
|
1607
|
+
Object.entries(restorePointData.sessionStorage).forEach(([key, value]) => {
|
|
1608
|
+
sessionStorage.setItem(key, value);
|
|
1609
|
+
});
|
|
1610
|
+
} catch (e) {
|
|
1611
|
+
console.error('[ChromePilot] Error restoring sessionStorage:', e);
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// Restore DOM state
|
|
1615
|
+
const snapshot = restorePointData.domSnapshot;
|
|
1616
|
+
|
|
1617
|
+
// Restore form values
|
|
1618
|
+
Object.entries(snapshot.formData).forEach(([selector, value]) => {
|
|
1619
|
+
try {
|
|
1620
|
+
const element = document.querySelector(selector);
|
|
1621
|
+
if (element) element.value = value;
|
|
1622
|
+
} catch (e) {}
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
// Restore checkboxes
|
|
1626
|
+
Object.entries(snapshot.checkboxStates).forEach(([selector, checked]) => {
|
|
1627
|
+
try {
|
|
1628
|
+
const element = document.querySelector(selector);
|
|
1629
|
+
if (element) element.checked = checked;
|
|
1630
|
+
} catch (e) {}
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
// Restore radio buttons
|
|
1634
|
+
Object.entries(snapshot.radioStates).forEach(([selector, state]) => {
|
|
1635
|
+
try {
|
|
1636
|
+
const element = document.querySelector(selector);
|
|
1637
|
+
if (element && state.checked) element.checked = true;
|
|
1638
|
+
} catch (e) {}
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
// Restore select values
|
|
1642
|
+
Object.entries(snapshot.selectValues).forEach(([selector, data]) => {
|
|
1643
|
+
try {
|
|
1644
|
+
const element = document.querySelector(selector);
|
|
1645
|
+
if (element) {
|
|
1646
|
+
element.value = data.value;
|
|
1647
|
+
element.selectedIndex = data.selectedIndex;
|
|
1648
|
+
}
|
|
1649
|
+
} catch (e) {}
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
// Restore textarea values
|
|
1653
|
+
Object.entries(snapshot.textareaValues).forEach(([selector, value]) => {
|
|
1654
|
+
try {
|
|
1655
|
+
const element = document.querySelector(selector);
|
|
1656
|
+
if (element) element.value = value;
|
|
1657
|
+
} catch (e) {}
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
// Restore custom element states
|
|
1661
|
+
snapshot.customElements.forEach(({ selector, attributes }) => {
|
|
1662
|
+
try {
|
|
1663
|
+
const element = document.querySelector(selector);
|
|
1664
|
+
if (element) {
|
|
1665
|
+
Object.entries(attributes).forEach(([attr, value]) => {
|
|
1666
|
+
if (value !== null) element.setAttribute(attr, value);
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
} catch (e) {}
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
// Restore scroll position
|
|
1673
|
+
window.scrollTo(restorePointData.scrollX, restorePointData.scrollY);
|
|
1674
|
+
|
|
1675
|
+
// Focus active element if any
|
|
1676
|
+
if (snapshot.activeElement) {
|
|
1677
|
+
try {
|
|
1678
|
+
const element = document.querySelector(snapshot.activeElement);
|
|
1679
|
+
if (element) element.focus();
|
|
1680
|
+
} catch (e) {}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Show notification
|
|
1684
|
+
showRestorePointNotification('State restored successfully!');
|
|
1685
|
+
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
console.error('[ChromePilot] Error restoring from restore point:', error);
|
|
1688
|
+
showRestorePointNotification('Error restoring state', true);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Screen interaction recording functions
|
|
1693
|
+
function sendScreenInteraction(interaction) {
|
|
1694
|
+
if (!isScreenRecording || !isExtensionValid()) return;
|
|
1695
|
+
|
|
1696
|
+
chrome.runtime.sendMessage({
|
|
1697
|
+
action: 'screenInteraction',
|
|
1698
|
+
interaction: interaction
|
|
1699
|
+
}).catch(err => {
|
|
1700
|
+
// Ignore errors if extension context is invalid
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
async function recordScreenClick(event) {
|
|
1705
|
+
if (!isScreenRecording) return;
|
|
1706
|
+
|
|
1707
|
+
let element = event.target;
|
|
1708
|
+
|
|
1709
|
+
// If clicking on an SVG element or its children, find the parent button/link
|
|
1710
|
+
if (element.tagName === 'svg' || element.tagName === 'path' || element.tagName === 'g' || element.tagName === 'circle' || element.tagName === 'rect') {
|
|
1711
|
+
const clickableParent = element.closest('button, a, [role="button"], [onclick]');
|
|
1712
|
+
if (clickableParent) {
|
|
1713
|
+
element = clickableParent;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
const interaction = {
|
|
1718
|
+
type: 'click',
|
|
1719
|
+
selector: getUniqueSelector(element),
|
|
1720
|
+
xpath: getXPath(element),
|
|
1721
|
+
x: event.clientX,
|
|
1722
|
+
y: event.clientY,
|
|
1723
|
+
timestamp: Date.now() - recordingScheduledStartTime // Relative timestamp from scheduled start
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
// Add text content for buttons and links
|
|
1727
|
+
if (element.tagName === 'BUTTON' || element.tagName === 'A') {
|
|
1728
|
+
interaction.text = element.textContent.trim();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Enhanced click tracking data (conditional based on user setting)
|
|
1732
|
+
const enhanceCapture = await shouldEnhanceCapture();
|
|
1733
|
+
if (enhanceCapture) {
|
|
1734
|
+
try {
|
|
1735
|
+
console.log('[ChromePilot] Capturing enhanced screen click data...');
|
|
1736
|
+
|
|
1737
|
+
// Capture enhanced element data (only include fields with meaningful values)
|
|
1738
|
+
const capturedHTML = captureElementHTML(element);
|
|
1739
|
+
if (capturedHTML) {
|
|
1740
|
+
interaction.element_html = capturedHTML;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
const componentData = getEnhancedComponentInfo(element);
|
|
1744
|
+
if (componentData) {
|
|
1745
|
+
try {
|
|
1746
|
+
JSON.stringify(componentData); // Test if serializable
|
|
1747
|
+
interaction.component_data = componentData;
|
|
1748
|
+
} catch (e) {
|
|
1749
|
+
console.error('[ChromePilot] Component data cannot be serialized:', e.message);
|
|
1750
|
+
interaction.component_data = { error: 'Serialization failed', type: typeof componentData };
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const eventHandlers = extractEventHandlers(element);
|
|
1755
|
+
if (eventHandlers) {
|
|
1756
|
+
try {
|
|
1757
|
+
JSON.stringify(eventHandlers); // Test if serializable
|
|
1758
|
+
interaction.event_handlers = eventHandlers;
|
|
1759
|
+
} catch (e) {
|
|
1760
|
+
console.error('[ChromePilot] Event handlers cannot be serialized:', e.message);
|
|
1761
|
+
interaction.event_handlers = { error: 'Serialization failed', type: typeof eventHandlers };
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const elementState = captureElementState(element);
|
|
1766
|
+
if (elementState) {
|
|
1767
|
+
try {
|
|
1768
|
+
JSON.stringify(elementState); // Test if serializable
|
|
1769
|
+
interaction.element_state = elementState;
|
|
1770
|
+
} catch (e) {
|
|
1771
|
+
console.error('[ChromePilot] Element state cannot be serialized:', e.message);
|
|
1772
|
+
interaction.element_state = { error: 'Serialization failed', type: typeof elementState };
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
const perfMetrics = getPerformanceMetrics();
|
|
1777
|
+
if (perfMetrics) {
|
|
1778
|
+
try {
|
|
1779
|
+
JSON.stringify(perfMetrics); // Test if serializable
|
|
1780
|
+
interaction.performance_metrics = perfMetrics;
|
|
1781
|
+
} catch (e) {
|
|
1782
|
+
console.error('[ChromePilot] Performance metrics cannot be serialized:', e.message);
|
|
1783
|
+
interaction.performance_metrics = { error: 'Serialization failed', type: typeof perfMetrics };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
console.log('[ChromePilot] Enhanced screen click data captured successfully');
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
console.warn('[ChromePilot] Error capturing enhanced screen click data:', error);
|
|
1790
|
+
// Continue with basic click recording if enhanced capture fails
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
console.log('[ChromePilot] Enhanced screen click capture disabled - using basic capture only');
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
sendScreenInteraction(interaction);
|
|
1797
|
+
|
|
1798
|
+
// Trigger visual feedback
|
|
1799
|
+
if (window.screenCaptureVisualFeedback) {
|
|
1800
|
+
window.screenCaptureVisualFeedback.handleScreenCaptureInteraction(interaction);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
function recordScreenInput(event) {
|
|
1805
|
+
if (!isScreenRecording) return;
|
|
1806
|
+
|
|
1807
|
+
const element = event.target;
|
|
1808
|
+
const interaction = {
|
|
1809
|
+
type: 'input',
|
|
1810
|
+
selector: getUniqueSelector(element),
|
|
1811
|
+
xpath: getXPath(element),
|
|
1812
|
+
value: element.value,
|
|
1813
|
+
timestamp: Date.now() - recordingScheduledStartTime // Relative timestamp from scheduled start
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
// Add input type for better context
|
|
1817
|
+
if (element.type) {
|
|
1818
|
+
interaction.inputType = element.type;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Add placeholder for context
|
|
1822
|
+
if (element.placeholder) {
|
|
1823
|
+
interaction.placeholder = element.placeholder;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
sendScreenInteraction(interaction);
|
|
1827
|
+
|
|
1828
|
+
// Trigger visual feedback
|
|
1829
|
+
if (window.screenCaptureVisualFeedback) {
|
|
1830
|
+
window.screenCaptureVisualFeedback.handleScreenCaptureInteraction(interaction);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
function recordScreenKeypress(event) {
|
|
1835
|
+
if (!isScreenRecording) return;
|
|
1836
|
+
|
|
1837
|
+
// Only record special keys
|
|
1838
|
+
if (event.key === 'Enter' || event.key === 'Escape' || event.key === 'Tab') {
|
|
1839
|
+
const interaction = {
|
|
1840
|
+
type: 'keypress',
|
|
1841
|
+
key: event.key,
|
|
1842
|
+
timestamp: Date.now() - recordingScheduledStartTime // Relative timestamp from scheduled start
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
sendScreenInteraction(interaction);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function recordScreenScroll(event) {
|
|
1850
|
+
if (!isScreenRecording) return;
|
|
1851
|
+
|
|
1852
|
+
// Throttle scroll events
|
|
1853
|
+
if (this.scrollTimeout) {
|
|
1854
|
+
clearTimeout(this.scrollTimeout);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
this.scrollTimeout = setTimeout(() => {
|
|
1858
|
+
const interaction = {
|
|
1859
|
+
type: 'scroll',
|
|
1860
|
+
scrollX: window.scrollX,
|
|
1861
|
+
scrollY: window.scrollY,
|
|
1862
|
+
timestamp: Date.now() - recordingScheduledStartTime // Relative timestamp from scheduled start
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
sendScreenInteraction(interaction);
|
|
1866
|
+
}, 200);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Record screen drag start - mirrors workflow recordMouseDown
|
|
1870
|
+
function recordScreenMouseDown(event) {
|
|
1871
|
+
if (!isScreenRecording) return;
|
|
1872
|
+
|
|
1873
|
+
// Only track left mouse button
|
|
1874
|
+
if (event.button !== 0) return;
|
|
1875
|
+
|
|
1876
|
+
isScreenRecordingDragging = true;
|
|
1877
|
+
screenRecordingDragStartElement = event.target;
|
|
1878
|
+
screenRecordingDragStartX = event.clientX;
|
|
1879
|
+
screenRecordingDragStartY = event.clientY;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// Record screen drag end - mirrors workflow recordMouseUp
|
|
1883
|
+
function recordScreenMouseUp(event) {
|
|
1884
|
+
if (!isScreenRecording || !isScreenRecordingDragging) return;
|
|
1885
|
+
|
|
1886
|
+
const endX = event.clientX;
|
|
1887
|
+
const endY = event.clientY;
|
|
1888
|
+
|
|
1889
|
+
// Only record if there was actual movement (same 5px threshold as workflow)
|
|
1890
|
+
const distance = Math.sqrt(Math.pow(endX - screenRecordingDragStartX, 2) + Math.pow(endY - screenRecordingDragStartY, 2));
|
|
1891
|
+
if (distance > 5) {
|
|
1892
|
+
const interaction = {
|
|
1893
|
+
type: 'drag',
|
|
1894
|
+
selector: getUniqueSelector(screenRecordingDragStartElement),
|
|
1895
|
+
xpath: getXPath(screenRecordingDragStartElement),
|
|
1896
|
+
x: screenRecordingDragStartX,
|
|
1897
|
+
y: screenRecordingDragStartY,
|
|
1898
|
+
value: JSON.stringify({
|
|
1899
|
+
endX: endX,
|
|
1900
|
+
endY: endY,
|
|
1901
|
+
distance: Math.round(distance)
|
|
1902
|
+
}),
|
|
1903
|
+
timestamp: Date.now() - recordingScheduledStartTime
|
|
1904
|
+
};
|
|
1905
|
+
|
|
1906
|
+
// Add element info if dragging specific elements (mirrors workflow logic)
|
|
1907
|
+
if (screenRecordingDragStartElement.tagName === 'IMG' || screenRecordingDragStartElement.draggable) {
|
|
1908
|
+
interaction.text = screenRecordingDragStartElement.tagName.toLowerCase();
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
sendScreenInteraction(interaction);
|
|
1912
|
+
|
|
1913
|
+
// Trigger visual feedback
|
|
1914
|
+
if (window.screenCaptureVisualFeedback) {
|
|
1915
|
+
window.screenCaptureVisualFeedback.handleScreenCaptureInteraction(interaction);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
isScreenRecordingDragging = false;
|
|
1920
|
+
screenRecordingDragStartElement = null;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
// Global variables for synchronized timing
|
|
1924
|
+
let recordingScheduledStartTime = null;
|
|
1925
|
+
let recordingSessionId = null;
|
|
1926
|
+
|
|
1927
|
+
// Start/stop screen interaction tracking
|
|
1928
|
+
function startScreenInteractionTracking(scheduledStartTime, sessionId) {
|
|
1929
|
+
// Store the scheduled start time and session ID for timestamp calculations
|
|
1930
|
+
recordingScheduledStartTime = scheduledStartTime;
|
|
1931
|
+
recordingSessionId = sessionId;
|
|
1932
|
+
|
|
1933
|
+
console.log('[ChromePilot] Screen interaction tracking scheduled to start at:', new Date(scheduledStartTime));
|
|
1934
|
+
|
|
1935
|
+
// Trigger visual feedback start
|
|
1936
|
+
if (window.screenCaptureVisualFeedback) {
|
|
1937
|
+
window.screenCaptureVisualFeedback.handleStartScreenCapture({ sessionId, scheduledStartTime });
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// Wait until the scheduled start time before activating tracking
|
|
1941
|
+
const waitTime = Math.max(0, scheduledStartTime - Date.now());
|
|
1942
|
+
setTimeout(() => {
|
|
1943
|
+
isScreenRecording = true;
|
|
1944
|
+
|
|
1945
|
+
// Add event listeners
|
|
1946
|
+
document.addEventListener('click', recordScreenClick, true);
|
|
1947
|
+
document.addEventListener('input', recordScreenInput, true);
|
|
1948
|
+
document.addEventListener('change', recordScreenInput, true);
|
|
1949
|
+
document.addEventListener('keydown', recordScreenKeypress, true);
|
|
1950
|
+
window.addEventListener('scroll', recordScreenScroll, true);
|
|
1951
|
+
document.addEventListener('mousedown', recordScreenMouseDown, true);
|
|
1952
|
+
document.addEventListener('mouseup', recordScreenMouseUp, true);
|
|
1953
|
+
|
|
1954
|
+
console.log('[ChromePilot] Screen interaction tracking started at scheduled time');
|
|
1955
|
+
}, waitTime);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function stopScreenInteractionTracking() {
|
|
1959
|
+
isScreenRecording = false;
|
|
1960
|
+
|
|
1961
|
+
// Array of cleanup operations for systematic execution
|
|
1962
|
+
const cleanupOperations = [
|
|
1963
|
+
() => document.removeEventListener('click', recordScreenClick, true),
|
|
1964
|
+
() => document.removeEventListener('input', recordScreenInput, true),
|
|
1965
|
+
() => document.removeEventListener('change', recordScreenInput, true),
|
|
1966
|
+
() => document.removeEventListener('keydown', recordScreenKeypress, true),
|
|
1967
|
+
() => window.removeEventListener('scroll', recordScreenScroll, true),
|
|
1968
|
+
() => document.removeEventListener('mousedown', recordScreenMouseDown, true),
|
|
1969
|
+
() => document.removeEventListener('mouseup', recordScreenMouseUp, true)
|
|
1970
|
+
];
|
|
1971
|
+
|
|
1972
|
+
// Execute all cleanup with individual error handling
|
|
1973
|
+
cleanupOperations.forEach((cleanup, index) => {
|
|
1974
|
+
try {
|
|
1975
|
+
cleanup();
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
console.warn(`[ChromePilot] Failed cleanup operation ${index}:`, e);
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
|
|
1981
|
+
// ALWAYS reset ALL state variables regardless of errors above
|
|
1982
|
+
isScreenRecordingDragging = false;
|
|
1983
|
+
screenRecordingDragStartElement = null;
|
|
1984
|
+
screenRecordingDragStartX = 0; // CRITICAL: This was missing
|
|
1985
|
+
screenRecordingDragStartY = 0; // CRITICAL: This was missing
|
|
1986
|
+
|
|
1987
|
+
console.log('[ChromePilot] Screen interaction tracking stopped');
|
|
1988
|
+
|
|
1989
|
+
// Visual feedback stop with error isolation
|
|
1990
|
+
try {
|
|
1991
|
+
if (window.screenCaptureVisualFeedback) {
|
|
1992
|
+
window.screenCaptureVisualFeedback.handleStopScreenCapture({});
|
|
1993
|
+
}
|
|
1994
|
+
} catch (e) {
|
|
1995
|
+
console.warn('[ChromePilot] Failed to stop visual feedback:', e);
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// Show restore point notification
|
|
2000
|
+
function showRestorePointNotification(message, isError = false) {
|
|
2001
|
+
const notification = document.createElement('div');
|
|
2002
|
+
notification.textContent = message;
|
|
2003
|
+
notification.style.cssText = `
|
|
2004
|
+
position: fixed;
|
|
2005
|
+
top: 20px;
|
|
2006
|
+
right: 20px;
|
|
2007
|
+
padding: 12px 24px;
|
|
2008
|
+
background: ${isError ? '#f44336' : '#4CAF50'};
|
|
2009
|
+
color: white;
|
|
2010
|
+
border-radius: 4px;
|
|
2011
|
+
font-family: Arial, sans-serif;
|
|
2012
|
+
font-size: 14px;
|
|
2013
|
+
z-index: 10000;
|
|
2014
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
2015
|
+
animation: slideIn 0.3s ease-out;
|
|
2016
|
+
`;
|
|
2017
|
+
|
|
2018
|
+
const style = document.createElement('style');
|
|
2019
|
+
style.textContent = `
|
|
2020
|
+
@keyframes slideIn {
|
|
2021
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
2022
|
+
to { transform: translateX(0); opacity: 1; }
|
|
2023
|
+
}
|
|
2024
|
+
`;
|
|
2025
|
+
document.head.appendChild(style);
|
|
2026
|
+
|
|
2027
|
+
document.body.appendChild(notification);
|
|
2028
|
+
|
|
2029
|
+
setTimeout(() => {
|
|
2030
|
+
notification.style.animation = 'slideOut 0.3s ease-in forwards';
|
|
2031
|
+
notification.style.animationName = 'slideOut';
|
|
2032
|
+
setTimeout(() => {
|
|
2033
|
+
notification.remove();
|
|
2034
|
+
style.remove();
|
|
2035
|
+
}, 300);
|
|
2036
|
+
}, 3000);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Clean up when navigating away or extension is unloaded
|
|
2040
|
+
window.addEventListener('beforeunload', cleanup);
|
|
2041
|
+
// Use pagehide instead of deprecated unload event for better Chrome compatibility
|
|
2042
|
+
window.addEventListener('pagehide', cleanup);
|
|
2043
|
+
|
|
2044
|
+
// Clean up if extension context becomes invalid
|
|
2045
|
+
// Note: We don't need chrome.runtime.connect() here because:
|
|
2046
|
+
// 1. It requires a corresponding onConnect listener in background script
|
|
2047
|
+
// 2. The beforeunload/unload listeners already handle cleanup
|
|
2048
|
+
// 3. Extension invalidation is handled by isExtensionValid() checks
|
|
2049
|
+
|
|
2050
|
+
// Recording UI elements
|
|
2051
|
+
let recordingOverlay = null;
|
|
2052
|
+
let recordingStartTime = null;
|
|
2053
|
+
let recordingTimer = null;
|
|
2054
|
+
let isRecording = false;
|
|
2055
|
+
|
|
2056
|
+
// Screen recording interaction tracking
|
|
2057
|
+
let isScreenRecording = false;
|
|
2058
|
+
|
|
2059
|
+
// Screen recording drag detection - mirrors workflow recording logic
|
|
2060
|
+
// See recordMouseDown/Up (workflow) for reference implementation
|
|
2061
|
+
let isScreenRecordingDragging = false;
|
|
2062
|
+
let screenRecordingDragStartElement = null;
|
|
2063
|
+
let screenRecordingDragStartX = 0;
|
|
2064
|
+
let screenRecordingDragStartY = 0;
|
|
2065
|
+
|
|
2066
|
+
// Full data recording system components
|
|
2067
|
+
let fullDataRecording = null;
|
|
2068
|
+
let dataBuffer = null;
|
|
2069
|
+
// uploadManager is now centralized in background script
|
|
2070
|
+
let performanceMonitor = null;
|
|
2071
|
+
let piiRedactor = null;
|
|
2072
|
+
let domTracker = null;
|
|
2073
|
+
let networkTracker = null;
|
|
2074
|
+
let functionTracker = null;
|
|
2075
|
+
let currentFullRecordingId = null;
|
|
2076
|
+
// Disable full data recording permanently
|
|
2077
|
+
let isFullDataRecordingActive = false;
|
|
2078
|
+
|
|
2079
|
+
// Create floating recording interface
|
|
2080
|
+
function showRecordingInterface() {
|
|
2081
|
+
console.log('showRecordingInterface called');
|
|
2082
|
+
|
|
2083
|
+
// Remove any existing recording overlay
|
|
2084
|
+
if (recordingOverlay) {
|
|
2085
|
+
recordingOverlay.remove();
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Create recording overlay
|
|
2089
|
+
recordingOverlay = document.createElement('div');
|
|
2090
|
+
recordingOverlay.id = 'chrome-pilot-recording-overlay';
|
|
2091
|
+
recordingOverlay.style.cssText = `
|
|
2092
|
+
position: fixed;
|
|
2093
|
+
bottom: 20px;
|
|
2094
|
+
right: 20px;
|
|
2095
|
+
width: 300px;
|
|
2096
|
+
background: white;
|
|
2097
|
+
border: 2px solid #f44336;
|
|
2098
|
+
border-radius: 8px;
|
|
2099
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
2100
|
+
z-index: 2147483647;
|
|
2101
|
+
font-family: Arial, sans-serif;
|
|
2102
|
+
padding: 20px;
|
|
2103
|
+
`;
|
|
2104
|
+
|
|
2105
|
+
recordingOverlay.innerHTML = `
|
|
2106
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
2107
|
+
<h3 style="margin: 0; font-size: 16px; color: #333;">Screen Recording</h3>
|
|
2108
|
+
<button id="chrome-pilot-close-recording" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #666;">×</button>
|
|
2109
|
+
</div>
|
|
2110
|
+
<p style="margin: 0 0 15px 0; font-size: 13px; color: #666;">
|
|
2111
|
+
Recording this tab with console logs
|
|
2112
|
+
</p>
|
|
2113
|
+
<div id="chrome-pilot-recording-status" style="text-align: center; margin-bottom: 15px; font-size: 14px; color: #666;"></div>
|
|
2114
|
+
<button id="chrome-pilot-record-btn" style="
|
|
2115
|
+
width: 100%;
|
|
2116
|
+
padding: 10px;
|
|
2117
|
+
background: #f44336;
|
|
2118
|
+
color: white;
|
|
2119
|
+
border: none;
|
|
2120
|
+
border-radius: 4px;
|
|
2121
|
+
cursor: pointer;
|
|
2122
|
+
font-size: 14px;
|
|
2123
|
+
font-weight: 500;
|
|
2124
|
+
">Start Recording</button>
|
|
2125
|
+
<div id="chrome-pilot-recording-result" style="margin-top: 15px; display: none;"></div>
|
|
2126
|
+
`;
|
|
2127
|
+
|
|
2128
|
+
document.body.appendChild(recordingOverlay);
|
|
2129
|
+
|
|
2130
|
+
// Add event listeners
|
|
2131
|
+
document.getElementById('chrome-pilot-close-recording').addEventListener('click', () => {
|
|
2132
|
+
if (isRecording) {
|
|
2133
|
+
if (confirm('Stop recording and close?')) {
|
|
2134
|
+
stopRecording();
|
|
2135
|
+
recordingOverlay.remove();
|
|
2136
|
+
recordingOverlay = null;
|
|
2137
|
+
}
|
|
2138
|
+
} else {
|
|
2139
|
+
recordingOverlay.remove();
|
|
2140
|
+
recordingOverlay = null;
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
|
|
2144
|
+
document.getElementById('chrome-pilot-record-btn').addEventListener('click', () => {
|
|
2145
|
+
if (!isRecording) {
|
|
2146
|
+
startRecording();
|
|
2147
|
+
} else {
|
|
2148
|
+
stopRecording();
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Start recording from content script
|
|
2154
|
+
function startRecording() {
|
|
2155
|
+
const recordBtn = document.getElementById('chrome-pilot-record-btn');
|
|
2156
|
+
const statusDiv = document.getElementById('chrome-pilot-recording-status');
|
|
2157
|
+
|
|
2158
|
+
recordBtn.disabled = true;
|
|
2159
|
+
recordBtn.textContent = 'Starting...';
|
|
2160
|
+
|
|
2161
|
+
// Request recording from background script
|
|
2162
|
+
chrome.runtime.sendMessage({
|
|
2163
|
+
action: 'startRecordingFromContent'
|
|
2164
|
+
}, (response) => {
|
|
2165
|
+
if (chrome.runtime.lastError) {
|
|
2166
|
+
console.error('Error starting recording:', chrome.runtime.lastError);
|
|
2167
|
+
statusDiv.textContent = 'Error: ' + chrome.runtime.lastError.message;
|
|
2168
|
+
recordBtn.disabled = false;
|
|
2169
|
+
recordBtn.textContent = 'Start Recording';
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
if (response && response.success) {
|
|
2174
|
+
isRecording = true;
|
|
2175
|
+
recordBtn.disabled = false;
|
|
2176
|
+
recordBtn.textContent = 'Stop Recording';
|
|
2177
|
+
recordBtn.style.background = '#4CAF50';
|
|
2178
|
+
recordingStartTime = Date.now();
|
|
2179
|
+
|
|
2180
|
+
// Start performance monitoring when recording begins
|
|
2181
|
+
if (performanceMonitor) {
|
|
2182
|
+
performanceMonitor.startMonitoring();
|
|
2183
|
+
console.log('[ChromeDebug MCP] Performance monitoring started with recording');
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// Start timer display
|
|
2187
|
+
recordingTimer = setInterval(() => {
|
|
2188
|
+
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
|
|
2189
|
+
const minutes = Math.floor(elapsed / 60);
|
|
2190
|
+
const seconds = elapsed % 60;
|
|
2191
|
+
statusDiv.innerHTML = `<span style="color: #f44336; font-weight: bold;">● Recording: ${minutes}:${seconds.toString().padStart(2, '0')}</span>`;
|
|
2192
|
+
}, 1000);
|
|
2193
|
+
} else {
|
|
2194
|
+
statusDiv.textContent = 'Error: ' + (response?.error || 'Failed to start recording');
|
|
2195
|
+
recordBtn.disabled = false;
|
|
2196
|
+
recordBtn.textContent = 'Start Recording';
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// Stop recording
|
|
2202
|
+
function stopRecording() {
|
|
2203
|
+
const recordBtn = document.getElementById('chrome-pilot-record-btn');
|
|
2204
|
+
const statusDiv = document.getElementById('chrome-pilot-recording-status');
|
|
2205
|
+
const resultDiv = document.getElementById('chrome-pilot-recording-result');
|
|
2206
|
+
|
|
2207
|
+
recordBtn.disabled = true;
|
|
2208
|
+
recordBtn.textContent = 'Stopping...';
|
|
2209
|
+
|
|
2210
|
+
if (recordingTimer) {
|
|
2211
|
+
clearInterval(recordingTimer);
|
|
2212
|
+
recordingTimer = null;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// Show processing message
|
|
2216
|
+
statusDiv.innerHTML = '<span style="color: #ff9800;">Processing recording...</span>';
|
|
2217
|
+
|
|
2218
|
+
// Request stop from background script
|
|
2219
|
+
chrome.runtime.sendMessage({
|
|
2220
|
+
action: 'stopRecordingFromContent'
|
|
2221
|
+
}, (response) => {
|
|
2222
|
+
if (chrome.runtime.lastError) {
|
|
2223
|
+
console.error('Error stopping recording:', chrome.runtime.lastError);
|
|
2224
|
+
statusDiv.textContent = 'Error: ' + chrome.runtime.lastError.message;
|
|
2225
|
+
recordBtn.disabled = false;
|
|
2226
|
+
recordBtn.textContent = 'Stop Recording';
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// Update status to show upload in progress
|
|
2231
|
+
if (response && response.success) {
|
|
2232
|
+
statusDiv.innerHTML = '<span style="color: #2196F3;">Uploading recording...</span>';
|
|
2233
|
+
// The recordingComplete message will show the ID when upload is done
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
isRecording = false;
|
|
2237
|
+
recordBtn.disabled = false;
|
|
2238
|
+
recordBtn.textContent = 'Start Recording';
|
|
2239
|
+
recordBtn.style.background = '#f44336';
|
|
2240
|
+
|
|
2241
|
+
// Stop performance monitoring when recording ends
|
|
2242
|
+
if (performanceMonitor) {
|
|
2243
|
+
performanceMonitor.stopMonitoring();
|
|
2244
|
+
console.log('[ChromeDebug MCP] Performance monitoring stopped with recording');
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
// Show frame capture flash indicator
|
|
2250
|
+
function showFrameFlash() {
|
|
2251
|
+
if (!frameFlashIndicator) {
|
|
2252
|
+
frameFlashIndicator = document.createElement('div');
|
|
2253
|
+
frameFlashIndicator.style.cssText = `
|
|
2254
|
+
position: fixed;
|
|
2255
|
+
top: 0;
|
|
2256
|
+
left: 0;
|
|
2257
|
+
width: 100vw;
|
|
2258
|
+
height: 100vh;
|
|
2259
|
+
background: rgba(255, 255, 255, 0.3);
|
|
2260
|
+
pointer-events: none;
|
|
2261
|
+
z-index: 999999;
|
|
2262
|
+
opacity: 0;
|
|
2263
|
+
transition: opacity 0.1s ease-in-out;
|
|
2264
|
+
`;
|
|
2265
|
+
document.body.appendChild(frameFlashIndicator);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Flash effect
|
|
2269
|
+
frameFlashIndicator.style.opacity = '1';
|
|
2270
|
+
setTimeout(() => {
|
|
2271
|
+
frameFlashIndicator.style.opacity = '0';
|
|
2272
|
+
}, 100);
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// ============================================================================
|
|
2276
|
+
// FULL DATA RECORDING SYSTEM INTEGRATION
|
|
2277
|
+
// ============================================================================
|
|
2278
|
+
|
|
2279
|
+
// Initialize full data recording system (DISABLED)
|
|
2280
|
+
/*
|
|
2281
|
+
async function initializeFullDataRecordingSystem() {
|
|
2282
|
+
try {
|
|
2283
|
+
console.log('[ChromeDebug MCP] Initializing full data recording system...');
|
|
2284
|
+
|
|
2285
|
+
// Initialize core components
|
|
2286
|
+
if (typeof DataBuffer !== 'undefined') {
|
|
2287
|
+
dataBuffer = new DataBuffer();
|
|
2288
|
+
await dataBuffer.init();
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// UploadManager is now centralized in background script
|
|
2292
|
+
// Content script will communicate via message passing
|
|
2293
|
+
|
|
2294
|
+
if (typeof PIIRedactor !== 'undefined') {
|
|
2295
|
+
piiRedactor = new PIIRedactor();
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
if (typeof PerformanceMonitor !== 'undefined') {
|
|
2299
|
+
performanceMonitor = new PerformanceMonitor();
|
|
2300
|
+
performanceMonitor.init(dataBuffer, {
|
|
2301
|
+
onThrottleRequired: (alerts) => {
|
|
2302
|
+
console.warn('[ChromeDebug MCP] Performance throttling required:', JSON.stringify(alerts, null, 2));
|
|
2303
|
+
// Could automatically reduce instrumentation level
|
|
2304
|
+
},
|
|
2305
|
+
onPerformanceAlert: (alerts) => {
|
|
2306
|
+
console.warn('[ChromeDebug MCP] Performance alerts:', JSON.stringify(alerts, null, 2));
|
|
2307
|
+
}
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// Initialize data collection components
|
|
2312
|
+
if (typeof DOMTracker !== 'undefined') {
|
|
2313
|
+
domTracker = new DOMTracker();
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
if (typeof NetworkTracker !== 'undefined') {
|
|
2317
|
+
networkTracker = new NetworkTracker();
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (typeof FunctionTracker !== 'undefined') {
|
|
2321
|
+
// Create FunctionTracker with onDataReady callback to persist data
|
|
2322
|
+
functionTracker = new FunctionTracker(
|
|
2323
|
+
performanceMonitor,
|
|
2324
|
+
piiRedactor,
|
|
2325
|
+
// onDataReady callback - called when function traces need to be persisted
|
|
2326
|
+
async (events) => {
|
|
2327
|
+
if (dataBuffer) {
|
|
2328
|
+
try {
|
|
2329
|
+
await dataBuffer.addBatch(events);
|
|
2330
|
+
console.log(`[Chrome Debug] Persisted ${events.length} function traces`);
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
console.error('[ChromeDebug MCP] Failed to persist function traces:', error);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// Expose ChromePilotTracker API for manual instrumentation
|
|
2340
|
+
if (!window.ChromePilotTracker) {
|
|
2341
|
+
window.ChromePilotTracker = {
|
|
2342
|
+
isRecording: false,
|
|
2343
|
+
_functionTracker: functionTracker,
|
|
2344
|
+
|
|
2345
|
+
// Register a single function for tracing
|
|
2346
|
+
register: function(name, fn) {
|
|
2347
|
+
if (!this.isRecording || typeof fn !== 'function') {
|
|
2348
|
+
return fn; // Return original function if not recording
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
if (!this._functionTracker) {
|
|
2352
|
+
console.warn('[ChromePilotTracker] Function tracker not available');
|
|
2353
|
+
return fn;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// Use the existing wrapFunction from function-tracker
|
|
2357
|
+
return this._functionTracker.wrapFunction(fn, name, {
|
|
2358
|
+
isUserDefined: true,
|
|
2359
|
+
registeredManually: true
|
|
2360
|
+
});
|
|
2361
|
+
},
|
|
2362
|
+
|
|
2363
|
+
// Wrap an entire object/module
|
|
2364
|
+
wrap: function(obj, options = {}) {
|
|
2365
|
+
if (!this.isRecording || !obj || typeof obj !== 'object') {
|
|
2366
|
+
return obj; // Return original if not recording or invalid
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
if (!this._functionTracker) {
|
|
2370
|
+
console.warn('[ChromePilotTracker] Function tracker not available');
|
|
2371
|
+
return obj;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const self = this;
|
|
2375
|
+
const prefix = options.prefix || obj.constructor?.name || 'Object';
|
|
2376
|
+
|
|
2377
|
+
// Use Proxy to intercept method calls
|
|
2378
|
+
return new Proxy(obj, {
|
|
2379
|
+
get(target, prop, receiver) {
|
|
2380
|
+
const original = Reflect.get(target, prop, receiver);
|
|
2381
|
+
|
|
2382
|
+
// Only wrap functions
|
|
2383
|
+
if (typeof original === 'function') {
|
|
2384
|
+
const functionName = `${prefix}.${String(prop)}`;
|
|
2385
|
+
|
|
2386
|
+
// Check if this method should be ignored
|
|
2387
|
+
if (options.ignore && options.ignore.includes(String(prop))) {
|
|
2388
|
+
return original;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// Wrap the function
|
|
2392
|
+
return self._functionTracker.wrapFunction(original, functionName, {
|
|
2393
|
+
isUserDefined: true,
|
|
2394
|
+
registeredManually: true,
|
|
2395
|
+
parentObject: prefix
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
return original;
|
|
2400
|
+
}
|
|
2401
|
+
});
|
|
2402
|
+
},
|
|
2403
|
+
|
|
2404
|
+
// Update recording status
|
|
2405
|
+
_setRecordingStatus: function(status) {
|
|
2406
|
+
this.isRecording = status;
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
console.log('[ChromeDebug MCP] ChromePilotTracker API exposed for manual instrumentation');
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
console.log('[ChromeDebug MCP] Full data recording system initialized successfully');
|
|
2414
|
+
return true;
|
|
2415
|
+
|
|
2416
|
+
} catch (error) {
|
|
2417
|
+
console.error('[ChromeDebug MCP] Failed to initialize full data recording system:', error);
|
|
2418
|
+
return false;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Start full data recording with user configuration (DISABLED)
|
|
2423
|
+
async function startFullDataRecording(config) {
|
|
2424
|
+
try {
|
|
2425
|
+
if (isFullDataRecordingActive) {
|
|
2426
|
+
console.warn('[ChromeDebug MCP] Full data recording already active');
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
console.log('[ChromeDebug MCP] Starting full data recording with config:', config);
|
|
2431
|
+
|
|
2432
|
+
// Initialize system if not already done
|
|
2433
|
+
if (!dataBuffer) {
|
|
2434
|
+
const initialized = await initializeFullDataRecordingSystem();
|
|
2435
|
+
if (!initialized) {
|
|
2436
|
+
throw new Error('Failed to initialize recording system');
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// Use the recording ID from config if provided, otherwise generate one
|
|
2441
|
+
const recordingId = config.recordingId || `recording_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2442
|
+
currentFullRecordingId = recordingId; // Store globally for stop function
|
|
2443
|
+
|
|
2444
|
+
// DataBuffer configuration is handled internally
|
|
2445
|
+
// Store the recording ID in chrome.storage for the dataBuffer to use
|
|
2446
|
+
if (dataBuffer && chrome.storage && chrome.storage.local) {
|
|
2447
|
+
await chrome.storage.local.set({ currentRecordingId: recordingId });
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// Data ready callback for all trackers
|
|
2451
|
+
const onDataReady = (events) => {
|
|
2452
|
+
if (dataBuffer) {
|
|
2453
|
+
events.forEach(event => dataBuffer.addEvent(event));
|
|
2454
|
+
}
|
|
2455
|
+
};
|
|
2456
|
+
|
|
2457
|
+
// Start performance monitoring (lazy loading - only when recording)
|
|
2458
|
+
if (performanceMonitor) {
|
|
2459
|
+
performanceMonitor.startMonitoring();
|
|
2460
|
+
console.log('[ChromeDebug MCP] Performance monitoring started');
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// Configure and start DOM tracking
|
|
2464
|
+
if (domTracker && config.captureTypes.domMutations) {
|
|
2465
|
+
domTracker.init({
|
|
2466
|
+
recordingId: recordingId,
|
|
2467
|
+
onDataReady: onDataReady,
|
|
2468
|
+
piiRedactor: piiRedactor,
|
|
2469
|
+
performanceMonitor: performanceMonitor,
|
|
2470
|
+
mutationThrottle: config.instrumentationLevel <= 2 ? 20 : 50 // Throttle for lower levels
|
|
2471
|
+
});
|
|
2472
|
+
domTracker.startTracking();
|
|
2473
|
+
console.log('[ChromeDebug MCP] DOM tracking started');
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// Configure and start network tracking
|
|
2477
|
+
if (networkTracker && config.captureTypes.networkRequests) {
|
|
2478
|
+
networkTracker.init({
|
|
2479
|
+
recordingId: recordingId,
|
|
2480
|
+
onDataReady: onDataReady,
|
|
2481
|
+
piiRedactor: piiRedactor,
|
|
2482
|
+
performanceMonitor: performanceMonitor,
|
|
2483
|
+
captureRequestBody: config.instrumentationLevel >= 3,
|
|
2484
|
+
captureResponseBody: config.instrumentationLevel >= 4,
|
|
2485
|
+
maxBodySize: config.instrumentationLevel >= 4 ? 50 * 1024 : 10 * 1024
|
|
2486
|
+
});
|
|
2487
|
+
networkTracker.startTracking();
|
|
2488
|
+
console.log('[ChromeDebug MCP] Network tracking started');
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// Configure and start function tracking
|
|
2492
|
+
if (functionTracker && config.captureTypes.functionCalls) {
|
|
2493
|
+
// Create a wrapper that sends data to background script for upload
|
|
2494
|
+
const functionTrackerDataReady = (data) => {
|
|
2495
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
2496
|
+
// Send batch to background script for upload
|
|
2497
|
+
chrome.runtime.sendMessage({
|
|
2498
|
+
type: 'UPLOAD_BATCH',
|
|
2499
|
+
recordingId: recordingId,
|
|
2500
|
+
events: data
|
|
2501
|
+
}, (response) => {
|
|
2502
|
+
if (chrome.runtime.lastError) {
|
|
2503
|
+
console.error('[ChromeDebug MCP] Failed to send function traces to background:', chrome.runtime.lastError);
|
|
2504
|
+
} else {
|
|
2505
|
+
console.log(`[Chrome Debug] Sent ${data.length} function traces to background for upload`);
|
|
2506
|
+
}
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
};
|
|
2510
|
+
|
|
2511
|
+
functionTracker.init({
|
|
2512
|
+
recordingId: recordingId,
|
|
2513
|
+
onDataReady: functionTrackerDataReady,
|
|
2514
|
+
piiRedactor: piiRedactor,
|
|
2515
|
+
performanceMonitor: performanceMonitor,
|
|
2516
|
+
maxCallDepth: config.instrumentationLevel <= 2 ? 5 : 10,
|
|
2517
|
+
trackReturnValues: config.instrumentationLevel >= 3,
|
|
2518
|
+
targetCategories: {
|
|
2519
|
+
userDefined: true,
|
|
2520
|
+
eventHandlers: config.instrumentationLevel >= 2,
|
|
2521
|
+
asyncFunctions: config.instrumentationLevel >= 3,
|
|
2522
|
+
domMethods: config.instrumentationLevel >= 4,
|
|
2523
|
+
nativeAPIs: config.instrumentationLevel >= 5
|
|
2524
|
+
}
|
|
2525
|
+
});
|
|
2526
|
+
functionTracker.startTracking();
|
|
2527
|
+
|
|
2528
|
+
// Update ChromePilotTracker status
|
|
2529
|
+
if (window.ChromePilotTracker) {
|
|
2530
|
+
window.ChromePilotTracker._setRecordingStatus(true);
|
|
2531
|
+
window.ChromePilotTracker._functionTracker = functionTracker;
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
console.log('[ChromeDebug MCP] Function tracking started');
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// Start upload manager via background script
|
|
2538
|
+
chrome.runtime.sendMessage({
|
|
2539
|
+
type: 'START_UPLOAD_MANAGER'
|
|
2540
|
+
}, (response) => {
|
|
2541
|
+
if (chrome.runtime.lastError) {
|
|
2542
|
+
console.error('[ChromeDebug MCP] Error starting upload manager:', chrome.runtime.lastError.message);
|
|
2543
|
+
} else {
|
|
2544
|
+
console.log('[ChromeDebug MCP] Upload manager started via background script');
|
|
2545
|
+
}
|
|
2546
|
+
});
|
|
2547
|
+
|
|
2548
|
+
isFullDataRecordingActive = true;
|
|
2549
|
+
|
|
2550
|
+
// Full data recording indicator removed - functionality disabled
|
|
2551
|
+
|
|
2552
|
+
console.log('[ChromeDebug MCP] Full data recording started successfully:', recordingId);
|
|
2553
|
+
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
console.error('[ChromeDebug MCP] Failed to start full data recording:', error);
|
|
2556
|
+
throw error;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// Stop full data recording (DISABLED)
|
|
2561
|
+
async function stopFullDataRecording() {
|
|
2562
|
+
try {
|
|
2563
|
+
if (!isFullDataRecordingActive) {
|
|
2564
|
+
console.warn('[ChromeDebug MCP] Full data recording not active');
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
console.log('[ChromeDebug MCP] Stopping full data recording...');
|
|
2569
|
+
|
|
2570
|
+
// Stop all trackers
|
|
2571
|
+
if (domTracker) {
|
|
2572
|
+
domTracker.stopTracking();
|
|
2573
|
+
console.log('[ChromeDebug MCP] DOM tracking stopped');
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
if (networkTracker) {
|
|
2577
|
+
networkTracker.stopTracking();
|
|
2578
|
+
console.log('[ChromeDebug MCP] Network tracking stopped');
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
if (functionTracker) {
|
|
2582
|
+
functionTracker.stopTracking();
|
|
2583
|
+
|
|
2584
|
+
// Update ChromePilotTracker status
|
|
2585
|
+
if (window.ChromePilotTracker) {
|
|
2586
|
+
window.ChromePilotTracker._setRecordingStatus(false);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
console.log('[ChromeDebug MCP] Function tracking stopped');
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
// Stop performance monitoring
|
|
2593
|
+
if (performanceMonitor) {
|
|
2594
|
+
performanceMonitor.stopMonitoring();
|
|
2595
|
+
console.log('[ChromeDebug MCP] Performance monitoring stopped');
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
// Create final batch for any remaining events
|
|
2599
|
+
if (dataBuffer && chrome.storage && chrome.storage.local) {
|
|
2600
|
+
try {
|
|
2601
|
+
// Get the current recording ID
|
|
2602
|
+
const result = await chrome.storage.local.get(['currentRecordingId']);
|
|
2603
|
+
if (result.currentRecordingId) {
|
|
2604
|
+
// Create a batch with any remaining events
|
|
2605
|
+
await dataBuffer.createBatch(result.currentRecordingId);
|
|
2606
|
+
console.log('[ChromeDebug MCP] Final batch created for remaining events');
|
|
2607
|
+
}
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
console.warn('[ChromeDebug MCP] Could not create final batch:', error);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// Finalize uploads via background script
|
|
2614
|
+
await new Promise((resolve) => {
|
|
2615
|
+
chrome.runtime.sendMessage({
|
|
2616
|
+
type: 'FINALIZE_UPLOADS',
|
|
2617
|
+
recordingId: currentFullRecordingId
|
|
2618
|
+
}, (response) => {
|
|
2619
|
+
if (chrome.runtime.lastError) {
|
|
2620
|
+
console.error('[ChromeDebug MCP] Error finalizing uploads:', chrome.runtime.lastError.message);
|
|
2621
|
+
} else {
|
|
2622
|
+
console.log('[ChromeDebug MCP] Upload manager stopped via background script');
|
|
2623
|
+
}
|
|
2624
|
+
resolve();
|
|
2625
|
+
});
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
isFullDataRecordingActive = false;
|
|
2629
|
+
currentFullRecordingId = null; // Clear the recording ID
|
|
2630
|
+
|
|
2631
|
+
// Full data recording indicator removed - functionality disabled
|
|
2632
|
+
|
|
2633
|
+
console.log('[ChromeDebug MCP] Full data recording stopped successfully');
|
|
2634
|
+
|
|
2635
|
+
} catch (error) {
|
|
2636
|
+
console.error('[ChromeDebug MCP] Failed to stop full data recording:', error);
|
|
2637
|
+
throw error;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
|
|
2642
|
+
// Full data recording indicator functions removed - functionality disabled
|
|
2643
|
+
*/
|
|
2644
|
+
|
|
2645
|
+
// Screen Capture Visual Feedback System
|
|
2646
|
+
class ScreenCaptureVisualFeedback {
|
|
2647
|
+
constructor() {
|
|
2648
|
+
this.actionCount = 0;
|
|
2649
|
+
this.isRecording = false;
|
|
2650
|
+
this.actionCounterElement = null;
|
|
2651
|
+
this.recordingIndicatorElement = null;
|
|
2652
|
+
this.activeHighlights = new Set();
|
|
2653
|
+
this.countdownInterval = null;
|
|
2654
|
+
|
|
2655
|
+
// Bind methods to preserve context
|
|
2656
|
+
this.handleStartScreenCapture = this.handleStartScreenCapture.bind(this);
|
|
2657
|
+
this.handleScreenCaptureInteraction = this.handleScreenCaptureInteraction.bind(this);
|
|
2658
|
+
this.handleFrameCaptured = this.handleFrameCaptured.bind(this);
|
|
2659
|
+
this.handleStopScreenCapture = this.handleStopScreenCapture.bind(this);
|
|
2660
|
+
|
|
2661
|
+
this.init();
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
init() {
|
|
2665
|
+
// Listen for screen capture events
|
|
2666
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
2667
|
+
switch (message.type) {
|
|
2668
|
+
case 'start-screen-capture-tracking':
|
|
2669
|
+
this.handleStartScreenCapture(message);
|
|
2670
|
+
break;
|
|
2671
|
+
case 'screen-capture-interaction':
|
|
2672
|
+
this.handleScreenCaptureInteraction(message);
|
|
2673
|
+
break;
|
|
2674
|
+
case 'screen-capture-frame-captured':
|
|
2675
|
+
this.handleFrameCaptured(message);
|
|
2676
|
+
break;
|
|
2677
|
+
case 'stop-screen-capture-tracking':
|
|
2678
|
+
this.handleStopScreenCapture(message);
|
|
2679
|
+
break;
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
handleStartScreenCapture(message) {
|
|
2685
|
+
this.isRecording = true;
|
|
2686
|
+
this.actionCount = 0;
|
|
2687
|
+
this.showRecordingIndicator();
|
|
2688
|
+
// Action counter is now part of the unified indicator, initialized to 0 in HTML
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
handleScreenCaptureInteraction(interaction) {
|
|
2692
|
+
if (!this.isRecording) return;
|
|
2693
|
+
|
|
2694
|
+
this.actionCount++;
|
|
2695
|
+
this.showActionCounter(this.actionCount);
|
|
2696
|
+
|
|
2697
|
+
// Show click highlight for click interactions
|
|
2698
|
+
if (interaction.type === 'click' && interaction.x !== undefined && interaction.y !== undefined) {
|
|
2699
|
+
this.showClickHighlight(interaction.x, interaction.y);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// Send interaction data to background
|
|
2703
|
+
chrome.runtime.sendMessage({
|
|
2704
|
+
type: 'screen-capture-interaction-logged',
|
|
2705
|
+
interaction: {
|
|
2706
|
+
type: interaction.type,
|
|
2707
|
+
x: interaction.x,
|
|
2708
|
+
y: interaction.y,
|
|
2709
|
+
timestamp: Date.now(),
|
|
2710
|
+
target: interaction.target,
|
|
2711
|
+
value: interaction.value
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
handleFrameCaptured(message) {
|
|
2717
|
+
// Frame captured - could update recording indicator if needed
|
|
2718
|
+
if (this.recordingIndicatorElement) {
|
|
2719
|
+
// Add a subtle pulse effect to indicate frame capture
|
|
2720
|
+
this.recordingIndicatorElement.style.animation = 'screen-capture-pulse 0.3s ease-in-out';
|
|
2721
|
+
setTimeout(() => {
|
|
2722
|
+
if (this.recordingIndicatorElement) {
|
|
2723
|
+
this.recordingIndicatorElement.style.animation = '';
|
|
2724
|
+
}
|
|
2725
|
+
}, 300);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
handleStopScreenCapture(message) {
|
|
2730
|
+
this.isRecording = false;
|
|
2731
|
+
this.hideRecordingIndicator();
|
|
2732
|
+
this.hideActionCounter();
|
|
2733
|
+
this.cleanup();
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
showClickHighlight(x, y) {
|
|
2737
|
+
const highlight = document.createElement('div');
|
|
2738
|
+
highlight.className = 'screen-capture-click-highlight';
|
|
2739
|
+
highlight.style.left = `${x}px`;
|
|
2740
|
+
highlight.style.top = `${y}px`;
|
|
2741
|
+
|
|
2742
|
+
document.body.appendChild(highlight);
|
|
2743
|
+
this.activeHighlights.add(highlight);
|
|
2744
|
+
|
|
2745
|
+
// Remove highlight after animation
|
|
2746
|
+
setTimeout(() => {
|
|
2747
|
+
if (highlight && highlight.parentNode) {
|
|
2748
|
+
highlight.parentNode.removeChild(highlight);
|
|
2749
|
+
}
|
|
2750
|
+
this.activeHighlights.delete(highlight);
|
|
2751
|
+
}, 500);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
showActionCounter(count) {
|
|
2755
|
+
// Update the inline action count in the unified indicator
|
|
2756
|
+
if (this.recordingIndicatorElement) {
|
|
2757
|
+
const actionBadge = this.recordingIndicatorElement.querySelector('.action-count-badge');
|
|
2758
|
+
if (actionBadge) {
|
|
2759
|
+
actionBadge.textContent = `${count} actions`;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
hideActionCounter() {
|
|
2765
|
+
// Action counter is now part of unified indicator, handled by hideRecordingIndicator
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
showRecordingIndicator() {
|
|
2769
|
+
if (!this.recordingIndicatorElement) {
|
|
2770
|
+
// Add required CSS styles if not already added
|
|
2771
|
+
if (!document.getElementById('screen-capture-styles')) {
|
|
2772
|
+
const style = document.createElement('style');
|
|
2773
|
+
style.id = 'screen-capture-styles';
|
|
2774
|
+
style.textContent = `
|
|
2775
|
+
@keyframes pulse {
|
|
2776
|
+
0% { opacity: 1; }
|
|
2777
|
+
50% { opacity: 0.5; }
|
|
2778
|
+
100% { opacity: 1; }
|
|
2779
|
+
}
|
|
2780
|
+
@keyframes screen-capture-pulse {
|
|
2781
|
+
0% { opacity: 1; transform: scale(1); }
|
|
2782
|
+
50% { opacity: 0.7; transform: scale(1.1); }
|
|
2783
|
+
100% { opacity: 1; transform: scale(1); }
|
|
2784
|
+
}
|
|
2785
|
+
.screen-capture-click-highlight {
|
|
2786
|
+
position: fixed;
|
|
2787
|
+
width: 40px;
|
|
2788
|
+
height: 40px;
|
|
2789
|
+
border: 3px solid #4CAF50;
|
|
2790
|
+
border-radius: 50%;
|
|
2791
|
+
background: rgba(76, 175, 80, 0.3);
|
|
2792
|
+
transform: translate(-50%, -50%);
|
|
2793
|
+
pointer-events: none;
|
|
2794
|
+
z-index: 2147483646;
|
|
2795
|
+
animation: screen-capture-ripple 0.5s ease-out;
|
|
2796
|
+
}
|
|
2797
|
+
@keyframes screen-capture-ripple {
|
|
2798
|
+
0% {
|
|
2799
|
+
transform: translate(-50%, -50%) scale(0);
|
|
2800
|
+
opacity: 1;
|
|
2801
|
+
}
|
|
2802
|
+
100% {
|
|
2803
|
+
transform: translate(-50%, -50%) scale(1);
|
|
2804
|
+
opacity: 0;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
`;
|
|
2808
|
+
document.head.appendChild(style);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
this.recordingIndicatorElement = document.createElement('div');
|
|
2812
|
+
this.recordingIndicatorElement.className = 'screen-capture-recording-indicator-unified';
|
|
2813
|
+
|
|
2814
|
+
// Use inline styles to match workflow recording exactly
|
|
2815
|
+
Object.assign(this.recordingIndicatorElement.style, {
|
|
2816
|
+
position: 'fixed',
|
|
2817
|
+
bottom: '80px', // Keep different from workflow to avoid collision
|
|
2818
|
+
left: '20px',
|
|
2819
|
+
background: 'rgba(0, 0, 0, 0.9)',
|
|
2820
|
+
color: 'white',
|
|
2821
|
+
padding: '12px 20px',
|
|
2822
|
+
borderRadius: '25px',
|
|
2823
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif',
|
|
2824
|
+
fontSize: '14px',
|
|
2825
|
+
fontWeight: '500',
|
|
2826
|
+
zIndex: '2147483647',
|
|
2827
|
+
display: 'flex',
|
|
2828
|
+
alignItems: 'center',
|
|
2829
|
+
gap: '8px',
|
|
2830
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
2831
|
+
backdropFilter: 'blur(10px)',
|
|
2832
|
+
WebkitBackdropFilter: 'blur(10px)',
|
|
2833
|
+
pointerEvents: 'none',
|
|
2834
|
+
userSelect: 'none'
|
|
2835
|
+
});
|
|
2836
|
+
|
|
2837
|
+
// Start with countdown
|
|
2838
|
+
this.startCountdownSequence();
|
|
2839
|
+
document.body.appendChild(this.recordingIndicatorElement);
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
this.recordingIndicatorElement.style.display = 'flex';
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
startCountdownSequence() {
|
|
2846
|
+
let count = 3;
|
|
2847
|
+
|
|
2848
|
+
// Initial countdown display
|
|
2849
|
+
this.recordingIndicatorElement.innerHTML = `
|
|
2850
|
+
<span style="color: #FF9800; font-size: 20px; animation: pulse 1.5s infinite;">●</span>
|
|
2851
|
+
<span>Starting in ${count}...</span>
|
|
2852
|
+
<span class="action-count-badge" style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-size: 12px;">0 actions</span>
|
|
2853
|
+
`;
|
|
2854
|
+
|
|
2855
|
+
this.countdownInterval = setInterval(() => {
|
|
2856
|
+
count--;
|
|
2857
|
+
if (count > 0) {
|
|
2858
|
+
// Update countdown
|
|
2859
|
+
this.recordingIndicatorElement.innerHTML = `
|
|
2860
|
+
<span style="color: #FF9800; font-size: 20px; animation: pulse 1.5s infinite;">●</span>
|
|
2861
|
+
<span>Starting in ${count}...</span>
|
|
2862
|
+
<span class="action-count-badge" style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-size: 12px;">0 actions</span>
|
|
2863
|
+
`;
|
|
2864
|
+
} else {
|
|
2865
|
+
// Switch to recording display
|
|
2866
|
+
this.recordingIndicatorElement.innerHTML = `
|
|
2867
|
+
<span style="color: #4CAF50; font-size: 20px; animation: pulse 1.5s infinite;">●</span>
|
|
2868
|
+
<span>Recording Screen Capture...</span>
|
|
2869
|
+
<span class="action-count-badge" style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 12px; font-size: 12px;">0 actions</span>
|
|
2870
|
+
`;
|
|
2871
|
+
clearInterval(this.countdownInterval);
|
|
2872
|
+
this.countdownInterval = null;
|
|
2873
|
+
}
|
|
2874
|
+
}, 1000);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
hideRecordingIndicator() {
|
|
2878
|
+
// Clear countdown interval if running
|
|
2879
|
+
if (this.countdownInterval) {
|
|
2880
|
+
clearInterval(this.countdownInterval);
|
|
2881
|
+
this.countdownInterval = null;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
if (this.recordingIndicatorElement) {
|
|
2885
|
+
// CRITICAL FIX: Remove from DOM instead of just hiding
|
|
2886
|
+
if (this.recordingIndicatorElement.parentNode) {
|
|
2887
|
+
this.recordingIndicatorElement.parentNode.removeChild(this.recordingIndicatorElement);
|
|
2888
|
+
}
|
|
2889
|
+
this.recordingIndicatorElement = null;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
cleanup() {
|
|
2894
|
+
// Clear countdown interval if running
|
|
2895
|
+
if (this.countdownInterval) {
|
|
2896
|
+
clearInterval(this.countdownInterval);
|
|
2897
|
+
this.countdownInterval = null;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
// Remove all active highlights
|
|
2901
|
+
this.activeHighlights.forEach(highlight => {
|
|
2902
|
+
if (highlight && highlight.parentNode) {
|
|
2903
|
+
highlight.parentNode.removeChild(highlight);
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
this.activeHighlights.clear();
|
|
2907
|
+
|
|
2908
|
+
// Hide indicators
|
|
2909
|
+
this.hideRecordingIndicator();
|
|
2910
|
+
|
|
2911
|
+
// Reset action count
|
|
2912
|
+
this.actionCount = 0;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
// Initialize Screen Capture Visual Feedback System
|
|
2917
|
+
if (window === window.top) {
|
|
2918
|
+
window.screenCaptureVisualFeedback = new ScreenCaptureVisualFeedback();
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
// Initialize basic workflow recording components (lightweight)
|
|
2922
|
+
setTimeout(() => {
|
|
2923
|
+
// Only initialize if we're not in an iframe and basic functions are needed
|
|
2924
|
+
if (window === window.top) {
|
|
2925
|
+
// Initialize essential globals for workflow recording
|
|
2926
|
+
if (typeof window.chromePilotWorkflowData === 'undefined') {
|
|
2927
|
+
window.chromePilotWorkflowData = {
|
|
2928
|
+
isRecording: false,
|
|
2929
|
+
sessionId: null,
|
|
2930
|
+
actions: [],
|
|
2931
|
+
startTime: null
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// Initialize FunctionTracker for workflow recording if not already available
|
|
2936
|
+
if (!window.ChromePilotTracker && typeof FunctionTracker !== 'undefined') {
|
|
2937
|
+
// Create a lightweight FunctionTracker for workflow recording
|
|
2938
|
+
const workflowFunctionTracker = new FunctionTracker();
|
|
2939
|
+
|
|
2940
|
+
// Expose ChromePilotTracker API for manual instrumentation
|
|
2941
|
+
window.ChromePilotTracker = {
|
|
2942
|
+
isRecording: false,
|
|
2943
|
+
_functionTracker: workflowFunctionTracker,
|
|
2944
|
+
|
|
2945
|
+
// Register a single function for tracing
|
|
2946
|
+
register: function(name, fn) {
|
|
2947
|
+
if (!this.isRecording || typeof fn !== 'function') {
|
|
2948
|
+
return fn; // Return original function if not recording
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
if (!this._functionTracker) {
|
|
2952
|
+
console.warn('[ChromePilotTracker] Function tracker not available');
|
|
2953
|
+
return fn;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// Use the existing wrapFunction from function-tracker
|
|
2957
|
+
return this._functionTracker.wrapFunction(fn, name, {
|
|
2958
|
+
isUserDefined: true,
|
|
2959
|
+
registeredManually: true
|
|
2960
|
+
});
|
|
2961
|
+
},
|
|
2962
|
+
|
|
2963
|
+
// Wrap an entire object/module
|
|
2964
|
+
wrap: function(obj, options = {}) {
|
|
2965
|
+
if (!this.isRecording || !obj || typeof obj !== 'object') {
|
|
2966
|
+
return obj; // Return original if not recording or invalid
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
if (!this._functionTracker) {
|
|
2970
|
+
console.warn('[ChromePilotTracker] Function tracker not available');
|
|
2971
|
+
return obj;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
const self = this;
|
|
2975
|
+
const prefix = options.prefix || obj.constructor?.name || 'Object';
|
|
2976
|
+
|
|
2977
|
+
// Use Proxy to intercept method calls
|
|
2978
|
+
return new Proxy(obj, {
|
|
2979
|
+
get(target, prop, receiver) {
|
|
2980
|
+
const original = Reflect.get(target, prop, receiver);
|
|
2981
|
+
|
|
2982
|
+
// Only wrap functions
|
|
2983
|
+
if (typeof original === 'function') {
|
|
2984
|
+
const functionName = `${prefix}.${String(prop)}`;
|
|
2985
|
+
|
|
2986
|
+
// Check if this method should be ignored
|
|
2987
|
+
if (options.ignore && options.ignore.includes(String(prop))) {
|
|
2988
|
+
return original;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// Wrap the function
|
|
2992
|
+
return self._functionTracker.wrapFunction(original, functionName, {
|
|
2993
|
+
isUserDefined: true,
|
|
2994
|
+
registeredManually: true,
|
|
2995
|
+
parentObject: prefix
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
return original;
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
},
|
|
3003
|
+
|
|
3004
|
+
// Update recording status
|
|
3005
|
+
_setRecordingStatus: function(status) {
|
|
3006
|
+
this.isRecording = status;
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
|
|
3010
|
+
console.log('[ChromeDebug MCP] ChromePilotTracker initialized for workflow recording');
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
// Initialize minimal workflow recording support
|
|
3014
|
+
console.log('[ChromeDebug MCP] Basic workflow recording ready (lightweight mode)');
|
|
3015
|
+
}
|
|
3016
|
+
}, 500);
|
|
3017
|
+
|
|
3018
|
+
// Full data recording system will be initialized on-demand when recording begins
|
|
3019
|
+
// This prevents unnecessary resource usage on every page load
|
|
3020
|
+
|
|
3021
|
+
} // End of initializeChromePilot function
|
|
3022
|
+
})(); // End of IIFE wrapper
|