vibesurf 0.1.20__py3-none-any.whl → 0.1.22__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 (43) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +1 -1
  3. vibe_surf/agents/prompts/vibe_surf_prompt.py +1 -0
  4. vibe_surf/backend/api/task.py +1 -1
  5. vibe_surf/backend/api/voices.py +481 -0
  6. vibe_surf/backend/database/migrations/v004_add_voice_profiles.sql +35 -0
  7. vibe_surf/backend/database/models.py +38 -1
  8. vibe_surf/backend/database/queries.py +189 -1
  9. vibe_surf/backend/main.py +2 -0
  10. vibe_surf/backend/shared_state.py +1 -1
  11. vibe_surf/backend/voice_model_config.py +25 -0
  12. vibe_surf/browser/agen_browser_profile.py +2 -0
  13. vibe_surf/browser/agent_browser_session.py +5 -4
  14. vibe_surf/chrome_extension/background.js +271 -25
  15. vibe_surf/chrome_extension/content.js +147 -0
  16. vibe_surf/chrome_extension/permission-iframe.html +38 -0
  17. vibe_surf/chrome_extension/permission-request.html +104 -0
  18. vibe_surf/chrome_extension/scripts/api-client.js +61 -0
  19. vibe_surf/chrome_extension/scripts/file-manager.js +53 -12
  20. vibe_surf/chrome_extension/scripts/main.js +53 -12
  21. vibe_surf/chrome_extension/scripts/permission-iframe-request.js +188 -0
  22. vibe_surf/chrome_extension/scripts/permission-request.js +118 -0
  23. vibe_surf/chrome_extension/scripts/session-manager.js +30 -4
  24. vibe_surf/chrome_extension/scripts/settings-manager.js +690 -3
  25. vibe_surf/chrome_extension/scripts/ui-manager.js +961 -147
  26. vibe_surf/chrome_extension/scripts/user-settings-storage.js +422 -0
  27. vibe_surf/chrome_extension/scripts/voice-recorder.js +514 -0
  28. vibe_surf/chrome_extension/sidepanel.html +106 -29
  29. vibe_surf/chrome_extension/styles/components.css +35 -0
  30. vibe_surf/chrome_extension/styles/input.css +164 -1
  31. vibe_surf/chrome_extension/styles/layout.css +1 -1
  32. vibe_surf/chrome_extension/styles/settings-environment.css +138 -0
  33. vibe_surf/chrome_extension/styles/settings-forms.css +7 -7
  34. vibe_surf/chrome_extension/styles/variables.css +51 -0
  35. vibe_surf/tools/voice_asr.py +79 -8
  36. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/METADATA +9 -13
  37. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/RECORD +41 -34
  38. vibe_surf/chrome_extension/icons/convert-svg.js +0 -33
  39. vibe_surf/chrome_extension/icons/logo-preview.html +0 -187
  40. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/WHEEL +0 -0
  41. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/entry_points.txt +0 -0
  42. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/licenses/LICENSE +0 -0
  43. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ from typing import List, Optional, Dict, Any
8
8
  from sqlalchemy.ext.asyncio import AsyncSession
9
9
  from sqlalchemy import select, update, delete, func, desc, and_, or_
10
10
  from sqlalchemy.orm import selectinload
11
- from .models import Task, TaskStatus, LLMProfile, UploadedFile, McpProfile
11
+ from .models import Task, TaskStatus, LLMProfile, UploadedFile, McpProfile, VoiceProfile, VoiceModelType
12
12
  from ..utils.encryption import encrypt_api_key, decrypt_api_key
13
13
  import logging
14
14
  import json
@@ -929,3 +929,191 @@ class UploadedFileQueries:
929
929
  except Exception as e:
930
930
  logger.error(f"Failed to cleanup deleted files: {e}")
931
931
  raise
932
+
933
+
934
+ class VoiceProfileQueries:
935
+ """Query operations for VoiceProfile model"""
936
+
937
+ @staticmethod
938
+ async def create_profile(
939
+ db: AsyncSession,
940
+ voice_profile_name: str,
941
+ voice_model_type: str,
942
+ voice_model_name: str,
943
+ api_key: Optional[str] = None,
944
+ voice_meta_params: Optional[Dict[str, Any]] = None,
945
+ description: Optional[str] = None
946
+ ) -> Dict[str, Any]:
947
+ """Create a new Voice profile with encrypted API key"""
948
+ try:
949
+ # Encrypt API key if provided
950
+ encrypted_api_key = encrypt_api_key(api_key) if api_key else None
951
+
952
+ profile = VoiceProfile(
953
+ voice_profile_name=voice_profile_name,
954
+ voice_model_type=VoiceModelType(voice_model_type),
955
+ voice_model_name=voice_model_name,
956
+ encrypted_api_key=encrypted_api_key,
957
+ voice_meta_params=voice_meta_params or {},
958
+ description=description
959
+ )
960
+
961
+ db.add(profile)
962
+ await db.flush()
963
+ await db.refresh(profile)
964
+
965
+ # Extract data immediately to avoid greenlet issues
966
+ profile_data = {
967
+ "profile_id": profile.profile_id,
968
+ "voice_profile_name": profile.voice_profile_name,
969
+ "voice_model_type": profile.voice_model_type.value,
970
+ "voice_model_name": profile.voice_model_name,
971
+ "voice_meta_params": profile.voice_meta_params,
972
+ "description": profile.description,
973
+ "is_active": profile.is_active,
974
+ "created_at": profile.created_at,
975
+ "updated_at": profile.updated_at,
976
+ "last_used_at": profile.last_used_at
977
+ }
978
+
979
+ return profile_data
980
+ except Exception as e:
981
+ logger.error(f"Failed to create Voice profile {voice_profile_name}: {e}")
982
+ raise
983
+
984
+ @staticmethod
985
+ async def get_profile(db: AsyncSession, voice_profile_name: str) -> Optional[VoiceProfile]:
986
+ """Get Voice profile by name"""
987
+ try:
988
+ result = await db.execute(
989
+ select(VoiceProfile).where(VoiceProfile.voice_profile_name == voice_profile_name)
990
+ )
991
+ profile = result.scalar_one_or_none()
992
+ if profile:
993
+ # Ensure all attributes are loaded by accessing them
994
+ _ = (profile.profile_id, profile.created_at, profile.updated_at,
995
+ profile.last_used_at, profile.is_active)
996
+ return profile
997
+ except Exception as e:
998
+ logger.error(f"Failed to get Voice profile {voice_profile_name}: {e}")
999
+ raise
1000
+
1001
+ @staticmethod
1002
+ async def get_profile_with_decrypted_key(db: AsyncSession, voice_profile_name: str) -> Optional[Dict[str, Any]]:
1003
+ """Get Voice profile with decrypted API key"""
1004
+ try:
1005
+ profile = await VoiceProfileQueries.get_profile(db, voice_profile_name)
1006
+ if not profile:
1007
+ return None
1008
+
1009
+ # Decrypt API key
1010
+ decrypted_api_key = decrypt_api_key(profile.encrypted_api_key) if profile.encrypted_api_key else None
1011
+
1012
+ return {
1013
+ "profile_id": profile.profile_id,
1014
+ "voice_profile_name": profile.voice_profile_name,
1015
+ "voice_model_type": profile.voice_model_type.value,
1016
+ "voice_model_name": profile.voice_model_name,
1017
+ "api_key": decrypted_api_key, # Decrypted for use
1018
+ "voice_meta_params": profile.voice_meta_params,
1019
+ "description": profile.description,
1020
+ "is_active": profile.is_active,
1021
+ "created_at": profile.created_at,
1022
+ "updated_at": profile.updated_at,
1023
+ "last_used_at": profile.last_used_at
1024
+ }
1025
+ except Exception as e:
1026
+ logger.error(f"Failed to get Voice profile with decrypted key {voice_profile_name}: {e}")
1027
+ raise
1028
+
1029
+ @staticmethod
1030
+ async def list_profiles(
1031
+ db: AsyncSession,
1032
+ voice_model_type: Optional[str] = None,
1033
+ active_only: bool = True,
1034
+ limit: int = 50,
1035
+ offset: int = 0
1036
+ ) -> List[VoiceProfile]:
1037
+ """List Voice profiles"""
1038
+ try:
1039
+ query = select(VoiceProfile)
1040
+
1041
+ if active_only:
1042
+ query = query.where(VoiceProfile.is_active == True)
1043
+
1044
+ if voice_model_type:
1045
+ query = query.where(VoiceProfile.voice_model_type == VoiceModelType(voice_model_type))
1046
+
1047
+ query = query.order_by(desc(VoiceProfile.last_used_at), desc(VoiceProfile.created_at))
1048
+ query = query.limit(limit).offset(offset)
1049
+
1050
+ result = await db.execute(query)
1051
+ profiles = result.scalars().all()
1052
+
1053
+ # Ensure all attributes are loaded for each profile
1054
+ for profile in profiles:
1055
+ _ = (profile.profile_id, profile.created_at, profile.updated_at,
1056
+ profile.last_used_at, profile.is_active)
1057
+
1058
+ return profiles
1059
+ except Exception as e:
1060
+ logger.error(f"Failed to list Voice profiles: {e}")
1061
+ raise
1062
+
1063
+ @staticmethod
1064
+ async def update_profile(
1065
+ db: AsyncSession,
1066
+ voice_profile_name: str,
1067
+ updates: Dict[str, Any]
1068
+ ) -> bool:
1069
+ """Update Voice profile"""
1070
+ try:
1071
+ # Handle API key encryption if present
1072
+ if "api_key" in updates:
1073
+ api_key = updates.pop("api_key")
1074
+ if api_key:
1075
+ updates["encrypted_api_key"] = encrypt_api_key(api_key)
1076
+ else:
1077
+ updates["encrypted_api_key"] = None
1078
+
1079
+ # Handle voice_model_type enum conversion
1080
+ if "voice_model_type" in updates:
1081
+ updates["voice_model_type"] = VoiceModelType(updates["voice_model_type"])
1082
+
1083
+ result = await db.execute(
1084
+ update(VoiceProfile)
1085
+ .where(VoiceProfile.voice_profile_name == voice_profile_name)
1086
+ .values(**updates)
1087
+ )
1088
+
1089
+ return result.rowcount > 0
1090
+ except Exception as e:
1091
+ logger.error(f"Failed to update Voice profile {voice_profile_name}: {e}")
1092
+ raise
1093
+
1094
+ @staticmethod
1095
+ async def delete_profile(db: AsyncSession, voice_profile_name: str) -> bool:
1096
+ """Delete Voice profile"""
1097
+ try:
1098
+ result = await db.execute(
1099
+ delete(VoiceProfile).where(VoiceProfile.voice_profile_name == voice_profile_name)
1100
+ )
1101
+ return result.rowcount > 0
1102
+ except Exception as e:
1103
+ logger.error(f"Failed to delete Voice profile {voice_profile_name}: {e}")
1104
+ raise
1105
+
1106
+
1107
+ @staticmethod
1108
+ async def update_last_used(db: AsyncSession, voice_profile_name: str) -> bool:
1109
+ """Update the last_used_at timestamp for a profile"""
1110
+ try:
1111
+ result = await db.execute(
1112
+ update(VoiceProfile)
1113
+ .where(VoiceProfile.voice_profile_name == voice_profile_name)
1114
+ .values(last_used_at=func.now())
1115
+ )
1116
+ return result.rowcount > 0
1117
+ except Exception as e:
1118
+ logger.error(f"Failed to update last_used for Voice profile {voice_profile_name}: {e}")
1119
+ raise
vibe_surf/backend/main.py CHANGED
@@ -20,6 +20,7 @@ from .api.files import router as files_router
20
20
  from .api.activity import router as activity_router
21
21
  from .api.config import router as config_router
22
22
  from .api.browser import router as browser_router
23
+ from .api.voices import router as voices_router
23
24
 
24
25
  # Import shared state
25
26
  from . import shared_state
@@ -51,6 +52,7 @@ app.include_router(files_router, prefix="/api", tags=["files"])
51
52
  app.include_router(activity_router, prefix="/api", tags=["activity"])
52
53
  app.include_router(config_router, prefix="/api", tags=["config"])
53
54
  app.include_router(browser_router, prefix="/api", tags=["browser"])
55
+ app.include_router(voices_router, prefix="/api", tags=["voices"])
54
56
 
55
57
  # Global variable to control browser monitoring task
56
58
  browser_monitor_task = None
@@ -174,7 +174,7 @@ async def execute_task_background(
174
174
  db_session,
175
175
  task_id=task_id,
176
176
  task_result=result,
177
- task_status=active_task.get("status", "completed"),
177
+ task_status=active_task.get("status", "completed") if active_task else "completed",
178
178
  report_path=report_path
179
179
  )
180
180
  await db_session.commit()
@@ -0,0 +1,25 @@
1
+ """
2
+ Voice Model Configuration
3
+
4
+ Centralized configuration for all supported voice providers and their models.
5
+ """
6
+
7
+ # Voice Providers and their supported models
8
+ VOICE_MODELS = {
9
+ "qwen-asr": {
10
+ "model_type": "asr",
11
+ "requires_api_key": True,
12
+ "provider": "qwen",
13
+ },
14
+ "openai-asr": {
15
+ "model_type": "asr",
16
+ "requires_api_key": True,
17
+ "provider": "openai",
18
+ "supports_base_url": True,
19
+ },
20
+ "gemini-asr": {
21
+ "model_type": "asr",
22
+ "requires_api_key": True,
23
+ "provider": "gemini",
24
+ }
25
+ }
@@ -17,6 +17,8 @@ from browser_use.observability import observe_debug
17
17
  from browser_use.utils import _log_pretty_path, logger
18
18
 
19
19
  from browser_use.browser import BrowserProfile
20
+ from browser_use.browser.profile import CHROME_DEFAULT_ARGS, CHROME_DOCKER_ARGS, CHROME_HEADLESS_ARGS, \
21
+ CHROME_DETERMINISTIC_RENDERING_ARGS, CHROME_DISABLE_SECURITY_ARGS, BrowserLaunchArgs
20
22
 
21
23
 
22
24
  class AgentBrowserProfile(BrowserProfile):
@@ -43,6 +43,7 @@ from browser_use.browser.events import (
43
43
  TabClosedEvent,
44
44
  TabCreatedEvent,
45
45
  )
46
+ from browser_use.browser.profile import BrowserProfile, ProxySettings
46
47
 
47
48
  DEFAULT_BROWSER_PROFILE = AgentBrowserProfile()
48
49
 
@@ -94,7 +95,7 @@ class AgentBrowserSession(BrowserSession):
94
95
  deterministic_rendering: bool | None = None,
95
96
  allowed_domains: list[str] | None = None,
96
97
  keep_alive: bool | None = None,
97
- proxy: any | None = None,
98
+ proxy: ProxySettings | None = None,
98
99
  enable_default_extensions: bool | None = None,
99
100
  window_size: dict | None = None,
100
101
  window_position: dict | None = None,
@@ -428,9 +429,9 @@ class AgentBrowserSession(BrowserSession):
428
429
  self._popups_watchdog.attach_to_session()
429
430
 
430
431
  # Initialize PermissionsWatchdog (handles granting and revoking browser permissions like clipboard, microphone, camera, etc.)
431
- PermissionsWatchdog.model_rebuild()
432
- self._permissions_watchdog = PermissionsWatchdog(event_bus=self.event_bus, browser_session=self)
433
- self._permissions_watchdog.attach_to_session()
432
+ # PermissionsWatchdog.model_rebuild()
433
+ # self._permissions_watchdog = PermissionsWatchdog(event_bus=self.event_bus, browser_session=self)
434
+ # self._permissions_watchdog.attach_to_session()
434
435
 
435
436
  # Initialize DefaultActionWatchdog (handles all default actions like click, type, scroll, go back, go forward, refresh, wait, send keys, upload file, scroll to text, etc.)
436
437
  CustomActionWatchdog.model_rebuild()
@@ -135,12 +135,17 @@ class VibeSurfBackground {
135
135
  console.error('[VibeSurf] Failed to open side panel:', error);
136
136
 
137
137
  // Show notification with helpful message
138
- await chrome.notifications.create({
139
- type: 'basic',
140
- iconUrl: chrome.runtime.getURL('icons/icon48.png') || '',
141
- title: 'VibeSurf',
142
- message: 'Side panel failed. Please update Chrome to the latest version or try right-clicking the extension icon.'
143
- });
138
+ try {
139
+ await chrome.notifications.create({
140
+ type: 'basic',
141
+ iconUrl: '', // Use empty string to avoid icon issues
142
+ title: 'VibeSurf',
143
+ message: 'Side panel failed. Please update Chrome to the latest version or try right-clicking the extension icon.'
144
+ });
145
+ } catch (notifError) {
146
+ console.warn('[VibeSurf] Notification failed:', notifError);
147
+ // Don't throw, just log the warning
148
+ }
144
149
 
145
150
  // Fallback: try to open in new tab
146
151
  try {
@@ -155,12 +160,14 @@ class VibeSurfBackground {
155
160
  }
156
161
 
157
162
  handleMessage(message, sender, sendResponse) {
163
+ console.log('[VibeSurf] Received message:', message.type);
158
164
 
159
165
  // Handle async messages properly
160
166
  (async () => {
161
167
  try {
162
168
  let result;
163
169
 
170
+ console.log('[VibeSurf] Processing message type:', message.type);
164
171
  switch (message.type) {
165
172
  case 'GET_CURRENT_TAB':
166
173
  result = await this.getCurrentTabInfo();
@@ -206,9 +213,56 @@ class VibeSurfBackground {
206
213
  result = await this.getAllTabs();
207
214
  break;
208
215
 
216
+ case 'REQUEST_MICROPHONE_PERMISSION':
217
+ result = await this.requestMicrophonePermission();
218
+ break;
219
+
220
+ case 'REQUEST_MICROPHONE_PERMISSION_WITH_UI':
221
+ console.log('[VibeSurf] Handling REQUEST_MICROPHONE_PERMISSION_WITH_UI');
222
+ result = await this.requestMicrophonePermissionWithUI();
223
+ break;
224
+
225
+ case 'MICROPHONE_PERMISSION_RESULT':
226
+ console.log('[VibeSurf] Received MICROPHONE_PERMISSION_RESULT:', message);
227
+ console.log('[VibeSurf] Permission granted:', message.granted);
228
+ console.log('[VibeSurf] Permission error:', message.error);
229
+
230
+ // Handle permission result from URL parameter approach
231
+ if (message.granted !== undefined) {
232
+ console.log('[VibeSurf] Processing permission result with granted:', message.granted);
233
+
234
+ // Store the result for the original tab to retrieve
235
+ chrome.storage.local.set({
236
+ microphonePermissionResult: {
237
+ granted: message.granted,
238
+ error: message.error,
239
+ timestamp: Date.now()
240
+ }
241
+ });
242
+
243
+ // Also send to any waiting listeners
244
+ console.log('[VibeSurf] Broadcasting permission result to all tabs...');
245
+ chrome.runtime.sendMessage({
246
+ type: 'MICROPHONE_PERMISSION_RESULT',
247
+ granted: message.granted,
248
+ error: message.error
249
+ }).then(() => {
250
+ console.log('[VibeSurf] Permission result broadcast successful');
251
+ }).catch((err) => {
252
+ console.log('[VibeSurf] Permission result broadcast failed (no listeners):', err);
253
+ });
254
+ }
255
+ result = { acknowledged: true };
256
+ break;
257
+
209
258
  default:
210
- console.warn('[VibeSurf] Unknown message type:', message.type);
211
- result = { error: 'Unknown message type' };
259
+ console.warn('[VibeSurf] Unknown message type:', message.type, 'Available handlers:', [
260
+ 'GET_CURRENT_TAB', 'UPDATE_BADGE', 'SHOW_NOTIFICATION', 'COPY_TO_CLIPBOARD',
261
+ 'HEALTH_CHECK', 'GET_BACKEND_STATUS', 'STORE_SESSION_DATA', 'GET_SESSION_DATA',
262
+ 'OPEN_FILE_URL', 'OPEN_FILE_SYSTEM', 'GET_ALL_TABS', 'REQUEST_MICROPHONE_PERMISSION',
263
+ 'REQUEST_MICROPHONE_PERMISSION_WITH_UI', 'MICROPHONE_PERMISSION_RESULT'
264
+ ]);
265
+ result = { error: 'Unknown message type', receivedType: message.type };
212
266
  }
213
267
 
214
268
  sendResponse(result);
@@ -316,28 +370,63 @@ class VibeSurfBackground {
316
370
  }
317
371
 
318
372
  async showNotification(data) {
319
- const { title, message, type = 'info', iconUrl = 'icons/icon48.png' } = data;
373
+ const { title, message, type = 'info', iconUrl } = data;
320
374
 
321
375
  // Map custom types to valid Chrome notification types
322
376
  const validType = ['basic', 'image', 'list', 'progress'].includes(type) ? type : 'basic';
323
377
 
324
- const notificationId = await chrome.notifications.create({
325
- type: validType,
326
- iconUrl,
327
- title: title || 'VibeSurf',
328
- message
329
- });
378
+ // Simplified icon handling - try available icons without validation
379
+ let finalIconUrl = '';
380
+
381
+ // Try to use extension icons in order of preference, but don't validate with fetch
382
+ const iconCandidates = [
383
+ iconUrl ? chrome.runtime.getURL(iconUrl) : null,
384
+ chrome.runtime.getURL('icons/icon48.png'),
385
+ chrome.runtime.getURL('icons/logo.png')
386
+ ].filter(Boolean);
330
387
 
331
- return { notificationId };
388
+ // Use the first candidate, or empty string as fallback
389
+ finalIconUrl = iconCandidates[0] || '';
390
+
391
+ try {
392
+ const notificationId = await chrome.notifications.create({
393
+ type: validType,
394
+ iconUrl: finalIconUrl,
395
+ title: title || 'VibeSurf',
396
+ message
397
+ });
398
+
399
+ return { notificationId };
400
+ } catch (error) {
401
+ console.warn('[VibeSurf] Notification with icon failed, trying without icon:', error);
402
+ // Try once more with empty icon URL
403
+ try {
404
+ const notificationId = await chrome.notifications.create({
405
+ type: validType,
406
+ iconUrl: '', // Empty string will use browser default
407
+ title: title || 'VibeSurf',
408
+ message
409
+ });
410
+ return { notificationId };
411
+ } catch (fallbackError) {
412
+ console.error('[VibeSurf] Fallback notification also failed:', fallbackError);
413
+ throw new Error(`Failed to create notification: ${error.message}`);
414
+ }
415
+ }
332
416
  }
333
417
 
334
418
  async showWelcomeNotification() {
335
- await chrome.notifications.create({
336
- type: 'basic',
337
- iconUrl: 'icons/icon48.png',
338
- title: 'Welcome to VibeSurf!',
339
- message: 'Click the VibeSurf icon in the toolbar to start automating your browsing tasks.'
340
- });
419
+ try {
420
+ await chrome.notifications.create({
421
+ type: 'basic',
422
+ iconUrl: '', // Use empty string to avoid icon issues
423
+ title: 'Welcome to VibeSurf!',
424
+ message: 'Click the VibeSurf icon in the toolbar to start automating your browsing tasks.'
425
+ });
426
+ } catch (error) {
427
+ console.warn('[VibeSurf] Welcome notification failed:', error);
428
+ // Don't throw, just log the warning
429
+ }
341
430
  }
342
431
 
343
432
  async checkBackendStatus(backendUrl = null) {
@@ -458,7 +547,40 @@ class VibeSurfBackground {
458
547
  return { success: false, error: 'No file URL provided' };
459
548
  }
460
549
 
550
+ // Add a unique request ID to track duplicate calls
551
+ const requestId = Date.now() + Math.random();
552
+ console.log(`[VibeSurf] openFileUrl called with ID: ${requestId}, URL: ${fileUrl}`);
553
+
461
554
  try {
555
+ // Validate URL format before attempting to open
556
+ try {
557
+ new URL(fileUrl);
558
+ } catch (urlError) {
559
+ console.warn('[VibeSurf] Invalid URL format:', fileUrl, urlError);
560
+ return { success: false, error: 'Invalid file URL format' };
561
+ }
562
+
563
+ // Check if this is an HTTP/HTTPS URL and handle it appropriately
564
+ if (fileUrl.startsWith('http://') || fileUrl.startsWith('https://')) {
565
+ console.log(`[VibeSurf] Detected HTTP(S) URL, creating tab for: ${fileUrl}`);
566
+
567
+ // Try to create a new tab with the URL
568
+ const tab = await chrome.tabs.create({
569
+ url: fileUrl,
570
+ active: true
571
+ });
572
+
573
+ if (tab && tab.id) {
574
+ console.log(`[VibeSurf] Successfully opened HTTP(S) URL in tab: ${tab.id} (request: ${requestId})`);
575
+ return { success: true, tabId: tab.id };
576
+ } else {
577
+ console.warn(`[VibeSurf] Tab creation returned but no tab ID for request: ${requestId}`);
578
+ return { success: false, error: 'Failed to create tab - no tab ID returned' };
579
+ }
580
+ }
581
+
582
+ // For file:// URLs, try the original approach
583
+ console.log(`[VibeSurf] Attempting to open file URL: ${fileUrl} (request: ${requestId})`);
462
584
 
463
585
  // Try to create a new tab with the file URL
464
586
  const tab = await chrome.tabs.create({
@@ -467,16 +589,25 @@ class VibeSurfBackground {
467
589
  });
468
590
 
469
591
  if (tab && tab.id) {
592
+ console.log(`[VibeSurf] Successfully opened file in tab: ${tab.id} (request: ${requestId})`);
470
593
  return { success: true, tabId: tab.id };
471
594
  } else {
472
- return { success: false, error: 'Failed to create tab' };
595
+ console.warn(`[VibeSurf] Tab creation returned but no tab ID for request: ${requestId}`);
596
+ return { success: false, error: 'Failed to create tab - no tab ID returned' };
473
597
  }
474
598
 
475
599
  } catch (error) {
476
- console.error('[VibeSurf] Error opening file URL:', error);
600
+ console.error(`[VibeSurf] Error opening file URL (request: ${requestId}):`, error);
601
+
602
+ // Provide more specific error messages
603
+ let errorMessage = error.message || 'Unknown error opening file';
604
+ if (error.message && error.message.includes('file://')) {
605
+ errorMessage = 'Browser security restricts opening local files. Try copying the file path and opening manually.';
606
+ }
607
+
477
608
  return {
478
609
  success: false,
479
- error: error.message || 'Unknown error opening file'
610
+ error: errorMessage
480
611
  };
481
612
  }
482
613
  }
@@ -624,6 +755,121 @@ class VibeSurfBackground {
624
755
  }
625
756
  }
626
757
 
758
+ // Request microphone permission through background script
759
+ async requestMicrophonePermission() {
760
+ try {
761
+ console.log('[VibeSurf] Requesting microphone permission through background script');
762
+
763
+ // Get the active tab to inject script
764
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
765
+
766
+ if (!tab) {
767
+ throw new Error('No active tab found');
768
+ }
769
+
770
+ // Check if we can inject script into this tab
771
+ if (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') ||
772
+ tab.url.startsWith('edge://') || tab.url.startsWith('moz-extension://')) {
773
+ throw new Error('Cannot access microphone from this type of page');
774
+ }
775
+
776
+ // Inject script to request microphone permission
777
+ const results = await chrome.scripting.executeScript({
778
+ target: { tabId: tab.id },
779
+ func: () => {
780
+ return new Promise((resolve, reject) => {
781
+ try {
782
+ // Check if mediaDevices is available
783
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
784
+ reject(new Error('Media devices not supported'));
785
+ return;
786
+ }
787
+
788
+ // Request microphone with minimal constraints
789
+ const constraints = { audio: true, video: false };
790
+
791
+ navigator.mediaDevices.getUserMedia(constraints)
792
+ .then(stream => {
793
+ // Stop the stream immediately after getting permission
794
+ stream.getTracks().forEach(track => track.stop());
795
+ resolve({ success: true, hasPermission: true });
796
+ })
797
+ .catch(error => {
798
+ reject(new Error(`Microphone permission denied: ${error.name} - ${error.message}`));
799
+ });
800
+ } catch (error) {
801
+ reject(new Error(`Failed to request microphone permission: ${error.message}`));
802
+ }
803
+ });
804
+ }
805
+ });
806
+
807
+ const result = await results[0].result;
808
+ console.log('[VibeSurf] Microphone permission result:', result);
809
+ return result;
810
+
811
+ } catch (error) {
812
+ console.error('[VibeSurf] Failed to request microphone permission:', error);
813
+ return { success: false, error: error.message };
814
+ }
815
+ }
816
+
817
+ // Create a proper permission request page that opens in a new tab
818
+ async requestMicrophonePermissionWithUI() {
819
+ try {
820
+ console.log('[VibeSurf] Opening permission request page in new tab');
821
+
822
+ // Use the existing permission-request.html file
823
+ const permissionPageUrl = chrome.runtime.getURL('permission-request.html');
824
+
825
+ // Create a tab with the permission page
826
+ const permissionTab = await chrome.tabs.create({
827
+ url: permissionPageUrl,
828
+ active: true
829
+ });
830
+
831
+ console.log('[VibeSurf] Created permission tab:', permissionTab.id);
832
+
833
+ // Return a promise that resolves when we get the permission result
834
+ return new Promise((resolve) => {
835
+ const messageHandler = (message, sender, sendResponse) => {
836
+ if (message.type === 'MICROPHONE_PERMISSION_RESULT') {
837
+ console.log('[VibeSurf] Received permission result:', message);
838
+
839
+ // Clean up the message listener
840
+ chrome.runtime.onMessage.removeListener(messageHandler);
841
+
842
+ // Close the permission tab
843
+ chrome.tabs.remove(permissionTab.id).catch(() => {
844
+ // Tab might already be closed
845
+ });
846
+
847
+ // Resolve the promise
848
+ if (message.granted) {
849
+ resolve({ success: true, hasPermission: true });
850
+ } else {
851
+ resolve({ success: false, error: message.error || 'Permission denied by user' });
852
+ }
853
+ }
854
+ };
855
+
856
+ // Add the message listener
857
+ chrome.runtime.onMessage.addListener(messageHandler);
858
+
859
+ // Set a timeout to clean up if the tab is closed without response
860
+ setTimeout(() => {
861
+ chrome.runtime.onMessage.removeListener(messageHandler);
862
+ chrome.tabs.remove(permissionTab.id).catch(() => {});
863
+ resolve({ success: false, error: 'Permission request timed out' });
864
+ }, 30000); // 30 second timeout
865
+ });
866
+
867
+ } catch (error) {
868
+ console.error('[VibeSurf] Failed to create permission UI:', error);
869
+ return { success: false, error: error.message };
870
+ }
871
+ }
872
+
627
873
  // Cleanup method for extension unload
628
874
  async cleanup() {
629
875