vibesurf 0.1.22__py3-none-any.whl → 0.1.24__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) {
@@ -1723,10 +1805,12 @@ class VibeSurfUIManager {
1723
1805
 
1724
1806
  // Remove href temporarily to prevent default browser behavior
1725
1807
  const originalHref = link.getAttribute('href');
1808
+ const dataFilePath = link.getAttribute('data-file-path');
1726
1809
  link.setAttribute('href', '#');
1727
1810
 
1728
- const href = originalHref;
1729
- if (!href || href === '#') return;
1811
+ // Use data-file-path if available (for file:// links), otherwise use href
1812
+ const targetUrl = dataFilePath || originalHref;
1813
+ if (!targetUrl || (targetUrl === '#' && !dataFilePath)) return;
1730
1814
 
1731
1815
  // Debounce - prevent rapid repeated clicks
1732
1816
  if (link.hasAttribute('data-link-processing')) {
@@ -1737,22 +1821,22 @@ class VibeSurfUIManager {
1737
1821
  link.setAttribute('data-link-processing', 'true');
1738
1822
 
1739
1823
  try {
1740
- console.log('[VibeSurf] Processing link:', href);
1824
+ console.log('[VibeSurf] Processing link:', targetUrl);
1741
1825
 
1742
1826
  // Handle file:// links using existing logic
1743
- if (href.startsWith('file://')) {
1744
- await this.handleFileLinkClick(href);
1827
+ if (targetUrl.startsWith('file://')) {
1828
+ await this.handleFileLinkClick(targetUrl);
1745
1829
  }
1746
1830
  // Handle HTTP/HTTPS links
1747
- else if (href.startsWith('http://') || href.startsWith('https://')) {
1748
- await this.handleHttpLinkClick(href);
1831
+ else if (targetUrl.startsWith('http://') || targetUrl.startsWith('https://')) {
1832
+ await this.handleHttpLinkClick(targetUrl);
1749
1833
  }
1750
1834
  // Handle other protocols or relative URLs
1751
1835
  else {
1752
- await this.handleOtherLinkClick(href);
1836
+ await this.handleOtherLinkClick(targetUrl);
1753
1837
  }
1754
1838
 
1755
- console.log('[VibeSurf] Link processed successfully:', href);
1839
+ console.log('[VibeSurf] Link processed successfully:', targetUrl);
1756
1840
  } catch (error) {
1757
1841
  console.error('[VibeSurf] Error handling link click:', error);
1758
1842
  this.showNotification(`Failed to open link: ${error.message}`, 'error');
@@ -1981,6 +2065,8 @@ class VibeSurfUIManager {
1981
2065
  return '🔄';
1982
2066
  case 'request':
1983
2067
  return '💡';
2068
+ case 'additional_request':
2069
+ return '➕';
1984
2070
  default:
1985
2071
  return '💡';
1986
2072
  }
@@ -3095,9 +3181,300 @@ class VibeSurfUIManager {
3095
3181
 
3096
3182
  return selectedTabsData;
3097
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
+ }
3098
3477
  }
3099
3478
 
3100
- // Export for use in other modules
3101
- if (typeof window !== 'undefined') {
3102
- window.VibeSurfUIManager = VibeSurfUIManager;
3103
- }
3479
+ // Call the export method
3480
+ VibeSurfUIManager.exportToWindow();
@@ -154,7 +154,7 @@
154
154
  <textarea
155
155
  id="task-input"
156
156
  class="task-input"
157
- placeholder="Describe your browsing task..."
157
+ placeholder="Ask anything (/ for skills, @ to specify tab)"
158
158
  rows="3"></textarea>
159
159
  <!-- Tab Selection Dropdown -->
160
160
  <div id="tab-selector-dropdown" class="tab-selector-dropdown hidden">
@@ -173,6 +173,18 @@
173
173
  </div>
174
174
  </div>
175
175
  </div>
176
+
177
+ <!-- Skill Selection Dropdown -->
178
+ <div id="skill-selector-dropdown" class="skill-selector-dropdown hidden">
179
+ <div class="skill-selector-header">
180
+ <span class="skill-selector-title">Select Skills</span>
181
+ </div>
182
+ <div class="skill-selector-content">
183
+ <div id="skill-options-list" class="skill-options-list">
184
+ <!-- Skill options will be populated here -->
185
+ </div>
186
+ </div>
187
+ </div>
176
188
  <div class="input-actions">
177
189
  <button id="voice-record-btn" class="action-btn voice-record-btn" title="Voice Input">
178
190
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -642,4 +642,119 @@ select.task-running-disabled {
642
642
  .voice-record-btn.recording .recording-duration {
643
643
  opacity: 1;
644
644
  visibility: visible;
645
+ }
646
+
647
+ /* Skill Selection Dropdown */
648
+ .skill-selector-dropdown {
649
+ position: fixed !important;
650
+ z-index: 1000;
651
+ background: var(--bg-primary);
652
+ border: 1px solid var(--border-color);
653
+ border-radius: var(--radius-md);
654
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
655
+ max-height: 300px;
656
+ overflow: hidden;
657
+ display: none;
658
+ }
659
+
660
+ .skill-selector-header {
661
+ display: flex;
662
+ align-items: center;
663
+ justify-content: space-between;
664
+ padding: var(--spacing-sm) var(--spacing-md);
665
+ background: var(--bg-secondary);
666
+ border-bottom: 1px solid var(--border-color);
667
+ }
668
+
669
+ .skill-selector-title {
670
+ font-size: var(--font-size-sm);
671
+ font-weight: var(--font-weight-medium);
672
+ color: var(--text-primary);
673
+ }
674
+
675
+ .skill-selector-close {
676
+ width: 24px;
677
+ height: 24px;
678
+ border: none;
679
+ background: none;
680
+ font-size: 18px;
681
+ color: var(--text-muted);
682
+ cursor: pointer;
683
+ border-radius: var(--radius-sm);
684
+ display: flex;
685
+ align-items: center;
686
+ justify-content: center;
687
+ transition: all var(--transition-fast);
688
+ }
689
+
690
+ .skill-selector-close:hover {
691
+ background: var(--bg-hover);
692
+ color: var(--text-primary);
693
+ }
694
+
695
+ .skill-selector-content {
696
+ max-height: 200px;
697
+ overflow-y: auto;
698
+ padding: var(--spacing-xs);
699
+ }
700
+
701
+ .skill-options-list {
702
+ display: flex;
703
+ flex-direction: column;
704
+ gap: 2px;
705
+ }
706
+
707
+ .skill-option {
708
+ display: flex;
709
+ align-items: center;
710
+ gap: var(--spacing-sm);
711
+ padding: var(--spacing-xs) var(--spacing-sm);
712
+ cursor: pointer;
713
+ border-radius: var(--radius-sm);
714
+ transition: background-color var(--transition-fast);
715
+ }
716
+
717
+ .skill-option:hover {
718
+ background: var(--bg-hover);
719
+ }
720
+
721
+ .skill-option.selected {
722
+ background: rgba(0, 122, 204, 0.1);
723
+ border: 1px solid rgba(0, 122, 204, 0.3);
724
+ }
725
+
726
+ .skill-name {
727
+ flex: 1;
728
+ font-size: var(--font-size-sm);
729
+ color: var(--text-primary);
730
+ overflow: hidden;
731
+ text-overflow: ellipsis;
732
+ white-space: nowrap;
733
+ }
734
+
735
+ .skill-description {
736
+ font-size: var(--font-size-xs);
737
+ color: var(--text-muted);
738
+ margin-top: 2px;
739
+ }
740
+
741
+ /* Skill selection indicator in textarea */
742
+ .task-input.has-selected-skills {
743
+ border-color: var(--accent-color);
744
+ background: rgba(40, 167, 69, 0.02);
745
+ }
746
+
747
+ /* Selected skills display */
748
+ .selected-skills-indicator {
749
+ position: absolute;
750
+ bottom: 8px;
751
+ left: 8px;
752
+ background: var(--accent-color);
753
+ color: white;
754
+ padding: 2px 6px;
755
+ border-radius: var(--radius-sm);
756
+ font-size: 10px;
757
+ font-weight: var(--font-weight-medium);
758
+ pointer-events: none;
759
+ z-index: 2;
645
760
  }