@dynamicu/chromedebug-mcp 2.7.1 → 2.7.4

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.
@@ -131,6 +131,15 @@ const WORKFLOW_RECORDING_CONSOLE_CONFIG = {
131
131
  backgroundAction: 'workflowConsoleLog'
132
132
  };
133
133
 
134
+ // Console interception configuration for screenshot recording
135
+ const SCREENSHOT_RECORDING_CONSOLE_CONFIG = {
136
+ overrideFlagName: '__chromePilotScreenshotConsoleOverridden',
137
+ originalConsoleName: '__chromePilotScreenshotOriginalConsole',
138
+ relayFlagName: '__chromePilotScreenshotConsoleRelay',
139
+ messageType: 'chrome-debug-screenshot-console-log',
140
+ backgroundAction: 'screenshotConsoleLog'
141
+ };
142
+
134
143
  // Frame queue for handling validation race conditions
135
144
  // v2.1.2: Prevents frame loss when lease renewal hasn't propagated yet
136
145
  const pendingFrameQueue = new Map(); // sessionId -> array of frame batches
@@ -607,7 +616,7 @@ class LogTabBuffer {
607
616
  * could make LogTabBuffer accept a configurable storage key prefix to eliminate duplication.
608
617
  */
609
618
  class WorkflowLogBuffer {
610
- constructor(tabId) {
619
+ constructor(tabId, customStorageKey = null) {
611
620
  this.tabId = tabId;
612
621
  this.buffer = [];
613
622
  this.processing = false;
@@ -615,7 +624,7 @@ class WorkflowLogBuffer {
615
624
  this.totalLogsAdded = 0;
616
625
  this.totalLogsFlushed = 0;
617
626
  this.lastFlushTime = 0;
618
- this.storageKey = `workflow_${tabId}`;
627
+ this.storageKey = customStorageKey || `workflow_${tabId}`;
619
628
  }
620
629
 
621
630
  // Add log to buffer with queuing to prevent race conditions
@@ -805,6 +814,23 @@ const DATA_PROTECTION_TIERS = {
805
814
  }
806
815
  };
807
816
 
817
+ // Centralized screenshot capture throttling
818
+ // Chrome's captureVisibleTab has a rate limit of ~2 calls/second
819
+ let lastCaptureTime = 0;
820
+ const CAPTURE_MIN_INTERVAL_MS = 550; // Slightly under Chrome's limit for safety margin
821
+
822
+ async function throttledCaptureVisibleTab(windowId, options) {
823
+ const now = Date.now();
824
+ const elapsed = now - lastCaptureTime;
825
+
826
+ if (elapsed < CAPTURE_MIN_INTERVAL_MS) {
827
+ await new Promise(r => setTimeout(r, CAPTURE_MIN_INTERVAL_MS - elapsed));
828
+ }
829
+
830
+ lastCaptureTime = Date.now();
831
+ return chrome.tabs.captureVisibleTab(windowId, options);
832
+ }
833
+
808
834
  // Initialize on startup
809
835
  async function initializeServices() {
810
836
  try {
@@ -914,6 +940,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
914
940
  const handledByOtherListener = [
915
941
  'startWorkflowRecording',
916
942
  'stopWorkflowRecording',
943
+ 'startScreenshotRecording', // v2.7.3: screenshot recording mode
944
+ 'stopScreenshotRecording', // v2.7.3: screenshot recording mode
945
+ 'captureScreenshotForMode', // v2.7.3: screenshot capture from content script
917
946
  'sendToServer',
918
947
  'getScreenshotFormats',
919
948
  'saveWorkflowRecording',
@@ -931,6 +960,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
931
960
  'openFrameEditor',
932
961
  'checkConnection',
933
962
  'workflowConsoleLog', // v2.0.8: handle workflow console logs
963
+ 'screenshotConsoleLog', // v2.7.3: handle screenshot console logs
934
964
  'workflowAction', // v2.0.8: handle workflow actions
935
965
  'renewLease', // v2.1.2: session manager lease renewal for frame capture
936
966
  'frameSessionComplete', // Frame recording completion notification
@@ -1080,10 +1110,13 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1080
1110
  // Visual feedback system message handlers
1081
1111
  case 'start-screen-capture-tracking':
1082
1112
  // Forward to content script for visual feedback
1083
- if (message.sessionId && sender.tab) {
1084
- chrome.tabs.sendMessage(sender.tab.id, {
1113
+ // Use message.tabId for offscreen documents (sender.tab is undefined for offscreen)
1114
+ const startTrackingTabId = message.tabId || sender.tab?.id;
1115
+ if (message.sessionId && startTrackingTabId) {
1116
+ chrome.tabs.sendMessage(startTrackingTabId, {
1085
1117
  type: 'start-screen-capture-tracking',
1086
- sessionId: message.sessionId
1118
+ sessionId: message.sessionId,
1119
+ videoMode: message.videoMode || false
1087
1120
  }).catch(() => {
1088
1121
  // Content script might not be available
1089
1122
  // console.log('[Background] Could not forward start-screen-capture-tracking to content script');
@@ -1094,8 +1127,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1094
1127
 
1095
1128
  case 'stop-screen-capture-tracking':
1096
1129
  // Forward to content script
1097
- if (sender.tab) {
1098
- chrome.tabs.sendMessage(sender.tab.id, {
1130
+ // Use message.tabId for offscreen documents (sender.tab is undefined for offscreen)
1131
+ const stopTrackingTabId = message.tabId || sender.tab?.id;
1132
+ if (stopTrackingTabId) {
1133
+ chrome.tabs.sendMessage(stopTrackingTabId, {
1099
1134
  type: 'stop-screen-capture-tracking'
1100
1135
  }).catch(() => {
1101
1136
  // console.log('[Background] Could not forward stop-screen-capture-tracking to content script');
@@ -1150,8 +1185,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1150
1185
 
1151
1186
  case 'show-screen-capture-flash':
1152
1187
  // Forward flash trigger to content script
1153
- if (sender.tab) {
1154
- chrome.tabs.sendMessage(sender.tab.id, {
1188
+ // Use message.tabId for offscreen documents (sender.tab is undefined for offscreen)
1189
+ const flashTabId = message.tabId || sender.tab?.id;
1190
+ if (flashTabId) {
1191
+ chrome.tabs.sendMessage(flashTabId, {
1155
1192
  type: 'screen-capture-frame-captured'
1156
1193
  }).catch(() => {
1157
1194
  // console.log('[Background] Could not forward frame flash to content script');
@@ -1280,6 +1317,21 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1280
1317
  }
1281
1318
  break;
1282
1319
 
1320
+ case 'deleteAllBrowserRecordings':
1321
+ // Delete all browser-only recordings
1322
+ if (browserRecordingManager) {
1323
+ browserRecordingManager.deleteAllRecordings().then(result => {
1324
+ sendResponse(result);
1325
+ }).catch(error => {
1326
+ console.error('[Background] Failed to delete all recordings:', error);
1327
+ sendResponse({ success: false, deletedCount: 0, error: error.message });
1328
+ });
1329
+ return true; // Async response
1330
+ } else {
1331
+ sendResponse({ success: false, deletedCount: 0, error: 'Manager not available' });
1332
+ }
1333
+ break;
1334
+
1283
1335
  case 'getBrowserRecordingFrames':
1284
1336
  // Get frames for a browser-only recording
1285
1337
  if (dataBuffer && message.sessionId) {
@@ -1375,12 +1427,35 @@ chrome.runtime.onInstalled.addListener(async (details) => {
1375
1427
  chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1376
1428
  // Inject console logging on navigation during recording
1377
1429
  if (changeInfo.status === 'loading') {
1378
- // Use session manager validation if available
1430
+ // Skip restricted URLs
1431
+ if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://')) {
1432
+ return;
1433
+ }
1434
+
1435
+ // Check for WORKFLOW recording first (takes priority)
1436
+ if (workflowRecordingTabs.has(tabId) && workflowIncludeLogs.get(tabId)) {
1437
+ // console.log('[Navigation] Re-injecting workflow console logging for tab:', tabId);
1438
+ self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG).catch(err => {
1439
+ console.error('Failed to re-inject workflow console logging:', err);
1440
+ });
1441
+ return; // Don't also inject screen recording logs
1442
+ }
1443
+
1444
+ // Check for SCREENSHOT recording (second priority)
1445
+ if (screenshotRecordingTabs.has(tabId)) {
1446
+ // console.log('[Navigation] Re-injecting screenshot console logging for tab:', tabId);
1447
+ self.ConsoleInterceptionLibrary.startConsoleInterception(tabId, SCREENSHOT_RECORDING_CONSOLE_CONFIG).catch(err => {
1448
+ console.error('Failed to re-inject screenshot console logging:', err);
1449
+ });
1450
+ return; // Don't also inject screen recording logs
1451
+ }
1452
+
1453
+ // Check for SCREEN recording
1379
1454
  if (sessionManager) {
1380
1455
  isCurrentlyRecordingAsync().then(async (recording) => {
1381
1456
  const currentTabId = await getCurrentTabIdAsync();
1382
- if (recording && tabId === currentTabId && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) {
1383
- // console.log('Re-injecting console logging for navigation during recording');
1457
+ if (recording && tabId === currentTabId) {
1458
+ // console.log('[Navigation] Re-injecting screen recording console logging for tab:', tabId);
1384
1459
  startCapturingLogs(tabId).catch(err => {
1385
1460
  console.error('Failed to re-inject console logging:', err);
1386
1461
  });
@@ -1389,9 +1464,9 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1389
1464
  console.error('Error checking recording state for navigation:', error);
1390
1465
  });
1391
1466
  } else {
1392
- // Legacy fallback
1393
- if (tabId === recordingTabId && isCurrentlyRecording && tab.url && !tab.url.startsWith('chrome://') && !tab.url.startsWith('chrome-extension://')) {
1394
- // console.log('Re-injecting console logging for navigation during recording');
1467
+ // Legacy fallback for screen recording
1468
+ if (tabId === recordingTabId && isCurrentlyRecording) {
1469
+ // console.log('[Navigation] Re-injecting screen recording console logging (legacy) for tab:', tabId);
1395
1470
  startCapturingLogs(tabId).catch(err => {
1396
1471
  console.error('Failed to re-inject console logging:', err);
1397
1472
  });
@@ -1501,6 +1576,40 @@ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
1501
1576
  console.error(`[Workflow] Error updating storage for closed tab ${tabId}:`, err);
1502
1577
  });
1503
1578
  }
1579
+
1580
+ // Clean up any screenshot recording state for closed tabs
1581
+ if (screenshotRecordingTabs.has(tabId)) {
1582
+ // console.log(`[Screenshot] Tab ${tabId} closed during recording, cleaning up`);
1583
+ screenshotRecordingTabs.delete(tabId);
1584
+ screenshotRecordingSettings.delete(tabId);
1585
+ screenshotRecordingSessionIds.delete(tabId);
1586
+
1587
+ // Clean up screenshot log buffer
1588
+ const screenshotBuffer = screenshotLogBuffers.get(tabId);
1589
+ if (screenshotBuffer) {
1590
+ screenshotBuffer.cleanup().then(() => {
1591
+ screenshotLogBuffers.delete(tabId);
1592
+ // console.log(`[Screenshot] Cleaned up log buffer for closed tab ${tabId}`);
1593
+ }).catch(err => {
1594
+ console.error(`[Screenshot] Error cleaning up buffer for closed tab ${tabId}:`, err);
1595
+ screenshotLogBuffers.delete(tabId);
1596
+ });
1597
+ }
1598
+
1599
+ // Remove session storage
1600
+ chrome.storage.session.remove(`screenshot_logs_${tabId}`).catch(err => {
1601
+ console.error(`[Screenshot] Error removing session storage for closed tab ${tabId}:`, err);
1602
+ });
1603
+
1604
+ // Notify popup that recording has stopped (tab was closed)
1605
+ chrome.storage.local.set({
1606
+ screenshotRecording: false,
1607
+ screenshotStartTime: null,
1608
+ screenshotSessionId: null
1609
+ }).catch(err => {
1610
+ console.error(`[Screenshot] Error updating storage for closed tab ${tabId}:`, err);
1611
+ });
1612
+ }
1504
1613
  });
1505
1614
 
1506
1615
  // Session Manager for robust recording state management
@@ -1519,10 +1628,14 @@ let isCurrentlyRecording = false; // DEPRECATED: Use isCurrentlyRecordingAsync()
1519
1628
  let currentRecordingSessionId = null; // DEPRECATED: Use getCurrentSessionIdAsync() instead
1520
1629
  let frameCounter = new Map(); // DEPRECATED: Session manager handles frame counting
1521
1630
 
1631
+ // Session metadata cache - preserves essential settings (like videoMode) after currentSession is cleared
1632
+ // This allows in-flight frame batches to access session settings even after stopRecording() executes
1633
+ const sessionMetadataCache = new Map();
1634
+
1522
1635
  // =============================================================================
1523
1636
  // INACTIVITY AUTO-STOP - Stop recording if user is inactive
1524
1637
  // =============================================================================
1525
- const DEFAULT_INACTIVITY_TIMEOUT_MS = 10 * 1000; // 10 seconds default
1638
+ const DEFAULT_INACTIVITY_TIMEOUT_MS = 60 * 1000; // 60 seconds default (1 minute)
1526
1639
  let lastUserActivityTime = Date.now();
1527
1640
  let inactivityCheckInterval = null;
1528
1641
  let currentInactivityTimeout = DEFAULT_INACTIVITY_TIMEOUT_MS;
@@ -1546,7 +1659,7 @@ async function startInactivityMonitoring(tabId) {
1546
1659
 
1547
1660
  // Load user's configured timeout from storage
1548
1661
  const settings = await chrome.storage.sync.get(['inactivityTimeout']);
1549
- currentInactivityTimeout = (settings.inactivityTimeout || 10) * 1000; // Convert seconds to ms (default 10s)
1662
+ currentInactivityTimeout = (settings.inactivityTimeout || 60) * 1000; // Convert seconds to ms (default 60s)
1550
1663
 
1551
1664
  lastUserActivityTime = Date.now();
1552
1665
 
@@ -1784,6 +1897,12 @@ let workflowScreenshotSettings = new Map(); // Map of tabId to screenshot settin
1784
1897
  let workflowSessionNames = new Map(); // Map of tabId to session name
1785
1898
  let workflowLogBuffers = new Map(); // Map of tabId to WorkflowLogBuffer instance
1786
1899
 
1900
+ // Screenshot recording state (click-to-capture mode with countdown)
1901
+ let screenshotRecordingTabs = new Map(); // Map of tabId to recording state
1902
+ let screenshotRecordingSettings = new Map(); // Map of tabId to {countdown, hideCursor}
1903
+ let screenshotRecordingSessionIds = new Map(); // Map of tabId to sessionId
1904
+ let screenshotLogBuffers = new Map(); // Map of tabId to WorkflowLogBuffer instance for console logs
1905
+
1787
1906
  //=============================================================================
1788
1907
  // COMPREHENSIVE ERROR HANDLING SYSTEM
1789
1908
  // Designed collaboratively with Second Opinion Analyst and Testing Expert
@@ -2388,6 +2507,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2388
2507
  }
2389
2508
 
2390
2509
  if (request.action === 'startRecording') {
2510
+ console.log('[Background] startRecording received settings:', request.settings);
2391
2511
  startRecording(request.tabId, request.settings).then(() => {
2392
2512
  sendResponse({ success: true });
2393
2513
  }).catch((error) => {
@@ -2696,7 +2816,52 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2696
2816
  });
2697
2817
  return true;
2698
2818
  }
2699
-
2819
+
2820
+ // Screenshot recording handlers (v2.7.3)
2821
+ if (request.action === 'startScreenshotRecording') {
2822
+ // console.log('[Screenshot] Starting screenshot recording for tab:', request.tabId);
2823
+ startScreenshotRecording(
2824
+ request.tabId,
2825
+ request.settings
2826
+ ).then((result) => {
2827
+ sendResponse({ success: true, sessionId: result.sessionId });
2828
+ }).catch((error) => {
2829
+ console.error('[Screenshot] Error starting screenshot recording:', error);
2830
+ sendResponse({ success: false, error: error.message });
2831
+ });
2832
+ return true;
2833
+ }
2834
+
2835
+ if (request.action === 'stopScreenshotRecording') {
2836
+ const tabId = request.tabId || sender.tab?.id;
2837
+ // console.log('[Screenshot] Stopping screenshot recording for tab:', tabId);
2838
+ stopScreenshotRecording(tabId).then(async (result) => {
2839
+ // Track usage for FREE tier after successful recording
2840
+ if (result.success) {
2841
+ const usageResult = await incrementUsageAfterRecording();
2842
+ result.usage = usageResult;
2843
+ }
2844
+ sendResponse(result);
2845
+ }).catch((error) => {
2846
+ console.error('[Screenshot] Error stopping screenshot recording:', error);
2847
+ sendResponse({ success: false, error: error.message });
2848
+ });
2849
+ return true;
2850
+ }
2851
+
2852
+ // Handle screenshot capture requests from content script
2853
+ if (request.action === 'captureScreenshotForMode') {
2854
+ const tabId = request.tabId || sender.tab?.id;
2855
+ // console.log('[Screenshot] Capturing screenshot for tab:', tabId);
2856
+ captureHighQualityScreenshot(tabId).then((screenshot) => {
2857
+ sendResponse({ success: true, screenshot: screenshot });
2858
+ }).catch((error) => {
2859
+ console.error('[Screenshot] Error capturing screenshot:', error);
2860
+ sendResponse({ success: false, error: error.message });
2861
+ });
2862
+ return true;
2863
+ }
2864
+
2700
2865
  // Handle messages from offscreen document
2701
2866
  if (request.target === 'background') {
2702
2867
  if (request.type === 'frame-batch-ready') {
@@ -2705,10 +2870,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2705
2870
  handleFrameCaptureComplete(request.data);
2706
2871
  } else if (request.type === 'show-frame-flash') {
2707
2872
  // Relay flash message to content script
2873
+ // console.log('[Background] Relaying frame-flash to tab:', request.tabId);
2708
2874
  chrome.tabs.sendMessage(request.tabId, {
2709
2875
  action: 'showFrameFlash'
2710
- }).catch(() => {
2876
+ }).then(() => {
2877
+ // console.log('[Background] Frame flash message sent successfully to tab:', request.tabId);
2878
+ }).catch((error) => {
2711
2879
  // Content script might not be available, that's ok
2880
+ // console.log('[Background] Frame flash failed for tab:', request.tabId, error?.message);
2712
2881
  });
2713
2882
  }
2714
2883
  }
@@ -2809,7 +2978,23 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2809
2978
  }
2810
2979
  }
2811
2980
  }
2812
-
2981
+
2982
+ // Buffer screenshot console logs using race-safe WorkflowLogBuffer
2983
+ if (request.action === 'screenshotConsoleLog' && sender.tab) {
2984
+ const tabId = sender.tab.id;
2985
+ if (screenshotRecordingTabs.has(tabId)) {
2986
+ const buffer = screenshotLogBuffers.get(tabId);
2987
+ if (buffer) {
2988
+ // Use race-safe buffer to prevent read-modify-write race conditions
2989
+ buffer.addLog(request.log).catch(err => {
2990
+ console.error('[ScreenshotLogBuffer] Error adding log:', err);
2991
+ });
2992
+ } else {
2993
+ console.warn(`[Screenshot] No buffer found for tab ${tabId}, log dropped`);
2994
+ }
2995
+ }
2996
+ }
2997
+
2813
2998
  // Handle getCookies for restore points
2814
2999
  if (request.action === 'getCookies' && sender.tab) {
2815
3000
  chrome.cookies.getAll({ url: request.url }, (cookies) => {
@@ -2919,10 +3104,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2919
3104
  // console.log('[SCREENSHOT-DEBUG] background.js - Sending response with success: true');
2920
3105
  sendResponse({ success: true, screenshotData });
2921
3106
  }).catch((error) => {
2922
- console.error('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot FAILED');
2923
- console.error('[SCREENSHOT-DEBUG] background.js - Error:', error);
2924
- console.error('[SCREENSHOT-DEBUG] background.js - Error message:', error.message);
2925
- console.error('[SCREENSHOT-DEBUG] background.js - Error stack:', error.stack);
3107
+ // Rate limit errors are expected and handled by throttling - don't spam console
3108
+ if (!error.message?.includes('MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND')) {
3109
+ console.error('[Screenshot] Workflow capture failed:', error.message);
3110
+ }
2926
3111
  sendResponse({ success: false, error: error.message });
2927
3112
  });
2928
3113
  return true;
@@ -2956,7 +3141,7 @@ async function takeStandaloneSnapshot(tabId, note = '') {
2956
3141
  const sessionId = `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2957
3142
 
2958
3143
  // Capture screenshot
2959
- const screenshotData = await chrome.tabs.captureVisibleTab(null, {
3144
+ const screenshotData = await throttledCaptureVisibleTab(null, {
2960
3145
  format: 'jpeg',
2961
3146
  quality: 30 // Default quality for snapshots
2962
3147
  });
@@ -3069,7 +3254,7 @@ async function captureTabScreenshot(tabId, settings) {
3069
3254
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureOptions:', JSON.stringify(captureOptions));
3070
3255
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Calling chrome.tabs.captureVisibleTab...');
3071
3256
 
3072
- const dataUrl = await chrome.tabs.captureVisibleTab(windowId, captureOptions);
3257
+ const dataUrl = await throttledCaptureVisibleTab(windowId, captureOptions);
3073
3258
 
3074
3259
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureVisibleTab SUCCESS');
3075
3260
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl length:', dataUrl?.length);
@@ -3121,10 +3306,12 @@ async function captureTabScreenshot(tabId, settings) {
3121
3306
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Final dataUrl length:', dataUrl?.length);
3122
3307
  return dataUrl;
3123
3308
  } catch (error) {
3124
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - EXCEPTION CAUGHT');
3125
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error:', error);
3126
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error message:', error.message);
3127
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error stack:', error.stack);
3309
+ // Only log unexpected errors, rate limits are handled by throttling
3310
+ if (error.message?.includes('MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND')) {
3311
+ console.warn('[Screenshot] Rate limited by Chrome, throttling applied');
3312
+ } else {
3313
+ console.error('[Screenshot] Capture failed:', error.message);
3314
+ }
3128
3315
  throw error;
3129
3316
  }
3130
3317
  }
@@ -3251,6 +3438,13 @@ async function startRecording(tabId, settings = {}) {
3251
3438
  currentOwnerId = sessionResult.ownerId;
3252
3439
 
3253
3440
  const sessionId = currentSession.sessionId;
3441
+
3442
+ // Cache essential metadata for this session (persists after currentSession is cleared)
3443
+ // This ensures in-flight frame batches can access videoMode even after stopRecording()
3444
+ sessionMetadataCache.set(sessionId, {
3445
+ videoMode: settings.videoMode || false,
3446
+ sessionName: settings.sessionName || null
3447
+ });
3254
3448
  const scheduledStartTime = Date.now() + 3000; // Schedule start 3 seconds in future for countdown synchronization
3255
3449
 
3256
3450
  // STEP 4: Initialize browser-only recording if in browser-only mode
@@ -3261,7 +3455,8 @@ async function startRecording(tabId, settings = {}) {
3261
3455
  tabId: tabId,
3262
3456
  url: tab.url,
3263
3457
  title: tab.title || settings.sessionName || 'Browser Recording',
3264
- startTime: scheduledStartTime
3458
+ startTime: scheduledStartTime,
3459
+ isVideoMode: settings.videoMode || false
3265
3460
  });
3266
3461
  }
3267
3462
 
@@ -3277,12 +3472,15 @@ async function startRecording(tabId, settings = {}) {
3277
3472
  logStreamer.setSessionId(sessionId);
3278
3473
  }
3279
3474
 
3280
- // Update storage
3475
+ // Update storage - include isVideoModeActive to prevent race condition with popup
3476
+ // The popup's storage listener checks for recordingActive changes and calls updateRecordingUI()
3477
+ // If isVideoModeActive isn't included, the popup will show wrong button states
3281
3478
  chrome.storage.local.set({
3282
3479
  recordingActive: true,
3283
3480
  recordingStartTime: scheduledStartTime,
3284
3481
  recordingTabId: tabId,
3285
- recordingSessionId: sessionId
3482
+ recordingSessionId: sessionId,
3483
+ isVideoModeActive: settings.videoMode || false
3286
3484
  });
3287
3485
 
3288
3486
  // Update badge
@@ -3315,10 +3513,12 @@ async function startRecording(tabId, settings = {}) {
3315
3513
  }
3316
3514
 
3317
3515
  // Notify content script that recording has started with scheduled time
3516
+ console.log('[Background] Sending recordingStarted to content with videoMode:', settings.videoMode);
3318
3517
  chrome.tabs.sendMessage(tabId, {
3319
3518
  action: 'recordingStarted',
3320
3519
  scheduledStartTime: scheduledStartTime,
3321
- sessionId: sessionId
3520
+ sessionId: sessionId,
3521
+ videoMode: settings.videoMode || false // Pass video mode for hidden indicator
3322
3522
  }).catch(async (error) => {
3323
3523
  // Content script might not be injected, try to inject it
3324
3524
  // console.log('Could not notify content script, attempting to inject:', error);
@@ -3344,7 +3544,8 @@ async function startRecording(tabId, settings = {}) {
3344
3544
  chrome.tabs.sendMessage(tabId, {
3345
3545
  action: 'recordingStarted',
3346
3546
  scheduledStartTime: scheduledStartTime,
3347
- sessionId: sessionId
3547
+ sessionId: sessionId,
3548
+ videoMode: settings.videoMode || false
3348
3549
  }).catch(() => {
3349
3550
  console.error('Failed to show recording indicator even after injection');
3350
3551
  });
@@ -3360,12 +3561,22 @@ async function startRecording(tabId, settings = {}) {
3360
3561
  await browserRecordingManager.startRecording(sessionId, {
3361
3562
  tabId,
3362
3563
  url: tab.url,
3363
- title: tab.title
3564
+ title: tab.title,
3565
+ isVideoMode: settings.videoMode || false
3364
3566
  });
3365
3567
  // console.log('[Recording] Browser recording initialized for session:', sessionId);
3366
3568
  }
3367
3569
 
3368
3570
  // Send stream ID, session info, and settings to offscreen document for frame capture
3571
+ const frameCaptureSettings = {
3572
+ frameRate: settings.frameRate || 1,
3573
+ imageQuality: settings.imageQuality || (serverMode === 'browser-only' ? 15 : 30),
3574
+ frameFlash: settings.frameFlash !== false,
3575
+ maxWidth: settings.maxWidth, // Pass through for video mode (undefined uses default)
3576
+ maxHeight: settings.maxHeight, // Pass through for video mode (undefined uses default)
3577
+ videoMode: settings.videoMode || false
3578
+ };
3579
+ console.log('[Background] Sending frame capture settings:', frameCaptureSettings);
3369
3580
  await chrome.runtime.sendMessage({
3370
3581
  type: 'start-frame-capture',
3371
3582
  target: 'offscreen',
@@ -3375,11 +3586,7 @@ async function startRecording(tabId, settings = {}) {
3375
3586
  ownerId: currentOwnerId,
3376
3587
  scheduledStartTime: scheduledStartTime,
3377
3588
  mode: serverMode, // Pass mode to offscreen document
3378
- settings: {
3379
- frameRate: settings.frameRate || 1,
3380
- imageQuality: settings.imageQuality || (serverMode === 'browser-only' ? 15 : 30),
3381
- frameFlash: settings.frameFlash !== false
3382
- }
3589
+ settings: frameCaptureSettings
3383
3590
  });
3384
3591
 
3385
3592
  // console.log('Recording started in', serverMode, 'mode');
@@ -3514,10 +3721,20 @@ async function stopRecording() {
3514
3721
  }
3515
3722
  }
3516
3723
 
3724
+ // Note: stoppedSessionId was already captured at the start of stopRecording()
3725
+ // It's used below for deferred cache cleanup
3726
+
3517
3727
  // Clear session cache
3518
3728
  currentSession = null;
3519
3729
  currentOwnerId = null;
3520
3730
 
3731
+ // Schedule deferred cleanup of metadata cache (in-flight batches may still need it)
3732
+ if (stoppedSessionId && sessionMetadataCache.has(stoppedSessionId)) {
3733
+ setTimeout(() => {
3734
+ sessionMetadataCache.delete(stoppedSessionId);
3735
+ }, 10000); // 10 second delay to allow in-flight batches to complete
3736
+ }
3737
+
3521
3738
  // Update legacy state for backward compatibility (will be removed)
3522
3739
  isCurrentlyRecording = false;
3523
3740
  recordingTabId = null;
@@ -3816,7 +4033,8 @@ async function stopWorkflowRecording(tabId) {
3816
4033
  screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3817
4034
  actions: workflow,
3818
4035
  logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3819
- functionTraces: functionTraces // Include function execution traces
4036
+ functionTraces: functionTraces, // Include function execution traces
4037
+ recordingMode: 'workflow' // Explicitly mark as workflow recording for tab filtering
3820
4038
  };
3821
4039
 
3822
4040
  // console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - HTTP POST payload being prepared');
@@ -3920,6 +4138,278 @@ async function stopCapturingWorkflowLogs(tabId) {
3920
4138
  return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
3921
4139
  }
3922
4140
 
4141
+ // ============================================================
4142
+ // SCREENSHOT RECORDING FUNCTIONS (v2.7.3)
4143
+ // Click-to-capture mode with configurable countdown
4144
+ // ============================================================
4145
+
4146
+ /**
4147
+ * Start screenshot recording mode for a tab
4148
+ * @param {number} tabId - The tab ID to start recording on
4149
+ * @param {object} settings - Recording settings {countdown: number, hideCursor: boolean}
4150
+ */
4151
+ async function startScreenshotRecording(tabId, settings = {}) {
4152
+ try {
4153
+ // console.log('[Screenshot] Starting screenshot recording for tab:', tabId, 'settings:', settings);
4154
+
4155
+ // Check if this tab allows content script injection
4156
+ const tab = await chrome.tabs.get(tabId);
4157
+ if (!tab.url || tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') || tab.url.startsWith('moz-extension://')) {
4158
+ throw new Error('Cannot record screenshots on restricted pages (chrome:// URLs)');
4159
+ }
4160
+
4161
+ // Generate session ID
4162
+ const sessionId = `screenshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
4163
+
4164
+ // Store settings
4165
+ screenshotRecordingTabs.set(tabId, true);
4166
+ screenshotRecordingSettings.set(tabId, settings);
4167
+ screenshotRecordingSessionIds.set(tabId, sessionId);
4168
+
4169
+ // Store session ID for recovery
4170
+ await chrome.storage.local.set({ currentScreenshotSessionId: sessionId });
4171
+
4172
+ // Initialize console log capture for screenshot recording
4173
+ // Clear any existing logs and set up log buffer
4174
+ await chrome.storage.session.set({ [`screenshot_logs_${tabId}`]: [] });
4175
+ const buffer = new WorkflowLogBuffer(tabId, `screenshot_logs_${tabId}`);
4176
+ screenshotLogBuffers.set(tabId, buffer);
4177
+
4178
+ // Ensure content script is injected before sending message
4179
+ try {
4180
+ await chrome.scripting.executeScript({
4181
+ target: { tabId: tabId },
4182
+ files: ['content.js']
4183
+ });
4184
+ await chrome.scripting.insertCSS({
4185
+ target: { tabId: tabId },
4186
+ files: ['content.css']
4187
+ });
4188
+ } catch (e) {
4189
+ // Content script might already be injected - this is fine
4190
+ // console.log('[Screenshot] Content script injection note:', e.message);
4191
+ }
4192
+
4193
+ // Start console interception for log capture
4194
+ await self.ConsoleInterceptionLibrary.startConsoleInterception(
4195
+ tabId,
4196
+ SCREENSHOT_RECORDING_CONSOLE_CONFIG
4197
+ );
4198
+
4199
+ // Tell the content script to start screenshot recording mode
4200
+ await chrome.tabs.sendMessage(tabId, {
4201
+ action: 'startScreenshotRecording',
4202
+ settings: settings,
4203
+ tabId: tabId,
4204
+ sessionId: sessionId
4205
+ });
4206
+
4207
+ // console.log('[Screenshot] Screenshot recording started successfully, sessionId:', sessionId);
4208
+ return { success: true, sessionId: sessionId };
4209
+ } catch (error) {
4210
+ console.error('[Screenshot] Error starting screenshot recording:', error);
4211
+ throw error;
4212
+ }
4213
+ }
4214
+
4215
+ /**
4216
+ * Stop screenshot recording and save to server
4217
+ * @param {number} tabId - The tab ID to stop recording on
4218
+ */
4219
+ async function stopScreenshotRecording(tabId) {
4220
+ try {
4221
+ // console.log('[Screenshot] Stopping screenshot recording for tab:', tabId);
4222
+
4223
+ // Check if recording was active
4224
+ if (!screenshotRecordingTabs.has(tabId)) {
4225
+ throw new Error('No screenshot recording active for this tab');
4226
+ }
4227
+
4228
+ // Get screenshot data from content script
4229
+ const response = await chrome.tabs.sendMessage(tabId, {
4230
+ action: 'getScreenshotData'
4231
+ });
4232
+
4233
+ if (!response || !response.success) {
4234
+ throw new Error('Failed to get screenshots from content script');
4235
+ }
4236
+
4237
+ let screenshots = response.screenshots || [];
4238
+ // console.log(`[Screenshot] Retrieved ${screenshots.length} screenshots`);
4239
+
4240
+ // Get settings before cleanup
4241
+ const sessionId = screenshotRecordingSessionIds.get(tabId);
4242
+ const settings = screenshotRecordingSettings.get(tabId);
4243
+
4244
+ // Flush log buffer and get logs
4245
+ const buffer = screenshotLogBuffers.get(tabId);
4246
+ if (buffer) {
4247
+ await buffer.flush();
4248
+ }
4249
+
4250
+ // Get logs from session storage
4251
+ const storageKey = `screenshot_logs_${tabId}`;
4252
+ const logResult = await chrome.storage.session.get(storageKey);
4253
+ const allLogs = logResult[storageKey] || [];
4254
+ // console.log(`[Screenshot] Retrieved ${allLogs.length} console logs`);
4255
+
4256
+ // Associate logs with screenshots using timestamp windows
4257
+ // Window: 500ms before click to min(next click timestamp, current + 10s)
4258
+ screenshots = screenshots.map((screenshot, index) => {
4259
+ const windowStart = screenshot.timestamp - 500; // 500ms before click
4260
+ const maxWindowEnd = screenshot.timestamp + 10000; // 10s max window
4261
+ const nextTimestamp = index < screenshots.length - 1
4262
+ ? screenshots[index + 1].timestamp
4263
+ : screenshot.timestamp + 10000;
4264
+ const windowEnd = Math.min(nextTimestamp, maxWindowEnd);
4265
+
4266
+ const screenshotLogs = allLogs.filter(log =>
4267
+ log.timestamp >= windowStart && log.timestamp < windowEnd
4268
+ );
4269
+
4270
+ return {
4271
+ ...screenshot,
4272
+ logs: screenshotLogs
4273
+ };
4274
+ });
4275
+
4276
+ // Stop console interception
4277
+ try {
4278
+ await self.ConsoleInterceptionLibrary.stopConsoleInterception(
4279
+ tabId,
4280
+ SCREENSHOT_RECORDING_CONSOLE_CONFIG
4281
+ );
4282
+ } catch (e) {
4283
+ // Ignore errors when stopping interception
4284
+ }
4285
+
4286
+ // Clean up log buffers
4287
+ if (buffer) {
4288
+ await buffer.cleanup();
4289
+ }
4290
+ screenshotLogBuffers.delete(tabId);
4291
+ await chrome.storage.session.remove(storageKey);
4292
+
4293
+ // Clean up state
4294
+ screenshotRecordingTabs.delete(tabId);
4295
+ screenshotRecordingSettings.delete(tabId);
4296
+ screenshotRecordingSessionIds.delete(tabId);
4297
+
4298
+ // Notify popup that recording has stopped
4299
+ await chrome.storage.local.set({
4300
+ screenshotRecording: false,
4301
+ screenshotStartTime: null,
4302
+ screenshotSessionId: null
4303
+ });
4304
+
4305
+ // Get current URL and title
4306
+ const tab = await chrome.tabs.get(tabId);
4307
+
4308
+ // Try to save to server
4309
+ try {
4310
+ const serverPorts = CONFIG_PORTS.slice(0, 5);
4311
+ let serverResult = null;
4312
+
4313
+ for (const port of serverPorts) {
4314
+ try {
4315
+ // Convert screenshots to action format for workflow storage (with logs)
4316
+ const actions = screenshots.map((screenshot, index) => ({
4317
+ type: 'screenshot',
4318
+ timestamp: screenshot.timestamp,
4319
+ x: screenshot.x || 0,
4320
+ y: screenshot.y || 0,
4321
+ screenshot_data: screenshot.data,
4322
+ logs: screenshot.logs || [],
4323
+ index: index
4324
+ }));
4325
+
4326
+ const payloadData = {
4327
+ sessionId: sessionId,
4328
+ name: null, // Screenshots don't have custom names
4329
+ url: tab.url,
4330
+ title: tab.title,
4331
+ includeLogs: true, // Now capturing logs
4332
+ screenshotSettings: {
4333
+ enabled: true,
4334
+ format: 'jpeg',
4335
+ quality: 100 // High quality for screenshot mode
4336
+ },
4337
+ actions: actions,
4338
+ logs: allLogs, // Include all logs for the session
4339
+ functionTraces: [],
4340
+ recordingMode: 'screenshot' // Mark as screenshot recording
4341
+ };
4342
+
4343
+ const httpResponse = await fetch(`http://localhost:${port}/chromedebug/workflow-recording`, {
4344
+ method: 'POST',
4345
+ headers: {
4346
+ 'Content-Type': 'application/json'
4347
+ },
4348
+ body: JSON.stringify(payloadData)
4349
+ });
4350
+
4351
+ if (httpResponse.ok) {
4352
+ serverResult = await httpResponse.json();
4353
+ // console.log(`[Screenshot] Saved to server on port ${port}:`, serverResult);
4354
+ break;
4355
+ }
4356
+ } catch (error) {
4357
+ // console.log(`[Screenshot] Failed to save to server on port ${port}:`, error.message);
4358
+ }
4359
+ }
4360
+
4361
+ // Clean up session ID from storage
4362
+ await chrome.storage.local.remove(['currentScreenshotSessionId']);
4363
+
4364
+ if (serverResult) {
4365
+ return {
4366
+ success: true,
4367
+ screenshotCount: screenshots.length,
4368
+ sessionId: serverResult.workflowId || serverResult.sessionId || sessionId,
4369
+ savedToServer: true
4370
+ };
4371
+ } else {
4372
+ return {
4373
+ success: true,
4374
+ screenshotCount: screenshots.length,
4375
+ sessionId: sessionId,
4376
+ savedToServer: false
4377
+ };
4378
+ }
4379
+ } catch (error) {
4380
+ console.error('[Screenshot] Error saving screenshots:', error);
4381
+ await chrome.storage.local.remove(['currentScreenshotSessionId']);
4382
+ return {
4383
+ success: true,
4384
+ screenshotCount: screenshots.length,
4385
+ sessionId: sessionId,
4386
+ savedToServer: false,
4387
+ error: error.message
4388
+ };
4389
+ }
4390
+ } catch (error) {
4391
+ console.error('[Screenshot] Error stopping screenshot recording:', error);
4392
+ throw error;
4393
+ }
4394
+ }
4395
+
4396
+ /**
4397
+ * Capture a high-quality screenshot for screenshot recording mode
4398
+ * @param {number} tabId - The tab to capture
4399
+ */
4400
+ async function captureHighQualityScreenshot(tabId) {
4401
+ try {
4402
+ const dataUrl = await throttledCaptureVisibleTab(null, {
4403
+ format: 'jpeg',
4404
+ quality: 100 // Best quality for screenshot mode
4405
+ });
4406
+ return dataUrl;
4407
+ } catch (error) {
4408
+ console.error('[Screenshot] Error capturing screenshot:', error);
4409
+ throw error;
4410
+ }
4411
+ }
4412
+
3923
4413
  // Delete recording from server
3924
4414
  async function deleteRecordingFromServer(recordingId, sendResponse) {
3925
4415
  try {
@@ -4078,9 +4568,13 @@ async function handleFrameBatch(batchData, skipValidation = false) {
4078
4568
  const ports = await getServerPorts();
4079
4569
  let uploadSuccess = false;
4080
4570
  let lastError = null;
4081
-
4082
- // Get session name from current session
4083
- const sessionName = currentSession?.settings?.sessionName || null;
4571
+
4572
+ // Get session name and video mode from current session OR cached metadata
4573
+ // The cache persists after stopRecording() clears currentSession, ensuring
4574
+ // in-flight frame batches still have access to videoMode setting
4575
+ const cachedMetadata = sessionMetadataCache.get(batchData.sessionId);
4576
+ const sessionName = currentSession?.settings?.sessionName || cachedMetadata?.sessionName || null;
4577
+ const isVideoMode = currentSession?.settings?.videoMode ?? cachedMetadata?.videoMode ?? false;
4084
4578
 
4085
4579
  for (const port of ports) {
4086
4580
  try {
@@ -4090,6 +4584,8 @@ async function handleFrameBatch(batchData, skipValidation = false) {
4090
4584
  if (sessionName) {
4091
4585
  formData.append('sessionName', sessionName);
4092
4586
  }
4587
+ // Always include isVideoMode for proper recording classification
4588
+ formData.append('isVideoMode', isVideoMode.toString());
4093
4589
 
4094
4590
  const uploadResponse = await fetch(`http://localhost:${port}/chromedebug/frame-batch`, {
4095
4591
  method: 'POST',
@@ -4101,33 +4597,41 @@ async function handleFrameBatch(batchData, skipValidation = false) {
4101
4597
  // console.log(`Frame batch uploaded successfully to port ${port}:`, result);
4102
4598
  recordingServerPort = port; // Store the successful port
4103
4599
  uploadSuccess = true;
4104
-
4105
- // Also save frames to Chrome local storage
4106
- const storageKey = batchData.sessionId;
4107
- const existingData = await chrome.storage.local.get([storageKey]);
4108
- const sessionData = existingData[storageKey] || {
4109
- sessionId: batchData.sessionId,
4110
- frames: [],
4111
- timestamp: Date.now()
4112
- };
4113
-
4114
- // Append new frames
4115
- sessionData.frames = sessionData.frames.concat(batchData.frames);
4116
- await chrome.storage.local.set({ [storageKey]: sessionData });
4117
- // console.log(`Saved ${batchData.frames.length} frames to local storage, total: ${sessionData.frames.length}`);
4600
+
4601
+ // NOTE: Do NOT store frames to local storage in server mode!
4602
+ // Chrome extension storage.local has a 5MB quota limit.
4603
+ // At 4 FPS with ~100KB/frame, this fills in ~12 seconds.
4604
+ // Frames are already on the server, no backup needed.
4605
+
4606
+ // Track frame count in memory instead
4607
+ const currentCount = frameCounter.get(batchData.sessionId) || 0;
4608
+ const newCount = currentCount + batchData.frames.length;
4609
+ frameCounter.set(batchData.sessionId, newCount);
4118
4610
 
4119
4611
  // Send frame count update to content script
4120
4612
  if (recordingTabId) {
4121
4613
  try {
4122
4614
  await chrome.tabs.sendMessage(recordingTabId, {
4123
4615
  action: 'updateFrameCount',
4124
- frameCount: sessionData.frames.length
4616
+ frameCount: newCount
4125
4617
  });
4126
4618
  } catch (e) {
4127
4619
  // Tab might be closed, ignore
4128
4620
  }
4129
4621
  }
4130
4622
 
4623
+ // Clean up metadata cache if recording is stopped
4624
+ // Use a delay to ensure any remaining batches can still access the cache
4625
+ if (!isCurrentlyRecording && sessionMetadataCache.has(batchData.sessionId)) {
4626
+ const sessionToClean = batchData.sessionId;
4627
+ setTimeout(() => {
4628
+ // Only delete if this specific session is no longer active
4629
+ if (currentSession?.sessionId !== sessionToClean) {
4630
+ sessionMetadataCache.delete(sessionToClean);
4631
+ }
4632
+ }, 10000); // 10 second delay for safety margin
4633
+ }
4634
+
4131
4635
  break;
4132
4636
  } else {
4133
4637
  const errorText = await uploadResponse.text();
@@ -4653,11 +5157,12 @@ async function handleFrameCaptureComplete(sessionData) {
4653
5157
  frameCounter.delete(sessionData.sessionId); // Clean up frame counter
4654
5158
  screenInteractions = []; // Clear interactions
4655
5159
 
4656
- // Update storage
4657
- chrome.storage.local.set({
4658
- recordingActive: false,
5160
+ // Update storage - clear isVideoModeActive for consistency
5161
+ chrome.storage.local.set({
5162
+ recordingActive: false,
4659
5163
  recordingStartTime: null,
4660
- recordingTabId: null
5164
+ recordingTabId: null,
5165
+ isVideoModeActive: false
4661
5166
  });
4662
5167
 
4663
5168
  // Update badge