vibesurf 0.1.9a6__py3-none-any.whl → 0.1.10__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.
Files changed (41) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/vibe_surf_agent.py +25 -15
  3. vibe_surf/backend/api/browser.py +66 -0
  4. vibe_surf/backend/api/task.py +2 -1
  5. vibe_surf/backend/main.py +76 -1
  6. vibe_surf/backend/shared_state.py +2 -0
  7. vibe_surf/browser/agent_browser_session.py +312 -62
  8. vibe_surf/browser/browser_manager.py +57 -92
  9. vibe_surf/browser/watchdogs/dom_watchdog.py +43 -43
  10. vibe_surf/chrome_extension/background.js +84 -0
  11. vibe_surf/chrome_extension/manifest.json +3 -1
  12. vibe_surf/chrome_extension/scripts/file-manager.js +526 -0
  13. vibe_surf/chrome_extension/scripts/history-manager.js +658 -0
  14. vibe_surf/chrome_extension/scripts/modal-manager.js +487 -0
  15. vibe_surf/chrome_extension/scripts/session-manager.js +31 -8
  16. vibe_surf/chrome_extension/scripts/settings-manager.js +1214 -0
  17. vibe_surf/chrome_extension/scripts/ui-manager.js +770 -3186
  18. vibe_surf/chrome_extension/sidepanel.html +27 -4
  19. vibe_surf/chrome_extension/styles/activity.css +574 -0
  20. vibe_surf/chrome_extension/styles/base.css +76 -0
  21. vibe_surf/chrome_extension/styles/history-modal.css +791 -0
  22. vibe_surf/chrome_extension/styles/input.css +429 -0
  23. vibe_surf/chrome_extension/styles/layout.css +186 -0
  24. vibe_surf/chrome_extension/styles/responsive.css +454 -0
  25. vibe_surf/chrome_extension/styles/settings-environment.css +165 -0
  26. vibe_surf/chrome_extension/styles/settings-forms.css +389 -0
  27. vibe_surf/chrome_extension/styles/settings-modal.css +141 -0
  28. vibe_surf/chrome_extension/styles/settings-profiles.css +244 -0
  29. vibe_surf/chrome_extension/styles/settings-responsive.css +144 -0
  30. vibe_surf/chrome_extension/styles/settings-utilities.css +25 -0
  31. vibe_surf/chrome_extension/styles/variables.css +54 -0
  32. vibe_surf/cli.py +1 -0
  33. vibe_surf/controller/vibesurf_tools.py +0 -2
  34. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/METADATA +18 -2
  35. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/RECORD +39 -23
  36. vibe_surf/chrome_extension/styles/main.css +0 -2338
  37. vibe_surf/chrome_extension/styles/settings.css +0 -1100
  38. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/WHEEL +0 -0
  39. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/entry_points.txt +0 -0
  40. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/licenses/LICENSE +0 -0
  41. {vibesurf-0.1.9a6.dist-info → vibesurf-0.1.10.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,526 @@
1
+ // File Manager - Handles file uploads, file links, and file operations
2
+ // Manages file selection, upload, display, and file:// protocol handling
3
+
4
+ class VibeSurfFileManager {
5
+ constructor(sessionManager) {
6
+ this.sessionManager = sessionManager;
7
+ this.state = {
8
+ uploadedFiles: [],
9
+ isHandlingFileLink: false
10
+ };
11
+ this.elements = {};
12
+ this.eventListeners = new Map();
13
+
14
+ this.bindElements();
15
+ this.bindEvents();
16
+ }
17
+
18
+ bindElements() {
19
+ this.elements = {
20
+ // File input and buttons
21
+ attachFileBtn: document.getElementById('attach-file-btn'),
22
+ fileInput: document.getElementById('file-input'),
23
+
24
+ // File list container (created dynamically)
25
+ uploadedFilesList: null
26
+ };
27
+ }
28
+
29
+ bindEvents() {
30
+ // File attachment
31
+ this.elements.attachFileBtn?.addEventListener('click', this.handleAttachFiles.bind(this));
32
+ this.elements.fileInput?.addEventListener('change', this.handleFileSelection.bind(this));
33
+
34
+ // File link handling with delegation
35
+ document.addEventListener('click', this.handleFileLinkClick.bind(this));
36
+
37
+ // Listen for files uploaded event from session manager
38
+ if (this.sessionManager) {
39
+ this.sessionManager.on('filesUploaded', this.handleFilesUploaded.bind(this));
40
+ }
41
+ }
42
+
43
+ // Event system for communicating with main UI manager
44
+ on(event, callback) {
45
+ if (!this.eventListeners.has(event)) {
46
+ this.eventListeners.set(event, []);
47
+ }
48
+ this.eventListeners.get(event).push(callback);
49
+ }
50
+
51
+ emit(event, data) {
52
+ if (this.eventListeners.has(event)) {
53
+ this.eventListeners.get(event).forEach(callback => {
54
+ try {
55
+ callback(data);
56
+ } catch (error) {
57
+ console.error(`[FileManager] Event callback error for ${event}:`, error);
58
+ }
59
+ });
60
+ }
61
+ }
62
+
63
+ // File Upload Handling
64
+ handleAttachFiles() {
65
+ this.elements.fileInput?.click();
66
+ }
67
+
68
+ async handleFileSelection(event) {
69
+ const files = Array.from(event.target.files);
70
+
71
+ if (files.length === 0) return;
72
+
73
+ try {
74
+ this.emit('loading', { message: `Uploading ${files.length} file(s)...` });
75
+
76
+ const response = await this.sessionManager.uploadFiles(files);
77
+
78
+ console.log('[FileManager] File upload response:', response);
79
+
80
+ // If SessionManager doesn't trigger the event, handle it directly
81
+ if (response && response.files) {
82
+ console.log('[FileManager] Manually handling uploaded files');
83
+ this.handleFilesUploaded({
84
+ sessionId: this.sessionManager.getCurrentSessionId(),
85
+ files: response.files
86
+ });
87
+ }
88
+
89
+ this.emit('loading', { hide: true });
90
+ this.emit('notification', {
91
+ message: `${files.length} file(s) uploaded successfully`,
92
+ type: 'success'
93
+ });
94
+
95
+ // Clear file input
96
+ event.target.value = '';
97
+ } catch (error) {
98
+ this.emit('loading', { hide: true });
99
+ this.emit('notification', {
100
+ message: `File upload failed: ${error.message}`,
101
+ type: 'error'
102
+ });
103
+ }
104
+ }
105
+
106
+ handleFilesUploaded(data) {
107
+ console.log('[FileManager] Files uploaded event received:', data);
108
+
109
+ // Ensure data.files is always an array - handle both single file and array cases
110
+ let filesArray = [];
111
+ if (data.files) {
112
+ if (Array.isArray(data.files)) {
113
+ filesArray = data.files;
114
+ } else {
115
+ // If single file object, wrap in array
116
+ filesArray = [data.files];
117
+ console.log('[FileManager] Single file detected, wrapping in array');
118
+ }
119
+ }
120
+
121
+ console.log('[FileManager] Processing files array:', filesArray);
122
+
123
+ if (filesArray.length > 0) {
124
+ // Append new files to existing uploaded files (for multiple uploads)
125
+ const newFiles = filesArray.map(file => ({
126
+ id: file.file_id,
127
+ name: file.original_filename,
128
+ path: file.file_path, // Updated to use file_path field
129
+ size: file.file_size,
130
+ type: file.mime_type,
131
+ stored_filename: file.stored_filename,
132
+ file_path: file.file_path // Add file_path for backward compatibility
133
+ }));
134
+
135
+ console.log('[FileManager] Mapped new files:', newFiles);
136
+
137
+ // Add to existing files instead of replacing
138
+ this.state.uploadedFiles = [...this.state.uploadedFiles, ...newFiles];
139
+
140
+ console.log('[FileManager] Updated uploaded files state:', this.state.uploadedFiles);
141
+
142
+ // Update the visual file list
143
+ this.updateFilesList();
144
+ } else {
145
+ console.warn('[FileManager] No files to process in uploaded data');
146
+ }
147
+ }
148
+
149
+ // File List Management
150
+ updateFilesList() {
151
+ const container = this.getOrCreateFilesListContainer();
152
+
153
+ // Debug logging to identify the issue
154
+ console.log('[FileManager] updateFilesList called');
155
+ console.log('[FileManager] uploadedFiles type:', typeof this.state.uploadedFiles);
156
+ console.log('[FileManager] uploadedFiles isArray:', Array.isArray(this.state.uploadedFiles));
157
+ console.log('[FileManager] uploadedFiles value:', this.state.uploadedFiles);
158
+
159
+ // Ensure uploadedFiles is always an array
160
+ if (!Array.isArray(this.state.uploadedFiles)) {
161
+ console.error('[FileManager] uploadedFiles is not an array, resetting to empty array');
162
+ this.state.uploadedFiles = [];
163
+ }
164
+
165
+ if (this.state.uploadedFiles.length === 0) {
166
+ container.style.display = 'none';
167
+ return;
168
+ }
169
+
170
+ container.style.display = 'block';
171
+
172
+ // Build HTML safely with proper validation
173
+ let filesHTML = '';
174
+ try {
175
+ filesHTML = this.state.uploadedFiles.map((file, index) => {
176
+ console.log(`[FileManager] Processing file ${index}:`, file);
177
+
178
+ // Validate file object structure
179
+ if (!file || typeof file !== 'object') {
180
+ console.error(`[FileManager] Invalid file object at index ${index}:`, file);
181
+ return '';
182
+ }
183
+
184
+ // Extract properties safely with fallbacks
185
+ const fileId = file.id || file.file_id || `file_${index}`;
186
+ const fileName = file.name || file.original_filename || 'Unknown file';
187
+ const filePath = file.path || file.file_path || file.stored_filename || 'Unknown path';
188
+
189
+ console.log(`[FileManager] File display data: id=${fileId}, name=${fileName}, path=${filePath}`);
190
+
191
+ return `
192
+ <div class="file-item" data-file-id="${fileId}">
193
+ <span class="file-name" title="${filePath}">${fileName}</span>
194
+ <button class="file-remove-btn" title="Remove file" data-file-id="${fileId}">×</button>
195
+ </div>
196
+ `;
197
+ }).join('');
198
+ } catch (error) {
199
+ console.error('[FileManager] Error generating files HTML:', error);
200
+ filesHTML = '<div class="error-message">Error displaying files</div>';
201
+ }
202
+
203
+ container.innerHTML = `
204
+ <div class="files-items">
205
+ ${filesHTML}
206
+ </div>
207
+ `;
208
+
209
+ // Add event listeners for remove buttons
210
+ container.querySelectorAll('.file-remove-btn').forEach(btn => {
211
+ btn.addEventListener('click', (e) => {
212
+ e.preventDefault();
213
+ const fileId = btn.dataset.fileId;
214
+ this.removeUploadedFile(fileId);
215
+ });
216
+ });
217
+ }
218
+
219
+ getOrCreateFilesListContainer() {
220
+ let container = document.getElementById('uploaded-files-list');
221
+
222
+ if (!container) {
223
+ container = document.createElement('div');
224
+ container.id = 'uploaded-files-list';
225
+ container.className = 'uploaded-files-container';
226
+
227
+ // Insert after the textarea-container to avoid affecting button layout
228
+ const taskInput = document.getElementById('task-input');
229
+ if (taskInput) {
230
+ const textareaContainer = taskInput.closest('.textarea-container');
231
+ if (textareaContainer && textareaContainer.parentElement) {
232
+ // Insert after the textarea-container but before the input-footer
233
+ const inputFooter = textareaContainer.parentElement.querySelector('.input-footer');
234
+ if (inputFooter) {
235
+ textareaContainer.parentElement.insertBefore(container, inputFooter);
236
+ } else {
237
+ textareaContainer.parentElement.insertBefore(container, textareaContainer.nextSibling);
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ return container;
244
+ }
245
+
246
+ removeUploadedFile(fileId) {
247
+ console.log('[FileManager] Removing uploaded file:', fileId);
248
+
249
+ // Remove from state
250
+ this.state.uploadedFiles = this.state.uploadedFiles.filter(file => file.id !== fileId);
251
+
252
+ // Update visual list
253
+ this.updateFilesList();
254
+
255
+ this.emit('notification', {
256
+ message: 'File removed from upload list',
257
+ type: 'info'
258
+ });
259
+ }
260
+
261
+ clearUploadedFiles() {
262
+ this.state.uploadedFiles = [];
263
+ this.updateFilesList();
264
+ }
265
+
266
+ // File Link Handling
267
+ handleFileLinkClick(event) {
268
+ const target = event.target;
269
+
270
+ // Check if clicked element is a file link
271
+ if (target.matches('a.file-link') || target.closest('a.file-link')) {
272
+ event.preventDefault();
273
+
274
+ const fileLink = target.matches('a.file-link') ? target : target.closest('a.file-link');
275
+ const filePath = fileLink.getAttribute('data-file-path');
276
+
277
+ this.handleFileLink(filePath);
278
+ }
279
+ }
280
+
281
+ async handleFileLink(filePath) {
282
+ // Prevent multiple simultaneous calls
283
+ if (this.state.isHandlingFileLink) {
284
+ return;
285
+ }
286
+
287
+ this.state.isHandlingFileLink = true;
288
+
289
+ try {
290
+ // First decode the URL-encoded path
291
+ let decodedPath = decodeURIComponent(filePath);
292
+
293
+ // Remove file:// protocol prefix and normalize
294
+ let cleanPath = decodedPath.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '');
295
+
296
+ // Ensure path starts with / for Unix paths if not Windows drive
297
+ if (!cleanPath.startsWith('/') && !cleanPath.match(/^[A-Za-z]:/)) {
298
+ cleanPath = '/' + cleanPath;
299
+ }
300
+
301
+ // Convert all backslashes to forward slashes
302
+ cleanPath = cleanPath.replace(/\\/g, '/');
303
+
304
+ // Create proper file URL - always use triple slash for proper format
305
+ const fileUrl = cleanPath.match(/^[A-Za-z]:/) ?
306
+ `file:///${cleanPath}` :
307
+ `file:///${cleanPath.replace(/^\//, '')}`; // Remove leading slash and add triple slash
308
+
309
+ // Create Windows format path for system open
310
+ const windowsPath = cleanPath.replace(/\//g, '\\');
311
+
312
+ // Show user notification about the action
313
+ this.emit('notification', {
314
+ message: `Opening file: ${cleanPath}`,
315
+ type: 'info'
316
+ });
317
+
318
+ // Use setTimeout to prevent UI blocking
319
+ setTimeout(async () => {
320
+ try {
321
+ // Primary strategy: Try browser open first for HTML files (more reliable)
322
+ if (fileUrl.toLowerCase().endsWith('.html') || fileUrl.toLowerCase().endsWith('.htm')) {
323
+ try {
324
+ const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
325
+ if (opened) {
326
+ this.emit('notification', {
327
+ message: 'File opened in browser',
328
+ type: 'success'
329
+ });
330
+ return;
331
+ } else {
332
+ // If browser is blocked, try system open
333
+ await this.trySystemOpen(windowsPath, fileUrl);
334
+ return;
335
+ }
336
+ } catch (browserError) {
337
+ await this.trySystemOpen(windowsPath, fileUrl);
338
+ return;
339
+ }
340
+ } else {
341
+ // For non-HTML files, try system open first
342
+ const systemSuccess = await this.trySystemOpen(windowsPath, fileUrl);
343
+ if (systemSuccess) return;
344
+
345
+ // Fallback to browser if system open fails
346
+ try {
347
+ const opened = window.open(fileUrl, '_blank', 'noopener,noreferrer');
348
+ if (opened) {
349
+ this.emit('notification', {
350
+ message: 'File opened in browser',
351
+ type: 'success'
352
+ });
353
+ return;
354
+ }
355
+ } catch (browserError) {
356
+ console.error('[FileManager] Failed to open file:', browserError);
357
+ }
358
+ }
359
+
360
+ // Last resort: Copy path to clipboard
361
+ this.copyToClipboardFallback(fileUrl);
362
+
363
+ } catch (error) {
364
+ console.error('[FileManager] Error in async file handling:', error);
365
+ this.emit('notification', {
366
+ message: `Unable to open file: ${error.message}`,
367
+ type: 'error'
368
+ });
369
+ } finally {
370
+ this.state.isHandlingFileLink = false;
371
+ }
372
+ }, 50); // Small delay to prevent UI blocking
373
+
374
+ } catch (error) {
375
+ console.error('[FileManager] Error handling file link:', error);
376
+ this.emit('notification', {
377
+ message: `Unable to open file: ${error.message}`,
378
+ type: 'error'
379
+ });
380
+ this.state.isHandlingFileLink = false;
381
+ }
382
+ }
383
+
384
+ async trySystemOpen(windowsPath, fileUrl) {
385
+ try {
386
+ const systemOpenPromise = chrome.runtime.sendMessage({
387
+ type: 'OPEN_FILE_SYSTEM',
388
+ data: { filePath: windowsPath }
389
+ });
390
+
391
+ // Add timeout to prevent hanging
392
+ const systemOpenResponse = await Promise.race([
393
+ systemOpenPromise,
394
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000))
395
+ ]);
396
+
397
+ if (systemOpenResponse && systemOpenResponse.success) {
398
+ this.emit('notification', {
399
+ message: 'File opened with system default application',
400
+ type: 'success'
401
+ });
402
+ return true;
403
+ }
404
+ return false;
405
+ } catch (systemError) {
406
+ return false;
407
+ }
408
+ }
409
+
410
+ async copyToClipboardFallback(fileUrl) {
411
+ try {
412
+ await navigator.clipboard.writeText(fileUrl);
413
+ this.emit('notification', {
414
+ message: 'File URL copied to clipboard - paste in browser address bar',
415
+ type: 'info'
416
+ });
417
+ } catch (clipboardError) {
418
+ console.error('[FileManager] Clipboard failed:', clipboardError);
419
+ this.emit('notification', {
420
+ message: 'Unable to open file. URL: ' + fileUrl,
421
+ type: 'warning'
422
+ });
423
+ }
424
+ }
425
+
426
+ // Utility Methods
427
+ formatFileSize(bytes) {
428
+ if (!bytes) return '0 B';
429
+
430
+ const k = 1024;
431
+ const sizes = ['B', 'KB', 'MB', 'GB'];
432
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
433
+
434
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
435
+ }
436
+
437
+ escapeHtml(text) {
438
+ if (typeof text !== 'string') return '';
439
+ const div = document.createElement('div');
440
+ div.textContent = text;
441
+ return div.innerHTML;
442
+ }
443
+
444
+ // Public interface
445
+ getUploadedFiles() {
446
+ return [...this.state.uploadedFiles]; // Return copy
447
+ }
448
+
449
+ getUploadedFilesForTask() {
450
+ if (this.state.uploadedFiles.length === 0) {
451
+ return null;
452
+ }
453
+
454
+ console.log('[FileManager] Raw uploaded files state:', this.state.uploadedFiles);
455
+
456
+ // Extract the first file path (backend expects single string)
457
+ const firstFile = this.state.uploadedFiles[0];
458
+ let filePath = null;
459
+
460
+ if (typeof firstFile === 'string') {
461
+ filePath = firstFile;
462
+ } else if (firstFile && typeof firstFile === 'object') {
463
+ // Extract path and normalize
464
+ filePath = firstFile.file_path || firstFile.path || firstFile.stored_filename || firstFile.file_path;
465
+ if (filePath) {
466
+ filePath = filePath.replace(/\\/g, '/');
467
+ console.log('[FileManager] Normalized file path:', filePath);
468
+ }
469
+ }
470
+
471
+ if (filePath) {
472
+ // Show info if multiple files uploaded but only first will be processed
473
+ if (this.state.uploadedFiles.length > 1) {
474
+ console.warn('[FileManager] Multiple files uploaded, but backend only supports single file. Using first file:', filePath);
475
+ this.emit('notification', {
476
+ message: `Multiple files uploaded. Only the first file "${firstFile.name || filePath}" will be processed.`,
477
+ type: 'warning'
478
+ });
479
+ }
480
+
481
+ return filePath;
482
+ } else {
483
+ console.error('[FileManager] Could not extract file path from uploaded file:', firstFile);
484
+ return null;
485
+ }
486
+ }
487
+
488
+ hasUploadedFiles() {
489
+ return this.state.uploadedFiles.length > 0;
490
+ }
491
+
492
+ // Enable/disable file attachment based on task running state
493
+ setEnabled(enabled) {
494
+ if (this.elements.attachFileBtn) {
495
+ this.elements.attachFileBtn.disabled = !enabled;
496
+
497
+ if (enabled) {
498
+ this.elements.attachFileBtn.classList.remove('task-running-disabled');
499
+ this.elements.attachFileBtn.removeAttribute('title');
500
+ } else {
501
+ this.elements.attachFileBtn.classList.add('task-running-disabled');
502
+ this.elements.attachFileBtn.setAttribute('title', 'Disabled while task is running');
503
+ }
504
+ }
505
+ }
506
+
507
+ // Get current state
508
+ getState() {
509
+ return { ...this.state };
510
+ }
511
+
512
+ // Update session manager reference
513
+ setSessionManager(sessionManager) {
514
+ this.sessionManager = sessionManager;
515
+
516
+ // Re-bind files uploaded event
517
+ if (this.sessionManager) {
518
+ this.sessionManager.on('filesUploaded', this.handleFilesUploaded.bind(this));
519
+ }
520
+ }
521
+ }
522
+
523
+ // Export for use in other modules
524
+ if (typeof window !== 'undefined') {
525
+ window.VibeSurfFileManager = VibeSurfFileManager;
526
+ }