@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.
Files changed (44) hide show
  1. package/backend/chat/stream-manager.ts +23 -12
  2. package/backend/mcp/project-context.ts +20 -0
  3. package/backend/mcp/servers/browser-automation/actions.ts +0 -2
  4. package/backend/mcp/servers/browser-automation/browser.ts +80 -143
  5. package/backend/mcp/servers/browser-automation/inspection.ts +5 -11
  6. package/backend/preview/browser/browser-mcp-control.ts +174 -195
  7. package/backend/preview/browser/browser-preview-service.ts +3 -3
  8. package/backend/preview/browser/browser-video-capture.ts +12 -14
  9. package/backend/preview/browser/scripts/video-stream.ts +14 -14
  10. package/backend/preview/browser/types.ts +7 -7
  11. package/backend/preview/index.ts +1 -1
  12. package/backend/terminal/stream-manager.ts +40 -26
  13. package/backend/ws/preview/index.ts +3 -3
  14. package/backend/ws/system/operations.ts +23 -0
  15. package/frontend/components/chat/message/MessageBubble.svelte +2 -2
  16. package/frontend/components/chat/tools/components/FileHeader.svelte +1 -1
  17. package/frontend/components/chat/tools/components/TerminalCommand.svelte +8 -1
  18. package/frontend/components/common/overlay/Dialog.svelte +1 -1
  19. package/frontend/components/common/overlay/Lightbox.svelte +2 -2
  20. package/frontend/components/common/overlay/Modal.svelte +2 -2
  21. package/frontend/components/common/xterm/XTerm.svelte +6 -1
  22. package/frontend/components/git/ConflictResolver.svelte +1 -1
  23. package/frontend/components/git/GitModal.svelte +2 -2
  24. package/frontend/components/preview/browser/BrowserPreview.svelte +1 -1
  25. package/frontend/components/preview/browser/components/Canvas.svelte +1 -1
  26. package/frontend/components/preview/browser/components/Toolbar.svelte +4 -4
  27. package/frontend/components/preview/browser/core/mcp-handlers.svelte.ts +58 -64
  28. package/frontend/components/settings/SettingsModal.svelte +1 -1
  29. package/frontend/components/settings/general/DataManagementSettings.svelte +5 -66
  30. package/frontend/components/terminal/Terminal.svelte +1 -29
  31. package/frontend/components/tunnel/TunnelInactive.svelte +7 -5
  32. package/frontend/components/workspace/DesktopNavigator.svelte +1 -1
  33. package/frontend/components/workspace/PanelHeader.svelte +22 -16
  34. package/frontend/components/workspace/panels/GitPanel.svelte +1 -6
  35. package/frontend/services/preview/browser/browser-webcodecs.service.ts +2 -2
  36. package/frontend/services/project/status.service.ts +11 -1
  37. package/frontend/stores/core/sessions.svelte.ts +11 -1
  38. package/frontend/stores/features/terminal.svelte.ts +56 -26
  39. package/frontend/stores/ui/theme.svelte.ts +1 -1
  40. package/frontend/utils/ws.ts +42 -0
  41. package/index.html +2 -2
  42. package/package.json +1 -1
  43. package/shared/utils/ws-client.ts +21 -4
  44. 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
- for await (const message of engine.streamQuery({
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 control when stream completes
928
- browserMcpControl.releaseControl();
929
- debug.log('mcp', '✅ Auto-released MCP control on stream completion');
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 control on stream error
992
- browserMcpControl.releaseControl();
993
- debug.log('mcp', '✅ Auto-released MCP control on stream error');
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 control on stream cancellation
1165
- browserMcpControl.releaseControl();
1166
- debug.log('mcp', '✅ Auto-released MCP control on stream cancellation');
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
- * Session management is handled internally - no session tools exposed.
10
- * All operations work on the active tab automatically.
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
- const fallbackProjectId = activeProjects[0];
45
- debug.warn('mcp', `⚠️ No project context found, falling back to first active project: ${fallbackProjectId}`);
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
- // Tab response types
53
- interface FrontendTab {
54
- id: string;
55
- url: string;
56
- title: string;
57
- sessionId: string | null;
58
- isActive: boolean;
59
- }
60
-
61
- interface TabsListResponse {
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
- * Throws error if no active tab found
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 MCP is already controlling a specific tab, that tab is returned regardless
94
- * of which tab the user has currently active in the frontend. This prevents MCP
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
- // If MCP is already controlling a specific tab, stick to that tab.
101
- // This prevents user tab-switching from hijacking the MCP session.
102
- const controlState = browserMcpControl.getControlState();
103
- if (controlState.isControlling && controlState.browserTabId) {
104
- const controlledTab = previewService.getTab(controlState.browserTabId);
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 MCP-controlled tab: ${controlledTab.id} (ignoring active tab)`);
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 'open_tab'.");
81
+ throw new Error("No active tab found. Open a tab first using 'open_new_tab'.");
116
82
  }
117
83
 
118
- // Acquire control for active tab (ensures UI sync after idle timeout)
119
- // This is idempotent - if already controlling this tab, just updates timestamp
120
- const resolvedProjectId = previewService.getProjectId();
121
- if (!browserMcpControl.isTabControlled(tab.id, resolvedProjectId)) {
122
- const acquired = browserMcpControl.acquireControl(tab.id, undefined, resolvedProjectId);
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
- debug.log('mcp', `📊 Found ${tabs.length} tabs`);
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
- `${index + 1}. ${tab.isActive ? '* ' : ' '}[${tab.id}] ${tab.title || 'Untitled'}\n URL: ${tab.url || '(empty)'}`
159
- ).join('\n\n');
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
- // Update last action to keep control alive
162
- browserMcpControl.updateLastAction();
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 success = previewService.switchTab(args.tabId);
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
- // Get the tab that was just activated
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.releaseControl();
209
- browserMcpControl.acquireControl(tab.id, undefined, previewService.getProjectId());
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-creates browser session and acquires MCP control
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
- debug.log('mcp', `✅ Using service for project: ${previewService.getProjectId()}`);
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
- // Auto-acquire control of the new tab
263
- browserMcpControl.releaseControl();
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
- * Auto-destroys browser session and releases MCP control
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
- // Release control of closed tab
310
- browserMcpControl.releaseControl();
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
- // Update last action to keep control alive
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
- // Update last action to keep control alive
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
- // Update last action to keep control alive
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
- // Update last action to keep control alive
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
- // Update last action to keep control alive
475
- browserMcpControl.updateLastAction();
469
+
476
470
 
477
471
  return {
478
472
  content: [