@myrialabs/clopen 0.2.7 → 0.2.9
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/terminal/stream-manager.ts +40 -26
- package/backend/ws/preview/index.ts +3 -3
- package/backend/ws/system/operations.ts +23 -0
- package/frontend/components/chat/message/MessageBubble.svelte +2 -2
- package/frontend/components/chat/tools/components/FileHeader.svelte +1 -1
- package/frontend/components/chat/tools/components/TerminalCommand.svelte +8 -1
- package/frontend/components/common/overlay/Dialog.svelte +1 -1
- package/frontend/components/common/overlay/Lightbox.svelte +2 -2
- package/frontend/components/common/overlay/Modal.svelte +2 -2
- package/frontend/components/common/xterm/XTerm.svelte +6 -1
- package/frontend/components/git/ConflictResolver.svelte +1 -1
- package/frontend/components/git/GitModal.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 +4 -4
- package/frontend/components/preview/browser/core/mcp-handlers.svelte.ts +58 -64
- package/frontend/components/settings/SettingsModal.svelte +1 -1
- package/frontend/components/settings/general/DataManagementSettings.svelte +5 -66
- package/frontend/components/terminal/Terminal.svelte +1 -29
- package/frontend/components/tunnel/TunnelInactive.svelte +7 -5
- package/frontend/components/workspace/DesktopNavigator.svelte +1 -1
- package/frontend/components/workspace/PanelHeader.svelte +22 -16
- package/frontend/components/workspace/panels/GitPanel.svelte +1 -6
- package/frontend/services/preview/browser/browser-webcodecs.service.ts +2 -2
- package/frontend/services/project/status.service.ts +11 -1
- package/frontend/stores/core/sessions.svelte.ts +11 -1
- package/frontend/stores/features/terminal.svelte.ts +56 -26
- package/frontend/stores/ui/theme.svelte.ts +1 -1
- package/frontend/utils/ws.ts +42 -0
- package/index.html +2 -2
- package/package.json +1 -1
- package/shared/utils/ws-client.ts +21 -4
- package/static/manifest.json +2 -2
|
@@ -485,7 +485,8 @@ class StreamManager extends EventEmitter {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
// Stream messages through the engine adapter
|
|
488
|
-
|
|
488
|
+
// Wrap in execution context so MCP tool handlers can access chatSessionId/projectId
|
|
489
|
+
const streamIterable = engine.streamQuery({
|
|
489
490
|
projectPath: actualProjectPath,
|
|
490
491
|
prompt: enginePrompt,
|
|
491
492
|
resume: resumeSessionId,
|
|
@@ -493,7 +494,11 @@ class StreamManager extends EventEmitter {
|
|
|
493
494
|
includePartialMessages: true,
|
|
494
495
|
abortController: streamState.abortController,
|
|
495
496
|
...(claudeAccountId !== undefined && { claudeAccountId }),
|
|
496
|
-
})
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
await projectContextService.runWithContextAsync(
|
|
500
|
+
{ chatSessionId, projectId, streamId: streamState.streamId },
|
|
501
|
+
async () => { for await (const message of streamIterable) {
|
|
497
502
|
// Check if cancelled (cancelStream() already set status and emitted event)
|
|
498
503
|
if ((streamState.status as string) === 'cancelled' || streamState.abortController?.signal.aborted) {
|
|
499
504
|
break;
|
|
@@ -909,7 +914,7 @@ class StreamManager extends EventEmitter {
|
|
|
909
914
|
sender_id: requestData.senderId,
|
|
910
915
|
sender_name: requestData.senderName
|
|
911
916
|
});
|
|
912
|
-
}
|
|
917
|
+
} }); // end runWithContextAsync + for await
|
|
913
918
|
|
|
914
919
|
// Only mark as completed if not already cancelled/errored
|
|
915
920
|
if (streamState.status === 'active') {
|
|
@@ -924,9 +929,11 @@ class StreamManager extends EventEmitter {
|
|
|
924
929
|
this.emitStreamLifecycle(streamState, 'completed');
|
|
925
930
|
}
|
|
926
931
|
|
|
927
|
-
// Auto-release MCP
|
|
928
|
-
|
|
929
|
-
|
|
932
|
+
// Auto-release all MCP-controlled tabs for this chat session
|
|
933
|
+
if (chatSessionId) {
|
|
934
|
+
browserMcpControl.releaseSession(chatSessionId);
|
|
935
|
+
debug.log('mcp', `✅ Auto-released MCP tabs for session ${chatSessionId.slice(0, 8)} on stream completion`);
|
|
936
|
+
}
|
|
930
937
|
|
|
931
938
|
} catch (error) {
|
|
932
939
|
// Don't overwrite status if already cancelled by cancelStream()
|
|
@@ -988,9 +995,11 @@ class StreamManager extends EventEmitter {
|
|
|
988
995
|
this.emitStreamLifecycle(streamState, 'error');
|
|
989
996
|
}
|
|
990
997
|
|
|
991
|
-
// Auto-release MCP
|
|
992
|
-
|
|
993
|
-
|
|
998
|
+
// Auto-release all MCP-controlled tabs for this chat session
|
|
999
|
+
if (requestData.chatSessionId) {
|
|
1000
|
+
browserMcpControl.releaseSession(requestData.chatSessionId);
|
|
1001
|
+
debug.log('mcp', `✅ Auto-released MCP tabs for session ${requestData.chatSessionId.slice(0, 8)} on stream error`);
|
|
1002
|
+
}
|
|
994
1003
|
} finally {
|
|
995
1004
|
// Capture snapshot ONCE at stream end (regardless of completion/error/cancel).
|
|
996
1005
|
// Associates the snapshot with the user message (checkpoint) that triggered the stream.
|
|
@@ -1161,9 +1170,11 @@ class StreamManager extends EventEmitter {
|
|
|
1161
1170
|
|
|
1162
1171
|
this.emitStreamLifecycle(streamState, 'cancelled');
|
|
1163
1172
|
|
|
1164
|
-
// Auto-release MCP
|
|
1165
|
-
|
|
1166
|
-
|
|
1173
|
+
// Auto-release all MCP-controlled tabs for this chat session
|
|
1174
|
+
if (streamState.chatSessionId) {
|
|
1175
|
+
browserMcpControl.releaseSession(streamState.chatSessionId);
|
|
1176
|
+
debug.log('mcp', `✅ Auto-released MCP tabs for session ${streamState.chatSessionId.slice(0, 8)} on stream cancellation`);
|
|
1177
|
+
}
|
|
1167
1178
|
|
|
1168
1179
|
return true;
|
|
1169
1180
|
}
|
|
@@ -184,6 +184,26 @@ class ProjectContextService {
|
|
|
184
184
|
return executionContext.run(context, callback);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Get chat session ID from current execution context
|
|
189
|
+
* Used by MCP browser automation to track which chat session is controlling tabs
|
|
190
|
+
*/
|
|
191
|
+
getCurrentChatSessionId(): string | null {
|
|
192
|
+
const context = this.getCurrentContext();
|
|
193
|
+
|
|
194
|
+
// 1. From AsyncLocalStorage execution context (highest priority)
|
|
195
|
+
if (context?.chatSessionId) {
|
|
196
|
+
return context.chatSessionId;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 2. Fallback to most recent active stream
|
|
200
|
+
if (this.mostRecentActiveStream) {
|
|
201
|
+
return this.mostRecentActiveStream.chatSessionId;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
187
207
|
/**
|
|
188
208
|
* Get project ID from current execution context
|
|
189
209
|
* This is the primary method MCP handlers should use
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { browserPreviewServiceManager, type BrowserPreviewService } from "$backend/preview";
|
|
6
6
|
import type { BrowserAutonomousAction } from "$backend/preview/browser/types";
|
|
7
|
-
import { browserMcpControl } from "$backend/preview";
|
|
8
7
|
import { projectContextService } from "$backend/mcp/project-context";
|
|
9
8
|
import { getActiveTabSession } from "./browser";
|
|
10
9
|
import { debug } from "$shared/utils/logger";
|
|
@@ -116,7 +115,6 @@ export async function actionsHandler(args: {
|
|
|
116
115
|
// Note: Cursor events are emitted by performAutonomousActions internally
|
|
117
116
|
// with proper delays between each action. No need to emit here.
|
|
118
117
|
const results = await previewService.performAutonomousActions(sessionId, processedActions);
|
|
119
|
-
browserMcpControl.updateLastAction();
|
|
120
118
|
|
|
121
119
|
// Format response with extracted data if any
|
|
122
120
|
const extractedData = results?.filter((r: any) => r.action === 'extract_data') || [];
|
|
@@ -6,144 +6,100 @@
|
|
|
6
6
|
* - Open and close tabs (auto session management)
|
|
7
7
|
* - Navigate active tab
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Control model:
|
|
10
|
+
* - Each chat session can control multiple tabs (accumulated via switch/open)
|
|
11
|
+
* - A tab can only be controlled by one chat session at a time
|
|
12
|
+
* - All controlled tabs are released when the chat stream ends
|
|
13
|
+
* - list_tabs shows MCP control status so AI avoids conflicts
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import { ws } from "$backend/utils/ws";
|
|
14
16
|
import { debug } from "$shared/utils/logger";
|
|
15
17
|
import { browserMcpControl, browserPreviewServiceManager, type BrowserPreviewService } from "$backend/preview";
|
|
16
18
|
import { projectContextService } from "$backend/mcp/project-context";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Get BrowserPreviewService for current MCP execution context
|
|
20
|
-
*
|
|
21
|
-
* Uses projectContextService to determine the correct project based on:
|
|
22
|
-
* 1. Explicit projectId parameter (if provided)
|
|
23
|
-
* 2. Current active chat session context
|
|
24
|
-
* 3. Most recent active stream
|
|
25
|
-
* 4. Fallback to first available project
|
|
26
22
|
*/
|
|
27
23
|
function getPreviewService(projectId?: string): BrowserPreviewService {
|
|
28
|
-
// 1. Use explicit projectId if provided
|
|
29
24
|
if (projectId) {
|
|
30
|
-
debug.log('mcp', `Using explicit projectId: ${projectId}`);
|
|
31
25
|
return browserPreviewServiceManager.getService(projectId);
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
// 2. Try to get projectId from current execution context
|
|
35
28
|
const contextProjectId = projectContextService.getCurrentProjectId();
|
|
36
29
|
if (contextProjectId) {
|
|
37
|
-
debug.log('mcp', `Using projectId from context: ${contextProjectId}`);
|
|
38
30
|
return browserPreviewServiceManager.getService(contextProjectId);
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
// 3. Fallback: Get first available project's service
|
|
42
33
|
const activeProjects = browserPreviewServiceManager.getActiveProjects();
|
|
43
34
|
if (activeProjects.length > 0) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return browserPreviewServiceManager.getService(fallbackProjectId);
|
|
35
|
+
debug.warn('mcp', `⚠️ No project context found, falling back to first active project: ${activeProjects[0]}`);
|
|
36
|
+
return browserPreviewServiceManager.getService(activeProjects[0]);
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
throw new Error('No active browser preview service found. Project isolation requires projectId.');
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tabs: FrontendTab[];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
interface ActiveTabResponse {
|
|
66
|
-
tab: FrontendTab | null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface SwitchTabResponse {
|
|
70
|
-
success: boolean;
|
|
71
|
-
tab?: FrontendTab;
|
|
72
|
-
error?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface OpenTabResponse {
|
|
76
|
-
success: boolean;
|
|
77
|
-
tab?: FrontendTab;
|
|
78
|
-
error?: string;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface CloseTabResponse {
|
|
82
|
-
success: boolean;
|
|
83
|
-
closedTabId?: string;
|
|
84
|
-
newActiveTab?: FrontendTab;
|
|
85
|
-
error?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Get chatSessionId from current execution context.
|
|
44
|
+
* Required for session-scoped MCP control.
|
|
45
|
+
*/
|
|
46
|
+
function getChatSessionId(): string {
|
|
47
|
+
const chatSessionId = projectContextService.getCurrentChatSessionId();
|
|
48
|
+
if (!chatSessionId) {
|
|
49
|
+
throw new Error('No chat session context available. Cannot acquire MCP control.');
|
|
50
|
+
}
|
|
51
|
+
return chatSessionId;
|
|
86
52
|
}
|
|
87
53
|
|
|
88
54
|
/**
|
|
89
|
-
* Internal helper: Get active tab
|
|
90
|
-
*
|
|
91
|
-
* Automatically acquires MCP control for the active tab to ensure UI sync
|
|
55
|
+
* Internal helper: Get active tab for MCP operations.
|
|
56
|
+
* Automatically acquires MCP control for the chat session.
|
|
92
57
|
*
|
|
93
|
-
* If
|
|
94
|
-
*
|
|
95
|
-
* from "following" the user when they switch tabs mid-session.
|
|
58
|
+
* If this chat session already controls tabs, uses the most recently
|
|
59
|
+
* controlled tab (not necessarily the frontend's active tab).
|
|
96
60
|
*/
|
|
97
61
|
export async function getActiveTabSession(projectId?: string) {
|
|
98
62
|
const previewService = getPreviewService(projectId);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
63
|
+
const resolvedProjectId = previewService.getProjectId();
|
|
64
|
+
const chatSessionId = getChatSessionId();
|
|
65
|
+
|
|
66
|
+
// Check if this session already controls any tabs — use the last one
|
|
67
|
+
const sessionTabs = browserMcpControl.getSessionTabs(chatSessionId);
|
|
68
|
+
if (sessionTabs.length > 0) {
|
|
69
|
+
// Use the last tab this session was working with
|
|
70
|
+
const lastTabId = sessionTabs[sessionTabs.length - 1];
|
|
71
|
+
const controlledTab = previewService.getTab(lastTabId);
|
|
105
72
|
if (controlledTab) {
|
|
106
|
-
debug.log('mcp', `🎮 Using
|
|
73
|
+
debug.log('mcp', `🎮 Using session-controlled tab: ${controlledTab.id}`);
|
|
107
74
|
return { tab: controlledTab, session: controlledTab };
|
|
108
75
|
}
|
|
109
76
|
}
|
|
110
77
|
|
|
111
|
-
// No controlled tab — use the active tab and acquire control
|
|
78
|
+
// No controlled tab — use the frontend's active tab and acquire control
|
|
112
79
|
const tab = previewService.getActiveTab();
|
|
113
|
-
|
|
114
80
|
if (!tab) {
|
|
115
|
-
throw new Error("No active tab found. Open a tab first using '
|
|
81
|
+
throw new Error("No active tab found. Open a tab first using 'open_new_tab'.");
|
|
116
82
|
}
|
|
117
83
|
|
|
118
|
-
// Acquire control
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (acquired) {
|
|
124
|
-
debug.log('mcp', `🔄 Auto-acquired control for tab ${tab.id} (resumed after idle)`);
|
|
125
|
-
}
|
|
84
|
+
// Acquire control (will fail if another session owns this tab)
|
|
85
|
+
const acquired = browserMcpControl.acquireControl(tab.id, chatSessionId, resolvedProjectId);
|
|
86
|
+
if (!acquired) {
|
|
87
|
+
const owner = browserMcpControl.getTabOwner(tab.id);
|
|
88
|
+
throw new Error(`Tab '${tab.id}' is controlled by another chat session (${owner?.slice(0, 8)}...). Use a different tab.`);
|
|
126
89
|
}
|
|
127
90
|
|
|
128
|
-
// For backward compatibility, return both tab and session-like reference
|
|
129
|
-
// Note: In tab-centric architecture, tab IS the session
|
|
130
91
|
return { tab, session: tab };
|
|
131
92
|
}
|
|
132
93
|
|
|
133
94
|
/**
|
|
134
|
-
* List all open tabs in the browser preview
|
|
95
|
+
* List all open tabs in the browser preview.
|
|
96
|
+
* Shows MCP control status so AI can avoid conflicts.
|
|
135
97
|
*/
|
|
136
98
|
export async function listTabsHandler(projectId?: string) {
|
|
137
99
|
try {
|
|
138
|
-
debug.log('mcp', '📋 MCP requesting tab list');
|
|
139
|
-
debug.log('mcp', `🔍 Input projectId: ${projectId || '(none)'}`);
|
|
140
|
-
|
|
141
|
-
// Get all tabs directly from backend
|
|
142
100
|
const previewService = getPreviewService(projectId);
|
|
143
|
-
debug.log('mcp', `✅ Using service for project: ${previewService.getProjectId()}`);
|
|
144
|
-
|
|
145
101
|
const tabs = previewService.getAllTabs();
|
|
146
|
-
|
|
102
|
+
const chatSessionId = projectContextService.getCurrentChatSessionId();
|
|
147
103
|
|
|
148
104
|
if (tabs.length === 0) {
|
|
149
105
|
return {
|
|
@@ -154,12 +110,17 @@ export async function listTabsHandler(projectId?: string) {
|
|
|
154
110
|
};
|
|
155
111
|
}
|
|
156
112
|
|
|
157
|
-
const tabList = tabs.map((tab: any, index: number) =>
|
|
158
|
-
|
|
159
|
-
|
|
113
|
+
const tabList = tabs.map((tab: any, index: number) => {
|
|
114
|
+
const owner = browserMcpControl.getTabOwner(tab.id);
|
|
115
|
+
let status = '';
|
|
116
|
+
if (owner) {
|
|
117
|
+
status = owner === chatSessionId
|
|
118
|
+
? ' (MCP: this session)'
|
|
119
|
+
: ' (MCP: another session)';
|
|
120
|
+
}
|
|
160
121
|
|
|
161
|
-
|
|
162
|
-
|
|
122
|
+
return `${index + 1}. ${tab.isActive ? '* ' : ' '}[${tab.id}] ${tab.title || 'Untitled'}${status}\n URL: ${tab.url || '(empty)'}`;
|
|
123
|
+
}).join('\n\n');
|
|
163
124
|
|
|
164
125
|
return {
|
|
165
126
|
content: [{
|
|
@@ -180,16 +141,18 @@ export async function listTabsHandler(projectId?: string) {
|
|
|
180
141
|
}
|
|
181
142
|
|
|
182
143
|
/**
|
|
183
|
-
* Switch to a specific tab by ID
|
|
144
|
+
* Switch to a specific tab by ID.
|
|
145
|
+
* Adds the tab to this session's controlled set (does NOT release other tabs).
|
|
184
146
|
*/
|
|
185
147
|
export async function switchTabHandler(args: { tabId: string; projectId?: string }) {
|
|
186
148
|
try {
|
|
187
149
|
debug.log('mcp', `🔄 MCP switching to tab: ${args.tabId}`);
|
|
188
150
|
|
|
189
|
-
// Switch tab directly in backend
|
|
190
151
|
const previewService = getPreviewService(args.projectId);
|
|
191
|
-
const
|
|
152
|
+
const chatSessionId = getChatSessionId();
|
|
153
|
+
const resolvedProjectId = previewService.getProjectId();
|
|
192
154
|
|
|
155
|
+
const success = previewService.switchTab(args.tabId);
|
|
193
156
|
if (!success) {
|
|
194
157
|
return {
|
|
195
158
|
content: [{
|
|
@@ -200,18 +163,22 @@ export async function switchTabHandler(args: { tabId: string; projectId?: string
|
|
|
200
163
|
};
|
|
201
164
|
}
|
|
202
165
|
|
|
203
|
-
//
|
|
166
|
+
// Acquire control of the new tab (adds to session's set, doesn't release others)
|
|
204
167
|
const tab = previewService.getTab(args.tabId);
|
|
205
|
-
|
|
206
|
-
// Update MCP control to the new tab
|
|
207
168
|
if (tab) {
|
|
208
|
-
browserMcpControl.
|
|
209
|
-
|
|
169
|
+
const acquired = browserMcpControl.acquireControl(tab.id, chatSessionId, resolvedProjectId);
|
|
170
|
+
if (!acquired) {
|
|
171
|
+
const owner = browserMcpControl.getTabOwner(tab.id);
|
|
172
|
+
return {
|
|
173
|
+
content: [{
|
|
174
|
+
type: "text" as const,
|
|
175
|
+
text: `Tab '${args.tabId}' is controlled by another chat session (${owner?.slice(0, 8)}...). Cannot switch to it.`
|
|
176
|
+
}],
|
|
177
|
+
isError: true
|
|
178
|
+
};
|
|
179
|
+
}
|
|
210
180
|
}
|
|
211
181
|
|
|
212
|
-
// Update last action to keep control alive
|
|
213
|
-
browserMcpControl.updateLastAction();
|
|
214
|
-
|
|
215
182
|
return {
|
|
216
183
|
content: [{
|
|
217
184
|
type: "text" as const,
|
|
@@ -231,40 +198,31 @@ export async function switchTabHandler(args: { tabId: string; projectId?: string
|
|
|
231
198
|
}
|
|
232
199
|
|
|
233
200
|
/**
|
|
234
|
-
* Open a new tab with optional URL and viewport configuration
|
|
235
|
-
* Auto-
|
|
201
|
+
* Open a new tab with optional URL and viewport configuration.
|
|
202
|
+
* Auto-acquires MCP control for the new tab.
|
|
236
203
|
*/
|
|
237
204
|
export async function openNewTabHandler(args: { url?: string; deviceSize?: 'desktop' | 'laptop' | 'tablet' | 'mobile'; rotation?: 'portrait' | 'landscape'; projectId?: string }) {
|
|
238
205
|
try {
|
|
239
206
|
const deviceSize = args.deviceSize || 'laptop';
|
|
240
207
|
|
|
241
|
-
// Determine default rotation based on device size if not specified
|
|
242
208
|
let rotation: 'portrait' | 'landscape';
|
|
243
209
|
if (args.rotation) {
|
|
244
210
|
rotation = args.rotation;
|
|
245
211
|
} else {
|
|
246
|
-
// Desktop and laptop default to landscape
|
|
247
|
-
// Tablet and mobile default to portrait
|
|
248
212
|
rotation = (deviceSize === 'desktop' || deviceSize === 'laptop') ? 'landscape' : 'portrait';
|
|
249
213
|
}
|
|
250
214
|
|
|
251
215
|
debug.log('mcp', `📑 MCP opening new tab with URL: ${args.url || '(empty)'}`);
|
|
252
|
-
debug.log('mcp', `📱 Device: ${deviceSize}, Rotation: ${rotation}`);
|
|
253
|
-
debug.log('mcp', `🔍 Input projectId: ${args.projectId || '(none)'}`);
|
|
254
216
|
|
|
255
|
-
// Create tab directly in backend
|
|
256
217
|
const previewService = getPreviewService(args.projectId);
|
|
257
|
-
|
|
218
|
+
const chatSessionId = getChatSessionId();
|
|
219
|
+
const resolvedProjectId = previewService.getProjectId();
|
|
258
220
|
|
|
259
221
|
const tab = await previewService.createTab(args.url || undefined, deviceSize, rotation);
|
|
260
222
|
debug.log('mcp', `✅ Tab created: ${tab.id}`);
|
|
261
223
|
|
|
262
|
-
//
|
|
263
|
-
browserMcpControl.
|
|
264
|
-
browserMcpControl.acquireControl(tab.id, undefined, previewService.getProjectId());
|
|
265
|
-
|
|
266
|
-
// Update last action to keep control alive
|
|
267
|
-
browserMcpControl.updateLastAction();
|
|
224
|
+
// Acquire control of the new tab (adds to session's set)
|
|
225
|
+
browserMcpControl.acquireControl(tab.id, chatSessionId, resolvedProjectId);
|
|
268
226
|
|
|
269
227
|
return {
|
|
270
228
|
content: [{
|
|
@@ -285,14 +243,13 @@ export async function openNewTabHandler(args: { url?: string; deviceSize?: 'desk
|
|
|
285
243
|
}
|
|
286
244
|
|
|
287
245
|
/**
|
|
288
|
-
* Close a specific tab by ID
|
|
289
|
-
*
|
|
246
|
+
* Close a specific tab by ID.
|
|
247
|
+
* Releases MCP control for the closed tab only.
|
|
290
248
|
*/
|
|
291
249
|
export async function closeTabHandler(args: { tabId: string; projectId?: string }) {
|
|
292
250
|
try {
|
|
293
251
|
debug.log('mcp', `❌ MCP closing tab: ${args.tabId}`);
|
|
294
252
|
|
|
295
|
-
// Close tab directly in backend
|
|
296
253
|
const previewService = getPreviewService(args.projectId);
|
|
297
254
|
const result = await previewService.closeTab(args.tabId);
|
|
298
255
|
|
|
@@ -306,19 +263,8 @@ export async function closeTabHandler(args: { tabId: string; projectId?: string
|
|
|
306
263
|
};
|
|
307
264
|
}
|
|
308
265
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// If there's a new active tab, acquire control
|
|
313
|
-
if (result.newActiveTabId) {
|
|
314
|
-
const newActiveTab = previewService.getTab(result.newActiveTabId);
|
|
315
|
-
if (newActiveTab) {
|
|
316
|
-
browserMcpControl.acquireControl(newActiveTab.id, undefined, previewService.getProjectId());
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Update last action to keep control alive
|
|
321
|
-
browserMcpControl.updateLastAction();
|
|
266
|
+
// releaseTab is already called by autoReleaseForTab inside closeTab()
|
|
267
|
+
// No need to manually release here
|
|
322
268
|
|
|
323
269
|
let responseText = `Tab '${args.tabId}' closed successfully.`;
|
|
324
270
|
if (result.newActiveTabId) {
|
|
@@ -349,25 +295,21 @@ export async function closeTabHandler(args: { tabId: string; projectId?: string
|
|
|
349
295
|
}
|
|
350
296
|
|
|
351
297
|
/**
|
|
352
|
-
* Navigate active tab to a different URL
|
|
298
|
+
* Navigate active tab to a different URL.
|
|
353
299
|
* Waits for page load. Session state preserved.
|
|
354
300
|
*/
|
|
355
301
|
export async function navigateHandler(args: { url: string; projectId?: string }) {
|
|
356
302
|
try {
|
|
357
|
-
// Get active tab session
|
|
358
303
|
const { session } = await getActiveTabSession(args.projectId);
|
|
359
304
|
|
|
360
|
-
// Navigate and wait for page to load
|
|
361
305
|
await session.page.goto(args.url, {
|
|
362
306
|
waitUntil: 'domcontentloaded',
|
|
363
307
|
timeout: 30000
|
|
364
308
|
});
|
|
365
309
|
|
|
366
|
-
// Wait a bit for dynamic content to load
|
|
367
310
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
368
311
|
|
|
369
312
|
const finalUrl = session.page.url();
|
|
370
|
-
browserMcpControl.updateLastAction();
|
|
371
313
|
|
|
372
314
|
return {
|
|
373
315
|
content: [{
|
|
@@ -388,11 +330,10 @@ export async function navigateHandler(args: { url: string; projectId?: string })
|
|
|
388
330
|
}
|
|
389
331
|
|
|
390
332
|
/**
|
|
391
|
-
* Change viewport settings (device size and rotation) for active tab
|
|
333
|
+
* Change viewport settings (device size and rotation) for active tab.
|
|
392
334
|
*/
|
|
393
335
|
export async function setViewportHandler(args: { deviceSize?: 'desktop' | 'laptop' | 'tablet' | 'mobile'; rotation?: 'portrait' | 'landscape'; projectId?: string }) {
|
|
394
336
|
try {
|
|
395
|
-
// Get active tab
|
|
396
337
|
const { tab } = await getActiveTabSession(args.projectId);
|
|
397
338
|
|
|
398
339
|
const deviceSize = args.deviceSize || tab.deviceSize;
|
|
@@ -400,7 +341,6 @@ export async function setViewportHandler(args: { deviceSize?: 'desktop' | 'lapto
|
|
|
400
341
|
|
|
401
342
|
debug.log('mcp', `📱 MCP changing viewport for tab ${tab.id}: ${deviceSize} (${rotation})`);
|
|
402
343
|
|
|
403
|
-
// Get preview service and update viewport
|
|
404
344
|
const previewService = getPreviewService(args.projectId);
|
|
405
345
|
const success = await previewService.setViewport(tab.id, deviceSize, rotation);
|
|
406
346
|
|
|
@@ -414,9 +354,6 @@ export async function setViewportHandler(args: { deviceSize?: 'desktop' | 'lapto
|
|
|
414
354
|
};
|
|
415
355
|
}
|
|
416
356
|
|
|
417
|
-
// Update last action to keep control alive
|
|
418
|
-
browserMcpControl.updateLastAction();
|
|
419
|
-
|
|
420
357
|
return {
|
|
421
358
|
content: [{
|
|
422
359
|
type: "text" as const,
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { browserPreviewServiceManager, type BrowserPreviewService } from "$backend/preview";
|
|
6
|
-
import { browserMcpControl } from "$backend/preview";
|
|
7
6
|
import { projectContextService } from "$backend/mcp/project-context";
|
|
8
7
|
import { getActiveTabSession } from "./browser";
|
|
9
8
|
import { debug } from "$shared/utils/logger";
|
|
@@ -49,8 +48,7 @@ export async function getConsoleLogsHandler(args: {
|
|
|
49
48
|
const previewService = getPreviewService(args.projectId);
|
|
50
49
|
const logs = previewService.getConsoleLogs(sessionId);
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
browserMcpControl.updateLastAction();
|
|
51
|
+
|
|
54
52
|
|
|
55
53
|
if (logs.length === 0) {
|
|
56
54
|
return {
|
|
@@ -97,8 +95,7 @@ export async function clearConsoleLogsHandler(args: { projectId?: string } = {})
|
|
|
97
95
|
const previewService = getPreviewService(args.projectId);
|
|
98
96
|
const success = previewService.clearConsoleLogs(sessionId);
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
browserMcpControl.updateLastAction();
|
|
98
|
+
|
|
102
99
|
|
|
103
100
|
if (!success) {
|
|
104
101
|
return {
|
|
@@ -141,8 +138,7 @@ export async function executeConsoleHandler(args: {
|
|
|
141
138
|
const previewService = getPreviewService(args.projectId);
|
|
142
139
|
const result = await previewService.executeConsoleCommand(sessionId, args.command);
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
browserMcpControl.updateLastAction();
|
|
141
|
+
|
|
146
142
|
|
|
147
143
|
return {
|
|
148
144
|
content: [{
|
|
@@ -438,8 +434,7 @@ export async function analyzeDomHandler(args: {
|
|
|
438
434
|
filtered = analysis;
|
|
439
435
|
}
|
|
440
436
|
|
|
441
|
-
|
|
442
|
-
browserMcpControl.updateLastAction();
|
|
437
|
+
|
|
443
438
|
|
|
444
439
|
return {
|
|
445
440
|
content: [{
|
|
@@ -471,8 +466,7 @@ export async function takeScreenshotHandler() {
|
|
|
471
466
|
type: 'png'
|
|
472
467
|
});
|
|
473
468
|
|
|
474
|
-
|
|
475
|
-
browserMcpControl.updateLastAction();
|
|
469
|
+
|
|
476
470
|
|
|
477
471
|
return {
|
|
478
472
|
content: [
|