// CodexLens Manager - Configuration, Model Management, and Semantic Dependencies // Extracted from cli-manager.js for better maintainability // ============================================================ // EVENT HANDLERS - 用于防止内存泄漏的处理器引用 // ============================================================ var _workspaceStatusHandler = null; // ============================================================ // CACHE BRIDGE - 使用全局 PreloadService // ============================================================ // 缓存键映射(旧键名 -> 新键名) const CACHE_KEY_MAP = { workspaceStatus: 'workspace-status', config: 'codexlens-config', rerankerConfig: 'codexlens-reranker-config', status: 'codexlens-status', env: 'codexlens-env', models: 'codexlens-models', rerankerModels: 'codexlens-reranker-models', semanticStatus: 'codexlens-semantic-status', gpuList: 'codexlens-gpu-list', indexes: 'codexlens-indexes' }; /** * 兼容性函数:检查缓存是否有效 * @param {string} key - 旧缓存键 * @returns {boolean} */ function isCacheValid(key) { if (!window.cacheManager) return false; const newKey = CACHE_KEY_MAP[key] || key; return window.cacheManager.isValid(newKey); } /** * 兼容性函数:获取缓存数据 * @param {string} key - 旧缓存键 * @returns {*} */ function getCachedData(key) { if (!window.cacheManager) return null; const newKey = CACHE_KEY_MAP[key] || key; return window.cacheManager.get(newKey); } /** * 兼容性函数:设置缓存数据 * @param {string} key - 旧缓存键 * @param {*} data - 数据 * @param {number} ttl - 可选的 TTL */ function setCacheData(key, data, ttl = 180000) { if (!window.cacheManager) return; const newKey = CACHE_KEY_MAP[key] || key; window.cacheManager.set(newKey, data, ttl); } /** * 兼容性函数:使缓存失效 * @param {string} key - 旧缓存键(可选,不提供则清除所有) */ function invalidateCache(key) { if (!window.cacheManager) return; if (key) { const newKey = CACHE_KEY_MAP[key] || key; window.cacheManager.invalidate(newKey); } else { // 清除所有 codexlens 相关缓存 Object.values(CACHE_KEY_MAP).forEach(function(k) { window.cacheManager.invalidate(k); }); } } /** * 预加载 CodexLens 数据 * 现在委托给全局 PreloadService * @returns {Promise} */ async function preloadCodexLensData() { console.log('[CodexLens] Preload delegated to PreloadService'); if (!window.preloadService) { console.warn('[CodexLens] PreloadService not available'); return; } // 注册额外的数据源(如果尚未注册) const additionalSources = [ { key: 'codexlens-config', url: '/api/codexlens/config', priority: true, ttl: 300000 }, { key: 'codexlens-reranker-config', url: '/api/codexlens/reranker/config', priority: false, ttl: 300000 }, { key: 'codexlens-reranker-models', url: '/api/codexlens/reranker/models', priority: false, ttl: 600000 }, { key: 'codexlens-semantic-status', url: '/api/codexlens/semantic/status', priority: false, ttl: 300000 }, { key: 'codexlens-env', url: '/api/codexlens/env', priority: false, ttl: 300000 } ]; additionalSources.forEach(function(src) { if (!window.preloadService.sources.has(src.key)) { window.preloadService.register(src.key, () => fetch(src.url).then(r => r.ok ? r.json() : Promise.reject(r)), { isHighPriority: src.priority, ttl: src.ttl } ); } }); // 触发预加载 const preloadKeys = ['workspace-status', 'codexlens-config', 'codexlens-models']; await Promise.all(preloadKeys.map(key => window.preloadService.preload(key).catch(() => null))); } // ============================================================ // UTILITY FUNCTIONS // ============================================================ /** * Escape HTML special characters to prevent XSS */ function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // ============================================================ // WORKSPACE INDEX STATUS // ============================================================ /** * Refresh workspace index status (FTS and Vector coverage) * Uses progressive rendering: show cached data first, auto-refresh after background update * @param {boolean} forceRefresh - Force refresh, bypass cache */ async function refreshWorkspaceIndexStatus(forceRefresh) { var container = document.getElementById('workspaceIndexStatusContent'); var headerFtsEl = document.getElementById('headerFtsPercent'); var headerVectorEl = document.getElementById('headerVectorPercent'); // If neither container nor header elements exist, nothing to update if (!container && !headerFtsEl) return; // Render function var render = function(result) { updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl); }; // 1. Try to render from cache immediately var cachedResult = getCachedData('workspaceStatus'); if (cachedResult && !forceRefresh) { render(cachedResult); } else if (container) { // Show skeleton screen container.innerHTML = '
' + ' ' + (t('common.loading') || 'Loading...') + '
'; if (window.lucide) lucide.createIcons(); } // 2. Listen for data update events (防止内存泄漏:先移除旧监听器) if (window.eventManager) { // 移除之前的监听器(如果存在) if (_workspaceStatusHandler) { window.eventManager.off('data:updated:workspace-status', _workspaceStatusHandler); } // 创建新的监听器并保存引用 _workspaceStatusHandler = function(data) { render(data); }; window.eventManager.on('data:updated:workspace-status', _workspaceStatusHandler); } // 3. Trigger background loading (with fallback to direct fetch) try { var freshData; if (window.preloadService) { freshData = await window.preloadService.preload('workspace-status', { force: forceRefresh }); } else { // Fallback: direct fetch if preloadService not available var path = encodeURIComponent(projectPath || ''); var response = await fetch('/api/codexlens/workspace-status?path=' + path); if (!response.ok) throw new Error('HTTP ' + response.status); freshData = await response.json(); } render(freshData); } catch (err) { console.error('[CodexLens] Failed to load workspace status:', err); if (headerFtsEl) headerFtsEl.textContent = '--'; if (headerVectorEl) headerVectorEl.textContent = '--'; if (container) { container.innerHTML = '
' + ' ' + (t('common.error') || 'Error') + ': ' + err.message + '
'; } } if (window.lucide) lucide.createIcons(); } /** * Update workspace status UI with result data * @param {Object} result - API result * @param {HTMLElement} container - Container element * @param {HTMLElement} headerFtsEl - FTS header element * @param {HTMLElement} headerVectorEl - Vector header element */ function updateWorkspaceStatusUI(result, container, headerFtsEl, headerVectorEl) { if (result.success) { var ftsPercent = result.hasIndex ? (result.fts.percent || 0) : 0; var vectorPercent = result.hasIndex ? (result.vector.percent || 0) : 0; // Update header badges (always update if elements exist) if (headerFtsEl) { headerFtsEl.textContent = ftsPercent + '%'; headerFtsEl.className = 'text-sm font-medium ' + (ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground')); } if (headerVectorEl) { headerVectorEl.textContent = vectorPercent.toFixed(1) + '%'; headerVectorEl.className = 'text-sm font-medium ' + (vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground'))); } // Update detailed container (if exists) if (container) { var html = ''; if (!result.hasIndex) { // No index for current workspace html = '
' + '
' + ' ' + (t('codexlens.noIndexFound') || 'No index found for current workspace') + '
' + '' + '
'; } else { // FTS Status var ftsColor = ftsPercent >= 100 ? 'bg-success' : (ftsPercent > 0 ? 'bg-blue-500' : 'bg-muted-foreground'); var ftsTextColor = ftsPercent >= 100 ? 'text-success' : (ftsPercent > 0 ? 'text-blue-500' : 'text-muted-foreground'); html += '
' + '
' + '' + ' ' + '' + (t('codexlens.ftsIndex') || 'FTS Index') + '' + '' + '' + ftsPercent + '%' + '
' + '
' + '
' + '
' + '
' + (result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') + '
' + '
'; // Vector Status var vectorColor = vectorPercent >= 100 ? 'bg-success' : (vectorPercent >= 50 ? 'bg-purple-500' : (vectorPercent > 0 ? 'bg-purple-400' : 'bg-muted-foreground')); var vectorTextColor = vectorPercent >= 100 ? 'text-success' : (vectorPercent >= 50 ? 'text-purple-500' : (vectorPercent > 0 ? 'text-purple-400' : 'text-muted-foreground')); html += '
' + '
' + '' + ' ' + '' + (t('codexlens.vectorIndex') || 'Vector Index') + '' + '' + '' + vectorPercent.toFixed(1) + '%' + '
' + '
' + '
' + '
' + '
' + (result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') + (result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') + '
' + '
'; // Vector search availability indicator if (vectorPercent >= 50) { html += '
' + '' + '' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '' + '
'; } else if (vectorPercent > 0) { html += '
' + '' + '' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '' + '
'; } } container.innerHTML = html; } } else { // Error from API if (headerFtsEl) headerFtsEl.textContent = '--'; if (headerVectorEl) headerVectorEl.textContent = '--'; if (container) { container.innerHTML = '
' + ' ' + (result.error || t('common.error') || 'Error loading status') + '
'; } } if (window.lucide) lucide.createIcons(); } // ============================================================ // CODEXLENS CONFIGURATION MODAL // ============================================================ /** * Show CodexLens configuration modal * @param {boolean} forceRefresh - Force refresh, bypass cache */ async function showCodexLensConfigModal(forceRefresh) { try { // Check cache first for config and status var config, status; var usedCache = false; if (!forceRefresh && isCacheValid('config') && isCacheValid('status')) { config = getCachedData('config'); status = getCachedData('status'); usedCache = true; } else { showRefreshToast(t('codexlens.loadingConfig'), 'info'); // Fetch current config and status in parallel const [configResponse, statusResponse] = await Promise.all([ fetch('/api/codexlens/config'), fetch('/api/codexlens/status') ]); config = await configResponse.json(); status = await statusResponse.json(); // Cache the results setCacheData('config', config); setCacheData('status', status); } // Update window.cliToolsStatus to ensure isInstalled is correct if (!window.cliToolsStatus) { window.cliToolsStatus = {}; } window.cliToolsStatus.codexlens = { ...(window.cliToolsStatus.codexlens || {}), installed: status.ready || false, version: status.version || null }; const modalHtml = buildCodexLensConfigContent(config); // Create and show modal const tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; const modal = tempContainer.firstElementChild; document.body.appendChild(modal); // Initialize icons if (window.lucide) lucide.createIcons(); // Initialize event handlers initCodexLensConfigEvents(config); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build CodexLens configuration modal content - Tabbed Layout */ function buildCodexLensConfigContent(config) { const indexDir = config.index_dir || '~/.codexlens/indexes'; const indexCount = config.index_count || 0; const isInstalled = window.cliToolsStatus?.codexlens?.installed || false; const embeddingCoverage = config.embedding_coverage || 0; const apiMaxWorkers = config.api_max_workers || 4; const apiBatchSize = config.api_batch_size || 8; return ''; container.innerHTML = html; if (window.lucide) lucide.createIcons(); return; } // Show models for local backend if (embeddingBackend !== 'litellm') { var models = result.result.models; var predefinedModels = models.filter(function(m) { return m.source !== 'discovered'; }); var discoveredModels = models.filter(function(m) { return m.source === 'discovered'; }); // Split predefined models into recommended and others var recommendedModels = predefinedModels.filter(function(m) { return m.recommended; }); var otherModels = predefinedModels.filter(function(m) { return !m.recommended; }); // Helper function to render model card function renderModelCard(model) { var statusIcon = model.installed ? '' : ''; var sizeText = model.installed ? model.actual_size_mb.toFixed(0) + ' MB' : '~' + model.estimated_size_mb + ' MB'; var actionBtn = model.installed ? '' : ''; var recommendedBadge = model.recommended ? 'Rec' : ''; return '
' + '
' + statusIcon + '' + model.profile + '' + recommendedBadge + '' + '' + model.dimensions + 'd' + '
' + '
' + '' + sizeText + '' + actionBtn + '
' + '
'; } // Show recommended models (always visible) if (recommendedModels.length > 0) { html += '
' + ' Recommended Models (' + recommendedModels.length + ')
'; recommendedModels.forEach(function(model) { html += renderModelCard(model); }); } // Show other models (collapsed by default) if (otherModels.length > 0) { html += '
' + '' + '
'; } // Show discovered models (user manually placed) if (discoveredModels.length > 0) { html += '
' + ' Discovered Models
'; discoveredModels.forEach(function(model) { var sizeText = model.actual_size_mb ? model.actual_size_mb.toFixed(0) + ' MB' : 'Unknown'; var safeProfile = model.profile.replace(/[^a-zA-Z0-9-_]/g, '-'); html += '
' + '
' + '' + '' + escapeHtml(model.model_name) + '' + 'Manual' + '' + (model.dimensions || '?') + 'd' + '
' + '
' + '' + sizeText + '' + '' + '
' + '
'; }); } // Show manual install guide var guide = result.result.manual_install_guide; if (guide) { html += '
' + '
' + ' Manual Model Installation' + '
' + '
'; if (guide.steps) { guide.steps.forEach(function(step) { html += '
' + escapeHtml(step) + '
'; }); } if (guide.example) { html += '
' + '' + escapeHtml(guide.example) + '' + '
'; } // Show multi-platform paths if (guide.paths) { html += '
' + '
Cache paths:
' + '
'; if (guide.paths.windows) { html += '
Windows: ' + escapeHtml(guide.paths.windows) + '
'; } if (guide.paths.linux) { html += '
Linux: ' + escapeHtml(guide.paths.linux) + '
'; } if (guide.paths.macos) { html += '
macOS: ' + escapeHtml(guide.paths.macos) + '
'; } html += '
'; } html += '
'; } // Custom model download section html += '
' + '
' + ' Download Custom Model' + '
' + '
' + '' + '' + '
' + '
' + '
Only ONNX-format models work with FastEmbed (e.g., Xenova/* models)
' + '
PyTorch models (intfloat/*, sentence-transformers/*) will download but won\'t work with local embedding
' + '
' + '
'; } else { // LiteLLM backend - show API info html += '
' + '' + '
Using API embeddings
' + '
Model configured via CODEXLENS_EMBEDDING_MODEL
' + '
'; } html += ''; container.innerHTML = html; if (window.lucide) lucide.createIcons(); } catch (err) { container.innerHTML = '
' + escapeHtml(err.message) + '
'; } } /** * Toggle visibility of other (non-recommended) models */ function toggleOtherModels() { var container = document.getElementById('otherModelsContainer'); var chevron = document.getElementById('otherModelsChevron'); if (container && chevron) { var isHidden = container.classList.contains('hidden'); container.classList.toggle('hidden'); chevron.style.transform = isHidden ? 'rotate(90deg)' : ''; } } /** * Download model (simplified version) */ async function downloadModel(profile) { var modelCard = document.getElementById('model-' + profile); if (!modelCard) return; var originalHTML = modelCard.innerHTML; // Show loading state modelCard.innerHTML = '
' + '
' + '
' + 'Downloading ' + profile + '...' + '
' + '' + '
'; try { var response = await fetch('/api/codexlens/models/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: profile }) }); var result = await response.json(); if (result.success) { showRefreshToast('Model downloaded: ' + profile, 'success'); invalidateCache('models'); loadModelList(true); } else { showRefreshToast('Download failed: ' + result.error, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } /** * Delete model (simplified) */ async function deleteModel(profile) { if (!confirm('Delete model ' + profile + '?')) { return; } var modelCard = document.getElementById('model-' + profile); if (!modelCard) return; var originalHTML = modelCard.innerHTML; modelCard.innerHTML = '
' + '
' + '
' + 'Deleting...' + '
' + '
'; try { var response = await fetch('/api/codexlens/models/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: profile }) }); var result = await response.json(); if (result.success) { showRefreshToast('Model deleted: ' + profile, 'success'); invalidateCache('models'); loadModelList(true); } else { showRefreshToast('Delete failed: ' + result.error, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); modelCard.innerHTML = originalHTML; if (window.lucide) lucide.createIcons(); } } /** * Download a custom HuggingFace model by name */ async function downloadCustomModel() { var input = document.getElementById('customModelInput'); if (!input) return; var modelName = input.value.trim(); if (!modelName) { showRefreshToast('Please enter a model name', 'error'); return; } if (!modelName.includes('/')) { showRefreshToast('Invalid format. Use: org/model-name', 'error'); return; } // Disable input and show loading input.disabled = true; var originalPlaceholder = input.placeholder; input.placeholder = 'Downloading...'; input.value = ''; try { var response = await fetch('/api/codexlens/models/download-custom', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_name: modelName, model_type: 'embedding' }) }); var result = await response.json(); if (result.success) { showRefreshToast('Custom model downloaded: ' + modelName, 'success'); invalidateCache('models'); loadModelList(true); } else { showRefreshToast('Download failed: ' + result.error, 'error'); input.disabled = false; input.placeholder = originalPlaceholder; } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); input.disabled = false; input.placeholder = originalPlaceholder; } } /** * Delete a discovered (manually placed) model by its cache path */ async function deleteDiscoveredModel(cachePath) { if (!confirm('Delete this manually placed model?\n\nPath: ' + cachePath)) { return; } try { var response = await fetch('/api/codexlens/models/delete-path', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cache_path: cachePath }) }); var result = await response.json(); if (result.success) { showRefreshToast('Model deleted successfully', 'success'); invalidateCache('models'); loadModelList(true); } else { showRefreshToast('Delete failed: ' + result.error, 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } // ============================================================ // RERANKER MODEL MANAGEMENT // ============================================================ // Available reranker models (fastembed TextCrossEncoder) - fallback if API unavailable var RERANKER_MODELS = [ { id: 'ms-marco-mini', name: 'Xenova/ms-marco-MiniLM-L-6-v2', size: 90, desc: 'Fast, lightweight', recommended: true }, { id: 'ms-marco-12', name: 'Xenova/ms-marco-MiniLM-L-12-v2', size: 130, desc: 'Better accuracy', recommended: true }, { id: 'bge-base', name: 'BAAI/bge-reranker-base', size: 280, desc: 'High quality', recommended: true }, { id: 'bge-large', name: 'BAAI/bge-reranker-large', size: 560, desc: 'Maximum quality', recommended: false }, { id: 'jina-tiny', name: 'jinaai/jina-reranker-v1-tiny-en', size: 70, desc: 'Tiny, fast', recommended: true }, { id: 'jina-turbo', name: 'jinaai/jina-reranker-v1-turbo-en', size: 150, desc: 'Balanced', recommended: true } ]; /** * Load reranker model list with download/delete support and cache * @param {boolean} forceRefresh - Force refresh, bypass cache */ async function loadRerankerModelList(forceRefresh) { // Update both containers (advanced tab and page model management) var containers = [ document.getElementById('rerankerModelListContainer'), document.getElementById('pageRerankerModelListContainer') ].filter(Boolean); console.log('[CodexLens] loadRerankerModelList - containers found:', containers.length); if (containers.length === 0) { console.warn('[CodexLens] No reranker model list containers found'); return; } try { var config, modelsData; var useCache = !forceRefresh && isCacheValid('rerankerConfig') && isCacheValid('rerankerModels'); if (useCache) { config = getCachedData('rerankerConfig'); modelsData = getCachedData('rerankerModels'); console.log('[CodexLens] Using cached reranker data'); } else { // Fetch both config and models list in parallel var [configResponse, modelsResponse] = await Promise.all([ fetch('/api/codexlens/reranker/config'), fetch('/api/codexlens/reranker/models') ]); if (!configResponse.ok) { throw new Error('Failed to load reranker config: ' + configResponse.status); } config = await configResponse.json(); modelsData = modelsResponse.ok ? await modelsResponse.json() : null; // Cache the results setCacheData('rerankerConfig', config); setCacheData('rerankerModels', modelsData); console.log('[CodexLens] Reranker config loaded:', { backend: config.backend, model: config.model_name }); } // Handle API response format var currentModel = config.model_name || config.result?.reranker_model || 'Xenova/ms-marco-MiniLM-L-6-v2'; var currentBackend = config.backend || config.result?.reranker_backend || 'fastembed'; // Try to use API models, fall back to static list var models = RERANKER_MODELS; var modelsFromApi = false; if (modelsData && modelsData.success && modelsData.result && modelsData.result.models) { models = modelsData.result.models.map(function(m) { return { id: m.profile, name: m.model_name, size: m.installed && m.actual_size_mb ? m.actual_size_mb : m.estimated_size_mb, desc: m.description, installed: m.installed, recommended: m.recommended }; }); modelsFromApi = true; console.log('[CodexLens] Loaded ' + models.length + ' reranker models from API'); } var html = '
'; // Show current backend status var isApiBackend = currentBackend === 'litellm' || currentBackend === 'api'; var backendLabel = isApiBackend ? 'API (' + (currentBackend === 'litellm' ? 'LiteLLM' : 'Remote') + ')' : 'Local (FastEmbed)'; var backendIcon = isApiBackend ? 'cloud' : 'hard-drive'; html += '
' + '
' + '' + '' + backendLabel + '' + '
' + 'via Environment Variables' + '
'; // Helper to match model names (handles different prefixes like Xenova/ vs cross-encoder/) function modelMatches(current, target) { if (!current || !target) return false; // Exact match if (current === target) return true; // Match by base name (after last /) var currentBase = current.split('/').pop(); var targetBase = target.split('/').pop(); return currentBase === targetBase; } // Show API info when using API backend if (isApiBackend) { html += '
' + '
' + '' + '' + t('codexlens.usingApiReranker') + '' + '
' + '
' + '' + t('codexlens.currentModel') + ':' + '' + escapeHtml(currentModel) + '' + '
' + '
'; } // Local models section title html += '
' + '' + t('codexlens.localModels') + '
'; models.forEach(function(model) { var isActive = !isApiBackend && modelMatches(currentModel, model.name); var isInstalled = model.installed; // Status icon var statusIcon; if (isActive) { statusIcon = ''; } else if (isInstalled) { statusIcon = ''; } else { statusIcon = ''; } // Action buttons var actionBtns = ''; if (isActive) { actionBtns = '' + t('codexlens.active') + ''; if (isInstalled) { actionBtns += ''; } } else if (isInstalled) { // Installed but not active - can select or delete if (isApiBackend) { actionBtns = ''; } else { actionBtns = ''; } actionBtns += ''; } else { // Not installed - show download button actionBtns = ''; } // Size display var sizeText = (isInstalled && model.size) ? model.size + ' MB' : '~' + model.size + ' MB'; // Recommendation badge var recBadge = model.recommended ? ' ' : ''; html += '
' + '
' + statusIcon + '' + model.id + recBadge + '' + '' + '' + model.desc + '' + '
' + '
' + '' + sizeText + '' + actionBtns + '
' + '
'; }); html += '
'; // Update all containers containers.forEach(function(container) { container.innerHTML = html; }); if (window.lucide) lucide.createIcons(); } catch (err) { var errorHtml = '
' + escapeHtml(err.message) + '
'; containers.forEach(function(container) { container.innerHTML = errorHtml; }); } } /** * Download reranker model */ async function downloadRerankerModel(profile) { var container = document.getElementById('reranker-' + profile); if (container) { container.innerHTML = '
' + '' + '' + t('codexlens.downloading') + '' + '
'; if (window.lucide) lucide.createIcons(); } try { var response = await fetch('/api/codexlens/reranker/models/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: profile }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.downloadComplete') + ': ' + profile, 'success'); invalidateCache('rerankerModels'); loadRerankerModelList(true); } else { showRefreshToast(t('codexlens.downloadFailed') + ': ' + (result.error || 'Unknown error'), 'error'); loadRerankerModelList(true); } } catch (err) { showRefreshToast(t('codexlens.downloadFailed') + ': ' + err.message, 'error'); loadRerankerModelList(true); } } /** * Delete reranker model */ async function deleteRerankerModel(profile) { if (!confirm(t('codexlens.deleteModelConfirm') + ' ' + profile + '?')) { return; } try { var response = await fetch('/api/codexlens/reranker/models/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profile: profile }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.modelDeleted') + ': ' + profile, 'success'); invalidateCache('rerankerModels'); loadRerankerModelList(true); } else { showRefreshToast('Failed to delete: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } /** * Update reranker backend */ async function updateRerankerBackend(backend) { try { var response = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: backend }) }); var result = await response.json(); if (result.success) { showRefreshToast('Reranker backend updated: ' + backend, 'success'); invalidateCache('rerankerConfig'); loadRerankerModelList(true); } else { showRefreshToast('Failed to update: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } /** * Select reranker model */ async function selectRerankerModel(modelName) { try { var response = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_name: modelName }) }); var result = await response.json(); if (result.success) { showRefreshToast('Reranker model selected: ' + modelName.split('/').pop(), 'success'); invalidateCache('rerankerConfig'); loadRerankerModelList(true); } else { showRefreshToast('Failed to select: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } /** * Switch from API to local reranker backend and select model */ async function switchToLocalReranker(modelName) { try { // First switch backend to fastembed var backendResponse = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: 'fastembed' }) }); var backendResult = await backendResponse.json(); if (!backendResult.success) { showRefreshToast('Failed to switch backend: ' + (backendResult.error || 'Unknown error'), 'error'); return; } // Then select the model var modelResponse = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_name: modelName }) }); var modelResult = await modelResponse.json(); if (modelResult.success) { showRefreshToast(t('codexlens.switchedToLocal') + ': ' + modelName.split('/').pop(), 'success'); invalidateCache('rerankerConfig'); invalidateCache('rerankerModels'); loadRerankerModelList(true); // Also reload env variables to reflect the change if (typeof loadEnvVariables === 'function') { loadEnvVariables(); } } else { showRefreshToast('Failed to select model: ' + (modelResult.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Error: ' + err.message, 'error'); } } // ============================================================ // MODEL TAB & MODE MANAGEMENT // ============================================================ /** * Switch between Embedding and Reranker tabs in CodexLens manager */ function switchCodexLensModelTab(tabName) { console.log('[CodexLens] Switching to tab:', tabName); // Update tab buttons using direct style manipulation for reliability var tabs = document.querySelectorAll('.model-tab'); tabs.forEach(function(tab) { var isActive = tab.getAttribute('data-tab') === tabName; if (isActive) { tab.className = 'model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-primary text-primary'; tab.style.backgroundColor = 'rgba(var(--primary), 0.05)'; } else { tab.className = 'model-tab flex-1 px-4 py-2.5 text-sm font-medium border-b-2 border-transparent text-muted-foreground'; tab.style.backgroundColor = ''; } }); // Update tab content var embeddingContent = document.getElementById('embeddingTabContent'); var rerankerContent = document.getElementById('rerankerTabContent'); if (embeddingContent && rerankerContent) { if (tabName === 'embedding') { embeddingContent.classList.remove('hidden'); embeddingContent.style.display = 'block'; rerankerContent.classList.add('hidden'); rerankerContent.style.display = 'none'; // Reload embedding models when switching to embedding tab loadModelList(); } else { embeddingContent.classList.add('hidden'); embeddingContent.style.display = 'none'; rerankerContent.classList.remove('hidden'); rerankerContent.style.display = 'block'; // Load reranker models when switching to reranker tab loadRerankerModelList(); } } } /** * Update model mode (Local vs API) */ function updateModelMode(mode) { var modeSelect = document.getElementById('modelModeSelect'); // Store mode preference (will be saved when locked) if (modeSelect) { modeSelect.setAttribute('data-current-mode', mode); } } /** * Load GPU devices for mode selector */ async function loadGpuDevicesForModeSelector() { var gpuSelect = document.getElementById('gpuDeviceSelect'); var gpuSection = document.getElementById('gpuConfigSection'); if (!gpuSelect) return; try { var response = await fetch('/api/codexlens/gpu/list'); if (!response.ok) { console.warn('[CodexLens] GPU list endpoint returned:', response.status); gpuSelect.innerHTML = ''; // Hide section if no GPU devices available if (gpuSection) gpuSection.classList.add('hidden'); return; } var result = await response.json(); var html = ''; if (result.devices && result.devices.length > 1) { // Only show section if multiple GPUs available result.devices.forEach(function(device, index) { html += ''; }); gpuSelect.innerHTML = html; if (gpuSection) gpuSection.classList.remove('hidden'); } else { // Single or no GPU - hide section gpuSelect.innerHTML = html; if (gpuSection) gpuSection.classList.add('hidden'); } } catch (err) { console.error('Failed to load GPU devices:', err); if (gpuSection) gpuSection.classList.add('hidden'); } } /** * Toggle model mode lock (save configuration) */ async function toggleModelModeLock() { var lockBtn = document.getElementById('modelModeLockBtn'); var modeSelect = document.getElementById('modelModeSelect'); var gpuSelect = document.getElementById('gpuDeviceSelect'); if (!lockBtn || !modeSelect) return; var isLocked = lockBtn.getAttribute('data-locked') === 'true'; if (isLocked) { // Unlock - enable editing lockBtn.setAttribute('data-locked', 'false'); lockBtn.innerHTML = 'Lock'; lockBtn.classList.remove('btn-primary'); lockBtn.classList.add('btn-outline'); modeSelect.disabled = false; if (gpuSelect) gpuSelect.disabled = false; if (window.lucide) lucide.createIcons(); } else { // Lock - save configuration var mode = modeSelect.value; var gpuDevice = gpuSelect ? gpuSelect.value : 'auto'; try { // Save embedding backend preference var embeddingBackend = mode === 'local' ? 'fastembed' : 'litellm'; await fetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ embedding_backend: embeddingBackend, gpu_device: gpuDevice }) }); // Save reranker backend preference var rerankerBackend = mode === 'local' ? 'fastembed' : 'litellm'; await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: rerankerBackend }) }); // Update UI to locked state lockBtn.setAttribute('data-locked', 'true'); lockBtn.innerHTML = 'Locked'; lockBtn.classList.remove('btn-outline'); lockBtn.classList.add('btn-primary'); modeSelect.disabled = true; if (gpuSelect) gpuSelect.disabled = true; if (window.lucide) lucide.createIcons(); showRefreshToast('Configuration saved: ' + (mode === 'local' ? 'Local (FastEmbed)' : 'API (LiteLLM)'), 'success'); // Refresh model lists to reflect new backend loadModelList(); loadRerankerModelList(); } catch (err) { showRefreshToast('Failed to save configuration: ' + err.message, 'error'); } } } /** * Initialize model mode from saved config */ async function initModelModeFromConfig() { var modeSelect = document.getElementById('modelModeSelect'); if (!modeSelect) return; try { var response = await fetch('/api/codexlens/config'); var config = await response.json(); var embeddingBackend = config.embedding_backend || 'fastembed'; var mode = embeddingBackend === 'litellm' ? 'api' : 'local'; modeSelect.value = mode; modeSelect.setAttribute('data-current-mode', mode); } catch (err) { console.error('Failed to load model mode config:', err); } } /** * Update compact semantic status badge in header */ async function updateSemanticStatusBadge() { var badge = document.getElementById('semanticStatusBadge'); if (!badge) return; try { var response = await fetch('/api/codexlens/semantic/status'); var result = await response.json(); if (result.available) { var accelerator = result.accelerator || 'CPU'; var badgeClass = 'bg-success/20 text-success'; var icon = 'check-circle'; if (accelerator === 'CUDA') { badgeClass = 'bg-green-500/20 text-green-600'; icon = 'zap'; } else if (accelerator === 'DirectML') { badgeClass = 'bg-blue-500/20 text-blue-600'; icon = 'cpu'; } badge.innerHTML = '' + '' + accelerator + ''; } else { badge.innerHTML = '' + '' + 'Not Ready' + ''; } if (window.lucide) lucide.createIcons(); } catch (err) { badge.innerHTML = 'Error'; } } // ============================================================ // CODEXLENS ACTIONS // ============================================================ /** * Initialize CodexLens index with bottom floating progress bar * @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector) * @param {string} embeddingModel - Model profile: 'code', 'fast' * @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API) * @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1) * @param {boolean} incremental - Incremental mode: true=skip unchanged, false=full rebuild (default: false) */ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, maxWorkers, incremental) { indexType = indexType || 'vector'; embeddingModel = embeddingModel || 'code'; embeddingBackend = embeddingBackend || 'fastembed'; maxWorkers = maxWorkers || 1; incremental = incremental !== undefined ? incremental : false; // Default: full rebuild // For vector/full index with local backend, check if semantic dependencies are available // LiteLLM backend uses remote embeddings and does not require fastembed/ONNX deps. if ((indexType === 'vector' || indexType === 'full') && embeddingBackend !== 'litellm') { try { var semanticResponse = await fetch('/api/codexlens/semantic/status'); var semanticStatus = await semanticResponse.json(); if (!semanticStatus.available) { // Semantic deps not installed - show confirmation dialog var installDeps = confirm( (t('codexlens.semanticNotInstalled') || 'Semantic search dependencies are not installed.') + '\n\n' + (t('codexlens.installDepsPrompt') || 'Would you like to install them now? (This may take a few minutes)\n\nClick "Cancel" to create FTS index only.') ); if (installDeps) { // Install semantic dependencies first showRefreshToast(t('codexlens.installingDeps') || 'Installing semantic dependencies...', 'info'); try { var installResponse = await csrfFetch('/api/codexlens/semantic/install', { method: 'POST' }); var installResult = await installResponse.json(); if (!installResult.success) { showRefreshToast((t('codexlens.depsInstallFailed') || 'Failed to install dependencies') + ': ' + installResult.error, 'error'); // Fall back to FTS only indexType = 'normal'; } else { showRefreshToast(t('codexlens.depsInstalled') || 'Dependencies installed successfully', 'success'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); indexType = 'normal'; } } else { // User chose to skip - create FTS only indexType = 'normal'; } } } catch (err) { console.warn('[CodexLens] Could not check semantic status:', err); // Continue with requested type, backend will handle fallback } } // Remove existing progress bar if any closeCodexLensIndexModal(); // Create bottom floating progress bar var progressBar = document.createElement('div'); progressBar.id = 'codexlensIndexFloating'; progressBar.className = 'fixed bottom-0 left-0 right-0 z-50 bg-card border-t border-border shadow-lg transform transition-transform duration-300'; // Determine display label var indexTypeLabel; if (indexType === 'full') { indexTypeLabel = 'FTS + Vector'; } else if (indexType === 'vector') { indexTypeLabel = 'Vector'; } else { indexTypeLabel = 'FTS'; } // Add model info for vector indexes var modelLabel = ''; if (indexType !== 'normal') { var modelNames = { code: 'Code', fast: 'Fast' }; var backendLabel = embeddingBackend === 'litellm' ? 'API: ' : ''; modelLabel = ' [' + backendLabel + (modelNames[embeddingModel] || embeddingModel) + ']'; } progressBar.innerHTML = '
' + '
' + '
' + '
' + '
' + '
' + '' + t('codexlens.indexing') + ' (' + indexTypeLabel + modelLabel + ')' + '0%' + '
' + '
' + t('codexlens.preparingIndex') + '
' + '
' + '
' + '' + '' + '' + '
' + '
'; document.body.appendChild(progressBar); if (window.lucide) lucide.createIcons(); // For 'full' type, use 'vector' in the API (it creates FTS + embeddings) var apiIndexType = (indexType === 'full') ? 'vector' : indexType; // Start indexing with specified type and model startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend, maxWorkers, incremental); } /** * Start the indexing process * @param {string} indexType - 'vector' or 'normal' * @param {string} embeddingModel - Model profile: 'code', 'fast' * @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API) * @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1) * @param {boolean} incremental - Incremental mode (default: false for full rebuild) */ async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend, maxWorkers, incremental) { indexType = indexType || 'vector'; embeddingModel = embeddingModel || 'code'; embeddingBackend = embeddingBackend || 'fastembed'; maxWorkers = maxWorkers || 1; incremental = incremental !== undefined ? incremental : false; // Default: full rebuild var statusText = document.getElementById('codexlensIndexStatus'); var progressBar = document.getElementById('codexlensIndexProgressBar'); var percentText = document.getElementById('codexlensIndexPercent'); var spinner = document.getElementById('codexlensIndexSpinner'); // Setup WebSocket listener for progress events window.codexlensIndexProgressHandler = function(data) { var payload = data.payload || data; console.log('[CodexLens] Progress event received:', payload); if (statusText) statusText.textContent = payload.message || t('codexlens.indexing'); if (progressBar) progressBar.style.width = (payload.percent || 0) + '%'; if (percentText) percentText.textContent = (payload.percent || 0) + '%'; // Handle completion if (payload.stage === 'complete') { handleIndexComplete(true, payload.message); } else if (payload.stage === 'error') { handleIndexComplete(false, payload.message); } }; // Register with notification system if (typeof registerWsEventHandler === 'function') { registerWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler); console.log('[CodexLens] Registered WebSocket progress handler'); } else { console.warn('[CodexLens] registerWsEventHandler not available'); } try { console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend, 'maxWorkers:', maxWorkers, 'incremental:', incremental); var response = await fetch('/api/codexlens/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend, maxWorkers: maxWorkers, incremental: incremental }) }); var result = await response.json(); console.log('[CodexLens] Init result:', result); // Check if completed successfully (WebSocket might have already reported) if (result.success) { // For vector index, check if embeddings were actually generated var embeddingsResult = result.result && result.result.embeddings; if (indexType === 'vector' && embeddingsResult && !embeddingsResult.generated) { // FTS succeeded but embeddings failed - show partial success var errorMsg = embeddingsResult.error || t('codexlens.embeddingsFailed'); handleIndexComplete(false, t('codexlens.ftsSuccessEmbeddingsFailed') || 'FTS index created, but embeddings failed: ' + errorMsg); } else { handleIndexComplete(true, t('codexlens.indexComplete')); } } else if (!result.success) { handleIndexComplete(false, result.error || t('common.unknownError')); } } catch (err) { console.error('[CodexLens] Init error:', err); handleIndexComplete(false, err.message); } } /** * Handle index completion */ function handleIndexComplete(success, message) { var statusText = document.getElementById('codexlensIndexStatus'); var progressBar = document.getElementById('codexlensIndexProgressBar'); var percentText = document.getElementById('codexlensIndexPercent'); var spinner = document.getElementById('codexlensIndexSpinner'); var floatingBar = document.getElementById('codexlensIndexFloating'); // Unregister WebSocket handler if (typeof unregisterWsEventHandler === 'function' && window.codexlensIndexProgressHandler) { unregisterWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler); } if (success) { if (progressBar) progressBar.style.width = '100%'; if (percentText) percentText.textContent = '100%'; if (statusText) statusText.textContent = t('codexlens.indexComplete'); if (spinner) { spinner.classList.remove('animate-spin', 'border-primary'); spinner.classList.add('border-green-500'); spinner.innerHTML = ''; if (window.lucide) lucide.createIcons(); } if (floatingBar) { floatingBar.classList.add('bg-green-500/10'); } showRefreshToast(t('codexlens.indexSuccess'), 'success'); // Auto-close after 3 seconds setTimeout(function() { closeCodexLensIndexModal(); // Refresh status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { renderToolsSection(); if (window.lucide) lucide.createIcons(); }); } }, 3000); } else { if (progressBar) { progressBar.classList.remove('bg-primary'); progressBar.classList.add('bg-destructive'); } if (statusText) statusText.textContent = message || t('codexlens.indexFailed'); if (spinner) { spinner.classList.remove('animate-spin', 'border-primary'); spinner.innerHTML = ''; if (window.lucide) lucide.createIcons(); } if (floatingBar) { floatingBar.classList.add('bg-destructive/10'); } showRefreshToast(t('codexlens.indexFailed') + ': ' + message, 'error'); } } /** * Close floating progress bar */ function closeCodexLensIndexModal() { var floatingBar = document.getElementById('codexlensIndexFloating'); if (floatingBar) { floatingBar.classList.add('translate-y-full'); setTimeout(function() { floatingBar.remove(); }, 300); } // Unregister WebSocket handler if (typeof unregisterWsEventHandler === 'function' && window.codexlensIndexProgressHandler) { unregisterWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler); } } /** * Cancel the running indexing process */ async function cancelCodexLensIndexing() { var cancelBtn = document.getElementById('codexlensIndexCancelBtn'); var statusText = document.getElementById('codexlensIndexStatus'); // Disable button to prevent double-click if (cancelBtn) { cancelBtn.disabled = true; cancelBtn.textContent = t('common.canceling') || 'Canceling...'; } try { var response = await fetch('/api/codexlens/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); var result = await response.json(); if (result.success) { if (statusText) statusText.textContent = t('codexlens.indexCanceled') || 'Indexing canceled'; showRefreshToast(t('codexlens.indexCanceled') || 'Indexing canceled', 'info'); // Close the modal after a short delay setTimeout(function() { closeCodexLensIndexModal(); // Refresh status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { renderToolsSection(); if (window.lucide) lucide.createIcons(); }); } }, 1000); } else { showRefreshToast(t('codexlens.cancelFailed') + ': ' + result.error, 'error'); // Re-enable button on failure if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.textContent = t('common.cancel'); } } } catch (err) { console.error('[CodexLens] Cancel error:', err); showRefreshToast(t('common.error') + ': ' + err.message, 'error'); // Re-enable button on error if (cancelBtn) { cancelBtn.disabled = false; cancelBtn.textContent = t('common.cancel'); } } } /** * Install CodexLens * Note: Uses CodexLens-specific install wizard from cli-status.js * which calls /api/codexlens/bootstrap (Python venv), not the generic * CLI install that uses npm install -g (NPM packages) */ function installCodexLensFromManager() { // Use the CodexLens-specific install wizard from cli-status.js if (typeof openCodexLensInstallWizard === 'function') { openCodexLensInstallWizard(); } else { // Fallback: inline install wizard if cli-status.js not loaded showCodexLensInstallDialog(); } } /** * Fallback install dialog when cli-status.js is not loaded */ function showCodexLensInstallDialog() { var modal = document.createElement('div'); modal.id = 'codexlensInstallModalFallback'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = '
' + '
' + '
' + '
' + '' + '
' + '
' + '

' + t('codexlens.installCodexLens') + '

' + '

' + t('codexlens.installDesc') + '

' + '
' + '
' + '
' + '
' + '

' + t('codexlens.whatWillBeInstalled') + '

' + '
    ' + '
  • ' + '' + '' + t('codexlens.pythonVenv') + ' - ' + t('codexlens.pythonVenvDesc') + '' + '
  • ' + '
  • ' + '' + '' + t('codexlens.codexlensPackage') + ' - ' + t('codexlens.codexlensPackageDesc') + '' + '
  • ' + '
  • ' + '' + 'SQLite FTS5 - ' + t('codexlens.sqliteFtsDesc') + '' + '
  • ' + '
' + '
' + '
' + '
' + '' + '
' + '

' + t('codexlens.installLocation') + '

' + '

~/.codexlens/venv

' + '

' + t('codexlens.installTime') + '

' + '
' + '
' + '
' + '' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(modal); if (window.lucide) lucide.createIcons(); } function closeCodexLensInstallDialogFallback() { var modal = document.getElementById('codexlensInstallModalFallback'); if (modal) modal.remove(); } async function startCodexLensInstallFallback() { var progressDiv = document.getElementById('codexlensInstallProgressFallback'); var installBtn = document.getElementById('codexlensInstallBtnFallback'); var statusText = document.getElementById('codexlensInstallStatusFallback'); var progressBar = document.getElementById('codexlensInstallProgressBarFallback'); progressDiv.classList.remove('hidden'); installBtn.disabled = true; installBtn.innerHTML = '' + t('codexlens.installing') + ''; var stages = [ { progress: 10, text: t('codexlens.creatingVenv') }, { progress: 30, text: t('codexlens.installingPip') }, { progress: 50, text: t('codexlens.installingPackage') }, { progress: 70, text: t('codexlens.settingUpDeps') }, { progress: 90, text: t('codexlens.finalizing') } ]; var currentStage = 0; var progressInterval = setInterval(function() { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = stages[currentStage].progress + '%'; currentStage++; } }, 1500); try { var response = await fetch('/api/codexlens/bootstrap', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); var result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = t('codexlens.installComplete'); setTimeout(function() { closeCodexLensInstallDialogFallback(); showRefreshToast(t('codexlens.installSuccess'), 'success'); // Refresh the page to update status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { if (typeof renderCodexLensManager === 'function') renderCodexLensManager(); }); } else { location.reload(); } }, 1000); } else { statusText.textContent = t('common.error') + ': ' + result.error; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = t('common.error') + ': ' + err.message; progressBar.classList.add('bg-destructive'); installBtn.disabled = false; installBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } /** * Uninstall CodexLens * Note: Uses CodexLens-specific uninstall wizard from cli-status.js * which calls /api/codexlens/uninstall (Python venv), not the generic * CLI uninstall that uses /api/cli/uninstall (NPM packages) */ function uninstallCodexLensFromManager() { // Use the CodexLens-specific uninstall wizard from cli-status.js if (typeof openCodexLensUninstallWizard === 'function') { openCodexLensUninstallWizard(); } else { // Fallback: inline uninstall wizard if cli-status.js not loaded showCodexLensUninstallDialog(); } } /** * Fallback uninstall dialog when cli-status.js is not loaded */ function showCodexLensUninstallDialog() { var modal = document.createElement('div'); modal.id = 'codexlensUninstallModalFallback'; modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50'; modal.innerHTML = '
' + '
' + '
' + '
' + '' + '
' + '
' + '

' + t('codexlens.uninstall') + '

' + '

' + t('codexlens.uninstallDesc') + '

' + '
' + '
' + '
' + '
' + '

' + t('codexlens.whatWillBeRemoved') + '

' + '
    ' + '
  • ' + '' + '' + t('codexlens.removeVenv') + '' + '
  • ' + '
  • ' + '' + '' + t('codexlens.removeData') + '' + '
  • ' + '
  • ' + '' + '' + t('codexlens.removeConfig') + '' + '
  • ' + '
' + '
' + '' + '
' + '
' + '
' + '' + '' + '
' + '
'; document.body.appendChild(modal); if (window.lucide) lucide.createIcons(); } function closeCodexLensUninstallDialogFallback() { var modal = document.getElementById('codexlensUninstallModalFallback'); if (modal) modal.remove(); } async function startCodexLensUninstallFallback() { var progressDiv = document.getElementById('codexlensUninstallProgressFallback'); var uninstallBtn = document.getElementById('codexlensUninstallBtnFallback'); var statusText = document.getElementById('codexlensUninstallStatusFallback'); var progressBar = document.getElementById('codexlensUninstallProgressBarFallback'); progressDiv.classList.remove('hidden'); uninstallBtn.disabled = true; uninstallBtn.innerHTML = '' + t('codexlens.uninstalling') + ''; var stages = [ { progress: 25, text: t('codexlens.removingVenv') }, { progress: 50, text: t('codexlens.removingData') }, { progress: 75, text: t('codexlens.removingConfig') }, { progress: 90, text: t('codexlens.finalizing') } ]; var currentStage = 0; var progressInterval = setInterval(function() { if (currentStage < stages.length) { statusText.textContent = stages[currentStage].text; progressBar.style.width = stages[currentStage].progress + '%'; currentStage++; } }, 500); try { var response = await fetch('/api/codexlens/uninstall', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); clearInterval(progressInterval); var result = await response.json(); if (result.success) { progressBar.style.width = '100%'; statusText.textContent = t('codexlens.uninstallComplete'); setTimeout(function() { closeCodexLensUninstallDialogFallback(); showRefreshToast(t('codexlens.uninstallSuccess'), 'success'); // Refresh the page to update status if (typeof loadCodexLensStatus === 'function') { loadCodexLensStatus().then(function() { if (typeof renderCodexLensManager === 'function') renderCodexLensManager(); }); } else { location.reload(); } }, 1000); } else { statusText.textContent = t('common.error') + ': ' + result.error; progressBar.classList.add('bg-destructive'); uninstallBtn.disabled = false; uninstallBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } catch (err) { clearInterval(progressInterval); statusText.textContent = t('common.error') + ': ' + err.message; progressBar.classList.add('bg-destructive'); uninstallBtn.disabled = false; uninstallBtn.innerHTML = ' ' + t('common.retry'); if (window.lucide) lucide.createIcons(); } } /** * Clean current workspace index */ async function cleanCurrentWorkspaceIndex() { if (!confirm(t('codexlens.cleanCurrentWorkspaceConfirm'))) { return; } try { showRefreshToast(t('codexlens.cleaning'), 'info'); // Get current workspace path (projectPath is a global variable from state.js) var workspacePath = projectPath; var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: workspacePath }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.cleanCurrentWorkspaceSuccess'), 'success'); // Refresh status if (typeof loadCodexLensStatus === 'function') { await loadCodexLensStatus(); renderToolsSection(); if (window.lucide) lucide.createIcons(); } } else { showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Clean all CodexLens indexes */ async function cleanCodexLensIndexes() { if (!confirm(t('codexlens.cleanConfirm'))) { return; } try { showRefreshToast(t('codexlens.cleaning'), 'info'); var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ all: true }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.cleanSuccess'), 'success'); // Refresh status if (typeof loadCodexLensStatus === 'function') { await loadCodexLensStatus(); renderToolsSection(); if (window.lucide) lucide.createIcons(); } } else { showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // ============================================================ // CODEXLENS MANAGER PAGE (Independent View) // ============================================================ /** * Render CodexLens Manager as an independent page view */ async function renderCodexLensManager() { var container = document.getElementById('mainContent'); if (!container) return; // Start preloading immediately (non-blocking) preloadCodexLensData(); // Hide stats grid and search var statsGrid = document.getElementById('statsGrid'); var searchContainer = document.querySelector('.search-container'); if (statsGrid) statsGrid.style.display = 'none'; if (searchContainer) searchContainer.style.display = 'none'; container.innerHTML = '
' + t('common.loading') + '
'; try { // Use aggregated endpoint for faster page load (single API call) var dashboardData = null; var config = { index_dir: '~/.codexlens/indexes', index_count: 0 }; if (typeof loadCodexLensDashboardInit === 'function') { console.log('[CodexLens] Using aggregated dashboard-init endpoint...'); dashboardData = await loadCodexLensDashboardInit(); if (dashboardData && dashboardData.config) { config = dashboardData.config; console.log('[CodexLens] Dashboard init loaded, config:', config); } } else if (typeof loadCodexLensStatus === 'function') { // Fallback to legacy individual calls console.log('[CodexLens] Fallback to legacy loadCodexLensStatus...'); await loadCodexLensStatus(); var response = await fetch('/api/codexlens/config'); config = await response.json(); } // Load LiteLLM API config for embedding backend options (parallel with page render) var litellmPromise = (async () => { try { console.log('[CodexLens] Loading LiteLLM config...'); var litellmResponse = await fetch('/api/litellm-api/config'); if (litellmResponse.ok) { window.litellmApiConfig = await litellmResponse.json(); console.log('[CodexLens] LiteLLM config loaded, providers:', window.litellmApiConfig?.providers?.length || 0); } } catch (e) { console.warn('[CodexLens] Could not load LiteLLM config:', e); } })(); container.innerHTML = buildCodexLensManagerPage(config); if (window.lucide) lucide.createIcons(); initCodexLensManagerPageEvents(config); // Load additional data in parallel (non-blocking) var isInstalled = window.cliToolsStatus?.codexlens?.installed || dashboardData?.installed; // OPTIMIZATION: Load critical status first (workspace index status for header badges) // This is prioritized as it updates the visible header immediately if (isInstalled) { refreshWorkspaceIndexStatus(); // Updates header FTS/Vector badges } // Wait for LiteLLM config before loading semantic deps (it may need provider info) await litellmPromise; // OPTIMIZATION: Batch non-critical loads with requestIdleCallback or setTimeout // This prevents blocking the main thread and allows smoother UI updates var deferredLoads = function() { // Load all independent status checks in parallel Promise.all([ // FastEmbed and semantic deps status (independent) Promise.resolve().then(function() { loadFastEmbedInstallStatus(); }), Promise.resolve().then(function() { loadSemanticDepsStatus(); }), // Model lists (independent) Promise.resolve().then(function() { loadModelList(); }), Promise.resolve().then(function() { loadRerankerModelList(); }), // File watcher status (independent) Promise.resolve().then(function() { initWatcherStatus(); }) ]).then(function() { // After all loads complete, update the semantic status badge updateSemanticStatusBadge(); }); // Load index stats if installed (independent from above) if (isInstalled) { loadIndexStatsForPage(); checkIndexHealth(); } }; // Use requestIdleCallback for deferred loads if available, otherwise use setTimeout if (typeof requestIdleCallback === 'function') { requestIdleCallback(deferredLoads, { timeout: 500 }); } else { setTimeout(deferredLoads, 50); } } catch (err) { container.innerHTML = '

' + t('common.error') + ': ' + escapeHtml(err.message) + '

'; if (window.lucide) lucide.createIcons(); } } /** * Build CodexLens Manager page content */ function buildCodexLensManagerPage(config) { var indexDir = config.index_dir || '~/.codexlens/indexes'; var indexCount = config.index_count || 0; var isInstalled = window.cliToolsStatus?.codexlens?.installed || false; return '
' + // Header with status '
' + '
' + '
' + '
' + '' + '
' + '
' + '

' + t('codexlens.config') + '

' + '

' + t('codexlens.configDesc') + '

' + '
' + '
' + '
' + (isInstalled ? ' ' + t('codexlens.installed') + '' : ' ' + t('codexlens.notInstalled') + '') + '
' + '' + t('codexlens.indexes') + ':' + '' + indexCount + '' + '
' + // Workspace Index Status badges (FTS/Vector percentages) (isInstalled ? '
' + '
' + '' + 'FTS:' + '--' + '
' + '
' + '' + 'Vector:' + '--' + '
' + '
' : '') + '
' + '
' + '
' + (isInstalled ? // Installed: Show full management UI '
' + // Left Column '
' + // Index Management Section - Combined Create Index + Maintenance '
' + '

' + t('codexlens.indexManagement') + '

' + '
' + // Index Actions - Primary buttons '
' + '' + '' + '
' + // Incremental Update button '' + '

' + t('codexlens.indexTypeHint') + '

' + // Maintenance Actions '
' + '
' + '' + '' + '' + '
' + '
' + '
' + '
' + // Storage Path Section '
' + '

' + t('codexlens.indexStoragePath') + '

' + '
' + '
' + '' + '
' + indexDir + '
' + '
' + '
' + '' + '
' + '' + '' + '
' + '

' + t('codexlens.pathInfo') + '

' + '
' + '
' + '
' + // Environment Variables Section '
' + '
' + '

' + t('codexlens.environmentVariables') + '

' + '' + '
' + '
' + '
Click Load to view/edit ~/.codexlens/.env
' + '
' + '
' + // File Watcher Card (moved from right column) '
' + '
' + '
' + '
' + '' + '

File Watcher

' + '
' + '
' + 'Stopped' + '' + '
' + '
' + '
' + '
' + '

Monitor file changes and auto-update index

' + // Stats row '
' + '
' + '
-
' + '
Files
' + '
' + '
' + '
0
' + '
Changes
' + '
' + '
' + '
-
' + '
Uptime
' + '
' + '
' + // Recent activity log '
' + '
' + 'Recent Activity' + '' + '
' + '
' + '
No activity yet. Start watcher to monitor files.
' + '
' + '
' + '
' + '
' + '
' + // Right Column '
' + // FastEmbed Installation Card (shown when not installed) '' + // Combined: Semantic Status + Model Management with Tabs '
' + // Compact Header with Semantic Status '
' + '
' + '

' + t('codexlens.modelManagement') + '

' + '
' + 'Checking...' + '
' + '
' + '
' + // Tabs for Embedding / Reranker '
' + '
' + '' + '' + '
' + '
' + // Tab Content '
' + // Embedding Tab Content '
' + '
' + '
' + '
' + t('codexlens.loadingModels') + '
' + '
' + '
' + // Reranker Tab Content '' + '
' + '
' + '
' + '
' + // Ignore Patterns Section '
' + '
' + '
' + '' + '' + (t('codexlens.ignorePatterns') || 'Ignore Patterns') + '' + '-' + '
' + '
' + '' + '
' + '
' + '
' + '

' + (t('codexlens.ignorePatternsDesc') || 'Configure directories and files to exclude from indexing. Changes apply to new indexes only.') + '

' + '
' + // Directory Patterns '
' + '' + '' + '

' + (t('codexlens.directoryPatternsHint') || 'One pattern per line') + '

' + '
' + // Extension Filters '
' + '' + '' + '

' + (t('codexlens.extensionFiltersHint') || 'Files skipped for embedding') + '

' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + // Index Manager Section '
' + '
' + '
' + '' + '' + t('index.manager') + '' + '-' + '...' + '
' + '
' + '' + '
' + '
' + // Index Health Details '' + '
' + '
' + '' + '' + indexDir + '' + '
' + '
' + '
' + '
-
' + '
' + t('index.projects') + '
' + '
' + '
' + '
-
' + '
' + t('index.totalSize') + '
' + '
' + '
' + '
-
' + '
' + t('index.vectorIndexes') + '
' + '
' + '
' + '
-
' + '
' + t('index.ftsIndexes') + '
' + '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + t('index.projectId') + '' + t('index.size') + '' + t('index.type') + '' + t('index.lastModified') + '
' + t('common.loading') + '
' + '
' + '
' + '' + '
' + '
' + '
' + // Test Search Section '
' + '

' + t('codexlens.testSearch') + '

' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '' + '
' + '
' : // Not installed: Show install prompt '
' + '
' + '
' + '' + '
' + '

' + t('codexlens.installCodexLens') + '

' + '

' + t('codexlens.installFirst') + '

' + '' + '
' + '
' ) + '
'; } /** * Build model select options for the page */ function buildModelSelectOptionsForPage() { var installedModels = window.cliToolsStatus?.codexlens?.installedModels || []; var allModels = window.cliToolsStatus?.codexlens?.allModels || []; if (allModels.length === 0) { // Fallback to default models if not loaded return '' + ''; } var options = ''; allModels.forEach(function(model) { var isInstalled = model.installed || installedModels.includes(model.profile); var label = model.profile + (isInstalled ? ' ✓' : ''); var selected = model.profile === 'code' ? ' selected' : ''; options += ''; }); return options; } /** * Validate concurrency input value (min 1, no max limit) */ function validateConcurrencyInput(input) { var value = parseInt(input.value, 10); if (isNaN(value) || value < 1) { input.value = 1; } } /** * Handle embedding backend change */ function onEmbeddingBackendChange() { var backendSelect = document.getElementById('pageBackendSelect'); var modelSelect = document.getElementById('pageModelSelect'); var concurrencySelector = document.getElementById('concurrencySelector'); var rotationSection = document.getElementById('rotationSection'); if (!backendSelect || !modelSelect) { console.warn('[CodexLens] Backend or model select not found'); return; } var backend = backendSelect.value; console.log('[CodexLens] Backend changed to:', backend); console.log('[CodexLens] Current litellmApiConfig:', window.litellmApiConfig); if (backend === 'litellm') { // Load LiteLLM embedding models console.log('[CodexLens] Building LiteLLM model options...'); var options = buildLiteLLMModelOptions(); console.log('[CodexLens] Built options HTML:', options); modelSelect.innerHTML = options; // Show concurrency selector for API backend if (concurrencySelector) { concurrencySelector.classList.remove('hidden'); } // Show rotation section and load status if (rotationSection) { rotationSection.classList.remove('hidden'); loadRotationStatus(); } } else { // Load local fastembed models modelSelect.innerHTML = buildModelSelectOptionsForPage(); // Hide concurrency selector for local backend if (concurrencySelector) { concurrencySelector.classList.add('hidden'); } // Hide rotation section for local backend if (rotationSection) { rotationSection.classList.add('hidden'); } } } /** * Build LiteLLM model options from config */ function buildLiteLLMModelOptions() { var litellmConfig = window.litellmApiConfig || {}; console.log('[CodexLens] litellmApiConfig:', litellmConfig); var providers = litellmConfig.providers || []; console.log('[CodexLens] providers count:', providers.length); var options = ''; providers.forEach(function(provider) { console.log('[CodexLens] Processing provider:', provider.id, 'enabled:', provider.enabled); if (!provider.enabled) return; // Check embeddingModels array (config structure) var models = provider.embeddingModels || provider.models || []; console.log('[CodexLens] Provider', provider.id, 'embeddingModels:', models.length, models); models.forEach(function(model) { console.log('[CodexLens] Processing model:', model.id, 'type:', model.type, 'enabled:', model.enabled); // Accept embedding type or models from embeddingModels array if (model.type && model.type !== 'embedding') return; if (!model.enabled) return; var label = model.name || model.id; var providerName = provider.name || provider.id; var selected = options === '' ? ' selected' : ''; options += ''; console.log('[CodexLens] Added option:', label, 'from', providerName); }); }); if (options === '') { console.warn('[CodexLens] No embedding models found in LiteLLM config'); options = ''; } return options; } // Make functions globally accessible window.onEmbeddingBackendChange = onEmbeddingBackendChange; /** * Initialize index from page - uses env-based config * Model/backend configured in Environment Variables section */ function initCodexLensIndexFromPage(indexType) { // For FTS-only index, no embedding config needed if (indexType === 'normal') { initCodexLensIndex(indexType); } else { // Use litellm backend with env-configured model (default 4 workers) // The CLI will read EMBEDDING_MODEL/LITELLM_MODEL from env initCodexLensIndex(indexType, null, 'litellm', 4); } } // ============================================================ // INDEX OPERATIONS - 4 Button Functions // ============================================================ /** * Run FTS full index (rebuild full-text search index) * Creates FTS index without embeddings */ window.runFtsFullIndex = async function runFtsFullIndex() { showRefreshToast(t('codexlens.startingFtsFullIndex') || 'Starting FTS full index...', 'info'); // FTS only, no embeddings, full rebuild (incremental=false) initCodexLensIndex('normal', null, 'fastembed', 1, false); } /** * Run FTS incremental update * Updates FTS index for changed files only */ window.runFtsIncrementalUpdate = async function runFtsIncrementalUpdate() { var projectPath = window.CCW_PROJECT_ROOT || '.'; showRefreshToast(t('codexlens.startingFtsIncremental') || 'Starting FTS incremental update...', 'info'); try { // Use index update endpoint for FTS incremental var response = await fetch('/api/codexlens/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath, indexType: 'normal', // FTS only incremental: true }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.ftsIncrementalComplete') || 'FTS incremental update completed', 'success'); renderCodexLensManager(); } else { showRefreshToast((t('codexlens.ftsIncrementalFailed') || 'FTS incremental failed') + ': ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); } } /** * Run Vector full index (generate all embeddings) * Generates embeddings for all files */ window.runVectorFullIndex = async function runVectorFullIndex() { showRefreshToast(t('codexlens.startingVectorFullIndex') || 'Starting Vector full index...', 'info'); try { // Fetch env settings to get the configured embedding model var envResponse = await fetch('/api/codexlens/env'); var envData = await envResponse.json(); var embeddingModel = envData.CODEXLENS_EMBEDDING_MODEL || envData.LITELLM_EMBEDDING_MODEL || 'code'; // Use litellm backend with env-configured model, full rebuild (incremental=false) initCodexLensIndex('vector', embeddingModel, 'litellm', 4, false); } catch (err) { // Fallback to default model if env fetch fails initCodexLensIndex('vector', 'code', 'litellm', 4, false); } } /** * Run Vector incremental update * Generates embeddings for new/changed files only */ window.runVectorIncrementalUpdate = async function runVectorIncrementalUpdate() { var projectPath = window.CCW_PROJECT_ROOT || '.'; showRefreshToast(t('codexlens.startingVectorIncremental') || 'Starting Vector incremental update...', 'info'); try { // Fetch env settings to get the configured embedding model var envResponse = await fetch('/api/codexlens/env'); var envData = await envResponse.json(); var embeddingModel = envData.CODEXLENS_EMBEDDING_MODEL || envData.LITELLM_EMBEDDING_MODEL || null; // Use embeddings endpoint for vector incremental var requestBody = { path: projectPath, incremental: true, // Only new/changed files backend: 'litellm', maxWorkers: 4 }; // Add model if configured in env if (embeddingModel) { requestBody.model = embeddingModel; } var response = await fetch('/api/codexlens/embeddings/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); var result = await response.json(); if (result.success) { var stats = result.result || {}; var msg = (t('codexlens.vectorIncrementalComplete') || 'Vector incremental completed') + (stats.chunks_created ? ': ' + stats.chunks_created + ' chunks' : ''); showRefreshToast(msg, 'success'); renderCodexLensManager(); } else { showRefreshToast((t('codexlens.vectorIncrementalFailed') || 'Vector incremental failed') + ': ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); } } /** * Run incremental update on the current workspace index */ window.runIncrementalUpdate = async function runIncrementalUpdate() { var projectPath = window.CCW_PROJECT_ROOT || '.'; showRefreshToast('Starting incremental update...', 'info'); try { var response = await fetch('/api/codexlens/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath }) }); var result = await response.json(); if (result.success) { showRefreshToast('Incremental update completed', 'success'); } else { showRefreshToast('Update failed: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { showRefreshToast('Update error: ' + err.message, 'error'); } } /** * Toggle file watcher (watchdog) on/off */ window.toggleWatcher = async function toggleWatcher() { console.log('[CodexLens] toggleWatcher called'); // Debug: uncomment to test if function is called // alert('toggleWatcher called!'); var projectPath = window.CCW_PROJECT_ROOT || '.'; console.log('[CodexLens] Project path:', projectPath); // Check current status first try { console.log('[CodexLens] Checking watcher status...'); // Pass path parameter to get specific watcher status var statusResponse = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath)); var statusResult = await statusResponse.json(); console.log('[CodexLens] Status result:', statusResult); // Handle both single watcher response and array response var isRunning = false; if (statusResult.success) { if (typeof statusResult.running === 'boolean') { isRunning = statusResult.running; } else if (statusResult.watchers && Array.isArray(statusResult.watchers)) { var normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/'); var matchingWatcher = statusResult.watchers.find(function(w) { var watcherPath = (w.root_path || '').toLowerCase().replace(/\\/g, '/'); return watcherPath === normalizedPath || watcherPath.includes(normalizedPath) || normalizedPath.includes(watcherPath); }); isRunning = matchingWatcher ? matchingWatcher.running : false; } } // Toggle: if running, stop; if stopped, start var action = isRunning ? 'stop' : 'start'; console.log('[CodexLens] Action:', action); var response = await fetch('/api/codexlens/watch/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: projectPath }) }); var result = await response.json(); console.log('[CodexLens] Action result:', result); if (result.success) { var newRunning = action === 'start'; updateWatcherUI(newRunning); showRefreshToast('File watcher ' + (newRunning ? 'started' : 'stopped'), 'success'); } else { showRefreshToast('Watcher ' + action + ' failed: ' + (result.error || 'Unknown error'), 'error'); } } catch (err) { console.error('[CodexLens] Watcher error:', err); showRefreshToast('Watcher error: ' + err.message, 'error'); } } /** * Update watcher UI state */ function updateWatcherUI(running, stats) { var statusBadge = document.getElementById('watcherStatusBadge'); if (statusBadge) { var badgeClass = running ? 'bg-success/20 text-success' : 'bg-muted text-muted-foreground'; var badgeText = running ? 'Running' : 'Stopped'; var iconName = running ? 'pause' : 'play'; statusBadge.innerHTML = '' + badgeText + '' + ''; if (window.lucide) lucide.createIcons(); } // Update stats if provided if (stats) { var filesCount = document.getElementById('watcherFilesCount'); var changesCount = document.getElementById('watcherChangesCount'); var uptimeDisplay = document.getElementById('watcherUptimeDisplay'); if (filesCount) filesCount.textContent = stats.files_watched || '-'; // Support both changes_detected and events_processed if (changesCount) changesCount.textContent = stats.events_processed || stats.changes_detected || '0'; if (uptimeDisplay) uptimeDisplay.textContent = formatUptime(stats.uptime_seconds); } // Start or stop polling based on running state if (running) { startWatcherPolling(); } else { stopWatcherPolling(); } } // Watcher polling interval var watcherPollInterval = null; var watcherStartTime = null; var watcherChangesCount = 0; /** * Format uptime in human readable format */ function formatUptime(seconds) { if (!seconds || seconds < 0) return '-'; if (seconds < 60) return Math.floor(seconds) + 's'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm'; var hours = Math.floor(seconds / 3600); var mins = Math.floor((seconds % 3600) / 60); return hours + 'h ' + mins + 'm'; } /** * Start polling watcher status */ function startWatcherPolling() { if (watcherPollInterval) return; // Already polling watcherStartTime = Date.now(); var projectPath = window.CCW_PROJECT_ROOT || '.'; watcherPollInterval = setInterval(async function() { try { // Must include path parameter to get specific watcher status var response = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath)); var result = await response.json(); if (result.success && result.running) { // Update uptime from server response var uptimeDisplay = document.getElementById('watcherUptimeDisplay'); if (uptimeDisplay && result.uptime_seconds !== undefined) { uptimeDisplay.textContent = formatUptime(result.uptime_seconds); } // Update changes count from events_processed if (result.events_processed !== undefined) { var changesCount = document.getElementById('watcherChangesCount'); if (changesCount) changesCount.textContent = result.events_processed; } // Update files count if available if (result.files_watched !== undefined) { var filesCount = document.getElementById('watcherFilesCount'); if (filesCount) filesCount.textContent = result.files_watched; } // Check for new events if (result.recent_events && result.recent_events.length > 0) { result.recent_events.forEach(function(event) { addWatcherLogEntry(event.type, event.path); }); } } else if (result.success && result.running === false) { // Watcher stopped externally (only if running is explicitly false) updateWatcherUI(false); stopWatcherPolling(); } } catch (err) { console.warn('[Watcher] Poll error:', err); } }, 3000); // Poll every 3 seconds } /** * Stop polling watcher status */ function stopWatcherPolling() { if (watcherPollInterval) { clearInterval(watcherPollInterval); watcherPollInterval = null; } watcherStartTime = null; } /** * Add entry to watcher activity log */ function addWatcherLogEntry(type, path) { var logContainer = document.getElementById('watcherActivityLog'); if (!logContainer) return; // Clear "no activity" message if present var noActivity = logContainer.querySelector('.text-muted-foreground:only-child'); if (noActivity && noActivity.textContent.includes('No activity')) { logContainer.innerHTML = ''; } // Increment changes count watcherChangesCount++; var changesCount = document.getElementById('watcherChangesCount'); if (changesCount) changesCount.textContent = watcherChangesCount; // Create log entry var timestamp = new Date().toLocaleTimeString(); var typeColors = { 'created': 'text-success', 'modified': 'text-warning', 'deleted': 'text-destructive', 'renamed': 'text-primary', 'indexed': 'text-success' }; var typeIcons = { 'created': '+', 'modified': '~', 'deleted': '-', 'renamed': '→', 'indexed': '✓' }; var colorClass = typeColors[type] || 'text-muted-foreground'; var icon = typeIcons[type] || '•'; // Get just the filename var filename = path.split(/[/\\]/).pop(); var entry = document.createElement('div'); entry.className = 'flex items-center gap-2 py-0.5'; entry.innerHTML = '' + timestamp + '' + '' + icon + '' + '' + escapeHtml(filename) + ''; // Add to top of log logContainer.insertBefore(entry, logContainer.firstChild); // Keep only last 50 entries while (logContainer.children.length > 50) { logContainer.removeChild(logContainer.lastChild); } } /** * Clear watcher activity log */ function clearWatcherLog() { var logContainer = document.getElementById('watcherActivityLog'); if (logContainer) { logContainer.innerHTML = '
Log cleared. Waiting for file changes...
'; } watcherChangesCount = 0; var changesCount = document.getElementById('watcherChangesCount'); if (changesCount) changesCount.textContent = '0'; } /** * Initialize watcher status on page load */ async function initWatcherStatus() { try { var projectPath = window.CCW_PROJECT_ROOT || '.'; // Pass path parameter to get specific watcher status var response = await fetch('/api/codexlens/watch/status?path=' + encodeURIComponent(projectPath)); var result = await response.json(); if (result.success) { // Handle both single watcher response (with path param) and array response (without path param) var running = result.running; var uptime = result.uptime_seconds || 0; var filesWatched = result.files_watched; // If response has watchers array (no path param), find matching watcher if (result.watchers && Array.isArray(result.watchers)) { var normalizedPath = projectPath.toLowerCase().replace(/\\/g, '/'); var matchingWatcher = result.watchers.find(function(w) { var watcherPath = (w.root_path || '').toLowerCase().replace(/\\/g, '/'); return watcherPath === normalizedPath || watcherPath.includes(normalizedPath) || normalizedPath.includes(watcherPath); }); if (matchingWatcher) { running = matchingWatcher.running; uptime = matchingWatcher.uptime_seconds || 0; } else { running = false; } } updateWatcherUI(running, { files_watched: filesWatched, changes_detected: 0, uptime_seconds: uptime }); } } catch (err) { console.warn('[Watcher] Failed to get initial status:', err); } } // Make functions globally accessible window.runIncrementalUpdate = runIncrementalUpdate; window.toggleWatcher = toggleWatcher; window.updateWatcherUI = updateWatcherUI; window.addWatcherLogEntry = addWatcherLogEntry; window.clearWatcherLog = clearWatcherLog; window.initWatcherStatus = initWatcherStatus; /** * Initialize CodexLens Manager page event handlers */ function initCodexLensManagerPageEvents(currentConfig) { var saveBtn = document.getElementById('saveIndexPathBtn'); if (saveBtn) { saveBtn.onclick = async function() { var indexDirInput = document.getElementById('indexDirInput'); var newIndexDir = indexDirInput ? indexDirInput.value.trim() : ''; if (!newIndexDir) { showRefreshToast(t('codexlens.pathEmpty'), 'error'); return; } if (newIndexDir === currentConfig.index_dir) { showRefreshToast(t('codexlens.pathUnchanged'), 'info'); return; } saveBtn.disabled = true; saveBtn.innerHTML = '' + t('common.saving') + ''; try { var response = await csrfFetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ index_dir: newIndexDir }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.configSaved'), 'success'); renderCodexLensManager(); } else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } saveBtn.disabled = false; saveBtn.innerHTML = ' ' + t('codexlens.saveConfig'); if (window.lucide) lucide.createIcons(); }; } var runSearchBtn = document.getElementById('runSearchBtn'); if (runSearchBtn) { runSearchBtn.onclick = async function() { var searchType = document.getElementById('searchTypeSelect').value; var searchMode = document.getElementById('searchModeSelect').value; var query = document.getElementById('searchQueryInput').value.trim(); var resultsDiv = document.getElementById('searchResults'); var resultCount = document.getElementById('searchResultCount'); var resultContent = document.getElementById('searchResultContent'); if (!query) { showRefreshToast(t('codexlens.enterQuery'), 'warning'); return; } runSearchBtn.disabled = true; runSearchBtn.innerHTML = '' + t('codexlens.searching') + ''; resultsDiv.classList.add('hidden'); try { var endpoint = '/api/codexlens/' + searchType; var params = new URLSearchParams({ query: query, limit: '20' }); if (searchType === 'search' || searchType === 'search_files') { params.append('mode', searchMode); } var response = await fetch(endpoint + '?' + params.toString()); var result = await response.json(); if (result.success) { var results = result.results || result.files || []; resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount'); resultContent.textContent = JSON.stringify(results, null, 2); resultsDiv.classList.remove('hidden'); } else { resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError')); resultsDiv.classList.remove('hidden'); } } catch (err) { resultContent.textContent = t('common.exception') + ': ' + err.message; resultsDiv.classList.remove('hidden'); } runSearchBtn.disabled = false; runSearchBtn.innerHTML = ' ' + t('codexlens.runSearch'); if (window.lucide) lucide.createIcons(); }; } var searchInput = document.getElementById('searchQueryInput'); if (searchInput) { searchInput.onkeypress = function(e) { if (e.key === 'Enter' && runSearchBtn) { runSearchBtn.click(); } }; } // Initialize ignore patterns count badge (delayed to ensure function is defined) setTimeout(function() { if (typeof initIgnorePatternsCount === 'function') { initIgnorePatternsCount(); } }, 100); } /** * Show index initialization modal */ function showIndexInitModal() { // Use initCodexLensIndex with default settings initCodexLensIndex('vector', 'code'); } /** * Load index stats for the CodexLens Manager page */ async function loadIndexStatsForPage() { try { var response = await fetch('/api/codexlens/indexes'); if (!response.ok) throw new Error('Failed to load index stats'); var data = await response.json(); renderIndexStatsForPage(data); } catch (err) { console.error('[CodexLens] Failed to load index stats:', err); var tbody = document.getElementById('indexTableBody'); if (tbody) { tbody.innerHTML = '' + escapeHtml(err.message) + ''; } } } /** * Render index stats in the CodexLens Manager page */ function renderIndexStatsForPage(data) { var summary = data.summary || {}; var indexes = data.indexes || []; var indexDir = data.indexDir || ''; // Update summary stats var totalSizeEl = document.getElementById('indexTotalSize'); var projectCountEl = document.getElementById('indexProjectCount'); var totalSizeValEl = document.getElementById('indexTotalSizeVal'); var vectorCountEl = document.getElementById('indexVectorCount'); var ftsCountEl = document.getElementById('indexFtsCount'); var indexDirEl = document.getElementById('indexDirDisplay'); if (totalSizeEl) totalSizeEl.textContent = summary.totalSizeFormatted || '0 B'; if (projectCountEl) projectCountEl.textContent = summary.totalProjects || 0; if (totalSizeValEl) totalSizeValEl.textContent = summary.totalSizeFormatted || '0 B'; if (vectorCountEl) vectorCountEl.textContent = summary.vectorIndexCount || 0; if (ftsCountEl) ftsCountEl.textContent = summary.normalIndexCount || 0; if (indexDirEl && indexDir) { indexDirEl.textContent = indexDir; indexDirEl.title = indexDir; } // Render table rows var tbody = document.getElementById('indexTableBody'); if (!tbody) return; if (indexes.length === 0) { tbody.innerHTML = '' + (t('index.noIndexes') || 'No indexes yet') + ''; return; } var rows = ''; indexes.forEach(function(idx) { var vectorBadge = idx.hasVectorIndex ? '' + (t('index.vector') || 'Vector') + '' : ''; var normalBadge = idx.hasNormalIndex ? '' + (t('index.fts') || 'FTS') + '' : ''; rows += '' + '' + '' + escapeHtml(idx.id) + '' + '' + '' + (idx.sizeFormatted || '-') + '' + '
' + vectorBadge + normalBadge + '
' + '' + formatTimeAgoSimple(idx.lastModified) + '' + '' + '' + '' + ''; }); tbody.innerHTML = rows; if (window.lucide) lucide.createIcons(); } /** * Simple time ago formatter */ function formatTimeAgoSimple(isoString) { if (!isoString) return t('common.never') || 'Never'; var date = new Date(isoString); var now = new Date(); var diffMs = now - date; var diffMins = Math.floor(diffMs / 60000); var diffHours = Math.floor(diffMins / 60); var diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return t('common.justNow') || 'Just now'; if (diffMins < 60) return diffMins + 'm ' + (t('common.ago') || 'ago'); if (diffHours < 24) return diffHours + 'h ' + (t('common.ago') || 'ago'); if (diffDays < 30) return diffDays + 'd ' + (t('common.ago') || 'ago'); return date.toLocaleDateString(); } /** * Check and display index health for current workspace */ async function checkIndexHealth() { var healthBadge = document.getElementById('indexHealthBadge'); var healthDetails = document.getElementById('indexHealthDetails'); var lastUpdateEl = document.getElementById('indexLastUpdate'); var commitsSinceEl = document.getElementById('indexCommitsSince'); if (!healthBadge) return; try { // Get current workspace index info var indexResponse = await fetch('/api/codexlens/indexes'); var indexData = await indexResponse.json(); var indexes = indexData.indexes || []; // Find current workspace index (newest one or matching current path) var currentIndex = indexes.length > 0 ? indexes[0] : null; if (!currentIndex) { healthBadge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground'; healthBadge.textContent = 'No Index'; if (healthDetails) healthDetails.classList.add('hidden'); return; } var lastIndexTime = currentIndex.lastModified ? new Date(currentIndex.lastModified) : null; // Estimate staleness based on time (git API not available) var commitsSince = 0; if (lastIndexTime) { var hoursSince = (Date.now() - lastIndexTime.getTime()) / (1000 * 60 * 60); // Rough estimate: assume ~2 commits per hour on active projects commitsSince = Math.floor(hoursSince / 2); } // Determine health status var healthStatus = 'good'; var healthText = 'Up to date'; var healthClass = 'bg-success/20 text-success'; if (commitsSince > 50 || (lastIndexTime && (Date.now() - lastIndexTime.getTime()) > 7 * 24 * 60 * 60 * 1000)) { // More than 50 commits or 7 days old healthStatus = 'outdated'; healthText = 'Outdated'; healthClass = 'bg-destructive/20 text-destructive'; } else if (commitsSince > 10 || (lastIndexTime && (Date.now() - lastIndexTime.getTime()) > 24 * 60 * 60 * 1000)) { // More than 10 commits or 1 day old healthStatus = 'stale'; healthText = 'Stale'; healthClass = 'bg-warning/20 text-warning'; } // Update badge healthBadge.className = 'text-xs px-2 py-0.5 rounded-full ' + healthClass; healthBadge.textContent = healthText; // Update details section if (healthDetails && healthStatus !== 'good') { healthDetails.classList.remove('hidden'); if (lastUpdateEl) lastUpdateEl.textContent = lastIndexTime ? formatTimeAgoSimple(currentIndex.lastModified) : 'Unknown'; if (commitsSinceEl) { commitsSinceEl.textContent = commitsSince; commitsSinceEl.className = 'font-medium ' + (commitsSince > 20 ? 'text-destructive' : commitsSince > 5 ? 'text-warning' : 'text-foreground'); } } else if (healthDetails) { healthDetails.classList.add('hidden'); } } catch (err) { console.error('[CodexLens] Failed to check index health:', err); healthBadge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground'; healthBadge.textContent = 'Unknown'; } } // Make function globally accessible window.checkIndexHealth = checkIndexHealth; /** * Clean a specific project's index from the page */ async function cleanIndexProjectFromPage(projectId) { if (!confirm((t('index.cleanProjectConfirm') || 'Clean index for') + ' ' + projectId + '?')) { return; } try { showRefreshToast(t('index.cleaning') || 'Cleaning index...', 'info'); var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId: projectId }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('index.cleanSuccess') || 'Index cleaned successfully', 'success'); await loadIndexStatsForPage(); } else { showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); } } /** * Clean all indexes from the page */ async function cleanAllIndexesFromPage() { if (!confirm(t('index.cleanAllConfirm') || 'Are you sure you want to clean ALL indexes? This cannot be undone.')) { return; } try { showRefreshToast(t('index.cleaning') || 'Cleaning indexes...', 'info'); var response = await fetch('/api/codexlens/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ all: true }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('index.cleanAllSuccess') || 'All indexes cleaned', 'success'); await loadIndexStatsForPage(); } else { showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); } } // ============================================================ // MULTI-PROVIDER ROTATION CONFIGURATION // ============================================================ /** * Load and display rotation status in the page */ async function loadRotationStatus() { try { // Load from unified embedding-pool API (handles both new and legacy config) var response = await fetch('/api/litellm-api/embedding-pool'); if (!response.ok) { console.warn('[CodexLens] Failed to load embedding pool config:', response.status); return; } var data = await response.json(); window.embeddingPoolConfig = data.poolConfig; window.embeddingPoolAvailableModels = data.availableModels || []; // Also get endpoint count var endpointsResponse = await fetch('/api/litellm-api/codexlens/rotation/endpoints'); var endpointsData = endpointsResponse.ok ? await endpointsResponse.json() : { count: 0 }; updateRotationStatusDisplay(data.poolConfig, endpointsData.count); } catch (err) { console.error('[CodexLens] Error loading rotation status:', err); } } /** * Update the rotation status display in the page * @param {Object} poolConfig - The embedding pool configuration * @param {number} endpointCount - Number of active endpoints */ function updateRotationStatusDisplay(poolConfig, endpointCount) { var badge = document.getElementById('rotationStatusBadge'); var detailsEl = document.getElementById('rotationDetails'); var modelNameEl = document.getElementById('rotationModelName'); var countEl = document.getElementById('rotationEndpointCount'); if (!badge) return; if (poolConfig && poolConfig.enabled) { badge.textContent = t('common.enabled'); badge.className = 'text-xs px-2 py-0.5 rounded-full bg-success/10 text-success'; // Show details if (detailsEl) { detailsEl.classList.remove('hidden'); if (modelNameEl) modelNameEl.textContent = poolConfig.targetModel || ''; if (countEl) countEl.textContent = (endpointCount || 0) + ' ' + t('codexlens.totalEndpoints').toLowerCase(); } } else { badge.textContent = t('common.disabled'); badge.className = 'text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground'; if (detailsEl) detailsEl.classList.add('hidden'); } } /** * Navigate to API Settings Embedding Pool tab */ function navigateToApiSettingsEmbeddingPool() { // Navigate to API Settings page with embedding-pool tab if (typeof switchView === 'function') { switchView('api-settings'); // Give time for page to render, then switch to embedding-pool tab setTimeout(function() { if (typeof switchSidebarTab === 'function') { switchSidebarTab('embedding-pool'); } }, 100); } } /** * Show the rotation configuration modal */ async function showRotationConfigModal() { try { // Load current config if not already loaded if (!window.rotationConfig) { await loadRotationStatus(); } var rotationConfig = window.rotationConfig || { enabled: false, strategy: 'round_robin', defaultCooldown: 60, targetModel: 'qwen3-embedding', providers: [] }; var availableProviders = window.availableRotationProviders || []; var modalHtml = buildRotationConfigModal(rotationConfig, availableProviders); var tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; var modal = tempContainer.firstElementChild; document.body.appendChild(modal); if (window.lucide) lucide.createIcons(); initRotationConfigEvents(rotationConfig, availableProviders); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build the rotation configuration modal HTML */ function buildRotationConfigModal(rotationConfig, availableProviders) { var isEnabled = rotationConfig.enabled || false; var strategy = rotationConfig.strategy || 'round_robin'; var cooldown = rotationConfig.defaultCooldown || 60; var targetModel = rotationConfig.targetModel || 'qwen3-embedding'; var configuredProviders = rotationConfig.providers || []; // Build provider list HTML var providerListHtml = ''; if (availableProviders.length === 0) { providerListHtml = '
' + t('codexlens.noRotationProviders') + '
'; } else { availableProviders.forEach(function(provider, index) { // Find if this provider is already configured var configured = configuredProviders.find(function(p) { return p.providerId === provider.providerId; }); var isProviderEnabled = configured ? configured.enabled : false; var weight = configured ? configured.weight : 1; var maxConcurrent = configured ? configured.maxConcurrentPerKey : 4; var useAllKeys = configured ? configured.useAllKeys : true; // Get model options var modelOptions = provider.embeddingModels.map(function(m) { var selected = configured && configured.modelId === m.modelId ? 'selected' : ''; return ''; }).join(''); // Get key count var keyCount = provider.apiKeys.filter(function(k) { return k.enabled; }).length; providerListHtml += '
' + '
' + '
' + '' + '' + '' + keyCount + ' keys' + '
' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
'; }); } return ''; } /** * Initialize rotation config modal events */ function initRotationConfigEvents(rotationConfig, availableProviders) { // Store in window for save function window._rotationAvailableProviders = availableProviders; } /** * Close the rotation config modal */ function closeRotationModal() { var modal = document.getElementById('rotationConfigModal'); if (modal) modal.remove(); } /** * Save the rotation configuration */ async function saveRotationConfig() { try { var enabledToggle = document.getElementById('rotationEnabledToggle'); var strategySelect = document.getElementById('rotationStrategy'); var cooldownInput = document.getElementById('rotationCooldown'); var targetModelInput = document.getElementById('rotationTargetModel'); var enabled = enabledToggle ? enabledToggle.checked : false; var strategy = strategySelect ? strategySelect.value : 'round_robin'; var cooldown = cooldownInput ? parseInt(cooldownInput.value, 10) : 60; var targetModel = targetModelInput ? targetModelInput.value.trim() : 'qwen3-embedding'; // Collect provider configurations var providers = []; var providerToggles = document.querySelectorAll('.rotation-provider-toggle'); providerToggles.forEach(function(toggle) { var providerId = toggle.getAttribute('data-provider-id'); var isEnabled = toggle.checked; var modelSelect = document.querySelector('.rotation-model-select[data-provider-id="' + providerId + '"]'); var weightInput = document.querySelector('.rotation-weight-input[data-provider-id="' + providerId + '"]'); var concurrentInput = document.querySelector('.rotation-concurrent-input[data-provider-id="' + providerId + '"]'); var useAllKeysToggle = document.querySelector('.rotation-use-all-keys[data-provider-id="' + providerId + '"]'); providers.push({ providerId: providerId, modelId: modelSelect ? modelSelect.value : '', weight: weightInput ? parseFloat(weightInput.value) || 1 : 1, maxConcurrentPerKey: concurrentInput ? parseInt(concurrentInput.value, 10) || 4 : 4, useAllKeys: useAllKeysToggle ? useAllKeysToggle.checked : true, enabled: isEnabled }); }); var rotationConfig = { enabled: enabled, strategy: strategy, defaultCooldown: cooldown, targetModel: targetModel, providers: providers }; var response = await fetch('/api/litellm-api/codexlens/rotation', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(rotationConfig) }); var result = await response.json(); if (result.success) { // Show sync result in toast var syncMsg = ''; if (result.syncResult) { if (result.syncResult.success) { syncMsg = ' (' + result.syncResult.endpointCount + ' ' + t('codexlens.endpointsSynced') + ')'; } else { syncMsg = ' (' + t('codexlens.syncFailed') + ': ' + result.syncResult.message + ')'; } } showRefreshToast(t('codexlens.rotationSaved') + syncMsg, 'success'); window.rotationConfig = rotationConfig; updateRotationStatusDisplay(rotationConfig); closeRotationModal(); } else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // ============================================================ // RERANKER CONFIGURATION MODAL // ============================================================ /** * Show Reranker configuration modal */ async function showRerankerConfigModal() { try { showRefreshToast(t('codexlens.loadingRerankerConfig') || 'Loading reranker configuration...', 'info'); // Fetch current reranker config const response = await fetch('/api/codexlens/reranker/config'); const config = await response.json(); if (!config.success) { showRefreshToast(t('common.error') + ': ' + (config.error || 'Failed to load config'), 'error'); return; } const modalHtml = buildRerankerConfigContent(config); // Create and show modal const tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; const modal = tempContainer.firstElementChild; document.body.appendChild(modal); // Initialize icons if (window.lucide) lucide.createIcons(); // Initialize event handlers initRerankerConfigEvents(config); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build Reranker configuration modal content */ function buildRerankerConfigContent(config) { const backend = config.backend || 'onnx'; const modelName = config.model_name || ''; const apiProvider = config.api_provider || 'siliconflow'; const apiKeySet = config.api_key_set || false; const availableBackends = config.available_backends || ['onnx', 'api', 'litellm', 'legacy']; const apiProviders = config.api_providers || ['siliconflow', 'cohere', 'jina']; const litellmEndpoints = config.litellm_endpoints || []; const litellmModels = config.litellm_models || []; // Rich model info with providers // ONNX models const onnxModels = [ 'cross-encoder/ms-marco-MiniLM-L-6-v2', 'cross-encoder/ms-marco-TinyBERT-L-2-v2', 'BAAI/bge-reranker-base', 'BAAI/bge-reranker-large' ]; // Build backend options const hasLitellmModels = litellmModels.length > 0 || litellmEndpoints.length > 0; const backendOptions = availableBackends.map(function(b) { const labels = { 'onnx': 'ONNX (Local, Optimum)', 'api': 'API (Manual Config)', 'litellm': hasLitellmModels ? 'LiteLLM (Auto-configured)' : 'LiteLLM (Not configured)', 'legacy': 'Legacy (SentenceTransformers)' }; return ''; }).join(''); // Build API provider options const providerOptions = apiProviders.map(function(p) { return ''; }).join(''); // Build ONNX model options const onnxModelOptions = onnxModels.map(function(m) { return ''; }).join(''); // Build LiteLLM model options (use rich model data if available) const litellmOptions = litellmModels.length > 0 ? litellmModels.map(function(m) { // Display: "ModelName (Provider)" for better UX const providerNames = m.providers && m.providers.length > 0 ? m.providers.join(', ') : 'Unknown'; const displayName = m.modelName + ' (' + providerNames + ')'; return ''; }).join('') : (litellmEndpoints.length > 0 ? litellmEndpoints.map(function(ep) { return ''; }).join('') : ''); return ''; } /** * Toggle reranker configuration sections based on selected backend */ function toggleRerankerSections() { var backend = document.getElementById('rerankerBackend').value; document.getElementById('rerankerOnnxSection').style.display = backend === 'onnx' ? 'block' : 'none'; document.getElementById('rerankerApiSection').style.display = backend === 'api' ? 'block' : 'none'; document.getElementById('rerankerLitellmSection').style.display = backend === 'litellm' ? 'block' : 'none'; document.getElementById('rerankerLegacySection').style.display = backend === 'legacy' ? 'block' : 'none'; } /** * Initialize reranker config modal events */ function initRerankerConfigEvents(config) { // Handle ONNX model custom input toggle var onnxModelSelect = document.getElementById('rerankerOnnxModel'); var customModelInput = document.getElementById('rerankerCustomModel'); if (onnxModelSelect && customModelInput) { onnxModelSelect.addEventListener('change', function() { customModelInput.style.display = this.value === 'custom' ? 'block' : 'none'; }); } // Store original config for reset window._rerankerOriginalConfig = config; } /** * Close the reranker config modal */ function closeRerankerModal() { var modal = document.getElementById('rerankerConfigModal'); if (modal) modal.remove(); } /** * Reset reranker config to original values */ function resetRerankerConfig() { var config = window._rerankerOriginalConfig; if (!config) return; document.getElementById('rerankerBackend').value = config.backend || 'onnx'; toggleRerankerSections(); // Reset ONNX section var onnxModels = [ 'cross-encoder/ms-marco-MiniLM-L-6-v2', 'cross-encoder/ms-marco-TinyBERT-L-2-v2', 'BAAI/bge-reranker-base', 'BAAI/bge-reranker-large' ]; if (onnxModels.includes(config.model_name)) { document.getElementById('rerankerOnnxModel').value = config.model_name; document.getElementById('rerankerCustomModel').style.display = 'none'; } else { document.getElementById('rerankerOnnxModel').value = 'custom'; document.getElementById('rerankerCustomModel').value = config.model_name || ''; document.getElementById('rerankerCustomModel').style.display = 'block'; } // Reset API section document.getElementById('rerankerApiProvider').value = config.api_provider || 'siliconflow'; document.getElementById('rerankerApiKey').value = ''; document.getElementById('rerankerApiModel').value = config.model_name || ''; showRefreshToast(t('common.reset') || 'Reset to original values', 'info'); } /** * Save reranker configuration */ async function saveRerankerConfig() { try { var backend = document.getElementById('rerankerBackend').value; var payload = { backend: backend }; // Collect model name based on backend if (backend === 'onnx') { var onnxModel = document.getElementById('rerankerOnnxModel').value; if (onnxModel === 'custom') { payload.model_name = document.getElementById('rerankerCustomModel').value.trim(); } else { payload.model_name = onnxModel; } } else if (backend === 'api') { payload.api_provider = document.getElementById('rerankerApiProvider').value; payload.model_name = document.getElementById('rerankerApiModel').value.trim(); var apiKey = document.getElementById('rerankerApiKey').value.trim(); if (apiKey) { payload.api_key = apiKey; } } else if (backend === 'litellm') { payload.litellm_endpoint = document.getElementById('rerankerLitellmEndpoint').value; } var response = await fetch('/api/codexlens/reranker/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); var result = await response.json(); if (result.success) { showRefreshToast((t('codexlens.rerankerConfigSaved') || 'Reranker configuration saved') + ': ' + result.message, 'success'); closeRerankerModal(); } else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // ============================================================ // FILE WATCHER CONTROL // ============================================================ /** * Show File Watcher control modal */ async function showWatcherControlModal() { try { showRefreshToast(t('codexlens.loadingWatcherStatus') || 'Loading watcher status...', 'info'); // Fetch current watcher status and indexed projects in parallel const [statusResponse, indexesResponse] = await Promise.all([ fetch('/api/codexlens/watch/status'), fetch('/api/codexlens/indexes') ]); const status = await statusResponse.json(); const indexes = await indexesResponse.json(); // Get first indexed project path as default let defaultPath = ''; if (indexes.success && indexes.projects && indexes.projects.length > 0) { // Sort by last_indexed desc and pick the most recent const sorted = indexes.projects.sort((a, b) => new Date(b.last_indexed || 0) - new Date(a.last_indexed || 0) ); defaultPath = sorted[0].source_root || ''; } const modalHtml = buildWatcherControlContent(status, defaultPath); // Create and show modal const tempContainer = document.createElement('div'); tempContainer.innerHTML = modalHtml; const modal = tempContainer.firstElementChild; document.body.appendChild(modal); // Initialize icons if (window.lucide) lucide.createIcons(); // Start polling if watcher is running if (status.running) { startWatcherStatusPolling(); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } /** * Build File Watcher control modal content * @param {Object} status - Watcher status * @param {string} defaultPath - Default path from indexed projects */ function buildWatcherControlContent(status, defaultPath) { const running = status.running || false; defaultPath = defaultPath || ''; const rootPath = status.root_path || ''; const eventsProcessed = status.events_processed || 0; const uptimeSeconds = status.uptime_seconds || 0; // Format uptime const formatUptime = function(seconds) { if (seconds < 60) return seconds + 's'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's'; return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; }; return ''; } /** * Toggle file watcher on/off */ async function toggleWatcher() { var toggle = document.getElementById('watcherToggle'); var shouldRun = toggle.checked; try { if (shouldRun) { // Start watcher var watchPath = document.getElementById('watcherPath').value.trim(); var debounceMs = parseInt(document.getElementById('watcherDebounce').value, 10) || 1000; var response = await fetch('/api/codexlens/watch/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: watchPath || undefined, debounce_ms: debounceMs }) }); var result = await response.json(); if (result.success) { showRefreshToast((t('codexlens.watcherStarted') || 'Watcher started') + ': ' + result.path, 'success'); document.getElementById('watcherStats').style.display = 'block'; document.getElementById('watcherStartConfig').style.display = 'none'; startWatcherStatusPolling(); } else { toggle.checked = false; showRefreshToast(t('common.error') + ': ' + result.error, 'error'); } } else { // Stop watcher var response = await fetch('/api/codexlens/watch/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); var result = await response.json(); if (result.success) { showRefreshToast((t('codexlens.watcherStopped') || 'Watcher stopped') + ': ' + result.events_processed + ' events processed', 'success'); document.getElementById('watcherStats').style.display = 'none'; document.getElementById('watcherStartConfig').style.display = 'block'; stopWatcherStatusPolling(); } else { toggle.checked = true; showRefreshToast(t('common.error') + ': ' + result.error, 'error'); } } } catch (err) { toggle.checked = !shouldRun; showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } // Watcher status polling var watcherPollingInterval = null; function startWatcherStatusPolling() { if (watcherPollingInterval) return; watcherPollingInterval = setInterval(async function() { try { // Check if modal elements still exist (modal may be closed) var eventsCountEl = document.getElementById('watcherEventsCount'); var uptimeEl = document.getElementById('watcherUptime'); var toggleEl = document.getElementById('watcherToggle'); var statsEl = document.getElementById('watcherStats'); var configEl = document.getElementById('watcherStartConfig'); // If modal elements don't exist, stop polling if (!eventsCountEl && !toggleEl) { stopWatcherStatusPolling(); return; } var response = await fetch('/api/codexlens/watch/status'); var status = await response.json(); if (status.running) { if (eventsCountEl) eventsCountEl.textContent = status.events_processed || 0; // Format uptime var seconds = status.uptime_seconds || 0; var formatted = seconds < 60 ? seconds + 's' : seconds < 3600 ? Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's' : Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; if (uptimeEl) uptimeEl.textContent = formatted; } else { // Watcher stopped externally stopWatcherStatusPolling(); if (toggleEl) toggleEl.checked = false; if (statsEl) statsEl.style.display = 'none'; if (configEl) configEl.style.display = 'block'; } } catch (err) { console.error('Failed to poll watcher status:', err); } }, 2000); } function stopWatcherStatusPolling() { if (watcherPollingInterval) { clearInterval(watcherPollingInterval); watcherPollingInterval = null; } stopCountdownTimer(); } // Countdown timer for pending queue var countdownInterval = null; var currentCountdownSeconds = 0; function startCountdownTimer(seconds) { currentCountdownSeconds = seconds; if (countdownInterval) return; countdownInterval = setInterval(function() { var timerEl = document.getElementById('countdownTimer'); if (!timerEl) { stopCountdownTimer(); return; } if (currentCountdownSeconds <= 0) { timerEl.textContent = '--:--'; } else { currentCountdownSeconds--; timerEl.textContent = formatCountdown(currentCountdownSeconds); } }, 1000); } function stopCountdownTimer() { if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } } function formatCountdown(seconds) { if (seconds <= 0) return '--:--'; var mins = Math.floor(seconds / 60); var secs = seconds % 60; return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs; } /** * Immediately flush pending queue and trigger indexing */ async function flushWatcherNow() { var btn = document.getElementById('flushNowBtn'); if (btn) { btn.disabled = true; btn.innerHTML = ' Indexing...'; if (typeof lucide !== 'undefined') lucide.createIcons(); } try { var watchPath = document.getElementById('watcherPath'); var path = watchPath ? watchPath.value.trim() : ''; var response = await fetch('/api/codexlens/watch/flush', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path || undefined }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.indexTriggered') || 'Indexing triggered', 'success'); } else { showRefreshToast(t('common.error') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } finally { if (btn) { btn.disabled = false; btn.innerHTML = '' + (t('codexlens.indexNow') || 'Index Now'); if (typeof lucide !== 'undefined') lucide.createIcons(); } } } window.flushWatcherNow = flushWatcherNow; /** * Show index history in a modal */ async function showIndexHistory() { try { var watchPath = document.getElementById('watcherPath'); var path = watchPath ? watchPath.value.trim() : ''; var response = await fetch('/api/codexlens/watch/history?limit=10&path=' + encodeURIComponent(path)); var result = await response.json(); if (!result.success || !result.history || result.history.length === 0) { showRefreshToast(t('codexlens.noHistory') || 'No index history available', 'info'); return; } var historyHtml = result.history.slice().reverse().map(function(h, i) { var timestamp = h.timestamp ? new Date(h.timestamp * 1000).toLocaleString() : 'Unknown'; return '
' + '
' + '#' + (result.history.length - i) + '' + '' + timestamp + '' + '
' + '
' + '
' + (h.files_indexed || 0) + ' indexed
' + '
' + (h.files_removed || 0) + ' removed
' + '
+' + (h.symbols_added || 0) + ' symbols
' + '
' + ((h.errors && h.errors.length) || 0) + ' errors
' + '
' + (h.errors && h.errors.length > 0 ? '
' + h.errors.slice(0, 2).map(function(e) { return '
• ' + e + '
'; }).join('') + (h.errors.length > 2 ? '
... and ' + (h.errors.length - 2) + ' more
' : '') + '
' : '') + '
'; }).join(''); var modal = document.createElement('div'); modal.id = 'indexHistoryModal'; modal.className = 'modal-backdrop'; modal.innerHTML = ''; document.body.appendChild(modal); if (typeof lucide !== 'undefined') lucide.createIcons(); } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } window.showIndexHistory = showIndexHistory; /** * Update pending queue UI elements */ function updatePendingQueueUI(queue) { var countEl = document.getElementById('pendingFileCount'); var timerEl = document.getElementById('countdownTimer'); var listEl = document.getElementById('pendingFilesList'); var flushBtn = document.getElementById('flushNowBtn'); if (countEl) countEl.textContent = queue.file_count || 0; if (queue.countdown_seconds > 0) { currentCountdownSeconds = queue.countdown_seconds; if (timerEl) timerEl.textContent = formatCountdown(queue.countdown_seconds); startCountdownTimer(queue.countdown_seconds); } else { if (timerEl) timerEl.textContent = '--:--'; } if (flushBtn) flushBtn.disabled = (queue.file_count || 0) === 0; if (listEl && queue.files) { listEl.innerHTML = queue.files.map(function(f) { return '
' + '' + '' + f + '' + '
'; }).join(''); if (typeof lucide !== 'undefined') lucide.createIcons(); } } /** * Update last index result UI */ function updateLastIndexResult(result) { var statsEl = document.getElementById('lastIndexStats'); var sectionEl = document.getElementById('watcherLastIndex'); if (sectionEl) sectionEl.style.display = 'block'; if (statsEl) { statsEl.innerHTML = '
' + '
' + (result.files_indexed || 0) + '
' + '
Indexed
' + '
' + '
' + '
' + (result.files_removed || 0) + '
' + '
Removed
' + '
' + '
' + '
' + (result.symbols_added || 0) + '
' + '
+Symbols
' + '
' + '
' + '
' + ((result.errors && result.errors.length) || 0) + '
' + '
Errors
' + '
'; } // Clear pending queue after indexing updatePendingQueueUI({ file_count: 0, files: [], countdown_seconds: 0 }); } /** * Close the watcher control modal */ function closeWatcherModal() { stopWatcherStatusPolling(); var modal = document.getElementById('watcherControlModal'); if (modal) modal.remove(); } /** * Handle watcher status update from WebSocket * @param {Object} payload - { running: boolean, path?: string, error?: string, events_processed?: number, uptime_seconds?: number } */ function handleWatcherStatusUpdate(payload) { var toggle = document.getElementById('watcherToggle'); var statsDiv = document.getElementById('watcherStats'); var configDiv = document.getElementById('watcherStartConfig'); var eventsCountEl = document.getElementById('watcherEventsCount'); var uptimeEl = document.getElementById('watcherUptime'); // Update events count if provided (real-time updates) if (payload.events_processed !== undefined && eventsCountEl) { eventsCountEl.textContent = payload.events_processed; } // Update uptime if provided if (payload.uptime_seconds !== undefined && uptimeEl) { var seconds = payload.uptime_seconds; var formatted = seconds < 60 ? seconds + 's' : seconds < 3600 ? Math.floor(seconds / 60) + 'm ' + (seconds % 60) + 's' : Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; uptimeEl.textContent = formatted; } // Also update main page watcher status badge if it exists var statusBadge = document.getElementById('watcherStatusBadge'); if (statusBadge && payload.running !== undefined) { updateWatcherUI(payload.running, { events_processed: payload.events_processed, uptime_seconds: payload.uptime_seconds }); } if (payload.error) { // Watcher failed - update UI to show stopped state if (toggle) toggle.checked = false; if (statsDiv) statsDiv.style.display = 'none'; if (configDiv) configDiv.style.display = 'block'; stopWatcherStatusPolling(); } else if (payload.running) { // Watcher started if (toggle) toggle.checked = true; if (statsDiv) statsDiv.style.display = 'block'; if (configDiv) configDiv.style.display = 'none'; startWatcherStatusPolling(); } else if (payload.running === false) { // Watcher stopped normally (only if running is explicitly false) if (toggle) toggle.checked = false; if (statsDiv) statsDiv.style.display = 'none'; if (configDiv) configDiv.style.display = 'block'; stopWatcherStatusPolling(); } } // ============================================================ // IGNORE PATTERNS CONFIGURATION // ============================================================ // Cache for default patterns (loaded once) var ignorePatternsDefaults = null; /** * Toggle ignore patterns section visibility */ function toggleIgnorePatternsSection() { var content = document.getElementById('ignorePatternsContent'); var chevron = document.getElementById('ignorePatternsChevron'); if (content && chevron) { var isHidden = content.classList.contains('hidden'); content.classList.toggle('hidden'); chevron.style.transform = isHidden ? 'rotate(180deg)' : ''; } } window.toggleIgnorePatternsSection = toggleIgnorePatternsSection; /** * Load ignore patterns from server */ async function loadIgnorePatterns() { try { var response = await fetch('/api/codexlens/ignore-patterns'); var data = await response.json(); if (data.success) { // Cache defaults ignorePatternsDefaults = data.defaults; // Populate textareas var patternsInput = document.getElementById('ignorePatternsInput'); var filtersInput = document.getElementById('extensionFiltersInput'); if (patternsInput) { patternsInput.value = (data.patterns || []).join('\n'); } if (filtersInput) { filtersInput.value = (data.extensionFilters || []).join('\n'); } // Update count badge var countBadge = document.getElementById('ignorePatternsCount'); if (countBadge) { var total = (data.patterns || []).length + (data.extensionFilters || []).length; countBadge.textContent = total + ' ' + (t('common.patterns') || 'patterns'); } } } catch (err) { console.error('Failed to load ignore patterns:', err); } } window.loadIgnorePatterns = loadIgnorePatterns; /** * Save ignore patterns to server */ async function saveIgnorePatterns() { var patternsInput = document.getElementById('ignorePatternsInput'); var filtersInput = document.getElementById('extensionFiltersInput'); var patterns = patternsInput ? patternsInput.value.split('\n').map(function(p) { return p.trim(); }).filter(function(p) { return p; }) : []; var extensionFilters = filtersInput ? filtersInput.value.split('\n').map(function(p) { return p.trim(); }).filter(function(p) { return p; }) : []; try { var response = await fetch('/api/codexlens/ignore-patterns', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patterns: patterns, extensionFilters: extensionFilters }) }); var result = await response.json(); if (result.success) { showRefreshToast(t('codexlens.ignorePatternsSaved') || 'Ignore patterns saved', 'success'); // Update count badge var countBadge = document.getElementById('ignorePatternsCount'); if (countBadge) { var total = patterns.length + extensionFilters.length; countBadge.textContent = total + ' ' + (t('common.patterns') || 'patterns'); } } else { showRefreshToast(t('common.error') + ': ' + result.error, 'error'); } } catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); } } window.saveIgnorePatterns = saveIgnorePatterns; /** * Reset ignore patterns to defaults */ async function resetIgnorePatterns() { if (!ignorePatternsDefaults) { // Load defaults first if not cached try { var response = await fetch('/api/codexlens/ignore-patterns'); var data = await response.json(); if (data.success) { ignorePatternsDefaults = data.defaults; } } catch (err) { console.error('Failed to load defaults:', err); return; } } if (ignorePatternsDefaults) { var patternsInput = document.getElementById('ignorePatternsInput'); var filtersInput = document.getElementById('extensionFiltersInput'); if (patternsInput) { patternsInput.value = (ignorePatternsDefaults.patterns || []).join('\n'); } if (filtersInput) { filtersInput.value = (ignorePatternsDefaults.extensionFilters || []).join('\n'); } showRefreshToast(t('codexlens.ignorePatternReset') || 'Reset to defaults (click Save to apply)', 'info'); } } window.resetIgnorePatterns = resetIgnorePatterns; /** * Initialize ignore patterns count badge (called on page load) * Also loads patterns into textarea if section is visible */ async function initIgnorePatternsCount() { // Fallback defaults in case API fails var fallbackDefaults = { patterns: [ '.git', '.svn', '.hg', '.venv', 'venv', 'env', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', 'node_modules', 'bower_components', '.npm', '.yarn', 'dist', 'build', 'out', 'target', 'bin', 'obj', '_build', 'coverage', 'htmlcov', '.idea', '.vscode', '.vs', '.eclipse', '.codexlens', '.cache', '.parcel-cache', '.turbo', '.next', '.nuxt', 'logs', 'tmp', 'temp' ], extensionFilters: [ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'composer.lock', 'Gemfile.lock', 'poetry.lock', '*.min.js', '*.min.css', '*.bundle.js', '*.svg', '*.map' ] }; var patterns = fallbackDefaults.patterns; var extensionFilters = fallbackDefaults.extensionFilters; try { var response = await fetch('/api/codexlens/ignore-patterns'); var data = await response.json(); if (data.success) { // Cache defaults ignorePatternsDefaults = data.defaults || fallbackDefaults; patterns = data.patterns || fallbackDefaults.patterns; extensionFilters = data.extensionFilters || fallbackDefaults.extensionFilters; } else { console.warn('Ignore patterns API returned error, using defaults'); ignorePatternsDefaults = fallbackDefaults; } } catch (err) { console.warn('Failed to fetch ignore patterns, using defaults:', err); ignorePatternsDefaults = fallbackDefaults; } // Update count badge var countBadge = document.getElementById('ignorePatternsCount'); if (countBadge) { var total = patterns.length + extensionFilters.length; countBadge.textContent = total + ' ' + (t('common.patterns') || 'patterns'); } // Populate textareas if they exist var patternsInput = document.getElementById('ignorePatternsInput'); var filtersInput = document.getElementById('extensionFiltersInput'); if (patternsInput) { patternsInput.value = patterns.join('\n'); } if (filtersInput) { filtersInput.value = extensionFilters.join('\n'); } } window.initIgnorePatternsCount = initIgnorePatternsCount; // ============================================================ // CACHE MANAGEMENT - Global Exports // ============================================================ window.invalidateCodexLensCache = invalidateCache; window.refreshCodexLensData = async function(forceRefresh) { invalidateCache(); await refreshWorkspaceIndexStatus(true); showRefreshToast(t('common.refreshed') || 'Refreshed', 'success'); };