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') +
+ '
' +
+ '
' +
+ (t('codexlens.createIndex') || 'Create Index') +
+ ' ' +
+ '
';
+ } 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') +
+ ' ' +
+ '' +
+ ' ' + (t('common.refresh') || 'Refresh') +
+ ' ' +
+ '' +
+ '
' +
+ '
' +
+ ' 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