vibesurf 0.1.9a6__py3-none-any.whl → 0.1.11__py3-none-any.whl

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.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (69) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +68 -45
  3. vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
  4. vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
  5. vibe_surf/agents/report_writer_agent.py +380 -226
  6. vibe_surf/agents/vibe_surf_agent.py +878 -814
  7. vibe_surf/agents/views.py +130 -0
  8. vibe_surf/backend/api/activity.py +3 -1
  9. vibe_surf/backend/api/browser.py +70 -0
  10. vibe_surf/backend/api/config.py +8 -5
  11. vibe_surf/backend/api/files.py +59 -50
  12. vibe_surf/backend/api/models.py +2 -2
  13. vibe_surf/backend/api/task.py +47 -13
  14. vibe_surf/backend/database/manager.py +24 -18
  15. vibe_surf/backend/database/queries.py +199 -192
  16. vibe_surf/backend/database/schemas.py +1 -1
  17. vibe_surf/backend/main.py +80 -3
  18. vibe_surf/backend/shared_state.py +30 -35
  19. vibe_surf/backend/utils/encryption.py +3 -1
  20. vibe_surf/backend/utils/llm_factory.py +41 -36
  21. vibe_surf/browser/agent_browser_session.py +308 -62
  22. vibe_surf/browser/browser_manager.py +71 -100
  23. vibe_surf/browser/utils.py +5 -3
  24. vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
  25. vibe_surf/chrome_extension/background.js +88 -0
  26. vibe_surf/chrome_extension/manifest.json +3 -1
  27. vibe_surf/chrome_extension/scripts/api-client.js +13 -0
  28. vibe_surf/chrome_extension/scripts/file-manager.js +482 -0
  29. vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
  30. vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
  31. vibe_surf/chrome_extension/scripts/session-manager.js +52 -11
  32. vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
  33. vibe_surf/chrome_extension/scripts/ui-manager.js +1530 -3163
  34. vibe_surf/chrome_extension/sidepanel.html +47 -7
  35. vibe_surf/chrome_extension/styles/activity.css +934 -0
  36. vibe_surf/chrome_extension/styles/base.css +76 -0
  37. vibe_surf/chrome_extension/styles/history-modal.css +791 -0
  38. vibe_surf/chrome_extension/styles/input.css +568 -0
  39. vibe_surf/chrome_extension/styles/layout.css +186 -0
  40. vibe_surf/chrome_extension/styles/responsive.css +454 -0
  41. vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
  42. vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
  43. vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
  44. vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
  45. vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
  46. vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
  47. vibe_surf/chrome_extension/styles/variables.css +54 -0
  48. vibe_surf/cli.py +5 -22
  49. vibe_surf/common.py +35 -0
  50. vibe_surf/llm/openai_compatible.py +148 -93
  51. vibe_surf/logger.py +99 -0
  52. vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -221
  53. vibe_surf/tools/file_system.py +415 -0
  54. vibe_surf/{controller → tools}/mcp_client.py +4 -3
  55. vibe_surf/tools/report_writer_tools.py +21 -0
  56. vibe_surf/tools/vibesurf_tools.py +657 -0
  57. vibe_surf/tools/views.py +120 -0
  58. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/METADATA +23 -3
  59. vibesurf-0.1.11.dist-info/RECORD +93 -0
  60. vibe_surf/chrome_extension/styles/main.css +0 -2338
  61. vibe_surf/chrome_extension/styles/settings.css +0 -1100
  62. vibe_surf/controller/file_system.py +0 -53
  63. vibe_surf/controller/views.py +0 -37
  64. vibesurf-0.1.9a6.dist-info/RECORD +0 -71
  65. /vibe_surf/{controller → tools}/__init__.py +0 -0
  66. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/WHEEL +0 -0
  67. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/entry_points.txt +0 -0
  68. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/licenses/LICENSE +0 -0
  69. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.11.dist-info}/top_level.txt +0 -0
@@ -5,10 +5,11 @@ import json
5
5
  import enum
6
6
  import base64
7
7
  import mimetypes
8
-
8
+ import datetime
9
+ from pathvalidate import sanitize_filename
9
10
  from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable, TypeVar
10
11
  from pydantic import BaseModel
11
- from browser_use.tools.service import Controller, Tools
12
+ from browser_use.tools.service import Tools
12
13
  import logging
13
14
  from browser_use.agent.views import ActionModel, ActionResult
14
15
  from browser_use.utils import time_execution_sync
@@ -38,34 +39,227 @@ from browser_use.dom.service import EnhancedDOMTreeNode
38
39
  from browser_use.browser.views import BrowserError
39
40
  from browser_use.mcp.client import MCPClient
40
41
 
41
-
42
42
  from vibe_surf.browser.agent_browser_session import AgentBrowserSession
43
- from vibe_surf.controller.views import HoverAction, ExtractionAction, FileExtractionAction
44
- from vibe_surf.controller.mcp_client import VibeSurfMCPClient
43
+ from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction
44
+ from vibe_surf.tools.mcp_client import CustomMCPClient
45
+ from vibe_surf.tools.file_system import CustomFileSystem
46
+ from vibe_surf.logger import get_logger
47
+ from vibe_surf.tools.vibesurf_tools import VibeSurfTools
45
48
 
46
- logger = logging.getLogger(__name__)
49
+ logger = get_logger(__name__)
47
50
 
48
51
  Context = TypeVar('Context')
49
52
 
50
53
  T = TypeVar('T', bound=BaseModel)
51
54
 
52
55
 
53
- class VibeSurfTools(Tools):
56
+ class BrowserUseTools(Tools, VibeSurfTools):
54
57
  def __init__(self,
55
58
  exclude_actions: list[str] = [],
56
59
  output_model: type[T] | None = None,
57
60
  display_files_in_done_text: bool = True,
58
- mcp_server_config: Optional[Dict[str, Any]] = None
59
61
  ):
60
- super().__init__(exclude_actions=exclude_actions, output_model=output_model,
61
- display_files_in_done_text=display_files_in_done_text)
62
+ Tools.__init__(self, exclude_actions=exclude_actions, output_model=output_model,
63
+ display_files_in_done_text=display_files_in_done_text)
62
64
  self._register_browser_actions()
63
- self.mcp_server_config = mcp_server_config
64
- self.mcp_clients = {}
65
+ self._register_file_actions()
66
+
67
+ def _register_done_action(self, output_model: type[T] | None, display_files_in_done_text: bool = True):
68
+ if output_model is not None:
69
+ self.display_files_in_done_text = display_files_in_done_text
70
+
71
+ @self.registry.action(
72
+ 'Complete task - with return text and if the task is finished (success=True) or not yet completely finished (success=False), because last step is reached',
73
+ param_model=StructuredOutputAction[output_model],
74
+ )
75
+ async def done(params: StructuredOutputAction):
76
+ # Exclude success from the output JSON since it's an internal parameter
77
+ output_dict = params.data.model_dump()
78
+
79
+ # Enums are not serializable, convert to string
80
+ for key, value in output_dict.items():
81
+ if isinstance(value, enum.Enum):
82
+ output_dict[key] = value.value
83
+
84
+ return ActionResult(
85
+ is_done=True,
86
+ success=params.success,
87
+ extracted_content=json.dumps(output_dict),
88
+ long_term_memory=f'Task completed. Success Status: {params.success}',
89
+ )
90
+
91
+ else:
92
+
93
+ @self.registry.action(
94
+ 'Complete task - provide a summary of results for the user. Set success=True if task completed successfully, false otherwise. Text should be your response to the user summarizing results. Include files in files_to_display if you would like to display to the user or there files are important for the task result.',
95
+ param_model=DoneAction,
96
+ )
97
+ async def done(params: DoneAction, file_system: CustomFileSystem):
98
+ user_message = params.text
99
+
100
+ len_text = len(params.text)
101
+ len_max_memory = 100
102
+ memory = f'Task completed: {params.success} - {params.text[:len_max_memory]}'
103
+ if len_text > len_max_memory:
104
+ memory += f' - {len_text - len_max_memory} more characters'
105
+
106
+ attachments = []
107
+ if params.files_to_display:
108
+ if self.display_files_in_done_text:
109
+ file_msg = ''
110
+ for file_name in params.files_to_display:
111
+ if file_name == 'todo.md':
112
+ continue
113
+ file_content = await file_system.display_file(file_name)
114
+ if file_content:
115
+ file_msg += f'\n\n{file_name}:\n{file_content}'
116
+ attachments.append(file_name)
117
+ if file_msg:
118
+ user_message += '\n\nAttachments:'
119
+ user_message += file_msg
120
+ else:
121
+ logger.warning('Agent wanted to display files but none were found')
122
+ else:
123
+ for file_name in params.files_to_display:
124
+ if file_name == 'todo.md':
125
+ continue
126
+ file_content = await file_system.display_file(file_name)
127
+ if file_content:
128
+ attachments.append(file_name)
129
+
130
+ attachments = [file_name for file_name in attachments]
131
+
132
+ return ActionResult(
133
+ is_done=True,
134
+ success=params.success,
135
+ extracted_content=user_message,
136
+ long_term_memory=memory,
137
+ attachments=attachments,
138
+ )
65
139
 
66
140
  def _register_browser_actions(self):
67
141
  """Register custom browser actions"""
68
142
 
143
+ @self.registry.action('Upload file to interactive element with file path', param_model=UploadFileAction)
144
+ async def upload_file_to_element(
145
+ params: UploadFileAction, browser_session: BrowserSession, file_system: FileSystem
146
+ ):
147
+
148
+ # For local browsers, ensure the file exists on the local filesystem
149
+ full_file_path = params.path
150
+ if not os.path.exists(full_file_path):
151
+ full_file_path = str(file_system.get_dir() / params.path)
152
+ if not os.path.exists(full_file_path):
153
+ msg = f'File {params.path} does not exist'
154
+ return ActionResult(error=msg)
155
+
156
+ # Get the selector map to find the node
157
+ selector_map = await browser_session.get_selector_map()
158
+ if params.index not in selector_map:
159
+ msg = f'Element with index {params.index} does not exist.'
160
+ return ActionResult(error=msg)
161
+
162
+ node = selector_map[params.index]
163
+
164
+ # Helper function to find file input near the selected element
165
+ def find_file_input_near_element(
166
+ node: EnhancedDOMTreeNode, max_height: int = 3, max_descendant_depth: int = 3
167
+ ) -> EnhancedDOMTreeNode | None:
168
+ """Find the closest file input to the selected element."""
169
+
170
+ def find_file_input_in_descendants(n: EnhancedDOMTreeNode, depth: int) -> EnhancedDOMTreeNode | None:
171
+ if depth < 0:
172
+ return None
173
+ if browser_session.is_file_input(n):
174
+ return n
175
+ for child in n.children_nodes or []:
176
+ result = find_file_input_in_descendants(child, depth - 1)
177
+ if result:
178
+ return result
179
+ return None
180
+
181
+ current = node
182
+ for _ in range(max_height + 1):
183
+ # Check the current node itself
184
+ if browser_session.is_file_input(current):
185
+ return current
186
+ # Check all descendants of the current node
187
+ result = find_file_input_in_descendants(current, max_descendant_depth)
188
+ if result:
189
+ return result
190
+ # Check all siblings and their descendants
191
+ if current.parent_node:
192
+ for sibling in current.parent_node.children_nodes or []:
193
+ if sibling is current:
194
+ continue
195
+ if browser_session.is_file_input(sibling):
196
+ return sibling
197
+ result = find_file_input_in_descendants(sibling, max_descendant_depth)
198
+ if result:
199
+ return result
200
+ current = current.parent_node
201
+ if not current:
202
+ break
203
+ return None
204
+
205
+ # Try to find a file input element near the selected element
206
+ file_input_node = find_file_input_near_element(node)
207
+
208
+ # If not found near the selected element, fallback to finding the closest file input to current scroll position
209
+ if file_input_node is None:
210
+ logger.info(
211
+ f'No file upload element found near index {params.index}, searching for closest file input to scroll position'
212
+ )
213
+
214
+ # Get current scroll position
215
+ cdp_session = await browser_session.get_or_create_cdp_session()
216
+ try:
217
+ scroll_info = await cdp_session.cdp_client.send.Runtime.evaluate(
218
+ params={'expression': 'window.scrollY || window.pageYOffset || 0'},
219
+ session_id=cdp_session.session_id
220
+ )
221
+ current_scroll_y = scroll_info.get('result', {}).get('value', 0)
222
+ except Exception:
223
+ current_scroll_y = 0
224
+
225
+ # Find all file inputs in the selector map and pick the closest one to scroll position
226
+ closest_file_input = None
227
+ min_distance = float('inf')
228
+
229
+ for idx, element in selector_map.items():
230
+ if browser_session.is_file_input(element):
231
+ # Get element's Y position
232
+ if element.absolute_position:
233
+ element_y = element.absolute_position.y
234
+ distance = abs(element_y - current_scroll_y)
235
+ if distance < min_distance:
236
+ min_distance = distance
237
+ closest_file_input = element
238
+
239
+ if closest_file_input:
240
+ file_input_node = closest_file_input
241
+ logger.info(f'Found file input closest to scroll position (distance: {min_distance}px)')
242
+ else:
243
+ msg = 'No file upload element found on the page'
244
+ logger.error(msg)
245
+ raise BrowserError(msg)
246
+ # TODO: figure out why this fails sometimes + add fallback hail mary, just look for any file input on page
247
+
248
+ # Dispatch upload file event with the file input node
249
+ try:
250
+ event = browser_session.event_bus.dispatch(UploadFileEvent(node=file_input_node, file_path=full_file_path))
251
+ await event
252
+ await event.event_result(raise_if_any=True, raise_if_none=False)
253
+ msg = f'Successfully uploaded file to index {params.index}'
254
+ logger.info(f'📁 {msg}')
255
+ return ActionResult(
256
+ extracted_content=msg,
257
+ long_term_memory=f'Uploaded file {params.path} to element {params.index}',
258
+ )
259
+ except Exception as e:
260
+ logger.error(f'Failed to upload file: {e}')
261
+ raise BrowserError(f'Failed to upload file: {e}')
262
+
69
263
  @self.registry.action(
70
264
  'Hover over an element',
71
265
  param_model=HoverAction,
@@ -358,224 +552,42 @@ Provide the extracted information in a clear, structured format."""
358
552
  logger.debug(f'Error extracting content: {e}')
359
553
  raise RuntimeError(str(e))
360
554
 
361
- @self.registry.action('Read file_name from file system. If this is a file not in Current workspace dir or with a absolute path, Set external_file=True.')
362
- async def read_file(file_name: str, external_file: bool, file_system: FileSystem):
363
- if not os.path.exists(file_name):
364
- # if not exists, assume it is external_file
365
- external_file = True
366
- else:
367
- external_file = False
368
- result = await file_system.read_file(file_name, external_file=external_file)
369
-
370
- MAX_MEMORY_SIZE = 1000
371
- if len(result) > MAX_MEMORY_SIZE:
372
- lines = result.splitlines()
373
- display = ''
374
- lines_count = 0
375
- for line in lines:
376
- if len(display) + len(line) < MAX_MEMORY_SIZE:
377
- display += line + '\n'
378
- lines_count += 1
379
- else:
380
- break
381
- remaining_lines = len(lines) - lines_count
382
- memory = f'{display}{remaining_lines} more lines...' if remaining_lines > 0 else display
383
- else:
384
- memory = result
385
- logger.info(f'💾 {memory}')
386
- return ActionResult(
387
- extracted_content=result,
388
- include_in_memory=True,
389
- long_term_memory=memory,
390
- include_extracted_content_only_once=True,
391
- )
392
-
393
555
  @self.registry.action(
394
- 'Extract content from a file. Support image files, pdf and more.',
395
- param_model=FileExtractionAction,
556
+ 'Take a screenshot of the current page and save it to the file system',
557
+ param_model=NoParamsAction
396
558
  )
397
- async def extract_content_from_file(
398
- params: FileExtractionAction,
399
- page_extraction_llm: BaseChatModel,
400
- file_system: FileSystem,
401
- ):
559
+ async def take_screenshot(_: NoParamsAction, browser_session: AgentBrowserSession, file_system: FileSystem):
402
560
  try:
403
- # Get file path
404
- file_path = params.file_path
561
+ # Take screenshot using browser session
562
+ screenshot = await browser_session.take_screenshot()
405
563
 
406
- # Check if file exists
407
- if not os.path.exists(file_path):
408
- file_path = os.path.join(file_system.get_dir(), file_path)
409
-
410
- # Determine if file is an image based on MIME type
411
- mime_type, _ = mimetypes.guess_type(file_path)
412
- is_image = mime_type and mime_type.startswith('image/')
413
-
414
- if is_image:
415
- # Handle image files with LLM vision
416
- try:
417
- # Read image file and encode to base64
418
- with open(file_path, 'rb') as image_file:
419
- image_data = image_file.read()
420
- image_base64 = base64.b64encode(image_data).decode('utf-8')
421
-
422
- # Create content parts similar to the user's example
423
- content_parts: list[ContentPartTextParam | ContentPartImageParam] = [
424
- ContentPartTextParam(text=f"Query: {params.query}")
425
- ]
426
-
427
- # Add the image
428
- content_parts.append(
429
- ContentPartImageParam(
430
- image_url=ImageURL(
431
- url=f'data:{mime_type};base64,{image_base64}',
432
- media_type=mime_type,
433
- detail='high',
434
- ),
435
- )
436
- )
437
-
438
- # Create user message and invoke LLM
439
- user_message = UserMessage(content=content_parts, cache=True)
440
- response = await asyncio.wait_for(
441
- page_extraction_llm.ainvoke([user_message]),
442
- timeout=120.0,
443
- )
444
-
445
- extracted_content = f'File: {file_path}\nQuery: {params.query}\nExtracted Content:\n{response.completion}'
446
-
447
- except Exception as e:
448
- raise Exception(f'Failed to process image file {file_path}: {str(e)}')
449
-
450
- else:
451
- # Handle non-image files by reading content
452
- try:
453
- file_content = await file_system.read_file(file_path, external_file=True)
454
-
455
- # Create a simple prompt for text extraction
456
- prompt = f"""Extract the requested information from this file content.
564
+ # Generate timestamp for filename
565
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
457
566
 
458
- Query: {params.query}
567
+ # Get file system directory path (Path type)
568
+ fs_dir = file_system.get_dir()
459
569
 
460
- File: {file_path}
461
- File Content:
462
- {file_content}
570
+ # Create screenshots directory if it doesn't exist
571
+ screenshots_dir = fs_dir / "screenshots"
572
+ screenshots_dir.mkdir(exist_ok=True)
463
573
 
464
- Provide the extracted information in a clear, structured format."""
574
+ # Save screenshot to file system
575
+ page_title = await browser_session.get_current_page_title()
576
+ page_title = sanitize_filename(page_title)
577
+ filename = f"{page_title}-{timestamp}.png"
578
+ filepath = screenshots_dir / filename
465
579
 
466
- response = await asyncio.wait_for(
467
- page_extraction_llm.ainvoke([UserMessage(content=prompt)]),
468
- timeout=120.0,
469
- )
470
-
471
- extracted_content = f'File: {file_path}\nQuery: {params.query}\nExtracted Content:\n{response.completion}'
472
-
473
- except Exception as e:
474
- raise Exception(f'Failed to read file {file_path}: {str(e)}')
475
-
476
- # Handle memory storage
477
- if len(extracted_content) < 1000:
478
- memory = extracted_content
479
- include_extracted_content_only_once = False
480
- else:
481
- save_result = await file_system.save_extracted_content(extracted_content)
482
- memory = (
483
- f'Extracted content from file {file_path} for query: {params.query}\nContent saved to file system: {save_result}'
484
- )
485
- include_extracted_content_only_once = True
580
+ with open(filepath, "wb") as f:
581
+ f.write(base64.b64decode(screenshot))
486
582
 
487
- logger.info(f'📄 Extracted content from file: {file_path}')
583
+ msg = f'📸 Screenshot saved to path: {str(filepath.relative_to(fs_dir))}'
584
+ logger.info(msg)
488
585
  return ActionResult(
489
- extracted_content=extracted_content,
490
- include_extracted_content_only_once=include_extracted_content_only_once,
491
- long_term_memory=memory,
492
- )
493
-
494
- except Exception as e:
495
- logger.debug(f'Error extracting content from file: {e}')
496
- raise RuntimeError(str(e))
497
-
498
- async def register_mcp_clients(self, mcp_server_config: Optional[Dict[str, Any]] = None):
499
- self.mcp_server_config = mcp_server_config or self.mcp_server_config
500
- if self.mcp_server_config:
501
- await self.unregister_mcp_clients()
502
- await self.register_mcp_tools()
503
-
504
- async def register_mcp_tools(self):
505
- """
506
- Register the MCP tools used by this controller.
507
- """
508
- if not self.mcp_server_config:
509
- return
510
-
511
- # Handle both formats: with or without "mcpServers" key
512
- mcp_servers = self.mcp_server_config.get('mcpServers', self.mcp_server_config)
513
-
514
- if not mcp_servers:
515
- return
516
-
517
- for server_name, server_config in mcp_servers.items():
518
- try:
519
- logger.info(f'Connecting to MCP server: {server_name}')
520
-
521
- # Create MCP client
522
- client = VibeSurfMCPClient(
523
- server_name=server_name,
524
- command=server_config['command'],
525
- args=server_config['args'],
526
- env=server_config.get('env', None)
586
+ extracted_content=msg,
587
+ include_in_memory=True,
588
+ long_term_memory=f'Screenshot saved to {str(filepath.relative_to(fs_dir))}',
527
589
  )
528
-
529
- # Connect to the MCP server
530
- await client.connect(timeout=200)
531
-
532
- # Register tools to controller with prefix
533
- prefix = f"mcp.{server_name}."
534
- await client.register_to_tools(
535
- tools=self,
536
- prefix=prefix
537
- )
538
-
539
- # Store client for later cleanup
540
- self.mcp_clients[server_name] = client
541
-
542
- logger.info(f'Successfully registered MCP server: {server_name} with prefix: {prefix}')
543
-
544
- except Exception as e:
545
- logger.error(f'Failed to register MCP server {server_name}: {str(e)}')
546
- # Continue with other servers even if one fails
547
-
548
- async def unregister_mcp_clients(self):
549
- """
550
- Unregister and disconnect all MCP clients.
551
- """
552
- # Disconnect all MCP clients
553
- for server_name, client in self.mcp_clients.items():
554
- try:
555
- logger.info(f'Disconnecting MCP server: {server_name}')
556
- await client.disconnect()
557
590
  except Exception as e:
558
- logger.error(f'Failed to disconnect MCP server {server_name}: {str(e)}')
559
-
560
- # Remove MCP tools from registry
561
- try:
562
- # Get all registered actions
563
- actions_to_remove = []
564
- for action_name in list(self.registry.registry.actions.keys()):
565
- if action_name.startswith('mcp.'):
566
- actions_to_remove.append(action_name)
567
-
568
- # Remove MCP actions from registry
569
- for action_name in actions_to_remove:
570
- if action_name in self.registry.registry.actions:
571
- del self.registry.registry.actions[action_name]
572
- logger.info(f'Removed MCP action: {action_name}')
573
-
574
- except Exception as e:
575
- logger.error(f'Failed to remove MCP actions from registry: {str(e)}')
576
-
577
- # Clear the clients dictionary
578
- self.mcp_clients.clear()
579
- logger.info('All MCP clients unregistered and disconnected')
580
-
581
- VibeSurfController = VibeSurfTools
591
+ error_msg = f'Failed to take screenshot: {str(e)}'
592
+ logger.error(error_msg)
593
+ return ActionResult(error=error_msg)