@dynamicu/chromedebug-mcp 2.6.4 → 2.6.7
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 +25 -0
- package/chrome-extension/activation-manager.js +18 -4
- package/chrome-extension/background.js +439 -53
- package/chrome-extension/browser-recording-manager.js +256 -0
- package/chrome-extension/content.js +167 -97
- package/chrome-extension/data-buffer.js +206 -17
- package/chrome-extension/frame-capture.js +52 -15
- package/chrome-extension/manifest.free.json +3 -4
- package/chrome-extension/popup.html +109 -5
- package/chrome-extension/popup.js +597 -159
- package/config/chromedebug-config.json +101 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +1 -1
- package/scripts/package-pro-extension.js +1 -1
- package/scripts/webpack.config.free.cjs +4 -1
- package/scripts/webpack.config.pro.cjs +4 -1
- package/src/chrome-controller.js +7 -7
- package/src/cli.js +6 -0
- package/src/database.js +6 -2
- package/src/http-server.js +3 -2
- package/src/index.js +6 -1
- package/src/services/unified-session-manager.js +22 -2
- package/src/validation/schemas.js +36 -6
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserRecordingManager - Manage recordings stored in IndexedDB
|
|
3
|
+
*
|
|
4
|
+
* Handles recording lifecycle for browser-only mode:
|
|
5
|
+
* - Stores frames and metadata in IndexedDB
|
|
6
|
+
* - Exports recordings as ZIP files
|
|
7
|
+
* - Manages cleanup and deletion
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class BrowserRecordingManager {
|
|
11
|
+
constructor(db) {
|
|
12
|
+
this.db = db; // DataBuffer instance
|
|
13
|
+
this.activeRecordings = new Map(); // sessionId -> recording metadata
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Start a new browser recording
|
|
18
|
+
* @param {string} sessionId - Unique session identifier
|
|
19
|
+
* @param {Object} metadata - Recording metadata (tabId, url, timestamp, etc.)
|
|
20
|
+
* @returns {Promise<{success: boolean, sessionId: string}>}
|
|
21
|
+
*/
|
|
22
|
+
async startRecording(sessionId, metadata = {}) {
|
|
23
|
+
try {
|
|
24
|
+
// Check if recording already exists (cleanup from previous incomplete session)
|
|
25
|
+
const existingRecording = await this.db.getBrowserRecording(sessionId);
|
|
26
|
+
if (existingRecording) {
|
|
27
|
+
console.warn(`[BrowserRecordingManager] Cleaning up existing recording: ${sessionId}`);
|
|
28
|
+
await this.deleteRecording(sessionId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const recording = {
|
|
32
|
+
sessionId,
|
|
33
|
+
startTime: Date.now(),
|
|
34
|
+
tabId: metadata.tabId,
|
|
35
|
+
url: metadata.url,
|
|
36
|
+
title: metadata.title || 'Browser Recording',
|
|
37
|
+
frameCount: 0,
|
|
38
|
+
status: 'recording',
|
|
39
|
+
metadata
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Store recording metadata in IndexedDB
|
|
43
|
+
await this.db.addBrowserRecording(recording);
|
|
44
|
+
|
|
45
|
+
// Track active recording
|
|
46
|
+
this.activeRecordings.set(sessionId, recording);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
sessionId,
|
|
51
|
+
startTime: recording.startTime
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error('Failed to start browser recording:', {
|
|
55
|
+
name: error.name,
|
|
56
|
+
message: error.message,
|
|
57
|
+
stack: error.stack,
|
|
58
|
+
error: error
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
sessionId,
|
|
63
|
+
error: `${error.name}: ${error.message}`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add a frame to an active recording
|
|
70
|
+
* @param {string} sessionId
|
|
71
|
+
* @param {Object} frameData - {frameIndex, screenshot, logs, timestamp}
|
|
72
|
+
* @returns {Promise<boolean>}
|
|
73
|
+
*/
|
|
74
|
+
async addFrame(sessionId, frameData) {
|
|
75
|
+
try {
|
|
76
|
+
const frame = {
|
|
77
|
+
sessionId,
|
|
78
|
+
frameIndex: frameData.frameIndex,
|
|
79
|
+
screenshot: frameData.screenshot, // Base64 data URL
|
|
80
|
+
logs: frameData.logs || [],
|
|
81
|
+
timestamp: frameData.timestamp || Date.now(),
|
|
82
|
+
absoluteTimestamp: frameData.absoluteTimestamp // Preserve absolute timestamp for log association
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await this.db.addBrowserFrame(frame);
|
|
86
|
+
|
|
87
|
+
// Update frame count
|
|
88
|
+
const recording = this.activeRecordings.get(sessionId);
|
|
89
|
+
if (recording) {
|
|
90
|
+
recording.frameCount++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Failed to add frame:', error);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stop recording and finalize
|
|
102
|
+
* @param {string} sessionId
|
|
103
|
+
* @returns {Promise<{success: boolean, frameCount: number, duration: number}>}
|
|
104
|
+
*/
|
|
105
|
+
async stopRecording(sessionId) {
|
|
106
|
+
try {
|
|
107
|
+
const recording = this.activeRecordings.get(sessionId);
|
|
108
|
+
if (!recording) {
|
|
109
|
+
throw new Error(`Recording ${sessionId} not found`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const endTime = Date.now();
|
|
113
|
+
const duration = endTime - recording.startTime;
|
|
114
|
+
|
|
115
|
+
// Update recording status
|
|
116
|
+
recording.status = 'completed';
|
|
117
|
+
recording.endTime = endTime;
|
|
118
|
+
recording.duration = duration;
|
|
119
|
+
|
|
120
|
+
await this.db.updateBrowserRecording(sessionId, {
|
|
121
|
+
status: 'completed',
|
|
122
|
+
endTime,
|
|
123
|
+
duration,
|
|
124
|
+
frameCount: recording.frameCount
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.activeRecordings.delete(sessionId);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
success: true,
|
|
131
|
+
sessionId,
|
|
132
|
+
frameCount: recording.frameCount,
|
|
133
|
+
duration
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error('Failed to stop recording:', error);
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
sessionId,
|
|
140
|
+
error: error.message
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Export recording as downloadable ZIP blob
|
|
147
|
+
* @param {string} sessionId
|
|
148
|
+
* @returns {Promise<{blob: Blob, filename: string}>}
|
|
149
|
+
*/
|
|
150
|
+
async exportRecording(sessionId) {
|
|
151
|
+
try {
|
|
152
|
+
// Get recording metadata
|
|
153
|
+
const recording = await this.db.getBrowserRecording(sessionId);
|
|
154
|
+
if (!recording) {
|
|
155
|
+
throw new Error(`Recording ${sessionId} not found`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Get all frames for this session
|
|
159
|
+
const frames = await this.db.getBrowserFrames(sessionId);
|
|
160
|
+
|
|
161
|
+
// Create JSON manifest
|
|
162
|
+
const manifest = {
|
|
163
|
+
sessionId,
|
|
164
|
+
title: recording.title,
|
|
165
|
+
url: recording.url,
|
|
166
|
+
startTime: recording.startTime,
|
|
167
|
+
endTime: recording.endTime,
|
|
168
|
+
duration: recording.duration,
|
|
169
|
+
frameCount: frames.length,
|
|
170
|
+
exportedAt: Date.now()
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Build export data structure
|
|
174
|
+
const exportData = {
|
|
175
|
+
manifest,
|
|
176
|
+
recording,
|
|
177
|
+
frames: frames.map(frame => ({
|
|
178
|
+
frameIndex: frame.frameIndex,
|
|
179
|
+
timestamp: frame.timestamp,
|
|
180
|
+
screenshot: frame.screenshot,
|
|
181
|
+
logs: frame.logs
|
|
182
|
+
}))
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Convert to JSON string
|
|
186
|
+
const jsonString = JSON.stringify(exportData, null, 2);
|
|
187
|
+
|
|
188
|
+
// Create blob
|
|
189
|
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
190
|
+
|
|
191
|
+
// Generate filename
|
|
192
|
+
const date = new Date(recording.startTime).toISOString().split('T')[0];
|
|
193
|
+
const filename = `chrome-pilot-recording-${date}-${sessionId.slice(-8)}.json`;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
blob,
|
|
197
|
+
filename,
|
|
198
|
+
size: blob.size,
|
|
199
|
+
frameCount: frames.length
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('Failed to export recording:', error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Delete a recording and all its frames
|
|
209
|
+
* @param {string} sessionId
|
|
210
|
+
* @returns {Promise<{success: boolean, deletedFrames: number}>}
|
|
211
|
+
*/
|
|
212
|
+
async deleteRecording(sessionId) {
|
|
213
|
+
try {
|
|
214
|
+
// Delete all frames
|
|
215
|
+
const deletedFrames = await this.db.deleteBrowserFrames(sessionId);
|
|
216
|
+
|
|
217
|
+
// Delete recording metadata
|
|
218
|
+
await this.db.deleteBrowserRecording(sessionId);
|
|
219
|
+
|
|
220
|
+
// Remove from active recordings if present
|
|
221
|
+
this.activeRecordings.delete(sessionId);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
sessionId,
|
|
226
|
+
deletedFrames
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('Failed to delete recording:', error);
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
sessionId,
|
|
233
|
+
error: error.message
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* List all recordings
|
|
240
|
+
* @returns {Promise<Array>}
|
|
241
|
+
*/
|
|
242
|
+
async listRecordings() {
|
|
243
|
+
try {
|
|
244
|
+
return await this.db.getAllBrowserRecordings();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error('Failed to list recordings:', error);
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Export for use in other extension files
|
|
254
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
255
|
+
module.exports = BrowserRecordingManager;
|
|
256
|
+
}
|
|
@@ -398,25 +398,25 @@ function getComponentInfo(element) {
|
|
|
398
398
|
// Idempotent protection for cleanup function
|
|
399
399
|
let cleanupExecuted = false;
|
|
400
400
|
|
|
401
|
-
function cleanup() {
|
|
401
|
+
async function cleanup() {
|
|
402
402
|
// Idempotent protection - only run cleanup once
|
|
403
403
|
if (cleanupExecuted) {
|
|
404
404
|
console.log('[ChromeDebug MCP] Cleanup already executed, skipping');
|
|
405
405
|
return;
|
|
406
406
|
}
|
|
407
|
-
|
|
407
|
+
|
|
408
408
|
cleanupExecuted = true;
|
|
409
409
|
console.log('[ChromeDebug MCP] Executing cleanup...');
|
|
410
|
-
|
|
410
|
+
|
|
411
411
|
// Clean up recording indicators and event listeners
|
|
412
412
|
try {
|
|
413
413
|
// Legacy recording indicator cleanup removed - now handled by ScreenCaptureVisualFeedback
|
|
414
|
-
|
|
414
|
+
|
|
415
415
|
// Stop workflow recording if active
|
|
416
416
|
if (typeof isWorkflowRecording !== 'undefined' && isWorkflowRecording) {
|
|
417
417
|
console.log('[ChromeDebug MCP] Stopping workflow recording during cleanup');
|
|
418
418
|
if (typeof stopWorkflowRecording === 'function') {
|
|
419
|
-
stopWorkflowRecording();
|
|
419
|
+
await stopWorkflowRecording();
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
|
|
@@ -498,8 +498,13 @@ if (isExtensionValid()) {
|
|
|
498
498
|
sendResponse({ success: true });
|
|
499
499
|
} else if (request.action === 'stopWorkflowRecording') {
|
|
500
500
|
console.log('[ChromeDebug MCP] Stopping workflow recording');
|
|
501
|
-
|
|
502
|
-
|
|
501
|
+
stopWorkflowRecording().then(workflow => {
|
|
502
|
+
sendResponse({ success: true, workflow: workflow });
|
|
503
|
+
}).catch(error => {
|
|
504
|
+
console.error('[ChromeDebug MCP] Error stopping workflow recording:', error);
|
|
505
|
+
sendResponse({ success: false, error: error.message });
|
|
506
|
+
});
|
|
507
|
+
return true; // Keep channel open for async response
|
|
503
508
|
} else if (request.action === 'createRestorePoint') {
|
|
504
509
|
// Create restore point for current workflow
|
|
505
510
|
createRestorePoint(request.actionIndex || workflowActions.length).then(restorePointId => {
|
|
@@ -852,27 +857,72 @@ let isWorkflowRecording = false;
|
|
|
852
857
|
let workflowActions = [];
|
|
853
858
|
let workflowRecordingIndicator = null;
|
|
854
859
|
let workflowScreenshotSettings = null;
|
|
860
|
+
let lastScreenshotTime = 0;
|
|
861
|
+
let pendingScreenshots = []; // Track pending screenshot captures
|
|
862
|
+
|
|
863
|
+
// Async screenshot capture without blocking click recording
|
|
864
|
+
function captureScreenshotAsync(action, actionIndex) {
|
|
865
|
+
// Create the async capture promise
|
|
866
|
+
const capturePromise = (async () => {
|
|
867
|
+
try {
|
|
868
|
+
// Chrome API rate limit: ~1-2 screenshots per second
|
|
869
|
+
// We enforce 600ms minimum between screenshots to stay under the limit
|
|
870
|
+
const MIN_SCREENSHOT_INTERVAL = 600; // milliseconds
|
|
871
|
+
const now = Date.now();
|
|
872
|
+
const timeSinceLastScreenshot = now - lastScreenshotTime;
|
|
873
|
+
|
|
874
|
+
if (timeSinceLastScreenshot < MIN_SCREENSHOT_INTERVAL) {
|
|
875
|
+
// Wait until we can safely capture
|
|
876
|
+
const waitTime = MIN_SCREENSHOT_INTERVAL - timeSinceLastScreenshot;
|
|
877
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const screenshotData = await captureWorkflowScreenshot();
|
|
881
|
+
|
|
882
|
+
if (screenshotData) {
|
|
883
|
+
// Add screenshot to the action object (it's already in the array)
|
|
884
|
+
action.screenshot_data = screenshotData;
|
|
885
|
+
lastScreenshotTime = Date.now(); // Update timestamp after successful capture
|
|
886
|
+
}
|
|
887
|
+
} catch (error) {
|
|
888
|
+
console.error(`Error capturing screenshot for action ${actionIndex}:`, error);
|
|
889
|
+
}
|
|
890
|
+
})();
|
|
891
|
+
|
|
892
|
+
// Track this promise in pendingScreenshots
|
|
893
|
+
pendingScreenshots.push(capturePromise);
|
|
894
|
+
|
|
895
|
+
// Remove from pending when done (success or failure)
|
|
896
|
+
capturePromise.finally(() => {
|
|
897
|
+
const index = pendingScreenshots.indexOf(capturePromise);
|
|
898
|
+
if (index > -1) {
|
|
899
|
+
pendingScreenshots.splice(index, 1);
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
return capturePromise;
|
|
904
|
+
}
|
|
855
905
|
|
|
856
906
|
// Helper function to capture screenshot
|
|
857
907
|
async function captureWorkflowScreenshot() {
|
|
858
908
|
if (!workflowScreenshotSettings || !workflowScreenshotSettings.enabled) {
|
|
859
909
|
return null;
|
|
860
910
|
}
|
|
861
|
-
|
|
911
|
+
|
|
862
912
|
try {
|
|
863
913
|
// Send message to background script to capture screenshot
|
|
864
914
|
const response = await chrome.runtime.sendMessage({
|
|
865
915
|
action: 'captureWorkflowScreenshot',
|
|
866
916
|
settings: workflowScreenshotSettings
|
|
867
917
|
});
|
|
868
|
-
|
|
918
|
+
|
|
869
919
|
if (response && response.success) {
|
|
870
920
|
return response.screenshotData;
|
|
871
921
|
}
|
|
872
922
|
} catch (error) {
|
|
873
|
-
console.error('
|
|
923
|
+
console.error('Error in captureWorkflowScreenshot:', error);
|
|
874
924
|
}
|
|
875
|
-
|
|
925
|
+
|
|
876
926
|
return null;
|
|
877
927
|
}
|
|
878
928
|
|
|
@@ -1024,16 +1074,16 @@ async function shouldEnhanceCapture() {
|
|
|
1024
1074
|
}
|
|
1025
1075
|
|
|
1026
1076
|
// Record click action
|
|
1027
|
-
|
|
1077
|
+
function recordClick(event) {
|
|
1028
1078
|
if (!isWorkflowRecording) {
|
|
1029
1079
|
console.log('[ChromePilot] Click ignored - not recording');
|
|
1030
1080
|
return;
|
|
1031
1081
|
}
|
|
1032
|
-
|
|
1082
|
+
|
|
1033
1083
|
console.log('[ChromePilot] Recording click event');
|
|
1034
|
-
|
|
1084
|
+
|
|
1035
1085
|
let element = event.target;
|
|
1036
|
-
|
|
1086
|
+
|
|
1037
1087
|
// If clicking on an SVG element or its children, find the parent button/link
|
|
1038
1088
|
if (element.tagName === 'svg' || element.tagName === 'path' || element.tagName === 'g' || element.tagName === 'circle' || element.tagName === 'rect') {
|
|
1039
1089
|
const clickableParent = element.closest('button, a, [role="button"], [onclick]');
|
|
@@ -1042,7 +1092,7 @@ async function recordClick(event) {
|
|
|
1042
1092
|
element = clickableParent;
|
|
1043
1093
|
}
|
|
1044
1094
|
}
|
|
1045
|
-
|
|
1095
|
+
|
|
1046
1096
|
const action = {
|
|
1047
1097
|
type: 'click',
|
|
1048
1098
|
selector: getUniqueSelector(element),
|
|
@@ -1057,92 +1107,92 @@ async function recordClick(event) {
|
|
|
1057
1107
|
action.text = element.textContent.trim();
|
|
1058
1108
|
}
|
|
1059
1109
|
|
|
1060
|
-
//
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
try {
|
|
1064
|
-
console.log('[ChromePilot] Capturing enhanced click data...');
|
|
1110
|
+
// Record the action immediately (synchronously) - don't wait for anything
|
|
1111
|
+
workflowActions.push(action);
|
|
1112
|
+
const actionIndex = workflowActions.length - 1;
|
|
1065
1113
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1114
|
+
// Enhanced click tracking data (async, non-blocking)
|
|
1115
|
+
// Check setting and apply enhanced data asynchronously without blocking
|
|
1116
|
+
shouldEnhanceCapture().then(enhanceCapture => {
|
|
1117
|
+
if (enhanceCapture) {
|
|
1118
|
+
try {
|
|
1119
|
+
console.log('[ChromePilot] Capturing enhanced click data asynchronously...');
|
|
1071
1120
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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 };
|
|
1121
|
+
// Capture enhanced element data (only include fields with meaningful values)
|
|
1122
|
+
const capturedHTML = captureElementHTML(element);
|
|
1123
|
+
if (capturedHTML) {
|
|
1124
|
+
action.element_html = capturedHTML;
|
|
1080
1125
|
}
|
|
1081
|
-
}
|
|
1082
1126
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1127
|
+
const componentData = getEnhancedComponentInfo(element);
|
|
1128
|
+
if (componentData) {
|
|
1129
|
+
try {
|
|
1130
|
+
JSON.stringify(componentData); // Test if serializable
|
|
1131
|
+
action.component_data = componentData;
|
|
1132
|
+
} catch (e) {
|
|
1133
|
+
console.error('[ChromePilot] Component data cannot be serialized:', e.message);
|
|
1134
|
+
action.component_data = { error: 'Serialization failed', type: typeof componentData };
|
|
1135
|
+
}
|
|
1091
1136
|
}
|
|
1092
|
-
}
|
|
1093
1137
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1138
|
+
const eventHandlers = extractEventHandlers(element);
|
|
1139
|
+
if (eventHandlers) {
|
|
1140
|
+
try {
|
|
1141
|
+
JSON.stringify(eventHandlers); // Test if serializable
|
|
1142
|
+
action.event_handlers = eventHandlers;
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
console.error('[ChromePilot] Event handlers cannot be serialized:', e.message);
|
|
1145
|
+
action.event_handlers = { error: 'Serialization failed', type: typeof eventHandlers };
|
|
1146
|
+
}
|
|
1102
1147
|
}
|
|
1103
|
-
}
|
|
1104
1148
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1149
|
+
const elementState = captureElementState(element);
|
|
1150
|
+
if (elementState) {
|
|
1151
|
+
try {
|
|
1152
|
+
JSON.stringify(elementState); // Test if serializable
|
|
1153
|
+
action.element_state = elementState;
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
console.error('[ChromePilot] Element state cannot be serialized:', e.message);
|
|
1156
|
+
action.element_state = { error: 'Serialization failed', type: typeof elementState };
|
|
1157
|
+
}
|
|
1113
1158
|
}
|
|
1114
|
-
}
|
|
1115
1159
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1160
|
+
const perfMetrics = getPerformanceMetrics();
|
|
1161
|
+
if (perfMetrics) {
|
|
1162
|
+
try {
|
|
1163
|
+
JSON.stringify(perfMetrics); // Test if serializable
|
|
1164
|
+
action.performance_metrics = perfMetrics;
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
console.error('[ChromePilot] Performance metrics cannot be serialized:', e.message);
|
|
1167
|
+
action.performance_metrics = { error: 'Serialization failed', type: typeof perfMetrics };
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
console.log('[ChromePilot] Enhanced click data captured successfully');
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
console.warn('[ChromePilot] Error capturing enhanced click data:', error);
|
|
1174
|
+
// Continue with basic click recording if enhanced capture fails
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
console.log('[ChromePilot] Enhanced click capture disabled - using basic capture only');
|
|
1120
1178
|
}
|
|
1121
|
-
}
|
|
1122
|
-
console.
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Capture screenshot
|
|
1179
|
+
}).catch(error => {
|
|
1180
|
+
console.error('[ChromePilot] Error checking enhanced capture setting:', error);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// Capture screenshot asynchronously without blocking
|
|
1126
1184
|
if (workflowScreenshotSettings && workflowScreenshotSettings.enabled) {
|
|
1127
|
-
//
|
|
1128
|
-
|
|
1129
|
-
const screenshotData = await captureWorkflowScreenshot();
|
|
1130
|
-
if (screenshotData) {
|
|
1131
|
-
action.screenshot_data = screenshotData;
|
|
1132
|
-
}
|
|
1133
|
-
}, 100);
|
|
1185
|
+
// Don't await - let screenshot capture happen in background
|
|
1186
|
+
captureScreenshotAsync(action, actionIndex);
|
|
1134
1187
|
}
|
|
1135
|
-
|
|
1136
|
-
workflowActions.push(action);
|
|
1137
|
-
console.log('[ChromePilot] Action recorded, total actions:', workflowActions.length);
|
|
1138
1188
|
updateWorkflowActionCount();
|
|
1139
|
-
|
|
1189
|
+
|
|
1140
1190
|
// Visual feedback
|
|
1141
1191
|
flashElement(element);
|
|
1142
1192
|
}
|
|
1143
1193
|
|
|
1144
1194
|
// Record input changes
|
|
1145
|
-
|
|
1195
|
+
function recordInput(event) {
|
|
1146
1196
|
if (!isWorkflowRecording) return;
|
|
1147
1197
|
|
|
1148
1198
|
const element = event.target;
|
|
@@ -1163,20 +1213,13 @@ async function recordInput(event) {
|
|
|
1163
1213
|
if (element.placeholder) {
|
|
1164
1214
|
action.placeholder = element.placeholder;
|
|
1165
1215
|
}
|
|
1166
|
-
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const screenshotData = await captureWorkflowScreenshot();
|
|
1171
|
-
if (screenshotData) {
|
|
1172
|
-
action.screenshot_data = screenshotData;
|
|
1173
|
-
}
|
|
1174
|
-
}, 100);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1216
|
+
|
|
1217
|
+
// NOTE: Screenshots are only captured for clicks, not input events
|
|
1218
|
+
// This prevents multiple screenshot attempts for a single user action
|
|
1219
|
+
|
|
1177
1220
|
workflowActions.push(action);
|
|
1178
1221
|
updateWorkflowActionCount();
|
|
1179
|
-
|
|
1222
|
+
|
|
1180
1223
|
// Visual feedback
|
|
1181
1224
|
flashElement(element);
|
|
1182
1225
|
}
|
|
@@ -1256,6 +1299,8 @@ function startWorkflowRecording(screenshotSettings) {
|
|
|
1256
1299
|
isWorkflowRecording = true;
|
|
1257
1300
|
workflowActions = [];
|
|
1258
1301
|
workflowScreenshotSettings = screenshotSettings;
|
|
1302
|
+
lastScreenshotTime = 0; // Reset rate limit timer
|
|
1303
|
+
pendingScreenshots = []; // Clear any pending screenshots from previous recording
|
|
1259
1304
|
createWorkflowRecordingIndicator();
|
|
1260
1305
|
|
|
1261
1306
|
console.log('[ChromePilot] Workflow recording started with settings:', screenshotSettings);
|
|
@@ -1326,11 +1371,20 @@ function startWorkflowRecording(screenshotSettings) {
|
|
|
1326
1371
|
}
|
|
1327
1372
|
|
|
1328
1373
|
// Stop workflow recording
|
|
1329
|
-
function stopWorkflowRecording() {
|
|
1374
|
+
async function stopWorkflowRecording() {
|
|
1330
1375
|
isWorkflowRecording = false;
|
|
1331
1376
|
removeWorkflowRecordingIndicator();
|
|
1332
|
-
|
|
1377
|
+
|
|
1333
1378
|
console.log(`[ChromePilot] Stopping recording. Captured ${workflowActions.length} actions`);
|
|
1379
|
+
|
|
1380
|
+
// Wait for all pending screenshots to complete
|
|
1381
|
+
if (pendingScreenshots.length > 0) {
|
|
1382
|
+
console.log(`[SCREENSHOT-DEBUG] Waiting for ${pendingScreenshots.length} pending screenshots to complete...`);
|
|
1383
|
+
await Promise.all(pendingScreenshots);
|
|
1384
|
+
console.log('[SCREENSHOT-DEBUG] All pending screenshots completed!');
|
|
1385
|
+
} else {
|
|
1386
|
+
console.log('[SCREENSHOT-DEBUG] No pending screenshots to wait for');
|
|
1387
|
+
}
|
|
1334
1388
|
|
|
1335
1389
|
// Disconnect from FunctionTracker and disable recording
|
|
1336
1390
|
if (window.ChromePilotTracker && window.ChromePilotTracker._functionTracker) {
|
|
@@ -1387,6 +1441,22 @@ function stopWorkflowRecording() {
|
|
|
1387
1441
|
functionTraces = [...functionTraces, ...embeddedTraces];
|
|
1388
1442
|
}
|
|
1389
1443
|
|
|
1444
|
+
// Log screenshot data before returning
|
|
1445
|
+
console.log('[SCREENSHOT-DEBUG] stopWorkflowRecording - Preparing to return workflow');
|
|
1446
|
+
console.log('[SCREENSHOT-DEBUG] stopWorkflowRecording - Total actions:', workflowActions.length);
|
|
1447
|
+
|
|
1448
|
+
const actionsWithScreenshots = workflowActions.filter(a => a.screenshot_data);
|
|
1449
|
+
console.log('[SCREENSHOT-DEBUG] stopWorkflowRecording - Actions WITH screenshot_data:', actionsWithScreenshots.length);
|
|
1450
|
+
|
|
1451
|
+
workflowActions.forEach((action, index) => {
|
|
1452
|
+
console.log(`[SCREENSHOT-DEBUG] stopWorkflowRecording - Action ${index}:`, {
|
|
1453
|
+
type: action.type,
|
|
1454
|
+
hasScreenshotData: !!action.screenshot_data,
|
|
1455
|
+
screenshotDataLength: action.screenshot_data?.length,
|
|
1456
|
+
screenshotPreview: action.screenshot_data?.substring(0, 50)
|
|
1457
|
+
});
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1390
1460
|
// Return the recorded workflow with proper functionTraces field
|
|
1391
1461
|
return {
|
|
1392
1462
|
actions: workflowActions,
|