@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.
@@ -473,7 +473,7 @@
473
473
  bind:isConsoleOpen
474
474
  {tabs}
475
475
  {activeTabId}
476
- mcpControlledTabId={mcpHandler.mcpControlState.controlledTabId}
476
+ mcpControlledTabIds={mcpHandler.getControlledTabIds()}
477
477
  onGoClick={handleGoClick}
478
478
  onRefresh={refreshPreview}
479
479
  onOpenInExternalBrowser={() => {}}
@@ -406,7 +406,7 @@
406
406
  // This matches the loading overlay background roughly
407
407
  if (ctx) {
408
408
  ctx.imageSmoothingEnabled = true;
409
- ctx.imageSmoothingQuality = 'low'; // Faster rendering
409
+ ctx.imageSmoothingQuality = 'medium';
410
410
  ctx.fillStyle = '#f1f5f9'; // slate-100 - neutral light color
411
411
  ctx.fillRect(0, 0, canvasElement.width, canvasElement.height);
412
412
  }
@@ -25,7 +25,7 @@
25
25
  // Tab state
26
26
  tabs = $bindable<any[]>([]),
27
27
  activeTabId = $bindable<string | null>(null),
28
- mcpControlledTabId = $bindable<string | null>(null),
28
+ mcpControlledTabIds = $bindable<Set<string>>(new Set()),
29
29
 
30
30
  // Callbacks
31
31
  onGoClick = $bindable<() => void>(() => {}),
@@ -233,11 +233,11 @@
233
233
  <span class="truncate max-w-28" title={tab.url}>
234
234
  {tab.title || 'New Tab'}
235
235
  </span>
236
- {#if tab.id === mcpControlledTabId}
236
+ {#if mcpControlledTabIds.has(tab.id)}
237
237
  <span title="MCP Controlled" class="flex"><Icon name="lucide:lock" class="w-3 h-3 flex-shrink-0 text-amber-500" /></span>
238
238
  {/if}
239
239
  <!-- Close button -->
240
- {#if tab.id !== mcpControlledTabId}
240
+ {#if !mcpControlledTabIds.has(tab.id)}
241
241
  <span
242
242
  role="button"
243
243
  tabindex="0"
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Browser MCP Event Handlers
3
3
  * Handles MCP (Model Context Protocol) control events for BrowserPreview
4
+ *
5
+ * Supports multi-tab control: each chat session can control multiple tabs.
6
+ * Tracks controlled tabs via a Set of backend tab IDs (session IDs).
4
7
  */
5
8
 
6
9
  import { debug } from '$shared/utils/logger';
@@ -8,14 +11,6 @@ import { showInfo, showWarning } from '$frontend/stores/ui/notification.svelte';
8
11
  import ws from '$frontend/utils/ws';
9
12
  import type { TabManager } from './tab-manager.svelte';
10
13
 
11
- // MCP Control State interface
12
- export interface McpControlState {
13
- isControlled: boolean;
14
- controlledTabId: string | null;
15
- browserSessionId: string | null;
16
- startedAt: number | null;
17
- }
18
-
19
14
  export interface McpHandlerConfig {
20
15
  tabManager: TabManager;
21
16
  transformBrowserToDisplayCoordinates?: (x: number, y: number) => { x: number, y: number } | null;
@@ -30,13 +25,8 @@ export interface McpHandlerConfig {
30
25
  export function createMcpHandler(config: McpHandlerConfig) {
31
26
  const { tabManager, transformBrowserToDisplayCoordinates, onCursorUpdate, onCursorHide, onLaunchRequest } = config;
32
27
 
33
- // MCP control state
34
- let mcpControlState = $state<McpControlState>({
35
- isControlled: false,
36
- controlledTabId: null,
37
- browserSessionId: null,
38
- startedAt: null
39
- });
28
+ // Set of backend tab IDs (session IDs) currently controlled by MCP
29
+ let controlledSessionIds = $state(new Set<string>());
40
30
 
41
31
  /**
42
32
  * Setup WebSocket event listeners for MCP control events
@@ -44,7 +34,7 @@ export function createMcpHandler(config: McpHandlerConfig) {
44
34
  function setupEventListeners() {
45
35
  debug.log('preview', '🎧 Setting up MCP event listeners...');
46
36
 
47
- // Listen for MCP control start/end events
37
+ // Listen for MCP control start/end events (per-tab)
48
38
  ws.on('preview:browser-mcp-control-start', (data) => {
49
39
  debug.log('preview', `📥 Received mcp-control-start:`, data);
50
40
  handleControlStart(data);
@@ -93,60 +83,72 @@ export function createMcpHandler(config: McpHandlerConfig) {
93
83
  }
94
84
 
95
85
  /**
96
- * Check if current tab is MCP controlled
86
+ * Check if current active tab is MCP controlled
97
87
  */
98
88
  function isCurrentTabMcpControlled(): boolean {
99
- return mcpControlState.isControlled && mcpControlState.controlledTabId === tabManager.activeTabId;
89
+ const activeTab = tabManager.tabs.find(t => t.id === tabManager.activeTabId);
90
+ if (!activeTab?.sessionId) return false;
91
+ return controlledSessionIds.has(activeTab.sessionId);
100
92
  }
101
93
 
102
94
  /**
103
- * Get MCP control state
95
+ * Check if a specific frontend tab is MCP controlled (by sessionId)
104
96
  */
105
- function getControlState(): McpControlState {
106
- return mcpControlState;
97
+ function isSessionControlled(sessionId: string): boolean {
98
+ return controlledSessionIds.has(sessionId);
107
99
  }
108
100
 
109
- // Private handlers
101
+ /**
102
+ * Get set of frontend tab IDs that are MCP controlled
103
+ */
104
+ function getControlledTabIds(): Set<string> {
105
+ const result = new Set<string>();
106
+ for (const tab of tabManager.tabs) {
107
+ if (tab.sessionId && controlledSessionIds.has(tab.sessionId)) {
108
+ result.add(tab.id);
109
+ }
110
+ }
111
+ return result;
112
+ }
110
113
 
111
- function handleControlStart(data: { browserSessionId: string; mcpSessionId?: string; timestamp: number }) {
112
- debug.log('preview', `🎮 MCP control started for session: ${data.browserSessionId}`);
114
+ // Private handlers
113
115
 
114
- // Find which tab has this session
115
- const tab = tabManager.tabs.find(t => t.sessionId === data.browserSessionId);
116
+ function handleControlStart(data: { browserTabId: string; chatSessionId?: string; timestamp: number }) {
117
+ debug.log('preview', `🎮 MCP control started for tab: ${data.browserTabId}`);
116
118
 
117
- mcpControlState = {
118
- isControlled: true,
119
- controlledTabId: tab?.id || null,
120
- browserSessionId: data.browserSessionId,
121
- startedAt: data.timestamp
122
- };
119
+ // Add to controlled set (reassign for Svelte reactivity)
120
+ controlledSessionIds = new Set([...controlledSessionIds, data.browserTabId]);
123
121
 
124
- // Show toast notification
125
- showWarning('MCP Control Started', 'An MCP agent is now controlling the browser. User input is blocked.', 5000);
122
+ // Show toast only for the first controlled tab
123
+ if (controlledSessionIds.size === 1) {
124
+ showWarning('MCP Control Started', 'An MCP agent is now controlling the browser. User input is blocked.', 5000);
125
+ }
126
126
  }
127
127
 
128
- function handleControlEnd(data: { browserSessionId: string; timestamp: number }) {
129
- debug.log('preview', `🎮 MCP control ended for session: ${data.browserSessionId}`);
128
+ function handleControlEnd(data: { browserTabId: string; timestamp: number }) {
129
+ debug.log('preview', `🎮 MCP control ended for tab: ${data.browserTabId}`);
130
130
 
131
- mcpControlState = {
132
- isControlled: false,
133
- controlledTabId: null,
134
- browserSessionId: null,
135
- startedAt: null
136
- };
131
+ // Remove from controlled set (reassign for Svelte reactivity)
132
+ const newSet = new Set(controlledSessionIds);
133
+ newSet.delete(data.browserTabId);
134
+ controlledSessionIds = newSet;
137
135
 
138
- // Hide cursor
139
- if (onCursorHide) {
136
+ // Hide cursor if the released tab was the active one
137
+ const activeTab = tabManager.tabs.find(t => t.id === tabManager.activeTabId);
138
+ if (activeTab?.sessionId === data.browserTabId && onCursorHide) {
140
139
  onCursorHide();
141
140
  }
142
141
 
143
- // Show toast notification
144
- showInfo('MCP Control Ended', 'MCP agent released control. You can now interact with the browser.', 4000);
142
+ // Show toast when all tabs released
143
+ if (controlledSessionIds.size === 0) {
144
+ showInfo('MCP Control Ended', 'MCP agent released control. You can now interact with the browser.', 4000);
145
+ }
145
146
  }
146
147
 
147
148
  function handleCursorPosition(data: { sessionId: string; x: number; y: number; timestamp: number; source: 'mcp' }) {
148
- // Only show cursor if MCP is controlling AND user is currently viewing that tab
149
- if (mcpControlState.isControlled && mcpControlState.browserSessionId === data.sessionId && isCurrentTabMcpControlled() && transformBrowserToDisplayCoordinates) {
149
+ // Only show cursor if this tab is controlled AND user is viewing it
150
+ const activeTab = tabManager.tabs.find(t => t.id === tabManager.activeTabId);
151
+ if (activeTab?.sessionId === data.sessionId && controlledSessionIds.has(data.sessionId) && transformBrowserToDisplayCoordinates) {
150
152
  const transformedPosition = transformBrowserToDisplayCoordinates(data.x, data.y);
151
153
  if (transformedPosition && onCursorUpdate) {
152
154
  onCursorUpdate(transformedPosition.x, transformedPosition.y, false);
@@ -155,8 +157,9 @@ export function createMcpHandler(config: McpHandlerConfig) {
155
157
  }
156
158
 
157
159
  function handleCursorClick(data: { sessionId: string; x: number; y: number; timestamp: number; source: 'mcp' }) {
158
- // Only show cursor click if MCP is controlling AND user is currently viewing that tab
159
- if (mcpControlState.isControlled && mcpControlState.browserSessionId === data.sessionId && isCurrentTabMcpControlled() && transformBrowserToDisplayCoordinates) {
160
+ // Only show cursor click if this tab is controlled AND user is viewing it
161
+ const activeTab = tabManager.tabs.find(t => t.id === tabManager.activeTabId);
162
+ if (activeTab?.sessionId === data.sessionId && controlledSessionIds.has(data.sessionId) && transformBrowserToDisplayCoordinates) {
160
163
  const transformedPosition = transformBrowserToDisplayCoordinates(data.x, data.y);
161
164
  if (transformedPosition && onCursorUpdate) {
162
165
  onCursorUpdate(transformedPosition.x, transformedPosition.y, true);
@@ -170,12 +173,7 @@ export function createMcpHandler(config: McpHandlerConfig) {
170
173
  */
171
174
  function restoreControlState(frontendTabId: string, browserSessionId: string): void {
172
175
  debug.log('preview', `🔄 Restoring MCP control state for tab: ${frontendTabId} (session: ${browserSessionId})`);
173
- mcpControlState = {
174
- isControlled: true,
175
- controlledTabId: frontendTabId,
176
- browserSessionId: browserSessionId,
177
- startedAt: Date.now()
178
- };
176
+ controlledSessionIds = new Set([...controlledSessionIds, browserSessionId]);
179
177
  }
180
178
 
181
179
  /**
@@ -183,12 +181,7 @@ export function createMcpHandler(config: McpHandlerConfig) {
183
181
  */
184
182
  function resetControlState(): void {
185
183
  debug.log('preview', `🔄 Resetting MCP control state`);
186
- mcpControlState = {
187
- isControlled: false,
188
- controlledTabId: null,
189
- browserSessionId: null,
190
- startedAt: null
191
- };
184
+ controlledSessionIds = new Set();
192
185
  if (onCursorHide) {
193
186
  onCursorHide();
194
187
  }
@@ -327,10 +320,11 @@ export function createMcpHandler(config: McpHandlerConfig) {
327
320
  return {
328
321
  setupEventListeners,
329
322
  isCurrentTabMcpControlled,
330
- getControlState,
323
+ isSessionControlled,
324
+ getControlledTabIds,
331
325
  restoreControlState,
332
326
  resetControlState,
333
- get mcpControlState() { return mcpControlState; }
327
+ get controlledSessionIds() { return controlledSessionIds; }
334
328
  };
335
329
  }
336
330
 
@@ -1426,12 +1426,7 @@
1426
1426
  <div class="space-y-1 px-1">
1427
1427
  {#each tags as tag (tag.name)}
1428
1428
  <div class="group flex items-center gap-2 px-2.5 py-2 rounded-md hover:bg-slate-100 dark:hover:bg-slate-800/60 transition-colors">
1429
- <span title={tag.isAnnotated ? 'Annotated tag' : 'Lightweight tag'} class="shrink-0">
1430
- <Icon
1431
- name={tag.isAnnotated ? 'lucide:bookmark' : 'lucide:tag'}
1432
- class="w-4 h-4 {tag.isAnnotated ? 'text-amber-500' : 'text-slate-400'}"
1433
- />
1434
- </span>
1429
+ <Icon name="lucide:tag" class="w-4 h-4 text-slate-400 shrink-0" />
1435
1430
  <div class="flex-1 min-w-0">
1436
1431
  <p class="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{tag.name}</p>
1437
1432
  <div class="flex items-center gap-1.5">
@@ -208,7 +208,7 @@ export class BrowserWebCodecsService {
208
208
 
209
209
  if (this.ctx) {
210
210
  this.ctx.imageSmoothingEnabled = true;
211
- this.ctx.imageSmoothingQuality = 'low';
211
+ this.ctx.imageSmoothingQuality = 'medium';
212
212
  }
213
213
 
214
214
  this.clearCanvas();
@@ -1003,7 +1003,7 @@ export class BrowserWebCodecsService {
1003
1003
 
1004
1004
  if (this.ctx) {
1005
1005
  this.ctx.imageSmoothingEnabled = true;
1006
- this.ctx.imageSmoothingQuality = 'low';
1006
+ this.ctx.imageSmoothingQuality = 'medium';
1007
1007
  }
1008
1008
 
1009
1009
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myrialabs/clopen",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
5
5
  "author": "Myria Labs",
6
6
  "license": "MIT",