vibesurf 0.1.19__py3-none-any.whl → 0.1.21__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 (42) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/report_writer_agent.py +1 -1
  3. vibe_surf/backend/api/task.py +1 -1
  4. vibe_surf/backend/api/voices.py +481 -0
  5. vibe_surf/backend/database/migrations/v003_fix_task_status_case.sql +11 -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 +3 -3
  14. vibe_surf/chrome_extension/background.js +224 -9
  15. vibe_surf/chrome_extension/content.js +147 -0
  16. vibe_surf/chrome_extension/manifest.json +11 -2
  17. vibe_surf/chrome_extension/permission-iframe.html +38 -0
  18. vibe_surf/chrome_extension/permission-request.html +104 -0
  19. vibe_surf/chrome_extension/scripts/api-client.js +61 -0
  20. vibe_surf/chrome_extension/scripts/main.js +8 -2
  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/settings-manager.js +690 -3
  24. vibe_surf/chrome_extension/scripts/ui-manager.js +730 -119
  25. vibe_surf/chrome_extension/scripts/user-settings-storage.js +422 -0
  26. vibe_surf/chrome_extension/scripts/voice-recorder.js +514 -0
  27. vibe_surf/chrome_extension/sidepanel.html +106 -29
  28. vibe_surf/chrome_extension/styles/components.css +35 -0
  29. vibe_surf/chrome_extension/styles/input.css +164 -1
  30. vibe_surf/chrome_extension/styles/layout.css +1 -1
  31. vibe_surf/chrome_extension/styles/settings-environment.css +138 -0
  32. vibe_surf/chrome_extension/styles/settings-forms.css +7 -7
  33. vibe_surf/chrome_extension/styles/variables.css +51 -0
  34. vibe_surf/tools/voice_asr.py +125 -0
  35. {vibesurf-0.1.19.dist-info → vibesurf-0.1.21.dist-info}/METADATA +9 -12
  36. {vibesurf-0.1.19.dist-info → vibesurf-0.1.21.dist-info}/RECORD +40 -31
  37. vibe_surf/chrome_extension/icons/convert-svg.js +0 -33
  38. vibe_surf/chrome_extension/icons/logo-preview.html +0 -187
  39. {vibesurf-0.1.19.dist-info → vibesurf-0.1.21.dist-info}/WHEEL +0 -0
  40. {vibesurf-0.1.19.dist-info → vibesurf-0.1.21.dist-info}/entry_points.txt +0 -0
  41. {vibesurf-0.1.19.dist-info → vibesurf-0.1.21.dist-info}/licenses/LICENSE +0 -0
  42. {vibesurf-0.1.19.dist-info → vibesurf-0.1.21.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):
@@ -428,9 +428,9 @@ class AgentBrowserSession(BrowserSession):
428
428
  self._popups_watchdog.attach_to_session()
429
429
 
430
430
  # 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()
431
+ # PermissionsWatchdog.model_rebuild()
432
+ # self._permissions_watchdog = PermissionsWatchdog(event_bus=self.event_bus, browser_session=self)
433
+ # self._permissions_watchdog.attach_to_session()
434
434
 
435
435
  # 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
436
  CustomActionWatchdog.model_rebuild()
@@ -155,12 +155,14 @@ class VibeSurfBackground {
155
155
  }
156
156
 
157
157
  handleMessage(message, sender, sendResponse) {
158
+ console.log('[VibeSurf] Received message:', message.type);
158
159
 
159
160
  // Handle async messages properly
160
161
  (async () => {
161
162
  try {
162
163
  let result;
163
164
 
165
+ console.log('[VibeSurf] Processing message type:', message.type);
164
166
  switch (message.type) {
165
167
  case 'GET_CURRENT_TAB':
166
168
  result = await this.getCurrentTabInfo();
@@ -206,9 +208,56 @@ class VibeSurfBackground {
206
208
  result = await this.getAllTabs();
207
209
  break;
208
210
 
211
+ case 'REQUEST_MICROPHONE_PERMISSION':
212
+ result = await this.requestMicrophonePermission();
213
+ break;
214
+
215
+ case 'REQUEST_MICROPHONE_PERMISSION_WITH_UI':
216
+ console.log('[VibeSurf] Handling REQUEST_MICROPHONE_PERMISSION_WITH_UI');
217
+ result = await this.requestMicrophonePermissionWithUI();
218
+ break;
219
+
220
+ case 'MICROPHONE_PERMISSION_RESULT':
221
+ console.log('[VibeSurf] Received MICROPHONE_PERMISSION_RESULT:', message);
222
+ console.log('[VibeSurf] Permission granted:', message.granted);
223
+ console.log('[VibeSurf] Permission error:', message.error);
224
+
225
+ // Handle permission result from URL parameter approach
226
+ if (message.granted !== undefined) {
227
+ console.log('[VibeSurf] Processing permission result with granted:', message.granted);
228
+
229
+ // Store the result for the original tab to retrieve
230
+ chrome.storage.local.set({
231
+ microphonePermissionResult: {
232
+ granted: message.granted,
233
+ error: message.error,
234
+ timestamp: Date.now()
235
+ }
236
+ });
237
+
238
+ // Also send to any waiting listeners
239
+ console.log('[VibeSurf] Broadcasting permission result to all tabs...');
240
+ chrome.runtime.sendMessage({
241
+ type: 'MICROPHONE_PERMISSION_RESULT',
242
+ granted: message.granted,
243
+ error: message.error
244
+ }).then(() => {
245
+ console.log('[VibeSurf] Permission result broadcast successful');
246
+ }).catch((err) => {
247
+ console.log('[VibeSurf] Permission result broadcast failed (no listeners):', err);
248
+ });
249
+ }
250
+ result = { acknowledged: true };
251
+ break;
252
+
209
253
  default:
210
- console.warn('[VibeSurf] Unknown message type:', message.type);
211
- result = { error: 'Unknown message type' };
254
+ console.warn('[VibeSurf] Unknown message type:', message.type, 'Available handlers:', [
255
+ 'GET_CURRENT_TAB', 'UPDATE_BADGE', 'SHOW_NOTIFICATION', 'COPY_TO_CLIPBOARD',
256
+ 'HEALTH_CHECK', 'GET_BACKEND_STATUS', 'STORE_SESSION_DATA', 'GET_SESSION_DATA',
257
+ 'OPEN_FILE_URL', 'OPEN_FILE_SYSTEM', 'GET_ALL_TABS', 'REQUEST_MICROPHONE_PERMISSION',
258
+ 'REQUEST_MICROPHONE_PERMISSION_WITH_UI', 'MICROPHONE_PERMISSION_RESULT'
259
+ ]);
260
+ result = { error: 'Unknown message type', receivedType: message.type };
212
261
  }
213
262
 
214
263
  sendResponse(result);
@@ -321,14 +370,65 @@ class VibeSurfBackground {
321
370
  // Map custom types to valid Chrome notification types
322
371
  const validType = ['basic', 'image', 'list', 'progress'].includes(type) ? type : 'basic';
323
372
 
324
- const notificationId = await chrome.notifications.create({
325
- type: validType,
326
- iconUrl,
327
- title: title || 'VibeSurf',
328
- message
329
- });
373
+ // Validate icon URL and provide fallback
374
+ let validatedIconUrl = iconUrl;
375
+ try {
376
+ // Check if icon URL is accessible
377
+ if (iconUrl && iconUrl !== 'icons/icon48.png') {
378
+ // For custom icons, validate they exist
379
+ const response = await fetch(iconUrl, { method: 'HEAD' });
380
+ if (!response.ok) {
381
+ validatedIconUrl = 'icons/icon48.png';
382
+ }
383
+ } else {
384
+ // Use default icon, check if it exists
385
+ const defaultIconUrl = chrome.runtime.getURL('icons/icon48.png');
386
+ const response = await fetch(defaultIconUrl, { method: 'HEAD' });
387
+ if (response.ok) {
388
+ validatedIconUrl = defaultIconUrl;
389
+ } else {
390
+ // Fallback to logo.png if icon48.png doesn't exist
391
+ const logoUrl = chrome.runtime.getURL('icons/logo.png');
392
+ const logoResponse = await fetch(logoUrl, { method: 'HEAD' });
393
+ if (logoResponse.ok) {
394
+ validatedIconUrl = logoUrl;
395
+ } else {
396
+ // If no icons work, use empty string (browser will use default)
397
+ validatedIconUrl = '';
398
+ }
399
+ }
400
+ }
401
+ } catch (error) {
402
+ console.warn('[VibeSurf] Icon validation failed, using fallback:', error);
403
+ // Use empty string as fallback (browser will use default icon)
404
+ validatedIconUrl = '';
405
+ }
330
406
 
331
- return { notificationId };
407
+ try {
408
+ const notificationId = await chrome.notifications.create({
409
+ type: validType,
410
+ iconUrl: validatedIconUrl,
411
+ title: title || 'VibeSurf',
412
+ message
413
+ });
414
+
415
+ return { notificationId };
416
+ } catch (error) {
417
+ console.error('[VibeSurf] Failed to create notification:', error);
418
+ // Try once more with empty icon URL
419
+ try {
420
+ const notificationId = await chrome.notifications.create({
421
+ type: validType,
422
+ iconUrl: '', // Empty string will use browser default
423
+ title: title || 'VibeSurf',
424
+ message
425
+ });
426
+ return { notificationId };
427
+ } catch (fallbackError) {
428
+ console.error('[VibeSurf] Fallback notification also failed:', fallbackError);
429
+ throw new Error(`Failed to create notification: ${error.message}`);
430
+ }
431
+ }
332
432
  }
333
433
 
334
434
  async showWelcomeNotification() {
@@ -624,6 +724,121 @@ class VibeSurfBackground {
624
724
  }
625
725
  }
626
726
 
727
+ // Request microphone permission through background script
728
+ async requestMicrophonePermission() {
729
+ try {
730
+ console.log('[VibeSurf] Requesting microphone permission through background script');
731
+
732
+ // Get the active tab to inject script
733
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
734
+
735
+ if (!tab) {
736
+ throw new Error('No active tab found');
737
+ }
738
+
739
+ // Check if we can inject script into this tab
740
+ if (tab.url.startsWith('chrome://') || tab.url.startsWith('chrome-extension://') ||
741
+ tab.url.startsWith('edge://') || tab.url.startsWith('moz-extension://')) {
742
+ throw new Error('Cannot access microphone from this type of page');
743
+ }
744
+
745
+ // Inject script to request microphone permission
746
+ const results = await chrome.scripting.executeScript({
747
+ target: { tabId: tab.id },
748
+ func: () => {
749
+ return new Promise((resolve, reject) => {
750
+ try {
751
+ // Check if mediaDevices is available
752
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
753
+ reject(new Error('Media devices not supported'));
754
+ return;
755
+ }
756
+
757
+ // Request microphone with minimal constraints
758
+ const constraints = { audio: true, video: false };
759
+
760
+ navigator.mediaDevices.getUserMedia(constraints)
761
+ .then(stream => {
762
+ // Stop the stream immediately after getting permission
763
+ stream.getTracks().forEach(track => track.stop());
764
+ resolve({ success: true, hasPermission: true });
765
+ })
766
+ .catch(error => {
767
+ reject(new Error(`Microphone permission denied: ${error.name} - ${error.message}`));
768
+ });
769
+ } catch (error) {
770
+ reject(new Error(`Failed to request microphone permission: ${error.message}`));
771
+ }
772
+ });
773
+ }
774
+ });
775
+
776
+ const result = await results[0].result;
777
+ console.log('[VibeSurf] Microphone permission result:', result);
778
+ return result;
779
+
780
+ } catch (error) {
781
+ console.error('[VibeSurf] Failed to request microphone permission:', error);
782
+ return { success: false, error: error.message };
783
+ }
784
+ }
785
+
786
+ // Create a proper permission request page that opens in a new tab
787
+ async requestMicrophonePermissionWithUI() {
788
+ try {
789
+ console.log('[VibeSurf] Opening permission request page in new tab');
790
+
791
+ // Use the existing permission-request.html file
792
+ const permissionPageUrl = chrome.runtime.getURL('permission-request.html');
793
+
794
+ // Create a tab with the permission page
795
+ const permissionTab = await chrome.tabs.create({
796
+ url: permissionPageUrl,
797
+ active: true
798
+ });
799
+
800
+ console.log('[VibeSurf] Created permission tab:', permissionTab.id);
801
+
802
+ // Return a promise that resolves when we get the permission result
803
+ return new Promise((resolve) => {
804
+ const messageHandler = (message, sender, sendResponse) => {
805
+ if (message.type === 'MICROPHONE_PERMISSION_RESULT') {
806
+ console.log('[VibeSurf] Received permission result:', message);
807
+
808
+ // Clean up the message listener
809
+ chrome.runtime.onMessage.removeListener(messageHandler);
810
+
811
+ // Close the permission tab
812
+ chrome.tabs.remove(permissionTab.id).catch(() => {
813
+ // Tab might already be closed
814
+ });
815
+
816
+ // Resolve the promise
817
+ if (message.granted) {
818
+ resolve({ success: true, hasPermission: true });
819
+ } else {
820
+ resolve({ success: false, error: message.error || 'Permission denied by user' });
821
+ }
822
+ }
823
+ };
824
+
825
+ // Add the message listener
826
+ chrome.runtime.onMessage.addListener(messageHandler);
827
+
828
+ // Set a timeout to clean up if the tab is closed without response
829
+ setTimeout(() => {
830
+ chrome.runtime.onMessage.removeListener(messageHandler);
831
+ chrome.tabs.remove(permissionTab.id).catch(() => {});
832
+ resolve({ success: false, error: 'Permission request timed out' });
833
+ }, 30000); // 30 second timeout
834
+ });
835
+
836
+ } catch (error) {
837
+ console.error('[VibeSurf] Failed to create permission UI:', error);
838
+ return { success: false, error: error.message };
839
+ }
840
+ }
841
+
627
842
  // Cleanup method for extension unload
628
843
  async cleanup() {
629
844