@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,2446 @@
|
|
|
1
|
+
// Chrome Debug Extension Popup Script v2.0
|
|
2
|
+
const EXTENSION_VERSION = '2.0.4-BUILD-20250119';
|
|
3
|
+
console.log(`[popup.js] Loaded version: ${EXTENSION_VERSION}`);
|
|
4
|
+
|
|
5
|
+
// Import Firebase license client
|
|
6
|
+
import { FirebaseLicenseClient } from './firebase-client.js';
|
|
7
|
+
import { LEMONSQUEEZY_CHECKOUT_URL } from './firebase-config.module.js';
|
|
8
|
+
|
|
9
|
+
// Initialize license client
|
|
10
|
+
const licenseClient = new FirebaseLicenseClient();
|
|
11
|
+
let currentUserId = null;
|
|
12
|
+
|
|
13
|
+
// Global variables for recording functionality
|
|
14
|
+
let isRecording = false;
|
|
15
|
+
let recordingTimer = null;
|
|
16
|
+
let recordingStartTime = null;
|
|
17
|
+
let isStoppingRecording = false;
|
|
18
|
+
|
|
19
|
+
// Countdown timer variables
|
|
20
|
+
let countdownTimer = null;
|
|
21
|
+
let countdownInterval = null;
|
|
22
|
+
let nextSnapshotTime = null;
|
|
23
|
+
let currentFrameRate = 1;
|
|
24
|
+
|
|
25
|
+
// Workflow recording variables
|
|
26
|
+
let isWorkflowRecording = false;
|
|
27
|
+
let workflowRecordingTimer = null;
|
|
28
|
+
let workflowStartTime = null;
|
|
29
|
+
|
|
30
|
+
// Debounce timer for screenshot quality slider
|
|
31
|
+
let screenshotQualityDebounceTimer = null;
|
|
32
|
+
|
|
33
|
+
// Workflow recording functions
|
|
34
|
+
function updateWorkflowRecordingUI() {
|
|
35
|
+
const workflowBtn = document.getElementById('workflowRecordBtn');
|
|
36
|
+
const workflowStatus = document.getElementById('workflowRecordingStatus');
|
|
37
|
+
const saveRestorePointBtn = document.getElementById('saveRestorePointBtn');
|
|
38
|
+
|
|
39
|
+
console.log('[Popup v2.0.6] updateWorkflowRecordingUI called, isWorkflowRecording:', isWorkflowRecording);
|
|
40
|
+
console.log('[Popup v2.0.6] workflowBtn element exists?', !!workflowBtn);
|
|
41
|
+
|
|
42
|
+
if (workflowBtn) {
|
|
43
|
+
if (isWorkflowRecording) {
|
|
44
|
+
console.log('[Popup v2.0.6] Updating button to STOP state');
|
|
45
|
+
console.log('[Popup v2.0.6] Current button text:', workflowBtn.textContent);
|
|
46
|
+
workflowBtn.textContent = 'Stop Workflow Recording';
|
|
47
|
+
workflowBtn.style.background = '#e91e63';
|
|
48
|
+
workflowBtn.classList.add('recording');
|
|
49
|
+
console.log('[Popup v2.0.6] Button updated - new text:', workflowBtn.textContent);
|
|
50
|
+
console.log('[Popup v2.0.6] Button classes:', workflowBtn.className);
|
|
51
|
+
startWorkflowTimer();
|
|
52
|
+
// Show restore point button during recording
|
|
53
|
+
if (saveRestorePointBtn) {
|
|
54
|
+
saveRestorePointBtn.style.display = 'block';
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
workflowBtn.textContent = 'Start Workflow Recording';
|
|
58
|
+
workflowBtn.style.background = '#9c27b0';
|
|
59
|
+
workflowBtn.classList.remove('recording');
|
|
60
|
+
stopWorkflowTimer();
|
|
61
|
+
if (workflowStatus) {
|
|
62
|
+
workflowStatus.textContent = '';
|
|
63
|
+
}
|
|
64
|
+
// Hide restore point button when not recording
|
|
65
|
+
if (saveRestorePointBtn) {
|
|
66
|
+
saveRestorePointBtn.style.display = 'none';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function startWorkflowTimer() {
|
|
73
|
+
// Get stored start time or use current time
|
|
74
|
+
chrome.storage.local.get(['workflowStartTime'], (result) => {
|
|
75
|
+
workflowStartTime = result.workflowStartTime || Date.now();
|
|
76
|
+
|
|
77
|
+
workflowRecordingTimer = setInterval(() => {
|
|
78
|
+
const elapsed = Math.floor((Date.now() - workflowStartTime) / 1000);
|
|
79
|
+
const minutes = Math.floor(elapsed / 60);
|
|
80
|
+
const seconds = elapsed % 60;
|
|
81
|
+
const workflowStatus = document.getElementById('workflowRecordingStatus');
|
|
82
|
+
if (workflowStatus) {
|
|
83
|
+
workflowStatus.innerHTML = `Recording workflow: <span class="recording-timer">${minutes}:${seconds.toString().padStart(2, '0')}</span>`;
|
|
84
|
+
}
|
|
85
|
+
}, 1000);
|
|
86
|
+
|
|
87
|
+
// Update timer immediately
|
|
88
|
+
const elapsed = Math.floor((Date.now() - workflowStartTime) / 1000);
|
|
89
|
+
const minutes = Math.floor(elapsed / 60);
|
|
90
|
+
const seconds = elapsed % 60;
|
|
91
|
+
const workflowStatus = document.getElementById('workflowRecordingStatus');
|
|
92
|
+
if (workflowStatus) {
|
|
93
|
+
workflowStatus.innerHTML = `Recording workflow: <span class="recording-timer">${minutes}:${seconds.toString().padStart(2, '0')}</span>`;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stopWorkflowTimer() {
|
|
99
|
+
if (workflowRecordingTimer) {
|
|
100
|
+
clearInterval(workflowRecordingTimer);
|
|
101
|
+
workflowRecordingTimer = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Import configuration - this will be available globally
|
|
106
|
+
const CONFIG_PORTS = CHROMEDEBUG_CONFIG?.ports || [3001, 3000, 3002, 3028]; // Fallback to defaults
|
|
107
|
+
|
|
108
|
+
// Site management functions
|
|
109
|
+
async function initializeSiteManagement() {
|
|
110
|
+
const siteStatusIcon = document.getElementById('siteStatusIcon');
|
|
111
|
+
const siteStatusText = document.getElementById('siteStatusText');
|
|
112
|
+
const toggleSiteBtn = document.getElementById('toggleSiteBtn');
|
|
113
|
+
const addSiteBtn = document.getElementById('addSiteBtn');
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Get current tab
|
|
117
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
118
|
+
if (!tab || !tab.url) {
|
|
119
|
+
updateSiteStatus('❓', 'Cannot access current tab', true);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const url = new URL(tab.url);
|
|
124
|
+
const hostname = url.hostname;
|
|
125
|
+
|
|
126
|
+
// Get site restriction settings
|
|
127
|
+
const settings = await chrome.storage.sync.get(['chromePilotMode', 'chromePilotAllowedSites', 'chromePilotRestrictedSites']);
|
|
128
|
+
const mode = settings.chromePilotMode || 'whitelist';
|
|
129
|
+
const allowedSites = settings.chromePilotAllowedSites || ['localhost:*', '127.0.0.1:*', '*.local', '*.test', '*.dev'];
|
|
130
|
+
const restrictedSites = settings.chromePilotRestrictedSites || ['youtube.com', '*.youtube.com', 'google.com', '*.google.com'];
|
|
131
|
+
|
|
132
|
+
// Test if site is allowed
|
|
133
|
+
const isAllowed = testSiteAccess(hostname, mode, allowedSites, restrictedSites);
|
|
134
|
+
|
|
135
|
+
if (isAllowed) {
|
|
136
|
+
updateSiteStatus('✅', `Active on ${hostname}`, false);
|
|
137
|
+
toggleSiteBtn.textContent = mode === 'whitelist' ? 'Remove from Whitelist' : 'Add to Blacklist';
|
|
138
|
+
toggleSiteBtn.className = 'site-toggle-btn block';
|
|
139
|
+
addSiteBtn.textContent = 'Manage Sites';
|
|
140
|
+
} else {
|
|
141
|
+
updateSiteStatus('❌', `Blocked on ${hostname}`, false);
|
|
142
|
+
toggleSiteBtn.textContent = mode === 'whitelist' ? 'Add to Whitelist' : 'Remove from Blacklist';
|
|
143
|
+
toggleSiteBtn.className = 'site-toggle-btn allow';
|
|
144
|
+
addSiteBtn.textContent = 'Manage Sites';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add event listeners
|
|
148
|
+
toggleSiteBtn.addEventListener('click', () => toggleCurrentSite(hostname, mode, allowedSites, restrictedSites, isAllowed));
|
|
149
|
+
addSiteBtn.addEventListener('click', () => addCurrentSite(hostname, mode));
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('Site management initialization error:', error);
|
|
153
|
+
updateSiteStatus('⚠️', 'Error checking site status', true);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function updateSiteStatus(icon, text, disabled) {
|
|
158
|
+
const siteStatusIcon = document.getElementById('siteStatusIcon');
|
|
159
|
+
const siteStatusText = document.getElementById('siteStatusText');
|
|
160
|
+
const toggleSiteBtn = document.getElementById('toggleSiteBtn');
|
|
161
|
+
const addSiteBtn = document.getElementById('addSiteBtn');
|
|
162
|
+
|
|
163
|
+
if (siteStatusIcon) siteStatusIcon.textContent = icon;
|
|
164
|
+
if (siteStatusText) siteStatusText.textContent = text;
|
|
165
|
+
if (toggleSiteBtn) toggleSiteBtn.disabled = disabled;
|
|
166
|
+
if (addSiteBtn) addSiteBtn.disabled = disabled;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function toggleCurrentSite(hostname, mode, allowedSites, restrictedSites, currentlyAllowed) {
|
|
170
|
+
try {
|
|
171
|
+
let newAllowedSites = [...allowedSites];
|
|
172
|
+
let newRestrictedSites = [...restrictedSites];
|
|
173
|
+
|
|
174
|
+
if (mode === 'whitelist') {
|
|
175
|
+
if (currentlyAllowed) {
|
|
176
|
+
// Remove from whitelist
|
|
177
|
+
newAllowedSites = newAllowedSites.filter(site => site !== hostname && site !== `*.${hostname.split('.').slice(1).join('.')}`);
|
|
178
|
+
} else {
|
|
179
|
+
// Add to whitelist
|
|
180
|
+
if (!newAllowedSites.includes(hostname)) {
|
|
181
|
+
newAllowedSites.push(hostname);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
if (currentlyAllowed) {
|
|
186
|
+
// Add to blacklist
|
|
187
|
+
if (!newRestrictedSites.includes(hostname)) {
|
|
188
|
+
newRestrictedSites.push(hostname);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Remove from blacklist
|
|
192
|
+
newRestrictedSites = newRestrictedSites.filter(site => site !== hostname && site !== `*.${hostname.split('.').slice(1).join('.')}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await chrome.storage.sync.set({
|
|
197
|
+
chromePilotAllowedSites: newAllowedSites,
|
|
198
|
+
chromePilotRestrictedSites: newRestrictedSites
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Refresh site status
|
|
202
|
+
initializeSiteManagement();
|
|
203
|
+
|
|
204
|
+
updateSiteStatus('✅', 'Settings updated. Reload tab to apply changes.', false);
|
|
205
|
+
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error toggling site:', error);
|
|
208
|
+
updateSiteStatus('⚠️', 'Error updating settings', false);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function addCurrentSite(hostname, mode) {
|
|
213
|
+
try {
|
|
214
|
+
// Open options page
|
|
215
|
+
chrome.tabs.create({ url: chrome.runtime.getURL('options.html') });
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Error opening options:', error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function testSiteAccess(hostname, mode, allowedSites, restrictedSites) {
|
|
222
|
+
hostname = hostname.toLowerCase();
|
|
223
|
+
|
|
224
|
+
if (mode === 'blacklist') {
|
|
225
|
+
// Block if hostname matches any restricted site pattern
|
|
226
|
+
return !restrictedSites.some(pattern => {
|
|
227
|
+
// Special handling for patterns with port wildcards like "localhost:*"
|
|
228
|
+
if (pattern.includes(':*')) {
|
|
229
|
+
const basePattern = pattern.replace(':*', '').toLowerCase();
|
|
230
|
+
if (hostname === basePattern) {
|
|
231
|
+
return true; // Match hostname regardless of port
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (pattern.includes('*')) {
|
|
236
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\./g, '\\.'));
|
|
237
|
+
return regex.test(hostname);
|
|
238
|
+
}
|
|
239
|
+
return hostname.includes(pattern.toLowerCase());
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
// Whitelist mode: only allow if hostname matches allowed sites
|
|
243
|
+
if (allowedSites.length === 0) return true; // Empty whitelist allows all
|
|
244
|
+
|
|
245
|
+
return allowedSites.some(pattern => {
|
|
246
|
+
// Special handling for patterns with port wildcards like "localhost:*"
|
|
247
|
+
if (pattern.includes(':*')) {
|
|
248
|
+
const basePattern = pattern.replace(':*', '').toLowerCase();
|
|
249
|
+
if (hostname === basePattern) {
|
|
250
|
+
return true; // Match hostname regardless of port
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (pattern.includes('*')) {
|
|
255
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\./g, '\\.'));
|
|
256
|
+
return regex.test(hostname);
|
|
257
|
+
}
|
|
258
|
+
return hostname.includes(pattern.toLowerCase());
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Function definitions (outside DOMContentLoaded for proper scope)
|
|
264
|
+
async function checkServerStatus() {
|
|
265
|
+
const statusEl = document.getElementById('serverStatus');
|
|
266
|
+
const statusTextEl = document.getElementById('statusText');
|
|
267
|
+
|
|
268
|
+
const ports = CONFIG_PORTS;
|
|
269
|
+
let connected = false;
|
|
270
|
+
let connectedPort = null;
|
|
271
|
+
|
|
272
|
+
for (const port of ports) {
|
|
273
|
+
try {
|
|
274
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/status`, {
|
|
275
|
+
method: 'GET',
|
|
276
|
+
mode: 'cors'
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (response.ok) {
|
|
280
|
+
connected = true;
|
|
281
|
+
connectedPort = port;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
// Try next port
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (connected) {
|
|
290
|
+
statusEl.className = 'server-status connected';
|
|
291
|
+
statusTextEl.textContent = `Server connected (port ${connectedPort})`;
|
|
292
|
+
} else {
|
|
293
|
+
statusEl.className = 'server-status disconnected';
|
|
294
|
+
statusTextEl.textContent = 'Server not running';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Return connection status and port for other functions to use
|
|
298
|
+
return { connected, connectedPort };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
function updateRecordingUI() {
|
|
303
|
+
const recordBtn = document.getElementById('recordBtn');
|
|
304
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
305
|
+
const manualSnapshotBtn = document.getElementById('manualSnapshotBtn');
|
|
306
|
+
const countdownDisplay = document.getElementById('countdownDisplay');
|
|
307
|
+
|
|
308
|
+
if (recordBtn) {
|
|
309
|
+
if (isRecording) {
|
|
310
|
+
recordBtn.textContent = 'Stop Recording';
|
|
311
|
+
recordBtn.classList.add('recording');
|
|
312
|
+
startRecordingTimer();
|
|
313
|
+
|
|
314
|
+
// Show manual snapshot button
|
|
315
|
+
if (manualSnapshotBtn) {
|
|
316
|
+
manualSnapshotBtn.style.display = 'block';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Show countdown if frame rate is 3+ seconds
|
|
320
|
+
if (currentFrameRate >= 3 && countdownDisplay) {
|
|
321
|
+
countdownDisplay.style.display = 'block';
|
|
322
|
+
startCountdownTimer();
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
recordBtn.textContent = 'Start Recording';
|
|
326
|
+
recordBtn.classList.remove('recording');
|
|
327
|
+
stopRecordingTimer();
|
|
328
|
+
stopCountdownTimer();
|
|
329
|
+
|
|
330
|
+
// Hide manual snapshot button and countdown
|
|
331
|
+
if (manualSnapshotBtn) {
|
|
332
|
+
manualSnapshotBtn.style.display = 'none';
|
|
333
|
+
}
|
|
334
|
+
if (countdownDisplay) {
|
|
335
|
+
countdownDisplay.style.display = 'none';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (recordingStatus) {
|
|
339
|
+
recordingStatus.textContent = '';
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function startRecordingTimer() {
|
|
346
|
+
// Use stored recordingStartTime if available, otherwise use current time
|
|
347
|
+
if (!recordingStartTime) {
|
|
348
|
+
chrome.storage.local.get(['recordingStartTime'], (result) => {
|
|
349
|
+
recordingStartTime = result.recordingStartTime || Date.now();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
recordingTimer = setInterval(() => {
|
|
354
|
+
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
|
|
355
|
+
const minutes = Math.floor(elapsed / 60);
|
|
356
|
+
const seconds = elapsed % 60;
|
|
357
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
358
|
+
if (recordingStatus) {
|
|
359
|
+
recordingStatus.innerHTML = `Recording: <span class="recording-timer">${minutes}:${seconds.toString().padStart(2, '0')}</span>`;
|
|
360
|
+
}
|
|
361
|
+
}, 1000);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function stopRecordingTimer() {
|
|
365
|
+
if (recordingTimer) {
|
|
366
|
+
clearInterval(recordingTimer);
|
|
367
|
+
recordingTimer = null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function startCountdownTimer() {
|
|
372
|
+
// Initialize next snapshot time if not already set
|
|
373
|
+
if (!nextSnapshotTime) {
|
|
374
|
+
nextSnapshotTime = Date.now() + (currentFrameRate * 1000);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
countdownInterval = setInterval(() => {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
const timeRemaining = Math.max(0, nextSnapshotTime - now);
|
|
380
|
+
const secondsRemaining = Math.ceil(timeRemaining / 1000);
|
|
381
|
+
|
|
382
|
+
const countdownTimer = document.getElementById('countdownTimer');
|
|
383
|
+
if (countdownTimer) {
|
|
384
|
+
countdownTimer.textContent = secondsRemaining;
|
|
385
|
+
|
|
386
|
+
// Add animation class for the last 3 seconds
|
|
387
|
+
if (secondsRemaining <= 3 && secondsRemaining > 0) {
|
|
388
|
+
countdownTimer.style.color = '#ff5252';
|
|
389
|
+
countdownTimer.classList.add('countdown-timer');
|
|
390
|
+
} else {
|
|
391
|
+
countdownTimer.style.color = '#f44336';
|
|
392
|
+
countdownTimer.classList.remove('countdown-timer');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Reset timer for next snapshot
|
|
397
|
+
if (timeRemaining <= 0) {
|
|
398
|
+
nextSnapshotTime = now + (currentFrameRate * 1000);
|
|
399
|
+
}
|
|
400
|
+
}, 100); // Update every 100ms for smooth countdown
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function stopCountdownTimer() {
|
|
404
|
+
if (countdownInterval) {
|
|
405
|
+
clearInterval(countdownInterval);
|
|
406
|
+
countdownInterval = null;
|
|
407
|
+
}
|
|
408
|
+
nextSnapshotTime = null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function takeManualSnapshot() {
|
|
412
|
+
// Send message to background script to trigger a manual snapshot
|
|
413
|
+
chrome.runtime.sendMessage({
|
|
414
|
+
action: 'takeManualSnapshot'
|
|
415
|
+
}, (response) => {
|
|
416
|
+
if (chrome.runtime.lastError) {
|
|
417
|
+
console.error('Error taking manual snapshot:', chrome.runtime.lastError);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (response && response.success) {
|
|
422
|
+
console.log('Manual snapshot taken successfully');
|
|
423
|
+
// Show brief feedback
|
|
424
|
+
const manualSnapshotBtn = document.getElementById('manualSnapshotBtn');
|
|
425
|
+
if (manualSnapshotBtn) {
|
|
426
|
+
const originalText = manualSnapshotBtn.textContent;
|
|
427
|
+
manualSnapshotBtn.textContent = 'Snapshot Taken!';
|
|
428
|
+
manualSnapshotBtn.style.background = '#45a049';
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
manualSnapshotBtn.textContent = originalText;
|
|
431
|
+
manualSnapshotBtn.style.background = '#4CAF50';
|
|
432
|
+
}, 1000);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
console.error('Failed to take manual snapshot:', response?.error);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
* SNAPSHOT FEATURE DISABLED (2025-10-01)
|
|
442
|
+
*
|
|
443
|
+
* This function provided "quick snapshots" (screenshot + console logs) for debugging.
|
|
444
|
+
*
|
|
445
|
+
* WHY DISABLED:
|
|
446
|
+
* - Without console log history, snapshots are just screenshots (users can do natively)
|
|
447
|
+
* - Console log capture requires always-on monitoring (privacy concern)
|
|
448
|
+
* - Cannot retrieve historical logs without constant interception
|
|
449
|
+
* - Core value proposition cannot be delivered cleanly
|
|
450
|
+
*
|
|
451
|
+
* ARCHITECTURAL CHALLENGE:
|
|
452
|
+
* To capture console logs, we need one of:
|
|
453
|
+
* 1. Always-on monitoring (privacy/performance concern)
|
|
454
|
+
* 2. Live-only capture (no historical context, defeats debugging purpose)
|
|
455
|
+
* 3. Historical API (doesn't exist in Chrome)
|
|
456
|
+
*
|
|
457
|
+
* TO RE-ENABLE:
|
|
458
|
+
* 1. Design privacy-conscious always-on log monitoring with:
|
|
459
|
+
* - User consent and clear privacy controls
|
|
460
|
+
* - Configurable retention periods
|
|
461
|
+
* - Memory-efficient log buffering
|
|
462
|
+
* - Visual indicator when monitoring active
|
|
463
|
+
* 2. Implement persistent log storage layer
|
|
464
|
+
* 3. Add user settings for log capture
|
|
465
|
+
* 4. Privacy impact assessment
|
|
466
|
+
* 5. Uncomment this function and UI elements
|
|
467
|
+
* 6. Uncomment related functions: loadSnapshots(), event handlers
|
|
468
|
+
* 7. Re-enable backend endpoints and methods
|
|
469
|
+
*
|
|
470
|
+
* RECOMMENDED ALTERNATIVE:
|
|
471
|
+
* Use "Start Recording" which properly captures screenshots + logs + user actions
|
|
472
|
+
*
|
|
473
|
+
* See: SNAPSHOT_FEATURE_DISABLED.md for detailed explanation
|
|
474
|
+
*/
|
|
475
|
+
/*
|
|
476
|
+
async function takeSnapshot() {
|
|
477
|
+
console.log('Taking standalone snapshot...');
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const serverStatus = await checkServerStatus();
|
|
481
|
+
if (!serverStatus.connected) {
|
|
482
|
+
console.error('Server not running');
|
|
483
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
484
|
+
if (recordingStatus) {
|
|
485
|
+
recordingStatus.innerHTML = '<strong style="color: #f44336;">Error: Server not running</strong><br>' +
|
|
486
|
+
'<small>Please start the Chrome Debug server first</small>';
|
|
487
|
+
}
|
|
488
|
+
setTimeout(() => {
|
|
489
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
490
|
+
}, 5000);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
495
|
+
|
|
496
|
+
if (!tab || !tab.id) {
|
|
497
|
+
console.error('No active tab found');
|
|
498
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
499
|
+
if (recordingStatus) {
|
|
500
|
+
recordingStatus.textContent = 'Error: No active tab';
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const noteInput = document.getElementById('snapshotNoteInput');
|
|
506
|
+
const note = noteInput ? noteInput.value.trim() : '';
|
|
507
|
+
|
|
508
|
+
const takeSnapshotBtn = document.getElementById('takeSnapshotBtn');
|
|
509
|
+
if (takeSnapshotBtn) {
|
|
510
|
+
takeSnapshotBtn.textContent = 'Taking Snapshot...';
|
|
511
|
+
takeSnapshotBtn.disabled = true;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
chrome.runtime.sendMessage({
|
|
515
|
+
action: 'takeStandaloneSnapshot',
|
|
516
|
+
tabId: tab.id,
|
|
517
|
+
note: note
|
|
518
|
+
}, (response) => {
|
|
519
|
+
if (takeSnapshotBtn) {
|
|
520
|
+
takeSnapshotBtn.disabled = false;
|
|
521
|
+
takeSnapshotBtn.textContent = '📸 Take Snapshot';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (chrome.runtime.lastError) {
|
|
525
|
+
console.error('Error taking snapshot:', chrome.runtime.lastError);
|
|
526
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
527
|
+
if (recordingStatus) {
|
|
528
|
+
recordingStatus.textContent = 'Error: ' + chrome.runtime.lastError.message;
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (response && response.success) {
|
|
534
|
+
console.log('Snapshot taken successfully:', response.sessionId);
|
|
535
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
536
|
+
if (recordingStatus) {
|
|
537
|
+
recordingStatus.innerHTML = `<strong style="color: #4CAF50;">Snapshot saved!</strong>`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (noteInput) {
|
|
541
|
+
noteInput.value = '';
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (response.sessionId) {
|
|
545
|
+
addRecording(response.sessionId, false, true, serverStatus.connectedPort, 'snapshot');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
setTimeout(() => {
|
|
549
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
550
|
+
}, 3000);
|
|
551
|
+
|
|
552
|
+
if (takeSnapshotBtn) {
|
|
553
|
+
const originalText = takeSnapshotBtn.textContent;
|
|
554
|
+
const originalColor = takeSnapshotBtn.style.background;
|
|
555
|
+
takeSnapshotBtn.textContent = '✅ Snapshot Saved!';
|
|
556
|
+
takeSnapshotBtn.style.background = '#4CAF50';
|
|
557
|
+
setTimeout(() => {
|
|
558
|
+
takeSnapshotBtn.textContent = originalText;
|
|
559
|
+
takeSnapshotBtn.style.background = originalColor;
|
|
560
|
+
}, 2000);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const snapshotsList = document.getElementById('snapshotsList');
|
|
564
|
+
if (snapshotsList && snapshotsList.style.display !== 'none') {
|
|
565
|
+
loadSnapshots();
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
569
|
+
if (recordingStatus) {
|
|
570
|
+
recordingStatus.textContent = 'Error: ' + (response?.error || 'Failed to take snapshot');
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
} catch (error) {
|
|
575
|
+
console.error('Error in takeSnapshot:', error);
|
|
576
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
577
|
+
if (recordingStatus) {
|
|
578
|
+
recordingStatus.textContent = 'Error: ' + error.message;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const takeSnapshotBtn = document.getElementById('takeSnapshotBtn');
|
|
582
|
+
if (takeSnapshotBtn) {
|
|
583
|
+
takeSnapshotBtn.disabled = false;
|
|
584
|
+
takeSnapshotBtn.textContent = '📸 Take Snapshot';
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
*/
|
|
589
|
+
|
|
590
|
+
function loadRecordings() {
|
|
591
|
+
chrome.storage.local.get(['recordings'], (result) => {
|
|
592
|
+
const recordings = result.recordings || [];
|
|
593
|
+
updateRecordingsDisplay(recordings);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function addRecording(recordingId, isSession = false, isFrameCapture = false, serverPort = null, type = null, sessionName = null) {
|
|
598
|
+
chrome.storage.local.get(['recordings'], (result) => {
|
|
599
|
+
const recordings = result.recordings || [];
|
|
600
|
+
recordings.unshift({
|
|
601
|
+
id: recordingId,
|
|
602
|
+
timestamp: Date.now(),
|
|
603
|
+
isSession: isSession,
|
|
604
|
+
isFrameCapture: isFrameCapture,
|
|
605
|
+
serverPort: serverPort,
|
|
606
|
+
type: type || (isSession ? 'session' : isFrameCapture ? 'recording' : 'recording'),
|
|
607
|
+
name: sessionName
|
|
608
|
+
}); // Add to beginning
|
|
609
|
+
chrome.storage.local.set({ recordings }, () => {
|
|
610
|
+
updateRecordingsDisplay(recordings);
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function deleteRecording(recordingId) {
|
|
616
|
+
// First, tell the background script to delete from server
|
|
617
|
+
chrome.runtime.sendMessage({
|
|
618
|
+
action: 'deleteRecording',
|
|
619
|
+
recordingId: recordingId
|
|
620
|
+
}, (response) => {
|
|
621
|
+
// Then remove from local storage
|
|
622
|
+
chrome.storage.local.get(['recordings'], (result) => {
|
|
623
|
+
const recordings = result.recordings || [];
|
|
624
|
+
const filtered = recordings.filter(r => r.id !== recordingId);
|
|
625
|
+
chrome.storage.local.set({ recordings: filtered }, () => {
|
|
626
|
+
updateRecordingsDisplay(filtered);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function updateRecordingsDisplay(recordings) {
|
|
633
|
+
const container = document.getElementById('recordingsContainer');
|
|
634
|
+
const listDiv = document.getElementById('recordingsList');
|
|
635
|
+
|
|
636
|
+
if (!container || !listDiv) return;
|
|
637
|
+
|
|
638
|
+
if (recordings.length === 0) {
|
|
639
|
+
listDiv.style.display = 'none';
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
listDiv.style.display = 'block';
|
|
644
|
+
container.innerHTML = recordings.map(recording => {
|
|
645
|
+
const portInfo = recording.serverPort ? ` - Port: ${recording.serverPort}` : '';
|
|
646
|
+
let typeInfo = '';
|
|
647
|
+
let typeIcon = '';
|
|
648
|
+
|
|
649
|
+
if (recording.type === 'snapshot') {
|
|
650
|
+
typeInfo = ' (snapshot)';
|
|
651
|
+
typeIcon = '📸 ';
|
|
652
|
+
} else if (recording.isFrameCapture) {
|
|
653
|
+
typeInfo = ' (frames)';
|
|
654
|
+
typeIcon = '🎥 ';
|
|
655
|
+
} else if (recording.isSession) {
|
|
656
|
+
typeInfo = ' (chunked)';
|
|
657
|
+
typeIcon = '📊 ';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const displayName = recording.name || recording.id;
|
|
661
|
+
|
|
662
|
+
return `
|
|
663
|
+
<div class="recording-item">
|
|
664
|
+
<div class="recording-info">
|
|
665
|
+
<div class="recording-id" title="${recording.id}">
|
|
666
|
+
<span class="recording-id-text">${typeIcon}${displayName}</span>
|
|
667
|
+
${typeInfo}${portInfo}
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
<div class="recording-buttons">
|
|
671
|
+
<button class="copy-id-btn" data-id="${recording.id}" title="Copy ID">Copy ID</button>
|
|
672
|
+
<button class="copy-prompt-btn" data-id="${recording.id}" data-port="${recording.serverPort || ''}" data-type="${recording.type || ''}" title="Copy Claude Prompt">Copy Prompt</button>
|
|
673
|
+
${recording.isFrameCapture || recording.type === 'snapshot' ? `<button class="view-btn" data-id="${recording.id}" data-type="${recording.type || ''}" title="View Screenshots">View</button>` : ''}
|
|
674
|
+
<button class="delete-btn" data-id="${recording.id}" title="Delete">Delete</button>
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
`;
|
|
678
|
+
}).join('');
|
|
679
|
+
|
|
680
|
+
// Add click handlers for copy ID buttons
|
|
681
|
+
container.querySelectorAll('.copy-id-btn').forEach(btn => {
|
|
682
|
+
btn.addEventListener('click', (e) => {
|
|
683
|
+
const recordingId = e.target.getAttribute('data-id');
|
|
684
|
+
navigator.clipboard.writeText(recordingId).then(() => {
|
|
685
|
+
// Visual feedback
|
|
686
|
+
const originalText = e.target.textContent;
|
|
687
|
+
e.target.textContent = 'Copied!';
|
|
688
|
+
e.target.style.background = '#4CAF50';
|
|
689
|
+
e.target.style.color = 'white';
|
|
690
|
+
setTimeout(() => {
|
|
691
|
+
e.target.textContent = originalText;
|
|
692
|
+
e.target.style.background = '';
|
|
693
|
+
e.target.style.color = '';
|
|
694
|
+
}, 1000);
|
|
695
|
+
}).catch(err => {
|
|
696
|
+
console.error('Failed to copy:', err);
|
|
697
|
+
alert('Failed to copy recording ID');
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
// Add click handlers for copy prompt buttons
|
|
703
|
+
container.querySelectorAll('.copy-prompt-btn').forEach(btn => {
|
|
704
|
+
btn.addEventListener('click', (e) => {
|
|
705
|
+
const recordingId = e.target.getAttribute('data-id');
|
|
706
|
+
const port = e.target.getAttribute('data-port');
|
|
707
|
+
const type = e.target.getAttribute('data-type');
|
|
708
|
+
const portText = port ? ` that was recorded on port ${port}` : '';
|
|
709
|
+
|
|
710
|
+
let prompt;
|
|
711
|
+
if (type === 'snapshot') {
|
|
712
|
+
prompt = `Please use the get_frame_screenshot function in Chrome Debug to view the snapshot "${recordingId}"${portText}. This is a single-frame screenshot with console logs captured for debugging.`;
|
|
713
|
+
} else {
|
|
714
|
+
prompt = `Please use the chrome_pilot_show_frames function in Chrome Debug to load the recording "${recordingId}"${portText}.`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
navigator.clipboard.writeText(prompt).then(() => {
|
|
718
|
+
// Visual feedback
|
|
719
|
+
const originalText = e.target.textContent;
|
|
720
|
+
e.target.textContent = 'Copied!';
|
|
721
|
+
e.target.style.background = '#4CAF50';
|
|
722
|
+
e.target.style.color = 'white';
|
|
723
|
+
setTimeout(() => {
|
|
724
|
+
e.target.textContent = originalText;
|
|
725
|
+
e.target.style.background = '';
|
|
726
|
+
e.target.style.color = '';
|
|
727
|
+
}, 1000);
|
|
728
|
+
}).catch(err => {
|
|
729
|
+
console.error('Failed to copy:', err);
|
|
730
|
+
alert('Failed to copy prompt');
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Add click handlers for delete buttons
|
|
736
|
+
container.querySelectorAll('.delete-btn').forEach(btn => {
|
|
737
|
+
btn.addEventListener('click', (e) => {
|
|
738
|
+
const recordingId = e.target.getAttribute('data-id');
|
|
739
|
+
if (confirm('Delete this recording?')) {
|
|
740
|
+
deleteRecording(recordingId);
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Add click handlers for view buttons
|
|
746
|
+
container.querySelectorAll('.view-btn').forEach(btn => {
|
|
747
|
+
btn.addEventListener('click', async (e) => {
|
|
748
|
+
const recordingId = e.target.getAttribute('data-id');
|
|
749
|
+
const type = e.target.getAttribute('data-type');
|
|
750
|
+
|
|
751
|
+
// Check if pro version (frame editor) is available
|
|
752
|
+
try {
|
|
753
|
+
const frameEditorUrl = type === 'snapshot'
|
|
754
|
+
? chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${recordingId}&type=snapshot`)
|
|
755
|
+
: chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${recordingId}`);
|
|
756
|
+
|
|
757
|
+
// Try to fetch the frame editor to see if it exists
|
|
758
|
+
const response = await fetch(frameEditorUrl, { method: 'HEAD' });
|
|
759
|
+
|
|
760
|
+
if (response.ok) {
|
|
761
|
+
// Pro version available - open frame editor
|
|
762
|
+
chrome.tabs.create({ url: frameEditorUrl });
|
|
763
|
+
} else {
|
|
764
|
+
throw new Error('Frame editor not available');
|
|
765
|
+
}
|
|
766
|
+
} catch (error) {
|
|
767
|
+
// Free version - show upgrade prompt
|
|
768
|
+
console.log('[ChromePilot Free] Frame editor not available - showing upgrade prompt');
|
|
769
|
+
alert('Frame Editor is a Pro feature. Upgrade to ChromePilot Pro to view detailed frame recordings with console logs and advanced debugging tools.');
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
// Initialize when DOM is ready
|
|
779
|
+
// License UI initialization function
|
|
780
|
+
async function initializeLicenseUI() {
|
|
781
|
+
console.log('[License] Initializing license UI');
|
|
782
|
+
|
|
783
|
+
// Get or create userId
|
|
784
|
+
const stored = await chrome.storage.local.get('chromedebug_user_id');
|
|
785
|
+
currentUserId = stored.chromedebug_user_id || crypto.randomUUID();
|
|
786
|
+
if (!stored.chromedebug_user_id) {
|
|
787
|
+
await chrome.storage.local.set({chromedebug_user_id: currentUserId});
|
|
788
|
+
console.log('[License] Created new user ID:', currentUserId);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Check license status
|
|
792
|
+
const licenseStatus = await licenseClient.getCachedLicenseStatus();
|
|
793
|
+
console.log('[License] License status:', licenseStatus);
|
|
794
|
+
|
|
795
|
+
// Update workflow recording PRO badge visibility
|
|
796
|
+
const workflowProBadge = document.getElementById('workflowProBadge');
|
|
797
|
+
|
|
798
|
+
if (licenseStatus.valid && licenseStatus.tier === 'pro') {
|
|
799
|
+
// Show pro status
|
|
800
|
+
document.getElementById('pro-tier-status').style.display = 'block';
|
|
801
|
+
document.getElementById('free-tier-status').style.display = 'none';
|
|
802
|
+
// Hide license activation input/button when Pro license is active
|
|
803
|
+
document.getElementById('license-activation').style.display = 'none';
|
|
804
|
+
// Hide PRO badge for Pro users
|
|
805
|
+
if (workflowProBadge) workflowProBadge.style.display = 'none';
|
|
806
|
+
console.log('[License] Displaying Pro tier status');
|
|
807
|
+
} else {
|
|
808
|
+
// Show free tier + usage
|
|
809
|
+
const usage = await licenseClient.checkUsageLimit(currentUserId);
|
|
810
|
+
console.log('[License] Usage check result:', usage);
|
|
811
|
+
|
|
812
|
+
// Handle both API response format (currentUsage, dailyLimit) and offline format (count, limit)
|
|
813
|
+
document.getElementById('usage-count').textContent = usage.currentUsage ?? usage.count ?? 0;
|
|
814
|
+
document.getElementById('usage-limit').textContent = usage.dailyLimit ?? usage.limit ?? 50;
|
|
815
|
+
document.getElementById('free-tier-status').style.display = 'block';
|
|
816
|
+
document.getElementById('pro-tier-status').style.display = 'none';
|
|
817
|
+
// Show license activation input/button for free tier
|
|
818
|
+
document.getElementById('license-activation').style.display = 'block';
|
|
819
|
+
// Show PRO badge for free users
|
|
820
|
+
if (workflowProBadge) workflowProBadge.style.display = 'block';
|
|
821
|
+
console.log('[License] Displaying Free tier status');
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// License activation handler
|
|
826
|
+
async function handleLicenseActivation() {
|
|
827
|
+
const licenseKey = document.getElementById('license-key-input').value.trim();
|
|
828
|
+
const messageDiv = document.getElementById('license-message');
|
|
829
|
+
|
|
830
|
+
if (!licenseKey) {
|
|
831
|
+
messageDiv.textContent = 'Please enter a license key';
|
|
832
|
+
messageDiv.style.color = '#f44336';
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
messageDiv.textContent = 'Validating...';
|
|
837
|
+
messageDiv.style.color = '#2196F3';
|
|
838
|
+
console.log('[License] Validating license key...');
|
|
839
|
+
|
|
840
|
+
const result = await licenseClient.validateLicense(licenseKey);
|
|
841
|
+
console.log('[License] Validation result:', result);
|
|
842
|
+
|
|
843
|
+
if (result.valid) {
|
|
844
|
+
messageDiv.textContent = 'License activated successfully!';
|
|
845
|
+
messageDiv.style.color = '#4caf50';
|
|
846
|
+
document.getElementById('license-key-input').value = '';
|
|
847
|
+
await initializeLicenseUI(); // Refresh UI
|
|
848
|
+
} else {
|
|
849
|
+
messageDiv.textContent = result.error || 'Invalid license key';
|
|
850
|
+
messageDiv.style.color = '#f44336';
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Upgrade button handler
|
|
855
|
+
function handleUpgradeClick() {
|
|
856
|
+
console.log('[License] Opening upgrade page:', LEMONSQUEEZY_CHECKOUT_URL);
|
|
857
|
+
chrome.tabs.create({url: LEMONSQUEEZY_CHECKOUT_URL});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
861
|
+
console.log('DOM loaded, initializing popup');
|
|
862
|
+
|
|
863
|
+
// Initialize license UI first
|
|
864
|
+
initializeLicenseUI().catch(error => {
|
|
865
|
+
console.error('[License] Failed to initialize license UI:', error);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// License event handlers
|
|
869
|
+
const activateLicenseBtn = document.getElementById('activate-license-btn');
|
|
870
|
+
const upgradeBtn = document.getElementById('upgrade-btn');
|
|
871
|
+
|
|
872
|
+
if (activateLicenseBtn) {
|
|
873
|
+
activateLicenseBtn.addEventListener('click', handleLicenseActivation);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (upgradeBtn) {
|
|
877
|
+
upgradeBtn.addEventListener('click', handleUpgradeClick);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Function to update button states based on server status
|
|
881
|
+
async function updateButtonStatesForServerStatus() {
|
|
882
|
+
const serverStatus = await checkServerStatus();
|
|
883
|
+
const recordBtn = document.getElementById('recordBtn');
|
|
884
|
+
const workflowRecordBtn = document.getElementById('workflowRecordBtn');
|
|
885
|
+
|
|
886
|
+
if (!serverStatus.connected) {
|
|
887
|
+
// Disable recording buttons when server is not running
|
|
888
|
+
if (recordBtn && !isRecording) {
|
|
889
|
+
recordBtn.style.opacity = '0.6';
|
|
890
|
+
recordBtn.title = 'Server not running - please start Chrome Debug server';
|
|
891
|
+
}
|
|
892
|
+
if (workflowRecordBtn && !isWorkflowRecording) {
|
|
893
|
+
workflowRecordBtn.style.opacity = '0.6';
|
|
894
|
+
workflowRecordBtn.title = 'Server not running - please start Chrome Debug server';
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
897
|
+
// Enable recording buttons when server is running
|
|
898
|
+
if (recordBtn) {
|
|
899
|
+
recordBtn.style.opacity = '1';
|
|
900
|
+
recordBtn.title = '';
|
|
901
|
+
}
|
|
902
|
+
if (workflowRecordBtn) {
|
|
903
|
+
workflowRecordBtn.style.opacity = '1';
|
|
904
|
+
workflowRecordBtn.title = '';
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Initialize server status check
|
|
910
|
+
updateButtonStatesForServerStatus();
|
|
911
|
+
setInterval(updateButtonStatesForServerStatus, 3000);
|
|
912
|
+
|
|
913
|
+
// Initialize site management functionality
|
|
914
|
+
initializeSiteManagement();
|
|
915
|
+
|
|
916
|
+
// Initialize enhanced capture toggle
|
|
917
|
+
const enhancedCaptureCheckbox = document.getElementById('enhancedCaptureCheckbox');
|
|
918
|
+
if (enhancedCaptureCheckbox) {
|
|
919
|
+
// Load setting from storage (default to false for performance)
|
|
920
|
+
chrome.storage.local.get(['enhancedClickCapture'], (result) => {
|
|
921
|
+
enhancedCaptureCheckbox.checked = result.enhancedClickCapture === true;
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Save changes when checkbox is clicked
|
|
925
|
+
enhancedCaptureCheckbox.addEventListener('change', () => {
|
|
926
|
+
chrome.storage.local.set({ enhancedClickCapture: enhancedCaptureCheckbox.checked });
|
|
927
|
+
console.log('Enhanced click capture setting changed to:', enhancedCaptureCheckbox.checked);
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Initialize state from storage
|
|
932
|
+
chrome.storage.local.get([
|
|
933
|
+
'frameRate',
|
|
934
|
+
'imageQuality',
|
|
935
|
+
'frameFlash',
|
|
936
|
+
'workflowRecording',
|
|
937
|
+
'includeLogsInExport',
|
|
938
|
+
'workflowScreenshotsEnabled',
|
|
939
|
+
'workflowScreenshotFormat',
|
|
940
|
+
'workflowScreenshotQuality',
|
|
941
|
+
'workflowScreenshotMaxWidth'
|
|
942
|
+
], (result) => {
|
|
943
|
+
|
|
944
|
+
// Restore workflow recording state
|
|
945
|
+
if (result.workflowRecording === true) {
|
|
946
|
+
isWorkflowRecording = true;
|
|
947
|
+
updateWorkflowRecordingUI();
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Restore include logs checkbox state (default to true)
|
|
951
|
+
if (includeLogsCheckbox) {
|
|
952
|
+
includeLogsCheckbox.checked = result.includeLogsInExport !== false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Load recording settings with defaults
|
|
956
|
+
const frameRateSelect = document.getElementById('frameRate');
|
|
957
|
+
const imageQualitySelect = document.getElementById('imageQuality');
|
|
958
|
+
const frameFlashCheckbox = document.getElementById('frameFlash');
|
|
959
|
+
|
|
960
|
+
// Set defaults if not stored
|
|
961
|
+
const frameRate = result.frameRate || '1';
|
|
962
|
+
const imageQuality = result.imageQuality || '30';
|
|
963
|
+
const frameFlash = result.frameFlash !== false; // Default to true
|
|
964
|
+
|
|
965
|
+
if (frameRateSelect) {
|
|
966
|
+
frameRateSelect.value = frameRate;
|
|
967
|
+
}
|
|
968
|
+
if (imageQualitySelect) {
|
|
969
|
+
imageQualitySelect.value = imageQuality;
|
|
970
|
+
}
|
|
971
|
+
if (frameFlashCheckbox) {
|
|
972
|
+
frameFlashCheckbox.checked = frameFlash;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Save defaults if they weren't stored
|
|
976
|
+
if (!result.frameRate || !result.imageQuality || result.frameFlash === undefined) {
|
|
977
|
+
chrome.storage.local.set({
|
|
978
|
+
frameRate: frameRate,
|
|
979
|
+
imageQuality: imageQuality,
|
|
980
|
+
frameFlash: frameFlash
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Restore workflow screenshot settings
|
|
985
|
+
const enableScreenshotsCheckbox = document.getElementById('enableScreenshotsCheckbox');
|
|
986
|
+
const screenshotFormat = document.getElementById('screenshotFormat');
|
|
987
|
+
const screenshotQuality = document.getElementById('screenshotQuality');
|
|
988
|
+
const screenshotMaxWidth = document.getElementById('screenshotMaxWidth');
|
|
989
|
+
const screenshotSettingsDiv = document.getElementById('screenshotSettings');
|
|
990
|
+
const qualityValue = document.getElementById('qualityValue');
|
|
991
|
+
|
|
992
|
+
if (enableScreenshotsCheckbox) {
|
|
993
|
+
// Default to true if not set
|
|
994
|
+
const screenshotsEnabled = result.workflowScreenshotsEnabled !== false;
|
|
995
|
+
enableScreenshotsCheckbox.checked = screenshotsEnabled;
|
|
996
|
+
|
|
997
|
+
if (screenshotSettingsDiv) {
|
|
998
|
+
screenshotSettingsDiv.style.display = screenshotsEnabled ? 'block' : 'none';
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (screenshotFormat && result.workflowScreenshotFormat) {
|
|
1003
|
+
screenshotFormat.value = result.workflowScreenshotFormat;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (screenshotQuality) {
|
|
1007
|
+
const quality = result.workflowScreenshotQuality || 30;
|
|
1008
|
+
screenshotQuality.value = quality;
|
|
1009
|
+
if (qualityValue) qualityValue.textContent = quality;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (screenshotMaxWidth && result.workflowScreenshotMaxWidth) {
|
|
1013
|
+
screenshotMaxWidth.value = result.workflowScreenshotMaxWidth;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Load saved recordings
|
|
1019
|
+
loadRecordings();
|
|
1020
|
+
|
|
1021
|
+
// Setup recording settings event listeners
|
|
1022
|
+
const frameRateSelect = document.getElementById('frameRate');
|
|
1023
|
+
const imageQualitySelect = document.getElementById('imageQuality');
|
|
1024
|
+
const frameFlashCheckbox = document.getElementById('frameFlash');
|
|
1025
|
+
|
|
1026
|
+
if (frameRateSelect) {
|
|
1027
|
+
frameRateSelect.addEventListener('change', () => {
|
|
1028
|
+
chrome.storage.local.set({ frameRate: frameRateSelect.value });
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (imageQualitySelect) {
|
|
1033
|
+
imageQualitySelect.addEventListener('change', () => {
|
|
1034
|
+
chrome.storage.local.set({ imageQuality: imageQualitySelect.value });
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (frameFlashCheckbox) {
|
|
1039
|
+
frameFlashCheckbox.addEventListener('change', () => {
|
|
1040
|
+
chrome.storage.local.set({ frameFlash: frameFlashCheckbox.checked });
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Check initial recording state from storage
|
|
1045
|
+
chrome.storage.local.get(['recordingActive', 'recordingStartTime', 'pendingRecording', 'pendingTabId'], (result) => {
|
|
1046
|
+
if (result.recordingActive) {
|
|
1047
|
+
isRecording = true;
|
|
1048
|
+
recordingStartTime = result.recordingStartTime || Date.now();
|
|
1049
|
+
updateRecordingUI();
|
|
1050
|
+
console.log('Restored recording state from storage');
|
|
1051
|
+
} else if (result.pendingRecording && result.pendingTabId) {
|
|
1052
|
+
// Auto-start recording for the pending tab
|
|
1053
|
+
console.log('Found pending recording for tab:', result.pendingTabId);
|
|
1054
|
+
const targetTabId = result.pendingTabId;
|
|
1055
|
+
chrome.storage.local.remove(['pendingRecording', 'pendingTabId']);
|
|
1056
|
+
|
|
1057
|
+
// Start recording
|
|
1058
|
+
chrome.runtime.sendMessage({
|
|
1059
|
+
action: 'startRecording',
|
|
1060
|
+
tabId: targetTabId
|
|
1061
|
+
}, (response) => {
|
|
1062
|
+
if (chrome.runtime.lastError) {
|
|
1063
|
+
console.error('Error starting recording:', chrome.runtime.lastError);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (response && response.success) {
|
|
1068
|
+
isRecording = true;
|
|
1069
|
+
recordingStartTime = Date.now();
|
|
1070
|
+
updateRecordingUI();
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
// Setup recording button handler
|
|
1078
|
+
const recordBtn = document.getElementById('recordBtn');
|
|
1079
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
1080
|
+
|
|
1081
|
+
if (recordBtn) {
|
|
1082
|
+
console.log('Setting up record button handler');
|
|
1083
|
+
recordBtn.addEventListener('click', async (e) => {
|
|
1084
|
+
e.preventDefault();
|
|
1085
|
+
console.log('Record button clicked, isRecording:', isRecording);
|
|
1086
|
+
|
|
1087
|
+
if (!isRecording && !isStoppingRecording) {
|
|
1088
|
+
try {
|
|
1089
|
+
// Check if server is running before starting recording
|
|
1090
|
+
const serverStatus = await checkServerStatus();
|
|
1091
|
+
if (!serverStatus.connected) {
|
|
1092
|
+
console.error('Server not running');
|
|
1093
|
+
if (recordingStatus) {
|
|
1094
|
+
recordingStatus.innerHTML = '<strong style="color: #f44336;">Error: Server not running</strong><br>' +
|
|
1095
|
+
'<small>Please start the Chrome Debug server first</small>';
|
|
1096
|
+
}
|
|
1097
|
+
// Clear error message after 5 seconds
|
|
1098
|
+
setTimeout(() => {
|
|
1099
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
1100
|
+
}, 5000);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Get the current active tab
|
|
1105
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1106
|
+
console.log('Current tab:', tab);
|
|
1107
|
+
|
|
1108
|
+
if (!tab || !tab.id) {
|
|
1109
|
+
console.error('No active tab found');
|
|
1110
|
+
if (recordingStatus) {
|
|
1111
|
+
recordingStatus.textContent = 'Error: No active tab';
|
|
1112
|
+
}
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Disable button while starting
|
|
1117
|
+
recordBtn.textContent = 'Starting...';
|
|
1118
|
+
recordBtn.disabled = true;
|
|
1119
|
+
|
|
1120
|
+
// Get recording settings
|
|
1121
|
+
const frameRateSelect = document.getElementById('frameRate');
|
|
1122
|
+
const imageQualitySelect = document.getElementById('imageQuality');
|
|
1123
|
+
const frameFlashCheckbox = document.getElementById('frameFlash');
|
|
1124
|
+
const screenRecordingSessionNameInput = document.getElementById('screenRecordingSessionNameInput');
|
|
1125
|
+
|
|
1126
|
+
const settings = {
|
|
1127
|
+
frameRate: frameRateSelect ? parseFloat(frameRateSelect.value) : 1,
|
|
1128
|
+
imageQuality: imageQualitySelect ? parseInt(imageQualitySelect.value) : 30,
|
|
1129
|
+
frameFlash: frameFlashCheckbox ? frameFlashCheckbox.checked : true,
|
|
1130
|
+
sessionName: screenRecordingSessionNameInput?.value.trim() || null
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// Send message to background to start recording
|
|
1134
|
+
chrome.runtime.sendMessage({
|
|
1135
|
+
action: 'startRecording',
|
|
1136
|
+
tabId: tab.id,
|
|
1137
|
+
settings: settings
|
|
1138
|
+
}, (response) => {
|
|
1139
|
+
recordBtn.disabled = false;
|
|
1140
|
+
|
|
1141
|
+
if (chrome.runtime.lastError) {
|
|
1142
|
+
console.error('Error starting recording:', chrome.runtime.lastError);
|
|
1143
|
+
recordingStatus.textContent = 'Error: ' + chrome.runtime.lastError.message;
|
|
1144
|
+
recordBtn.textContent = 'Start Recording';
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (response && response.success) {
|
|
1149
|
+
console.log('Recording started successfully');
|
|
1150
|
+
isRecording = true;
|
|
1151
|
+
recordingStartTime = Date.now();
|
|
1152
|
+
|
|
1153
|
+
// Capture current frame rate for countdown timer
|
|
1154
|
+
currentFrameRate = settings.frameRate;
|
|
1155
|
+
|
|
1156
|
+
updateRecordingUI();
|
|
1157
|
+
} else {
|
|
1158
|
+
recordingStatus.textContent = 'Error: ' + (response?.error || 'Failed to start recording');
|
|
1159
|
+
recordBtn.textContent = 'Start Recording';
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
console.error('Error in record button handler:', error);
|
|
1164
|
+
if (recordingStatus) {
|
|
1165
|
+
recordingStatus.textContent = 'Error: ' + error.message;
|
|
1166
|
+
}
|
|
1167
|
+
recordBtn.disabled = false;
|
|
1168
|
+
recordBtn.textContent = 'Start Recording';
|
|
1169
|
+
}
|
|
1170
|
+
} else if (isRecording && !isStoppingRecording) {
|
|
1171
|
+
// Stop recording
|
|
1172
|
+
console.log('Stopping recording...');
|
|
1173
|
+
isStoppingRecording = true;
|
|
1174
|
+
recordBtn.textContent = 'Stopping...';
|
|
1175
|
+
recordBtn.disabled = true;
|
|
1176
|
+
|
|
1177
|
+
chrome.runtime.sendMessage({
|
|
1178
|
+
action: 'stopRecording'
|
|
1179
|
+
}, (response) => {
|
|
1180
|
+
isStoppingRecording = false;
|
|
1181
|
+
recordBtn.disabled = false;
|
|
1182
|
+
|
|
1183
|
+
if (chrome.runtime.lastError) {
|
|
1184
|
+
console.error('Error stopping recording:', chrome.runtime.lastError);
|
|
1185
|
+
recordingStatus.textContent = 'Error: ' + chrome.runtime.lastError.message;
|
|
1186
|
+
recordBtn.textContent = 'Stop Recording';
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (response && response.success) {
|
|
1191
|
+
console.log('Recording stopped successfully');
|
|
1192
|
+
isRecording = false;
|
|
1193
|
+
updateRecordingUI();
|
|
1194
|
+
} else {
|
|
1195
|
+
recordingStatus.textContent = 'Error: ' + (response?.error || 'Failed to stop recording');
|
|
1196
|
+
recordBtn.textContent = 'Stop Recording';
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
} else {
|
|
1202
|
+
console.error('Record button not found!');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Setup manual snapshot button handler
|
|
1206
|
+
const manualSnapshotBtn = document.getElementById('manualSnapshotBtn');
|
|
1207
|
+
if (manualSnapshotBtn) {
|
|
1208
|
+
manualSnapshotBtn.addEventListener('click', (e) => {
|
|
1209
|
+
e.preventDefault();
|
|
1210
|
+
if (isRecording) {
|
|
1211
|
+
takeManualSnapshot();
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/*
|
|
1217
|
+
* SNAPSHOT FEATURE DISABLED (2025-10-01)
|
|
1218
|
+
* Snapshot button click handler disabled - see SNAPSHOT_FEATURE_DISABLED.md
|
|
1219
|
+
*/
|
|
1220
|
+
/*
|
|
1221
|
+
const takeSnapshotBtn = document.getElementById('takeSnapshotBtn');
|
|
1222
|
+
if (takeSnapshotBtn) {
|
|
1223
|
+
takeSnapshotBtn.addEventListener('click', async (e) => {
|
|
1224
|
+
e.preventDefault();
|
|
1225
|
+
await takeSnapshot();
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
*/
|
|
1229
|
+
|
|
1230
|
+
// Setup workflow recording button handler
|
|
1231
|
+
const workflowRecordBtn = document.getElementById('workflowRecordBtn');
|
|
1232
|
+
const workflowRecordingsList = document.getElementById('workflowRecordingsList');
|
|
1233
|
+
const workflowRecordingsContainer = document.getElementById('workflowRecordingsContainer');
|
|
1234
|
+
const includeLogsCheckbox = document.getElementById('includeLogsCheckbox');
|
|
1235
|
+
|
|
1236
|
+
if (workflowRecordBtn) {
|
|
1237
|
+
workflowRecordBtn.addEventListener('click', async () => {
|
|
1238
|
+
if (!isWorkflowRecording) {
|
|
1239
|
+
// Start workflow recording
|
|
1240
|
+
try {
|
|
1241
|
+
// Check if server is running before starting workflow recording
|
|
1242
|
+
const serverStatus = await checkServerStatus();
|
|
1243
|
+
if (!serverStatus.connected) {
|
|
1244
|
+
console.error('Server not running');
|
|
1245
|
+
const workflowStatus = document.getElementById('workflowRecordingStatus');
|
|
1246
|
+
if (workflowStatus) {
|
|
1247
|
+
workflowStatus.innerHTML = '<strong style="color: #f44336;">Error: Server not running</strong><br>' +
|
|
1248
|
+
'<small>Please start the Chrome Debug server first</small>';
|
|
1249
|
+
}
|
|
1250
|
+
// Clear error message after 5 seconds
|
|
1251
|
+
setTimeout(() => {
|
|
1252
|
+
if (workflowStatus) workflowStatus.textContent = '';
|
|
1253
|
+
}, 5000);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Check license before allowing workflow recording (same pattern as strategic plan)
|
|
1258
|
+
console.log('[License] Checking license for workflow recording...');
|
|
1259
|
+
const licenseCheck = await chrome.runtime.sendMessage({
|
|
1260
|
+
action: 'checkLicenseForWorkflow'
|
|
1261
|
+
});
|
|
1262
|
+
console.log('[License] License check result:', licenseCheck);
|
|
1263
|
+
|
|
1264
|
+
// If not Pro or limit reached, show upgrade modal
|
|
1265
|
+
if (!licenseCheck || !licenseCheck.allowed || (licenseCheck.tier && licenseCheck.tier !== 'pro')) {
|
|
1266
|
+
const message = licenseCheck?.message || 'Workflow Recording is a Pro feature. Upgrade to Pro for unlimited workflow recordings with function tracing.';
|
|
1267
|
+
|
|
1268
|
+
// Show modal
|
|
1269
|
+
if (confirm(`${message}\n\nWould you like to upgrade now?`)) {
|
|
1270
|
+
// Open upgrade page
|
|
1271
|
+
chrome.tabs.create({
|
|
1272
|
+
url: LEMONSQUEEZY_CHECKOUT_URL
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return; // Stop execution
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1280
|
+
|
|
1281
|
+
if (!tab || !tab.id) {
|
|
1282
|
+
console.error('No active tab found');
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// Get settings
|
|
1287
|
+
const includeLogsInExport = includeLogsCheckbox ? includeLogsCheckbox.checked : true;
|
|
1288
|
+
const sessionName = document.getElementById('sessionNameInput')?.value || null;
|
|
1289
|
+
const enableScreenshots = document.getElementById('enableScreenshotsCheckbox')?.checked || false;
|
|
1290
|
+
|
|
1291
|
+
let screenshotSettings = null;
|
|
1292
|
+
if (enableScreenshots) {
|
|
1293
|
+
screenshotSettings = {
|
|
1294
|
+
enabled: true,
|
|
1295
|
+
format: document.getElementById('screenshotFormat')?.value || 'jpeg',
|
|
1296
|
+
quality: parseInt(document.getElementById('screenshotQuality')?.value) || 30,
|
|
1297
|
+
maxWidth: parseInt(document.getElementById('screenshotMaxWidth')?.value) || null
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Send message to background script to start recording
|
|
1302
|
+
console.log('[Popup v2.0.6] Sending startWorkflowRecording message to background');
|
|
1303
|
+
chrome.runtime.sendMessage({
|
|
1304
|
+
action: 'startWorkflowRecording',
|
|
1305
|
+
tabId: tab.id,
|
|
1306
|
+
includeLogsInExport: includeLogsInExport,
|
|
1307
|
+
sessionName: sessionName,
|
|
1308
|
+
screenshotSettings: screenshotSettings
|
|
1309
|
+
}, (response) => {
|
|
1310
|
+
console.log('[Popup v2.0.6] Received response from background:', response);
|
|
1311
|
+
console.log('[Popup v2.0.6] chrome.runtime.lastError:', chrome.runtime.lastError);
|
|
1312
|
+
console.log('[Popup v2.0.6] Checking which branch will execute...');
|
|
1313
|
+
|
|
1314
|
+
if (chrome.runtime.lastError) {
|
|
1315
|
+
console.error('[Popup v2.0.6] RUNTIME ERROR BRANCH:', chrome.runtime.lastError);
|
|
1316
|
+
// Check if this tab allows content script injection
|
|
1317
|
+
if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
|
|
1318
|
+
console.log('Cannot inject content script into restricted URL:', tab.url);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
// Inject content script if needed
|
|
1322
|
+
chrome.scripting.executeScript({
|
|
1323
|
+
target: { tabId: tab.id },
|
|
1324
|
+
files: ['content.js']
|
|
1325
|
+
}, () => {
|
|
1326
|
+
if (chrome.runtime.lastError) {
|
|
1327
|
+
console.error('Failed to inject content script:', chrome.runtime.lastError);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
chrome.scripting.insertCSS({
|
|
1331
|
+
target: { tabId: tab.id },
|
|
1332
|
+
files: ['content.css']
|
|
1333
|
+
}, () => {
|
|
1334
|
+
if (chrome.runtime.lastError) {
|
|
1335
|
+
console.error('Failed to inject CSS:', chrome.runtime.lastError);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
// Try again after injection
|
|
1339
|
+
setTimeout(() => {
|
|
1340
|
+
chrome.runtime.sendMessage({
|
|
1341
|
+
action: 'startWorkflowRecording',
|
|
1342
|
+
tabId: tab.id,
|
|
1343
|
+
includeLogsInExport: includeLogsInExport,
|
|
1344
|
+
sessionName: sessionName,
|
|
1345
|
+
screenshotSettings: screenshotSettings
|
|
1346
|
+
}, (retryResponse) => {
|
|
1347
|
+
if (retryResponse && retryResponse.success) {
|
|
1348
|
+
isWorkflowRecording = true;
|
|
1349
|
+
// Save state and start time to storage
|
|
1350
|
+
const startTime = Date.now();
|
|
1351
|
+
chrome.storage.local.set({
|
|
1352
|
+
workflowRecording: true,
|
|
1353
|
+
workflowStartTime: startTime
|
|
1354
|
+
});
|
|
1355
|
+
workflowStartTime = startTime;
|
|
1356
|
+
updateWorkflowRecordingUI();
|
|
1357
|
+
if (workflowRecordingsList) workflowRecordingsList.style.display = 'none';
|
|
1358
|
+
}
|
|
1359
|
+
});
|
|
1360
|
+
}, 100);
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
} else if (response && response.success) {
|
|
1364
|
+
console.log('[Popup v2.0.6] *** SUCCESS BRANCH REACHED! ***');
|
|
1365
|
+
console.log('[Popup v2.0.6] Workflow recording started successfully, updating UI now');
|
|
1366
|
+
console.log('[Popup v2.0.6] Setting isWorkflowRecording = true');
|
|
1367
|
+
isWorkflowRecording = true;
|
|
1368
|
+
// Save state and start time to storage
|
|
1369
|
+
const startTime = Date.now();
|
|
1370
|
+
chrome.storage.local.set({
|
|
1371
|
+
workflowRecording: true,
|
|
1372
|
+
workflowStartTime: startTime
|
|
1373
|
+
});
|
|
1374
|
+
workflowStartTime = startTime;
|
|
1375
|
+
console.log('[Popup v2.0.6] About to call updateWorkflowRecordingUI()');
|
|
1376
|
+
updateWorkflowRecordingUI();
|
|
1377
|
+
console.log('[Popup v2.0.6] updateWorkflowRecordingUI() call completed');
|
|
1378
|
+
// Hide any previous results
|
|
1379
|
+
if (workflowRecordingsList) workflowRecordingsList.style.display = 'none';
|
|
1380
|
+
} else {
|
|
1381
|
+
console.error('[Popup v2.0.6] *** ELSE BRANCH - Unexpected response ***');
|
|
1382
|
+
console.error('[Popup v2.0.6] Response details:', {
|
|
1383
|
+
exists: !!response,
|
|
1384
|
+
success: response?.success,
|
|
1385
|
+
type: typeof response,
|
|
1386
|
+
keys: response ? Object.keys(response) : 'no response',
|
|
1387
|
+
fullResponse: response
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
console.error('Error starting workflow recording:', error);
|
|
1393
|
+
}
|
|
1394
|
+
} else {
|
|
1395
|
+
// Stop workflow recording
|
|
1396
|
+
try {
|
|
1397
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1398
|
+
|
|
1399
|
+
if (!tab || !tab.id) {
|
|
1400
|
+
console.error('No active tab found');
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Send message to background script to stop recording
|
|
1405
|
+
chrome.runtime.sendMessage({
|
|
1406
|
+
action: 'stopWorkflowRecording',
|
|
1407
|
+
tabId: tab.id
|
|
1408
|
+
}, async (response) => {
|
|
1409
|
+
if (chrome.runtime.lastError) {
|
|
1410
|
+
console.error('Error stopping workflow:', chrome.runtime.lastError);
|
|
1411
|
+
// Still stop recording in UI even if there was an error
|
|
1412
|
+
isWorkflowRecording = false;
|
|
1413
|
+
chrome.storage.local.set({
|
|
1414
|
+
workflowRecording: false,
|
|
1415
|
+
workflowStartTime: null
|
|
1416
|
+
});
|
|
1417
|
+
updateWorkflowRecordingUI();
|
|
1418
|
+
alert('Error stopping workflow recording. The recording may have been lost.');
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (response && response.success && response.workflow) {
|
|
1423
|
+
isWorkflowRecording = false;
|
|
1424
|
+
// Clear state from storage
|
|
1425
|
+
chrome.storage.local.set({
|
|
1426
|
+
workflowRecording: false,
|
|
1427
|
+
workflowStartTime: null
|
|
1428
|
+
});
|
|
1429
|
+
updateWorkflowRecordingUI();
|
|
1430
|
+
|
|
1431
|
+
// Display the workflow results
|
|
1432
|
+
if (workflowRecordingsContainer) {
|
|
1433
|
+
let workflowItem;
|
|
1434
|
+
|
|
1435
|
+
// Get current server port for prompt generation
|
|
1436
|
+
const serverConnection = await checkServerStatus();
|
|
1437
|
+
const currentServerPort = serverConnection.connectedPort;
|
|
1438
|
+
|
|
1439
|
+
// The response.workflow contains the full response from stopWorkflowRecording
|
|
1440
|
+
const workflowData = response.workflow;
|
|
1441
|
+
|
|
1442
|
+
if (workflowData.savedToServer) {
|
|
1443
|
+
// Create a saved recording entry like screen recordings
|
|
1444
|
+
workflowItem = document.createElement('div');
|
|
1445
|
+
workflowItem.className = 'recording-item';
|
|
1446
|
+
workflowItem.innerHTML = `
|
|
1447
|
+
<div class="recording-info">
|
|
1448
|
+
<div style="font-weight: bold; margin-bottom: 4px;">
|
|
1449
|
+
${workflowData.workflow.sessionId} (workflow)
|
|
1450
|
+
</div>
|
|
1451
|
+
<div style="color: #666; font-size: 11px;">
|
|
1452
|
+
${workflowData.workflow.actions.length} actions • ${workflowData.workflow.logs.length} logs
|
|
1453
|
+
</div>
|
|
1454
|
+
</div>
|
|
1455
|
+
<div style="display: flex; gap: 4px; margin-top: 8px;">
|
|
1456
|
+
<button class="copy-id-btn" style="padding: 4px 8px; background: #2196F3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
1457
|
+
Copy ID
|
|
1458
|
+
</button>
|
|
1459
|
+
<button class="copy-prompt-btn" data-id="${workflowData.workflow.sessionId}" data-port="${currentServerPort || ''}" style="padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
1460
|
+
Copy Prompt
|
|
1461
|
+
</button>
|
|
1462
|
+
</div>
|
|
1463
|
+
`;
|
|
1464
|
+
|
|
1465
|
+
// Add event listeners for the buttons
|
|
1466
|
+
const copyIdBtn = workflowItem.querySelector('.copy-id-btn');
|
|
1467
|
+
const copyPromptBtn = workflowItem.querySelector('.copy-prompt-btn');
|
|
1468
|
+
|
|
1469
|
+
copyIdBtn.addEventListener('click', () => {
|
|
1470
|
+
navigator.clipboard.writeText(workflowData.workflow.sessionId).then(() => {
|
|
1471
|
+
copyIdBtn.textContent = 'Copied!';
|
|
1472
|
+
copyIdBtn.style.background = '#4CAF50';
|
|
1473
|
+
setTimeout(() => {
|
|
1474
|
+
copyIdBtn.textContent = 'Copy ID';
|
|
1475
|
+
copyIdBtn.style.background = '#2196F3';
|
|
1476
|
+
}, 2000);
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
// copyPromptBtn is now handled by the shared event listener system with data-id and data-port attributes
|
|
1481
|
+
} else {
|
|
1482
|
+
// Fallback when server not available
|
|
1483
|
+
workflowItem = document.createElement('div');
|
|
1484
|
+
workflowItem.className = 'recording-item';
|
|
1485
|
+
workflowItem.innerHTML = `
|
|
1486
|
+
<div class="recording-info">
|
|
1487
|
+
<div style="font-weight: bold; margin-bottom: 4px; color: #ff6b6b;">
|
|
1488
|
+
Server not available - JSON downloaded
|
|
1489
|
+
</div>
|
|
1490
|
+
<div style="color: #666; font-size: 11px;">
|
|
1491
|
+
${workflowData.workflow.length} actions recorded
|
|
1492
|
+
</div>
|
|
1493
|
+
</div>
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Add the item to the container
|
|
1498
|
+
workflowRecordingsContainer.appendChild(workflowItem);
|
|
1499
|
+
workflowRecordingsList.style.display = 'block';
|
|
1500
|
+
|
|
1501
|
+
// Reload the list to show all recordings
|
|
1502
|
+
setTimeout(() => {
|
|
1503
|
+
loadWorkflowRecordings();
|
|
1504
|
+
}, 500);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
console.error('Error stopping workflow recording:', error);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// Setup include logs checkbox
|
|
1516
|
+
if (includeLogsCheckbox) {
|
|
1517
|
+
includeLogsCheckbox.addEventListener('change', () => {
|
|
1518
|
+
chrome.storage.local.set({ includeLogsInExport: includeLogsCheckbox.checked });
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Setup save restore point button
|
|
1523
|
+
const saveRestorePointBtn = document.getElementById('saveRestorePointBtn');
|
|
1524
|
+
const restorePointsList = document.getElementById('restorePointsList');
|
|
1525
|
+
const restorePointsContainer = document.getElementById('restorePointsContainer');
|
|
1526
|
+
|
|
1527
|
+
if (saveRestorePointBtn) {
|
|
1528
|
+
saveRestorePointBtn.addEventListener('click', async () => {
|
|
1529
|
+
try {
|
|
1530
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1531
|
+
|
|
1532
|
+
if (!tab || !tab.id) {
|
|
1533
|
+
console.error('No active tab found');
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Send message to content script to create restore point
|
|
1538
|
+
chrome.tabs.sendMessage(tab.id, {
|
|
1539
|
+
action: 'createRestorePoint'
|
|
1540
|
+
}, (response) => {
|
|
1541
|
+
if (chrome.runtime.lastError) {
|
|
1542
|
+
console.error('Error creating restore point:', chrome.runtime.lastError);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
if (response && response.success) {
|
|
1547
|
+
console.log('Restore point created:', response.restorePointId);
|
|
1548
|
+
// Refresh restore points list
|
|
1549
|
+
loadRestorePoints();
|
|
1550
|
+
} else {
|
|
1551
|
+
console.error('Failed to create restore point:', response);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
console.error('Error creating restore point:', error);
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Function to load and display restore points
|
|
1561
|
+
async function loadRestorePoints() {
|
|
1562
|
+
// Get current workflow ID from storage
|
|
1563
|
+
chrome.storage.local.get(['currentWorkflowId'], async (result) => {
|
|
1564
|
+
if (!result.currentWorkflowId) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Check server for restore points
|
|
1569
|
+
const ports = CONFIG_PORTS;
|
|
1570
|
+
for (const port of ports) {
|
|
1571
|
+
try {
|
|
1572
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/restore-points/${result.currentWorkflowId}`, {
|
|
1573
|
+
method: 'GET',
|
|
1574
|
+
mode: 'cors'
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
if (response.ok) {
|
|
1578
|
+
const restorePoints = await response.json();
|
|
1579
|
+
|
|
1580
|
+
if (restorePoints.length > 0) {
|
|
1581
|
+
restorePointsList.style.display = 'block';
|
|
1582
|
+
restorePointsContainer.innerHTML = '';
|
|
1583
|
+
|
|
1584
|
+
restorePoints.forEach(rp => {
|
|
1585
|
+
const item = document.createElement('div');
|
|
1586
|
+
item.className = 'restore-point-item';
|
|
1587
|
+
item.innerHTML = `
|
|
1588
|
+
<div class="restore-point-info">
|
|
1589
|
+
<div style="font-weight: bold;">Action ${rp.actionIndex}</div>
|
|
1590
|
+
<div style="font-size: 11px; color: #666;">
|
|
1591
|
+
${new Date(rp.timestamp).toLocaleString()}
|
|
1592
|
+
</div>
|
|
1593
|
+
<div style="font-size: 11px; color: #666;">
|
|
1594
|
+
${rp.title}
|
|
1595
|
+
</div>
|
|
1596
|
+
</div>
|
|
1597
|
+
<div>
|
|
1598
|
+
<button class="restore-btn" data-restore-id="${rp.id}">Restore</button>
|
|
1599
|
+
<button class="delete-btn" data-restore-id="${rp.id}" style="padding: 4px 8px; font-size: 10px;">Delete</button>
|
|
1600
|
+
</div>
|
|
1601
|
+
`;
|
|
1602
|
+
restorePointsContainer.appendChild(item);
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// Add event listeners to restore buttons
|
|
1606
|
+
document.querySelectorAll('.restore-btn').forEach(btn => {
|
|
1607
|
+
btn.addEventListener('click', async (e) => {
|
|
1608
|
+
const restoreId = e.target.getAttribute('data-restore-id');
|
|
1609
|
+
await restoreFromPoint(restoreId);
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// Add event listeners to delete buttons
|
|
1614
|
+
document.querySelectorAll('.restore-point-item .delete-btn').forEach(btn => {
|
|
1615
|
+
btn.addEventListener('click', async (e) => {
|
|
1616
|
+
const restoreId = e.target.getAttribute('data-restore-id');
|
|
1617
|
+
if (confirm('Delete this restore point?')) {
|
|
1618
|
+
await deleteRestorePoint(restoreId);
|
|
1619
|
+
loadRestorePoints(); // Refresh list
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
} else {
|
|
1624
|
+
restorePointsList.style.display = 'none';
|
|
1625
|
+
}
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
// Try next port
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// Function to restore from a restore point
|
|
1636
|
+
async function restoreFromPoint(restorePointId) {
|
|
1637
|
+
try {
|
|
1638
|
+
// First get the restore point data from server
|
|
1639
|
+
const ports = CONFIG_PORTS;
|
|
1640
|
+
let restorePointData = null;
|
|
1641
|
+
|
|
1642
|
+
for (const port of ports) {
|
|
1643
|
+
try {
|
|
1644
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/restore-point/${restorePointId}`, {
|
|
1645
|
+
method: 'GET',
|
|
1646
|
+
mode: 'cors'
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
if (response.ok) {
|
|
1650
|
+
restorePointData = await response.json();
|
|
1651
|
+
break;
|
|
1652
|
+
}
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
// Try next port
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (!restorePointData) {
|
|
1659
|
+
console.error('Could not retrieve restore point data');
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1664
|
+
|
|
1665
|
+
if (!tab || !tab.id) {
|
|
1666
|
+
console.error('No active tab found');
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Send message to content script to restore
|
|
1671
|
+
chrome.tabs.sendMessage(tab.id, {
|
|
1672
|
+
action: 'restoreFromPoint',
|
|
1673
|
+
restorePointData: restorePointData
|
|
1674
|
+
}, (response) => {
|
|
1675
|
+
if (chrome.runtime.lastError) {
|
|
1676
|
+
console.error('Error restoring from point:', chrome.runtime.lastError);
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (response && response.success) {
|
|
1681
|
+
console.log('Successfully restored from point');
|
|
1682
|
+
} else {
|
|
1683
|
+
console.error('Failed to restore from point:', response);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
console.error('Error restoring from point:', error);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// Function to delete a restore point
|
|
1692
|
+
async function deleteRestorePoint(restorePointId) {
|
|
1693
|
+
const ports = CONFIG_PORTS;
|
|
1694
|
+
|
|
1695
|
+
for (const port of ports) {
|
|
1696
|
+
try {
|
|
1697
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/restore-point/${restorePointId}`, {
|
|
1698
|
+
method: 'DELETE',
|
|
1699
|
+
mode: 'cors'
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
if (response.ok) {
|
|
1703
|
+
console.log('Restore point deleted');
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
// Try next port
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Setup screenshot settings handlers
|
|
1713
|
+
const enableScreenshotsCheckbox = document.getElementById('enableScreenshotsCheckbox');
|
|
1714
|
+
const screenshotSettingsDiv = document.getElementById('screenshotSettings');
|
|
1715
|
+
const qualitySlider = document.getElementById('screenshotQuality');
|
|
1716
|
+
const qualityValue = document.getElementById('qualityValue');
|
|
1717
|
+
|
|
1718
|
+
if (enableScreenshotsCheckbox) {
|
|
1719
|
+
enableScreenshotsCheckbox.addEventListener('change', () => {
|
|
1720
|
+
if (screenshotSettingsDiv) {
|
|
1721
|
+
screenshotSettingsDiv.style.display = enableScreenshotsCheckbox.checked ? 'block' : 'none';
|
|
1722
|
+
}
|
|
1723
|
+
// Save checkbox state with error handling
|
|
1724
|
+
chrome.storage.local.set({ workflowScreenshotsEnabled: enableScreenshotsCheckbox.checked }, () => {
|
|
1725
|
+
if (chrome.runtime.lastError) {
|
|
1726
|
+
console.error('Failed to save screenshot enabled setting:', chrome.runtime.lastError);
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// Screenshot format dropdown
|
|
1733
|
+
const screenshotFormat = document.getElementById('screenshotFormat');
|
|
1734
|
+
if (screenshotFormat) {
|
|
1735
|
+
screenshotFormat.addEventListener('change', () => {
|
|
1736
|
+
chrome.storage.local.set({ workflowScreenshotFormat: screenshotFormat.value }, () => {
|
|
1737
|
+
if (chrome.runtime.lastError) {
|
|
1738
|
+
console.error('Failed to save screenshot format:', chrome.runtime.lastError);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// Screenshot quality slider with debouncing
|
|
1745
|
+
if (qualitySlider && qualityValue) {
|
|
1746
|
+
qualitySlider.addEventListener('input', () => {
|
|
1747
|
+
// Update display immediately
|
|
1748
|
+
qualityValue.textContent = qualitySlider.value;
|
|
1749
|
+
|
|
1750
|
+
// Debounce the save operation
|
|
1751
|
+
clearTimeout(screenshotQualityDebounceTimer);
|
|
1752
|
+
screenshotQualityDebounceTimer = setTimeout(() => {
|
|
1753
|
+
const quality = parseInt(qualitySlider.value);
|
|
1754
|
+
chrome.storage.local.set({ workflowScreenshotQuality: quality }, () => {
|
|
1755
|
+
if (chrome.runtime.lastError) {
|
|
1756
|
+
console.error('Failed to save screenshot quality:', chrome.runtime.lastError);
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
}, 500);
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// Screenshot max width with validation
|
|
1764
|
+
const screenshotMaxWidth = document.getElementById('screenshotMaxWidth');
|
|
1765
|
+
if (screenshotMaxWidth) {
|
|
1766
|
+
screenshotMaxWidth.addEventListener('blur', () => {
|
|
1767
|
+
let value = screenshotMaxWidth.value.trim();
|
|
1768
|
+
|
|
1769
|
+
if (value === '') {
|
|
1770
|
+
value = null;
|
|
1771
|
+
} else {
|
|
1772
|
+
value = parseInt(value);
|
|
1773
|
+
if (isNaN(value) || value < 100 || value > 10000) {
|
|
1774
|
+
console.warn('Invalid max width value, must be between 100-10000');
|
|
1775
|
+
screenshotMaxWidth.value = '';
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
chrome.storage.local.set({ workflowScreenshotMaxWidth: value }, () => {
|
|
1781
|
+
if (chrome.runtime.lastError) {
|
|
1782
|
+
console.error('Failed to save screenshot max width:', chrome.runtime.lastError);
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
// Copy button handler is now inline in the workflow item creation
|
|
1790
|
+
|
|
1791
|
+
// Set up "View Past Recordings" links
|
|
1792
|
+
const viewWorkflowRecordingsLink = document.getElementById('viewWorkflowRecordings');
|
|
1793
|
+
const viewScreenRecordingsLink = document.getElementById('viewScreenRecordings');
|
|
1794
|
+
|
|
1795
|
+
if (viewWorkflowRecordingsLink) {
|
|
1796
|
+
viewWorkflowRecordingsLink.addEventListener('click', (e) => {
|
|
1797
|
+
e.preventDefault();
|
|
1798
|
+
const workflowRecordingsList = document.getElementById('workflowRecordingsList');
|
|
1799
|
+
if (workflowRecordingsList) {
|
|
1800
|
+
if (workflowRecordingsList.style.display === 'none' || workflowRecordingsList.style.display === '') {
|
|
1801
|
+
workflowRecordingsList.style.display = 'block';
|
|
1802
|
+
loadWorkflowRecordings();
|
|
1803
|
+
viewWorkflowRecordingsLink.textContent = 'Hide Recordings';
|
|
1804
|
+
} else {
|
|
1805
|
+
workflowRecordingsList.style.display = 'none';
|
|
1806
|
+
viewWorkflowRecordingsLink.textContent = 'View Past Recordings';
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
if (viewScreenRecordingsLink) {
|
|
1813
|
+
viewScreenRecordingsLink.addEventListener('click', (e) => {
|
|
1814
|
+
e.preventDefault();
|
|
1815
|
+
const recordingsList = document.getElementById('recordingsList');
|
|
1816
|
+
if (recordingsList) {
|
|
1817
|
+
if (recordingsList.style.display === 'none') {
|
|
1818
|
+
loadScreenRecordings();
|
|
1819
|
+
recordingsList.style.display = 'block';
|
|
1820
|
+
viewScreenRecordingsLink.textContent = 'Hide Recordings';
|
|
1821
|
+
} else {
|
|
1822
|
+
recordingsList.style.display = 'none';
|
|
1823
|
+
viewScreenRecordingsLink.textContent = 'View Past Recordings';
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/*
|
|
1830
|
+
* SNAPSHOT FEATURE DISABLED (2025-10-01)
|
|
1831
|
+
* View snapshots link handler disabled - see SNAPSHOT_FEATURE_DISABLED.md
|
|
1832
|
+
*/
|
|
1833
|
+
/*
|
|
1834
|
+
const viewSnapshotsLink = document.getElementById('viewSnapshotsLink');
|
|
1835
|
+
if (viewSnapshotsLink) {
|
|
1836
|
+
viewSnapshotsLink.addEventListener('click', (e) => {
|
|
1837
|
+
e.preventDefault();
|
|
1838
|
+
const snapshotsList = document.getElementById('snapshotsList');
|
|
1839
|
+
if (snapshotsList) {
|
|
1840
|
+
if (snapshotsList.style.display === 'none') {
|
|
1841
|
+
loadSnapshots();
|
|
1842
|
+
snapshotsList.style.display = 'block';
|
|
1843
|
+
viewSnapshotsLink.textContent = 'Hide Snapshots';
|
|
1844
|
+
} else {
|
|
1845
|
+
snapshotsList.style.display = 'none';
|
|
1846
|
+
viewSnapshotsLink.textContent = 'View Past Snapshots';
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
*/
|
|
1852
|
+
|
|
1853
|
+
// Don't load recordings on startup - only when user clicks the link
|
|
1854
|
+
|
|
1855
|
+
// Cleanup debounce timer on unload
|
|
1856
|
+
window.addEventListener('beforeunload', () => {
|
|
1857
|
+
if (screenshotQualityDebounceTimer) {
|
|
1858
|
+
clearTimeout(screenshotQualityDebounceTimer);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
// Function to load and display workflow recordings
|
|
1864
|
+
async function loadWorkflowRecordings() {
|
|
1865
|
+
const workflowRecordingsList = document.getElementById('workflowRecordingsList');
|
|
1866
|
+
const workflowRecordingsContainer = document.getElementById('workflowRecordingsContainer');
|
|
1867
|
+
|
|
1868
|
+
if (!workflowRecordingsList || !workflowRecordingsContainer) {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Clear existing items
|
|
1873
|
+
workflowRecordingsContainer.innerHTML = '';
|
|
1874
|
+
|
|
1875
|
+
// Check server for workflow recordings
|
|
1876
|
+
const ports = CONFIG_PORTS;
|
|
1877
|
+
for (const port of ports) {
|
|
1878
|
+
try {
|
|
1879
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recordings`);
|
|
1880
|
+
if (response.ok) {
|
|
1881
|
+
const data = await response.json();
|
|
1882
|
+
console.log('Loaded workflow recordings from port', port, ':', data);
|
|
1883
|
+
|
|
1884
|
+
// Handle both data.recordings and direct array response
|
|
1885
|
+
const recordings = data.recordings || data;
|
|
1886
|
+
|
|
1887
|
+
if (Array.isArray(recordings) && recordings.length > 0) {
|
|
1888
|
+
// Display each workflow recording
|
|
1889
|
+
recordings.forEach(recording => {
|
|
1890
|
+
const workflowItem = document.createElement('div');
|
|
1891
|
+
workflowItem.className = 'recording-item';
|
|
1892
|
+
|
|
1893
|
+
const displayName = recording.name || `Workflow ${recording.session_id}`;
|
|
1894
|
+
const date = new Date(recording.timestamp);
|
|
1895
|
+
const formattedDate = date.toLocaleString();
|
|
1896
|
+
|
|
1897
|
+
workflowItem.innerHTML = `
|
|
1898
|
+
<div class="recording-info">
|
|
1899
|
+
<div style="font-weight: bold; margin-bottom: 4px;">${displayName}</div>
|
|
1900
|
+
<div style="color: #666; font-size: 11px;">
|
|
1901
|
+
${recording.total_actions} actions • ${formattedDate}
|
|
1902
|
+
</div>
|
|
1903
|
+
${recording.screenshot_settings ? '<div style="color: #2196F3; font-size: 10px;">📸 Screenshots included</div>' : ''}
|
|
1904
|
+
</div>
|
|
1905
|
+
<div class="recording-buttons">
|
|
1906
|
+
<button class="play-btn" data-session-id="${recording.session_id}" style="padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
1907
|
+
▶ Play
|
|
1908
|
+
</button>
|
|
1909
|
+
<button class="get-prompt-btn" data-session-id="${recording.session_id}" data-name="${recording.name || ''}" style="padding: 4px 8px; background: #2196F3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
1910
|
+
Get Prompt
|
|
1911
|
+
</button>
|
|
1912
|
+
<button class="delete-btn" data-recording-id="${recording.id || recording.session_id}" style="padding: 4px 8px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
1913
|
+
Delete
|
|
1914
|
+
</button>
|
|
1915
|
+
</div>
|
|
1916
|
+
`;
|
|
1917
|
+
|
|
1918
|
+
// Add event listeners
|
|
1919
|
+
const playBtn = workflowItem.querySelector('.play-btn');
|
|
1920
|
+
const getPromptBtn = workflowItem.querySelector('.get-prompt-btn');
|
|
1921
|
+
const deleteBtn = workflowItem.querySelector('.delete-btn');
|
|
1922
|
+
|
|
1923
|
+
playBtn.addEventListener('click', () => playWorkflow(recording.session_id));
|
|
1924
|
+
getPromptBtn.addEventListener('click', () => {
|
|
1925
|
+
const sessionId = recording.session_id;
|
|
1926
|
+
const name = recording.name;
|
|
1927
|
+
const port = window.CHROMEDEBUG_CONFIG?.ports?.[0] || '3001';
|
|
1928
|
+
let prompt;
|
|
1929
|
+
|
|
1930
|
+
if (name) {
|
|
1931
|
+
// Use name-based prompt similar to screen recording format
|
|
1932
|
+
prompt = `Please use the get_workflow_recording function in Chrome Debug to load the workflow recording "${name}" (session: ${sessionId}) that was recorded on port ${port}.`;
|
|
1933
|
+
} else {
|
|
1934
|
+
// Use session ID based prompt
|
|
1935
|
+
prompt = `Please use the get_workflow_recording function in Chrome Debug to load the workflow recording "${sessionId}" that was recorded on port ${port}.`;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
navigator.clipboard.writeText(prompt).then(() => {
|
|
1939
|
+
getPromptBtn.textContent = 'Copied!';
|
|
1940
|
+
getPromptBtn.style.background = '#4CAF50';
|
|
1941
|
+
setTimeout(() => {
|
|
1942
|
+
getPromptBtn.textContent = 'Get Prompt';
|
|
1943
|
+
getPromptBtn.style.background = '#2196F3';
|
|
1944
|
+
}, 2000);
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
deleteBtn.addEventListener('click', () => {
|
|
1948
|
+
console.log('Delete clicked for recording:', recording);
|
|
1949
|
+
const recordingId = recording.id || recording.session_id;
|
|
1950
|
+
if (!recordingId) {
|
|
1951
|
+
console.error('No ID found for recording:', recording);
|
|
1952
|
+
alert('Cannot delete: Recording ID not found');
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
deleteWorkflowRecording(recordingId);
|
|
1956
|
+
});
|
|
1957
|
+
|
|
1958
|
+
workflowRecordingsContainer.appendChild(workflowItem);
|
|
1959
|
+
});
|
|
1960
|
+
} else {
|
|
1961
|
+
// Show "No recordings found" message
|
|
1962
|
+
const noRecordingsMsg = document.createElement('div');
|
|
1963
|
+
noRecordingsMsg.style.cssText = 'color: #666; font-size: 13px; text-align: center; padding: 20px;';
|
|
1964
|
+
noRecordingsMsg.textContent = 'No workflow recordings found';
|
|
1965
|
+
workflowRecordingsContainer.appendChild(noRecordingsMsg);
|
|
1966
|
+
}
|
|
1967
|
+
break; // Found working server
|
|
1968
|
+
}
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
console.error(`Failed to load recordings from port ${port}:`, error);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// If we couldn't connect to any server, show an error message
|
|
1975
|
+
if (workflowRecordingsContainer.innerHTML === '') {
|
|
1976
|
+
const errorMsg = document.createElement('div');
|
|
1977
|
+
errorMsg.style.cssText = 'color: #f44336; font-size: 13px; text-align: center; padding: 20px;';
|
|
1978
|
+
errorMsg.textContent = 'Could not connect to Chrome Debug server';
|
|
1979
|
+
workflowRecordingsContainer.appendChild(errorMsg);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// Function to play a workflow
|
|
1984
|
+
async function playWorkflow(sessionId) {
|
|
1985
|
+
console.log('Playing workflow:', sessionId);
|
|
1986
|
+
|
|
1987
|
+
try {
|
|
1988
|
+
// Get the current active tab
|
|
1989
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
1990
|
+
|
|
1991
|
+
if (!tab || !tab.id) {
|
|
1992
|
+
console.error('No active tab found');
|
|
1993
|
+
alert('Please open a tab to play the workflow');
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// Get workflow data from server
|
|
1998
|
+
let workflowData = null;
|
|
1999
|
+
const ports = CONFIG_PORTS;
|
|
2000
|
+
for (const port of ports) {
|
|
2001
|
+
try {
|
|
2002
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording/${sessionId}`);
|
|
2003
|
+
if (response.ok) {
|
|
2004
|
+
workflowData = await response.json();
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
console.error(`Failed to get workflow from port ${port}:`, error);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (!workflowData) {
|
|
2013
|
+
alert('Failed to load workflow data');
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// Check if we need to navigate to the workflow URL
|
|
2018
|
+
const currentUrl = tab.url;
|
|
2019
|
+
if (workflowData.url && currentUrl !== workflowData.url) {
|
|
2020
|
+
const navigate = confirm(`This workflow was recorded on:\n${workflowData.url}\n\nNavigate there now?`);
|
|
2021
|
+
if (navigate) {
|
|
2022
|
+
await chrome.tabs.update(tab.id, { url: workflowData.url });
|
|
2023
|
+
// Wait for navigation to complete
|
|
2024
|
+
await new Promise(resolve => {
|
|
2025
|
+
chrome.tabs.onUpdated.addListener(function listener(tabId, info) {
|
|
2026
|
+
if (tabId === tab.id && info.status === 'complete') {
|
|
2027
|
+
chrome.tabs.onUpdated.removeListener(listener);
|
|
2028
|
+
resolve();
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
});
|
|
2032
|
+
// Give the page a moment to settle
|
|
2033
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Send workflow to content script for playback
|
|
2038
|
+
chrome.tabs.sendMessage(tab.id, {
|
|
2039
|
+
action: 'playWorkflow',
|
|
2040
|
+
workflow: workflowData,
|
|
2041
|
+
actions: workflowData.actions
|
|
2042
|
+
}, (response) => {
|
|
2043
|
+
if (chrome.runtime.lastError) {
|
|
2044
|
+
console.error('Error playing workflow:', chrome.runtime.lastError);
|
|
2045
|
+
// Try to inject content script if it's not already loaded
|
|
2046
|
+
chrome.scripting.executeScript({
|
|
2047
|
+
target: { tabId: tab.id },
|
|
2048
|
+
files: ['content.js']
|
|
2049
|
+
}, () => {
|
|
2050
|
+
if (chrome.runtime.lastError) {
|
|
2051
|
+
console.error('Failed to inject content script:', chrome.runtime.lastError);
|
|
2052
|
+
alert('Failed to start workflow playback. Please reload the page and try again.');
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
// Try sending message again after injection
|
|
2056
|
+
setTimeout(() => {
|
|
2057
|
+
chrome.tabs.sendMessage(tab.id, {
|
|
2058
|
+
action: 'playWorkflow',
|
|
2059
|
+
workflow: workflowData,
|
|
2060
|
+
actions: workflowData.actions
|
|
2061
|
+
});
|
|
2062
|
+
}, 100);
|
|
2063
|
+
});
|
|
2064
|
+
} else if (response && response.success) {
|
|
2065
|
+
console.log('Workflow playback started');
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
console.error('Error playing workflow:', error);
|
|
2070
|
+
alert('Error playing workflow: ' + error.message);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Function to export a workflow
|
|
2075
|
+
async function exportWorkflow(sessionId) {
|
|
2076
|
+
// Check server for workflow data
|
|
2077
|
+
const ports = CONFIG_PORTS;
|
|
2078
|
+
for (const port of ports) {
|
|
2079
|
+
try {
|
|
2080
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording/${sessionId}`);
|
|
2081
|
+
if (response.ok) {
|
|
2082
|
+
const data = await response.json();
|
|
2083
|
+
|
|
2084
|
+
// Create and download JSON file
|
|
2085
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
2086
|
+
const url = URL.createObjectURL(blob);
|
|
2087
|
+
const a = document.createElement('a');
|
|
2088
|
+
a.href = url;
|
|
2089
|
+
a.download = `workflow_${sessionId}_${Date.now()}.json`;
|
|
2090
|
+
document.body.appendChild(a);
|
|
2091
|
+
a.click();
|
|
2092
|
+
document.body.removeChild(a);
|
|
2093
|
+
URL.revokeObjectURL(url);
|
|
2094
|
+
|
|
2095
|
+
break; // Found working server
|
|
2096
|
+
}
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
console.error(`Failed to export workflow from port ${port}:`, error);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// Function to delete a workflow recording
|
|
2104
|
+
async function deleteWorkflowRecording(recordingId) {
|
|
2105
|
+
if (!confirm('Are you sure you want to delete this workflow recording?')) {
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// Delete from server
|
|
2110
|
+
const ports = CONFIG_PORTS;
|
|
2111
|
+
for (const port of ports) {
|
|
2112
|
+
try {
|
|
2113
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/workflow-recording/${recordingId}`, {
|
|
2114
|
+
method: 'DELETE'
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
if (response.ok) {
|
|
2118
|
+
// Reload the list
|
|
2119
|
+
loadWorkflowRecordings();
|
|
2120
|
+
break; // Found working server
|
|
2121
|
+
}
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
console.error(`Failed to delete workflow from port ${port}:`, error);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Function to load and display screen recordings
|
|
2129
|
+
async function loadScreenRecordings() {
|
|
2130
|
+
const recordingsList = document.getElementById('recordingsList');
|
|
2131
|
+
const recordingsContainer = document.getElementById('recordingsContainer');
|
|
2132
|
+
|
|
2133
|
+
if (!recordingsList || !recordingsContainer) {
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Clear existing items
|
|
2138
|
+
recordingsContainer.innerHTML = '';
|
|
2139
|
+
|
|
2140
|
+
// Check server for screen recordings
|
|
2141
|
+
const ports = CONFIG_PORTS;
|
|
2142
|
+
for (const port of ports) {
|
|
2143
|
+
try {
|
|
2144
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/frame-sessions`);
|
|
2145
|
+
if (response.ok) {
|
|
2146
|
+
const data = await response.json();
|
|
2147
|
+
if (data && data.length > 0) {
|
|
2148
|
+
recordingsList.style.display = 'block';
|
|
2149
|
+
|
|
2150
|
+
// Display each screen recording
|
|
2151
|
+
data.forEach(recording => {
|
|
2152
|
+
const recordingItem = document.createElement('div');
|
|
2153
|
+
recordingItem.className = 'recording-item';
|
|
2154
|
+
|
|
2155
|
+
const date = new Date(recording.timestamp);
|
|
2156
|
+
const formattedDate = date.toLocaleString();
|
|
2157
|
+
|
|
2158
|
+
// Use session name if available, otherwise use session ID
|
|
2159
|
+
console.log('Recording data:', recording);
|
|
2160
|
+
console.log('Recording name:', recording.name);
|
|
2161
|
+
const displayName = recording.name || `Recording ${recording.sessionId}`;
|
|
2162
|
+
console.log('Display name:', displayName);
|
|
2163
|
+
|
|
2164
|
+
recordingItem.innerHTML = `
|
|
2165
|
+
<div class="recording-info">
|
|
2166
|
+
<div style="font-weight: bold; margin-bottom: 4px;">${displayName}</div>
|
|
2167
|
+
<div style="color: #666; font-size: 11px;">
|
|
2168
|
+
${recording.totalFrames} frames • ${formattedDate}
|
|
2169
|
+
</div>
|
|
2170
|
+
</div>
|
|
2171
|
+
<div class="recording-buttons">
|
|
2172
|
+
<button class="copy-id-btn" data-session-id="${recording.sessionId}" style="padding: 4px 8px; background: #2196F3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
2173
|
+
Copy ID
|
|
2174
|
+
</button>
|
|
2175
|
+
<button class="delete-btn" data-session-id="${recording.sessionId}" style="padding: 4px 8px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
2176
|
+
Delete
|
|
2177
|
+
</button>
|
|
2178
|
+
</div>
|
|
2179
|
+
`;
|
|
2180
|
+
|
|
2181
|
+
// Add event listeners
|
|
2182
|
+
const copyIdBtn = recordingItem.querySelector('.copy-id-btn');
|
|
2183
|
+
const deleteBtn = recordingItem.querySelector('.delete-btn');
|
|
2184
|
+
|
|
2185
|
+
copyIdBtn.addEventListener('click', () => {
|
|
2186
|
+
navigator.clipboard.writeText(recording.sessionId).then(() => {
|
|
2187
|
+
copyIdBtn.textContent = 'Copied!';
|
|
2188
|
+
copyIdBtn.style.background = '#4CAF50';
|
|
2189
|
+
setTimeout(() => {
|
|
2190
|
+
copyIdBtn.textContent = 'Copy ID';
|
|
2191
|
+
copyIdBtn.style.background = '#2196F3';
|
|
2192
|
+
}, 2000);
|
|
2193
|
+
});
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
deleteBtn.addEventListener('click', async () => {
|
|
2197
|
+
if (confirm('Are you sure you want to delete this screen recording?')) {
|
|
2198
|
+
try {
|
|
2199
|
+
const deleteResponse = await fetch(`http://localhost:${port}/chromedebug/recording/${recording.sessionId}`, {
|
|
2200
|
+
method: 'DELETE'
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
if (deleteResponse.ok) {
|
|
2204
|
+
// Reload the list
|
|
2205
|
+
loadScreenRecordings();
|
|
2206
|
+
}
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
console.error('Failed to delete recording:', error);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
recordingsContainer.appendChild(recordingItem);
|
|
2214
|
+
});
|
|
2215
|
+
} else {
|
|
2216
|
+
recordingsList.style.display = 'none';
|
|
2217
|
+
}
|
|
2218
|
+
break; // Found working server
|
|
2219
|
+
}
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
console.error(`Failed to load screen recordings from port ${port}:`, error);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
/*
|
|
2227
|
+
* SNAPSHOT FEATURE DISABLED (2025-10-01)
|
|
2228
|
+
*
|
|
2229
|
+
* This function loaded and displayed snapshot list in the popup UI.
|
|
2230
|
+
*
|
|
2231
|
+
* WHY DISABLED:
|
|
2232
|
+
* - Snapshots without console logs are just screenshots (users can do this natively)
|
|
2233
|
+
* - Console log capture requires always-on monitoring (privacy concern)
|
|
2234
|
+
* - Core value proposition (screenshot + searchable logs) cannot be achieved cleanly
|
|
2235
|
+
*
|
|
2236
|
+
* TO RE-ENABLE:
|
|
2237
|
+
* 1. Implement privacy-conscious always-on log monitoring
|
|
2238
|
+
* 2. Uncomment this function
|
|
2239
|
+
* 3. Re-enable backend /chromedebug/snapshots endpoint
|
|
2240
|
+
* 4. Re-enable snapshot UI in popup.html
|
|
2241
|
+
*
|
|
2242
|
+
* See: SNAPSHOT_FEATURE_DISABLED.md for full explanation
|
|
2243
|
+
*/
|
|
2244
|
+
/*
|
|
2245
|
+
async function loadSnapshots() {
|
|
2246
|
+
const snapshotsList = document.getElementById('snapshotsList');
|
|
2247
|
+
const snapshotsContainer = document.getElementById('snapshotsContainer');
|
|
2248
|
+
|
|
2249
|
+
if (!snapshotsList || !snapshotsContainer) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// Clear existing items
|
|
2254
|
+
snapshotsContainer.innerHTML = '';
|
|
2255
|
+
|
|
2256
|
+
// Check server for snapshots
|
|
2257
|
+
const ports = CONFIG_PORTS;
|
|
2258
|
+
for (const port of ports) {
|
|
2259
|
+
try {
|
|
2260
|
+
const response = await fetch(`http://localhost:${port}/chromedebug/snapshots`);
|
|
2261
|
+
if (response.ok) {
|
|
2262
|
+
const data = await response.json();
|
|
2263
|
+
if (data && data.snapshots && data.snapshots.length > 0) {
|
|
2264
|
+
snapshotsList.style.display = 'block';
|
|
2265
|
+
|
|
2266
|
+
// Display each snapshot
|
|
2267
|
+
data.snapshots.forEach(snapshot => {
|
|
2268
|
+
const snapshotItem = document.createElement('div');
|
|
2269
|
+
snapshotItem.className = 'recording-item';
|
|
2270
|
+
|
|
2271
|
+
const date = new Date(snapshot.timestamp);
|
|
2272
|
+
const formattedDate = date.toLocaleString();
|
|
2273
|
+
|
|
2274
|
+
// Build note display
|
|
2275
|
+
const noteDisplay = snapshot.user_note
|
|
2276
|
+
? `<div style="color: #666; font-size: 11px; margin-top: 2px;">Note: ${snapshot.user_note}</div>`
|
|
2277
|
+
: '';
|
|
2278
|
+
|
|
2279
|
+
snapshotItem.innerHTML = `
|
|
2280
|
+
<div class="recording-info">
|
|
2281
|
+
<div style="font-weight: bold; margin-bottom: 4px;">Snapshot ${snapshot.session_id}</div>
|
|
2282
|
+
<div style="color: #666; font-size: 11px;">
|
|
2283
|
+
${formattedDate}
|
|
2284
|
+
</div>
|
|
2285
|
+
${noteDisplay}
|
|
2286
|
+
</div>
|
|
2287
|
+
<div class="recording-buttons">
|
|
2288
|
+
<button class="copy-id-btn" data-session-id="${snapshot.session_id}" style="padding: 4px 8px; background: #2196F3; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
2289
|
+
Copy ID
|
|
2290
|
+
</button>
|
|
2291
|
+
<button class="copy-prompt-btn" data-session-id="${snapshot.session_id}" style="padding: 4px 8px; background: #9C27B0; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
2292
|
+
Copy Prompt
|
|
2293
|
+
</button>
|
|
2294
|
+
<button class="view-btn" data-session-id="${snapshot.session_id}" style="padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
2295
|
+
View
|
|
2296
|
+
</button>
|
|
2297
|
+
<button class="delete-btn" data-session-id="${snapshot.session_id}" style="padding: 4px 8px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">
|
|
2298
|
+
Delete
|
|
2299
|
+
</button>
|
|
2300
|
+
</div>
|
|
2301
|
+
`;
|
|
2302
|
+
|
|
2303
|
+
// Add event listeners
|
|
2304
|
+
const copyIdBtn = snapshotItem.querySelector('.copy-id-btn');
|
|
2305
|
+
const copyPromptBtn = snapshotItem.querySelector('.copy-prompt-btn');
|
|
2306
|
+
const viewBtn = snapshotItem.querySelector('.view-btn');
|
|
2307
|
+
const deleteBtn = snapshotItem.querySelector('.delete-btn');
|
|
2308
|
+
|
|
2309
|
+
copyIdBtn.addEventListener('click', () => {
|
|
2310
|
+
navigator.clipboard.writeText(snapshot.session_id).then(() => {
|
|
2311
|
+
copyIdBtn.textContent = 'Copied!';
|
|
2312
|
+
copyIdBtn.style.background = '#4CAF50';
|
|
2313
|
+
setTimeout(() => {
|
|
2314
|
+
copyIdBtn.textContent = 'Copy ID';
|
|
2315
|
+
copyIdBtn.style.background = '#2196F3';
|
|
2316
|
+
}, 2000);
|
|
2317
|
+
});
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
copyPromptBtn.addEventListener('click', () => {
|
|
2321
|
+
const prompt = `Please analyze this snapshot: ${snapshot.session_id}`;
|
|
2322
|
+
navigator.clipboard.writeText(prompt).then(() => {
|
|
2323
|
+
copyPromptBtn.textContent = 'Copied!';
|
|
2324
|
+
copyPromptBtn.style.background = '#4CAF50';
|
|
2325
|
+
setTimeout(() => {
|
|
2326
|
+
copyPromptBtn.textContent = 'Copy Prompt';
|
|
2327
|
+
copyPromptBtn.style.background = '#9C27B0';
|
|
2328
|
+
}, 2000);
|
|
2329
|
+
});
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
viewBtn.addEventListener('click', async () => {
|
|
2333
|
+
try {
|
|
2334
|
+
// Snapshots are single-frame recordings, so get frame 0
|
|
2335
|
+
const frameResponse = await fetch(`http://localhost:${port}/chromedebug/frame/${snapshot.session_id}/0`);
|
|
2336
|
+
if (frameResponse.ok) {
|
|
2337
|
+
const frameData = await frameResponse.json();
|
|
2338
|
+
const logsCount = frameData.logs?.length || 0;
|
|
2339
|
+
const noteText = snapshot.user_note ? `\nNote: ${snapshot.user_note}` : '';
|
|
2340
|
+
alert(`Snapshot ${snapshot.session_id}${noteText}\n\nConsole Logs: ${logsCount} entries\n\nUse "Copy ID" to share with Claude Code for detailed analysis.`);
|
|
2341
|
+
}
|
|
2342
|
+
} catch (error) {
|
|
2343
|
+
console.error('Failed to load snapshot details:', error);
|
|
2344
|
+
alert('Failed to load snapshot details. Please try again.');
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
deleteBtn.addEventListener('click', async () => {
|
|
2349
|
+
if (confirm('Are you sure you want to delete this snapshot?')) {
|
|
2350
|
+
try {
|
|
2351
|
+
const deleteResponse = await fetch(`http://localhost:${port}/chromedebug/recording/${snapshot.session_id}`, {
|
|
2352
|
+
method: 'DELETE'
|
|
2353
|
+
});
|
|
2354
|
+
|
|
2355
|
+
if (deleteResponse.ok) {
|
|
2356
|
+
// Reload the list
|
|
2357
|
+
loadSnapshots();
|
|
2358
|
+
}
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
console.error('Failed to delete snapshot:', error);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
});
|
|
2364
|
+
|
|
2365
|
+
snapshotsContainer.appendChild(snapshotItem);
|
|
2366
|
+
});
|
|
2367
|
+
} else {
|
|
2368
|
+
snapshotsList.style.display = 'none';
|
|
2369
|
+
}
|
|
2370
|
+
break; // Found working server
|
|
2371
|
+
}
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
console.error(`Failed to load snapshots from port ${port}:`, error);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
*/
|
|
2378
|
+
|
|
2379
|
+
// Listen for storage changes
|
|
2380
|
+
chrome.storage.onChanged.addListener((changes, namespace) => {
|
|
2381
|
+
if (namespace === 'local') {
|
|
2382
|
+
if (changes.recordingActive && !isStoppingRecording) {
|
|
2383
|
+
isRecording = changes.recordingActive.newValue === true;
|
|
2384
|
+
if (changes.recordingStartTime) {
|
|
2385
|
+
recordingStartTime = changes.recordingStartTime.newValue;
|
|
2386
|
+
}
|
|
2387
|
+
updateRecordingUI();
|
|
2388
|
+
}
|
|
2389
|
+
if (changes.workflowRecording) {
|
|
2390
|
+
isWorkflowRecording = changes.workflowRecording.newValue === true;
|
|
2391
|
+
updateWorkflowRecordingUI();
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
// Listen for messages from background script
|
|
2397
|
+
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
2398
|
+
console.log('Popup received message:', request);
|
|
2399
|
+
if (request.action === 'uploadComplete') {
|
|
2400
|
+
console.log('Upload complete with ID:', request.recordingId);
|
|
2401
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
2402
|
+
if (recordingStatus) {
|
|
2403
|
+
recordingStatus.innerHTML = `<strong style="color: #4CAF50;">Recording saved!</strong>`;
|
|
2404
|
+
}
|
|
2405
|
+
// Add to recordings list
|
|
2406
|
+
addRecording(request.recordingId);
|
|
2407
|
+
// Clear status after 3 seconds
|
|
2408
|
+
setTimeout(() => {
|
|
2409
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
2410
|
+
}, 3000);
|
|
2411
|
+
} else if (request.action === 'sessionComplete') {
|
|
2412
|
+
console.log('Session complete:', request.sessionId, 'with', request.totalChunks, 'chunks');
|
|
2413
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
2414
|
+
if (recordingStatus) {
|
|
2415
|
+
recordingStatus.innerHTML = `<strong style="color: #4CAF50;">Recording session saved!</strong><br>` +
|
|
2416
|
+
`<small>${request.totalChunks} chunks (${Math.round(request.duration / 1000)}s)</small>`;
|
|
2417
|
+
}
|
|
2418
|
+
// Add session to recordings list
|
|
2419
|
+
addRecording(request.sessionId, true);
|
|
2420
|
+
// Clear status after 5 seconds
|
|
2421
|
+
setTimeout(() => {
|
|
2422
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
2423
|
+
}, 5000);
|
|
2424
|
+
} else if (request.action === 'frameSessionComplete') {
|
|
2425
|
+
console.log('Frame session complete:', request.sessionId, 'with', request.totalFrames, 'frames');
|
|
2426
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
2427
|
+
if (recordingStatus) {
|
|
2428
|
+
recordingStatus.innerHTML = `<strong style="color: #4CAF50;">Recording saved!</strong><br>` +
|
|
2429
|
+
`<small>${request.totalFrames} frames (${Math.round(request.duration / 1000)}s)</small>`;
|
|
2430
|
+
}
|
|
2431
|
+
// Add frame session to recordings list
|
|
2432
|
+
addRecording(request.sessionId, false, true, request.serverPort, null, request.sessionName);
|
|
2433
|
+
// Clear status after 5 seconds
|
|
2434
|
+
setTimeout(() => {
|
|
2435
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
2436
|
+
}, 5000);
|
|
2437
|
+
} else if (request.action === 'uploadError') {
|
|
2438
|
+
const recordingStatus = document.getElementById('recordingStatus');
|
|
2439
|
+
if (recordingStatus) {
|
|
2440
|
+
recordingStatus.innerHTML = `<strong style="color: #f44336;">Upload error:</strong> ${request.error}`;
|
|
2441
|
+
}
|
|
2442
|
+
setTimeout(() => {
|
|
2443
|
+
if (recordingStatus) recordingStatus.textContent = '';
|
|
2444
|
+
}, 5000);
|
|
2445
|
+
}
|
|
2446
|
+
});
|