diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index 091e1b5f..d844ee00 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -88,10 +88,15 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise res.end(JSON.stringify({ success: true, indexes: [], totalSize: 0, totalSizeFormatted: '0 B' })); return true; } - // Get config for index directory path - const configResult = await executeCodexLens(['config', '--json']); + + // Execute all CLI commands in parallel + const [configResult, projectsResult, statusResult] = await Promise.all([ + executeCodexLens(['config', '--json']), + executeCodexLens(['projects', 'list', '--json']), + executeCodexLens(['status', '--json']) + ]); + let indexDir = ''; - if (configResult.success) { try { const config = extractJSON(configResult.output); @@ -104,8 +109,6 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise } } - // Get project list using 'projects list' command - const projectsResult = await executeCodexLens(['projects', 'list', '--json']); let indexes: any[] = []; let totalSize = 0; let vectorIndexCount = 0; @@ -115,7 +118,8 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise try { const projectsData = extractJSON(projectsResult.output); if (projectsData.success && Array.isArray(projectsData.result)) { - const { statSync, existsSync } = await import('fs'); + const { stat, readdir } = await import('fs/promises'); + const { existsSync } = await import('fs'); const { basename, join } = await import('path'); for (const project of projectsData.result) { @@ -136,15 +140,14 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise // Try to get actual index size from index_root if (project.index_root && existsSync(project.index_root)) { try { - const { readdirSync } = await import('fs'); - const files = readdirSync(project.index_root); + const files = await readdir(project.index_root); for (const file of files) { try { const filePath = join(project.index_root, file); - const stat = statSync(filePath); - projectSize += stat.size; - if (!lastModified || stat.mtime > lastModified) { - lastModified = stat.mtime; + const fileStat = await stat(filePath); + projectSize += fileStat.size; + if (!lastModified || fileStat.mtime > lastModified) { + lastModified = fileStat.mtime; } // Check for vector/embedding files if (file.includes('vector') || file.includes('embedding') || @@ -194,8 +197,7 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise } } - // Also get summary stats from status command - const statusResult = await executeCodexLens(['status', '--json']); + // Parse summary stats from status command (already fetched in parallel) let statusSummary: any = {}; if (statusResult.success) { @@ -250,6 +252,71 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise return true; } + // API: CodexLens Dashboard Init - Aggregated endpoint for page initialization + if (pathname === '/api/codexlens/dashboard-init') { + try { + const venvStatus = await checkVenvStatus(); + + if (!venvStatus.ready) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + installed: false, + status: venvStatus, + config: { index_dir: '~/.codexlens/indexes', index_count: 0 }, + semantic: { available: false } + })); + return true; + } + + // Parallel fetch all initialization data + const [configResult, statusResult, semanticStatus] = await Promise.all([ + executeCodexLens(['config', '--json']), + executeCodexLens(['status', '--json']), + checkSemanticStatus() + ]); + + // Parse config + let config = { index_dir: '~/.codexlens/indexes', index_count: 0 }; + if (configResult.success) { + try { + const configData = extractJSON(configResult.output); + if (configData.success && configData.result) { + config.index_dir = configData.result.index_dir || configData.result.index_root || config.index_dir; + } + } catch (e) { + console.error('[CodexLens] Failed to parse config for dashboard init:', e.message); + } + } + + // Parse status + let statusData: any = {}; + if (statusResult.success) { + try { + const status = extractJSON(statusResult.output); + if (status.success && status.result) { + config.index_count = status.result.projects_count || 0; + statusData = status.result; + } + } catch (e) { + console.error('[CodexLens] Failed to parse status for dashboard init:', e.message); + } + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + installed: true, + status: venvStatus, + config, + semantic: semanticStatus, + statusData + })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: err.message })); + } + return true; + } + // API: CodexLens Bootstrap (Install) if (pathname === '/api/codexlens/bootstrap' && req.method === 'POST') { handlePostRequest(req, res, async () => { diff --git a/ccw/src/tools/codex-lens.ts b/ccw/src/tools/codex-lens.ts index e29d6cab..50ee6f68 100644 --- a/ccw/src/tools/codex-lens.ts +++ b/ccw/src/tools/codex-lens.ts @@ -33,6 +33,14 @@ const VENV_PYTHON = let bootstrapChecked = false; let bootstrapReady = false; +// Venv status cache with TTL +interface VenvStatusCache { + status: ReadyStatus; + timestamp: number; +} +let venvStatusCache: VenvStatusCache | null = null; +const VENV_STATUS_TTL = 5 * 60 * 1000; // 5 minutes TTL + // Track running indexing process for cancellation let currentIndexingProcess: ReturnType | null = null; let currentIndexingAborted = false; @@ -116,6 +124,13 @@ interface ProgressInfo { totalFiles?: number; } +/** + * Clear venv status cache (call after install/uninstall operations) + */ +function clearVenvStatusCache(): void { + venvStatusCache = null; +} + /** * Detect available Python 3 executable * @returns Python executable command @@ -138,17 +153,27 @@ function getSystemPython(): string { /** * Check if CodexLens venv exists and has required packages + * @param force - Force refresh cache (default: false) * @returns Ready status */ -async function checkVenvStatus(): Promise { +async function checkVenvStatus(force = false): Promise { + // Use cached result if available and not expired + if (!force && venvStatusCache && (Date.now() - venvStatusCache.timestamp < VENV_STATUS_TTL)) { + return venvStatusCache.status; + } + // Check venv exists if (!existsSync(CODEXLENS_VENV)) { - return { ready: false, error: 'Venv not found' }; + const result = { ready: false, error: 'Venv not found' }; + venvStatusCache = { status: result, timestamp: Date.now() }; + return result; } // Check python executable exists if (!existsSync(VENV_PYTHON)) { - return { ready: false, error: 'Python executable not found in venv' }; + const result = { ready: false, error: 'Python executable not found in venv' }; + venvStatusCache = { status: result, timestamp: Date.now() }; + return result; } // Check codexlens is importable @@ -169,15 +194,21 @@ async function checkVenvStatus(): Promise { }); child.on('close', (code) => { + let result: ReadyStatus; if (code === 0) { - resolve({ ready: true, version: stdout.trim() }); + result = { ready: true, version: stdout.trim() }; } else { - resolve({ ready: false, error: `CodexLens not installed: ${stderr}` }); + result = { ready: false, error: `CodexLens not installed: ${stderr}` }; } + // Cache the result + venvStatusCache = { status: result, timestamp: Date.now() }; + resolve(result); }); child.on('error', (err) => { - resolve({ ready: false, error: `Failed to check venv: ${err.message}` }); + const result = { ready: false, error: `Failed to check venv: ${err.message}` }; + venvStatusCache = { status: result, timestamp: Date.now() }; + resolve(result); }); }); } @@ -581,6 +612,8 @@ async function bootstrapVenv(): Promise { execSync(`"${pipPath}" install codexlens`, { stdio: 'inherit' }); } + // Clear cache after successful installation + clearVenvStatusCache(); return { success: true }; } catch (err) { return { success: false, error: `Failed to install codexlens: ${(err as Error).message}` }; @@ -1300,6 +1333,7 @@ async function uninstallCodexLens(): Promise { // Reset bootstrap cache bootstrapChecked = false; bootstrapReady = false; + clearVenvStatusCache(); console.log('[CodexLens] CodexLens uninstalled successfully'); return { success: true, message: 'CodexLens uninstalled successfully' };