vibesurf 0.1.3__tar.gz → 0.1.5__tar.gz

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 (85) hide show
  1. {vibesurf-0.1.3 → vibesurf-0.1.5}/PKG-INFO +5 -1
  2. {vibesurf-0.1.3 → vibesurf-0.1.5}/README.md +4 -0
  3. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/_version.py +3 -3
  4. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/agents/report_writer_agent.py +16 -0
  5. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/agents/vibe_surf_agent.py +16 -0
  6. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/api/config.py +18 -4
  7. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/api/task.py +5 -0
  8. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/shared_state.py +4 -3
  9. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/utils/llm_factory.py +23 -6
  10. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/scripts/ui-manager.js +118 -1
  11. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/styles/components.css +40 -0
  12. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/cli.py +68 -3
  13. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibesurf.egg-info/PKG-INFO +5 -1
  14. {vibesurf-0.1.3 → vibesurf-0.1.5}/.env.example +0 -0
  15. {vibesurf-0.1.3 → vibesurf-0.1.5}/.github/workflows/publish.yml +0 -0
  16. {vibesurf-0.1.3 → vibesurf-0.1.5}/.gitignore +0 -0
  17. {vibesurf-0.1.3 → vibesurf-0.1.5}/.python-version +0 -0
  18. {vibesurf-0.1.3 → vibesurf-0.1.5}/LICENSE +0 -0
  19. {vibesurf-0.1.3 → vibesurf-0.1.5}/MANIFEST.in +0 -0
  20. {vibesurf-0.1.3 → vibesurf-0.1.5}/docs/PYPI_SETUP.md +0 -0
  21. {vibesurf-0.1.3 → vibesurf-0.1.5}/pyproject.toml +0 -0
  22. {vibesurf-0.1.3 → vibesurf-0.1.5}/setup.cfg +0 -0
  23. {vibesurf-0.1.3 → vibesurf-0.1.5}/tests/test_agents.py +0 -0
  24. {vibesurf-0.1.3 → vibesurf-0.1.5}/tests/test_backend_api.py +0 -0
  25. {vibesurf-0.1.3 → vibesurf-0.1.5}/tests/test_browser.py +0 -0
  26. {vibesurf-0.1.3 → vibesurf-0.1.5}/tests/test_controller.py +0 -0
  27. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/__init__.py +0 -0
  28. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/agents/__init__.py +0 -0
  29. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/agents/browser_use_agent.py +0 -0
  30. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/agents/prompts/__init__.py +0 -0
  31. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/agents/prompts/vibe_surf_prompt.py +0 -0
  32. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/__init__.py +0 -0
  33. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/api/__init__.py +0 -0
  34. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/api/activity.py +0 -0
  35. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/api/files.py +0 -0
  36. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/api/models.py +0 -0
  37. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/database/__init__.py +0 -0
  38. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/database/manager.py +0 -0
  39. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/database/models.py +0 -0
  40. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/database/queries.py +0 -0
  41. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/database/schemas.py +0 -0
  42. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/llm_config.py +0 -0
  43. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/main.py +0 -0
  44. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/migrations/__init__.py +0 -0
  45. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/migrations/init_db.py +0 -0
  46. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/migrations/seed_data.py +0 -0
  47. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/utils/__init__.py +0 -0
  48. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/backend/utils/encryption.py +0 -0
  49. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/__init__.py +0 -0
  50. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/agen_browser_profile.py +0 -0
  51. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/agent_browser_session.py +0 -0
  52. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/browser_manager.py +0 -0
  53. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/utils.py +0 -0
  54. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/watchdogs/__init__.py +0 -0
  55. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/watchdogs/action_watchdog.py +0 -0
  56. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/browser/watchdogs/dom_watchdog.py +0 -0
  57. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/background.js +0 -0
  58. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/config.js +0 -0
  59. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/content.js +0 -0
  60. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/dev-reload.js +0 -0
  61. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/icons/convert-svg.js +0 -0
  62. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/icons/logo-preview.html +0 -0
  63. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/icons/logo.png +0 -0
  64. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/manifest.json +0 -0
  65. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/popup.html +0 -0
  66. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/scripts/api-client.js +0 -0
  67. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/scripts/main.js +0 -0
  68. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/scripts/markdown-it.min.js +0 -0
  69. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/scripts/session-manager.js +0 -0
  70. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/sidepanel.html +0 -0
  71. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/styles/animations.css +0 -0
  72. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/styles/main.css +0 -0
  73. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/chrome_extension/styles/settings.css +0 -0
  74. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/controller/__init__.py +0 -0
  75. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/controller/file_system.py +0 -0
  76. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/controller/mcp_client.py +0 -0
  77. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/controller/vibesurf_controller.py +0 -0
  78. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/controller/views.py +0 -0
  79. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/llm/__init__.py +0 -0
  80. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibe_surf/llm/openai_compatible.py +0 -0
  81. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibesurf.egg-info/SOURCES.txt +0 -0
  82. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibesurf.egg-info/dependency_links.txt +0 -0
  83. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibesurf.egg-info/entry_points.txt +0 -0
  84. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibesurf.egg-info/requires.txt +0 -0
  85. {vibesurf-0.1.3 → vibesurf-0.1.5}/vibesurf.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibesurf
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: VibeSurf: A powerful browser assistant for vibe surfing
5
5
  Author: Shao Warm
6
6
  License: Apache-2.0
@@ -40,9 +40,13 @@ Requires-Dist: greenlet>=3.2.4
40
40
  Dynamic: license-file
41
41
 
42
42
  # VibeSurf: A powerful browser assistant for vibe surfing
43
+ [![Discord](https://img.shields.io/discord/1303749220842340412?color=7289DA&label=Discord&logo=discord&logoColor=white)](https://discord.gg/TXNnP9gJ)
44
+ [![WarmShao](https://img.shields.io/twitter/follow/warmshao?style=social)](https://x.com/warmshao)
43
45
 
44
46
  VibeSurf is an open-source AI agentic browser that revolutionizes browser automation and research.
45
47
 
48
+ If you're as excited about open-source AI browsing as I am, give it a star! ⭐
49
+
46
50
  ## ✨ Key Features
47
51
 
48
52
  - 🧠 **Advanced AI Automation**: Beyond browser automation, VibeSurf performs deep research, intelligent crawling, content summarization, and more to exploration.
@@ -1,7 +1,11 @@
1
1
  # VibeSurf: A powerful browser assistant for vibe surfing
2
+ [![Discord](https://img.shields.io/discord/1303749220842340412?color=7289DA&label=Discord&logo=discord&logoColor=white)](https://discord.gg/TXNnP9gJ)
3
+ [![WarmShao](https://img.shields.io/twitter/follow/warmshao?style=social)](https://x.com/warmshao)
2
4
 
3
5
  VibeSurf is an open-source AI agentic browser that revolutionizes browser automation and research.
4
6
 
7
+ If you're as excited about open-source AI browsing as I am, give it a star! ⭐
8
+
5
9
  ## ✨ Key Features
6
10
 
7
11
  - 🧠 **Advanced AI Automation**: Beyond browser automation, VibeSurf performs deep research, intelligent crawling, content summarization, and more to exploration.
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.3'
32
- __version_tuple__ = version_tuple = (0, 1, 3)
31
+ __version__ = version = '0.1.5'
32
+ __version_tuple__ = version_tuple = (0, 1, 5)
33
33
 
34
- __commit_id__ = commit_id = 'g337bd1e51'
34
+ __commit_id__ = commit_id = 'g9ac8f3cab'
@@ -91,6 +91,14 @@ class ReportWriterAgent:
91
91
  )
92
92
 
93
93
  response = await self.llm.ainvoke([UserMessage(content=content_prompt)])
94
+ logger.debug(f"Content generation response type: {type(response)}")
95
+ logger.debug(f"Content generation completion: {response.completion}")
96
+ logger.debug(f"Content generation completion type: {type(response.completion)}")
97
+
98
+ if response.completion is None:
99
+ logger.error("❌ Content generation returned None completion")
100
+ raise ValueError("LLM response completion is None - unable to generate report content")
101
+
94
102
  return response.completion
95
103
 
96
104
  async def _format_as_html(self, content: str) -> str:
@@ -98,6 +106,14 @@ class ReportWriterAgent:
98
106
  format_prompt = REPORT_FORMAT_PROMPT.format(report_content=content)
99
107
 
100
108
  response = await self.llm.ainvoke([UserMessage(content=format_prompt)])
109
+ logger.debug(f"Format generation response type: {type(response)}")
110
+ logger.debug(f"Format generation completion: {response.completion}")
111
+ logger.debug(f"Format generation completion type: {type(response.completion)}")
112
+
113
+ if response.completion is None:
114
+ logger.error("❌ Format generation returned None completion")
115
+ raise ValueError("LLM response completion is None - unable to format report as HTML")
116
+
101
117
  html_content = response.completion
102
118
 
103
119
  # Clean up the HTML content if needed
@@ -1565,9 +1565,25 @@ class VibeSurfAgent:
1565
1565
 
1566
1566
  except asyncio.CancelledError:
1567
1567
  logger.info("🛑 VibeSurfAgent execution was cancelled")
1568
+ # Add cancellation activity log
1569
+ if agent_activity_logs:
1570
+ activity_entry = {
1571
+ "agent_name": "VibeSurfAgent",
1572
+ "agent_status": "cancelled",
1573
+ "agent_msg": "Task execution was cancelled by user request."
1574
+ }
1575
+ agent_activity_logs.append(activity_entry)
1568
1576
  return f"# Task Execution Cancelled\n\n**Task:** {task}\n\nExecution was stopped by user request."
1569
1577
  except Exception as e:
1570
1578
  logger.error(f"❌ VibeSurfAgent execution failed: {e}")
1579
+ # Add error activity log
1580
+ if agent_activity_logs:
1581
+ activity_entry = {
1582
+ "agent_name": "VibeSurfAgent",
1583
+ "agent_status": "error",
1584
+ "agent_msg": f"Task execution failed: {str(e)}"
1585
+ }
1586
+ agent_activity_logs.append(activity_entry)
1571
1587
  return f"# Task Execution Failed\n\n**Task:** {task}\n\n**Error:** {str(e)}\n\nPlease try again or contact support."
1572
1588
  finally:
1573
1589
  # Reset state
@@ -115,10 +115,24 @@ async def create_llm_profile(
115
115
 
116
116
  except Exception as e:
117
117
  logger.error(f"Failed to create LLM profile: {e}")
118
- raise HTTPException(
119
- status_code=500,
120
- detail=f"Failed to create LLM profile: {str(e)}"
121
- )
118
+
119
+ # Handle specific database constraint errors
120
+ error_msg = str(e)
121
+ if "UNIQUE constraint failed: llm_profiles.profile_name" in error_msg:
122
+ raise HTTPException(
123
+ status_code=400,
124
+ detail=f"Profile with name '{profile_request.profile_name}' already exists. Please choose a different name."
125
+ )
126
+ elif "IntegrityError" in error_msg and "profile_name" in error_msg:
127
+ raise HTTPException(
128
+ status_code=400,
129
+ detail=f"Profile name '{profile_request.profile_name}' is already in use. Please choose a different name."
130
+ )
131
+ else:
132
+ raise HTTPException(
133
+ status_code=500,
134
+ detail=f"Failed to create LLM profile: {str(e)}"
135
+ )
122
136
 
123
137
  @router.get("/llm-profiles", response_model=List[LLMProfileResponse])
124
138
  async def list_llm_profiles(
@@ -80,6 +80,11 @@ async def submit_task(
80
80
  if not mcp_server_config and controller and hasattr(controller, 'mcp_server_config'):
81
81
  mcp_server_config = controller.mcp_server_config
82
82
 
83
+ # Ensure we have a valid MCP server config (never None)
84
+ if mcp_server_config is None:
85
+ mcp_server_config = {"mcpServers": {}}
86
+ logger.info("Using default empty MCP server configuration")
87
+
83
88
  # DEBUG: Log the type and content of mcp_server_config
84
89
  logger.info(f"mcp_server_config type: {type(mcp_server_config)}, value: {mcp_server_config}")
85
90
 
@@ -277,7 +277,8 @@ async def _load_active_mcp_servers():
277
277
 
278
278
  try:
279
279
  if not db_manager:
280
- return {}
280
+ logger.info("Database manager not available, returning empty MCP config")
281
+ return {"mcpServers": {}}
281
282
 
282
283
  from .database.queries import McpProfileQueries
283
284
 
@@ -298,13 +299,13 @@ async def _load_active_mcp_servers():
298
299
 
299
300
  except Exception as e:
300
301
  logger.warning(f"Failed to load MCP servers from database: {e}")
301
- return {}
302
+ return {"mcpServers": {}}
302
303
  finally:
303
304
  break
304
305
 
305
306
  except Exception as e:
306
307
  logger.warning(f"Database not available for MCP server loading: {e}")
307
- return {}
308
+ return {"mcpServers": {}}
308
309
 
309
310
 
310
311
  async def initialize_vibesurf_components():
@@ -41,17 +41,34 @@ def create_llm_from_profile(llm_profile):
41
41
  if not is_provider_supported(provider):
42
42
  raise ValueError(f"Unsupported provider: {provider}. Supported: {get_supported_providers()}")
43
43
 
44
- # Common parameters
44
+ # Define provider-specific parameter support
45
+ provider_param_support = {
46
+ "openai": ["temperature", "top_p", "frequency_penalty", "seed"],
47
+ "anthropic": ["temperature", "top_p"],
48
+ "google": ["temperature", "top_p"],
49
+ "azure_openai": ["temperature", "top_p", "frequency_penalty", "seed"],
50
+ "groq": ["temperature", "top_p"],
51
+ "ollama": ["temperature"],
52
+ "openrouter": ["temperature", "top_p"], # OpenRouter doesn't support max_tokens
53
+ "deepseek": ["temperature", "top_p"],
54
+ "aws_bedrock": ["temperature"],
55
+ "anthropic_bedrock": ["temperature"],
56
+ "openai_compatible": ["temperature", "top_p", "frequency_penalty", "seed"]
57
+ }
58
+
59
+ # Build common parameters based on provider support
60
+ supported_params = provider_param_support.get(provider, ["temperature", "top_p", "frequency_penalty", "seed"])
45
61
  common_params = {}
46
- if temperature is not None:
62
+
63
+ if temperature is not None and "temperature" in supported_params:
47
64
  common_params["temperature"] = temperature
48
- if max_tokens is not None:
65
+ if max_tokens is not None and "max_tokens" in supported_params:
49
66
  common_params["max_tokens"] = max_tokens
50
- if top_p is not None:
67
+ if top_p is not None and "top_p" in supported_params:
51
68
  common_params["top_p"] = top_p
52
- if frequency_penalty is not None:
69
+ if frequency_penalty is not None and "frequency_penalty" in supported_params:
53
70
  common_params["frequency_penalty"] = frequency_penalty
54
- if seed is not None:
71
+ if seed is not None and "seed" in supported_params:
55
72
  common_params["seed"] = seed
56
73
 
57
74
  # Add provider-specific config if available
@@ -1609,6 +1609,13 @@ class VibeSurfUIManager {
1609
1609
  console.log('[UIManager] Profile form submit triggered');
1610
1610
 
1611
1611
  const form = event.target;
1612
+
1613
+ // Prevent multiple submissions
1614
+ if (form.dataset.submitting === 'true') {
1615
+ console.log('[UIManager] Form already submitting, ignoring duplicate submission');
1616
+ return;
1617
+ }
1618
+
1612
1619
  const formData = new FormData(form);
1613
1620
  const type = form.dataset.type;
1614
1621
  const mode = form.dataset.mode;
@@ -1616,6 +1623,10 @@ class VibeSurfUIManager {
1616
1623
 
1617
1624
  console.log('[UIManager] Form submission details:', { type, mode, profileId });
1618
1625
 
1626
+ // Set submitting state and disable form
1627
+ form.dataset.submitting = 'true';
1628
+ this.setProfileFormSubmitting(true);
1629
+
1619
1630
  // Convert FormData to object
1620
1631
  const data = {};
1621
1632
 
@@ -1746,7 +1757,113 @@ class VibeSurfUIManager {
1746
1757
 
1747
1758
  } catch (error) {
1748
1759
  console.error(`[UIManager] Failed to ${mode} ${type} profile:`, error);
1749
- this.showNotification(`Failed to ${mode} ${type} profile: ${error.message}`, 'error');
1760
+
1761
+ // Handle specific error types for better user experience
1762
+ let errorMessage = error.message || 'Unknown error occurred';
1763
+
1764
+ if (errorMessage.includes('already exists') || errorMessage.includes('already in use')) {
1765
+ // For duplicate profile name errors, highlight the name field
1766
+ this.highlightProfileNameError(errorMessage);
1767
+ errorMessage = errorMessage; // Use the specific error message from backend
1768
+ } else if (errorMessage.includes('UNIQUE constraint')) {
1769
+ errorMessage = `Profile name '${data.profile_name || data.display_name}' already exists. Please choose a different name.`;
1770
+ this.highlightProfileNameError(errorMessage);
1771
+ }
1772
+
1773
+ this.showNotification(`Failed to ${mode} ${type} profile: ${errorMessage}`, 'error');
1774
+ } finally {
1775
+ // Reset form state
1776
+ form.dataset.submitting = 'false';
1777
+ this.setProfileFormSubmitting(false);
1778
+ }
1779
+ }
1780
+
1781
+ setProfileFormSubmitting(isSubmitting) {
1782
+ const form = this.elements.profileForm;
1783
+ const submitButton = this.elements.profileFormSubmit;
1784
+ const cancelButton = this.elements.profileFormCancel;
1785
+
1786
+ if (!form) return;
1787
+
1788
+ // Disable/enable form inputs
1789
+ const inputs = form.querySelectorAll('input, select, textarea');
1790
+ inputs.forEach(input => {
1791
+ input.disabled = isSubmitting;
1792
+ });
1793
+
1794
+ // Update submit button
1795
+ if (submitButton) {
1796
+ submitButton.disabled = isSubmitting;
1797
+ submitButton.textContent = isSubmitting ? 'Saving...' : 'Save Profile';
1798
+ }
1799
+
1800
+ // Update cancel button
1801
+ if (cancelButton) {
1802
+ cancelButton.disabled = isSubmitting;
1803
+ }
1804
+
1805
+ console.log(`[UIManager] Profile form submitting state: ${isSubmitting}`);
1806
+ }
1807
+
1808
+ highlightProfileNameError(errorMessage) {
1809
+ const nameInput = this.elements.profileForm?.querySelector('input[name="profile_name"], input[name="display_name"]');
1810
+
1811
+ if (nameInput) {
1812
+ // Add error styling
1813
+ nameInput.classList.add('form-error');
1814
+ nameInput.focus();
1815
+
1816
+ // Create or update error message
1817
+ let errorElement = nameInput.parentElement.querySelector('.profile-name-error');
1818
+ if (!errorElement) {
1819
+ errorElement = document.createElement('div');
1820
+ errorElement.className = 'form-error-message profile-name-error';
1821
+ nameInput.parentElement.appendChild(errorElement);
1822
+ }
1823
+
1824
+ errorElement.textContent = errorMessage;
1825
+
1826
+ // Remove error styling after user starts typing
1827
+ const removeError = () => {
1828
+ nameInput.classList.remove('form-error');
1829
+ if (errorElement) {
1830
+ errorElement.remove();
1831
+ }
1832
+ nameInput.removeEventListener('input', removeError);
1833
+ };
1834
+
1835
+ nameInput.addEventListener('input', removeError);
1836
+
1837
+ console.log('[UIManager] Highlighted profile name error:', errorMessage);
1838
+ }
1839
+ }
1840
+
1841
+ // Real-time profile name validation
1842
+ async validateProfileNameAvailability(profileName, profileType) {
1843
+ if (!profileName || profileName.trim().length < 2) {
1844
+ return { isValid: true, message: '' }; // Don't validate very short names
1845
+ }
1846
+
1847
+ try {
1848
+ // Check if profile already exists
1849
+ const profiles = profileType === 'llm' ? this.state.llmProfiles : this.state.mcpProfiles;
1850
+ const nameField = profileType === 'llm' ? 'profile_name' : 'display_name';
1851
+
1852
+ const existingProfile = profiles.find(profile =>
1853
+ profile[nameField].toLowerCase() === profileName.toLowerCase()
1854
+ );
1855
+
1856
+ if (existingProfile) {
1857
+ return {
1858
+ isValid: false,
1859
+ message: `${profileType.toUpperCase()} profile "${profileName}" already exists. Please choose a different name.`
1860
+ };
1861
+ }
1862
+
1863
+ return { isValid: true, message: '' };
1864
+ } catch (error) {
1865
+ console.error('[UIManager] Error validating profile name:', error);
1866
+ return { isValid: true, message: '' }; // Don't block on validation errors
1750
1867
  }
1751
1868
  }
1752
1869
 
@@ -366,6 +366,46 @@
366
366
  padding-right: 32px;
367
367
  }
368
368
 
369
+ /* Form input error state */
370
+ .form-input.form-error {
371
+ border-color: var(--danger-color) !important;
372
+ box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1) !important;
373
+ }
374
+
375
+ .form-input.form-error:focus {
376
+ border-color: var(--danger-color) !important;
377
+ box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.2) !important;
378
+ }
379
+
380
+ /* Form error message */
381
+ .form-error-message {
382
+ font-size: var(--font-size-xs);
383
+ color: var(--danger-color);
384
+ margin-top: var(--spacing-xs);
385
+ display: block;
386
+ font-weight: var(--font-weight-medium);
387
+ animation: slideDown 0.2s ease-out;
388
+ }
389
+
390
+ .profile-name-error {
391
+ background-color: rgba(220, 53, 69, 0.05);
392
+ padding: var(--spacing-xs) var(--spacing-sm);
393
+ border-radius: var(--radius-sm);
394
+ border: 1px solid rgba(220, 53, 69, 0.2);
395
+ }
396
+
397
+ @keyframes slideDown {
398
+ from {
399
+ opacity: 0;
400
+ transform: translateY(-10px);
401
+ }
402
+ to {
403
+ opacity: 1;
404
+ transform: translateY(0);
405
+ }
406
+ }
407
+
408
+ /* Legacy form error class for backwards compatibility */
369
409
  .form-error {
370
410
  font-size: var(--font-size-xs);
371
411
  color: var(--danger-color);
@@ -7,6 +7,7 @@ A command-line interface for VibeSurf browser automation tool.
7
7
  import os
8
8
  import sys
9
9
  import glob
10
+ import json
10
11
  import socket
11
12
  import platform
12
13
  import importlib.util
@@ -35,6 +36,17 @@ VIBESURF_LOGO = """
35
36
 
36
37
  console = Console()
37
38
 
39
+ # Add logger import for the workspace directory logging
40
+ try:
41
+ import logging
42
+ logger = logging.getLogger(__name__)
43
+ logging.basicConfig(level=logging.INFO)
44
+ except ImportError:
45
+ class SimpleLogger:
46
+ def info(self, msg):
47
+ console.print(f"[dim]{msg}[/dim]")
48
+ logger = SimpleLogger()
49
+
38
50
 
39
51
  def find_chrome_browser() -> Optional[str]:
40
52
  """Find Chrome browser executable."""
@@ -322,6 +334,55 @@ def start_backend(port: int) -> None:
322
334
  sys.exit(1)
323
335
 
324
336
 
337
+ def get_browser_execution_path() -> Optional[str]:
338
+ """Get browser execution path from envs.json or environment variables."""
339
+ # 1. Load environment variables
340
+ env_workspace_dir = os.getenv("VIBESURF_WORKSPACE", "")
341
+ if not env_workspace_dir or not env_workspace_dir.strip():
342
+ # Set default workspace directory based on OS
343
+ if platform.system() == "Windows":
344
+ default_workspace = os.path.join(os.environ.get("APPDATA", ""), "VibeSurf")
345
+ elif platform.system() == "Darwin": # macOS
346
+ default_workspace = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "VibeSurf")
347
+ else: # Linux and others
348
+ default_workspace = os.path.join(os.path.expanduser("~"), ".vibesurf")
349
+ workspace_dir = default_workspace
350
+ else:
351
+ workspace_dir = env_workspace_dir
352
+ workspace_dir = os.path.abspath(workspace_dir)
353
+ os.makedirs(workspace_dir, exist_ok=True)
354
+ logger.info("WorkSpace directory: {}".format(workspace_dir))
355
+
356
+ # Load environment configuration from envs.json
357
+ envs_file_path = os.path.join(workspace_dir, "envs.json")
358
+ browser_path_from_envs = None
359
+ try:
360
+ if os.path.exists(envs_file_path):
361
+ with open(envs_file_path, 'r', encoding='utf-8') as f:
362
+ envs = json.load(f)
363
+ browser_path_from_envs = envs.get("BROWSER_EXECUTION_PATH", "")
364
+ if browser_path_from_envs:
365
+ browser_path_from_envs = browser_path_from_envs.strip()
366
+ except (json.JSONDecodeError, IOError) as e:
367
+ logger.info(f"Failed to load envs.json: {e}")
368
+ browser_path_from_envs = None
369
+
370
+ # 2. Get BROWSER_EXECUTION_PATH from environment variables
371
+ browser_path_from_env = os.getenv("BROWSER_EXECUTION_PATH", "")
372
+ if browser_path_from_env:
373
+ browser_path_from_env = browser_path_from_env.strip()
374
+
375
+ # Check paths in priority order: 1. envs.json -> 2. environment variables
376
+ for source, path in [("envs.json", browser_path_from_envs), ("environment variable", browser_path_from_env)]:
377
+ if path and os.path.exists(path) and os.path.isfile(path):
378
+ console.print(f"[green]✅ Using browser path from {source}: {path}[/green]")
379
+ return path
380
+ elif path:
381
+ console.print(f"[yellow]⚠️ Browser path from {source} exists but file not found: {path}[/yellow]")
382
+
383
+ return None
384
+
385
+
325
386
  def main():
326
387
  """Main CLI entry point."""
327
388
  try:
@@ -329,10 +390,14 @@ def main():
329
390
  console.print(Panel(VIBESURF_LOGO, title="[bold cyan]VibeSurf CLI[/bold cyan]", border_style="cyan"))
330
391
  console.print("[dim]A powerful browser automation tool for vibe surfing 🏄‍♂️[/dim]\n")
331
392
 
332
- # Browser selection
333
- browser_path = select_browser()
393
+ # Check for existing browser path from configuration
394
+ browser_path = get_browser_execution_path()
395
+
396
+ # If no valid browser path found, ask user to select
334
397
  if not browser_path:
335
- return
398
+ browser_path = select_browser()
399
+ if not browser_path:
400
+ return
336
401
 
337
402
  # Port configuration
338
403
  port = configure_port()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibesurf
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: VibeSurf: A powerful browser assistant for vibe surfing
5
5
  Author: Shao Warm
6
6
  License: Apache-2.0
@@ -40,9 +40,13 @@ Requires-Dist: greenlet>=3.2.4
40
40
  Dynamic: license-file
41
41
 
42
42
  # VibeSurf: A powerful browser assistant for vibe surfing
43
+ [![Discord](https://img.shields.io/discord/1303749220842340412?color=7289DA&label=Discord&logo=discord&logoColor=white)](https://discord.gg/TXNnP9gJ)
44
+ [![WarmShao](https://img.shields.io/twitter/follow/warmshao?style=social)](https://x.com/warmshao)
43
45
 
44
46
  VibeSurf is an open-source AI agentic browser that revolutionizes browser automation and research.
45
47
 
48
+ If you're as excited about open-source AI browsing as I am, give it a star! ⭐
49
+
46
50
  ## ✨ Key Features
47
51
 
48
52
  - 🧠 **Advanced AI Automation**: Beyond browser automation, VibeSurf performs deep research, intelligent crawling, content summarization, and more to exploration.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes