@dynamicu/chromedebug-mcp 2.7.2 → 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) {
@@ -1389,6 +1441,15 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1389
1441
  return; // Don't also inject screen recording logs
1390
1442
  }
1391
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
+
1392
1453
  // Check for SCREEN recording
1393
1454
  if (sessionManager) {
1394
1455
  isCurrentlyRecordingAsync().then(async (recording) => {
@@ -1515,6 +1576,40 @@ chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
1515
1576
  console.error(`[Workflow] Error updating storage for closed tab ${tabId}:`, err);
1516
1577
  });
1517
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
+ }
1518
1613
  });
1519
1614
 
1520
1615
  // Session Manager for robust recording state management
@@ -1533,6 +1628,10 @@ let isCurrentlyRecording = false; // DEPRECATED: Use isCurrentlyRecordingAsync()
1533
1628
  let currentRecordingSessionId = null; // DEPRECATED: Use getCurrentSessionIdAsync() instead
1534
1629
  let frameCounter = new Map(); // DEPRECATED: Session manager handles frame counting
1535
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
+
1536
1635
  // =============================================================================
1537
1636
  // INACTIVITY AUTO-STOP - Stop recording if user is inactive
1538
1637
  // =============================================================================
@@ -1798,6 +1897,12 @@ let workflowScreenshotSettings = new Map(); // Map of tabId to screenshot settin
1798
1897
  let workflowSessionNames = new Map(); // Map of tabId to session name
1799
1898
  let workflowLogBuffers = new Map(); // Map of tabId to WorkflowLogBuffer instance
1800
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
+
1801
1906
  //=============================================================================
1802
1907
  // COMPREHENSIVE ERROR HANDLING SYSTEM
1803
1908
  // Designed collaboratively with Second Opinion Analyst and Testing Expert
@@ -2402,6 +2507,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2402
2507
  }
2403
2508
 
2404
2509
  if (request.action === 'startRecording') {
2510
+ console.log('[Background] startRecording received settings:', request.settings);
2405
2511
  startRecording(request.tabId, request.settings).then(() => {
2406
2512
  sendResponse({ success: true });
2407
2513
  }).catch((error) => {
@@ -2710,7 +2816,52 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2710
2816
  });
2711
2817
  return true;
2712
2818
  }
2713
-
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
+
2714
2865
  // Handle messages from offscreen document
2715
2866
  if (request.target === 'background') {
2716
2867
  if (request.type === 'frame-batch-ready') {
@@ -2719,10 +2870,14 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2719
2870
  handleFrameCaptureComplete(request.data);
2720
2871
  } else if (request.type === 'show-frame-flash') {
2721
2872
  // Relay flash message to content script
2873
+ // console.log('[Background] Relaying frame-flash to tab:', request.tabId);
2722
2874
  chrome.tabs.sendMessage(request.tabId, {
2723
2875
  action: 'showFrameFlash'
2724
- }).catch(() => {
2876
+ }).then(() => {
2877
+ // console.log('[Background] Frame flash message sent successfully to tab:', request.tabId);
2878
+ }).catch((error) => {
2725
2879
  // Content script might not be available, that's ok
2880
+ // console.log('[Background] Frame flash failed for tab:', request.tabId, error?.message);
2726
2881
  });
2727
2882
  }
2728
2883
  }
@@ -2823,7 +2978,23 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2823
2978
  }
2824
2979
  }
2825
2980
  }
2826
-
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
+
2827
2998
  // Handle getCookies for restore points
2828
2999
  if (request.action === 'getCookies' && sender.tab) {
2829
3000
  chrome.cookies.getAll({ url: request.url }, (cookies) => {
@@ -2933,10 +3104,10 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
2933
3104
  // console.log('[SCREENSHOT-DEBUG] background.js - Sending response with success: true');
2934
3105
  sendResponse({ success: true, screenshotData });
2935
3106
  }).catch((error) => {
2936
- console.error('[SCREENSHOT-DEBUG] background.js - captureTabScreenshot FAILED');
2937
- console.error('[SCREENSHOT-DEBUG] background.js - Error:', error);
2938
- console.error('[SCREENSHOT-DEBUG] background.js - Error message:', error.message);
2939
- 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
+ }
2940
3111
  sendResponse({ success: false, error: error.message });
2941
3112
  });
2942
3113
  return true;
@@ -2970,7 +3141,7 @@ async function takeStandaloneSnapshot(tabId, note = '') {
2970
3141
  const sessionId = `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2971
3142
 
2972
3143
  // Capture screenshot
2973
- const screenshotData = await chrome.tabs.captureVisibleTab(null, {
3144
+ const screenshotData = await throttledCaptureVisibleTab(null, {
2974
3145
  format: 'jpeg',
2975
3146
  quality: 30 // Default quality for snapshots
2976
3147
  });
@@ -3083,7 +3254,7 @@ async function captureTabScreenshot(tabId, settings) {
3083
3254
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureOptions:', JSON.stringify(captureOptions));
3084
3255
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Calling chrome.tabs.captureVisibleTab...');
3085
3256
 
3086
- const dataUrl = await chrome.tabs.captureVisibleTab(windowId, captureOptions);
3257
+ const dataUrl = await throttledCaptureVisibleTab(windowId, captureOptions);
3087
3258
 
3088
3259
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - captureVisibleTab SUCCESS');
3089
3260
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - dataUrl length:', dataUrl?.length);
@@ -3135,10 +3306,12 @@ async function captureTabScreenshot(tabId, settings) {
3135
3306
  // console.log('[SCREENSHOT-DEBUG] captureTabScreenshot - Final dataUrl length:', dataUrl?.length);
3136
3307
  return dataUrl;
3137
3308
  } catch (error) {
3138
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - EXCEPTION CAUGHT');
3139
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error:', error);
3140
- console.error('[SCREENSHOT-DEBUG] captureTabScreenshot - Error message:', error.message);
3141
- 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
+ }
3142
3315
  throw error;
3143
3316
  }
3144
3317
  }
@@ -3265,6 +3438,13 @@ async function startRecording(tabId, settings = {}) {
3265
3438
  currentOwnerId = sessionResult.ownerId;
3266
3439
 
3267
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
+ });
3268
3448
  const scheduledStartTime = Date.now() + 3000; // Schedule start 3 seconds in future for countdown synchronization
3269
3449
 
3270
3450
  // STEP 4: Initialize browser-only recording if in browser-only mode
@@ -3275,7 +3455,8 @@ async function startRecording(tabId, settings = {}) {
3275
3455
  tabId: tabId,
3276
3456
  url: tab.url,
3277
3457
  title: tab.title || settings.sessionName || 'Browser Recording',
3278
- startTime: scheduledStartTime
3458
+ startTime: scheduledStartTime,
3459
+ isVideoMode: settings.videoMode || false
3279
3460
  });
3280
3461
  }
3281
3462
 
@@ -3291,12 +3472,15 @@ async function startRecording(tabId, settings = {}) {
3291
3472
  logStreamer.setSessionId(sessionId);
3292
3473
  }
3293
3474
 
3294
- // 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
3295
3478
  chrome.storage.local.set({
3296
3479
  recordingActive: true,
3297
3480
  recordingStartTime: scheduledStartTime,
3298
3481
  recordingTabId: tabId,
3299
- recordingSessionId: sessionId
3482
+ recordingSessionId: sessionId,
3483
+ isVideoModeActive: settings.videoMode || false
3300
3484
  });
3301
3485
 
3302
3486
  // Update badge
@@ -3329,10 +3513,12 @@ async function startRecording(tabId, settings = {}) {
3329
3513
  }
3330
3514
 
3331
3515
  // Notify content script that recording has started with scheduled time
3516
+ console.log('[Background] Sending recordingStarted to content with videoMode:', settings.videoMode);
3332
3517
  chrome.tabs.sendMessage(tabId, {
3333
3518
  action: 'recordingStarted',
3334
3519
  scheduledStartTime: scheduledStartTime,
3335
- sessionId: sessionId
3520
+ sessionId: sessionId,
3521
+ videoMode: settings.videoMode || false // Pass video mode for hidden indicator
3336
3522
  }).catch(async (error) => {
3337
3523
  // Content script might not be injected, try to inject it
3338
3524
  // console.log('Could not notify content script, attempting to inject:', error);
@@ -3358,7 +3544,8 @@ async function startRecording(tabId, settings = {}) {
3358
3544
  chrome.tabs.sendMessage(tabId, {
3359
3545
  action: 'recordingStarted',
3360
3546
  scheduledStartTime: scheduledStartTime,
3361
- sessionId: sessionId
3547
+ sessionId: sessionId,
3548
+ videoMode: settings.videoMode || false
3362
3549
  }).catch(() => {
3363
3550
  console.error('Failed to show recording indicator even after injection');
3364
3551
  });
@@ -3374,12 +3561,22 @@ async function startRecording(tabId, settings = {}) {
3374
3561
  await browserRecordingManager.startRecording(sessionId, {
3375
3562
  tabId,
3376
3563
  url: tab.url,
3377
- title: tab.title
3564
+ title: tab.title,
3565
+ isVideoMode: settings.videoMode || false
3378
3566
  });
3379
3567
  // console.log('[Recording] Browser recording initialized for session:', sessionId);
3380
3568
  }
3381
3569
 
3382
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);
3383
3580
  await chrome.runtime.sendMessage({
3384
3581
  type: 'start-frame-capture',
3385
3582
  target: 'offscreen',
@@ -3389,11 +3586,7 @@ async function startRecording(tabId, settings = {}) {
3389
3586
  ownerId: currentOwnerId,
3390
3587
  scheduledStartTime: scheduledStartTime,
3391
3588
  mode: serverMode, // Pass mode to offscreen document
3392
- settings: {
3393
- frameRate: settings.frameRate || 1,
3394
- imageQuality: settings.imageQuality || (serverMode === 'browser-only' ? 15 : 30),
3395
- frameFlash: settings.frameFlash !== false
3396
- }
3589
+ settings: frameCaptureSettings
3397
3590
  });
3398
3591
 
3399
3592
  // console.log('Recording started in', serverMode, 'mode');
@@ -3528,10 +3721,20 @@ async function stopRecording() {
3528
3721
  }
3529
3722
  }
3530
3723
 
3724
+ // Note: stoppedSessionId was already captured at the start of stopRecording()
3725
+ // It's used below for deferred cache cleanup
3726
+
3531
3727
  // Clear session cache
3532
3728
  currentSession = null;
3533
3729
  currentOwnerId = null;
3534
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
+
3535
3738
  // Update legacy state for backward compatibility (will be removed)
3536
3739
  isCurrentlyRecording = false;
3537
3740
  recordingTabId = null;
@@ -3830,7 +4033,8 @@ async function stopWorkflowRecording(tabId) {
3830
4033
  screenshotSettings: screenshotSettings || {}, // v2.0.8 fix: ensure it's an object, not null
3831
4034
  actions: workflow,
3832
4035
  logs: includeLogs ? workflow.flatMap(action => action.logs || []) : [],
3833
- functionTraces: functionTraces // Include function execution traces
4036
+ functionTraces: functionTraces, // Include function execution traces
4037
+ recordingMode: 'workflow' // Explicitly mark as workflow recording for tab filtering
3834
4038
  };
3835
4039
 
3836
4040
  // console.log('[SCREENSHOT-DEBUG] background.stopWorkflowRecording - HTTP POST payload being prepared');
@@ -3934,6 +4138,278 @@ async function stopCapturingWorkflowLogs(tabId) {
3934
4138
  return await self.ConsoleInterceptionLibrary.stopConsoleInterception(tabId, WORKFLOW_RECORDING_CONSOLE_CONFIG);
3935
4139
  }
3936
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
+
3937
4413
  // Delete recording from server
3938
4414
  async function deleteRecordingFromServer(recordingId, sendResponse) {
3939
4415
  try {
@@ -4092,9 +4568,13 @@ async function handleFrameBatch(batchData, skipValidation = false) {
4092
4568
  const ports = await getServerPorts();
4093
4569
  let uploadSuccess = false;
4094
4570
  let lastError = null;
4095
-
4096
- // Get session name from current session
4097
- 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;
4098
4578
 
4099
4579
  for (const port of ports) {
4100
4580
  try {
@@ -4104,6 +4584,8 @@ async function handleFrameBatch(batchData, skipValidation = false) {
4104
4584
  if (sessionName) {
4105
4585
  formData.append('sessionName', sessionName);
4106
4586
  }
4587
+ // Always include isVideoMode for proper recording classification
4588
+ formData.append('isVideoMode', isVideoMode.toString());
4107
4589
 
4108
4590
  const uploadResponse = await fetch(`http://localhost:${port}/chromedebug/frame-batch`, {
4109
4591
  method: 'POST',
@@ -4115,33 +4597,41 @@ async function handleFrameBatch(batchData, skipValidation = false) {
4115
4597
  // console.log(`Frame batch uploaded successfully to port ${port}:`, result);
4116
4598
  recordingServerPort = port; // Store the successful port
4117
4599
  uploadSuccess = true;
4118
-
4119
- // Also save frames to Chrome local storage
4120
- const storageKey = batchData.sessionId;
4121
- const existingData = await chrome.storage.local.get([storageKey]);
4122
- const sessionData = existingData[storageKey] || {
4123
- sessionId: batchData.sessionId,
4124
- frames: [],
4125
- timestamp: Date.now()
4126
- };
4127
-
4128
- // Append new frames
4129
- sessionData.frames = sessionData.frames.concat(batchData.frames);
4130
- await chrome.storage.local.set({ [storageKey]: sessionData });
4131
- // 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);
4132
4610
 
4133
4611
  // Send frame count update to content script
4134
4612
  if (recordingTabId) {
4135
4613
  try {
4136
4614
  await chrome.tabs.sendMessage(recordingTabId, {
4137
4615
  action: 'updateFrameCount',
4138
- frameCount: sessionData.frames.length
4616
+ frameCount: newCount
4139
4617
  });
4140
4618
  } catch (e) {
4141
4619
  // Tab might be closed, ignore
4142
4620
  }
4143
4621
  }
4144
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
+
4145
4635
  break;
4146
4636
  } else {
4147
4637
  const errorText = await uploadResponse.text();
@@ -4667,11 +5157,12 @@ async function handleFrameCaptureComplete(sessionData) {
4667
5157
  frameCounter.delete(sessionData.sessionId); // Clean up frame counter
4668
5158
  screenInteractions = []; // Clear interactions
4669
5159
 
4670
- // Update storage
4671
- chrome.storage.local.set({
4672
- recordingActive: false,
5160
+ // Update storage - clear isVideoModeActive for consistency
5161
+ chrome.storage.local.set({
5162
+ recordingActive: false,
4673
5163
  recordingStartTime: null,
4674
- recordingTabId: null
5164
+ recordingTabId: null,
5165
+ isVideoModeActive: false
4675
5166
  });
4676
5167
 
4677
5168
  // Update badge