diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index b3b7ef2a..468c09e6 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -2366,5 +2366,121 @@ except Exception as e: return true; } + // API: Get workspace index status (FTS and Vector coverage percentages) + if (pathname === '/api/codexlens/workspace-status') { + try { + const projectPath = url.searchParams.get('path') || initialPath; + + // Check if CodexLens is installed first + const venvStatus = await checkVenvStatus(); + if (!venvStatus.ready) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + hasIndex: false, + fts: { indexed: false, percent: 0 }, + vector: { indexed: false, percent: 0 }, + message: 'CodexLens not installed' + })); + return true; + } + + let ftsStatus = { indexed: false, percent: 0, totalFiles: 0, indexedFiles: 0 }; + let vectorStatus = { indexed: false, percent: 0, totalFiles: 0, filesWithEmbeddings: 0, totalChunks: 0 }; + let hasIndex = false; + let indexRoot = ''; + + // First, get project info to check if index exists + const projectsResult = await executeCodexLens(['projects', 'show', projectPath, '--json']); + + if (projectsResult.success && projectsResult.output) { + try { + const projectData = extractJSON(projectsResult.output); + if (projectData.success && projectData.result) { + const project = projectData.result; + hasIndex = true; + indexRoot = project.index_root || ''; + + // FTS is always 100% when index exists + ftsStatus = { + indexed: true, + percent: 100, + totalFiles: project.total_files || 0, + indexedFiles: project.total_files || 0 + }; + + // Now get embeddings status for this specific project + const statusResult = await executeCodexLens(['index', 'status', projectPath, '--json']); + if (statusResult.success && statusResult.output) { + try { + const status = extractJSON(statusResult.output); + if (status.success && status.result && status.result.embeddings) { + const embeddings = status.result.embeddings; + + // Find the project-specific embedding info from indexes array + const indexes = embeddings.indexes || []; + let projectEmbedding = null; + + // Look for matching project by path or name + const { basename, resolve } = await import('path'); + const normalizedPath = resolve(projectPath).toLowerCase(); + const projectName = basename(projectPath); + + for (const idx of indexes) { + const idxPath = (idx.path || '').toLowerCase(); + const idxProject = (idx.project || '').toLowerCase(); + if (idxPath.includes(normalizedPath.replace(/\\/g, '/')) || + idxPath.includes(normalizedPath) || + idxProject === projectName.toLowerCase()) { + projectEmbedding = idx; + break; + } + } + + if (projectEmbedding) { + vectorStatus = { + indexed: projectEmbedding.has_embeddings || false, + percent: projectEmbedding.coverage_percent || 0, + totalFiles: projectEmbedding.total_files || project.total_files || 0, + filesWithEmbeddings: Math.round((projectEmbedding.coverage_percent || 0) * (projectEmbedding.total_files || 0) / 100), + totalChunks: projectEmbedding.total_chunks || 0 + }; + } else { + // No specific project found, use aggregated stats + vectorStatus = { + indexed: embeddings.indexes_with_embeddings > 0, + percent: 0, + totalFiles: project.total_files || 0, + filesWithEmbeddings: 0, + totalChunks: 0 + }; + } + } + } catch (e) { + console.error('[CodexLens] Failed to parse index status:', e.message); + } + } + } + } catch (e) { + console.error('[CodexLens] Failed to parse project data:', e.message); + } + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + success: true, + hasIndex, + indexRoot, + path: projectPath, + fts: ftsStatus, + vector: vectorStatus + })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + return true; + } + return false; } diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index 7c9da789..49f3bd25 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -375,6 +375,14 @@ const i18n = { 'codexlens.cleanFailed': 'Failed to clean indexes', 'codexlens.loadingConfig': 'Loading configuration...', + // Workspace Index Status + 'codexlens.workspaceStatus': 'Workspace Index Status', + 'codexlens.noIndexFound': 'No index found for current workspace', + 'codexlens.filesIndexed': 'files indexed', + 'codexlens.filesWithEmbeddings': 'files with embeddings', + 'codexlens.vectorSearchEnabled': 'Vector search enabled', + 'codexlens.vectorSearchPartial': 'Vector search requires ≥50% coverage', + // Model Management 'codexlens.semanticDeps': 'Semantic Dependencies', 'codexlens.checkingDeps': 'Checking dependencies...', @@ -2384,6 +2392,14 @@ const i18n = { 'codexlens.cleanFailed': '清理索引失败', 'codexlens.loadingConfig': '加载配置中...', + // 工作空间索引状态 + 'codexlens.workspaceStatus': '工作空间索引状态', + 'codexlens.noIndexFound': '当前工作空间未找到索引', + 'codexlens.filesIndexed': '个文件已索引', + 'codexlens.filesWithEmbeddings': '个文件已嵌入', + 'codexlens.vectorSearchEnabled': '向量搜索已启用', + 'codexlens.vectorSearchPartial': '向量搜索需要≥50%覆盖率', + // 模型管理 'codexlens.semanticDeps': '语义搜索依赖', 'codexlens.checkingDeps': '检查依赖中...', diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 4788a872..02c8d478 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -18,6 +18,117 @@ function escapeHtml(str) { .replace(/'/g, '''); } +// ============================================================ +// WORKSPACE INDEX STATUS +// ============================================================ + +/** + * Refresh workspace index status (FTS and Vector coverage) + */ +async function refreshWorkspaceIndexStatus() { + var container = document.getElementById('workspaceIndexStatusContent'); + if (!container) return; + + // Show loading state + container.innerHTML = '
' + + ' ' + (t('common.loading') || 'Loading...') + + '
'; + if (window.lucide) lucide.createIcons(); + + try { + var response = await fetch('/api/codexlens/workspace-status'); + var result = await response.json(); + + if (result.success) { + var html = ''; + + if (!result.hasIndex) { + // No index for current workspace + html = '
' + + '
' + + ' ' + + (t('codexlens.noIndexFound') || 'No index found for current workspace') + + '
' + + '' + + '
'; + } else { + // FTS Status + var ftsPercent = result.fts.percent || 0; + 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 vectorPercent = result.vector.percent || 0; + 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 { + container.innerHTML = '
' + + ' ' + + (result.error || t('common.error') || 'Error loading status') + + '
'; + } + } catch (err) { + console.error('[CodexLens] Failed to load workspace status:', err); + container.innerHTML = '
' + + ' ' + + (t('common.error') || 'Error') + ': ' + err.message + + '
'; + } + + if (window.lucide) lucide.createIcons(); +} + // ============================================================ // CODEXLENS CONFIGURATION MODAL // ============================================================ @@ -29,9 +140,23 @@ async function showCodexLensConfigModal() { try { showRefreshToast(t('codexlens.loadingConfig'), 'info'); - // Fetch current config - const response = await fetch('/api/codexlens/config'); - const config = await response.json(); + // Fetch current config and status in parallel + const [configResponse, statusResponse] = await Promise.all([ + fetch('/api/codexlens/config'), + fetch('/api/codexlens/status') + ]); + const config = await configResponse.json(); + const status = await statusResponse.json(); + + // 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); @@ -147,6 +272,25 @@ function buildCodexLensConfigContent(config) { '' + '' + + // Workspace Index Status (only if installed) + (isInstalled + ? '
' + + '
' + + '

' + + ' ' + (t('codexlens.workspaceStatus') || 'Workspace Index Status') + + '

' + + '' + + '
' + + '
' + + '
' + + ' Loading...' + + '
' + + '
' + + '
' + : '') + + // Index Operations - 4 buttons grid '
' + '

' + (t('codexlens.indexOperations') || 'Index Operations') + '

' + @@ -547,6 +691,9 @@ function initCodexLensConfigEvents(currentConfig) { // Load model lists (embedding and reranker) loadModelList(); loadRerankerModelList(); + + // Load workspace index status + refreshWorkspaceIndexStatus(); } // ============================================================ diff --git a/ccw/src/templates/dashboard-js/views/hook-manager.js b/ccw/src/templates/dashboard-js/views/hook-manager.js index 87983a63..e709926b 100644 --- a/ccw/src/templates/dashboard-js/views/hook-manager.js +++ b/ccw/src/templates/dashboard-js/views/hook-manager.js @@ -87,7 +87,7 @@ async function renderHookManager() {
${renderWizardCard('memory-update')} - ${renderWizardCard('memory-setup')} + ${renderWizardCard('danger-protection')} ${renderWizardCard('skill-context')}
@@ -216,10 +216,12 @@ function renderWizardCard(wizardId) { // Get translated wizard name and description const wizardName = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdate') : wizardId === 'memory-setup' ? t('hook.wizard.memorySetup') : - wizardId === 'skill-context' ? t('hook.wizard.skillContext') : wizard.name; + wizardId === 'skill-context' ? t('hook.wizard.skillContext') : + wizardId === 'danger-protection' ? t('hook.wizard.dangerProtection') : wizard.name; const wizardDesc = wizardId === 'memory-update' ? t('hook.wizard.memoryUpdateDesc') : wizardId === 'memory-setup' ? t('hook.wizard.memorySetupDesc') : - wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : wizard.description; + wizardId === 'skill-context' ? t('hook.wizard.skillContextDesc') : + wizardId === 'danger-protection' ? t('hook.wizard.dangerProtectionDesc') : wizard.description; // Translate options const getOptionName = (wizardId, optId) => { @@ -237,6 +239,12 @@ function renderWizardCard(wizardId) { if (optId === 'keyword') return t('hook.wizard.keywordMatching'); if (optId === 'auto') return t('hook.wizard.autoDetection'); } + if (wizardId === 'danger-protection') { + if (optId === 'bash-confirm') return t('hook.wizard.dangerBashConfirm'); + if (optId === 'file-protection') return t('hook.wizard.dangerFileProtection'); + if (optId === 'git-destructive') return t('hook.wizard.dangerGitDestructive'); + if (optId === 'network-confirm') return t('hook.wizard.dangerNetworkConfirm'); + } return wizard.options.find(o => o.id === optId)?.name || ''; }; @@ -255,6 +263,12 @@ function renderWizardCard(wizardId) { if (optId === 'keyword') return t('hook.wizard.keywordMatchingDesc'); if (optId === 'auto') return t('hook.wizard.autoDetectionDesc'); } + if (wizardId === 'danger-protection') { + if (optId === 'bash-confirm') return t('hook.wizard.dangerBashConfirmDesc'); + if (optId === 'file-protection') return t('hook.wizard.dangerFileProtectionDesc'); + if (optId === 'git-destructive') return t('hook.wizard.dangerGitDestructiveDesc'); + if (optId === 'network-confirm') return t('hook.wizard.dangerNetworkConfirmDesc'); + } return wizard.options.find(o => o.id === optId)?.description || ''; }; diff --git a/codex-lens/src/codexlens/cli/commands.py b/codex-lens/src/codexlens/cli/commands.py index 7e019cf8..776ba7b0 100644 --- a/codex-lens/src/codexlens/cli/commands.py +++ b/codex-lens/src/codexlens/cli/commands.py @@ -903,7 +903,7 @@ def status( schema_version = store._get_schema_version(conn) # Check if dual FTS tables exist cursor = conn.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('search_fts_exact', 'search_fts_fuzzy')" + "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('files_fts_exact', 'files_fts_fuzzy')" ) fts_tables = [row[0] for row in cursor.fetchall()] has_dual_fts = len(fts_tables) == 2