vibesurf 0.1.21__py3-none-any.whl → 0.1.23__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.

@@ -27,6 +27,7 @@ class VibeSurfUIManager {
27
27
 
28
28
  this.bindElements();
29
29
  this.initializeTabSelector(); // Initialize tab selector before binding events
30
+ this.initializeSkillSelector(); // Initialize skill selector
30
31
  this.initializeManagers();
31
32
  this.bindEvents();
32
33
  this.setupSessionListeners();
@@ -67,6 +68,10 @@ class VibeSurfUIManager {
67
68
  selectAllTabs: document.getElementById('select-all-tabs'),
68
69
  tabOptionsList: document.getElementById('tab-options-list'),
69
70
 
71
+ // Skill selector elements
72
+ skillSelectorDropdown: document.getElementById('skill-selector-dropdown'),
73
+ skillOptionsList: document.getElementById('skill-options-list'),
74
+
70
75
  // Loading
71
76
  loadingOverlay: document.getElementById('loading-overlay')
72
77
  };
@@ -581,7 +586,7 @@ class VibeSurfUIManager {
581
586
  } else if (isRunning) {
582
587
  this.elements.taskInput.placeholder = 'Task is running - please wait...';
583
588
  } else {
584
- this.elements.taskInput.placeholder = 'Enter your task description...';
589
+ this.elements.taskInput.placeholder = 'Ask anything (/ for skills, @ to specify tab)';
585
590
  }
586
591
  }
587
592
 
@@ -962,8 +967,6 @@ class VibeSurfUIManager {
962
967
 
963
968
  async handleSendTask() {
964
969
  const taskDescription = this.elements.taskInput?.value.trim();
965
- const taskStatus = this.sessionManager.getTaskStatus();
966
- const isPaused = taskStatus === 'paused';
967
970
 
968
971
  if (!taskDescription) {
969
972
  this.showNotification('Please enter a task description', 'warning');
@@ -972,6 +975,12 @@ class VibeSurfUIManager {
972
975
  }
973
976
 
974
977
  try {
978
+ // Check task status from session manager first (more reliable than API check)
979
+ const sessionTaskStatus = this.sessionManager.getTaskStatus();
980
+ const isPaused = sessionTaskStatus === 'paused';
981
+
982
+ console.log('[UIManager] handleSendTask - session task status:', sessionTaskStatus, 'isPaused:', isPaused);
983
+
975
984
  if (isPaused) {
976
985
  // Handle adding new task to paused execution
977
986
 
@@ -988,7 +997,7 @@ class VibeSurfUIManager {
988
997
  return;
989
998
  }
990
999
 
991
- // Original logic for new task submission
1000
+ // For non-paused states, check if any task is running
992
1001
  const statusCheck = await this.checkTaskStatus();
993
1002
  if (statusCheck.isRunning) {
994
1003
  const canProceed = await this.showTaskRunningWarning('send a new task');
@@ -1034,7 +1043,12 @@ class VibeSurfUIManager {
1034
1043
  const selectedTabsData = this.getSelectedTabsForTask();
1035
1044
  if (selectedTabsData) {
1036
1045
  taskData.selected_tabs = selectedTabsData;
1037
-
1046
+ }
1047
+
1048
+ // Add selected skills information if any
1049
+ const selectedSkillsData = this.getSelectedSkillsForTask();
1050
+ if (selectedSkillsData) {
1051
+ taskData.selected_skills = selectedSkillsData;
1038
1052
  }
1039
1053
 
1040
1054
 
@@ -1105,6 +1119,19 @@ class VibeSurfUIManager {
1105
1119
  event.preventDefault();
1106
1120
  return;
1107
1121
  }
1122
+ // Also handle skill token deletion
1123
+ if (this.handleSkillTokenDeletion(event)) {
1124
+ event.preventDefault();
1125
+ return;
1126
+ }
1127
+ }
1128
+
1129
+ // Handle Tab key for skill auto-completion when only one skill matches
1130
+ if (event.key === 'Tab' && this.skillSelectorState.isVisible) {
1131
+ if (this.skillSelectorState.filteredSkills.length === 1) {
1132
+ event.preventDefault();
1133
+ this.selectSkill(this.skillSelectorState.filteredSkills[0]);
1134
+ }
1108
1135
  }
1109
1136
  }
1110
1137
 
@@ -1164,6 +1191,60 @@ class VibeSurfUIManager {
1164
1191
  return false; // Allow default behavior
1165
1192
  }
1166
1193
 
1194
+ handleSkillTokenDeletion(event) {
1195
+ const input = event.target;
1196
+ const cursorPos = input.selectionStart;
1197
+ const text = input.value;
1198
+
1199
+ // Unicode markers for skill tokens
1200
+ const startMarker = '\u200D'; // Zero-width joiner
1201
+ const endMarker = '\u200E'; // Left-to-right mark
1202
+
1203
+ let tokenStart = -1;
1204
+ let tokenEnd = -1;
1205
+
1206
+ if (event.key === 'Backspace') {
1207
+ // Only delete if cursor is directly adjacent to end of token
1208
+ if (cursorPos > 0 && text[cursorPos - 1] === endMarker) {
1209
+ tokenEnd = cursorPos; // Include the marker
1210
+ // Find the corresponding start marker backwards
1211
+ for (let j = cursorPos - 2; j >= 0; j--) {
1212
+ if (text[j] === startMarker) {
1213
+ tokenStart = j;
1214
+ break;
1215
+ }
1216
+ }
1217
+ }
1218
+ } else if (event.key === 'Delete') {
1219
+ // Only delete if cursor is directly adjacent to start of token
1220
+ if (cursorPos < text.length && text[cursorPos] === startMarker) {
1221
+ tokenStart = cursorPos;
1222
+ // Find the corresponding end marker forwards
1223
+ for (let j = cursorPos + 1; j < text.length; j++) {
1224
+ if (text[j] === endMarker) {
1225
+ tokenEnd = j + 1; // Include the marker
1226
+ break;
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+
1232
+ // If we found a complete token, delete it
1233
+ if (tokenStart !== -1 && tokenEnd !== -1) {
1234
+ const beforeToken = text.substring(0, tokenStart);
1235
+ const afterToken = text.substring(tokenEnd);
1236
+ input.value = beforeToken + afterToken;
1237
+ input.setSelectionRange(tokenStart, tokenStart);
1238
+
1239
+ // Trigger input change event for validation
1240
+ this.handleTaskInputChange({ target: input });
1241
+
1242
+ return true; // Prevent default behavior
1243
+ }
1244
+
1245
+ return false; // Allow default behavior
1246
+ }
1247
+
1167
1248
  async handleLlmProfileChange(event) {
1168
1249
  const selectedProfile = event.target.value;
1169
1250
 
@@ -1229,8 +1310,6 @@ class VibeSurfUIManager {
1229
1310
  }
1230
1311
 
1231
1312
  handleTaskInputChange(event) {
1232
-
1233
-
1234
1313
  const hasText = event.target.value.trim().length > 0;
1235
1314
  const textarea = event.target;
1236
1315
  const llmProfile = this.elements.llmProfileSelect?.value;
@@ -1241,6 +1320,9 @@ class VibeSurfUIManager {
1241
1320
  // Check for @ character to trigger tab selector
1242
1321
  this.handleTabSelectorInput(event);
1243
1322
 
1323
+ // Check for / character to trigger skill selector
1324
+ this.handleSkillSelectorInput(event);
1325
+
1244
1326
  // Update send button state - special handling for pause state
1245
1327
  if (this.elements.sendBtn) {
1246
1328
  if (isPaused) {
@@ -1338,6 +1420,15 @@ class VibeSurfUIManager {
1338
1420
  terminateBtn?.classList.remove('hidden');
1339
1421
  break;
1340
1422
 
1423
+ case 'done':
1424
+ case 'completed':
1425
+ case 'finished':
1426
+ console.log(`[UIManager] Task completed with status: ${status}, hiding panel after delay`);
1427
+ // Treat as ready state
1428
+ panel.classList.add('hidden');
1429
+ panel.classList.remove('error-state');
1430
+ break;
1431
+
1341
1432
  default:
1342
1433
  console.log(`[UIManager] Unknown control panel status: ${status}, hiding panel`);
1343
1434
  panel.classList.add('hidden');
@@ -1443,9 +1534,21 @@ class VibeSurfUIManager {
1443
1534
  }
1444
1535
 
1445
1536
  logs.forEach(log => this.addActivityLog(log));
1537
+
1538
+ // Bind link click handlers to all existing activity items after loading
1539
+ this.bindLinkHandlersToAllActivityItems();
1540
+
1446
1541
  this.scrollActivityToBottom();
1447
1542
  }
1448
1543
 
1544
+ bindLinkHandlersToAllActivityItems() {
1545
+ // Bind link click handlers to all existing activity items
1546
+ const activityItems = this.elements.activityLog.querySelectorAll('.activity-item');
1547
+ activityItems.forEach(item => {
1548
+ this.bindLinkClickHandlers(item);
1549
+ });
1550
+ }
1551
+
1449
1552
  addActivityLog(activityData) {
1450
1553
  // Filter out "done" status messages from UI display only
1451
1554
  const agentStatus = activityData.agent_status || activityData.status || '';
@@ -1475,6 +1578,9 @@ class VibeSurfUIManager {
1475
1578
 
1476
1579
  // Bind copy button functionality
1477
1580
  this.bindCopyButtonEvent(activityItem, activityData);
1581
+
1582
+ // Bind link click handlers to prevent extension freezing
1583
+ this.bindLinkClickHandlers(activityItem);
1478
1584
  }
1479
1585
  }
1480
1586
  }
@@ -1677,6 +1783,149 @@ class VibeSurfUIManager {
1677
1783
  }
1678
1784
  }
1679
1785
 
1786
+ bindLinkClickHandlers(activityItem) {
1787
+ // Handle all link clicks within activity items to prevent extension freezing
1788
+ const links = activityItem.querySelectorAll('a');
1789
+
1790
+ links.forEach(link => {
1791
+ // Check if handler is already attached to prevent double binding
1792
+ if (link.hasAttribute('data-link-handler-attached')) {
1793
+ return;
1794
+ }
1795
+
1796
+ link.setAttribute('data-link-handler-attached', 'true');
1797
+
1798
+ link.addEventListener('click', async (e) => {
1799
+ console.log('[VibeSurf] Link click event detected:', e);
1800
+
1801
+ // Comprehensive event prevention
1802
+ e.preventDefault();
1803
+ e.stopPropagation();
1804
+ e.stopImmediatePropagation(); // Prevent any other handlers
1805
+
1806
+ // Remove href temporarily to prevent default browser behavior
1807
+ const originalHref = link.getAttribute('href');
1808
+ const dataFilePath = link.getAttribute('data-file-path');
1809
+ link.setAttribute('href', '#');
1810
+
1811
+ // Use data-file-path if available (for file:// links), otherwise use href
1812
+ const targetUrl = dataFilePath || originalHref;
1813
+ if (!targetUrl || (targetUrl === '#' && !dataFilePath)) return;
1814
+
1815
+ // Debounce - prevent rapid repeated clicks
1816
+ if (link.hasAttribute('data-link-processing')) {
1817
+ console.log('[VibeSurf] Link already processing, ignoring duplicate click');
1818
+ return;
1819
+ }
1820
+
1821
+ link.setAttribute('data-link-processing', 'true');
1822
+
1823
+ try {
1824
+ console.log('[VibeSurf] Processing link:', targetUrl);
1825
+
1826
+ // Handle file:// links using existing logic
1827
+ if (targetUrl.startsWith('file://')) {
1828
+ await this.handleFileLinkClick(targetUrl);
1829
+ }
1830
+ // Handle HTTP/HTTPS links
1831
+ else if (targetUrl.startsWith('http://') || targetUrl.startsWith('https://')) {
1832
+ await this.handleHttpLinkClick(targetUrl);
1833
+ }
1834
+ // Handle other protocols or relative URLs
1835
+ else {
1836
+ await this.handleOtherLinkClick(targetUrl);
1837
+ }
1838
+
1839
+ console.log('[VibeSurf] Link processed successfully:', targetUrl);
1840
+ } catch (error) {
1841
+ console.error('[VibeSurf] Error handling link click:', error);
1842
+ this.showNotification(`Failed to open link: ${error.message}`, 'error');
1843
+ } finally {
1844
+ // Restore original href
1845
+ link.setAttribute('href', originalHref);
1846
+
1847
+ // Remove processing flag after a short delay
1848
+ setTimeout(() => {
1849
+ link.removeAttribute('data-link-processing');
1850
+ }, 1000);
1851
+ }
1852
+ });
1853
+ });
1854
+ }
1855
+
1856
+ async handleFileLinkClick(fileUrl) {
1857
+ console.log('[UIManager] Opening file URL:', fileUrl);
1858
+
1859
+ // Use the background script to handle file URLs
1860
+ const result = await chrome.runtime.sendMessage({
1861
+ type: 'OPEN_FILE_URL',
1862
+ data: { fileUrl }
1863
+ });
1864
+
1865
+ if (!result.success) {
1866
+ throw new Error(result.error || 'Failed to open file');
1867
+ }
1868
+ }
1869
+
1870
+ async handleHttpLinkClick(url) {
1871
+ console.log('[VibeSurf] Opening HTTP URL:', url);
1872
+
1873
+ try {
1874
+ // Open HTTP/HTTPS links in a new tab
1875
+ const result = await chrome.runtime.sendMessage({
1876
+ type: 'OPEN_FILE_URL',
1877
+ data: { fileUrl: url }
1878
+ });
1879
+
1880
+ console.log('[VibeSurf] Background script response:', result);
1881
+
1882
+ if (!result || !result.success) {
1883
+ throw new Error(result?.error || 'Failed to create tab for URL');
1884
+ }
1885
+
1886
+ console.log('[VibeSurf] Successfully opened tab:', result.tabId);
1887
+ } catch (error) {
1888
+ console.error('[VibeSurf] Error opening HTTP URL:', error);
1889
+ throw error;
1890
+ }
1891
+ }
1892
+
1893
+ async handleOtherLinkClick(url) {
1894
+ console.log('[UIManager] Opening other URL:', url);
1895
+
1896
+ // For relative URLs or other protocols, try to open in new tab
1897
+ try {
1898
+ // Use the background script to handle URL opening
1899
+ const result = await chrome.runtime.sendMessage({
1900
+ type: 'OPEN_FILE_URL',
1901
+ data: { fileUrl: url }
1902
+ });
1903
+
1904
+ if (!result.success) {
1905
+ throw new Error(result.error || 'Failed to open URL');
1906
+ }
1907
+ } catch (error) {
1908
+ // If the background script method fails, try to construct absolute URL
1909
+ if (!url.startsWith('http')) {
1910
+ try {
1911
+ const absoluteUrl = new URL(url, window.location.href).href;
1912
+ const result = await chrome.runtime.sendMessage({
1913
+ type: 'OPEN_FILE_URL',
1914
+ data: { fileUrl: absoluteUrl }
1915
+ });
1916
+
1917
+ if (!result.success) {
1918
+ throw new Error(result.error || 'Failed to open absolute URL');
1919
+ }
1920
+ } catch (urlError) {
1921
+ throw new Error(`Failed to open URL: ${urlError.message}`);
1922
+ }
1923
+ } else {
1924
+ throw error;
1925
+ }
1926
+ }
1927
+ }
1928
+
1680
1929
  async copyMessageToClipboard(activityData) {
1681
1930
  try {
1682
1931
  // Extract only the message content (no agent info or timestamps)
@@ -1816,6 +2065,8 @@ class VibeSurfUIManager {
1816
2065
  return '🔄';
1817
2066
  case 'request':
1818
2067
  return '💡';
2068
+ case 'additional_request':
2069
+ return '➕';
1819
2070
  default:
1820
2071
  return '💡';
1821
2072
  }
@@ -2492,45 +2743,83 @@ class VibeSurfUIManager {
2492
2743
 
2493
2744
  // Cleanup
2494
2745
  destroy() {
2495
- // Stop task status monitoring
2496
- this.stopTaskStatusMonitoring();
2497
-
2498
- // Cleanup voice recorder
2499
- if (this.voiceRecorder) {
2500
- this.voiceRecorder.cleanup();
2746
+ // Prevent multiple cleanup calls
2747
+ if (this.isDestroying) {
2748
+ console.log('[UIManager] Cleanup already in progress, skipping...');
2749
+ return;
2501
2750
  }
2502
2751
 
2503
- // Cleanup managers
2504
- if (this.settingsManager) {
2505
- // Cleanup settings manager if it has destroy method
2506
- if (typeof this.settingsManager.destroy === 'function') {
2507
- this.settingsManager.destroy();
2508
- }
2509
- }
2752
+ this.isDestroying = true;
2753
+ console.log('[UIManager] Destroying UI manager...');
2510
2754
 
2511
- if (this.historyManager) {
2512
- // Cleanup history manager if it has destroy method
2513
- if (typeof this.historyManager.destroy === 'function') {
2514
- this.historyManager.destroy();
2755
+ try {
2756
+ // Stop task status monitoring
2757
+ this.stopTaskStatusMonitoring();
2758
+
2759
+ // Cleanup voice recorder
2760
+ if (this.voiceRecorder) {
2761
+ this.voiceRecorder.cleanup();
2762
+ this.voiceRecorder = null;
2515
2763
  }
2516
- }
2517
-
2518
- if (this.fileManager) {
2519
- // Cleanup file manager if it has destroy method
2520
- if (typeof this.fileManager.destroy === 'function') {
2521
- this.fileManager.destroy();
2764
+
2765
+ // Cleanup managers with error handling
2766
+ if (this.settingsManager) {
2767
+ try {
2768
+ if (typeof this.settingsManager.destroy === 'function') {
2769
+ this.settingsManager.destroy();
2770
+ }
2771
+ } catch (error) {
2772
+ console.error('[UIManager] Error destroying settings manager:', error);
2773
+ }
2774
+ this.settingsManager = null;
2522
2775
  }
2523
- }
2524
-
2525
- if (this.modalManager) {
2526
- // Cleanup modal manager if it has destroy method
2527
- if (typeof this.modalManager.destroy === 'function') {
2528
- this.modalManager.destroy();
2776
+
2777
+ if (this.historyManager) {
2778
+ try {
2779
+ if (typeof this.historyManager.destroy === 'function') {
2780
+ this.historyManager.destroy();
2781
+ }
2782
+ } catch (error) {
2783
+ console.error('[UIManager] Error destroying history manager:', error);
2784
+ }
2785
+ this.historyManager = null;
2786
+ }
2787
+
2788
+ if (this.fileManager) {
2789
+ try {
2790
+ if (typeof this.fileManager.destroy === 'function') {
2791
+ this.fileManager.destroy();
2792
+ }
2793
+ } catch (error) {
2794
+ console.error('[UIManager] Error destroying file manager:', error);
2795
+ }
2796
+ this.fileManager = null;
2797
+ }
2798
+
2799
+ if (this.modalManager) {
2800
+ try {
2801
+ if (typeof this.modalManager.destroy === 'function') {
2802
+ this.modalManager.destroy();
2803
+ }
2804
+ } catch (error) {
2805
+ console.error('[UIManager] Error destroying modal manager:', error);
2806
+ }
2807
+ this.modalManager = null;
2529
2808
  }
2809
+
2810
+ // Clear state
2811
+ this.state = {
2812
+ isLoading: false,
2813
+ isTaskRunning: false,
2814
+ taskInfo: null
2815
+ };
2816
+
2817
+ console.log('[UIManager] UI manager cleanup complete');
2818
+ } catch (error) {
2819
+ console.error('[UIManager] Error during destroy:', error);
2820
+ } finally {
2821
+ this.isDestroying = false;
2530
2822
  }
2531
-
2532
- // Clear state
2533
- this.state.currentModal = null;
2534
2823
  }
2535
2824
 
2536
2825
  // Tab Selector Methods
@@ -2892,9 +3181,300 @@ class VibeSurfUIManager {
2892
3181
 
2893
3182
  return selectedTabsData;
2894
3183
  }
3184
+
3185
+ // Skill Selector Methods
3186
+ initializeSkillSelector() {
3187
+ // Initialize skill selector state
3188
+ this.skillSelectorState = {
3189
+ isVisible: false,
3190
+ selectedSkills: [],
3191
+ allSkills: [],
3192
+ slashPosition: -1, // Position where / was typed
3193
+ currentFilter: '', // Current filter text after /
3194
+ filteredSkills: [] // Filtered skills based on current input
3195
+ };
3196
+
3197
+ // Bind skill selector events
3198
+ this.bindSkillSelectorEvents();
3199
+ }
3200
+
3201
+ bindSkillSelectorEvents() {
3202
+ // Hide on click outside
3203
+ document.addEventListener('click', (event) => {
3204
+ if (this.skillSelectorState.isVisible &&
3205
+ this.elements.skillSelectorDropdown &&
3206
+ !this.elements.skillSelectorDropdown.contains(event.target) &&
3207
+ !this.elements.taskInput?.contains(event.target)) {
3208
+ this.hideSkillSelector();
3209
+ }
3210
+ });
3211
+ }
3212
+
3213
+ handleSkillSelectorInput(event) {
3214
+ // Safety check - ensure skill selector state is initialized
3215
+ if (!this.skillSelectorState) {
3216
+ console.warn('[UIManager] Skill selector state not initialized');
3217
+ return;
3218
+ }
3219
+
3220
+ const inputValue = event.target.value;
3221
+ const cursorPosition = event.target.selectionStart;
3222
+
3223
+ // Check if / was just typed
3224
+ if (inputValue[cursorPosition - 1] === '/') {
3225
+ this.skillSelectorState.slashPosition = cursorPosition - 1;
3226
+ this.skillSelectorState.currentFilter = '';
3227
+ this.showSkillSelector();
3228
+ } else if (this.skillSelectorState.isVisible) {
3229
+ // Check if / was deleted - hide skill selector immediately
3230
+ if (this.skillSelectorState.slashPosition >= 0 &&
3231
+ (this.skillSelectorState.slashPosition >= inputValue.length ||
3232
+ inputValue[this.skillSelectorState.slashPosition] !== '/')) {
3233
+ this.hideSkillSelector();
3234
+ return;
3235
+ }
3236
+
3237
+ // Update filter based on text after /
3238
+ const textAfterSlash = inputValue.substring(this.skillSelectorState.slashPosition + 1, cursorPosition);
3239
+
3240
+ // Only consider text up to the next space or special character
3241
+ const filterText = textAfterSlash.split(/[\s@]/)[0];
3242
+
3243
+ if (this.skillSelectorState.currentFilter !== filterText) {
3244
+ this.skillSelectorState.currentFilter = filterText;
3245
+ this.filterSkills();
3246
+ }
3247
+
3248
+ // Hide skill selector if user typed a space or moved past the skill context
3249
+ if (textAfterSlash.includes(' ') || textAfterSlash.includes('@')) {
3250
+ this.hideSkillSelector();
3251
+ }
3252
+ }
3253
+ }
3254
+
3255
+ async showSkillSelector() {
3256
+ if (!this.elements.skillSelectorDropdown || !this.elements.taskInput) {
3257
+ console.error('[UIManager] Skill selector elements not found', {
3258
+ dropdown: this.elements.skillSelectorDropdown,
3259
+ taskInput: this.elements.taskInput
3260
+ });
3261
+ return;
3262
+ }
3263
+
3264
+ try {
3265
+ // Fetch skill data from backend if not already cached
3266
+ if (this.skillSelectorState.allSkills.length === 0) {
3267
+ await this.populateSkillSelector();
3268
+ }
3269
+
3270
+ // Filter skills based on current input
3271
+ this.filterSkills();
3272
+
3273
+ // Position the dropdown relative to the input
3274
+ this.positionSkillSelector();
3275
+
3276
+ // Show the dropdown with explicit visibility
3277
+ this.elements.skillSelectorDropdown.classList.remove('hidden');
3278
+ this.elements.skillSelectorDropdown.style.display = 'block';
3279
+ this.elements.skillSelectorDropdown.style.visibility = 'visible';
3280
+ this.elements.skillSelectorDropdown.style.opacity = '1';
3281
+ this.skillSelectorState.isVisible = true;
3282
+
3283
+ } catch (error) {
3284
+ console.error('[UIManager] Failed to show skill selector:', error);
3285
+ this.showNotification('Failed to load skills', 'error');
3286
+ }
3287
+ }
3288
+
3289
+ hideSkillSelector() {
3290
+ if (this.elements.skillSelectorDropdown) {
3291
+ this.elements.skillSelectorDropdown.classList.add('hidden');
3292
+ this.elements.skillSelectorDropdown.style.display = 'none';
3293
+ }
3294
+ this.skillSelectorState.isVisible = false;
3295
+ this.skillSelectorState.slashPosition = -1;
3296
+ this.skillSelectorState.currentFilter = '';
3297
+ this.skillSelectorState.filteredSkills = [];
3298
+ }
3299
+
3300
+ positionSkillSelector() {
3301
+ if (!this.elements.skillSelectorDropdown || !this.elements.taskInput) return;
3302
+
3303
+ const inputRect = this.elements.taskInput.getBoundingClientRect();
3304
+ const dropdown = this.elements.skillSelectorDropdown;
3305
+
3306
+ // Calculate 90% width of input
3307
+ const dropdownWidth = inputRect.width * 0.9;
3308
+
3309
+ // Position dropdown ABOVE the input (not below)
3310
+ dropdown.style.position = 'fixed';
3311
+ dropdown.style.bottom = `${window.innerHeight - inputRect.top + 5}px`; // Above the input
3312
+ dropdown.style.left = `${inputRect.left + (inputRect.width - dropdownWidth) / 2}px`; // Centered
3313
+ dropdown.style.width = `${dropdownWidth}px`; // 90% of input width
3314
+ dropdown.style.zIndex = '9999';
3315
+ dropdown.style.maxHeight = '300px';
3316
+ dropdown.style.overflowY = 'auto';
3317
+ }
3318
+
3319
+ async populateSkillSelector() {
3320
+ try {
3321
+ console.log('[UIManager] Fetching skills from backend...');
3322
+ // Get all skills from backend
3323
+ const skills = await this.apiClient.getAllSkills();
3324
+
3325
+ console.log('[UIManager] Skills received from backend:', skills);
3326
+
3327
+ if (!skills || !Array.isArray(skills) || skills.length === 0) {
3328
+ console.warn('[UIManager] No skills returned from backend');
3329
+ this.skillSelectorState.allSkills = [];
3330
+ return;
3331
+ }
3332
+
3333
+ this.skillSelectorState.allSkills = skills.map(skillName => ({
3334
+ name: skillName,
3335
+ displayName: skillName // Keep original skill name without transformation
3336
+ }));
3337
+ console.log('[UIManager] Processed skills:', this.skillSelectorState.allSkills);
3338
+
3339
+ } catch (error) {
3340
+ console.error('[UIManager] Failed to populate skill selector:', error);
3341
+ console.error('[UIManager] Error details:', {
3342
+ message: error.message,
3343
+ stack: error.stack,
3344
+ response: error.response,
3345
+ data: error.data
3346
+ });
3347
+
3348
+ // Show error to user
3349
+ this.showNotification(`Failed to load skills: ${error.message}`, 'error');
3350
+
3351
+ // Set empty array instead of fallback test data
3352
+ this.skillSelectorState.allSkills = [];
3353
+ }
3354
+ }
3355
+
3356
+ filterSkills() {
3357
+ const filter = this.skillSelectorState.currentFilter.toLowerCase();
3358
+
3359
+ if (!filter) {
3360
+ this.skillSelectorState.filteredSkills = this.skillSelectorState.allSkills;
3361
+ } else {
3362
+ this.skillSelectorState.filteredSkills = this.skillSelectorState.allSkills.filter(skill =>
3363
+ skill.name.toLowerCase().startsWith(filter) ||
3364
+ skill.displayName.toLowerCase().startsWith(filter)
3365
+ );
3366
+ }
3367
+
3368
+ this.renderSkillOptions();
3369
+ }
3370
+
3371
+ renderSkillOptions() {
3372
+ if (!this.elements.skillOptionsList) return;
3373
+
3374
+ // Clear existing options
3375
+ this.elements.skillOptionsList.innerHTML = '';
3376
+
3377
+ if (this.skillSelectorState.filteredSkills.length === 0) {
3378
+ const noResults = document.createElement('div');
3379
+ noResults.className = 'skill-option';
3380
+ noResults.innerHTML = '<span class="skill-name">No skills found</span>';
3381
+ noResults.style.opacity = '0.6';
3382
+ noResults.style.cursor = 'not-allowed';
3383
+ this.elements.skillOptionsList.appendChild(noResults);
3384
+ return;
3385
+ }
3386
+
3387
+ // Add skill options
3388
+ this.skillSelectorState.filteredSkills.forEach((skill, index) => {
3389
+ const option = this.createSkillOption(skill, index);
3390
+ this.elements.skillOptionsList.appendChild(option);
3391
+ });
3392
+ }
3393
+
3394
+ createSkillOption(skill, index) {
3395
+ const option = document.createElement('div');
3396
+ option.className = 'skill-option';
3397
+ option.dataset.skillName = skill.name;
3398
+ option.dataset.skillIndex = index;
3399
+
3400
+ option.innerHTML = `
3401
+ <span class="skill-name">${this.escapeHtml(skill.displayName)}</span>
3402
+ `;
3403
+
3404
+ // Add click event for skill selection
3405
+ option.addEventListener('click', () => {
3406
+ this.selectSkill(skill);
3407
+ });
3408
+
3409
+ return option;
3410
+ }
3411
+
3412
+ selectSkill(skill) {
3413
+ if (!this.elements.taskInput) return;
3414
+
3415
+ const input = this.elements.taskInput;
3416
+ const currentValue = input.value;
3417
+ const slashPosition = this.skillSelectorState.slashPosition;
3418
+
3419
+ // Use special Unicode characters as boundaries for easy deletion
3420
+ const SKILL_START_MARKER = '\u200D'; // Zero-width joiner
3421
+ const SKILL_END_MARKER = '\u200E'; // Left-to-right mark
3422
+
3423
+ // Create skill information string
3424
+ const skillInfo = `${SKILL_START_MARKER}/${skill.name}${SKILL_END_MARKER}`;
3425
+
3426
+ // Replace / with skill selection
3427
+ const beforeSlash = currentValue.substring(0, slashPosition);
3428
+ const afterSlash = currentValue.substring(slashPosition + 1 + this.skillSelectorState.currentFilter.length);
3429
+ const newValue = `${beforeSlash}${skillInfo} ${afterSlash}`;
3430
+
3431
+ input.value = newValue;
3432
+
3433
+ // Trigger input change event for validation
3434
+ this.handleTaskInputChange({ target: input });
3435
+
3436
+ // Set cursor position after the inserted text
3437
+ const newCursorPosition = beforeSlash.length + skillInfo.length + 1;
3438
+ input.setSelectionRange(newCursorPosition, newCursorPosition);
3439
+ input.focus();
3440
+
3441
+ // Hide the selector
3442
+ this.hideSkillSelector();
3443
+ }
3444
+
3445
+ getSelectedSkillsForTask() {
3446
+ if (!this.elements.taskInput) return null;
3447
+
3448
+ const inputValue = this.elements.taskInput.value;
3449
+ const SKILL_START_MARKER = '\u200D'; // Zero-width joiner
3450
+ const SKILL_END_MARKER = '\u200E'; // Left-to-right mark
3451
+
3452
+ const skills = [];
3453
+ let startIndex = 0;
3454
+
3455
+ while ((startIndex = inputValue.indexOf(SKILL_START_MARKER, startIndex)) !== -1) {
3456
+ const endIndex = inputValue.indexOf(SKILL_END_MARKER, startIndex);
3457
+ if (endIndex !== -1) {
3458
+ const skillText = inputValue.substring(startIndex + 1, endIndex);
3459
+ if (skillText.startsWith('/')) {
3460
+ skills.push(skillText.substring(1)); // Remove the / prefix
3461
+ }
3462
+ startIndex = endIndex + 1;
3463
+ } else {
3464
+ break;
3465
+ }
3466
+ }
3467
+
3468
+ return skills.length > 0 ? skills : null;
3469
+ }
3470
+
3471
+ // Export for use in other modules
3472
+ static exportToWindow() {
3473
+ if (typeof window !== 'undefined') {
3474
+ window.VibeSurfUIManager = VibeSurfUIManager;
3475
+ }
3476
+ }
2895
3477
  }
2896
3478
 
2897
- // Export for use in other modules
2898
- if (typeof window !== 'undefined') {
2899
- window.VibeSurfUIManager = VibeSurfUIManager;
2900
- }
3479
+ // Call the export method
3480
+ VibeSurfUIManager.exportToWindow();