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.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/browser_use_agent.py +1 -1
- vibe_surf/agents/prompts/vibe_surf_prompt.py +11 -0
- vibe_surf/agents/vibe_surf_agent.py +13 -2
- vibe_surf/backend/api/agent.py +38 -0
- vibe_surf/backend/api/task.py +1 -1
- vibe_surf/backend/main.py +2 -0
- vibe_surf/browser/agent_browser_session.py +7 -6
- vibe_surf/chrome_extension/background.js +82 -51
- vibe_surf/chrome_extension/manifest.json +2 -11
- vibe_surf/chrome_extension/scripts/api-client.js +5 -0
- vibe_surf/chrome_extension/scripts/file-manager.js +53 -12
- vibe_surf/chrome_extension/scripts/main.js +46 -11
- vibe_surf/chrome_extension/scripts/session-manager.js +30 -4
- vibe_surf/chrome_extension/scripts/ui-manager.js +623 -43
- vibe_surf/chrome_extension/sidepanel.html +13 -1
- vibe_surf/chrome_extension/styles/input.css +115 -0
- vibe_surf/tools/browser_use_tools.py +0 -90
- vibe_surf/tools/vibesurf_registry.py +52 -0
- vibe_surf/tools/vibesurf_tools.py +954 -5
- vibe_surf/tools/views.py +60 -0
- {vibesurf-0.1.21.dist-info → vibesurf-0.1.23.dist-info}/METADATA +2 -2
- {vibesurf-0.1.21.dist-info → vibesurf-0.1.23.dist-info}/RECORD +27 -25
- {vibesurf-0.1.21.dist-info → vibesurf-0.1.23.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.21.dist-info → vibesurf-0.1.23.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.21.dist-info → vibesurf-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {vibesurf-0.1.21.dist-info → vibesurf-0.1.23.dist-info}/top_level.txt +0 -0
|
@@ -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 = '
|
|
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
|
-
//
|
|
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
|
-
//
|
|
2496
|
-
this.
|
|
2497
|
-
|
|
2498
|
-
|
|
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
|
-
|
|
2504
|
-
|
|
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
|
-
|
|
2512
|
-
//
|
|
2513
|
-
|
|
2514
|
-
|
|
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
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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
|
-
//
|
|
2898
|
-
|
|
2899
|
-
window.VibeSurfUIManager = VibeSurfUIManager;
|
|
2900
|
-
}
|
|
3479
|
+
// Call the export method
|
|
3480
|
+
VibeSurfUIManager.exportToWindow();
|