@myrialabs/clopen 0.2.6 → 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 +24 -13
- package/backend/engine/adapters/claude/stream.ts +10 -19
- 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 +86 -132
- package/backend/mcp/servers/browser-automation/inspection.ts +5 -11
- package/backend/preview/browser/browser-mcp-control.ts +175 -180
- package/backend/preview/browser/browser-pool.ts +3 -1
- package/backend/preview/browser/browser-preview-service.ts +3 -3
- package/backend/preview/browser/browser-tab-manager.ts +1 -1
- package/backend/preview/browser/browser-video-capture.ts +12 -14
- package/backend/preview/browser/scripts/audio-stream.ts +11 -0
- 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/chat/stream.ts +1 -1
- package/backend/ws/preview/browser/tab-info.ts +5 -2
- package/backend/ws/preview/index.ts +3 -3
- package/frontend/components/chat/input/ChatInput.svelte +0 -3
- package/frontend/components/chat/input/composables/use-chat-actions.svelte.ts +6 -2
- package/frontend/components/chat/message/MessageBubble.svelte +2 -2
- package/frontend/components/history/HistoryModal.svelte +1 -1
- package/frontend/components/preview/browser/BrowserPreview.svelte +15 -1
- package/frontend/components/preview/browser/components/Canvas.svelte +323 -49
- package/frontend/components/preview/browser/components/Container.svelte +21 -0
- package/frontend/components/preview/browser/components/Toolbar.svelte +3 -3
- package/frontend/components/preview/browser/core/coordinator.svelte.ts +15 -0
- package/frontend/components/preview/browser/core/mcp-handlers.svelte.ts +78 -51
- package/frontend/components/preview/browser/core/tab-operations.svelte.ts +1 -0
- package/frontend/components/workspace/PanelHeader.svelte +15 -0
- package/frontend/components/workspace/panels/GitPanel.svelte +22 -13
- package/frontend/components/workspace/panels/PreviewPanel.svelte +2 -0
- package/frontend/services/chat/chat.service.ts +3 -7
- package/frontend/services/preview/browser/browser-webcodecs.service.ts +32 -135
- package/frontend/stores/core/app.svelte.ts +4 -3
- package/frontend/stores/core/presence.svelte.ts +3 -2
- package/frontend/stores/core/sessions.svelte.ts +2 -0
- package/frontend/stores/ui/notification.svelte.ts +4 -1
- package/package.json +1 -1
|
@@ -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.
|
|
@@ -1132,7 +1141,7 @@ class StreamManager extends EventEmitter {
|
|
|
1132
1141
|
}
|
|
1133
1142
|
}
|
|
1134
1143
|
|
|
1135
|
-
// Cancel the per-project engine
|
|
1144
|
+
// Cancel the per-project engine — this sends an interrupt to the
|
|
1136
1145
|
// still-alive SDK subprocess, then aborts the controller. If we abort
|
|
1137
1146
|
// the controller first, the subprocess dies and the SDK's subsequent
|
|
1138
1147
|
// interrupt write fails with "Operation aborted" (unhandled rejection
|
|
@@ -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
|
}
|
|
@@ -180,38 +180,29 @@ export class ClaudeCodeEngine implements AIEngine {
|
|
|
180
180
|
* Cancel active query
|
|
181
181
|
*/
|
|
182
182
|
async cancel(): Promise<void> {
|
|
183
|
-
// Resolve all pending AskUserQuestion promises
|
|
184
|
-
// This lets the SDK process the denial responses while the subprocess
|
|
185
|
-
// is still alive, preventing "Operation aborted" write errors.
|
|
183
|
+
// Resolve all pending AskUserQuestion promises before terminating.
|
|
186
184
|
for (const [, pending] of this.pendingUserAnswers) {
|
|
187
185
|
pending.resolve({ behavior: 'deny', message: 'Cancelled' });
|
|
188
186
|
}
|
|
189
187
|
this.pendingUserAnswers.clear();
|
|
190
188
|
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
189
|
+
// Use close() to forcefully terminate the query process and clean up
|
|
190
|
+
// all resources (docs: "Forcefully ends the query and cleans up all
|
|
191
|
+
// resources"). Unlike interrupt() which can hang indefinitely when the
|
|
192
|
+
// subprocess is unresponsive, close() is synchronous and guaranteed to
|
|
193
|
+
// complete — making cancel deterministic.
|
|
194
|
+
if (this.activeQuery && typeof this.activeQuery.close === 'function') {
|
|
196
195
|
try {
|
|
197
|
-
|
|
196
|
+
this.activeQuery.close();
|
|
198
197
|
} catch {
|
|
199
|
-
// Ignore
|
|
198
|
+
// Ignore close errors — process may already be dead
|
|
200
199
|
}
|
|
201
200
|
}
|
|
202
201
|
|
|
203
|
-
// Brief delay between interrupt and abort to let the SDK flush pending
|
|
204
|
-
// write operations (e.g., handleControlRequest responses). Without this,
|
|
205
|
-
// aborting immediately after interrupt kills the subprocess while the SDK
|
|
206
|
-
// still has in-flight writes, causing "Operation aborted" rejections.
|
|
207
202
|
if (this.activeController && !this.activeController.signal.aborted) {
|
|
208
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (this.activeController) {
|
|
212
203
|
this.activeController.abort();
|
|
213
|
-
this.activeController = null;
|
|
214
204
|
}
|
|
205
|
+
this.activeController = null;
|
|
215
206
|
this.activeQuery = null;
|
|
216
207
|
}
|
|
217
208
|
|
|
@@ -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,127 +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
|
-
*
|
|
55
|
+
* Internal helper: Get active tab for MCP operations.
|
|
56
|
+
* Automatically acquires MCP control for the chat session.
|
|
57
|
+
*
|
|
58
|
+
* If this chat session already controls tabs, uses the most recently
|
|
59
|
+
* controlled tab (not necessarily the frontend's active tab).
|
|
92
60
|
*/
|
|
93
61
|
export async function getActiveTabSession(projectId?: string) {
|
|
94
|
-
// Get active tab directly from backend tab manager
|
|
95
62
|
const previewService = getPreviewService(projectId);
|
|
96
|
-
const
|
|
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);
|
|
72
|
+
if (controlledTab) {
|
|
73
|
+
debug.log('mcp', `🎮 Using session-controlled tab: ${controlledTab.id}`);
|
|
74
|
+
return { tab: controlledTab, session: controlledTab };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
97
77
|
|
|
78
|
+
// No controlled tab — use the frontend's active tab and acquire control
|
|
79
|
+
const tab = previewService.getActiveTab();
|
|
98
80
|
if (!tab) {
|
|
99
|
-
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'.");
|
|
100
82
|
}
|
|
101
83
|
|
|
102
|
-
// Acquire control
|
|
103
|
-
|
|
104
|
-
if (!
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
debug.log('mcp', `🔄 Auto-acquired control for tab ${tab.id} (resumed after idle)`);
|
|
108
|
-
}
|
|
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.`);
|
|
109
89
|
}
|
|
110
90
|
|
|
111
|
-
// For backward compatibility, return both tab and session-like reference
|
|
112
|
-
// Note: In tab-centric architecture, tab IS the session
|
|
113
91
|
return { tab, session: tab };
|
|
114
92
|
}
|
|
115
93
|
|
|
116
94
|
/**
|
|
117
|
-
* 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.
|
|
118
97
|
*/
|
|
119
98
|
export async function listTabsHandler(projectId?: string) {
|
|
120
99
|
try {
|
|
121
|
-
debug.log('mcp', '📋 MCP requesting tab list');
|
|
122
|
-
debug.log('mcp', `🔍 Input projectId: ${projectId || '(none)'}`);
|
|
123
|
-
|
|
124
|
-
// Get all tabs directly from backend
|
|
125
100
|
const previewService = getPreviewService(projectId);
|
|
126
|
-
debug.log('mcp', `✅ Using service for project: ${previewService.getProjectId()}`);
|
|
127
|
-
|
|
128
101
|
const tabs = previewService.getAllTabs();
|
|
129
|
-
|
|
102
|
+
const chatSessionId = projectContextService.getCurrentChatSessionId();
|
|
130
103
|
|
|
131
104
|
if (tabs.length === 0) {
|
|
132
105
|
return {
|
|
@@ -137,12 +110,17 @@ export async function listTabsHandler(projectId?: string) {
|
|
|
137
110
|
};
|
|
138
111
|
}
|
|
139
112
|
|
|
140
|
-
const tabList = tabs.map((tab: any, index: number) =>
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
}
|
|
143
121
|
|
|
144
|
-
|
|
145
|
-
|
|
122
|
+
return `${index + 1}. ${tab.isActive ? '* ' : ' '}[${tab.id}] ${tab.title || 'Untitled'}${status}\n URL: ${tab.url || '(empty)'}`;
|
|
123
|
+
}).join('\n\n');
|
|
146
124
|
|
|
147
125
|
return {
|
|
148
126
|
content: [{
|
|
@@ -163,16 +141,18 @@ export async function listTabsHandler(projectId?: string) {
|
|
|
163
141
|
}
|
|
164
142
|
|
|
165
143
|
/**
|
|
166
|
-
* 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).
|
|
167
146
|
*/
|
|
168
147
|
export async function switchTabHandler(args: { tabId: string; projectId?: string }) {
|
|
169
148
|
try {
|
|
170
149
|
debug.log('mcp', `🔄 MCP switching to tab: ${args.tabId}`);
|
|
171
150
|
|
|
172
|
-
// Switch tab directly in backend
|
|
173
151
|
const previewService = getPreviewService(args.projectId);
|
|
174
|
-
const
|
|
152
|
+
const chatSessionId = getChatSessionId();
|
|
153
|
+
const resolvedProjectId = previewService.getProjectId();
|
|
175
154
|
|
|
155
|
+
const success = previewService.switchTab(args.tabId);
|
|
176
156
|
if (!success) {
|
|
177
157
|
return {
|
|
178
158
|
content: [{
|
|
@@ -183,18 +163,22 @@ export async function switchTabHandler(args: { tabId: string; projectId?: string
|
|
|
183
163
|
};
|
|
184
164
|
}
|
|
185
165
|
|
|
186
|
-
//
|
|
166
|
+
// Acquire control of the new tab (adds to session's set, doesn't release others)
|
|
187
167
|
const tab = previewService.getTab(args.tabId);
|
|
188
|
-
|
|
189
|
-
// Update MCP control to the new tab
|
|
190
168
|
if (tab) {
|
|
191
|
-
browserMcpControl.
|
|
192
|
-
|
|
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
|
+
}
|
|
193
180
|
}
|
|
194
181
|
|
|
195
|
-
// Update last action to keep control alive
|
|
196
|
-
browserMcpControl.updateLastAction();
|
|
197
|
-
|
|
198
182
|
return {
|
|
199
183
|
content: [{
|
|
200
184
|
type: "text" as const,
|
|
@@ -214,40 +198,31 @@ export async function switchTabHandler(args: { tabId: string; projectId?: string
|
|
|
214
198
|
}
|
|
215
199
|
|
|
216
200
|
/**
|
|
217
|
-
* Open a new tab with optional URL and viewport configuration
|
|
218
|
-
* Auto-
|
|
201
|
+
* Open a new tab with optional URL and viewport configuration.
|
|
202
|
+
* Auto-acquires MCP control for the new tab.
|
|
219
203
|
*/
|
|
220
204
|
export async function openNewTabHandler(args: { url?: string; deviceSize?: 'desktop' | 'laptop' | 'tablet' | 'mobile'; rotation?: 'portrait' | 'landscape'; projectId?: string }) {
|
|
221
205
|
try {
|
|
222
206
|
const deviceSize = args.deviceSize || 'laptop';
|
|
223
207
|
|
|
224
|
-
// Determine default rotation based on device size if not specified
|
|
225
208
|
let rotation: 'portrait' | 'landscape';
|
|
226
209
|
if (args.rotation) {
|
|
227
210
|
rotation = args.rotation;
|
|
228
211
|
} else {
|
|
229
|
-
// Desktop and laptop default to landscape
|
|
230
|
-
// Tablet and mobile default to portrait
|
|
231
212
|
rotation = (deviceSize === 'desktop' || deviceSize === 'laptop') ? 'landscape' : 'portrait';
|
|
232
213
|
}
|
|
233
214
|
|
|
234
215
|
debug.log('mcp', `📑 MCP opening new tab with URL: ${args.url || '(empty)'}`);
|
|
235
|
-
debug.log('mcp', `📱 Device: ${deviceSize}, Rotation: ${rotation}`);
|
|
236
|
-
debug.log('mcp', `🔍 Input projectId: ${args.projectId || '(none)'}`);
|
|
237
216
|
|
|
238
|
-
// Create tab directly in backend
|
|
239
217
|
const previewService = getPreviewService(args.projectId);
|
|
240
|
-
|
|
218
|
+
const chatSessionId = getChatSessionId();
|
|
219
|
+
const resolvedProjectId = previewService.getProjectId();
|
|
241
220
|
|
|
242
221
|
const tab = await previewService.createTab(args.url || undefined, deviceSize, rotation);
|
|
243
222
|
debug.log('mcp', `✅ Tab created: ${tab.id}`);
|
|
244
223
|
|
|
245
|
-
//
|
|
246
|
-
browserMcpControl.
|
|
247
|
-
browserMcpControl.acquireControl(tab.id);
|
|
248
|
-
|
|
249
|
-
// Update last action to keep control alive
|
|
250
|
-
browserMcpControl.updateLastAction();
|
|
224
|
+
// Acquire control of the new tab (adds to session's set)
|
|
225
|
+
browserMcpControl.acquireControl(tab.id, chatSessionId, resolvedProjectId);
|
|
251
226
|
|
|
252
227
|
return {
|
|
253
228
|
content: [{
|
|
@@ -268,14 +243,13 @@ export async function openNewTabHandler(args: { url?: string; deviceSize?: 'desk
|
|
|
268
243
|
}
|
|
269
244
|
|
|
270
245
|
/**
|
|
271
|
-
* Close a specific tab by ID
|
|
272
|
-
*
|
|
246
|
+
* Close a specific tab by ID.
|
|
247
|
+
* Releases MCP control for the closed tab only.
|
|
273
248
|
*/
|
|
274
249
|
export async function closeTabHandler(args: { tabId: string; projectId?: string }) {
|
|
275
250
|
try {
|
|
276
251
|
debug.log('mcp', `❌ MCP closing tab: ${args.tabId}`);
|
|
277
252
|
|
|
278
|
-
// Close tab directly in backend
|
|
279
253
|
const previewService = getPreviewService(args.projectId);
|
|
280
254
|
const result = await previewService.closeTab(args.tabId);
|
|
281
255
|
|
|
@@ -289,19 +263,8 @@ export async function closeTabHandler(args: { tabId: string; projectId?: string
|
|
|
289
263
|
};
|
|
290
264
|
}
|
|
291
265
|
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// If there's a new active tab, acquire control
|
|
296
|
-
if (result.newActiveTabId) {
|
|
297
|
-
const newActiveTab = previewService.getTab(result.newActiveTabId);
|
|
298
|
-
if (newActiveTab) {
|
|
299
|
-
browserMcpControl.acquireControl(newActiveTab.id);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Update last action to keep control alive
|
|
304
|
-
browserMcpControl.updateLastAction();
|
|
266
|
+
// releaseTab is already called by autoReleaseForTab inside closeTab()
|
|
267
|
+
// No need to manually release here
|
|
305
268
|
|
|
306
269
|
let responseText = `Tab '${args.tabId}' closed successfully.`;
|
|
307
270
|
if (result.newActiveTabId) {
|
|
@@ -332,25 +295,21 @@ export async function closeTabHandler(args: { tabId: string; projectId?: string
|
|
|
332
295
|
}
|
|
333
296
|
|
|
334
297
|
/**
|
|
335
|
-
* Navigate active tab to a different URL
|
|
298
|
+
* Navigate active tab to a different URL.
|
|
336
299
|
* Waits for page load. Session state preserved.
|
|
337
300
|
*/
|
|
338
301
|
export async function navigateHandler(args: { url: string; projectId?: string }) {
|
|
339
302
|
try {
|
|
340
|
-
// Get active tab session
|
|
341
303
|
const { session } = await getActiveTabSession(args.projectId);
|
|
342
304
|
|
|
343
|
-
// Navigate and wait for page to load
|
|
344
305
|
await session.page.goto(args.url, {
|
|
345
306
|
waitUntil: 'domcontentloaded',
|
|
346
307
|
timeout: 30000
|
|
347
308
|
});
|
|
348
309
|
|
|
349
|
-
// Wait a bit for dynamic content to load
|
|
350
310
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
351
311
|
|
|
352
312
|
const finalUrl = session.page.url();
|
|
353
|
-
browserMcpControl.updateLastAction();
|
|
354
313
|
|
|
355
314
|
return {
|
|
356
315
|
content: [{
|
|
@@ -371,11 +330,10 @@ export async function navigateHandler(args: { url: string; projectId?: string })
|
|
|
371
330
|
}
|
|
372
331
|
|
|
373
332
|
/**
|
|
374
|
-
* Change viewport settings (device size and rotation) for active tab
|
|
333
|
+
* Change viewport settings (device size and rotation) for active tab.
|
|
375
334
|
*/
|
|
376
335
|
export async function setViewportHandler(args: { deviceSize?: 'desktop' | 'laptop' | 'tablet' | 'mobile'; rotation?: 'portrait' | 'landscape'; projectId?: string }) {
|
|
377
336
|
try {
|
|
378
|
-
// Get active tab
|
|
379
337
|
const { tab } = await getActiveTabSession(args.projectId);
|
|
380
338
|
|
|
381
339
|
const deviceSize = args.deviceSize || tab.deviceSize;
|
|
@@ -383,7 +341,6 @@ export async function setViewportHandler(args: { deviceSize?: 'desktop' | 'lapto
|
|
|
383
341
|
|
|
384
342
|
debug.log('mcp', `📱 MCP changing viewport for tab ${tab.id}: ${deviceSize} (${rotation})`);
|
|
385
343
|
|
|
386
|
-
// Get preview service and update viewport
|
|
387
344
|
const previewService = getPreviewService(args.projectId);
|
|
388
345
|
const success = await previewService.setViewport(tab.id, deviceSize, rotation);
|
|
389
346
|
|
|
@@ -397,9 +354,6 @@ export async function setViewportHandler(args: { deviceSize?: 'desktop' | 'lapto
|
|
|
397
354
|
};
|
|
398
355
|
}
|
|
399
356
|
|
|
400
|
-
// Update last action to keep control alive
|
|
401
|
-
browserMcpControl.updateLastAction();
|
|
402
|
-
|
|
403
357
|
return {
|
|
404
358
|
content: [{
|
|
405
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: [
|