@myrialabs/clopen 0.2.7 → 0.2.8

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.
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * Browser MCP Control
3
3
  *
4
- * Manages exclusive MCP control over browser tabs.
5
- * Ensures only one MCP can control the browser at a time.
6
- * Emits events for frontend to show visual indicators.
7
- * Also handles MCP tab request/response coordination.
4
+ * Manages MCP control over browser tabs with multi-tab, session-scoped ownership.
5
+ * Each chat session can control multiple tabs simultaneously.
6
+ * A tab can only be controlled by one chat session at a time.
7
+ * All tabs are released when the chat session ends (stream complete/error/cancel).
8
8
  *
9
- * ARCHITECTURE: Event-based control management
10
- * - Control is maintained as long as the browser tab exists
11
- * - Auto-release when tab is destroyed via 'preview:browser-tab-destroyed' event
12
- * - NO timeouts or polling - 100% real-time event-driven
13
- * - Listens directly to preview service events for tab lifecycle
9
+ * ARCHITECTURE:
10
+ * - Control lifecycle follows chat sessions (no idle timeout)
11
+ * - Multiple tabs can be locked by one chat session (accumulated via switch/open)
12
+ * - Tab destroyed auto-release that single tab from its owning session
13
+ * - Stream ends releaseSession() releases all tabs owned by that session
14
+ * - Emits per-tab control-start/control-end events for frontend UI
14
15
  */
15
16
 
16
17
  import { EventEmitter } from 'events';
@@ -24,19 +25,17 @@ interface PendingTabRequest<T = any> {
24
25
  timeout: NodeJS.Timeout;
25
26
  }
26
27
 
27
- export interface McpControlState {
28
- isControlling: boolean;
29
- mcpSessionId: string | null;
30
- browserTabId: string | null;
31
- projectId: string | null;
32
- startedAt: number | null;
33
- lastActionAt: number | null;
28
+ /** Ownership info for a single tab */
29
+ interface TabOwnershipInfo {
30
+ chatSessionId: string;
31
+ projectId: string;
32
+ acquiredAt: number;
34
33
  }
35
34
 
36
35
  export interface McpControlEvent {
37
36
  type: 'mcp:control-start' | 'mcp:control-end';
38
37
  browserTabId: string;
39
- mcpSessionId?: string;
38
+ chatSessionId?: string;
40
39
  timestamp: number;
41
40
  }
42
41
 
@@ -57,18 +56,11 @@ export interface McpClickEvent {
57
56
  }
58
57
 
59
58
  export class BrowserMcpControl extends EventEmitter {
60
- private controlState: McpControlState = {
61
- isControlling: false,
62
- mcpSessionId: null,
63
- browserTabId: null,
64
- projectId: null,
65
- startedAt: null,
66
- lastActionAt: null
67
- };
68
-
69
- // Auto-release configuration
70
- private readonly IDLE_TIMEOUT_MS = 30000; // 30 seconds idle = auto release
71
- private idleCheckInterval: NodeJS.Timeout | null = null;
59
+ /** Tab ownership info (which chat session controls it) */
60
+ private tabOwnership = new Map<string, TabOwnershipInfo>();
61
+
62
+ /** Chat session → set of tab IDs it controls */
63
+ private sessionTabs = new Map<string, Set<string>>();
72
64
 
73
65
  // Pending tab requests (keyed by request type + timestamp)
74
66
  private pendingTabRequests = new Map<string, PendingTabRequest>();
@@ -98,16 +90,18 @@ export class BrowserMcpControl extends EventEmitter {
98
90
 
99
91
  /**
100
92
  * Handle tab destroyed event
101
- * Auto-release control if the destroyed tab was being controlled.
102
- * Uses the service's projectId to avoid cross-project false-positives.
93
+ * Auto-release control for the destroyed tab only
103
94
  */
104
95
  private handleTabDestroyed(tabId: string): void {
105
- if (!this.controlState.isControlling || this.controlState.browserTabId !== tabId) return;
106
- // Validate project to prevent cross-project collisions (tab IDs are not globally unique)
96
+ const ownership = this.tabOwnership.get(tabId);
97
+ if (!ownership) return;
98
+
99
+ // Validate project to prevent cross-project collisions
107
100
  const serviceProjectId = this.previewService?.getProjectId();
108
- if (serviceProjectId && this.controlState.projectId && this.controlState.projectId !== serviceProjectId) return;
109
- debug.warn('mcp', `⚠️ Controlled tab ${tabId} was destroyed - auto-releasing control`);
110
- this.releaseControl();
101
+ if (serviceProjectId && ownership.projectId !== serviceProjectId) return;
102
+
103
+ debug.warn('mcp', `⚠️ Controlled tab ${tabId} was destroyed - auto-releasing from session ${ownership.chatSessionId}`);
104
+ this.releaseTab(tabId);
111
105
  }
112
106
 
113
107
  /**
@@ -157,127 +151,190 @@ export class BrowserMcpControl extends EventEmitter {
157
151
  return false;
158
152
  }
159
153
 
154
+ // ============================================================================
155
+ // Control State Queries
156
+ // ============================================================================
157
+
160
158
  /**
161
- * Check if MCP is currently controlling a browser session
159
+ * Check if any tab is being controlled
162
160
  */
163
161
  isControlling(): boolean {
164
- return this.controlState.isControlling;
162
+ return this.tabOwnership.size > 0;
165
163
  }
166
164
 
167
165
  /**
168
- * Get current control state
166
+ * Check if a specific tab is being controlled (by any session)
169
167
  */
170
- getControlState(): McpControlState {
171
- return { ...this.controlState };
168
+ isTabControlled(browserTabId: string, projectId?: string): boolean {
169
+ const ownership = this.tabOwnership.get(browserTabId);
170
+ if (!ownership) return false;
171
+ if (projectId && ownership.projectId !== projectId) return false;
172
+ return true;
172
173
  }
173
174
 
174
175
  /**
175
- * Check if a specific browser tab is being controlled.
176
- * When projectId is provided, also validates the project to prevent cross-project
177
- * false-positives (tab IDs are only unique per project, not globally).
176
+ * Check if a tab is controlled by a specific chat session
178
177
  */
179
- isTabControlled(browserTabId: string, projectId?: string): boolean {
180
- if (!this.controlState.isControlling || this.controlState.browserTabId !== browserTabId) {
181
- return false;
182
- }
183
- if (projectId && this.controlState.projectId && this.controlState.projectId !== projectId) {
184
- return false;
185
- }
186
- return true;
178
+ isTabControlledBySession(browserTabId: string, chatSessionId: string): boolean {
179
+ const ownership = this.tabOwnership.get(browserTabId);
180
+ return ownership?.chatSessionId === chatSessionId;
187
181
  }
188
182
 
189
183
  /**
190
- * Acquire control of a browser tab
191
- * Returns true if control was acquired, false if already controlled by another MCP
184
+ * Get the chat session ID that controls a specific tab
192
185
  */
193
- acquireControl(browserTabId: string, mcpSessionId?: string, projectId?: string): boolean {
194
- // Validate tab exists before acquiring control
195
- if (this.previewService && !this.previewService.getTab(browserTabId)) {
196
- debug.warn('mcp', `❌ Cannot acquire control: tab ${browserTabId} does not exist`);
197
- return false;
198
- }
186
+ getTabOwner(browserTabId: string): string | null {
187
+ return this.tabOwnership.get(browserTabId)?.chatSessionId || null;
188
+ }
199
189
 
200
- // If already controlling the same tab, just update timestamp
201
- if (this.controlState.isControlling &&
202
- this.controlState.browserTabId === browserTabId) {
203
- this.controlState.lastActionAt = Date.now();
204
- return true;
205
- }
190
+ /**
191
+ * Get all tab IDs controlled by a specific chat session
192
+ */
193
+ getSessionTabs(chatSessionId: string): string[] {
194
+ const tabs = this.sessionTabs.get(chatSessionId);
195
+ return tabs ? Array.from(tabs) : [];
196
+ }
197
+
198
+ /**
199
+ * Get all controlled tab IDs (across all sessions)
200
+ */
201
+ getAllControlledTabs(): Map<string, TabOwnershipInfo> {
202
+ return new Map(this.tabOwnership);
203
+ }
204
+
205
+ // ============================================================================
206
+ // Control Acquisition
207
+ // ============================================================================
206
208
 
207
- // If another tab is being controlled, deny
208
- if (this.controlState.isControlling) {
209
- debug.warn('mcp', `MCP control denied: another tab (${this.controlState.browserTabId}) is already being controlled`);
209
+ /**
210
+ * Acquire control of a browser tab for a chat session.
211
+ *
212
+ * - If the tab is already owned by the same session → success (idempotent)
213
+ * - If the tab is owned by another session → denied
214
+ * - If the tab is free → acquire and add to session's controlled set
215
+ */
216
+ acquireControl(browserTabId: string, chatSessionId: string, projectId: string): boolean {
217
+ // Check existing ownership
218
+ const existingOwner = this.tabOwnership.get(browserTabId);
219
+
220
+ if (existingOwner) {
221
+ // Same session already owns it → idempotent success
222
+ if (existingOwner.chatSessionId === chatSessionId) {
223
+ return true;
224
+ }
225
+ // Different session owns it → denied
226
+ debug.warn('mcp', `❌ Tab ${browserTabId} is controlled by session ${existingOwner.chatSessionId}, denied for ${chatSessionId}`);
210
227
  return false;
211
228
  }
212
229
 
213
230
  // Acquire control
214
231
  const now = Date.now();
215
- this.controlState = {
216
- isControlling: true,
217
- mcpSessionId: mcpSessionId || null,
218
- browserTabId,
219
- projectId: projectId || null,
220
- startedAt: now,
221
- lastActionAt: now
222
- };
232
+ this.tabOwnership.set(browserTabId, {
233
+ chatSessionId,
234
+ projectId,
235
+ acquiredAt: now
236
+ });
223
237
 
224
- // Emit control start event to frontend
225
- this.emitControlStart(browserTabId, mcpSessionId);
238
+ // Add to session's tab set
239
+ let sessionSet = this.sessionTabs.get(chatSessionId);
240
+ if (!sessionSet) {
241
+ sessionSet = new Set();
242
+ this.sessionTabs.set(chatSessionId, sessionSet);
243
+ }
244
+ sessionSet.add(browserTabId);
226
245
 
227
- // Start idle check interval
228
- this.startIdleCheck();
246
+ // Emit control start event to frontend
247
+ this.emitControlStart(browserTabId, chatSessionId);
229
248
 
230
- debug.log('mcp', `🎮 MCP acquired control of tab: ${browserTabId} (event-based tracking)`);
249
+ debug.log('mcp', `🎮 Session ${chatSessionId.slice(0, 8)} acquired tab: ${browserTabId} (total: ${sessionSet.size} tabs)`);
231
250
  return true;
232
251
  }
233
252
 
253
+ // ============================================================================
254
+ // Control Release
255
+ // ============================================================================
256
+
234
257
  /**
235
- * Release control of a browser tab
258
+ * Release a single tab from its owning session.
259
+ * Used when a tab is closed via close_tab or destroyed.
236
260
  */
237
- releaseControl(browserTabId?: string): void {
238
- // If browserTabId provided, only release if it matches
239
- if (browserTabId && this.controlState.browserTabId !== browserTabId) {
240
- return;
261
+ releaseTab(browserTabId: string): void {
262
+ const ownership = this.tabOwnership.get(browserTabId);
263
+ if (!ownership) return;
264
+
265
+ // Remove from tab ownership
266
+ this.tabOwnership.delete(browserTabId);
267
+
268
+ // Remove from session's tab set
269
+ const sessionSet = this.sessionTabs.get(ownership.chatSessionId);
270
+ if (sessionSet) {
271
+ sessionSet.delete(browserTabId);
272
+ if (sessionSet.size === 0) {
273
+ this.sessionTabs.delete(ownership.chatSessionId);
274
+ }
241
275
  }
242
276
 
243
- if (!this.controlState.isControlling) {
277
+ // Emit control end event to frontend
278
+ this.emitControlEnd(browserTabId);
279
+
280
+ debug.log('mcp', `🎮 Released tab: ${browserTabId} (was owned by session ${ownership.chatSessionId.slice(0, 8)})`);
281
+ }
282
+
283
+ /**
284
+ * Release all tabs owned by a chat session.
285
+ * Called when chat stream ends (complete/error/cancel).
286
+ */
287
+ releaseSession(chatSessionId: string): void {
288
+ const sessionSet = this.sessionTabs.get(chatSessionId);
289
+ if (!sessionSet || sessionSet.size === 0) {
290
+ this.sessionTabs.delete(chatSessionId);
244
291
  return;
245
292
  }
246
293
 
247
- const releasedTabId = this.controlState.browserTabId;
294
+ const tabIds = Array.from(sessionSet);
295
+ debug.log('mcp', `🎮 Releasing ${tabIds.length} tabs for session ${chatSessionId.slice(0, 8)}`);
248
296
 
249
- // Reset state
250
- this.controlState = {
251
- isControlling: false,
252
- mcpSessionId: null,
253
- browserTabId: null,
254
- projectId: null,
255
- startedAt: null,
256
- lastActionAt: null
257
- };
297
+ for (const tabId of tabIds) {
298
+ this.tabOwnership.delete(tabId);
299
+ this.emitControlEnd(tabId);
300
+ }
258
301
 
259
- // Stop idle check interval
260
- this.stopIdleCheck();
302
+ this.sessionTabs.delete(chatSessionId);
261
303
 
262
- // Emit control end event to frontend
263
- if (releasedTabId) {
264
- this.emitControlEnd(releasedTabId);
265
- }
304
+ debug.log('mcp', `🎮 Session ${chatSessionId.slice(0, 8)} fully released`);
305
+ }
266
306
 
267
- debug.log('mcp', `🎮 MCP released control of tab: ${releasedTabId}`);
307
+ /**
308
+ * Auto-release control for a specific tab when it's closed.
309
+ * projectId is used to prevent accidental release across projects.
310
+ */
311
+ autoReleaseForTab(browserTabId: string, projectId?: string): void {
312
+ const ownership = this.tabOwnership.get(browserTabId);
313
+ if (!ownership) return;
314
+ if (projectId && ownership.projectId !== projectId) return;
315
+ debug.log('mcp', `🗑️ Auto-releasing tab: ${browserTabId} (closed)`);
316
+ this.releaseTab(browserTabId);
268
317
  }
269
318
 
270
319
  /**
271
- * Update last action timestamp (for tracking purposes only)
272
- * NOTE: This does NOT affect control lifecycle - control is maintained
273
- * as long as the session exists, regardless of action timestamps
320
+ * Force release all control (for cleanup)
274
321
  */
275
- updateLastAction(): void {
276
- if (this.controlState.isControlling) {
277
- this.controlState.lastActionAt = Date.now();
322
+ forceReleaseAll(): void {
323
+ // Emit control-end for all controlled tabs
324
+ for (const [tabId] of this.tabOwnership) {
325
+ this.emitControlEnd(tabId);
278
326
  }
327
+
328
+ this.tabOwnership.clear();
329
+ this.sessionTabs.clear();
330
+
331
+ debug.log('mcp', '🧹 Force released all MCP control');
279
332
  }
280
333
 
334
+ // ============================================================================
335
+ // Cursor Events
336
+ // ============================================================================
337
+
281
338
  /**
282
339
  * Emit cursor position event with MCP source
283
340
  */
@@ -319,14 +376,15 @@ export class BrowserMcpControl extends EventEmitter {
319
376
  });
320
377
  }
321
378
 
322
- /**
323
- * Emit control start event to frontend
324
- */
325
- private emitControlStart(browserTabId: string, mcpSessionId?: string): void {
379
+ // ============================================================================
380
+ // Private Event Emitters
381
+ // ============================================================================
382
+
383
+ private emitControlStart(browserTabId: string, chatSessionId?: string): void {
326
384
  const event: McpControlEvent = {
327
385
  type: 'mcp:control-start',
328
386
  browserTabId,
329
- mcpSessionId,
387
+ chatSessionId,
330
388
  timestamp: Date.now()
331
389
  };
332
390
 
@@ -335,9 +393,6 @@ export class BrowserMcpControl extends EventEmitter {
335
393
  debug.log('mcp', `📢 Emitted mcp:control-start for tab: ${browserTabId}`);
336
394
  }
337
395
 
338
- /**
339
- * Emit control end event to frontend
340
- */
341
396
  private emitControlEnd(browserTabId: string): void {
342
397
  const event: McpControlEvent = {
343
398
  type: 'mcp:control-end',
@@ -349,82 +404,6 @@ export class BrowserMcpControl extends EventEmitter {
349
404
 
350
405
  debug.log('mcp', `📢 Emitted mcp:control-end for tab: ${browserTabId}`);
351
406
  }
352
-
353
- /**
354
- * Start idle check interval
355
- */
356
- private startIdleCheck(): void {
357
- // Clear any existing interval
358
- this.stopIdleCheck();
359
-
360
- // Check every 10 seconds
361
- this.idleCheckInterval = setInterval(() => {
362
- this.checkAndReleaseIfIdle();
363
- }, 10000);
364
-
365
- debug.log('mcp', '⏰ Started idle check interval (30s timeout)');
366
- }
367
-
368
- /**
369
- * Stop idle check interval
370
- */
371
- private stopIdleCheck(): void {
372
- if (this.idleCheckInterval) {
373
- clearInterval(this.idleCheckInterval);
374
- this.idleCheckInterval = null;
375
- debug.log('mcp', '⏰ Stopped idle check interval');
376
- }
377
- }
378
-
379
- /**
380
- * Check if MCP control is idle and auto-release if timeout
381
- */
382
- private checkAndReleaseIfIdle(): void {
383
- if (!this.controlState.isControlling) {
384
- return;
385
- }
386
-
387
- const now = Date.now();
388
- const idleTime = now - (this.controlState.lastActionAt || this.controlState.startedAt || now);
389
-
390
- if (idleTime >= this.IDLE_TIMEOUT_MS) {
391
- debug.log('mcp', `⏰ MCP control idle for ${Math.round(idleTime / 1000)}s, auto-releasing...`);
392
- this.releaseControl();
393
- }
394
- }
395
-
396
- /**
397
- * Auto-release control for a specific browser tab (called when tab closes).
398
- * projectId is used to prevent accidental release across projects with same tab IDs.
399
- */
400
- autoReleaseForTab(browserTabId: string, projectId?: string): void {
401
- if (!this.controlState.isControlling || this.controlState.browserTabId !== browserTabId) return;
402
- if (projectId && this.controlState.projectId && this.controlState.projectId !== projectId) return;
403
- debug.log('mcp', `🗑️ Auto-releasing MCP control for closed tab: ${browserTabId}`);
404
- this.releaseControl(browserTabId);
405
- }
406
-
407
- /**
408
- * Force release all control (for cleanup)
409
- */
410
- forceReleaseAll(): void {
411
- this.stopIdleCheck();
412
-
413
- if (this.controlState.isControlling && this.controlState.browserTabId) {
414
- this.emitControlEnd(this.controlState.browserTabId);
415
- }
416
-
417
- this.controlState = {
418
- isControlling: false,
419
- mcpSessionId: null,
420
- browserTabId: null,
421
- projectId: null,
422
- startedAt: null,
423
- lastActionAt: null
424
- };
425
-
426
- debug.log('mcp', '🧹 Force released all MCP control');
427
- }
428
407
  }
429
408
 
430
409
  // Singleton instance
@@ -733,8 +733,8 @@ class BrowserPreviewServiceManager {
733
733
  browserMcpControl.on('control-start', (data) => {
734
734
  debug.log('preview', `🚀 Forwarding mcp-control-start to project ${projectId}:`, data);
735
735
  ws.emit.project(projectId, 'preview:browser-mcp-control-start', {
736
- browserSessionId: data.browserTabId,
737
- mcpSessionId: data.mcpSessionId,
736
+ browserTabId: data.browserTabId,
737
+ chatSessionId: data.chatSessionId,
738
738
  timestamp: data.timestamp
739
739
  });
740
740
  });
@@ -742,7 +742,7 @@ class BrowserPreviewServiceManager {
742
742
  browserMcpControl.on('control-end', (data) => {
743
743
  debug.log('preview', `🚀 Forwarding mcp-control-end to project ${projectId}:`, data);
744
744
  ws.emit.project(projectId, 'preview:browser-mcp-control-end', {
745
- browserSessionId: data.browserTabId,
745
+ browserTabId: data.browserTabId,
746
746
  timestamp: data.timestamp
747
747
  });
748
748
  });
@@ -4,9 +4,9 @@
4
4
  * Handles WebCodecs-based video streaming with WebRTC DataChannel transport.
5
5
  *
6
6
  * Video Architecture:
7
- * 1. Puppeteer CDP captures JPEG frames via Page.screencastFrame
8
- * 2. Decode JPEG to ImageBitmap in browser
9
- * 3. Encode with VideoEncoder (VP8) in browser
7
+ * 1. CDP captures JPEG frames via Page.screencastFrame
8
+ * 2. Direct CDP Runtime.evaluate sends base64 to page (bypasses Puppeteer IPC)
9
+ * 3. Page decodes JPEG via createImageBitmap, encodes with VP8 VideoEncoder
10
10
  * 4. Send encoded chunks via RTCDataChannel
11
11
  *
12
12
  * Audio Architecture:
@@ -347,17 +347,15 @@ export class BrowserVideoCapture extends EventEmitter {
347
347
  // ACK immediately
348
348
  cdp.send('Page.screencastFrameAck', { sessionId: event.sessionId }).catch(() => {});
349
349
 
350
- // Send frame to encoder
351
- page.evaluate((frameData) => {
352
- const peer = (window as any).__webCodecsPeer;
353
- if (!peer) return false;
354
- peer.encodeFrame(frameData);
355
- return true;
356
- }, event.data).catch((err) => {
357
- if (cdpFrameCount <= 5) {
358
- debug.warn('webcodecs', `Frame delivery error (frame ${cdpFrameCount}):`, err.message);
359
- }
360
- });
350
+ // Send frame to encoder via direct CDP (bypasses Puppeteer's
351
+ // ExecutionContext lookup, function serialization, Runtime.callFunctionOn
352
+ // overhead, and result deserialization). Base64 charset [A-Za-z0-9+/=]
353
+ // is safe to embed in a JS double-quoted string literal.
354
+ cdp.send('Runtime.evaluate', {
355
+ expression: `window.__webCodecsPeer?.encodeFrame("${event.data}")`,
356
+ awaitPromise: false,
357
+ returnByValue: false
358
+ }).catch(() => {});
361
359
  });
362
360
 
363
361
  // Start screencast with scaled dimensions
@@ -218,23 +218,24 @@ export function videoEncoderScript(config: StreamingConfig['video']) {
218
218
  if (!videoEncoder || !isCapturing) return;
219
219
 
220
220
  try {
221
- // Base64 to arrayBuffer (more efficient)
222
- const imageBuffer = await fetch(`data:image/jpeg;base64,${imageData}`);
223
- const arrayBuffer = await imageBuffer.arrayBuffer();
224
-
225
- // Decode JPEG to VideoFrame
226
- const decoder = new ImageDecoder({
227
- data: arrayBuffer,
228
- type: 'image/jpeg',
229
- });
221
+ // Direct base64 decode (avoids fetch() + data URL parsing overhead)
222
+ const binaryStr = atob(imageData);
223
+ const len = binaryStr.length;
224
+ const bytes = new Uint8Array(len);
225
+ for (let i = 0; i < len; i++) {
226
+ bytes[i] = binaryStr.charCodeAt(i);
227
+ }
230
228
 
231
- const { image } = await decoder.decode();
229
+ // Decode JPEG via createImageBitmap (avoids per-frame ImageDecoder
230
+ // constructor/destructor overhead)
231
+ const blob = new Blob([bytes], { type: 'image/jpeg' });
232
+ const bitmap = await createImageBitmap(blob);
232
233
 
233
234
  // Get aligned timestamp in microseconds
234
235
  const timestamp = performance.now() * 1000;
235
236
 
236
- // Create VideoFrame with aligned timestamp
237
- const frame = new VideoFrame(image, {
237
+ // Create VideoFrame from ImageBitmap
238
+ const frame = new VideoFrame(bitmap, {
238
239
  timestamp,
239
240
  alpha: 'discard'
240
241
  });
@@ -253,8 +254,7 @@ export function videoEncoderScript(config: StreamingConfig['video']) {
253
254
 
254
255
  // Close immediately to prevent memory leaks
255
256
  frame.close();
256
- image.close();
257
- decoder.close();
257
+ bitmap.close();
258
258
  } catch (error) {}
259
259
  }
260
260
 
@@ -229,10 +229,10 @@ export interface StreamingConfig {
229
229
  /**
230
230
  * Default streaming configuration
231
231
  *
232
- * Optimized for low-end servers without GPU:
233
- * - Software encoding only (hardwareAcceleration: 'no-preference')
234
- * - Lower bitrate for bandwidth efficiency
235
- * - VP8 for video (good software encoder performance)
232
+ * Optimized for visual quality with reasonable bandwidth:
233
+ * - Software encoding (hardwareAcceleration: 'no-preference')
234
+ * - JPEG quality 70 preserves thin borders/text, VP8 handles bandwidth
235
+ * - VP8 at 1.2Mbps for crisp UI/text rendering
236
236
  * - Opus for audio (efficient and widely supported)
237
237
  */
238
238
  export const DEFAULT_STREAMING_CONFIG: StreamingConfig = {
@@ -241,9 +241,9 @@ export const DEFAULT_STREAMING_CONFIG: StreamingConfig = {
241
241
  width: 0,
242
242
  height: 0,
243
243
  framerate: 24,
244
- bitrate: 600_000,
245
- keyframeInterval: 2,
246
- screenshotQuality: 35,
244
+ bitrate: 1_200_000,
245
+ keyframeInterval: 3,
246
+ screenshotQuality: 70,
247
247
  hardwareAcceleration: 'no-preference',
248
248
  latencyMode: 'realtime'
249
249
  },
@@ -2,7 +2,7 @@
2
2
  export * from './browser/types';
3
3
 
4
4
  // Export MCP control types
5
- export type { McpControlState, McpControlEvent, McpCursorEvent, McpClickEvent } from './browser/browser-mcp-control';
5
+ export type { McpControlEvent, McpCursorEvent, McpClickEvent } from './browser/browser-mcp-control';
6
6
 
7
7
  // Export the main preview service class and manager
8
8
  export {
@@ -117,12 +117,12 @@ export const previewRouter = createRouter()
117
117
  }))
118
118
  // MCP control events
119
119
  .emit('preview:browser-mcp-control-start', t.Object({
120
- browserSessionId: t.String(),
121
- mcpSessionId: t.Optional(t.String()),
120
+ browserTabId: t.String(),
121
+ chatSessionId: t.Optional(t.String()),
122
122
  timestamp: t.Number()
123
123
  }))
124
124
  .emit('preview:browser-mcp-control-end', t.Object({
125
- browserSessionId: t.String(),
125
+ browserTabId: t.String(),
126
126
  timestamp: t.Number()
127
127
  }))
128
128
  .emit('preview:browser-mcp-cursor-position', t.Object({
@@ -52,7 +52,7 @@
52
52
 
53
53
  // Auto-scroll reasoning/system content to bottom while receiving partial text
54
54
  $effect(() => {
55
- if (roleCategory !== 'reasoning' && roleCategory !== 'system') return;
55
+ if (roleCategory !== 'reasoning' && roleCategory !== 'system' && roleCategory !== 'compact') return;
56
56
  if (!scrollContainer) return;
57
57
  // Track message content changes (partialText for streaming, message for final)
58
58
  const _track = message.type === 'stream_event' && 'partialText' in message
@@ -93,7 +93,7 @@
93
93
  <!-- Message Content -->
94
94
  <div
95
95
  bind:this={scrollContainer}
96
- class="p-3 md:p-4 {roleCategory === 'reasoning' || roleCategory === 'system' ? 'max-h-80 overflow-y-auto' : ''}"
96
+ class="p-3 md:p-4 {roleCategory === 'reasoning' || roleCategory === 'system' || roleCategory === 'compact' ? 'max-h-80 overflow-y-auto' : ''}"
97
97
  >
98
98
  <div class="max-w-none space-y-4">
99
99
  <!-- Content rendering using MessageFormatter component -->