vibesurf 0.1.10__py3-none-any.whl → 0.1.12__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.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +68 -45
- vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
- vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
- vibe_surf/agents/report_writer_agent.py +380 -226
- vibe_surf/agents/vibe_surf_agent.py +880 -825
- vibe_surf/agents/views.py +130 -0
- vibe_surf/backend/api/activity.py +3 -1
- vibe_surf/backend/api/browser.py +9 -5
- vibe_surf/backend/api/config.py +8 -5
- vibe_surf/backend/api/files.py +59 -50
- vibe_surf/backend/api/models.py +2 -2
- vibe_surf/backend/api/task.py +46 -13
- vibe_surf/backend/database/manager.py +24 -18
- vibe_surf/backend/database/queries.py +199 -192
- vibe_surf/backend/database/schemas.py +1 -1
- vibe_surf/backend/main.py +4 -2
- vibe_surf/backend/shared_state.py +28 -35
- vibe_surf/backend/utils/encryption.py +3 -1
- vibe_surf/backend/utils/llm_factory.py +41 -36
- vibe_surf/browser/agent_browser_session.py +0 -4
- vibe_surf/browser/browser_manager.py +14 -8
- vibe_surf/browser/utils.py +5 -3
- vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
- vibe_surf/chrome_extension/background.js +4 -0
- vibe_surf/chrome_extension/scripts/api-client.js +13 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
- vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
- vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
- vibe_surf/chrome_extension/sidepanel.html +21 -4
- vibe_surf/chrome_extension/styles/activity.css +365 -5
- vibe_surf/chrome_extension/styles/input.css +139 -0
- vibe_surf/cli.py +5 -22
- vibe_surf/common.py +35 -0
- vibe_surf/llm/openai_compatible.py +217 -99
- vibe_surf/logger.py +99 -0
- vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
- vibe_surf/tools/file_system.py +437 -0
- vibe_surf/{controller → tools}/mcp_client.py +4 -3
- vibe_surf/tools/report_writer_tools.py +21 -0
- vibe_surf/tools/vibesurf_tools.py +657 -0
- vibe_surf/tools/views.py +120 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/METADATA +6 -2
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/RECORD +49 -43
- vibe_surf/controller/file_system.py +0 -53
- vibe_surf/controller/views.py +0 -37
- /vibe_surf/{controller → tools}/__init__.py +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ class VibeSurfUIManager {
|
|
|
19
19
|
this.modalManager = null;
|
|
20
20
|
|
|
21
21
|
this.bindElements();
|
|
22
|
+
this.initializeTabSelector(); // Initialize tab selector before binding events
|
|
22
23
|
this.initializeManagers();
|
|
23
24
|
this.bindEvents();
|
|
24
25
|
this.setupSessionListeners();
|
|
@@ -50,6 +51,13 @@ class VibeSurfUIManager {
|
|
|
50
51
|
taskInput: document.getElementById('task-input'),
|
|
51
52
|
sendBtn: document.getElementById('send-btn'),
|
|
52
53
|
|
|
54
|
+
// Tab selector elements
|
|
55
|
+
tabSelectorDropdown: document.getElementById('tab-selector-dropdown'),
|
|
56
|
+
tabSelectorCancel: document.getElementById('tab-selector-cancel'),
|
|
57
|
+
tabSelectorConfirm: document.getElementById('tab-selector-confirm'),
|
|
58
|
+
selectAllTabs: document.getElementById('select-all-tabs'),
|
|
59
|
+
tabOptionsList: document.getElementById('tab-options-list'),
|
|
60
|
+
|
|
53
61
|
// Loading
|
|
54
62
|
loadingOverlay: document.getElementById('loading-overlay')
|
|
55
63
|
};
|
|
@@ -289,11 +297,15 @@ class VibeSurfUIManager {
|
|
|
289
297
|
|
|
290
298
|
handleTaskPaused(data) {
|
|
291
299
|
this.updateControlPanel('paused');
|
|
300
|
+
// Force update UI for paused state - explicitly enable input
|
|
301
|
+
this.forceUpdateUIForPausedState();
|
|
292
302
|
this.showNotification('Task paused successfully', 'info');
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
handleTaskResumed(data) {
|
|
296
306
|
this.updateControlPanel('running');
|
|
307
|
+
// Force update UI for running state - disable input
|
|
308
|
+
this.forceUpdateUIForRunningState();
|
|
297
309
|
this.showNotification('Task resumed successfully', 'info');
|
|
298
310
|
}
|
|
299
311
|
|
|
@@ -402,26 +414,36 @@ class VibeSurfUIManager {
|
|
|
402
414
|
}
|
|
403
415
|
|
|
404
416
|
updateUIForTaskStatus(isRunning) {
|
|
417
|
+
const taskStatus = this.sessionManager.getTaskStatus();
|
|
418
|
+
const isPaused = taskStatus === 'paused';
|
|
419
|
+
|
|
405
420
|
// Disable/enable input elements based on task status
|
|
406
421
|
if (this.elements.taskInput) {
|
|
407
|
-
|
|
408
|
-
this.elements.taskInput.
|
|
409
|
-
|
|
410
|
-
'
|
|
422
|
+
// Allow input when paused or not running
|
|
423
|
+
this.elements.taskInput.disabled = isRunning && !isPaused;
|
|
424
|
+
if (isPaused) {
|
|
425
|
+
this.elements.taskInput.placeholder = 'Add additional information or guidance...';
|
|
426
|
+
} else if (isRunning) {
|
|
427
|
+
this.elements.taskInput.placeholder = 'Task is running - please wait...';
|
|
428
|
+
} else {
|
|
429
|
+
this.elements.taskInput.placeholder = 'Enter your task description...';
|
|
430
|
+
}
|
|
411
431
|
}
|
|
412
432
|
|
|
413
433
|
if (this.elements.sendBtn) {
|
|
414
|
-
|
|
434
|
+
// Enable send button when paused or when can submit new task
|
|
435
|
+
this.elements.sendBtn.disabled = (isRunning && !isPaused) || (!isPaused && !this.canSubmitTask()) || (isPaused && !this.canAddNewTask());
|
|
415
436
|
}
|
|
416
437
|
|
|
417
438
|
if (this.elements.llmProfileSelect) {
|
|
418
|
-
|
|
439
|
+
// Allow LLM profile change only when not running
|
|
440
|
+
this.elements.llmProfileSelect.disabled = isRunning && !isPaused;
|
|
419
441
|
}
|
|
420
442
|
|
|
421
|
-
// Update file manager state
|
|
443
|
+
// Update file manager state - keep disabled during pause (as per requirement)
|
|
422
444
|
this.fileManager.setEnabled(!isRunning);
|
|
423
445
|
|
|
424
|
-
// Also disable header buttons when task is running
|
|
446
|
+
// Also disable header buttons when task is running (but not when paused)
|
|
425
447
|
const headerButtons = [
|
|
426
448
|
this.elements.newSessionBtn,
|
|
427
449
|
this.elements.historyBtn,
|
|
@@ -430,8 +452,9 @@ class VibeSurfUIManager {
|
|
|
430
452
|
|
|
431
453
|
headerButtons.forEach(button => {
|
|
432
454
|
if (button) {
|
|
433
|
-
|
|
434
|
-
|
|
455
|
+
const shouldDisable = isRunning && !isPaused;
|
|
456
|
+
button.disabled = shouldDisable;
|
|
457
|
+
if (shouldDisable) {
|
|
435
458
|
button.classList.add('task-running-disabled');
|
|
436
459
|
button.setAttribute('title', 'Disabled while task is running');
|
|
437
460
|
} else {
|
|
@@ -442,6 +465,80 @@ class VibeSurfUIManager {
|
|
|
442
465
|
});
|
|
443
466
|
}
|
|
444
467
|
|
|
468
|
+
forceUpdateUIForPausedState() {
|
|
469
|
+
console.log('[UIManager] Force updating UI for paused state');
|
|
470
|
+
|
|
471
|
+
// Enable input during pause
|
|
472
|
+
if (this.elements.taskInput) {
|
|
473
|
+
this.elements.taskInput.disabled = false;
|
|
474
|
+
this.elements.taskInput.placeholder = 'Add additional information or guidance...';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (this.elements.sendBtn) {
|
|
478
|
+
const hasText = this.elements.taskInput?.value.trim().length > 0;
|
|
479
|
+
this.elements.sendBtn.disabled = !hasText;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Keep LLM profile disabled during pause (user doesn't need to change it)
|
|
483
|
+
if (this.elements.llmProfileSelect) {
|
|
484
|
+
this.elements.llmProfileSelect.disabled = true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Keep file manager disabled during pause
|
|
488
|
+
this.fileManager.setEnabled(false);
|
|
489
|
+
|
|
490
|
+
// Keep header buttons disabled during pause (only input and send should be available)
|
|
491
|
+
const headerButtons = [
|
|
492
|
+
this.elements.newSessionBtn,
|
|
493
|
+
this.elements.historyBtn,
|
|
494
|
+
this.elements.settingsBtn
|
|
495
|
+
];
|
|
496
|
+
|
|
497
|
+
headerButtons.forEach(button => {
|
|
498
|
+
if (button) {
|
|
499
|
+
button.disabled = true;
|
|
500
|
+
button.classList.add('task-running-disabled');
|
|
501
|
+
button.setAttribute('title', 'Disabled during pause - only input and send are available');
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
forceUpdateUIForRunningState() {
|
|
507
|
+
console.log('[UIManager] Force updating UI for running state');
|
|
508
|
+
|
|
509
|
+
// Disable input during running
|
|
510
|
+
if (this.elements.taskInput) {
|
|
511
|
+
this.elements.taskInput.disabled = true;
|
|
512
|
+
this.elements.taskInput.placeholder = 'Task is running - please wait...';
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (this.elements.sendBtn) {
|
|
516
|
+
this.elements.sendBtn.disabled = true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (this.elements.llmProfileSelect) {
|
|
520
|
+
this.elements.llmProfileSelect.disabled = true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Update file manager state
|
|
524
|
+
this.fileManager.setEnabled(false);
|
|
525
|
+
|
|
526
|
+
// Disable header buttons when task is running
|
|
527
|
+
const headerButtons = [
|
|
528
|
+
this.elements.newSessionBtn,
|
|
529
|
+
this.elements.historyBtn,
|
|
530
|
+
this.elements.settingsBtn
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
headerButtons.forEach(button => {
|
|
534
|
+
if (button) {
|
|
535
|
+
button.disabled = true;
|
|
536
|
+
button.classList.add('task-running-disabled');
|
|
537
|
+
button.setAttribute('title', 'Disabled while task is running');
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
445
542
|
canSubmitTask() {
|
|
446
543
|
const hasText = this.elements.taskInput?.value.trim().length > 0;
|
|
447
544
|
const llmProfile = this.elements.llmProfileSelect?.value;
|
|
@@ -449,6 +546,11 @@ class VibeSurfUIManager {
|
|
|
449
546
|
return hasText && hasLlmProfile && !this.state.isTaskRunning;
|
|
450
547
|
}
|
|
451
548
|
|
|
549
|
+
canAddNewTask() {
|
|
550
|
+
const hasText = this.elements.taskInput?.value.trim().length > 0;
|
|
551
|
+
return hasText;
|
|
552
|
+
}
|
|
553
|
+
|
|
452
554
|
async showTaskRunningWarning(action) {
|
|
453
555
|
const taskInfo = this.state.taskInfo;
|
|
454
556
|
const taskId = taskInfo?.task_id || 'unknown';
|
|
@@ -535,40 +637,59 @@ class VibeSurfUIManager {
|
|
|
535
637
|
}
|
|
536
638
|
|
|
537
639
|
async handleSendTask() {
|
|
538
|
-
// Check if task is already running with enhanced blocking
|
|
539
|
-
const statusCheck = await this.checkTaskStatus();
|
|
540
|
-
if (statusCheck.isRunning) {
|
|
541
|
-
const canProceed = await this.showTaskRunningWarning('send a new task');
|
|
542
|
-
if (!canProceed) {
|
|
543
|
-
this.showNotification('Cannot send task while another task is running. Please stop the current task first.', 'warning');
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
640
|
const taskDescription = this.elements.taskInput?.value.trim();
|
|
549
|
-
const
|
|
641
|
+
const taskStatus = this.sessionManager.getTaskStatus();
|
|
642
|
+
const isPaused = taskStatus === 'paused';
|
|
550
643
|
|
|
551
644
|
if (!taskDescription) {
|
|
552
645
|
this.showNotification('Please enter a task description', 'warning');
|
|
553
646
|
this.elements.taskInput?.focus();
|
|
554
647
|
return;
|
|
555
648
|
}
|
|
556
|
-
|
|
557
|
-
// Check if LLM profile is selected
|
|
558
|
-
if (!llmProfile || llmProfile.trim() === '') {
|
|
559
|
-
// Check if there are any LLM profiles available
|
|
560
|
-
const profiles = this.settingsManager.getLLMProfiles();
|
|
561
|
-
if (profiles.length === 0) {
|
|
562
|
-
// No LLM profiles configured at all
|
|
563
|
-
this.showLLMProfileRequiredModal('configure');
|
|
564
|
-
} else {
|
|
565
|
-
// LLM profiles exist but none selected
|
|
566
|
-
this.showLLMProfileRequiredModal('select');
|
|
567
|
-
}
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
649
|
+
|
|
571
650
|
try {
|
|
651
|
+
if (isPaused) {
|
|
652
|
+
// Handle adding new task to paused execution
|
|
653
|
+
console.log('[UIManager] Adding new task to paused execution:', taskDescription);
|
|
654
|
+
await this.sessionManager.addNewTaskToPaused(taskDescription);
|
|
655
|
+
|
|
656
|
+
// Clear the input after successful addition
|
|
657
|
+
this.clearTaskInput();
|
|
658
|
+
this.showNotification('Additional information added to the task', 'success');
|
|
659
|
+
|
|
660
|
+
// Automatically resume the task after adding new information
|
|
661
|
+
console.log('[UIManager] Auto-resuming task after adding new information');
|
|
662
|
+
await this.sessionManager.resumeTask('Auto-resume after adding new task information');
|
|
663
|
+
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Original logic for new task submission
|
|
668
|
+
const statusCheck = await this.checkTaskStatus();
|
|
669
|
+
if (statusCheck.isRunning) {
|
|
670
|
+
const canProceed = await this.showTaskRunningWarning('send a new task');
|
|
671
|
+
if (!canProceed) {
|
|
672
|
+
this.showNotification('Cannot send task while another task is running. Please stop the current task first.', 'warning');
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const llmProfile = this.elements.llmProfileSelect?.value;
|
|
678
|
+
|
|
679
|
+
// Check if LLM profile is selected
|
|
680
|
+
if (!llmProfile || llmProfile.trim() === '') {
|
|
681
|
+
// Check if there are any LLM profiles available
|
|
682
|
+
const profiles = this.settingsManager.getLLMProfiles();
|
|
683
|
+
if (profiles.length === 0) {
|
|
684
|
+
// No LLM profiles configured at all
|
|
685
|
+
this.showLLMProfileRequiredModal('configure');
|
|
686
|
+
} else {
|
|
687
|
+
// LLM profiles exist but none selected
|
|
688
|
+
this.showLLMProfileRequiredModal('select');
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
|
|
572
693
|
// Immediately clear welcome message and show user request
|
|
573
694
|
this.clearWelcomeMessage();
|
|
574
695
|
|
|
@@ -584,6 +705,13 @@ class VibeSurfUIManager {
|
|
|
584
705
|
console.log('[UIManager] Set upload_files_path to:', filePath);
|
|
585
706
|
}
|
|
586
707
|
|
|
708
|
+
// Add selected tabs information if any
|
|
709
|
+
const selectedTabsData = this.getSelectedTabsForTask();
|
|
710
|
+
if (selectedTabsData) {
|
|
711
|
+
taskData.selected_tabs = selectedTabsData;
|
|
712
|
+
console.log('[UIManager] Set selected_tabs to:', selectedTabsData);
|
|
713
|
+
}
|
|
714
|
+
|
|
587
715
|
console.log('[UIManager] Complete task data being submitted:', JSON.stringify(taskData, null, 2));
|
|
588
716
|
await this.sessionManager.submitTask(taskData);
|
|
589
717
|
|
|
@@ -612,9 +740,25 @@ class VibeSurfUIManager {
|
|
|
612
740
|
|
|
613
741
|
async handleTerminateTask() {
|
|
614
742
|
try {
|
|
743
|
+
// Temporarily stop task status monitoring during terminate to avoid conflicts
|
|
744
|
+
const wasMonitoring = !!this.taskStatusInterval;
|
|
745
|
+
if (wasMonitoring) {
|
|
746
|
+
this.stopTaskStatusMonitoring();
|
|
747
|
+
}
|
|
748
|
+
|
|
615
749
|
await this.sessionManager.stopTask('User clicked terminate');
|
|
750
|
+
|
|
751
|
+
// Restart monitoring after a brief delay if it was running
|
|
752
|
+
if (wasMonitoring) {
|
|
753
|
+
setTimeout(() => {
|
|
754
|
+
this.startTaskStatusMonitoring();
|
|
755
|
+
}, 1000);
|
|
756
|
+
}
|
|
616
757
|
} catch (error) {
|
|
617
|
-
|
|
758
|
+
// Only show error notification for actual failures, not status conflicts
|
|
759
|
+
if (!error.message.includes('status') && !error.message.includes('running')) {
|
|
760
|
+
this.showNotification(`Failed to terminate task: ${error.message}`, 'error');
|
|
761
|
+
}
|
|
618
762
|
}
|
|
619
763
|
}
|
|
620
764
|
|
|
@@ -622,7 +766,72 @@ class VibeSurfUIManager {
|
|
|
622
766
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
623
767
|
event.preventDefault();
|
|
624
768
|
this.handleSendTask();
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Handle tab token deletion
|
|
773
|
+
if (event.key === 'Backspace' || event.key === 'Delete') {
|
|
774
|
+
if (this.handleTabTokenDeletion(event)) {
|
|
775
|
+
event.preventDefault();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
handleTabTokenDeletion(event) {
|
|
782
|
+
const input = event.target;
|
|
783
|
+
const cursorPos = input.selectionStart;
|
|
784
|
+
const text = input.value;
|
|
785
|
+
|
|
786
|
+
// Unicode markers for tab tokens
|
|
787
|
+
const startMarker = '\u200B'; // Zero-width space
|
|
788
|
+
const endMarker = '\u200C'; // Zero-width non-joiner
|
|
789
|
+
|
|
790
|
+
let tokenStart = -1;
|
|
791
|
+
let tokenEnd = -1;
|
|
792
|
+
|
|
793
|
+
if (event.key === 'Backspace') {
|
|
794
|
+
// Only delete if cursor is directly adjacent to end of token
|
|
795
|
+
// Check if the character immediately before cursor is an endMarker
|
|
796
|
+
if (cursorPos > 0 && text[cursorPos - 1] === endMarker) {
|
|
797
|
+
tokenEnd = cursorPos; // Include the marker
|
|
798
|
+
// Find the corresponding start marker backwards
|
|
799
|
+
for (let j = cursorPos - 2; j >= 0; j--) {
|
|
800
|
+
if (text[j] === startMarker) {
|
|
801
|
+
tokenStart = j;
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} else if (event.key === 'Delete') {
|
|
807
|
+
// Only delete if cursor is directly adjacent to start of token
|
|
808
|
+
// Check if the character immediately at cursor is a startMarker
|
|
809
|
+
if (cursorPos < text.length && text[cursorPos] === startMarker) {
|
|
810
|
+
tokenStart = cursorPos;
|
|
811
|
+
// Find the corresponding end marker forwards
|
|
812
|
+
for (let j = cursorPos + 1; j < text.length; j++) {
|
|
813
|
+
if (text[j] === endMarker) {
|
|
814
|
+
tokenEnd = j + 1; // Include the marker
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// If we found a complete token, delete it
|
|
822
|
+
if (tokenStart !== -1 && tokenEnd !== -1) {
|
|
823
|
+
const beforeToken = text.substring(0, tokenStart);
|
|
824
|
+
const afterToken = text.substring(tokenEnd);
|
|
825
|
+
input.value = beforeToken + afterToken;
|
|
826
|
+
input.setSelectionRange(tokenStart, tokenStart);
|
|
827
|
+
|
|
828
|
+
// Trigger input change event for validation
|
|
829
|
+
this.handleTaskInputChange({ target: input });
|
|
830
|
+
|
|
831
|
+
return true; // Prevent default behavior
|
|
625
832
|
}
|
|
833
|
+
|
|
834
|
+
return false; // Allow default behavior
|
|
626
835
|
}
|
|
627
836
|
|
|
628
837
|
handleLlmProfileChange(event) {
|
|
@@ -633,14 +842,27 @@ class VibeSurfUIManager {
|
|
|
633
842
|
}
|
|
634
843
|
|
|
635
844
|
handleTaskInputChange(event) {
|
|
845
|
+
console.log('[UIManager] handleTaskInputChange called with value:', event.target.value);
|
|
846
|
+
|
|
636
847
|
const hasText = event.target.value.trim().length > 0;
|
|
637
848
|
const textarea = event.target;
|
|
638
849
|
const llmProfile = this.elements.llmProfileSelect?.value;
|
|
639
850
|
const hasLlmProfile = llmProfile && llmProfile.trim() !== '';
|
|
851
|
+
const taskStatus = this.sessionManager.getTaskStatus();
|
|
852
|
+
const isPaused = taskStatus === 'paused';
|
|
640
853
|
|
|
641
|
-
//
|
|
854
|
+
// Check for @ character to trigger tab selector
|
|
855
|
+
this.handleTabSelectorInput(event);
|
|
856
|
+
|
|
857
|
+
// Update send button state - special handling for pause state
|
|
642
858
|
if (this.elements.sendBtn) {
|
|
643
|
-
|
|
859
|
+
if (isPaused) {
|
|
860
|
+
// In pause state, only require text (no LLM profile needed for adding new info)
|
|
861
|
+
this.elements.sendBtn.disabled = !hasText;
|
|
862
|
+
} else {
|
|
863
|
+
// In normal state, require both text and LLM profile and no running task
|
|
864
|
+
this.elements.sendBtn.disabled = !(hasText && hasLlmProfile && !this.state.isTaskRunning);
|
|
865
|
+
}
|
|
644
866
|
}
|
|
645
867
|
|
|
646
868
|
// Auto-resize textarea based on content
|
|
@@ -846,8 +1068,6 @@ class VibeSurfUIManager {
|
|
|
846
1068
|
return;
|
|
847
1069
|
}
|
|
848
1070
|
|
|
849
|
-
const activityItem = this.createActivityItem(activityData);
|
|
850
|
-
|
|
851
1071
|
if (this.elements.activityLog) {
|
|
852
1072
|
// Remove welcome message if present
|
|
853
1073
|
const welcomeMsg = this.elements.activityLog.querySelector('.welcome-message');
|
|
@@ -855,12 +1075,137 @@ class VibeSurfUIManager {
|
|
|
855
1075
|
welcomeMsg.remove();
|
|
856
1076
|
}
|
|
857
1077
|
|
|
858
|
-
this
|
|
859
|
-
|
|
1078
|
+
// Check if this is a suggestion_tasks message
|
|
1079
|
+
if (agentStatus.toLowerCase() === 'suggestion_tasks') {
|
|
1080
|
+
// For suggestion_tasks, only show suggestion cards, not the normal message
|
|
1081
|
+
// But the message is still kept in session manager's logs for proper indexing
|
|
1082
|
+
this.addSuggestionTaskCards(activityData);
|
|
1083
|
+
} else {
|
|
1084
|
+
// For all other messages, show the normal activity item
|
|
1085
|
+
const activityItem = this.createActivityItem(activityData);
|
|
1086
|
+
this.elements.activityLog.appendChild(activityItem);
|
|
1087
|
+
activityItem.classList.add('fade-in');
|
|
1088
|
+
|
|
1089
|
+
// Bind copy button functionality
|
|
1090
|
+
this.bindCopyButtonEvent(activityItem, activityData);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
addSuggestionTaskCards(activityData) {
|
|
1096
|
+
const agentMsg = activityData.agent_msg || activityData.message || '';
|
|
1097
|
+
|
|
1098
|
+
if (!agentMsg || typeof agentMsg !== 'string') {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Parse tasks by splitting on newlines and filtering empty lines
|
|
1103
|
+
const tasks = agentMsg.split('\n')
|
|
1104
|
+
.map(task => task.trim())
|
|
1105
|
+
.filter(task => task.length > 0);
|
|
1106
|
+
|
|
1107
|
+
if (tasks.length === 0) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Create suggestion cards container
|
|
1112
|
+
const suggestionsContainer = document.createElement('div');
|
|
1113
|
+
suggestionsContainer.className = 'suggestion-tasks-container';
|
|
1114
|
+
|
|
1115
|
+
// Add header for suggestion cards
|
|
1116
|
+
const headerElement = document.createElement('div');
|
|
1117
|
+
headerElement.className = 'suggestion-tasks-header';
|
|
1118
|
+
headerElement.innerHTML = `
|
|
1119
|
+
<h4>Suggestion Follow-Up Tasks</h4>
|
|
1120
|
+
`;
|
|
1121
|
+
suggestionsContainer.appendChild(headerElement);
|
|
1122
|
+
|
|
1123
|
+
// Create cards container
|
|
1124
|
+
const cardsContainer = document.createElement('div');
|
|
1125
|
+
cardsContainer.className = 'suggestion-cards';
|
|
1126
|
+
|
|
1127
|
+
// Create individual task cards
|
|
1128
|
+
tasks.forEach((task, index) => {
|
|
1129
|
+
const taskCard = document.createElement('div');
|
|
1130
|
+
taskCard.className = 'suggestion-task-card';
|
|
1131
|
+
taskCard.setAttribute('data-task', task);
|
|
1132
|
+
taskCard.setAttribute('data-index', index);
|
|
1133
|
+
|
|
1134
|
+
taskCard.innerHTML = `
|
|
1135
|
+
<div class="suggestion-card-icon">
|
|
1136
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1137
|
+
<path d="M9 12L11 14L15 10M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1138
|
+
</svg>
|
|
1139
|
+
</div>
|
|
1140
|
+
<div class="suggestion-card-content">
|
|
1141
|
+
<div class="suggestion-task-text">${this.escapeHtml(task)}</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
<div class="suggestion-card-arrow">
|
|
1144
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
1145
|
+
<path d="M22 2L11 13M22 2L15 22L11 13M22 2L2 9L11 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1146
|
+
</svg>
|
|
1147
|
+
</div>
|
|
1148
|
+
`;
|
|
860
1149
|
|
|
861
|
-
//
|
|
862
|
-
|
|
1150
|
+
// Add click event handler
|
|
1151
|
+
taskCard.addEventListener('click', (e) => {
|
|
1152
|
+
e.preventDefault();
|
|
1153
|
+
this.handleSuggestionTaskClick(task);
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
cardsContainer.appendChild(taskCard);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
suggestionsContainer.appendChild(cardsContainer);
|
|
1160
|
+
|
|
1161
|
+
// Add the suggestions container to the activity log
|
|
1162
|
+
if (this.elements.activityLog) {
|
|
1163
|
+
this.elements.activityLog.appendChild(suggestionsContainer);
|
|
1164
|
+
suggestionsContainer.classList.add('fade-in');
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
handleSuggestionTaskClick(taskDescription) {
|
|
1169
|
+
console.log('[UIManager] Suggestion task clicked:', taskDescription);
|
|
1170
|
+
|
|
1171
|
+
// First check if task is running
|
|
1172
|
+
if (this.state.isTaskRunning) {
|
|
1173
|
+
this.showNotification('Cannot submit task while another task is running', 'warning');
|
|
1174
|
+
return;
|
|
863
1175
|
}
|
|
1176
|
+
|
|
1177
|
+
// Check if LLM profile is selected
|
|
1178
|
+
const llmProfile = this.elements.llmProfileSelect?.value;
|
|
1179
|
+
if (!llmProfile || llmProfile.trim() === '') {
|
|
1180
|
+
this.showLLMProfileRequiredModal('select');
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Set the task description in the input first
|
|
1185
|
+
if (!this.elements.taskInput) {
|
|
1186
|
+
console.error('[UIManager] Task input element not found');
|
|
1187
|
+
this.showNotification('Task input not available', 'error');
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
console.log('[UIManager] Setting task description and submitting...');
|
|
1192
|
+
this.elements.taskInput.value = taskDescription;
|
|
1193
|
+
this.elements.taskInput.focus();
|
|
1194
|
+
|
|
1195
|
+
// Trigger input change event for validation and auto-resize
|
|
1196
|
+
this.handleTaskInputChange({ target: this.elements.taskInput });
|
|
1197
|
+
|
|
1198
|
+
// Auto-submit the task after a short delay
|
|
1199
|
+
setTimeout(() => {
|
|
1200
|
+
console.log('[UIManager] Auto-submitting suggestion task...');
|
|
1201
|
+
this.handleSendTask();
|
|
1202
|
+
}, 100);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
escapeHtml(text) {
|
|
1206
|
+
const div = document.createElement('div');
|
|
1207
|
+
div.textContent = text;
|
|
1208
|
+
return div.innerHTML;
|
|
864
1209
|
}
|
|
865
1210
|
|
|
866
1211
|
createActivityItem(data) {
|
|
@@ -870,7 +1215,18 @@ class VibeSurfUIManager {
|
|
|
870
1215
|
const agentName = data.agent_name || 'system';
|
|
871
1216
|
const agentStatus = data.agent_status || data.status || 'info';
|
|
872
1217
|
const agentMsg = data.agent_msg || data.message || data.action_description || 'No description';
|
|
873
|
-
|
|
1218
|
+
|
|
1219
|
+
// Use backend timestamp if available, otherwise generate frontend timestamp
|
|
1220
|
+
let timestamp;
|
|
1221
|
+
if (data.timestamp) {
|
|
1222
|
+
timestamp = new Date(data.timestamp).toLocaleString();
|
|
1223
|
+
} else {
|
|
1224
|
+
timestamp = new Date().toLocaleString();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Extract token and cost information
|
|
1228
|
+
const totalTokens = data.total_tokens;
|
|
1229
|
+
const totalCost = data.total_cost;
|
|
874
1230
|
|
|
875
1231
|
// Determine if this is a user message (should be on the right)
|
|
876
1232
|
const isUser = agentName.toLowerCase() === 'user';
|
|
@@ -878,12 +1234,28 @@ class VibeSurfUIManager {
|
|
|
878
1234
|
// Set CSS classes based on agent type and status
|
|
879
1235
|
item.className = `activity-item ${isUser ? 'user-message' : 'agent-message'} ${agentStatus}`;
|
|
880
1236
|
|
|
1237
|
+
// Build metadata display (timestamp, tokens, cost)
|
|
1238
|
+
let metadataHtml = `<span class="message-time">${timestamp}</span>`;
|
|
1239
|
+
|
|
1240
|
+
if (totalTokens !== undefined || totalCost !== undefined) {
|
|
1241
|
+
metadataHtml += '<span class="message-metrics">';
|
|
1242
|
+
if (totalTokens !== undefined) {
|
|
1243
|
+
metadataHtml += `<span class="metric-item">tokens: ${totalTokens}</span>`;
|
|
1244
|
+
}
|
|
1245
|
+
if (totalCost !== undefined) {
|
|
1246
|
+
// Format cost to 4 decimal places
|
|
1247
|
+
const formattedCost = typeof totalCost === 'number' ? totalCost.toFixed(4) : parseFloat(totalCost || 0).toFixed(4);
|
|
1248
|
+
metadataHtml += `<span class="metric-item">cost: $${formattedCost}</span>`;
|
|
1249
|
+
}
|
|
1250
|
+
metadataHtml += '</span>';
|
|
1251
|
+
}
|
|
1252
|
+
|
|
881
1253
|
// Create the message structure similar to chat interface
|
|
882
1254
|
item.innerHTML = `
|
|
883
1255
|
<div class="message-container ${isUser ? 'user-container' : 'agent-container'}">
|
|
884
1256
|
<div class="message-header">
|
|
885
1257
|
<span class="agent-name">${agentName}</span>
|
|
886
|
-
<
|
|
1258
|
+
<div class="message-metadata">${metadataHtml}</div>
|
|
887
1259
|
</div>
|
|
888
1260
|
<div class="message-bubble ${isUser ? 'user-bubble' : 'agent-bubble'}">
|
|
889
1261
|
<div class="message-status">
|
|
@@ -1107,8 +1479,21 @@ class VibeSurfUIManager {
|
|
|
1107
1479
|
// Add task list support manually (markdown-it doesn't have built-in task lists)
|
|
1108
1480
|
formattedContent = this.preprocessTaskLists(formattedContent);
|
|
1109
1481
|
|
|
1110
|
-
// Pre-process file
|
|
1482
|
+
// Pre-process both regular file paths and file:// markdown links
|
|
1483
|
+
const regularFilePathRegex = /\[([^\]]+)\]\(([^)]*\/[^)]*\.[^)]+)\)/g;
|
|
1111
1484
|
const markdownFileLinkRegex = /\[([^\]]+)\]\((file:\/\/[^)]+)\)/g;
|
|
1485
|
+
|
|
1486
|
+
// Handle regular file paths (convert to file:// format)
|
|
1487
|
+
formattedContent = formattedContent.replace(regularFilePathRegex, (match, linkText, filePath) => {
|
|
1488
|
+
// Only process if it looks like a file path (contains / and extension) and not already a URL
|
|
1489
|
+
if (!filePath.startsWith('http') && !filePath.startsWith('file://') && (filePath.includes('/') || filePath.includes('\\'))) {
|
|
1490
|
+
const fileUrl = filePath.startsWith('/') ? `file://${filePath}` : `file:///${filePath}`;
|
|
1491
|
+
return `<a href="${fileUrl}" class="file-link-markdown">${linkText}</a>`;
|
|
1492
|
+
}
|
|
1493
|
+
return match;
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// Handle file:// links
|
|
1112
1497
|
formattedContent = formattedContent.replace(markdownFileLinkRegex, (match, linkText, fileUrl) => {
|
|
1113
1498
|
// Convert to HTML format that markdown-it will preserve
|
|
1114
1499
|
return `<a href="${fileUrl}" class="file-link-markdown">${linkText}</a>`;
|
|
@@ -1406,6 +1791,404 @@ class VibeSurfUIManager {
|
|
|
1406
1791
|
// Clear state
|
|
1407
1792
|
this.state.currentModal = null;
|
|
1408
1793
|
}
|
|
1794
|
+
|
|
1795
|
+
// Tab Selector Methods
|
|
1796
|
+
initializeTabSelector() {
|
|
1797
|
+
console.log('[UIManager] Initializing tab selector...');
|
|
1798
|
+
|
|
1799
|
+
// Initialize tab selector state
|
|
1800
|
+
this.tabSelectorState = {
|
|
1801
|
+
isVisible: false,
|
|
1802
|
+
selectedTabs: [],
|
|
1803
|
+
allTabs: [],
|
|
1804
|
+
atPosition: -1 // Position where @ was typed
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
console.log('[UIManager] Tab selector state initialized:', this.tabSelectorState);
|
|
1808
|
+
|
|
1809
|
+
// Bind tab selector events
|
|
1810
|
+
this.bindTabSelectorEvents();
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
bindTabSelectorEvents() {
|
|
1814
|
+
console.log('[UIManager] Binding tab selector events...');
|
|
1815
|
+
console.log('[UIManager] Available elements for binding:', {
|
|
1816
|
+
tabSelectorCancel: !!this.elements.tabSelectorCancel,
|
|
1817
|
+
tabSelectorConfirm: !!this.elements.tabSelectorConfirm,
|
|
1818
|
+
selectAllTabs: !!this.elements.selectAllTabs,
|
|
1819
|
+
tabSelectorDropdown: !!this.elements.tabSelectorDropdown
|
|
1820
|
+
});
|
|
1821
|
+
|
|
1822
|
+
// Select all radio button
|
|
1823
|
+
this.elements.selectAllTabs?.addEventListener('change', this.handleSelectAllTabs.bind(this));
|
|
1824
|
+
|
|
1825
|
+
// Hide on click outside
|
|
1826
|
+
document.addEventListener('click', (event) => {
|
|
1827
|
+
if (this.tabSelectorState.isVisible &&
|
|
1828
|
+
this.elements.tabSelectorDropdown &&
|
|
1829
|
+
!this.elements.tabSelectorDropdown.contains(event.target) &&
|
|
1830
|
+
!this.elements.taskInput?.contains(event.target)) {
|
|
1831
|
+
this.hideTabSelector();
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
console.log('[UIManager] Tab selector events bound successfully');
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
handleTabSelectorInput(event) {
|
|
1839
|
+
// Safety check - ensure tab selector state is initialized
|
|
1840
|
+
if (!this.tabSelectorState) {
|
|
1841
|
+
console.warn('[UIManager] Tab selector state not initialized');
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
const inputValue = event.target.value;
|
|
1846
|
+
const cursorPosition = event.target.selectionStart;
|
|
1847
|
+
|
|
1848
|
+
console.log('[UIManager] Tab selector input check:', {
|
|
1849
|
+
inputValue,
|
|
1850
|
+
cursorPosition,
|
|
1851
|
+
charAtCursor: inputValue[cursorPosition - 1],
|
|
1852
|
+
isAtSymbol: inputValue[cursorPosition - 1] === '@'
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
// Check if @ was just typed
|
|
1856
|
+
if (inputValue[cursorPosition - 1] === '@') {
|
|
1857
|
+
console.log('[UIManager] @ detected, showing tab selector');
|
|
1858
|
+
this.tabSelectorState.atPosition = cursorPosition - 1;
|
|
1859
|
+
this.showTabSelector();
|
|
1860
|
+
} else if (this.tabSelectorState.isVisible) {
|
|
1861
|
+
// Check if @ was deleted - hide tab selector immediately
|
|
1862
|
+
if (this.tabSelectorState.atPosition >= 0 &&
|
|
1863
|
+
(this.tabSelectorState.atPosition >= inputValue.length ||
|
|
1864
|
+
inputValue[this.tabSelectorState.atPosition] !== '@')) {
|
|
1865
|
+
console.log('[UIManager] @ deleted, hiding tab selector');
|
|
1866
|
+
this.hideTabSelector();
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Hide tab selector if user continues typing after @
|
|
1871
|
+
const textAfterAt = inputValue.substring(this.tabSelectorState.atPosition + 1, cursorPosition);
|
|
1872
|
+
if (textAfterAt.length > 0 && !textAfterAt.match(/^[\s]*$/)) {
|
|
1873
|
+
console.log('[UIManager] Hiding tab selector due to continued typing');
|
|
1874
|
+
this.hideTabSelector();
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async showTabSelector() {
|
|
1880
|
+
console.log('[UIManager] showTabSelector called');
|
|
1881
|
+
console.log('[UIManager] Tab selector elements:', {
|
|
1882
|
+
dropdown: !!this.elements.tabSelectorDropdown,
|
|
1883
|
+
taskInput: !!this.elements.taskInput,
|
|
1884
|
+
tabOptionsList: !!this.elements.tabOptionsList
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
if (!this.elements.tabSelectorDropdown || !this.elements.taskInput) {
|
|
1888
|
+
console.error('[UIManager] Tab selector elements not found', {
|
|
1889
|
+
dropdown: this.elements.tabSelectorDropdown,
|
|
1890
|
+
taskInput: this.elements.taskInput
|
|
1891
|
+
});
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
try {
|
|
1896
|
+
console.log('[UIManager] Fetching tab data...');
|
|
1897
|
+
// Fetch tab data from backend
|
|
1898
|
+
await this.populateTabSelector();
|
|
1899
|
+
|
|
1900
|
+
console.log('[UIManager] Positioning dropdown...');
|
|
1901
|
+
// Position the dropdown relative to the input
|
|
1902
|
+
this.positionTabSelector();
|
|
1903
|
+
|
|
1904
|
+
console.log('[UIManager] Showing dropdown...');
|
|
1905
|
+
// Show the dropdown with explicit visibility
|
|
1906
|
+
this.elements.tabSelectorDropdown.classList.remove('hidden');
|
|
1907
|
+
this.elements.tabSelectorDropdown.style.display = 'block';
|
|
1908
|
+
this.elements.tabSelectorDropdown.style.visibility = 'visible';
|
|
1909
|
+
this.elements.tabSelectorDropdown.style.opacity = '1';
|
|
1910
|
+
this.tabSelectorState.isVisible = true;
|
|
1911
|
+
|
|
1912
|
+
console.log('[UIManager] Tab selector shown successfully');
|
|
1913
|
+
console.log('[UIManager] Classes:', this.elements.tabSelectorDropdown.className);
|
|
1914
|
+
console.log('[UIManager] Computed styles:', {
|
|
1915
|
+
display: getComputedStyle(this.elements.tabSelectorDropdown).display,
|
|
1916
|
+
visibility: getComputedStyle(this.elements.tabSelectorDropdown).visibility,
|
|
1917
|
+
opacity: getComputedStyle(this.elements.tabSelectorDropdown).opacity,
|
|
1918
|
+
zIndex: getComputedStyle(this.elements.tabSelectorDropdown).zIndex,
|
|
1919
|
+
position: getComputedStyle(this.elements.tabSelectorDropdown).position
|
|
1920
|
+
});
|
|
1921
|
+
console.log('[UIManager] Dropdown content HTML:', this.elements.tabSelectorDropdown.innerHTML);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
console.error('[UIManager] Failed to show tab selector:', error);
|
|
1924
|
+
this.showNotification('Failed to load browser tabs', 'error');
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
hideTabSelector() {
|
|
1929
|
+
if (this.elements.tabSelectorDropdown) {
|
|
1930
|
+
this.elements.tabSelectorDropdown.classList.add('hidden');
|
|
1931
|
+
this.elements.tabSelectorDropdown.style.display = 'none'; // Ensure it's hidden
|
|
1932
|
+
}
|
|
1933
|
+
this.tabSelectorState.isVisible = false;
|
|
1934
|
+
this.tabSelectorState.selectedTabs = [];
|
|
1935
|
+
this.tabSelectorState.atPosition = -1;
|
|
1936
|
+
|
|
1937
|
+
console.log('[UIManager] Tab selector hidden');
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
positionTabSelector() {
|
|
1941
|
+
if (!this.elements.tabSelectorDropdown || !this.elements.taskInput) return;
|
|
1942
|
+
|
|
1943
|
+
const inputRect = this.elements.taskInput.getBoundingClientRect();
|
|
1944
|
+
const dropdown = this.elements.tabSelectorDropdown;
|
|
1945
|
+
|
|
1946
|
+
console.log('[UIManager] Positioning dropdown:', {
|
|
1947
|
+
inputRect,
|
|
1948
|
+
dropdownElement: dropdown
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1951
|
+
// Calculate 90% width of input
|
|
1952
|
+
const dropdownWidth = inputRect.width * 0.9;
|
|
1953
|
+
|
|
1954
|
+
// Position dropdown ABOVE the input (not below)
|
|
1955
|
+
dropdown.style.position = 'fixed';
|
|
1956
|
+
dropdown.style.bottom = `${window.innerHeight - inputRect.top + 5}px`; // Above the input
|
|
1957
|
+
dropdown.style.left = `${inputRect.left + (inputRect.width - dropdownWidth) / 2}px`; // Centered
|
|
1958
|
+
dropdown.style.width = `${dropdownWidth}px`; // 80% of input width
|
|
1959
|
+
dropdown.style.zIndex = '9999';
|
|
1960
|
+
dropdown.style.maxHeight = '300px';
|
|
1961
|
+
dropdown.style.overflowY = 'auto';
|
|
1962
|
+
|
|
1963
|
+
console.log('[UIManager] Dropdown positioned with styles:', {
|
|
1964
|
+
position: dropdown.style.position,
|
|
1965
|
+
bottom: dropdown.style.bottom,
|
|
1966
|
+
left: dropdown.style.left,
|
|
1967
|
+
width: dropdown.style.width,
|
|
1968
|
+
zIndex: dropdown.style.zIndex
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
async populateTabSelector() {
|
|
1973
|
+
try {
|
|
1974
|
+
console.log('[UIManager] Getting tab data from backend...');
|
|
1975
|
+
// Get all tabs and active tab from backend
|
|
1976
|
+
const [allTabsResponse, activeTabResponse] = await Promise.all([
|
|
1977
|
+
this.apiClient.getAllBrowserTabs(),
|
|
1978
|
+
this.apiClient.getActiveBrowserTab()
|
|
1979
|
+
]);
|
|
1980
|
+
|
|
1981
|
+
console.log('[UIManager] Raw API responses:', {
|
|
1982
|
+
allTabsResponse: JSON.stringify(allTabsResponse, null, 2),
|
|
1983
|
+
activeTabResponse: JSON.stringify(activeTabResponse, null, 2)
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
const allTabs = allTabsResponse.tabs || allTabsResponse || {};
|
|
1987
|
+
const activeTab = activeTabResponse.tab || activeTabResponse || {};
|
|
1988
|
+
const activeTabId = Object.keys(activeTab)[0];
|
|
1989
|
+
|
|
1990
|
+
console.log('[UIManager] Processed tab data:', {
|
|
1991
|
+
allTabsCount: Object.keys(allTabs).length,
|
|
1992
|
+
activeTabId,
|
|
1993
|
+
allTabIds: Object.keys(allTabs),
|
|
1994
|
+
allTabsData: allTabs,
|
|
1995
|
+
activeTabData: activeTab
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
this.tabSelectorState.allTabs = allTabs;
|
|
1999
|
+
|
|
2000
|
+
// Clear existing options
|
|
2001
|
+
if (this.elements.tabOptionsList) {
|
|
2002
|
+
this.elements.tabOptionsList.innerHTML = '';
|
|
2003
|
+
console.log('[UIManager] Cleared existing tab options');
|
|
2004
|
+
} else {
|
|
2005
|
+
console.error('[UIManager] tabOptionsList element not found!');
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// Add fallback test data if no tabs returned
|
|
2010
|
+
if (Object.keys(allTabs).length === 0) {
|
|
2011
|
+
console.warn('[UIManager] No tabs returned from API, adding test data for debugging');
|
|
2012
|
+
const testTabs = {
|
|
2013
|
+
'test-1': { title: 'Test Tab 1', url: 'https://example.com' },
|
|
2014
|
+
'test-2': { title: 'Test Tab 2', url: 'https://google.com' },
|
|
2015
|
+
'test-3': { title: 'Very Long Tab Title That Should Be Truncated', url: 'https://github.com' }
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
Object.entries(testTabs).forEach(([tabId, tabInfo]) => {
|
|
2019
|
+
const isActive = tabId === 'test-1';
|
|
2020
|
+
console.log('[UIManager] Creating test tab option:', { tabId, title: tabInfo.title, isActive });
|
|
2021
|
+
const option = this.createTabOption(tabId, tabInfo, isActive);
|
|
2022
|
+
this.elements.tabOptionsList.appendChild(option);
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
this.tabSelectorState.allTabs = testTabs;
|
|
2026
|
+
} else {
|
|
2027
|
+
// Add real tab options
|
|
2028
|
+
Object.entries(allTabs).forEach(([tabId, tabInfo]) => {
|
|
2029
|
+
const isActive = tabId === activeTabId;
|
|
2030
|
+
console.log('[UIManager] Creating tab option:', { tabId, title: tabInfo.title, isActive });
|
|
2031
|
+
const option = this.createTabOption(tabId, tabInfo, isActive);
|
|
2032
|
+
this.elements.tabOptionsList.appendChild(option);
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// Reset select all checkbox
|
|
2037
|
+
if (this.elements.selectAllTabs) {
|
|
2038
|
+
this.elements.selectAllTabs.checked = false;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
console.log('[UIManager] Tab selector populated with', Object.keys(this.tabSelectorState.allTabs).length, 'tabs');
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
console.error('[UIManager] Failed to populate tab selector:', error);
|
|
2044
|
+
throw error;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
createTabOption(tabId, tabInfo, isActive) {
|
|
2049
|
+
const option = document.createElement('div');
|
|
2050
|
+
option.className = `tab-option ${isActive ? 'active-tab' : ''}`;
|
|
2051
|
+
option.dataset.tabId = tabId;
|
|
2052
|
+
|
|
2053
|
+
// Format title (first 20 characters)
|
|
2054
|
+
const displayTitle = tabInfo.title ?
|
|
2055
|
+
(tabInfo.title.length > 20 ? tabInfo.title.substring(0, 20) + '...' : tabInfo.title) :
|
|
2056
|
+
'Unknown Title';
|
|
2057
|
+
|
|
2058
|
+
option.innerHTML = `
|
|
2059
|
+
<input type="radio" class="tab-radio" id="tab-${tabId}" name="tab-selection" value="${tabId}">
|
|
2060
|
+
<label for="tab-${tabId}" class="tab-label">
|
|
2061
|
+
<span class="tab-id">${tabId}:</span>
|
|
2062
|
+
<span class="tab-title">${this.escapeHtml(displayTitle)}</span>
|
|
2063
|
+
${isActive ? '<span class="active-indicator">(Active)</span>' : ''}
|
|
2064
|
+
</label>
|
|
2065
|
+
`;
|
|
2066
|
+
|
|
2067
|
+
// Add change event to radio button for auto-confirm
|
|
2068
|
+
const radio = option.querySelector('.tab-radio');
|
|
2069
|
+
radio?.addEventListener('change', this.handleTabSelection.bind(this));
|
|
2070
|
+
|
|
2071
|
+
return option;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
handleTabSelection(event) {
|
|
2075
|
+
const tabId = event.target.value;
|
|
2076
|
+
|
|
2077
|
+
if (event.target.checked) {
|
|
2078
|
+
// For radio buttons, replace the selected tabs array with just this tab
|
|
2079
|
+
this.tabSelectorState.selectedTabs = [tabId];
|
|
2080
|
+
|
|
2081
|
+
console.log('[UIManager] Selected tab:', tabId);
|
|
2082
|
+
|
|
2083
|
+
// Auto-confirm selection immediately
|
|
2084
|
+
this.confirmTabSelection();
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
handleSelectAllTabs(event) {
|
|
2089
|
+
if (event.target.checked) {
|
|
2090
|
+
// "Select All" means list all tabs in the input
|
|
2091
|
+
const allTabIds = Object.keys(this.tabSelectorState.allTabs);
|
|
2092
|
+
this.tabSelectorState.selectedTabs = allTabIds;
|
|
2093
|
+
|
|
2094
|
+
console.log('[UIManager] Select all tabs:', allTabIds);
|
|
2095
|
+
|
|
2096
|
+
// Auto-confirm selection immediately
|
|
2097
|
+
this.confirmTabSelection();
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
updateSelectAllState() {
|
|
2102
|
+
if (!this.elements.selectAllTabs || !this.elements.tabOptionsList) return;
|
|
2103
|
+
|
|
2104
|
+
const checkboxes = this.elements.tabOptionsList.querySelectorAll('.tab-checkbox');
|
|
2105
|
+
const checkedBoxes = this.elements.tabOptionsList.querySelectorAll('.tab-checkbox:checked');
|
|
2106
|
+
|
|
2107
|
+
if (checkboxes.length === 0) {
|
|
2108
|
+
this.elements.selectAllTabs.indeterminate = false;
|
|
2109
|
+
this.elements.selectAllTabs.checked = false;
|
|
2110
|
+
} else if (checkedBoxes.length === checkboxes.length) {
|
|
2111
|
+
this.elements.selectAllTabs.indeterminate = false;
|
|
2112
|
+
this.elements.selectAllTabs.checked = true;
|
|
2113
|
+
} else if (checkedBoxes.length > 0) {
|
|
2114
|
+
this.elements.selectAllTabs.indeterminate = true;
|
|
2115
|
+
this.elements.selectAllTabs.checked = false;
|
|
2116
|
+
} else {
|
|
2117
|
+
this.elements.selectAllTabs.indeterminate = false;
|
|
2118
|
+
this.elements.selectAllTabs.checked = false;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
confirmTabSelection() {
|
|
2123
|
+
if (this.tabSelectorState.selectedTabs.length === 0) {
|
|
2124
|
+
this.showNotification('Please select at least one tab', 'warning');
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Replace @ with selected tabs information
|
|
2129
|
+
this.insertSelectedTabsIntoInput();
|
|
2130
|
+
|
|
2131
|
+
// Hide the selector
|
|
2132
|
+
this.hideTabSelector();
|
|
2133
|
+
|
|
2134
|
+
console.log(`[UIManager] ${this.tabSelectorState.selectedTabs.length} tab(s) selected and confirmed`);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
insertSelectedTabsIntoInput() {
|
|
2138
|
+
if (!this.elements.taskInput) return;
|
|
2139
|
+
|
|
2140
|
+
const input = this.elements.taskInput;
|
|
2141
|
+
const currentValue = input.value;
|
|
2142
|
+
const atPosition = this.tabSelectorState.atPosition;
|
|
2143
|
+
|
|
2144
|
+
// Use special Unicode characters as boundaries for easy deletion
|
|
2145
|
+
const TAB_START_MARKER = '\u200B'; // Zero-width space
|
|
2146
|
+
const TAB_END_MARKER = '\u200C'; // Zero-width non-joiner
|
|
2147
|
+
|
|
2148
|
+
// Create tab information string in new format: @ tab_id: title[:20]
|
|
2149
|
+
const selectedTabsInfo = this.tabSelectorState.selectedTabs.map(tabId => {
|
|
2150
|
+
const tabInfo = this.tabSelectorState.allTabs[tabId];
|
|
2151
|
+
const displayTitle = tabInfo?.title ?
|
|
2152
|
+
(tabInfo.title.length > 20 ? tabInfo.title.substring(0, 20) + '...' : tabInfo.title) :
|
|
2153
|
+
'Unknown';
|
|
2154
|
+
return `${TAB_START_MARKER}@ ${tabId}: ${displayTitle}${TAB_END_MARKER}`;
|
|
2155
|
+
}).join(' ');
|
|
2156
|
+
|
|
2157
|
+
// Replace @ with tab selection (preserve the @ symbol)
|
|
2158
|
+
const beforeAt = currentValue.substring(0, atPosition);
|
|
2159
|
+
const afterAt = currentValue.substring(atPosition + 1);
|
|
2160
|
+
const newValue = `${beforeAt}${selectedTabsInfo} ${afterAt}`;
|
|
2161
|
+
|
|
2162
|
+
input.value = newValue;
|
|
2163
|
+
|
|
2164
|
+
// Trigger input change event for validation
|
|
2165
|
+
this.handleTaskInputChange({ target: input });
|
|
2166
|
+
|
|
2167
|
+
// Set cursor position after the inserted text
|
|
2168
|
+
const newCursorPosition = beforeAt.length + selectedTabsInfo.length + 1; // Add space
|
|
2169
|
+
input.setSelectionRange(newCursorPosition, newCursorPosition);
|
|
2170
|
+
input.focus();
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
getSelectedTabsForTask() {
|
|
2174
|
+
// Return selected tabs information for task submission
|
|
2175
|
+
if (this.tabSelectorState.selectedTabs.length === 0) {
|
|
2176
|
+
return null;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
const selectedTabsData = {};
|
|
2180
|
+
this.tabSelectorState.selectedTabs.forEach(tabId => {
|
|
2181
|
+
const tabInfo = this.tabSelectorState.allTabs[tabId];
|
|
2182
|
+
if (tabInfo) {
|
|
2183
|
+
selectedTabsData[tabId] = {
|
|
2184
|
+
url: tabInfo.url,
|
|
2185
|
+
title: tabInfo.title
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
return selectedTabsData;
|
|
2191
|
+
}
|
|
1409
2192
|
}
|
|
1410
2193
|
|
|
1411
2194
|
// Export for use in other modules
|