@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.
- package/backend/chat/stream-manager.ts +23 -12
- package/backend/mcp/project-context.ts +20 -0
- package/backend/mcp/servers/browser-automation/actions.ts +0 -2
- package/backend/mcp/servers/browser-automation/browser.ts +80 -143
- package/backend/mcp/servers/browser-automation/inspection.ts +5 -11
- package/backend/preview/browser/browser-mcp-control.ts +174 -195
- package/backend/preview/browser/browser-preview-service.ts +3 -3
- package/backend/preview/browser/browser-video-capture.ts +12 -14
- package/backend/preview/browser/scripts/video-stream.ts +14 -14
- package/backend/preview/browser/types.ts +7 -7
- package/backend/preview/index.ts +1 -1
- package/backend/ws/preview/index.ts +3 -3
- package/frontend/components/chat/message/MessageBubble.svelte +2 -2
- package/frontend/components/preview/browser/BrowserPreview.svelte +1 -1
- package/frontend/components/preview/browser/components/Canvas.svelte +1 -1
- package/frontend/components/preview/browser/components/Toolbar.svelte +3 -3
- package/frontend/components/preview/browser/core/mcp-handlers.svelte.ts +58 -64
- package/frontend/components/workspace/panels/GitPanel.svelte +1 -6
- package/frontend/services/preview/browser/browser-webcodecs.service.ts +2 -2
- package/package.json +1 -1
|
@@ -473,7 +473,7 @@
|
|
|
473
473
|
bind:isConsoleOpen
|
|
474
474
|
{tabs}
|
|
475
475
|
{activeTabId}
|
|
476
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
34
|
-
let
|
|
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
|
-
|
|
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
|
-
*
|
|
95
|
+
* Check if a specific frontend tab is MCP controlled (by sessionId)
|
|
104
96
|
*/
|
|
105
|
-
function
|
|
106
|
-
return
|
|
97
|
+
function isSessionControlled(sessionId: string): boolean {
|
|
98
|
+
return controlledSessionIds.has(sessionId);
|
|
107
99
|
}
|
|
108
100
|
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
debug.log('preview', `🎮 MCP control started for session: ${data.browserSessionId}`);
|
|
114
|
+
// Private handlers
|
|
113
115
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
function handleControlStart(data: { browserTabId: string; chatSessionId?: string; timestamp: number }) {
|
|
117
|
+
debug.log('preview', `🎮 MCP control started for tab: ${data.browserTabId}`);
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
125
|
-
|
|
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: {
|
|
129
|
-
debug.log('preview', `🎮 MCP control ended for
|
|
128
|
+
function handleControlEnd(data: { browserTabId: string; timestamp: number }) {
|
|
129
|
+
debug.log('preview', `🎮 MCP control ended for tab: ${data.browserTabId}`);
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
144
|
-
|
|
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
|
|
149
|
-
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
+
isSessionControlled,
|
|
324
|
+
getControlledTabIds,
|
|
331
325
|
restoreControlState,
|
|
332
326
|
resetControlState,
|
|
333
|
-
get
|
|
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
|
-
<
|
|
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 = '
|
|
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 = '
|
|
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.
|
|
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",
|