@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.
@@ -0,0 +1,545 @@
1
+ /**
2
+ * VideoPlayer - Canvas-based frame playback for Chrome Debug PRO
3
+ * Part of the video playback/export feature
4
+ */
5
+ class VideoPlayer {
6
+ constructor(container, options = {}) {
7
+ this.container = container;
8
+ this.canvas = null;
9
+ this.ctx = null;
10
+
11
+ // Frame data (passed from FrameEditor, not reloaded)
12
+ this.frames = [];
13
+ this.imageCache = new Map(); // frameIndex -> HTMLImageElement
14
+
15
+ // Playback state
16
+ this.currentFrameIndex = 0;
17
+ this.isPlaying = false;
18
+ this.playbackSpeed = 1.0;
19
+ this.animationId = null;
20
+ this.lastRenderTime = 0;
21
+ this.playbackStartTime = 0;
22
+ this.playbackStartFrame = 0;
23
+
24
+ // Display options
25
+ this.showLogs = options.showLogs !== false;
26
+ this.logPosition = options.logPosition || 'bottom';
27
+ this.showClickIndicators = true;
28
+ this.showMouseCursor = true;
29
+
30
+ // Interaction data
31
+ this.interactions = [];
32
+ this.originalWidth = null;
33
+ this.originalHeight = null;
34
+
35
+ // Preloading config
36
+ this.preloadBehind = 10;
37
+ this.preloadAhead = 40;
38
+ this.maxCacheSize = 50;
39
+
40
+ // Cached frame data for efficient overlay updates between frames
41
+ this.cachedFrameData = null;
42
+ this.cachedFrameIndex = -1;
43
+
44
+ // Viewport dimensions from recording (for accurate coordinate scaling)
45
+ this.recordedViewportWidth = null;
46
+ this.recordedViewportHeight = null;
47
+
48
+ // Callbacks
49
+ this.onFrameChange = options.onFrameChange || null;
50
+ this.onPlayStateChange = options.onPlayStateChange || null;
51
+ }
52
+
53
+ // Initialize with frame data
54
+ async initialize(frames) {
55
+ this.frames = frames;
56
+ this.createCanvas();
57
+ await this.preloadFramesAround(0);
58
+ this.renderFrame(0);
59
+ }
60
+
61
+ createCanvas() {
62
+ this.canvas = document.createElement('canvas');
63
+ this.canvas.className = 'video-player-canvas';
64
+ // Use willReadFrequently for better performance with getImageData calls during playback
65
+ this.ctx = this.canvas.getContext('2d', { willReadFrequently: true });
66
+ this.container.appendChild(this.canvas);
67
+ }
68
+
69
+ // Image loading with cache
70
+ async loadImage(frameIndex) {
71
+ if (this.imageCache.has(frameIndex)) {
72
+ return this.imageCache.get(frameIndex);
73
+ }
74
+
75
+ const frame = this.frames[frameIndex];
76
+ if (!frame || !frame.imageData) return null;
77
+
78
+ return new Promise((resolve, reject) => {
79
+ const img = new Image();
80
+ img.onload = () => {
81
+ this.addToCache(frameIndex, img);
82
+ resolve(img);
83
+ };
84
+ img.onerror = reject;
85
+ img.src = frame.imageData.startsWith('data:')
86
+ ? frame.imageData
87
+ : `data:image/jpeg;base64,${frame.imageData}`;
88
+ });
89
+ }
90
+
91
+ // Cache management with LRU eviction
92
+ addToCache(frameIndex, image) {
93
+ if (this.imageCache.size >= this.maxCacheSize) {
94
+ // Remove frames furthest from current position
95
+ const entries = [...this.imageCache.keys()];
96
+ entries.sort((a, b) =>
97
+ Math.abs(a - this.currentFrameIndex) - Math.abs(b - this.currentFrameIndex)
98
+ );
99
+ // Remove furthest 10
100
+ for (let i = entries.length - 1; i >= this.maxCacheSize - 10; i--) {
101
+ this.imageCache.delete(entries[i]);
102
+ }
103
+ }
104
+ this.imageCache.set(frameIndex, image);
105
+ }
106
+
107
+ // Preload frames around current position
108
+ async preloadFramesAround(centerIndex) {
109
+ const start = Math.max(0, centerIndex - this.preloadBehind);
110
+ const end = Math.min(this.frames.length - 1, centerIndex + this.preloadAhead);
111
+
112
+ const loadPromises = [];
113
+ for (let i = start; i <= end; i++) {
114
+ if (!this.imageCache.has(i)) {
115
+ loadPromises.push(this.loadImage(i).catch(() => null));
116
+ }
117
+ }
118
+
119
+ await Promise.all(loadPromises);
120
+ }
121
+
122
+ // Render a specific frame
123
+ async renderFrame(frameIndex) {
124
+ if (frameIndex < 0 || frameIndex >= this.frames.length) return;
125
+
126
+ const img = await this.loadImage(frameIndex);
127
+ if (!img) return;
128
+
129
+ // Set canvas size to match image's intrinsic dimensions (not CSS-styled dimensions)
130
+ // Use naturalWidth/naturalHeight to get the actual image pixel dimensions
131
+ const imgWidth = img.naturalWidth || img.width;
132
+ const imgHeight = img.naturalHeight || img.height;
133
+
134
+ if (this.canvas.width !== imgWidth || this.canvas.height !== imgHeight) {
135
+ this.canvas.width = imgWidth;
136
+ this.canvas.height = imgHeight;
137
+ this.originalWidth = imgWidth;
138
+ this.originalHeight = imgHeight;
139
+ }
140
+
141
+ // Clear and draw
142
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
143
+ this.ctx.drawImage(img, 0, 0);
144
+
145
+ // Cache the frame image data (before overlays) for efficient cursor updates
146
+ this.cachedFrameData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
147
+ this.cachedFrameIndex = frameIndex;
148
+
149
+ // Draw overlays (clicks and cursor)
150
+ const frame = this.frames[frameIndex];
151
+ if (frame) {
152
+ this.drawOverlays(frame);
153
+ }
154
+
155
+ this.currentFrameIndex = frameIndex;
156
+
157
+ // Callback
158
+ if (this.onFrameChange) {
159
+ this.onFrameChange(frameIndex, this.frames[frameIndex]);
160
+ }
161
+ }
162
+
163
+ // Playback control
164
+ play() {
165
+ if (this.isPlaying) return;
166
+ if (this.currentFrameIndex >= this.frames.length - 1) {
167
+ this.seek(0); // Restart from beginning if at end
168
+ }
169
+
170
+ this.isPlaying = true;
171
+ this.playbackStartTime = performance.now();
172
+ this.playbackStartFrame = this.currentFrameIndex;
173
+ this.lastRenderTime = performance.now();
174
+
175
+ if (this.onPlayStateChange) this.onPlayStateChange(true);
176
+
177
+ this.playbackLoop();
178
+ }
179
+
180
+ pause() {
181
+ this.isPlaying = false;
182
+ if (this.animationId) {
183
+ cancelAnimationFrame(this.animationId);
184
+ this.animationId = null;
185
+ }
186
+ if (this.onPlayStateChange) this.onPlayStateChange(false);
187
+ }
188
+
189
+ playbackLoop() {
190
+ if (!this.isPlaying) return;
191
+
192
+ const now = performance.now();
193
+ const currentFrame = this.frames[this.currentFrameIndex];
194
+ const nextFrame = this.frames[this.currentFrameIndex + 1];
195
+
196
+ if (!nextFrame) {
197
+ this.pause();
198
+ return;
199
+ }
200
+
201
+ // Calculate time to next frame based on recording timestamps
202
+ const frameDuration = (nextFrame.timestamp - currentFrame.timestamp) / this.playbackSpeed;
203
+
204
+ if (now - this.lastRenderTime >= frameDuration) {
205
+ // Time to advance to next frame
206
+ this.currentFrameIndex++;
207
+ this.renderFrame(this.currentFrameIndex);
208
+ this.lastRenderTime = now;
209
+
210
+ // Preload ahead during playback
211
+ if (this.currentFrameIndex % 10 === 0) {
212
+ this.preloadFramesAround(this.currentFrameIndex);
213
+ }
214
+ } else {
215
+ // Between frames - interpolate cursor position for smooth movement
216
+ // Calculate the interpolated timestamp based on elapsed time since last frame
217
+ const elapsedSinceFrame = (now - this.lastRenderTime) * this.playbackSpeed;
218
+ const interpolatedTimestamp = currentFrame.timestamp + elapsedSinceFrame;
219
+
220
+ // Only update overlays if we have cached frame data and mouse tracking data
221
+ if (this.cachedFrameData && this.cachedFrameIndex === this.currentFrameIndex &&
222
+ this.showMouseCursor && this.interactions.some(i => i.type === 'mousemove')) {
223
+ this.updateOverlaysAtTimestamp(interpolatedTimestamp);
224
+ }
225
+ }
226
+
227
+ this.animationId = requestAnimationFrame(() => this.playbackLoop());
228
+ }
229
+
230
+ // Update overlays at a specific timestamp (for smooth cursor interpolation)
231
+ updateOverlaysAtTimestamp(timestamp) {
232
+ if (!this.cachedFrameData) return;
233
+
234
+ // Restore the cached frame image
235
+ this.ctx.putImageData(this.cachedFrameData, 0, 0);
236
+
237
+ // Draw overlays at the interpolated timestamp
238
+ if (this.showClickIndicators) {
239
+ const clicks = this.interactions.filter(i =>
240
+ i.type === 'click' && i.x && i.y &&
241
+ Math.abs(i.timestamp - timestamp) <= 500
242
+ );
243
+ clicks.forEach(click => {
244
+ const age = Math.abs(timestamp - click.timestamp);
245
+ this.drawClickRipple(click, age); // Pass full click object for per-interaction scaling
246
+ });
247
+ }
248
+
249
+ if (this.showMouseCursor) {
250
+ const mousePos = this.getMousePositionAtTimestamp(timestamp);
251
+ if (mousePos) {
252
+ this.drawMouseCursor(mousePos); // Pass full position object for viewport scaling
253
+ }
254
+ }
255
+ }
256
+
257
+ // Seek to specific frame
258
+ async seek(frameIndex) {
259
+ const targetFrame = Math.max(0, Math.min(frameIndex, this.frames.length - 1));
260
+ await this.preloadFramesAround(targetFrame);
261
+ await this.renderFrame(targetFrame);
262
+ this.lastRenderTime = performance.now();
263
+ }
264
+
265
+ // Navigation
266
+ nextFrame() {
267
+ if (this.currentFrameIndex < this.frames.length - 1) {
268
+ this.seek(this.currentFrameIndex + 1);
269
+ }
270
+ }
271
+
272
+ prevFrame() {
273
+ if (this.currentFrameIndex > 0) {
274
+ this.seek(this.currentFrameIndex - 1);
275
+ }
276
+ }
277
+
278
+ // Speed control
279
+ setSpeed(speed) {
280
+ this.playbackSpeed = speed;
281
+ }
282
+
283
+ // Get playback progress
284
+ getProgress() {
285
+ if (this.frames.length === 0) return 0;
286
+ return this.currentFrameIndex / (this.frames.length - 1);
287
+ }
288
+
289
+ // Get duration
290
+ getDuration() {
291
+ if (this.frames.length < 2) return 0;
292
+ return this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp;
293
+ }
294
+
295
+ // Get current time
296
+ getCurrentTime() {
297
+ if (this.frames.length === 0) return 0;
298
+ return this.frames[this.currentFrameIndex].timestamp - this.frames[0].timestamp;
299
+ }
300
+
301
+ // Load interactions data and extract viewport dimensions
302
+ loadInteractions(interactions) {
303
+ this.interactions = interactions || [];
304
+
305
+ // Extract viewport dimensions from interaction data (mousemove and click events contain this)
306
+ // This is crucial for accurate coordinate scaling
307
+ const interactionWithViewport = this.interactions.find(i =>
308
+ (i.type === 'mousemove' || i.type === 'click') && i.viewportWidth && i.viewportHeight
309
+ );
310
+ if (interactionWithViewport) {
311
+ this.recordedViewportWidth = interactionWithViewport.viewportWidth;
312
+ this.recordedViewportHeight = interactionWithViewport.viewportHeight;
313
+ }
314
+ }
315
+
316
+ // Get the scale factors for coordinate conversion
317
+ getScaleFactors() {
318
+ // Use recorded viewport dimensions if available, otherwise fall back to image dimensions
319
+ const refWidth = this.recordedViewportWidth || this.originalWidth || this.canvas.width;
320
+ const refHeight = this.recordedViewportHeight || this.originalHeight || this.canvas.height;
321
+
322
+ return {
323
+ x: this.canvas.width / refWidth,
324
+ y: this.canvas.height / refHeight
325
+ };
326
+ }
327
+
328
+ // Get scale factors for a specific interaction (uses per-interaction viewport if available)
329
+ // This handles cases where viewport dimensions change during recording (e.g., browser resize)
330
+ getScaleFactorsForInteraction(interaction) {
331
+ // Prefer interaction-specific viewport dimensions, fall back to global recorded values
332
+ const refWidth = interaction.viewportWidth || this.recordedViewportWidth || this.originalWidth || this.canvas.width;
333
+ const refHeight = interaction.viewportHeight || this.recordedViewportHeight || this.originalHeight || this.canvas.height;
334
+ const dpr = interaction.devicePixelRatio || 1;
335
+
336
+ // CRITICAL FIX (per Gemini analysis):
337
+ //
338
+ // The problem: Click coordinates (clientX/Y) are in CSS pixels, but chrome.tabs.captureVisibleTab
339
+ // and getDisplayMedia capture at PHYSICAL pixel resolution (CSS pixels × devicePixelRatio).
340
+ //
341
+ // On a 2x Retina display with 1440px CSS viewport:
342
+ // - Physical pixels captured: 1440 × 2 = 2880
343
+ // - Screenshot (after constraints): e.g., 640px (scaled down from 2880)
344
+ // - WRONG scale: 640 / 1440 = 0.444 (too large!)
345
+ // - CORRECT scale: 640 / (1440 × 2) = 0.222
346
+ //
347
+ // The fix: Divide by devicePixelRatio to convert CSS viewport to physical pixel space
348
+ // before calculating the scale factor.
349
+
350
+ // Convert viewport dimensions to physical pixel space (what the screenshot was captured from)
351
+ const physicalRefWidth = refWidth * dpr;
352
+ const physicalRefHeight = refHeight * dpr;
353
+
354
+ // Calculate scale factors (canvas pixels / physical viewport pixels)
355
+ let scaleX = this.canvas.width / physicalRefWidth;
356
+ let scaleY = this.canvas.height / physicalRefHeight;
357
+
358
+ // Diagnostic logging for debugging click alignment issues
359
+ console.log('[VideoPlayer] Scale factors:', {
360
+ canvasWidth: this.canvas.width,
361
+ canvasHeight: this.canvas.height,
362
+ refWidth,
363
+ refHeight,
364
+ dpr,
365
+ physicalRefWidth,
366
+ physicalRefHeight,
367
+ scaleX,
368
+ scaleY,
369
+ interactionX: interaction.x,
370
+ interactionY: interaction.y,
371
+ calculatedCanvasX: interaction.x * scaleX,
372
+ calculatedCanvasY: interaction.y * scaleY
373
+ });
374
+
375
+ return { x: scaleX, y: scaleY };
376
+ }
377
+
378
+ // Get mouse position at timestamp (interpolated)
379
+ // Returns full position object with viewport dimensions for accurate scaling
380
+ getMousePositionAtTimestamp(timestamp) {
381
+ const mouseMoves = this.interactions.filter(i => i.type === 'mousemove');
382
+ if (mouseMoves.length === 0) return null;
383
+
384
+ let before = null;
385
+ let after = null;
386
+
387
+ for (const move of mouseMoves) {
388
+ if (move.timestamp <= timestamp) {
389
+ before = move;
390
+ } else if (!after && move.timestamp > timestamp) {
391
+ after = move;
392
+ break;
393
+ }
394
+ }
395
+
396
+ if (!before) return after ? {
397
+ x: after.x,
398
+ y: after.y,
399
+ viewportWidth: after.viewportWidth,
400
+ viewportHeight: after.viewportHeight,
401
+ devicePixelRatio: after.devicePixelRatio
402
+ } : null;
403
+
404
+ if (!after) return {
405
+ x: before.x,
406
+ y: before.y,
407
+ viewportWidth: before.viewportWidth,
408
+ viewportHeight: before.viewportHeight,
409
+ devicePixelRatio: before.devicePixelRatio
410
+ };
411
+
412
+ // Interpolate position, use 'before' viewport for scaling
413
+ const t = (timestamp - before.timestamp) / (after.timestamp - before.timestamp);
414
+ return {
415
+ x: before.x + (after.x - before.x) * t,
416
+ y: before.y + (after.y - before.y) * t,
417
+ viewportWidth: before.viewportWidth,
418
+ viewportHeight: before.viewportHeight,
419
+ devicePixelRatio: before.devicePixelRatio
420
+ };
421
+ }
422
+
423
+ // Draw click ripple effect - DISABLED
424
+ // The complex scaling calculation was causing click indicators to appear in wrong positions.
425
+ // Click visualization is now handled by drawMouseCursor which shows a click circle when
426
+ // there's a click at the current timestamp (using the working mousemove coordinate system).
427
+ drawClickRipple(click, age, maxAge = 500) {
428
+ // Intentionally empty - click indicators now shown at cursor position
429
+ // See drawMouseCursor() for click visualization
430
+ }
431
+
432
+ // Draw mouse cursor with optional click indicator
433
+ // Accepts position object with viewport dimensions for per-interaction scaling
434
+ // When isClicking is true, draws a larger click circle around the cursor
435
+ drawMouseCursor(posObj, isClicking = false, clickAge = 0) {
436
+ // Use per-interaction viewport scaling for accurate positioning
437
+ const scale = this.getScaleFactorsForInteraction(posObj);
438
+ const cx = posObj.x * scale.x;
439
+ const cy = posObj.y * scale.y;
440
+
441
+ this.ctx.save();
442
+ this.ctx.translate(cx, cy);
443
+
444
+ // Draw click indicator circle (bigger, more visible) when clicking
445
+ if (isClicking) {
446
+ const maxAge = 500;
447
+ const progress = Math.min(clickAge / maxAge, 1);
448
+ const alpha = 1 - progress;
449
+ const radius = 15 + (25 * progress); // Starts at 15, expands to 40
450
+
451
+ // Outer expanding ring
452
+ this.ctx.beginPath();
453
+ this.ctx.arc(0, 0, radius, 0, Math.PI * 2);
454
+ this.ctx.strokeStyle = `rgba(76, 175, 80, ${alpha})`; // Green color
455
+ this.ctx.lineWidth = 3;
456
+ this.ctx.stroke();
457
+
458
+ // Inner solid circle
459
+ this.ctx.beginPath();
460
+ this.ctx.arc(0, 0, 8, 0, Math.PI * 2);
461
+ this.ctx.fillStyle = `rgba(76, 175, 80, ${alpha * 0.6})`;
462
+ this.ctx.fill();
463
+ }
464
+
465
+ // Draw cursor arrow
466
+ this.ctx.fillStyle = 'white';
467
+ this.ctx.strokeStyle = 'black';
468
+ this.ctx.lineWidth = 1;
469
+
470
+ this.ctx.beginPath();
471
+ this.ctx.moveTo(0, 0);
472
+ this.ctx.lineTo(0, 18);
473
+ this.ctx.lineTo(4, 14);
474
+ this.ctx.lineTo(8, 22);
475
+ this.ctx.lineTo(11, 21);
476
+ this.ctx.lineTo(7, 13);
477
+ this.ctx.lineTo(13, 13);
478
+ this.ctx.closePath();
479
+
480
+ this.ctx.fill();
481
+ this.ctx.stroke();
482
+ this.ctx.restore();
483
+ }
484
+
485
+ // Draw all overlays
486
+ drawOverlays(frame) {
487
+ const timestamp = frame.timestamp;
488
+
489
+ // Find if there's an active click near this timestamp
490
+ let activeClick = null;
491
+ let clickAge = 0;
492
+ if (this.showClickIndicators) {
493
+ const clicks = this.interactions.filter(i =>
494
+ i.type === 'click' && i.x && i.y &&
495
+ Math.abs(i.timestamp - timestamp) <= 500
496
+ );
497
+ if (clicks.length > 0) {
498
+ // Find the closest click
499
+ activeClick = clicks.reduce((closest, click) => {
500
+ const age = Math.abs(timestamp - click.timestamp);
501
+ const closestAge = Math.abs(timestamp - closest.timestamp);
502
+ return age < closestAge ? click : closest;
503
+ });
504
+ clickAge = Math.abs(timestamp - activeClick.timestamp);
505
+ }
506
+ }
507
+
508
+ // Draw mouse cursor with click indicator if there's an active click
509
+ if (this.showMouseCursor) {
510
+ const mousePos = this.getMousePositionAtTimestamp(timestamp);
511
+ if (mousePos) {
512
+ const isClicking = activeClick !== null;
513
+ this.drawMouseCursor(mousePos, isClicking, clickAge);
514
+ }
515
+ }
516
+ }
517
+
518
+ // Setters for overlay options
519
+ setShowClickIndicators(show) {
520
+ this.showClickIndicators = show;
521
+ this.renderFrame(this.currentFrameIndex);
522
+ }
523
+
524
+ setShowMouseCursor(show) {
525
+ this.showMouseCursor = show;
526
+ this.renderFrame(this.currentFrameIndex);
527
+ }
528
+
529
+ // Cleanup - CRITICAL for memory management
530
+ destroy() {
531
+ this.pause();
532
+ this.imageCache.clear();
533
+ if (this.canvas && this.canvas.parentNode) {
534
+ this.canvas.parentNode.removeChild(this.canvas);
535
+ }
536
+ this.canvas = null;
537
+ this.ctx = null;
538
+ this.frames = [];
539
+ }
540
+ }
541
+
542
+ // Export for use in frame-editor.js
543
+ if (typeof module !== 'undefined' && module.exports) {
544
+ module.exports = VideoPlayer;
545
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamicu/chromedebug-mcp",
3
- "version": "2.7.1",
3
+ "version": "2.7.4",
4
4
  "description": "ChromeDebug MCP - MCP server that provides full control over a Chrome browser instance for debugging and automation with AI assistants like Claude Code",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -46,6 +46,6 @@ console.log('ℹ️ No manual server startup needed!');
46
46
  console.log(' Claude Code automatically starts both servers when it runs chromedebug-mcp\n');
47
47
 
48
48
  console.log('💎 Want unlimited recordings?');
49
- console.log(' Upgrade to Pro: https://chromedebug.com/buy/996773cb-682b-430f-b9e3-9ce2130bd967\n');
49
+ console.log(' Upgrade to Pro: https://chromedebug.com/checkout/buy/996773cb-682b-430f-b9e3-9ce2130bd967\n');
50
50
 
51
51
  console.log('📚 Full documentation: https://github.com/dynamicupgrade/ChromeDebug#readme\n');
@@ -70,6 +70,12 @@ module.exports = {
70
70
  // Copy Pro features needed for frame editor
71
71
  { from: 'chrome-extension/pro/jszip.min.js', to: 'pro/jszip.min.js' },
72
72
 
73
+ // Video player feature files (needed for frame editor styling)
74
+ { from: 'chrome-extension/pro/video-player.js', to: 'pro/video-player.js' },
75
+ { from: 'chrome-extension/pro/video-exporter.js', to: 'pro/video-exporter.js' },
76
+ { from: 'chrome-extension/pro/video-player.css', to: 'pro/video-player.css' },
77
+ { from: 'chrome-extension/pro/lib/mp4-muxer.min.js', to: 'pro/lib/mp4-muxer.min.js' },
78
+
73
79
  // Copy README
74
80
  { from: 'chrome-extension/README.md', to: 'README.md' }
75
81
  ]
@@ -78,6 +78,12 @@ module.exports = {
78
78
  { from: 'chrome-extension/pro/enhanced-capture.js', to: 'pro/enhanced-capture.js' },
79
79
  { from: 'chrome-extension/pro/jszip.min.js', to: 'pro/jszip.min.js' },
80
80
 
81
+ // Video player feature files
82
+ { from: 'chrome-extension/pro/video-player.js', to: 'pro/video-player.js' },
83
+ { from: 'chrome-extension/pro/video-exporter.js', to: 'pro/video-exporter.js' },
84
+ { from: 'chrome-extension/pro/video-player.css', to: 'pro/video-player.css' },
85
+ { from: 'chrome-extension/pro/lib/mp4-muxer.min.js', to: 'pro/lib/mp4-muxer.min.js' },
86
+
81
87
  // Copy README
82
88
  { from: 'chrome-extension/README.md', to: 'README.md' }
83
89
  ]
@@ -1978,12 +1978,12 @@ export class ChromeController {
1978
1978
  }
1979
1979
 
1980
1980
  // Workflow recording methods
1981
- async storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name = null, screenshotSettings = null, functionTraces = null) {
1981
+ async storeWorkflowRecording(sessionId, url, title, includeLogs, actions, logs, name = null, screenshotSettings = null, functionTraces = null, recordingMode = 'workflow') {
1982
1982
  try {
1983
1983
  const { database } = await import('./database.js');
1984
1984
 
1985
1985
  // Store the workflow recording
1986
- const workflowId = database.storeWorkflowRecording(sessionId, url, title, includeLogs, name, screenshotSettings);
1986
+ const workflowId = database.storeWorkflowRecording(sessionId, url, title, includeLogs, name, screenshotSettings, recordingMode);
1987
1987
 
1988
1988
  // Merge function traces into actions if provided
1989
1989
  let allActions = [...actions];
@@ -2035,10 +2035,10 @@ export class ChromeController {
2035
2035
  }
2036
2036
  }
2037
2037
 
2038
- async listWorkflowRecordings() {
2038
+ async listWorkflowRecordings(recordingModeFilter = null) {
2039
2039
  try {
2040
2040
  const { database } = await import('./database.js');
2041
- const recordings = database.listWorkflowRecordings();
2041
+ const recordings = database.listWorkflowRecordings(recordingModeFilter);
2042
2042
  return { recordings };
2043
2043
  } catch (error) {
2044
2044
  logger.error('Error listing workflow recordings:', error);
@@ -2213,10 +2213,10 @@ export class ChromeController {
2213
2213
  }
2214
2214
  }
2215
2215
 
2216
- async storeFrameBatch(sessionId, frames, sessionName = null) {
2216
+ async storeFrameBatch(sessionId, frames, sessionName = null, isVideoMode = false) {
2217
2217
  try {
2218
2218
  const { database } = await import('./database.js');
2219
- const result = await database.storeFrameBatch(sessionId, frames, sessionName);
2219
+ const result = await database.storeFrameBatch(sessionId, frames, sessionName, isVideoMode);
2220
2220
 
2221
2221
  // After storing frames, attempt to associate any deferred logs
2222
2222
  if (result && result.id) {