@dynamicu/chromedebug-mcp 2.6.7 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CLAUDE.md +1 -1
  2. package/README.md +1 -1
  3. package/chrome-extension/background.js +611 -505
  4. package/chrome-extension/browser-recording-manager.js +1 -1
  5. package/chrome-extension/chrome-debug-logger.js +168 -0
  6. package/chrome-extension/console-interception-library.js +430 -0
  7. package/chrome-extension/content.css +16 -16
  8. package/chrome-extension/content.js +458 -126
  9. package/chrome-extension/extension-config.js +1 -1
  10. package/chrome-extension/license-helper.js +26 -0
  11. package/chrome-extension/manifest.free.json +0 -3
  12. package/chrome-extension/options.js +1 -1
  13. package/chrome-extension/popup.html +221 -191
  14. package/chrome-extension/popup.js +88 -379
  15. package/chrome-extension/pro/enhanced-capture.js +406 -0
  16. package/chrome-extension/pro/frame-editor.html +410 -0
  17. package/chrome-extension/pro/frame-editor.js +1496 -0
  18. package/chrome-extension/pro/function-tracker.js +843 -0
  19. package/chrome-extension/pro/jszip.min.js +13 -0
  20. package/dist/chromedebug-extension-free.zip +0 -0
  21. package/package.json +3 -1
  22. package/scripts/webpack.config.free.cjs +8 -8
  23. package/scripts/webpack.config.pro.cjs +2 -0
  24. package/src/cli.js +2 -2
  25. package/src/database.js +55 -7
  26. package/src/index.js +9 -6
  27. package/src/mcp/server.js +2 -2
  28. package/src/services/process-manager.js +10 -6
  29. package/src/services/process-tracker.js +10 -5
  30. package/src/services/profile-manager.js +17 -2
  31. package/src/validation/schemas.js +2 -2
  32. package/src/index-direct.js +0 -157
  33. package/src/index-modular.js +0 -219
  34. package/src/index-monolithic-backup.js +0 -2230
  35. package/src/legacy/chrome-controller-old.js +0 -1406
  36. package/src/legacy/index-express.js +0 -625
  37. package/src/legacy/index-old.js +0 -977
  38. package/src/legacy/routes.js +0 -260
  39. package/src/legacy/shared-storage.js +0 -101
@@ -8,13 +8,129 @@ importScripts('pako.min.js');
8
8
  importScripts('data-buffer.js');
9
9
  importScripts('upload-manager.js');
10
10
  importScripts('chrome-session-manager.js');
11
- importScripts('firebase-config.js');
12
- importScripts('license-helper.js');
11
+ importScripts('console-interception-library.js'); // Shared console interception library
13
12
  importScripts('browser-recording-manager.js');
14
13
 
15
14
  const CONFIG_PORTS = CHROMEDEBUG_CONFIG.ports;
16
15
  const DISCOVERY_TIMEOUT = CHROMEDEBUG_CONFIG.discoveryTimeout;
17
16
 
17
+ // ============================================================
18
+ // FREE TIER USAGE TRACKING (Local Storage - No Firebase)
19
+ // ============================================================
20
+ // Daily limit: 5 recordings per day
21
+ // Resets at midnight local time
22
+ // Tracks both workflow and screen recordings
23
+ // ============================================================
24
+
25
+ const FREE_TIER_DAILY_LIMIT = 5;
26
+ const USAGE_STORAGE_KEY = 'chromedebug_daily_usage';
27
+
28
+ /**
29
+ * Check if this is the PRO version
30
+ */
31
+ function isProVersion() {
32
+ const manifest = chrome.runtime.getManifest();
33
+ return manifest.name.includes('PRO');
34
+ }
35
+
36
+ /**
37
+ * Get today's date string in local timezone (YYYY-MM-DD)
38
+ */
39
+ function getTodayDateString() {
40
+ const now = new Date();
41
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
42
+ }
43
+
44
+ /**
45
+ * Get current usage for today
46
+ * @returns {Promise<{count: number, date: string, limit: number}>}
47
+ */
48
+ async function getUsageToday() {
49
+ // PRO version: unlimited
50
+ if (isProVersion()) {
51
+ return { count: 0, date: getTodayDateString(), limit: Infinity, isPro: true };
52
+ }
53
+
54
+ const result = await chrome.storage.local.get(USAGE_STORAGE_KEY);
55
+ const usage = result[USAGE_STORAGE_KEY] || { count: 0, date: '' };
56
+ const today = getTodayDateString();
57
+
58
+ // Reset if new day
59
+ if (usage.date !== today) {
60
+ return { count: 0, date: today, limit: FREE_TIER_DAILY_LIMIT };
61
+ }
62
+
63
+ return { count: usage.count, date: today, limit: FREE_TIER_DAILY_LIMIT };
64
+ }
65
+
66
+ /**
67
+ * Check if user can start a new recording
68
+ * @returns {Promise<{allowed: boolean, count: number, limit: number, message?: string}>}
69
+ */
70
+ async function checkUsageBeforeRecording() {
71
+ const usage = await getUsageToday();
72
+
73
+ if (usage.isPro) {
74
+ return { allowed: true, count: 0, limit: Infinity, isPro: true };
75
+ }
76
+
77
+ if (usage.count >= usage.limit) {
78
+ return {
79
+ allowed: false,
80
+ count: usage.count,
81
+ limit: usage.limit,
82
+ message: `Daily limit reached (${usage.count}/${usage.limit}). Upgrade to Pro for unlimited recordings.`
83
+ };
84
+ }
85
+
86
+ return { allowed: true, count: usage.count, limit: usage.limit };
87
+ }
88
+
89
+ /**
90
+ * Increment usage count after successful recording
91
+ * @returns {Promise<{success: boolean, count: number, limit: number}>}
92
+ */
93
+ async function incrementUsageAfterRecording() {
94
+ // PRO version: skip tracking
95
+ if (isProVersion()) {
96
+ console.log('[Usage] PRO version - skipping usage tracking');
97
+ return { success: true, count: 0, limit: Infinity, isPro: true };
98
+ }
99
+
100
+ const today = getTodayDateString();
101
+ const result = await chrome.storage.local.get(USAGE_STORAGE_KEY);
102
+ const usage = result[USAGE_STORAGE_KEY] || { count: 0, date: '' };
103
+
104
+ // Reset if new day, then increment
105
+ const newCount = (usage.date === today) ? usage.count + 1 : 1;
106
+
107
+ await chrome.storage.local.set({
108
+ [USAGE_STORAGE_KEY]: { count: newCount, date: today }
109
+ });
110
+
111
+ console.log(`[Usage] Recording tracked: ${newCount}/${FREE_TIER_DAILY_LIMIT} today`);
112
+
113
+ return { success: true, count: newCount, limit: FREE_TIER_DAILY_LIMIT };
114
+ }
115
+
116
+ // Console interception configuration for screen recording
117
+ const SCREEN_RECORDING_CONSOLE_CONFIG = {
118
+ overrideFlagName: '__chromePilotConsoleOverridden',
119
+ originalConsoleName: '__chromePilotOriginalConsole',
120
+ relayFlagName: '__chromePilotConsoleRelay',
121
+ messageType: 'chrome-debug-console-log',
122
+ backgroundAction: 'consoleLog'
123
+ };
124
+
125
+ // Console interception configuration for workflow recording
126
+ const WORKFLOW_RECORDING_CONSOLE_CONFIG = {
127
+ overrideFlagName: '__chromePilotWorkflowConsoleOverridden',
128
+ originalConsoleName: '__chromePilotWorkflowOriginalConsole',
129
+ relayFlagName: '__chromePilotWorkflowConsoleRelay',
130
+ messageType: 'chrome-debug-workflow-console-log',
131
+ backgroundAction: 'workflowConsoleLog'
132
+ };
133
+
18
134
  // Frame queue for handling validation race conditions
19
135
  // v2.1.2: Prevents frame loss when lease renewal hasn't propagated yet
20
136
  const pendingFrameQueue = new Map(); // sessionId -> array of frame batches
@@ -481,6 +597,162 @@ class LogTabBuffer {
481
597
  }
482
598
  }
483
599
 
600
+ /**
601
+ * WorkflowLogBuffer - Race-safe log buffer for workflow recording
602
+ *
603
+ * Mirrors LogTabBuffer but uses workflow-specific storage keys (`workflow_${tabId}`).
604
+ * This prevents race conditions when multiple logs arrive rapidly during workflow recording.
605
+ *
606
+ * Technical Debt Note: This class duplicates LogTabBuffer logic. Future refactoring
607
+ * could make LogTabBuffer accept a configurable storage key prefix to eliminate duplication.
608
+ */
609
+ class WorkflowLogBuffer {
610
+ constructor(tabId) {
611
+ this.tabId = tabId;
612
+ this.buffer = [];
613
+ this.processing = false;
614
+ this.processingQueue = [];
615
+ this.totalLogsAdded = 0;
616
+ this.totalLogsFlushed = 0;
617
+ this.lastFlushTime = 0;
618
+ this.storageKey = `workflow_${tabId}`;
619
+ }
620
+
621
+ // Add log to buffer with queuing to prevent race conditions
622
+ async addLog(logEntry) {
623
+ return new Promise((resolve, reject) => {
624
+ this.processingQueue.push({
625
+ type: 'add',
626
+ logEntry,
627
+ resolve,
628
+ reject
629
+ });
630
+
631
+ // Process queue if not already processing
632
+ if (!this.processing) {
633
+ this.processQueue();
634
+ }
635
+ });
636
+ }
637
+
638
+ // Flush buffer to Chrome storage
639
+ async flush() {
640
+ return new Promise((resolve, reject) => {
641
+ this.processingQueue.push({
642
+ type: 'flush',
643
+ resolve,
644
+ reject
645
+ });
646
+
647
+ // Process queue if not already processing
648
+ if (!this.processing) {
649
+ this.processQueue();
650
+ }
651
+ });
652
+ }
653
+
654
+ // Sequential processing of queue to prevent race conditions
655
+ async processQueue() {
656
+ if (this.processing) {
657
+ return; // Already processing
658
+ }
659
+
660
+ this.processing = true;
661
+
662
+ try {
663
+ while (this.processingQueue.length > 0) {
664
+ const operation = this.processingQueue.shift();
665
+
666
+ try {
667
+ if (operation.type === 'add') {
668
+ this.buffer.push(operation.logEntry);
669
+ this.totalLogsAdded++;
670
+
671
+ // Auto-flush if buffer gets too large
672
+ if (this.buffer.length >= 100) {
673
+ await this.performFlush();
674
+ }
675
+
676
+ operation.resolve();
677
+ } else if (operation.type === 'flush') {
678
+ await this.performFlush();
679
+ operation.resolve();
680
+ }
681
+ } catch (error) {
682
+ operation.reject(error);
683
+ }
684
+ }
685
+ } finally {
686
+ this.processing = false;
687
+ }
688
+ }
689
+
690
+ // Actual flush operation to Chrome storage
691
+ async performFlush() {
692
+ if (this.buffer.length === 0) {
693
+ return; // Nothing to flush
694
+ }
695
+
696
+ try {
697
+ // Read current logs from storage
698
+ const result = await chrome.storage.session.get(this.storageKey);
699
+ const existingLogs = result[this.storageKey] || [];
700
+
701
+ // Append new logs
702
+ const updatedLogs = [...existingLogs, ...this.buffer];
703
+
704
+ // Check storage size limit
705
+ const estimatedSize = JSON.stringify(updatedLogs).length;
706
+ if (estimatedSize > 500000) { // ~500KB limit
707
+ const beforeCount = updatedLogs.length;
708
+ const retentionPercentage = 0.95;
709
+ const afterCount = Math.floor(updatedLogs.length * retentionPercentage);
710
+ console.warn(`[WorkflowLogBuffer] Storage size limit approached for tab ${this.tabId}, truncating from ${beforeCount} to ${afterCount} logs`);
711
+ const recentLogs = updatedLogs.slice(-afterCount);
712
+ await chrome.storage.session.set({ [this.storageKey]: recentLogs });
713
+ } else {
714
+ await chrome.storage.session.set({ [this.storageKey]: updatedLogs });
715
+ }
716
+
717
+ this.totalLogsFlushed += this.buffer.length;
718
+ console.log(`[WorkflowLogBuffer] Flushed ${this.buffer.length} logs for tab ${this.tabId} (total: ${this.totalLogsFlushed})`);
719
+
720
+ // Clear the buffer
721
+ this.buffer = [];
722
+ this.lastFlushTime = Date.now();
723
+
724
+ } catch (error) {
725
+ console.error(`[WorkflowLogBuffer] Failed to flush logs for tab ${this.tabId}:`, error);
726
+ throw error;
727
+ }
728
+ }
729
+
730
+ // Get buffer statistics
731
+ getStats() {
732
+ return {
733
+ bufferedLogs: this.buffer.length,
734
+ totalAdded: this.totalLogsAdded,
735
+ totalFlushed: this.totalLogsFlushed,
736
+ processing: this.processing,
737
+ queueLength: this.processingQueue.length,
738
+ lastFlushTime: this.lastFlushTime
739
+ };
740
+ }
741
+
742
+ // Clean up buffer when recording stops
743
+ async cleanup() {
744
+ try {
745
+ // Flush any remaining logs
746
+ if (this.buffer.length > 0) {
747
+ await this.performFlush();
748
+ }
749
+ console.log(`[WorkflowLogBuffer] Cleaned up buffer for tab ${this.tabId}`);
750
+ } catch (error) {
751
+ console.error(`[WorkflowLogBuffer] Error during cleanup for tab ${this.tabId}:`, error);
752
+ }
753
+ }
754
+ }
755
+
484
756
  // Initialize data buffer and upload manager
485
757
  let dataBuffer = null;
486
758
  let uploadManager = null;
@@ -655,10 +927,14 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
655
927
  'restoreFromPoint',
656
928
  'getWebSocketPort',
657
929
  'startRecording',
930
+ 'stopRecording',
931
+ 'openFrameEditor',
658
932
  'checkConnection',
659
933
  'workflowConsoleLog', // v2.0.8: handle workflow console logs
660
934
  'workflowAction', // v2.0.8: handle workflow actions
661
- 'renewLease' // v2.1.2: session manager lease renewal for frame capture
935
+ 'renewLease', // v2.1.2: session manager lease renewal for frame capture
936
+ 'frameSessionComplete', // Frame recording completion notification
937
+ 'recordingStopped' // Recording stopped notification for popup refresh
662
938
  ];
663
939
 
664
940
  if (handledByOtherListener.includes(messageType)) {
@@ -704,7 +980,27 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
704
980
  sendResponse({ status: 'error', message: error.message });
705
981
  }
706
982
  break;
707
-
983
+
984
+ case 'getUsage':
985
+ // Get current usage count for popup display
986
+ getUsageToday().then(usage => {
987
+ sendResponse({ success: true, ...usage });
988
+ }).catch(error => {
989
+ console.error('[Usage] Failed to get usage:', error);
990
+ sendResponse({ success: false, error: error.message });
991
+ });
992
+ return true; // Async response
993
+
994
+ case 'checkUsageLimit':
995
+ // Check if user can start a new recording
996
+ checkUsageBeforeRecording().then(result => {
997
+ sendResponse(result);
998
+ }).catch(error => {
999
+ console.error('[Usage] Failed to check usage limit:', error);
1000
+ sendResponse({ allowed: true, error: error.message }); // Fail-open
1001
+ });
1002
+ return true; // Async response
1003
+
708
1004
  case 'UPLOAD_BATCH':
709
1005
  // Add events to the background's DataBuffer for upload
710
1006
  if (message.events && message.recordingId) {
@@ -1051,96 +1347,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1051
1347
  }
1052
1348
  });
1053
1349
 
1054
- // Auto-activate PRO license on startup if activation file exists
1055
- async function checkAndActivateLicense() {
1056
- try {
1057
- console.log('[License Auto-Activation] Checking for activation file...');
1058
-
1059
- // Try to fetch the activation file from the extension directory
1060
- const activationUrl = chrome.runtime.getURL('license-activation.json');
1061
- const response = await fetch(activationUrl);
1062
-
1063
- if (!response.ok) {
1064
- console.log('[License Auto-Activation] No activation file found - using normal license flow');
1065
- return;
1066
- }
1067
-
1068
- const activationData = await response.json();
1069
- console.log('[License Auto-Activation] Found activation file:', {
1070
- tier: activationData.tier,
1071
- activated_at: activationData.activated_at
1072
- });
1073
-
1074
- // Check if license is already cached and valid
1075
- const cached = await chrome.storage.local.get('chromedebug_license_cache');
1076
- if (cached.chromedebug_license_cache?.valid && cached.chromedebug_license_cache?.tier === 'pro') {
1077
- console.log('[License Auto-Activation] License already activated and cached');
1078
- return;
1079
- }
1080
-
1081
- // Activate the license
1082
- const licenseKey = activationData.license_key;
1083
- console.log('[License Auto-Activation] Activating license...');
1084
-
1085
- // Call LemonSqueezy activation via Firebase Cloud Function
1086
- const instanceId = crypto.randomUUID();
1087
- await chrome.storage.local.set({
1088
- 'chromedebug_instance_id': instanceId,
1089
- 'ls_instance_id': instanceId,
1090
- 'ls_license_key': licenseKey
1091
- });
1092
-
1093
- const activationResponse = await fetch(`${FUNCTIONS_URL}/activateLicense`, {
1094
- method: 'POST',
1095
- headers: {'Content-Type': 'application/json'},
1096
- body: JSON.stringify({
1097
- licenseKey,
1098
- instanceId,
1099
- instanceName: `ChromeDebug-${navigator.userAgent.match(/Chrome\/(\d+)/)?.[1] || 'Unknown'}`
1100
- })
1101
- });
1102
-
1103
- if (!activationResponse.ok) {
1104
- throw new Error(`Activation failed: HTTP ${activationResponse.status}`);
1105
- }
1106
-
1107
- const activationResult = await activationResponse.json();
1108
- console.log('[License Auto-Activation] Activation result:', activationResult);
1109
-
1110
- if (activationResult.activated || activationResult.valid) {
1111
- // Cache the license status
1112
- const licenseCache = {
1113
- valid: true,
1114
- tier: 'pro',
1115
- licenseKey: licenseKey.substring(0, 8) + '...',
1116
- cachedAt: Date.now(),
1117
- graceUntil: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days
1118
- };
1119
-
1120
- await chrome.storage.local.set({
1121
- 'chromedebug_license_cache': licenseCache
1122
- });
1123
-
1124
- console.log('[License Auto-Activation] ✓ PRO license activated successfully!');
1125
- console.log('[License Auto-Activation] Cached license status:', licenseCache);
1126
- } else {
1127
- console.error('[License Auto-Activation] Activation returned invalid status:', activationResult);
1128
- }
1129
-
1130
- } catch (error) {
1131
- // Don't block extension startup on activation errors
1132
- console.error('[License Auto-Activation] Failed to auto-activate license:', error);
1133
- console.log('[License Auto-Activation] Extension will continue with normal license flow');
1134
- }
1135
- }
1350
+ // License checking removed - using separate Chrome Web Store listings for FREE and PRO
1136
1351
 
1137
1352
  // Session recovery on startup - handle any stuck recordings from previous session
1138
1353
  chrome.runtime.onStartup.addListener(async () => {
1139
1354
  console.log('[Background] Extension startup detected - checking for stuck sessions');
1140
1355
 
1141
- // Check for PRO license auto-activation first
1142
- await checkAndActivateLicense();
1143
-
1144
1356
  if (sessionManager) {
1145
1357
  try {
1146
1358
  // Use existing recovery method from session manager
@@ -1154,12 +1366,9 @@ chrome.runtime.onStartup.addListener(async () => {
1154
1366
  }
1155
1367
  });
1156
1368
 
1157
- // Extension install/update - also check for license activation
1369
+ // Extension install/update
1158
1370
  chrome.runtime.onInstalled.addListener(async (details) => {
1159
1371
  console.log('[Background] Extension installed/updated:', details.reason);
1160
-
1161
- // Check for PRO license auto-activation on install/update
1162
- await checkAndActivateLicense();
1163
1372
  });
1164
1373
 
1165
1374
  // Listen for tab updates to handle restore points
@@ -1257,6 +1466,43 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1257
1466
  }
1258
1467
  });
1259
1468
 
1469
+ // Defensive cleanup when tabs are closed
1470
+ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
1471
+ // Clean up any workflow recording state for closed tabs
1472
+ if (workflowRecordingTabs.has(tabId)) {
1473
+ console.log(`[Workflow] Tab ${tabId} closed during recording, cleaning up`);
1474
+ workflowRecordingTabs.delete(tabId);
1475
+ workflowIncludeLogs.delete(tabId);
1476
+ workflowScreenshotSettings.delete(tabId);
1477
+ workflowSessionNames.delete(tabId);
1478
+
1479
+ // Clean up WorkflowLogBuffer
1480
+ const buffer = workflowLogBuffers.get(tabId);
1481
+ if (buffer) {
1482
+ buffer.cleanup().then(() => {
1483
+ workflowLogBuffers.delete(tabId);
1484
+ console.log(`[Workflow] Cleaned up WorkflowLogBuffer for closed tab ${tabId}`);
1485
+ }).catch(err => {
1486
+ console.error(`[Workflow] Error cleaning up buffer for closed tab ${tabId}:`, err);
1487
+ workflowLogBuffers.delete(tabId);
1488
+ });
1489
+ }
1490
+
1491
+ // Remove session storage
1492
+ chrome.storage.session.remove(`workflow_${tabId}`).catch(err => {
1493
+ console.error(`[Workflow] Error removing session storage for closed tab ${tabId}:`, err);
1494
+ });
1495
+
1496
+ // Notify popup that recording has stopped (tab was closed)
1497
+ chrome.storage.local.set({
1498
+ workflowRecording: false,
1499
+ workflowStartTime: null
1500
+ }).catch(err => {
1501
+ console.error(`[Workflow] Error updating storage for closed tab ${tabId}:`, err);
1502
+ });
1503
+ }
1504
+ });
1505
+
1260
1506
  // Session Manager for robust recording state management
1261
1507
  let sessionManager = null;
1262
1508
  let currentSession = null; // Cache current session state
@@ -1399,7 +1645,7 @@ let workflowRecordingTabs = new Map(); // Map of tabId to recording state
1399
1645
  let workflowIncludeLogs = new Map(); // Map of tabId to includeLogsInExport setting
1400
1646
  let workflowScreenshotSettings = new Map(); // Map of tabId to screenshot settings
1401
1647
  let workflowSessionNames = new Map(); // Map of tabId to session name
1402
- let workflowUserIds = new Map(); // Map of tabId to userId for license tracking
1648
+ let workflowLogBuffers = new Map(); // Map of tabId to WorkflowLogBuffer instance
1403
1649
 
1404
1650
  //=============================================================================
1405
1651
  // COMPREHENSIVE ERROR HANDLING SYSTEM
@@ -1875,7 +2121,7 @@ async function downloadBackupData() {
1875
2121
 
1876
2122
  chrome.downloads.download({
1877
2123
  url: url,
1878
- filename: `chrome-pilot-backup-${Date.now()}.json`
2124
+ filename: `chrome-debug-backup-${Date.now()}.json`
1879
2125
  });
1880
2126
 
1881
2127
  console.log('[ErrorHandling] Backup data download initiated');
@@ -2015,8 +2261,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2015
2261
  }
2016
2262
 
2017
2263
  if (request.action === 'stopRecording') {
2018
- stopRecording().then(() => {
2019
- sendResponse({ success: true });
2264
+ stopRecording().then(async (result) => {
2265
+ // Track usage for FREE tier after successful screen recording
2266
+ if (result.success) {
2267
+ const usageResult = await incrementUsageAfterRecording();
2268
+ result.usage = usageResult; // Include updated usage in response
2269
+ }
2270
+ sendResponse(result); // Pass through { success: true, sessionId: ..., usage: ... }
2020
2271
  }).catch((error) => {
2021
2272
  console.error('Error stopping recording:', error);
2022
2273
  sendResponse({ success: false, error: error.message });
@@ -2024,6 +2275,23 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2024
2275
  return true;
2025
2276
  }
2026
2277
 
2278
+ if (request.action === 'openFrameEditor') {
2279
+ // Content scripts can't create tabs, so handle it here
2280
+ const sessionId = request.sessionId;
2281
+ const type = request.type; // 'workflow' or undefined for screen recordings
2282
+ const editorUrl = type === 'workflow'
2283
+ ? chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${sessionId}&type=workflow`)
2284
+ : chrome.runtime.getURL(`pro/frame-editor.html?sessionId=${sessionId}`);
2285
+
2286
+ chrome.tabs.create({ url: editorUrl }).then(() => {
2287
+ sendResponse({ success: true });
2288
+ }).catch((error) => {
2289
+ console.error('Error opening frame editor:', error);
2290
+ sendResponse({ success: false, error: error.message });
2291
+ });
2292
+ return true;
2293
+ }
2294
+
2027
2295
  // Get quota information for browser-only mode
2028
2296
  if (request.action === 'getQuotaInfo') {
2029
2297
  getQuotaInfo().then((quotaInfo) => {
@@ -2217,20 +2485,6 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2217
2485
  return true;
2218
2486
  }
2219
2487
 
2220
- // License check handler for workflow recording
2221
- if (request.action === 'checkLicenseForWorkflow') {
2222
- console.log('[License] popup.js requesting license check for workflow recording');
2223
- LicenseHelper.checkLicenseBeforeRecording().then((licenseCheck) => {
2224
- console.log('[License] License check result for popup:', licenseCheck);
2225
- sendResponse(licenseCheck);
2226
- }).catch((error) => {
2227
- console.error('[License] Error checking license:', error);
2228
- // Fail-open: allow recording on error
2229
- sendResponse({ allowed: true, error: error.message });
2230
- });
2231
- return true;
2232
- }
2233
-
2234
2488
  // Workflow recording handlers
2235
2489
  if (request.action === 'startWorkflowRecording') {
2236
2490
  console.log('Starting workflow recording for tab:', request.tabId);
@@ -2249,9 +2503,17 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2249
2503
  }
2250
2504
 
2251
2505
  if (request.action === 'stopWorkflowRecording') {
2252
- console.log('Stopping workflow recording for tab:', request.tabId);
2253
- stopWorkflowRecording(request.tabId).then((workflow) => {
2254
- sendResponse({ success: true, workflow: workflow });
2506
+ const tabId = request.tabId || sender.tab?.id;
2507
+ console.log('Stopping workflow recording for tab:', tabId);
2508
+ // stopWorkflowRecording already returns { success: true, workflow: {...}, savedToServer: bool }
2509
+ // so we pass it through directly - don't double-wrap!
2510
+ stopWorkflowRecording(tabId).then(async (result) => {
2511
+ // Track usage for FREE tier after successful recording
2512
+ if (result.success) {
2513
+ const usageResult = await incrementUsageAfterRecording();
2514
+ result.usage = usageResult; // Include updated usage in response
2515
+ }
2516
+ sendResponse(result);
2255
2517
  }).catch((error) => {
2256
2518
  console.error('Error stopping workflow recording:', error);
2257
2519
  sendResponse({ success: false, error: error.message });
@@ -2356,18 +2618,19 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2356
2618
  }
2357
2619
  }
2358
2620
 
2359
- // Buffer workflow console logs
2621
+ // Buffer workflow console logs using race-safe WorkflowLogBuffer
2360
2622
  if (request.action === 'workflowConsoleLog' && sender.tab) {
2361
2623
  const tabId = sender.tab.id;
2362
2624
  if (workflowRecordingTabs.has(tabId) && workflowIncludeLogs.get(tabId)) {
2363
- // Store logs in session storage for workflow recording
2364
- chrome.storage.session.get(`workflow_${tabId}`).then(result => {
2365
- const logs = result[`workflow_${tabId}`] || [];
2366
- logs.push(request.log);
2367
- chrome.storage.session.set({ [`workflow_${tabId}`]: logs });
2368
- }).catch(err => {
2369
- console.error('Error buffering workflow log:', err);
2370
- });
2625
+ const buffer = workflowLogBuffers.get(tabId);
2626
+ if (buffer) {
2627
+ // Use race-safe buffer to prevent read-modify-write race conditions
2628
+ buffer.addLog(request.log).catch(err => {
2629
+ console.error('[WorkflowLogBuffer] Error adding log:', err);
2630
+ });
2631
+ } else {
2632
+ console.warn(`[Workflow] No buffer found for tab ${tabId}, log dropped`);
2633
+ }
2371
2634
  }
2372
2635
  }
2373
2636
 
@@ -2742,31 +3005,10 @@ async function getQuotaInfo() {
2742
3005
 
2743
3006
  async function startRecording(tabId, settings = {}) {
2744
3007
  try {
2745
- // STEP 1: Detect server mode before starting
3008
+ // Detect server mode before starting
2746
3009
  await detectServerMode();
2747
3010
  console.log(`[Recording] Mode: ${serverMode}`);
2748
3011
 
2749
- // STEP 2: Check license and usage limits BEFORE recording
2750
- console.log('[License] Checking license before recording...');
2751
- const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
2752
- console.log('[License] License check result:', licenseCheck);
2753
-
2754
- if (!licenseCheck.allowed) {
2755
- // Show notification to user
2756
- chrome.notifications.create({
2757
- type: 'basic',
2758
- iconUrl: chrome.runtime.getURL('icon48.png'),
2759
- title: 'Recording Limit Reached',
2760
- message: licenseCheck.message || 'Upgrade to Pro for unlimited recordings.',
2761
- priority: 2
2762
- });
2763
-
2764
- throw new Error(licenseCheck.message || 'Recording not allowed');
2765
- }
2766
-
2767
- // Store userId for later usage tracking
2768
- const userId = licenseCheck.userId;
2769
-
2770
3012
  // Check if session manager is available
2771
3013
  if (!sessionManager) {
2772
3014
  throw new Error('Session manager not initialized');
@@ -2965,16 +3207,23 @@ async function stopRecording() {
2965
3207
  // Check if session manager is available
2966
3208
  if (!sessionManager) {
2967
3209
  console.warn('Session manager not available, stopping recording anyway');
2968
- return;
3210
+ return { success: false, error: 'Session manager not available' };
2969
3211
  }
2970
3212
 
2971
3213
  // Check if currently recording via session manager
2972
3214
  const currentlyRecording = await isCurrentlyRecordingAsync();
2973
3215
  if (!currentlyRecording) {
2974
3216
  console.log('No active recording session to stop');
2975
- return;
3217
+ return { success: false, error: 'No active recording' };
2976
3218
  }
2977
3219
 
3220
+ // Store sessionId before clearing it
3221
+ const stoppedSessionId = currentSession?.sessionId;
3222
+
3223
+ // Capture frame count and duration for the response
3224
+ let recordedFrameCount = 0;
3225
+ let recordedDuration = 0;
3226
+
2978
3227
  // Stop recording with session manager
2979
3228
  if (currentSession?.sessionId && currentOwnerId) {
2980
3229
  const stopResult = await sessionManager.stopRecording(currentSession.sessionId, currentOwnerId);
@@ -2982,18 +3231,15 @@ async function stopRecording() {
2982
3231
  console.error('Failed to stop session:', stopResult.error?.message);
2983
3232
  // Continue with cleanup anyway
2984
3233
  } else {
2985
- console.log(`Recording stopped. Duration: ${stopResult.sessionDuration}ms, Frames: ${stopResult.frameCount}`);
3234
+ recordedFrameCount = stopResult.frameCount || 0;
3235
+ recordedDuration = stopResult.sessionDuration || 0;
3236
+ console.log(`Recording stopped. Duration: ${recordedDuration}ms, Frames: ${recordedFrameCount}`);
2986
3237
 
2987
3238
  // Finalize browser-only recording if in browser-only mode
2988
3239
  if (serverMode === 'browser-only' && browserRecordingManager) {
2989
3240
  console.log('[Browser-Only] Finalizing browser recording in IndexedDB');
2990
3241
  await browserRecordingManager.stopRecording(currentSession.sessionId);
2991
3242
  }
2992
-
2993
- // STEP 2: Track usage AFTER successful recording completion
2994
- console.log('[License] Tracking usage after recording completion...');
2995
- const usageResult = await LicenseHelper.trackUsageAfterRecording();
2996
- console.log('[License] Usage tracking result:', usageResult);
2997
3243
  }
2998
3244
  }
2999
3245
 
@@ -3023,6 +3269,29 @@ async function stopRecording() {
3023
3269
  target: 'offscreen'
3024
3270
  });
3025
3271
 
3272
+ // CRITICAL: Store recordingTabId BEFORE clearing it (for cleanup)
3273
+ const previousRecordingTabId = recordingTabId;
3274
+ console.log('[Background] previousRecordingTabId:', previousRecordingTabId, 'stoppedSessionId:', stoppedSessionId);
3275
+
3276
+ // CRITICAL FIX: Send stop-screen-capture-tracking message to recording tab WITH sessionId
3277
+ // so it can show the completion UI
3278
+ if (previousRecordingTabId && stoppedSessionId) {
3279
+ try {
3280
+ console.log('[Background] Sending recording-complete-show-ui message to tab:', previousRecordingTabId, 'with sessionId:', stoppedSessionId);
3281
+ await chrome.tabs.sendMessage(previousRecordingTabId, {
3282
+ type: 'recording-complete-show-ui',
3283
+ sessionId: stoppedSessionId
3284
+ }).catch((err) => {
3285
+ console.log('[Background] Could not send completion UI message to recording tab:', err);
3286
+ });
3287
+ console.log('[Background] Successfully sent recording-complete-show-ui message');
3288
+ } catch (error) {
3289
+ console.error('[Background] Failed to send completion UI message:', error);
3290
+ }
3291
+ } else {
3292
+ console.log('[Background] NOT sending recording-complete-show-ui - missing previousRecordingTabId or stoppedSessionId');
3293
+ }
3294
+
3026
3295
  // CRITICAL FIX: Send stop-screen-capture-tracking message to ALL content scripts
3027
3296
  try {
3028
3297
  const tabs = await chrome.tabs.query({});
@@ -3043,12 +3312,22 @@ async function stopRecording() {
3043
3312
  console.error('[Background] Failed to send cleanup messages to content scripts:', error);
3044
3313
  }
3045
3314
 
3315
+ // CRITICAL FIX: Restore original console methods and remove event listeners
3316
+ if (previousRecordingTabId) {
3317
+ try {
3318
+ console.log('[Background] Stopping console log capture for tab:', previousRecordingTabId);
3319
+ await stopCapturingLogs(previousRecordingTabId);
3320
+ } catch (cleanupError) {
3321
+ console.error('[Background] Failed to cleanup console interceptor:', cleanupError);
3322
+ // Continue with stop process even if cleanup fails
3323
+ }
3324
+ }
3325
+
3046
3326
  // Clear session cache
3047
3327
  currentSession = null;
3048
3328
  currentOwnerId = null;
3049
3329
 
3050
3330
  // Update legacy state for backward compatibility (will be removed)
3051
- const previousRecordingTabId = recordingTabId; // Store for logBuffer cleanup
3052
3331
  isCurrentlyRecording = false;
3053
3332
  recordingTabId = null;
3054
3333
  currentRecordingSessionId = null;
@@ -3058,11 +3337,56 @@ async function stopRecording() {
3058
3337
  logBuffer.clearTab(previousRecordingTabId);
3059
3338
  }
3060
3339
 
3340
+ // CRITICAL: Clear pending frame queue to prevent infinite retry loop
3341
+ if (stoppedSessionId && pendingFrameQueue.has(stoppedSessionId)) {
3342
+ console.log(`[FrameQueue] Clearing ${pendingFrameQueue.get(stoppedSessionId).length} pending frames for stopped session ${stoppedSessionId}`);
3343
+ pendingFrameQueue.delete(stoppedSessionId);
3344
+ }
3345
+
3061
3346
  console.log('Stop message sent to offscreen document');
3347
+
3348
+ // Notify popup to refresh recordings list
3349
+ chrome.runtime.sendMessage({
3350
+ action: 'recordingStopped',
3351
+ sessionId: stoppedSessionId
3352
+ }).catch(() => {
3353
+ // Popup might not be open, that's okay
3354
+ console.log('[Background] Popup not available to notify of recording stop');
3355
+ });
3356
+
3357
+ // Format duration as human-readable string (e.g., "1m 30s" or "45s")
3358
+ const formatDurationForDisplay = (ms) => {
3359
+ const seconds = Math.floor(ms / 1000);
3360
+ if (seconds < 60) {
3361
+ return `${seconds}s`;
3362
+ }
3363
+ const minutes = Math.floor(seconds / 60);
3364
+ const remainingSeconds = seconds % 60;
3365
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
3366
+ };
3367
+
3368
+ // Return success with sessionId, frameCount and duration for popup display
3369
+ return {
3370
+ success: true,
3371
+ sessionId: stoppedSessionId,
3372
+ frameCount: recordedFrameCount,
3373
+ duration: formatDurationForDisplay(recordedDuration)
3374
+ };
3062
3375
  } catch (error) {
3063
3376
  console.error('Error in stopRecording:', error);
3064
3377
  // Clear session cache even on error
3065
3378
  const errorRecordingTabId = recordingTabId; // Store for cleanup
3379
+
3380
+ // CRITICAL FIX: Clean up console interceptor even on error
3381
+ if (errorRecordingTabId) {
3382
+ try {
3383
+ console.log('[Background] Emergency cleanup of console interceptor for tab:', errorRecordingTabId);
3384
+ await stopCapturingLogs(errorRecordingTabId);
3385
+ } catch (cleanupError) {
3386
+ console.error('[Background] Failed to cleanup console interceptor during error handling:', cleanupError);
3387
+ }
3388
+ }
3389
+
3066
3390
  currentSession = null;
3067
3391
  currentOwnerId = null;
3068
3392
  isCurrentlyRecording = false;
@@ -3081,204 +3405,16 @@ async function stopRecording() {
3081
3405
 
3082
3406
 
3083
3407
  // Capture console logs from the recording tab
3408
+ // REFACTORED: Now uses shared console-interception-library.js
3084
3409
  async function startCapturingLogs(tabId) {
3085
- // Check if this tab allows content script injection
3086
- const tab = await chrome.tabs.get(tabId);
3087
- if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
3088
- console.log('Cannot inject console logger into restricted URL:', tab.url);
3089
- console.warn('WARNING: Console logs cannot be captured on restricted pages (chrome://, chrome-extension://, etc.)');
3090
- console.warn('To capture console logs, please navigate to a regular web page before recording.');
3091
- return false;
3092
- }
3093
-
3094
- // First inject the main world console interceptor
3095
- try {
3096
- const results = await chrome.scripting.executeScript({
3097
- target: { tabId: tabId },
3098
- world: 'MAIN',
3099
- func: () => {
3100
- // Check if we've already overridden console methods
3101
- if (window.__chromePilotConsoleOverridden) {
3102
- console.log('[Chrome Debug] Console already overridden, skipping');
3103
- return 'already_installed';
3104
- }
3105
- window.__chromePilotConsoleOverridden = true;
3106
- console.log('[Chrome Debug] Installing console interceptor');
3107
-
3108
- // Override console methods to capture logs - complete coverage
3109
- const originalLog = console.log;
3110
- const originalError = console.error;
3111
- const originalWarn = console.warn;
3112
- const originalInfo = console.info;
3113
- const originalDebug = console.debug;
3114
- const originalTrace = console.trace;
3115
- const originalTable = console.table;
3116
- const originalDir = console.dir;
3117
- const originalGroup = console.group;
3118
- const originalGroupEnd = console.groupEnd;
3119
- const originalTime = console.time;
3120
- const originalTimeEnd = console.timeEnd;
3121
- const originalCount = console.count;
3122
-
3123
- const sendLog = (level, args) => {
3124
- try {
3125
- // Pre-serialize arguments to strings to avoid structured clone errors
3126
- const serializedArgs = args.map(arg => {
3127
- try {
3128
- if (arg === null) return 'null';
3129
- if (arg === undefined) return 'undefined';
3130
- if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
3131
- if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
3132
- if (typeof arg === 'object') {
3133
- // Try to stringify, but limit depth to avoid circular references
3134
- let stringified = JSON.stringify(arg, null, 2);
3135
-
3136
- // Check if this looks like a base64 image and truncate it
3137
- if (stringified.includes('data:image/') && stringified.length > 1000) {
3138
- const match = stringified.match(/data:image\/([^;]+);base64,(.{0,100})/);
3139
- if (match) {
3140
- return `[Base64 Image: ${match[1]}, ${stringified.length} bytes total, truncated...]`;
3141
- }
3142
- }
3143
-
3144
- // Truncate any extremely large strings
3145
- const maxLength = 5000;
3146
- if (stringified.length > maxLength) {
3147
- return stringified.substring(0, maxLength) + `... [TRUNCATED: ${stringified.length} total bytes]`;
3148
- }
3149
-
3150
- return stringified;
3151
- }
3152
-
3153
- // Also check for base64 strings directly
3154
- const strValue = String(arg);
3155
- if (strValue.includes('data:image/') && strValue.length > 1000) {
3156
- const match = strValue.match(/data:image\/([^;]+);base64,(.{0,100})/);
3157
- if (match) {
3158
- return `[Base64 Image: ${match[1]}, ${strValue.length} bytes total, truncated...]`;
3159
- }
3160
- }
3161
-
3162
- // Truncate any extremely large strings
3163
- if (strValue.length > 5000) {
3164
- return strValue.substring(0, 5000) + `... [TRUNCATED: ${strValue.length} total bytes]`;
3165
- }
3166
-
3167
- return strValue;
3168
- } catch (e) {
3169
- return '[Object: could not serialize]';
3170
- }
3171
- });
3172
-
3173
- // Post message to content script
3174
- window.postMessage({
3175
- type: 'chrome-pilot-console-log',
3176
- log: {
3177
- level,
3178
- message: serializedArgs.join(' '),
3179
- timestamp: Date.now()
3180
- }
3181
- }, '*');
3182
- } catch (e) {
3183
- // Ignore errors when sending logs
3184
- }
3185
- };
3186
-
3187
- console.log = (...args) => {
3188
- sendLog('log', args);
3189
- originalLog.apply(console, args);
3190
- };
3191
-
3192
- console.error = (...args) => {
3193
- sendLog('error', args);
3194
- originalError.apply(console, args);
3195
- };
3196
-
3197
- console.warn = (...args) => {
3198
- sendLog('warn', args);
3199
- originalWarn.apply(console, args);
3200
- };
3201
-
3202
- console.info = (...args) => {
3203
- sendLog('info', args);
3204
- originalInfo.apply(console, args);
3205
- };
3206
-
3207
- console.debug = (...args) => {
3208
- sendLog('debug', args);
3209
- originalDebug.apply(console, args);
3210
- };
3211
-
3212
- console.trace = (...args) => {
3213
- sendLog('trace', args);
3214
- originalTrace.apply(console, args);
3215
- };
3216
-
3217
- console.table = (...args) => {
3218
- sendLog('table', args);
3219
- originalTable.apply(console, args);
3220
- };
3221
-
3222
- console.dir = (...args) => {
3223
- sendLog('dir', args);
3224
- originalDir.apply(console, args);
3225
- };
3226
-
3227
- console.group = (...args) => {
3228
- sendLog('group', args);
3229
- originalGroup.apply(console, args);
3230
- };
3231
-
3232
- console.groupEnd = (...args) => {
3233
- sendLog('groupEnd', args);
3234
- originalGroupEnd.apply(console, args);
3235
- };
3236
-
3237
- console.time = (...args) => {
3238
- sendLog('time', args);
3239
- originalTime.apply(console, args);
3240
- };
3241
-
3242
- console.timeEnd = (...args) => {
3243
- sendLog('timeEnd', args);
3244
- originalTimeEnd.apply(console, args);
3245
- };
3246
-
3247
- console.count = (...args) => {
3248
- sendLog('count', args);
3249
- originalCount.apply(console, args);
3250
- };
3251
-
3252
- return 'console_installed';
3253
- }
3254
- });
3255
-
3256
- console.log('[Console Injection] MAIN world script injected successfully:', results);
3257
-
3258
- // Then inject a content script to relay messages from main world to background
3259
- await chrome.scripting.executeScript({
3260
- target: { tabId: tabId },
3261
- func: () => {
3262
- // Listen for messages from main world
3263
- window.addEventListener('message', (event) => {
3264
- if (event.data && event.data.type === 'chrome-pilot-console-log') {
3265
- // Forward to background script
3266
- chrome.runtime.sendMessage({
3267
- action: 'consoleLog',
3268
- log: event.data.log
3269
- });
3270
- }
3271
- });
3272
- }
3273
- });
3410
+ console.log('[Screen Recording] Starting console interception using shared library');
3411
+ return await self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, SCREEN_RECORDING_CONSOLE_CONFIG);
3412
+ }
3274
3413
 
3275
- console.log('[Console Injection] Content script relay injected successfully');
3276
- return true; // Successfully started capturing logs
3277
- } catch (error) {
3278
- console.error('[Console Injection] Failed to inject console interceptor:', error);
3279
- console.error('[Console Injection] Error details:', error.message, error.stack);
3280
- return false; // Failed to inject console interceptor
3281
- }
3414
+ // REFACTORED: Now uses shared console-interception-library.js via proper namespace
3415
+ async function stopCapturingLogs(tabId) {
3416
+ console.log('[Screen Recording] Stopping console interception using shared library');
3417
+ return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, SCREEN_RECORDING_CONSOLE_CONFIG);
3282
3418
  }
3283
3419
 
3284
3420
  // Workflow Recording Functions
@@ -3286,29 +3422,6 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
3286
3422
  try {
3287
3423
  console.log('Starting workflow recording for tab:', tabId);
3288
3424
 
3289
- // STEP 1: Check license and usage limits BEFORE recording (same pattern as startRecording)
3290
- console.log('[License] Checking license before workflow recording...');
3291
- const licenseCheck = await LicenseHelper.checkLicenseBeforeRecording();
3292
- console.log('[License] License check result:', licenseCheck);
3293
-
3294
- if (!licenseCheck.allowed) {
3295
- // Show notification to user (same pattern as screen recording)
3296
- chrome.notifications.create({
3297
- type: 'basic',
3298
- iconUrl: chrome.runtime.getURL('icon128.png'),
3299
- title: 'Recording Limit Reached',
3300
- message: licenseCheck.message || 'Daily limit reached. Upgrade to Pro for unlimited workflow recordings.',
3301
- buttons: [{ title: 'Upgrade to Pro' }],
3302
- priority: 2
3303
- });
3304
-
3305
- throw new Error(licenseCheck.message || 'Workflow recording not allowed');
3306
- }
3307
-
3308
- // Store userId for later usage tracking
3309
- const userId = licenseCheck.userId;
3310
- workflowUserIds.set(tabId, userId);
3311
-
3312
3425
  // Check if this tab allows content script injection
3313
3426
  const tab = await chrome.tabs.get(tabId);
3314
3427
  if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
@@ -3326,107 +3439,18 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
3326
3439
  const workflowId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3327
3440
  await chrome.storage.local.set({ currentWorkflowId: workflowId });
3328
3441
 
3329
- // Clear any existing logs for this tab
3442
+ // Clear any existing logs for this tab and initialize race-safe buffer
3330
3443
  await chrome.storage.session.set({ [`workflow_${tabId}`]: [] });
3331
-
3332
- // Inject main world console interceptor (similar to frame recording)
3333
- await chrome.scripting.executeScript({
3334
- target: { tabId: tabId },
3335
- world: 'MAIN',
3336
- func: () => {
3337
- // Check if already injected
3338
- if (window.__chromePilotWorkflowConsoleOverridden) {
3339
- return;
3340
- }
3341
- window.__chromePilotWorkflowConsoleOverridden = true;
3342
-
3343
- // Override console methods to capture logs
3344
- const originalLog = console.log;
3345
- const originalError = console.error;
3346
- const originalWarn = console.warn;
3347
- const originalInfo = console.info;
3348
-
3349
- const sendLog = (level, args) => {
3350
- try {
3351
- // Pre-serialize arguments to strings to avoid structured clone errors
3352
- const serializedArgs = args.map(arg => {
3353
- try {
3354
- if (arg === null) return 'null';
3355
- if (arg === undefined) return 'undefined';
3356
- if (typeof arg === 'function') return '[Function: ' + (arg.name || 'anonymous') + ']';
3357
- if (arg instanceof Element) return '[DOM Element: ' + arg.tagName + ']';
3358
- if (typeof arg === 'object') {
3359
- // Try to stringify, but limit depth to avoid circular references
3360
- return JSON.stringify(arg, null, 2);
3361
- }
3362
- return String(arg);
3363
- } catch (e) {
3364
- return '[Object: could not serialize]';
3365
- }
3366
- });
3367
-
3368
- // Post message to content script
3369
- window.postMessage({
3370
- type: 'chrome-pilot-workflow-console-log',
3371
- log: {
3372
- level,
3373
- message: serializedArgs.join(' '),
3374
- timestamp: Date.now()
3375
- }
3376
- }, '*');
3377
- } catch (e) {
3378
- // Ignore errors when sending logs
3379
- }
3380
- };
3381
-
3382
- console.log = (...args) => {
3383
- sendLog('log', args);
3384
- originalLog.apply(console, args);
3385
- };
3386
-
3387
- console.error = (...args) => {
3388
- sendLog('error', args);
3389
- originalError.apply(console, args);
3390
- };
3391
-
3392
- console.warn = (...args) => {
3393
- sendLog('warn', args);
3394
- originalWarn.apply(console, args);
3395
- };
3396
-
3397
- console.info = (...args) => {
3398
- sendLog('info', args);
3399
- originalInfo.apply(console, args);
3400
- };
3401
-
3402
- // Also capture window errors
3403
- window.addEventListener('error', (event) => {
3404
- sendLog('exception', [`${event.message} at ${event.filename}:${event.lineno}:${event.colno}`]);
3405
- });
3406
-
3407
- window.addEventListener('unhandledrejection', (event) => {
3408
- sendLog('exception', [`Unhandled Promise Rejection: ${event.reason}`]);
3409
- });
3410
- }
3411
- });
3412
-
3413
- // Inject content script to relay messages and handle workflow recording
3414
- await chrome.scripting.executeScript({
3415
- target: { tabId: tabId },
3416
- func: (includeLogsInExport) => {
3417
- // Listen for console log messages from main world
3418
- window.addEventListener('message', (event) => {
3419
- if (event.data && event.data.type === 'chrome-pilot-workflow-console-log') {
3420
- // Forward to background script
3421
- chrome.runtime.sendMessage({
3422
- action: 'workflowConsoleLog',
3423
- log: event.data.log
3424
- });
3425
- }
3426
- });
3427
- },
3428
- args: [includeLogsInExport]
3429
- });
3444
+
3445
+ // Initialize WorkflowLogBuffer for race-safe log handling
3446
+ if (includeLogsInExport) {
3447
+ const buffer = new WorkflowLogBuffer(tabId);
3448
+ workflowLogBuffers.set(tabId, buffer);
3449
+ console.log(`[Workflow] Initialized WorkflowLogBuffer for tab ${tabId}`);
3450
+ }
3451
+
3452
+ // REFACTORED: Use shared console interception library via proper namespace (replaces 150+ lines of duplicate code)
3453
+ await self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
3430
3454
 
3431
3455
  // Ensure content script is injected
3432
3456
  try {
@@ -3444,9 +3468,11 @@ async function startWorkflowRecording(tabId, includeLogsInExport, sessionName =
3444
3468
  }
3445
3469
 
3446
3470
  // Tell the content script to start recording
3471
+ // Include tabId so content script can use it when stopping recording
3447
3472
  await chrome.tabs.sendMessage(tabId, {
3448
3473
  action: 'startWorkflowRecording',
3449
- screenshotSettings: screenshotSettings
3474
+ screenshotSettings: screenshotSettings,
3475
+ tabId: tabId
3450
3476
  });
3451
3477
 
3452
3478
  console.log('Workflow recording started successfully');
@@ -3465,15 +3491,25 @@ async function stopWorkflowRecording(tabId) {
3465
3491
  throw new Error('No workflow recording active for this tab');
3466
3492
  }
3467
3493
 
3468
- // Get workflow from content script
3494
+ // Get workflow from content script using a different action to avoid circular dependency
3495
+ // (mini menu also sends 'stopWorkflowRecording' to background, creating confusion)
3469
3496
  const response = await chrome.tabs.sendMessage(tabId, {
3470
- action: 'stopWorkflowRecording'
3497
+ action: 'getWorkflowData'
3471
3498
  });
3472
-
3499
+
3473
3500
  if (!response || !response.success) {
3474
3501
  throw new Error('Failed to get workflow from content script');
3475
3502
  }
3476
-
3503
+
3504
+ // CRITICAL FIX: Restore original console methods and remove event listeners
3505
+ try {
3506
+ console.log('[Workflow] Stopping console log capture for tab:', tabId);
3507
+ await stopCapturingWorkflowLogs(tabId);
3508
+ } catch (cleanupError) {
3509
+ console.error('[Workflow] Failed to cleanup console interceptor:', cleanupError);
3510
+ // Continue with stop process even if cleanup fails
3511
+ }
3512
+
3477
3513
  // Handle both old format (array) and new format (object with actions and functionTraces)
3478
3514
  let workflowData = response.workflow || [];
3479
3515
  let workflow = Array.isArray(workflowData) ? workflowData : (workflowData.actions || []);
@@ -3494,22 +3530,38 @@ async function stopWorkflowRecording(tabId) {
3494
3530
  });
3495
3531
  });
3496
3532
 
3497
- // If logs should be included, get them from session storage
3533
+ // If logs should be included, flush buffer and get them from session storage
3498
3534
  if (workflowIncludeLogs.get(tabId)) {
3535
+ // CRITICAL: Flush any buffered logs before retrieval to ensure all logs are captured
3536
+ const buffer = workflowLogBuffers.get(tabId);
3537
+ if (buffer) {
3538
+ await buffer.flush();
3539
+ console.log(`[Workflow] Flushed buffer before retrieval, stats:`, buffer.getStats());
3540
+ }
3541
+
3499
3542
  const result = await chrome.storage.session.get(`workflow_${tabId}`);
3500
3543
  const logs = result[`workflow_${tabId}`] || [];
3501
3544
 
3502
- // Associate logs with workflow actions
3545
+ // Associate logs with workflow actions using NON-OVERLAPPING windows
3546
+ // Each log appears in exactly ONE action to avoid duplicates
3503
3547
  workflow = workflow.map((action, index) => {
3504
- // Find logs that occurred around this action
3548
+ // Calculate non-overlapping time window:
3549
+ // - Each action owns logs from ITS timestamp until the NEXT action's timestamp
3550
+ // - First action also includes 500ms before it
3551
+ // - Last action includes 5000ms after it
3552
+ const windowStart = index === 0
3553
+ ? action.timestamp - 500 // First action: include 500ms before
3554
+ : action.timestamp; // Other actions: start at this action's timestamp
3555
+
3556
+ const windowEnd = index < workflow.length - 1
3557
+ ? workflow[index + 1].timestamp // Until next action's timestamp
3558
+ : action.timestamp + 5000; // Last action: 5000ms after
3559
+
3560
+ // Use >= for start and < for end to ensure non-overlapping windows
3505
3561
  const actionLogs = logs.filter(log => {
3506
- // Get time window
3507
- const prevActionTime = index > 0 ? workflow[index - 1].timestamp : action.timestamp - 500;
3508
- const nextActionTime = index < workflow.length - 1 ? workflow[index + 1].timestamp : action.timestamp + 5000;
3509
-
3510
- return log.timestamp > prevActionTime && log.timestamp < nextActionTime;
3562
+ return log.timestamp >= windowStart && log.timestamp < windowEnd;
3511
3563
  });
3512
-
3564
+
3513
3565
  if (actionLogs.length > 0) {
3514
3566
  return { ...action, logs: actionLogs };
3515
3567
  }
@@ -3521,26 +3573,44 @@ async function stopWorkflowRecording(tabId) {
3521
3573
  const sessionName = workflowSessionNames.get(tabId);
3522
3574
  const screenshotSettings = workflowScreenshotSettings.get(tabId);
3523
3575
  const includeLogs = workflowIncludeLogs.get(tabId) || false;
3524
- const userId = workflowUserIds.get(tabId); // Get userId for usage tracking
3525
3576
 
3526
3577
  // Clean up
3527
3578
  workflowRecordingTabs.delete(tabId);
3528
3579
  workflowIncludeLogs.delete(tabId);
3529
3580
  workflowScreenshotSettings.delete(tabId);
3530
3581
  workflowSessionNames.delete(tabId);
3531
- workflowUserIds.delete(tabId); // Clean up userId tracking
3582
+
3583
+ // Clean up WorkflowLogBuffer
3584
+ const bufferToCleanup = workflowLogBuffers.get(tabId);
3585
+ if (bufferToCleanup) {
3586
+ await bufferToCleanup.cleanup();
3587
+ workflowLogBuffers.delete(tabId);
3588
+ console.log(`[Workflow] Cleaned up WorkflowLogBuffer for tab ${tabId}`);
3589
+ }
3590
+
3532
3591
  await chrome.storage.session.remove(`workflow_${tabId}`);
3533
-
3592
+
3593
+ // Notify popup and other extension components that recording has stopped
3594
+ // This is critical for popup UI sync when stopping from mini-menu
3595
+ await chrome.storage.local.set({
3596
+ workflowRecording: false,
3597
+ workflowStartTime: null
3598
+ });
3599
+
3534
3600
  console.log('Workflow recording stopped, returning workflow:', workflow);
3535
3601
 
3536
3602
  // Try to save to server
3537
3603
  try {
3538
3604
  const serverPorts = CONFIG_PORTS.slice(0, 5); // Use first 5 configured ports for workflow recording
3539
3605
  let serverResult = null;
3540
-
3606
+
3541
3607
  // Get current URL and title
3542
3608
  const tab = await chrome.tabs.get(tabId);
3543
- const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3609
+
3610
+ // Retrieve the workflow ID that was generated at start
3611
+ const storedData = await chrome.storage.local.get(['currentWorkflowId']);
3612
+ const sessionId = storedData.currentWorkflowId || `workflow_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3613
+ console.log('[Workflow] Using sessionId for save:', sessionId);
3544
3614
 
3545
3615
  for (const port of serverPorts) {
3546
3616
  try {
@@ -3584,14 +3654,6 @@ async function stopWorkflowRecording(tabId) {
3584
3654
  if (response.ok) {
3585
3655
  serverResult = await response.json();
3586
3656
  console.log(`[Workflow] Successfully saved to server on port ${port}`, serverResult);
3587
-
3588
- // STEP 2: Track usage AFTER successful workflow recording completion (same pattern as stopRecording)
3589
- if (userId) {
3590
- console.log('[License] Tracking usage after workflow recording completion...');
3591
- const usageResult = await LicenseHelper.trackUsageAfterRecording(userId);
3592
- console.log('[License] Usage tracking result:', usageResult);
3593
- }
3594
-
3595
3657
  break;
3596
3658
  } else {
3597
3659
  console.log(`[Workflow] Failed on port ${port}: ${response.status} ${response.statusText}`);
@@ -3604,10 +3666,13 @@ async function stopWorkflowRecording(tabId) {
3604
3666
  }
3605
3667
 
3606
3668
  if (serverResult) {
3669
+ // Clean up currentWorkflowId after successful save
3670
+ await chrome.storage.local.remove(['currentWorkflowId']);
3671
+
3607
3672
  return {
3608
3673
  success: true,
3609
3674
  workflow: {
3610
- sessionId: sessionId,
3675
+ sessionId: serverResult.workflowId || serverResult.sessionId || sessionId,
3611
3676
  url: tab.url,
3612
3677
  title: tab.title,
3613
3678
  actions: workflow,
@@ -3617,17 +3682,36 @@ async function stopWorkflowRecording(tabId) {
3617
3682
  serverResult: serverResult
3618
3683
  };
3619
3684
  } else {
3685
+ // Server not available - clean up but return local sessionId
3686
+ await chrome.storage.local.remove(['currentWorkflowId']);
3687
+
3620
3688
  return {
3621
3689
  success: true,
3622
- workflow: workflow,
3690
+ workflow: {
3691
+ sessionId: sessionId,
3692
+ url: tab.url,
3693
+ title: tab.title,
3694
+ actions: workflow,
3695
+ logs: []
3696
+ },
3623
3697
  savedToServer: false
3624
3698
  };
3625
3699
  }
3626
3700
  } catch (error) {
3627
3701
  console.error('Error saving workflow:', error);
3702
+ // Clean up on error too
3703
+ await chrome.storage.local.remove(['currentWorkflowId']);
3704
+
3705
+ // Return structured response even on error
3628
3706
  return {
3629
3707
  success: true,
3630
- workflow: workflow,
3708
+ workflow: {
3709
+ sessionId: sessionId,
3710
+ url: tab?.url || 'unknown',
3711
+ title: tab?.title || 'Untitled',
3712
+ actions: workflow,
3713
+ logs: []
3714
+ },
3631
3715
  savedToServer: false,
3632
3716
  error: error.message
3633
3717
  };
@@ -3638,6 +3722,13 @@ async function stopWorkflowRecording(tabId) {
3638
3722
  }
3639
3723
  }
3640
3724
 
3725
+ // CRITICAL FIX: Stop workflow console log capture and restore original methods
3726
+ // REFACTORED: Now uses shared console-interception-library.js via proper namespace
3727
+ async function stopCapturingWorkflowLogs(tabId) {
3728
+ console.log('[Workflow Recording] Stopping console interception using shared library');
3729
+ return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
3730
+ }
3731
+
3641
3732
  // Delete recording from server
3642
3733
  async function deleteRecordingFromServer(recordingId, sendResponse) {
3643
3734
  try {
@@ -3697,13 +3788,28 @@ async function retryPendingFrames(sessionId) {
3697
3788
 
3698
3789
  console.log(`[FrameQueue] Retrying ${pending.length} pending frame batches for session ${sessionId}`);
3699
3790
 
3791
+ // Check if this is still the active recording session
3792
+ if (currentSession?.sessionId !== sessionId) {
3793
+ console.log(`[FrameQueue] Session ${sessionId} is no longer active, clearing pending frames`);
3794
+ pendingFrameQueue.delete(sessionId);
3795
+ return;
3796
+ }
3797
+
3700
3798
  // Validate session now
3701
3799
  if (sessionManager) {
3702
3800
  const validationResult = await sessionManager.isSessionValid(sessionId);
3703
3801
  if (!validationResult.valid) {
3704
3802
  console.warn(`[FrameQueue] Session still invalid during retry: ${validationResult.reason}`);
3705
- // Try again after another delay
3706
- setTimeout(() => retryPendingFrames(sessionId), 200);
3803
+ // Try again after another delay (max 3 retries)
3804
+ const retryCount = pending[0]?.retryCount || 0;
3805
+ if (retryCount < 3) {
3806
+ // Mark all batches with retry count
3807
+ pending.forEach(batch => batch.retryCount = retryCount + 1);
3808
+ setTimeout(() => retryPendingFrames(sessionId), 200);
3809
+ } else {
3810
+ console.error(`[FrameQueue] Max retries (3) exceeded for session ${sessionId}, clearing queue`);
3811
+ pendingFrameQueue.delete(sessionId);
3812
+ }
3707
3813
  return;
3708
3814
  }
3709
3815
  }