mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: 添加工作空间索引状态接口,增强 CodexLens 状态检查功能,支持前端显示索引信息
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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': '检查依赖中...',
|
||||
|
||||
@@ -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 = '<div class="text-xs text-muted-foreground text-center py-2">' +
|
||||
'<i data-lucide="loader-2" class="w-4 h-4 animate-spin inline mr-1"></i> ' + (t('common.loading') || 'Loading...') +
|
||||
'</div>';
|
||||
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 = '<div class="text-center py-3">' +
|
||||
'<div class="text-sm text-muted-foreground mb-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(t('codexlens.noIndexFound') || 'No index found for current workspace') +
|
||||
'</div>' +
|
||||
'<button onclick="runFtsFullIndex()" class="text-xs text-primary hover:underline">' +
|
||||
(t('codexlens.createIndex') || 'Create Index') +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
} 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 += '<div class="space-y-1">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="file-text" class="w-3.5 h-3.5 text-blue-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.ftsIndex') || 'FTS Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + ftsTextColor + ' font-medium">' + ftsPercent + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + ftsColor + ' transition-all duration-300" style="width: ' + ftsPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.fts.indexedFiles || 0) + ' / ' + (result.fts.totalFiles || 0) + ' ' + (t('codexlens.filesIndexed') || 'files indexed') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// 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 += '<div class="space-y-1 mt-3">' +
|
||||
'<div class="flex items-center justify-between text-xs">' +
|
||||
'<span class="flex items-center gap-1.5">' +
|
||||
'<i data-lucide="brain" class="w-3.5 h-3.5 text-purple-500"></i> ' +
|
||||
'<span class="font-medium">' + (t('codexlens.vectorIndex') || 'Vector Index') + '</span>' +
|
||||
'</span>' +
|
||||
'<span class="' + vectorTextColor + ' font-medium">' + vectorPercent.toFixed(1) + '%</span>' +
|
||||
'</div>' +
|
||||
'<div class="h-1.5 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div class="h-full ' + vectorColor + ' transition-all duration-300" style="width: ' + vectorPercent + '%"></div>' +
|
||||
'</div>' +
|
||||
'<div class="text-xs text-muted-foreground">' +
|
||||
(result.vector.filesWithEmbeddings || 0) + ' / ' + (result.vector.totalFiles || 0) + ' ' + (t('codexlens.filesWithEmbeddings') || 'files with embeddings') +
|
||||
(result.vector.totalChunks > 0 ? ' (' + result.vector.totalChunks + ' chunks)' : '') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Vector search availability indicator
|
||||
if (vectorPercent >= 50) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="check-circle-2" class="w-3.5 h-3.5 text-success"></i>' +
|
||||
'<span class="text-xs text-success">' + (t('codexlens.vectorSearchEnabled') || 'Vector search enabled') + '</span>' +
|
||||
'</div>';
|
||||
} else if (vectorPercent > 0) {
|
||||
html += '<div class="flex items-center gap-1.5 mt-2 pt-2 border-t border-border">' +
|
||||
'<i data-lucide="alert-triangle" class="w-3.5 h-3.5 text-warning"></i>' +
|
||||
'<span class="text-xs text-warning">' + (t('codexlens.vectorSearchPartial') || 'Vector search requires ≥50% coverage') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
} else {
|
||||
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(result.error || t('common.error') || 'Error loading status') +
|
||||
'</div>';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[CodexLens] Failed to load workspace status:', err);
|
||||
container.innerHTML = '<div class="text-xs text-destructive text-center py-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-4 h-4 inline mr-1"></i> ' +
|
||||
(t('common.error') || 'Error') + ': ' + err.message +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
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) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Workspace Index Status (only if installed)
|
||||
(isInstalled
|
||||
? '<div class="rounded-lg border border-border p-4 mb-4 bg-card" id="workspaceIndexStatus">' +
|
||||
'<div class="flex items-center justify-between mb-3">' +
|
||||
'<h4 class="text-sm font-medium flex items-center gap-2">' +
|
||||
'<i data-lucide="hard-drive" class="w-4 h-4"></i> ' + (t('codexlens.workspaceStatus') || 'Workspace Index Status') +
|
||||
'</h4>' +
|
||||
'<button onclick="refreshWorkspaceIndexStatus()" class="text-xs text-primary hover:underline flex items-center gap-1" title="Refresh status">' +
|
||||
'<i data-lucide="refresh-cw" class="w-3 h-3"></i> ' + (t('common.refresh') || 'Refresh') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="workspaceIndexStatusContent" class="space-y-3">' +
|
||||
'<div class="text-xs text-muted-foreground text-center py-2">' +
|
||||
'<i data-lucide="loader-2" class="w-4 h-4 animate-spin inline mr-1"></i> Loading...' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
// Index Operations - 4 buttons grid
|
||||
'<div class="space-y-2">' +
|
||||
'<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">' + (t('codexlens.indexOperations') || 'Index Operations') + '</h4>' +
|
||||
@@ -547,6 +691,9 @@ function initCodexLensConfigEvents(currentConfig) {
|
||||
// Load model lists (embedding and reranker)
|
||||
loadModelList();
|
||||
loadRerankerModelList();
|
||||
|
||||
// Load workspace index status
|
||||
refreshWorkspaceIndexStatus();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -87,7 +87,7 @@ async function renderHookManager() {
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
${renderWizardCard('memory-update')}
|
||||
${renderWizardCard('memory-setup')}
|
||||
${renderWizardCard('danger-protection')}
|
||||
${renderWizardCard('skill-context')}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 || '';
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user