Add parallel search mode and index progress bar

Features:
- CCW smart_search: Add 'parallel' mode that runs hybrid + exact + ripgrep
  simultaneously with RRF (Reciprocal Rank Fusion) for result merging
- Dashboard: Add real-time progress bar for CodexLens index initialization
- MCP: Return progress metadata in init action response
- Codex-lens: Auto-detect optimal worker count for parallel indexing

Changes:
- smart-search.ts: Add parallel mode, RRF fusion, progress tracking
- codex-lens.ts: Add onProgress callback support, progress parsing
- codexlens-routes.ts: Broadcast index progress via WebSocket
- codexlens-manager.js: New index progress modal with real-time updates
- notifications.js: Add WebSocket event handler registration system
- i18n.js: Add English/Chinese translations for progress UI
- index_tree.py: Workers parameter now auto-detects CPU count (max 16)
- commands.py: CLI --workers parameter supports auto-detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-17 23:17:15 +08:00
parent 44d84116c3
commit 51a61bef31
8 changed files with 569 additions and 21 deletions

View File

@@ -87,6 +87,49 @@ let autoRefreshInterval = null;
let lastDataHash = null;
const AUTO_REFRESH_INTERVAL_MS = 30000; // 30 seconds
// Custom event handlers registry for components to subscribe to specific events
const wsEventHandlers = {};
/**
* Register a custom handler for a specific WebSocket event type
* @param {string} eventType - The event type to listen for
* @param {Function} handler - The handler function
*/
function registerWsEventHandler(eventType, handler) {
if (!wsEventHandlers[eventType]) {
wsEventHandlers[eventType] = [];
}
wsEventHandlers[eventType].push(handler);
}
/**
* Unregister a custom handler for a specific WebSocket event type
* @param {string} eventType - The event type
* @param {Function} handler - The handler function to remove
*/
function unregisterWsEventHandler(eventType, handler) {
if (wsEventHandlers[eventType]) {
wsEventHandlers[eventType] = wsEventHandlers[eventType].filter(h => h !== handler);
}
}
/**
* Dispatch event to registered handlers
* @param {string} eventType - The event type
* @param {Object} data - The full event data
*/
function dispatchToEventHandlers(eventType, data) {
if (wsEventHandlers[eventType]) {
wsEventHandlers[eventType].forEach(handler => {
try {
handler(data);
} catch (e) {
console.error('[WS] Error in custom handler for', eventType, e);
}
});
}
}
// ========== WebSocket Connection ==========
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
@@ -390,6 +433,12 @@ function handleNotification(data) {
console.log('[CodexLens] Uninstallation completed:', payload);
break;
case 'CODEXLENS_INDEX_PROGRESS':
// Handle CodexLens index progress updates
dispatchToEventHandlers('CODEXLENS_INDEX_PROGRESS', data);
console.log('[CodexLens] Index progress:', payload.stage, payload.percent + '%');
break;
default:
console.log('[WS] Unknown notification type:', type);
}

View File

@@ -278,6 +278,15 @@ const i18n = {
'codexlens.modelDeleteFailed': 'Model deletion failed',
'codexlens.deleteModelConfirm': 'Are you sure you want to delete model',
// CodexLens Indexing Progress
'codexlens.indexing': 'Indexing',
'codexlens.indexingDesc': 'Building code index for workspace',
'codexlens.preparingIndex': 'Preparing index...',
'codexlens.filesProcessed': 'Files processed',
'codexlens.indexComplete': 'Index complete',
'codexlens.indexSuccess': 'Index created successfully',
'codexlens.indexFailed': 'Indexing failed',
// Semantic Search Configuration
'semantic.settings': 'Semantic Search Settings',
'semantic.testSearch': 'Test Semantic Search',
@@ -1394,6 +1403,15 @@ const i18n = {
'codexlens.modelDeleteFailed': '模型删除失败',
'codexlens.deleteModelConfirm': '确定要删除模型',
// CodexLens 索引进度
'codexlens.indexing': '索引中',
'codexlens.indexingDesc': '正在为工作区构建代码索引',
'codexlens.preparingIndex': '准备索引...',
'codexlens.filesProcessed': '已处理文件',
'codexlens.indexComplete': '索引完成',
'codexlens.indexSuccess': '索引创建成功',
'codexlens.indexFailed': '索引失败',
// Semantic Search 配置
'semantic.settings': '语义搜索设置',
'semantic.testSearch': '测试语义搜索',

View File

@@ -542,10 +542,160 @@ async function deleteModel(profile) {
// ============================================================
/**
* Initialize CodexLens index
* Initialize CodexLens index with progress tracking
*/
function initCodexLensIndex() {
openCliInstallWizard('codexlens');
// Create progress modal
var modal = document.createElement('div');
modal.id = 'codexlensIndexModal';
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
modal.innerHTML =
'<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">' +
'<div class="p-6">' +
'<div class="flex items-center gap-3 mb-4">' +
'<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">' +
'<i data-lucide="database" class="w-5 h-5 text-primary"></i>' +
'</div>' +
'<div>' +
'<h3 class="text-lg font-semibold">' + t('codexlens.indexing') + '</h3>' +
'<p class="text-sm text-muted-foreground">' + t('codexlens.indexingDesc') + '</p>' +
'</div>' +
'</div>' +
'<div class="space-y-4">' +
'<div id="codexlensIndexProgress">' +
'<div class="flex items-center gap-3">' +
'<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div>' +
'<span class="text-sm" id="codexlensIndexStatus">' + t('codexlens.preparingIndex') + '</span>' +
'</div>' +
'<div class="mt-3 h-2 bg-muted rounded-full overflow-hidden">' +
'<div id="codexlensIndexProgressBar" class="h-full bg-primary transition-all duration-300" style="width: 0%"></div>' +
'</div>' +
'<div class="mt-2 text-xs text-muted-foreground" id="codexlensIndexDetails"></div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">' +
'<button class="btn-outline px-4 py-2" id="codexlensIndexCancelBtn" onclick="cancelCodexLensIndex()">' + t('common.cancel') + '</button>' +
'</div>' +
'</div>';
document.body.appendChild(modal);
if (window.lucide) lucide.createIcons();
// Start indexing
startCodexLensIndexing();
}
/**
* Start the indexing process
*/
async function startCodexLensIndexing() {
var statusText = document.getElementById('codexlensIndexStatus');
var progressBar = document.getElementById('codexlensIndexProgressBar');
var detailsText = document.getElementById('codexlensIndexDetails');
var cancelBtn = document.getElementById('codexlensIndexCancelBtn');
// Setup WebSocket listener for progress events
window.codexlensIndexProgressHandler = function(event) {
if (event.type === 'CODEXLENS_INDEX_PROGRESS') {
var payload = event.payload;
if (statusText) statusText.textContent = payload.message || t('codexlens.indexing');
if (progressBar) progressBar.style.width = (payload.percent || 0) + '%';
if (detailsText && payload.filesProcessed !== undefined) {
detailsText.textContent = t('codexlens.filesProcessed') + ': ' + payload.filesProcessed +
(payload.totalFiles ? ' / ' + payload.totalFiles : '');
}
// Handle completion
if (payload.stage === 'complete') {
handleIndexComplete(true, payload.message);
} else if (payload.stage === 'error') {
handleIndexComplete(false, payload.message);
}
}
};
// Register with notification system if available
if (typeof registerWsEventHandler === 'function') {
registerWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler);
}
try {
var response = await fetch('/api/codexlens/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: projectPath })
});
var result = await response.json();
if (!result.success && statusText) {
// If WebSocket didn't report error, show it now
handleIndexComplete(false, result.error || t('common.unknownError'));
}
} catch (err) {
handleIndexComplete(false, err.message);
}
}
/**
* Handle index completion
*/
function handleIndexComplete(success, message) {
var statusText = document.getElementById('codexlensIndexStatus');
var progressBar = document.getElementById('codexlensIndexProgressBar');
var cancelBtn = document.getElementById('codexlensIndexCancelBtn');
// Unregister WebSocket handler
if (typeof unregisterWsEventHandler === 'function') {
unregisterWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler);
}
if (success) {
if (progressBar) progressBar.style.width = '100%';
if (statusText) statusText.textContent = t('codexlens.indexComplete');
if (cancelBtn) cancelBtn.textContent = t('common.close');
showRefreshToast(t('codexlens.indexSuccess'), 'success');
// Auto-close after 2 seconds
setTimeout(function() {
closeCodexLensIndexModal();
// Refresh status
if (typeof loadCodexLensStatus === 'function') {
loadCodexLensStatus().then(function() {
renderToolsSection();
if (window.lucide) lucide.createIcons();
});
}
}, 2000);
} else {
if (progressBar) progressBar.classList.add('bg-destructive');
if (statusText) statusText.textContent = t('codexlens.indexFailed') + ': ' + message;
if (cancelBtn) cancelBtn.textContent = t('common.close');
showRefreshToast(t('codexlens.indexFailed') + ': ' + message, 'error');
}
}
/**
* Cancel indexing
*/
function cancelCodexLensIndex() {
closeCodexLensIndexModal();
}
/**
* Close index modal
*/
function closeCodexLensIndexModal() {
var modal = document.getElementById('codexlensIndexModal');
if (modal) modal.remove();
// Unregister WebSocket handler
if (typeof unregisterWsEventHandler === 'function' && window.codexlensIndexProgressHandler) {
unregisterWsEventHandler('CODEXLENS_INDEX_PROGRESS', window.codexlensIndexProgressHandler);
}
}
/**