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
@@ -53,10 +53,36 @@
53
53
  sendResponse(clickResult);
54
54
  break;
55
55
 
56
+ case 'INJECT_MICROPHONE_PERMISSION_IFRAME':
57
+ this.injectMicrophonePermissionIframe()
58
+ .then(result => sendResponse(result))
59
+ .catch(error => sendResponse({ success: false, error: error.message }));
60
+ return true; // Will respond asynchronously
61
+
62
+ case 'REMOVE_MICROPHONE_PERMISSION_IFRAME':
63
+ this.removeMicrophonePermissionIframe();
64
+ sendResponse({ success: true });
65
+ break;
66
+
56
67
  default:
57
68
  console.warn('[VibeSurf Content] Unknown message type:', message.type);
58
69
  }
59
70
  });
71
+
72
+ // Listen for postMessage from iframe
73
+ window.addEventListener('message', (event) => {
74
+ if (event.data && event.data.type === 'MICROPHONE_PERMISSION_RESULT') {
75
+ console.log('[VibeSurf Content] Received permission result from iframe:', event.data);
76
+
77
+ // Forward to extension
78
+ chrome.runtime.sendMessage({
79
+ type: 'MICROPHONE_PERMISSION_RESULT',
80
+ ...event.data
81
+ }).catch(() => {
82
+ // Ignore if no listeners
83
+ });
84
+ }
85
+ });
60
86
  }
61
87
 
62
88
  collectPageContext() {
@@ -233,6 +259,127 @@
233
259
  }
234
260
  }
235
261
 
262
+ // Inject hidden iframe for microphone permission request
263
+ async injectMicrophonePermissionIframe() {
264
+ try {
265
+ console.log('[VibeSurf Content] Injecting microphone permission iframe...');
266
+
267
+ // Check if iframe already exists
268
+ const existingIframe = document.getElementById('vibesurf-permission-iframe');
269
+ if (existingIframe) {
270
+ console.log('[VibeSurf Content] Permission iframe already exists');
271
+ return { success: true, alreadyExists: true };
272
+ }
273
+
274
+ // Create the iframe element
275
+ const iframe = document.createElement('iframe');
276
+ iframe.setAttribute('id', 'vibesurf-permission-iframe');
277
+ iframe.setAttribute('allow', 'microphone');
278
+ iframe.setAttribute('hidden', 'hidden');
279
+ iframe.style.display = 'none';
280
+ iframe.style.width = '0px';
281
+ iframe.style.height = '0px';
282
+ iframe.style.border = 'none';
283
+ iframe.style.position = 'fixed';
284
+ iframe.style.top = '-9999px';
285
+ iframe.style.left = '-9999px';
286
+ iframe.style.zIndex = '-1';
287
+
288
+ // Set the source to our permission iframe page
289
+ const iframeUrl = chrome.runtime.getURL('permission-iframe.html');
290
+ iframe.src = iframeUrl;
291
+
292
+ console.log('[VibeSurf Content] Creating iframe with URL:', iframeUrl);
293
+
294
+ // Return a promise that resolves when permission is granted/denied
295
+ return new Promise((resolve, reject) => {
296
+ const timeout = setTimeout(() => {
297
+ console.log('[VibeSurf Content] Permission iframe timeout');
298
+ this.removeMicrophonePermissionIframe();
299
+ reject(new Error('Permission request timeout'));
300
+ }, 30000); // 30 second timeout
301
+
302
+ // Listen for permission result
303
+ const messageHandler = (event) => {
304
+ if (event.data && event.data.type === 'MICROPHONE_PERMISSION_RESULT') {
305
+ console.log('[VibeSurf Content] Received permission result:', event.data);
306
+
307
+ clearTimeout(timeout);
308
+ window.removeEventListener('message', messageHandler);
309
+
310
+ if (event.data.success) {
311
+ resolve({
312
+ success: true,
313
+ granted: event.data.granted,
314
+ source: 'iframe'
315
+ });
316
+ } else {
317
+ resolve({
318
+ success: false,
319
+ granted: false,
320
+ error: event.data.error || 'Permission denied',
321
+ userMessage: event.data.userMessage
322
+ });
323
+ }
324
+
325
+ // Clean up iframe after a short delay
326
+ setTimeout(() => {
327
+ this.removeMicrophonePermissionIframe();
328
+ }, 1000);
329
+ }
330
+ };
331
+
332
+ window.addEventListener('message', messageHandler);
333
+
334
+ // Handle iframe load
335
+ iframe.onload = () => {
336
+ console.log('[VibeSurf Content] Permission iframe loaded successfully');
337
+
338
+ // Send message to iframe to start permission request
339
+ setTimeout(() => {
340
+ if (iframe.contentWindow) {
341
+ iframe.contentWindow.postMessage({
342
+ type: 'REQUEST_MICROPHONE_PERMISSION'
343
+ }, '*');
344
+ }
345
+ }, 100);
346
+ };
347
+
348
+ iframe.onerror = (error) => {
349
+ console.error('[VibeSurf Content] Permission iframe load error:', error);
350
+ clearTimeout(timeout);
351
+ window.removeEventListener('message', messageHandler);
352
+ this.removeMicrophonePermissionIframe();
353
+ reject(new Error('Failed to load permission iframe'));
354
+ };
355
+
356
+ // Append to document body
357
+ document.body.appendChild(iframe);
358
+ console.log('[VibeSurf Content] Permission iframe injected into page');
359
+ });
360
+
361
+ } catch (error) {
362
+ console.error('[VibeSurf Content] Failed to inject permission iframe:', error);
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ // Remove microphone permission iframe
368
+ removeMicrophonePermissionIframe() {
369
+ try {
370
+ const iframe = document.getElementById('vibesurf-permission-iframe');
371
+ if (iframe) {
372
+ console.log('[VibeSurf Content] Removing permission iframe');
373
+ iframe.remove();
374
+ return true;
375
+ }
376
+ return false;
377
+ } catch (error) {
378
+ console.error('[VibeSurf Content] Error removing permission iframe:', error);
379
+ return false;
380
+ }
381
+ }
382
+
236
383
  // Utility method to send context updates to background
237
384
  sendContextUpdate() {
238
385
  try {
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Microphone Permission Request</title>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ padding: 10px;
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ font-size: 12px;
12
+ background: #f5f5f5;
13
+ color: #333;
14
+ }
15
+ .status {
16
+ padding: 8px;
17
+ border-radius: 4px;
18
+ text-align: center;
19
+ }
20
+ .loading {
21
+ background: #bee3f8;
22
+ color: #2a4365;
23
+ }
24
+ .success {
25
+ background: #c6f6d5;
26
+ color: #22543d;
27
+ }
28
+ .error {
29
+ background: #fed7d7;
30
+ color: #742a2a;
31
+ }
32
+ </style>
33
+ </head>
34
+ <body>
35
+ <div id="status" class="status loading">Requesting microphone access...</div>
36
+ <script src="scripts/permission-iframe-request.js"></script>
37
+ </body>
38
+ </html>
@@ -0,0 +1,104 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>VibeSurf - Microphone Permission</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ display: flex;
11
+ justify-content: center;
12
+ align-items: center;
13
+ min-height: 100vh;
14
+ margin: 0;
15
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
16
+ color: #333;
17
+ }
18
+ .container {
19
+ background: white;
20
+ padding: 40px;
21
+ border-radius: 12px;
22
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
23
+ text-align: center;
24
+ max-width: 400px;
25
+ }
26
+ .logo {
27
+ width: 60px;
28
+ height: 60px;
29
+ margin-bottom: 20px;
30
+ }
31
+ h1 {
32
+ color: #2d3748;
33
+ margin-bottom: 10px;
34
+ font-size: 24px;
35
+ }
36
+ p {
37
+ color: #718096;
38
+ margin-bottom: 30px;
39
+ line-height: 1.6;
40
+ }
41
+ .button-group {
42
+ display: flex;
43
+ gap: 12px;
44
+ justify-content: center;
45
+ }
46
+ button {
47
+ padding: 12px 24px;
48
+ border: none;
49
+ border-radius: 6px;
50
+ font-size: 16px;
51
+ font-weight: 500;
52
+ cursor: pointer;
53
+ transition: all 0.2s;
54
+ }
55
+ .allow-btn {
56
+ background: #4299e1;
57
+ color: white;
58
+ }
59
+ .allow-btn:hover {
60
+ background: #3182ce;
61
+ transform: translateY(-1px);
62
+ }
63
+ .deny-btn {
64
+ background: #e2e8f0;
65
+ color: #4a5568;
66
+ }
67
+ .deny-btn:hover {
68
+ background: #cbd5e0;
69
+ }
70
+ #status {
71
+ margin-top: 20px;
72
+ padding: 12px;
73
+ border-radius: 6px;
74
+ font-weight: 500;
75
+ }
76
+ .success {
77
+ background: #c6f6d5;
78
+ color: #22543d;
79
+ }
80
+ .error {
81
+ background: #fed7d7;
82
+ color: #742a2a;
83
+ }
84
+ .loading {
85
+ background: #bee3f8;
86
+ color: #2a4365;
87
+ }
88
+ </style>
89
+ </head>
90
+ <body>
91
+ <div class="container">
92
+ <img src="icons/logo.png" alt="VibeSurf" class="logo">
93
+ <h1>Microphone Permission Required</h1>
94
+ <p>VibeSurf needs access to your microphone to enable voice input. This permission is used only when you click the microphone button.</p>
95
+ <div class="button-group">
96
+ <button id="allowBtn" class="allow-btn">Allow Microphone</button>
97
+ <button id="denyBtn" class="deny-btn">Deny</button>
98
+ </div>
99
+ <div id="status"></div>
100
+ </div>
101
+
102
+ <script src="scripts/permission-request.js"></script>
103
+ </body>
104
+ </html>
@@ -410,6 +410,67 @@ class VibeSurfAPIClient {
410
410
  return this.get(`/config/llm/providers/${encodeURIComponent(providerName)}/models`);
411
411
  }
412
412
 
413
+ // Voice Profile Management
414
+ async getVoiceProfiles(activeOnly = true, voiceModelType = null, limit = 50, offset = 0) {
415
+ const params = { active_only: activeOnly, limit, offset };
416
+ if (voiceModelType) {
417
+ params.voice_model_type = voiceModelType;
418
+ }
419
+ return this.get('/voices/voice-profiles', { params });
420
+ }
421
+
422
+ async getVoiceProfile(profileName) {
423
+ return this.get(`/voices/${encodeURIComponent(profileName)}`);
424
+ }
425
+
426
+ async createVoiceProfile(profileData) {
427
+ return this.post('/voices/voice-profiles', profileData);
428
+ }
429
+
430
+ async updateVoiceProfile(profileName, updateData) {
431
+ return this.put(`/voices/voice-profiles/${encodeURIComponent(profileName)}`, updateData);
432
+ }
433
+
434
+ async deleteVoiceProfile(profileName) {
435
+ return this.delete(`/voices/voice-profiles/${encodeURIComponent(profileName)}`);
436
+ }
437
+
438
+ // Voice Models - matches the backend route @router.get("/models")
439
+ async getVoiceModels(modelType = null) {
440
+ let url = '/voices/models';
441
+ if (modelType) {
442
+ url += `?model_type=${encodeURIComponent(modelType)}`;
443
+ }
444
+ return this.get(url);
445
+ }
446
+
447
+ // Voice Recording API
448
+ async transcribeAudio(audioBlob, voiceProfileName = null) {
449
+ const formData = new FormData();
450
+ formData.append('audio_file', audioBlob, 'recording.webm');
451
+
452
+ // Add voice profile name if provided
453
+ const params = {};
454
+ if (voiceProfileName) {
455
+ params.voice_profile_name = voiceProfileName;
456
+ }
457
+
458
+ return this.post('/voices/asr', formData, {
459
+ params,
460
+ headers: {} // Let browser set Content-Type with boundary for FormData
461
+ });
462
+ }
463
+
464
+ // Get available ASR profiles
465
+ async getASRProfiles(activeOnly = true) {
466
+ return this.get('/voices/voice-profiles', {
467
+ params: {
468
+ voice_model_type: 'asr',
469
+ active_only: activeOnly
470
+ }
471
+ });
472
+ }
473
+
413
474
  // Environment Variables
414
475
  async getEnvironmentVariables() {
415
476
  return this.get('/config/environments');
@@ -275,14 +275,28 @@ class VibeSurfFileManager {
275
275
  async handleFileLink(filePath) {
276
276
  // Prevent multiple simultaneous calls
277
277
  if (this.state.isHandlingFileLink) {
278
+ console.log('[FileManager] File link handling already in progress, skipping...');
278
279
  return;
279
280
  }
280
281
 
281
282
  this.state.isHandlingFileLink = true;
282
283
 
283
284
  try {
284
- // First decode the URL-encoded path
285
- let decodedPath = decodeURIComponent(filePath);
285
+ console.log('[FileManager] Handling file link:', filePath);
286
+
287
+ // Validate input
288
+ if (!filePath || typeof filePath !== 'string') {
289
+ throw new Error('Invalid file path provided');
290
+ }
291
+
292
+ // First decode the URL-encoded path safely
293
+ let decodedPath;
294
+ try {
295
+ decodedPath = decodeURIComponent(filePath);
296
+ } catch (decodeError) {
297
+ console.warn('[FileManager] Failed to decode URL, using original path:', decodeError);
298
+ decodedPath = filePath;
299
+ }
286
300
 
287
301
  // Remove file:// protocol prefix and normalize
288
302
  let cleanPath = decodedPath.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
@@ -300,21 +314,28 @@ class VibeSurfFileManager {
300
314
  `file:///${cleanPath}` :
301
315
  `file:///${cleanPath.replace(/^\//, '')}`; // Remove leading slash and add triple slash
302
316
 
317
+ console.log('[FileManager] Processed file URL:', fileUrl);
318
+
303
319
  // Show user notification about the action
304
320
  this.emit('notification', {
305
321
  message: `Opening file: ${cleanPath}`,
306
322
  type: 'info'
307
323
  });
308
324
 
309
- // Use setTimeout to prevent UI blocking
325
+ // Use setTimeout to prevent UI blocking and ensure proper cleanup
310
326
  setTimeout(async () => {
311
327
  try {
312
328
  // For user-clicked file links, use OPEN_FILE_URL to keep tab open
313
329
  // This prevents the auto-close behavior in OPEN_FILE_SYSTEM
314
- const fileOpenResponse = await chrome.runtime.sendMessage({
315
- type: 'OPEN_FILE_URL',
316
- data: { fileUrl: fileUrl }
317
- });
330
+ const fileOpenResponse = await Promise.race([
331
+ chrome.runtime.sendMessage({
332
+ type: 'OPEN_FILE_URL',
333
+ data: { fileUrl: fileUrl }
334
+ }),
335
+ new Promise((_, reject) =>
336
+ setTimeout(() => reject(new Error('File open timeout')), 5000)
337
+ )
338
+ ]);
318
339
 
319
340
  if (fileOpenResponse && fileOpenResponse.success) {
320
341
  this.emit('notification', {
@@ -322,12 +343,14 @@ class VibeSurfFileManager {
322
343
  type: 'success'
323
344
  });
324
345
  return;
346
+ } else if (fileOpenResponse && fileOpenResponse.error) {
347
+ console.warn('[FileManager] Background script file open failed:', fileOpenResponse.error);
325
348
  }
326
349
 
327
- // If OPEN_FILE_URL fails, try direct browser open
350
+ // If OPEN_FILE_URL fails, try direct browser open with additional safety
328
351
  try {
329
352
  const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
330
- if (opened) {
353
+ if (opened && !opened.closed) {
331
354
  this.emit('notification', {
332
355
  message: 'File opened in browser',
333
356
  type: 'success'
@@ -339,14 +362,32 @@ class VibeSurfFileManager {
339
362
  }
340
363
 
341
364
  // Last resort: Copy path to clipboard
342
- this.copyToClipboardFallback(fileUrl);
365
+ await this.copyToClipboardFallback(fileUrl);
343
366
 
344
367
  } catch (error) {
345
368
  console.error('[FileManager] Error in async file handling:', error);
369
+
370
+ // Provide more helpful error messages
371
+ let userMessage = 'Unable to open file';
372
+ if (error.message.includes('timeout')) {
373
+ userMessage = 'File open operation timed out. Try copying the path manually.';
374
+ } else if (error.message.includes('protocol')) {
375
+ userMessage = 'Browser security restricts opening local files. File path copied to clipboard.';
376
+ } else {
377
+ userMessage = `Unable to open file: ${error.message}`;
378
+ }
379
+
346
380
  this.emit('notification', {
347
- message: `Unable to open file: ${error.message}`,
381
+ message: userMessage,
348
382
  type: 'error'
349
383
  });
384
+
385
+ // Fallback to clipboard
386
+ try {
387
+ await this.copyToClipboardFallback(fileUrl);
388
+ } catch (clipboardError) {
389
+ console.error('[FileManager] Clipboard fallback also failed:', clipboardError);
390
+ }
350
391
  } finally {
351
392
  this.state.isHandlingFileLink = false;
352
393
  }
@@ -355,7 +396,7 @@ class VibeSurfFileManager {
355
396
  } catch (error) {
356
397
  console.error('[FileManager] Error handling file link:', error);
357
398
  this.emit('notification', {
358
- message: `Unable to open file: ${error.message}`,
399
+ message: `File link processing failed: ${error.message}`,
359
400
  type: 'error'
360
401
  });
361
402
  this.state.isHandlingFileLink = false;
@@ -154,11 +154,11 @@ class VibeSurfApp {
154
154
 
155
155
  async initializeUIManager() {
156
156
  this.uiManager = new VibeSurfUIManager(this.sessionManager, this.apiClient);
157
-
157
+
158
158
  // Initialize UI with loaded data
159
159
  await this.uiManager.initialize();
160
160
 
161
- console.log('[VibeSurf] UI manager initialized');
161
+ console.log('[VibeSurf] UI manager initialized successfully');
162
162
  }
163
163
 
164
164
  setupErrorHandling() {
@@ -189,6 +189,12 @@ class VibeSurfApp {
189
189
  // Periodic backend health check
190
190
  setInterval(async () => {
191
191
  try {
192
+ // Check if apiClient exists and is initialized
193
+ if (!this.apiClient || typeof this.apiClient.healthCheck !== 'function') {
194
+ console.warn('[VibeSurf] Health check skipped - API client not available');
195
+ return;
196
+ }
197
+
192
198
  const healthCheck = await this.apiClient.healthCheck();
193
199
 
194
200
  if (healthCheck.status === 'healthy') {
@@ -413,18 +419,37 @@ class VibeSurfApp {
413
419
 
414
420
  // Cleanup method
415
421
  destroy() {
416
- console.log('[VibeSurf] Cleaning up application...');
417
-
418
- if (this.uiManager) {
419
- this.uiManager.destroy();
422
+ // Prevent multiple cleanup calls
423
+ if (this.isDestroying || !this.isInitialized) {
424
+ console.log('[VibeSurf] Cleanup already in progress or app not initialized, skipping...');
425
+ return;
420
426
  }
421
427
 
422
- if (this.sessionManager) {
423
- this.sessionManager.destroy();
424
- }
428
+ this.isDestroying = true;
429
+ console.log('[VibeSurf] Cleaning up application...');
425
430
 
426
- this.isInitialized = false;
427
- console.log('[VibeSurf] Application cleanup complete');
431
+ try {
432
+ if (this.uiManager) {
433
+ this.uiManager.destroy();
434
+ this.uiManager = null;
435
+ }
436
+
437
+ if (this.sessionManager) {
438
+ this.sessionManager.destroy();
439
+ this.sessionManager = null;
440
+ }
441
+
442
+ if (this.apiClient) {
443
+ this.apiClient = null;
444
+ }
445
+
446
+ this.isInitialized = false;
447
+ console.log('[VibeSurf] Application cleanup complete');
448
+ } catch (error) {
449
+ console.error('[VibeSurf] Error during cleanup:', error);
450
+ } finally {
451
+ this.isDestroying = false;
452
+ }
428
453
  }
429
454
 
430
455
  // Get application status
@@ -456,11 +481,21 @@ document.addEventListener('DOMContentLoaded', async () => {
456
481
 
457
482
  // Handle page unload
458
483
  window.addEventListener('beforeunload', () => {
459
- if (window.vibeSurfApp) {
484
+ if (window.vibeSurfApp && window.vibeSurfApp.isInitialized && !window.vibeSurfApp.isDestroying) {
485
+ console.log('[VibeSurf] Page unloading, cleaning up...');
460
486
  window.vibeSurfApp.destroy();
461
487
  }
462
488
  });
463
489
 
490
+ // Handle visibility change to prevent unnecessary cleanup
491
+ document.addEventListener('visibilitychange', () => {
492
+ if (document.visibilityState === 'hidden') {
493
+ console.log('[VibeSurf] Page hidden, but not cleaning up (might be tab switch)');
494
+ } else if (document.visibilityState === 'visible') {
495
+ console.log('[VibeSurf] Page visible again');
496
+ }
497
+ });
498
+
464
499
  // Make app accessible for debugging
465
500
  if (typeof window !== 'undefined') {
466
501
  window.VibeSurfApp = VibeSurfApp;
@@ -482,6 +517,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
482
517
  .catch(error => sendResponse({ success: false, error: error.message }));
483
518
  return true; // Keep message channel open for async response
484
519
 
520
+ case 'MICROPHONE_PERMISSION_RESULT':
521
+ console.log('[VibeSurf] Received microphone permission result:', message);
522
+ // This message is typically handled by voice recorder, just acknowledge
523
+ sendResponse({ acknowledged: true });
524
+ break;
525
+
485
526
  default:
486
527
  console.warn('[VibeSurf] Unknown message type:', message.type);
487
528
  }