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.

Files changed (51) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +68 -45
  3. vibe_surf/agents/prompts/report_writer_prompt.py +73 -0
  4. vibe_surf/agents/prompts/vibe_surf_prompt.py +85 -172
  5. vibe_surf/agents/report_writer_agent.py +380 -226
  6. vibe_surf/agents/vibe_surf_agent.py +880 -825
  7. vibe_surf/agents/views.py +130 -0
  8. vibe_surf/backend/api/activity.py +3 -1
  9. vibe_surf/backend/api/browser.py +9 -5
  10. vibe_surf/backend/api/config.py +8 -5
  11. vibe_surf/backend/api/files.py +59 -50
  12. vibe_surf/backend/api/models.py +2 -2
  13. vibe_surf/backend/api/task.py +46 -13
  14. vibe_surf/backend/database/manager.py +24 -18
  15. vibe_surf/backend/database/queries.py +199 -192
  16. vibe_surf/backend/database/schemas.py +1 -1
  17. vibe_surf/backend/main.py +4 -2
  18. vibe_surf/backend/shared_state.py +28 -35
  19. vibe_surf/backend/utils/encryption.py +3 -1
  20. vibe_surf/backend/utils/llm_factory.py +41 -36
  21. vibe_surf/browser/agent_browser_session.py +0 -4
  22. vibe_surf/browser/browser_manager.py +14 -8
  23. vibe_surf/browser/utils.py +5 -3
  24. vibe_surf/browser/watchdogs/dom_watchdog.py +0 -45
  25. vibe_surf/chrome_extension/background.js +4 -0
  26. vibe_surf/chrome_extension/scripts/api-client.js +13 -0
  27. vibe_surf/chrome_extension/scripts/file-manager.js +27 -71
  28. vibe_surf/chrome_extension/scripts/session-manager.js +21 -3
  29. vibe_surf/chrome_extension/scripts/ui-manager.js +831 -48
  30. vibe_surf/chrome_extension/sidepanel.html +21 -4
  31. vibe_surf/chrome_extension/styles/activity.css +365 -5
  32. vibe_surf/chrome_extension/styles/input.css +139 -0
  33. vibe_surf/cli.py +5 -22
  34. vibe_surf/common.py +35 -0
  35. vibe_surf/llm/openai_compatible.py +217 -99
  36. vibe_surf/logger.py +99 -0
  37. vibe_surf/{controller/vibesurf_tools.py → tools/browser_use_tools.py} +233 -219
  38. vibe_surf/tools/file_system.py +437 -0
  39. vibe_surf/{controller → tools}/mcp_client.py +4 -3
  40. vibe_surf/tools/report_writer_tools.py +21 -0
  41. vibe_surf/tools/vibesurf_tools.py +657 -0
  42. vibe_surf/tools/views.py +120 -0
  43. {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/METADATA +6 -2
  44. {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/RECORD +49 -43
  45. vibe_surf/controller/file_system.py +0 -53
  46. vibe_surf/controller/views.py +0 -37
  47. /vibe_surf/{controller → tools}/__init__.py +0 -0
  48. {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/WHEEL +0 -0
  49. {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/entry_points.txt +0 -0
  50. {vibesurf-0.1.10.dist-info → vibesurf-0.1.12.dist-info}/licenses/LICENSE +0 -0
  51. {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
- this.elements.taskInput.disabled = isRunning;
408
- this.elements.taskInput.placeholder = isRunning ?
409
- 'Task is running - please wait...' :
410
- 'Enter your task description...';
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
- this.elements.sendBtn.disabled = isRunning || !this.canSubmitTask();
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
- this.elements.llmProfileSelect.disabled = isRunning;
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
- button.disabled = isRunning;
434
- if (isRunning) {
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 llmProfile = this.elements.llmProfileSelect?.value;
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
- this.showNotification(`Failed to terminate task: ${error.message}`, 'error');
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
- // Update send button state - require both text and LLM profile and no running task
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
- this.elements.sendBtn.disabled = !(hasText && hasLlmProfile && !this.state.isTaskRunning);
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.elements.activityLog.appendChild(activityItem);
859
- activityItem.classList.add('fade-in');
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
- // Bind copy button functionality
862
- this.bindCopyButtonEvent(activityItem, activityData);
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
- const timestamp = new Date(data.timestamp || Date.now()).toLocaleTimeString();
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
- <span class="message-time">${timestamp}</span>
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:// markdown links since markdown-it doesn't recognize them
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