diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index e6a6abe5..762d768c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,5 +1,6 @@ # Claude Instructions + - **CLI Tools Usage**: @~/.claude/workflows/cli-tools-usage.md - **Coding Philosophy**: @~/.claude/workflows/coding-philosophy.md - **Context Requirements**: @~/.claude/workflows/context-tools.md diff --git a/.claude/workflows/windows-platform.md b/.claude/workflows/windows-platform.md index 06aa3681..adfb98a9 100644 --- a/.claude/workflows/windows-platform.md +++ b/.claude/workflows/windows-platform.md @@ -1,16 +1,19 @@ # Windows Platform Guidelines -## Path Format Guidelines +## Path Format -### MCP Tools -- Use double backslash: `D:\\path\\file.txt` -- Example: `read_file(paths="D:\\Claude_dms3\\src\\index.ts")` +- **MCP Tools**: `D:\\path\\file.txt` +- **Bash**: `D:/path/file.txt` or `/d/path/file.txt` +- **Relative**: `./src/index.ts` -### Bash Commands -- Use forward slash: `D:/path/file.txt` or `/d/path/file.txt` -- Example: `cd D:/Claude_dms3/src` +## Bash Rules (Prevent Garbage Files) -### Relative Paths -- Universal format works in both MCP and Bash contexts -- Example: `./src/index.ts` or `../shared/utils.ts` +1. **Null redirect**: `command > NUL 2>&1` +2. **Quote all**: `echo "$VAR"`, `cat "file name.txt"` +3. **Variable assignment**: `export VAR=value && command` +4. **Regex escape**: `grep -F "State"` or `grep "State\"` +5. **Pipe output**: `command 2>&1 | ...` (avoid bare command output) +## Tool Priority + +MCP Tools > PowerShell > Git Bash > cmd diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index d69afa31..83bbeb37 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -423,20 +423,14 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise watcherProcess = null; } - // Cancel any running indexing process - if (currentIndexingProcess) { + // Cancel any running indexing process using exported function + if (isIndexingInProgress()) { console.log('[CodexLens] Cancelling indexing before uninstall...'); try { - if (process.platform === 'win32') { - const { execSync } = require('child_process'); - execSync(`taskkill /pid ${currentIndexingProcess.pid} /T /F`, { stdio: 'ignore' }); - } else { - currentIndexingProcess.kill('SIGKILL'); - } + cancelIndexing(); } catch { // Ignore errors } - currentIndexingProcess = null; } // Wait a moment for processes to fully exit and release handles @@ -977,27 +971,80 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise // API: List available GPU devices for selection if (pathname === '/api/codexlens/gpu/list' && req.method === 'GET') { try { - // Check if CodexLens is installed first (without auto-installing) + // Try CodexLens gpu-list first if available const venvStatus = await checkVenvStatus(); - if (!venvStatus.ready) { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, devices: [], selected_device_id: null })); - return true; + if (venvStatus.ready) { + const result = await executeCodexLens(['gpu-list', '--json']); + if (result.success) { + try { + const parsed = extractJSON(result.output); + if (parsed.devices && parsed.devices.length > 0) { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(parsed)); + return true; + } + } catch { + // Fall through to system detection + } + } } - const result = await executeCodexLens(['gpu-list', '--json']); - if (result.success) { + + // Fallback: Use system commands to detect GPUs + const devices: Array<{ name: string; type: string; index: number }> = []; + + if (process.platform === 'win32') { + // Windows: Use WMIC to get GPU info try { - const parsed = extractJSON(result.output); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(parsed)); - } catch { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, devices: [], output: result.output })); + const { execSync } = await import('child_process'); + const wmicOutput = execSync('wmic path win32_VideoController get name', { + encoding: 'utf-8', + timeout: 10000, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + const lines = wmicOutput.split('\n') + .map(line => line.trim()) + .filter(line => line && line !== 'Name'); + + lines.forEach((name, index) => { + if (name) { + const isIntegrated = name.toLowerCase().includes('intel') || + name.toLowerCase().includes('integrated'); + devices.push({ + name: name, + type: isIntegrated ? 'integrated' : 'discrete', + index: index + }); + } + }); + } catch (e) { + console.warn('[CodexLens] WMIC GPU detection failed:', (e as Error).message); } } else { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, error: result.error })); + // Linux/Mac: Try nvidia-smi for NVIDIA GPUs + try { + const { execSync } = await import('child_process'); + const nvidiaOutput = execSync('nvidia-smi --query-gpu=name --format=csv,noheader', { + encoding: 'utf-8', + timeout: 10000, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + const lines = nvidiaOutput.split('\n').filter(line => line.trim()); + lines.forEach((name, index) => { + devices.push({ + name: name.trim(), + type: 'discrete', + index: index + }); + }); + } catch { + // NVIDIA not available, that's fine + } } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, devices: devices, selected_device_id: null })); } catch (err) { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: false, error: err.message })); diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index a78e2316..b4ecb499 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -436,6 +436,32 @@ const i18n = { 'codexlens.modelListError': 'Failed to load models', 'codexlens.noModelsAvailable': 'No models available', + // FastEmbed Installation + 'codexlens.fastembedNotInstalled': 'FastEmbed not installed', + 'codexlens.fastembedDesc': 'FastEmbed provides local embedding models for semantic search', + 'codexlens.selectMode': 'Select Mode', + 'codexlens.cpuModeShort': 'Standard', + 'codexlens.directmlModeShort': 'Windows GPU', + 'codexlens.cudaModeShort': 'NVIDIA GPU', + 'codexlens.installFastembed': 'Install FastEmbed', + 'codexlens.installingFastembed': 'Installing FastEmbed...', + 'codexlens.installMayTakeTime': 'This may take several minutes...', + 'codexlens.fastembedInstalled': 'FastEmbed Installed', + 'codexlens.fastembedInstallFailed': 'FastEmbed installation failed', + 'codexlens.installFastembedFirst': 'Install FastEmbed above to manage local embedding models', + 'codexlens.detectedGpus': 'Detected GPUs', + 'codexlens.reinstallOptions': 'Reinstall Options', + 'codexlens.reinstallDesc': 'Reinstall with a different GPU mode:', + 'codexlens.confirmReinstall': 'This will reinstall FastEmbed. Continue?', + 'codexlens.fastembedReinstalled': 'FastEmbed reinstalled', + 'codexlens.reinstallingFastembed': 'Reinstalling FastEmbed...', + 'codexlens.activeAccelerator': 'Active Accelerator', + 'codexlens.active': 'Active', + 'codexlens.downloadedModels': 'Downloaded Models', + 'codexlens.noLocalModels': 'No models downloaded', + 'codexlens.configuredModels': 'Configured in API Settings', + 'codexlens.commonModels': 'Common Models', + // Model Download Progress 'codexlens.downloadingModel': 'Downloading', 'codexlens.connectingToHuggingFace': 'Connecting to Hugging Face...', @@ -2418,6 +2444,32 @@ const i18n = { 'codexlens.modelListError': '加载模型列表失败', 'codexlens.noModelsAvailable': '没有可用模型', + // FastEmbed 安装 + 'codexlens.fastembedNotInstalled': 'FastEmbed 未安装', + 'codexlens.fastembedDesc': 'FastEmbed 提供本地嵌入模型用于语义搜索', + 'codexlens.selectMode': '选择模式', + 'codexlens.cpuModeShort': '标准', + 'codexlens.directmlModeShort': 'Windows GPU', + 'codexlens.cudaModeShort': 'NVIDIA GPU', + 'codexlens.installFastembed': '安装 FastEmbed', + 'codexlens.installingFastembed': '正在安装 FastEmbed...', + 'codexlens.installMayTakeTime': '这可能需要几分钟...', + 'codexlens.fastembedInstalled': 'FastEmbed 已安装', + 'codexlens.fastembedInstallFailed': 'FastEmbed 安装失败', + 'codexlens.installFastembedFirst': '请先在上方安装 FastEmbed 以管理本地嵌入模型', + 'codexlens.detectedGpus': '检测到的 GPU', + 'codexlens.reinstallOptions': '重新安装选项', + 'codexlens.reinstallDesc': '使用不同的 GPU 模式重新安装:', + 'codexlens.confirmReinstall': '这将重新安装 FastEmbed。是否继续?', + 'codexlens.fastembedReinstalled': 'FastEmbed 已重新安装', + 'codexlens.reinstallingFastembed': '正在重新安装 FastEmbed...', + 'codexlens.activeAccelerator': '当前加速器', + 'codexlens.active': '使用中', + 'codexlens.downloadedModels': '已下载模型', + 'codexlens.noLocalModels': '暂无已下载模型', + 'codexlens.configuredModels': '已配置的 API 模型', + 'codexlens.commonModels': '常用模型', + // 模型下载进度 'codexlens.downloadingModel': '正在下载', 'codexlens.connectingToHuggingFace': '正在连接 Hugging Face...', diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 4511f92c..8fbf33ae 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -511,6 +511,9 @@ function initCodexLensConfigEvents(currentConfig) { }; } + // Load FastEmbed installation status (show/hide install card) + loadFastEmbedInstallStatus(); + // Load semantic dependencies status loadSemanticDepsStatus(); @@ -749,11 +752,13 @@ async function loadEnvVariables() { container.innerHTML = '
Loading...
'; try { - // Fetch env vars and configured models in parallel - var [envResponse, embeddingPoolResponse, rerankerPoolResponse] = await Promise.all([ + // Fetch env vars, configured models, and local models in parallel + var [envResponse, embeddingPoolResponse, rerankerPoolResponse, localModelsResponse, localRerankerModelsResponse] = await Promise.all([ fetch('/api/codexlens/env'), fetch('/api/litellm-api/embedding-pool').catch(function() { return null; }), - fetch('/api/litellm-api/reranker-pool').catch(function() { return null; }) + fetch('/api/litellm-api/reranker-pool').catch(function() { return null; }), + fetch('/api/codexlens/models').catch(function() { return null; }), + fetch('/api/codexlens/reranker/models').catch(function() { return null; }) ]); var result = await envResponse.json(); @@ -777,6 +782,26 @@ async function loadEnvVariables() { configuredRerankerModels = rerankerData.availableModels || []; } + // Get local downloaded embedding models + var localEmbeddingModels = []; + if (localModelsResponse && localModelsResponse.ok) { + var localData = await localModelsResponse.json(); + if (localData.success && localData.models) { + // Filter to only downloaded models + localEmbeddingModels = localData.models.filter(function(m) { return m.downloaded; }); + } + } + + // Get local downloaded reranker models + var localRerankerModels = []; + if (localRerankerModelsResponse && localRerankerModelsResponse.ok) { + var localRerankerData = await localRerankerModelsResponse.json(); + if (localRerankerData.success && localRerankerData.models) { + // Filter to only downloaded models + localRerankerModels = localRerankerData.models.filter(function(m) { return m.downloaded; }); + } + } + var env = result.env || {}; var settings = result.settings || {}; // Current settings from settings.json var html = '
'; @@ -841,10 +866,13 @@ async function loadEnvVariables() { var isReranker = key.indexOf('RERANKER') !== -1; var backendKey = isEmbedding ? 'CODEXLENS_EMBEDDING_BACKEND' : 'CODEXLENS_RERANKER_BACKEND'; var isApiBackend = env[backendKey] === 'litellm' || env[backendKey] === 'api'; - - // Choose model list based on backend type - var modelList = isApiBackend ? (config.apiModels || config.models || []) : (config.localModels || config.models || []); + + // Get actual downloaded local models + var actualLocalModels = isEmbedding ? localEmbeddingModels : localRerankerModels; + // Get configured API models var configuredModels = isEmbedding ? configuredEmbeddingModels : configuredRerankerModels; + // Fallback preset list for API models + var apiModelList = config.apiModels || []; html += '
' + '' + @@ -856,31 +884,44 @@ async function loadEnvVariables() { '
' + ''; - // For API backend: show configured models from API settings first - if (isApiBackend && configuredModels.length > 0) { - html += ''; - configuredModels.forEach(function(model) { - var providers = model.providers ? model.providers.join(', ') : ''; - html += ''; - }); - if (modelList.length > 0) { - html += ''; + if (isApiBackend) { + // For API backend: show configured models from API settings first + if (configuredModels.length > 0) { + html += ''; + configuredModels.forEach(function(model) { + var providers = model.providers ? model.providers.join(', ') : ''; + html += ''; + }); + } + // Then show common API models as suggestions + if (apiModelList.length > 0) { + html += ''; + apiModelList.forEach(function(group) { + group.items.forEach(function(model) { + // Skip if already in configured list + var exists = configuredModels.some(function(m) { return m.modelId === model; }); + if (!exists) { + html += ''; + } + }); + }); + } + } else { + // For local backend (fastembed): show actually downloaded models + if (actualLocalModels.length > 0) { + html += ''; + actualLocalModels.forEach(function(model) { + var modelId = model.model_id || model.id || model.name; + var displayName = model.display_name || model.name || modelId; + html += ''; + }); + } else { + html += ''; } } - - // Add model list (local or API based on backend) - modelList.forEach(function(group) { - group.items.forEach(function(model) { - // Skip if already in configured list - var exists = configuredModels.some(function(m) { return m.modelId === model; }); - if (!exists) { - html += ''; - } - }); - }); html += '
'; } else { @@ -1558,6 +1599,432 @@ async function installSplade(gpu) { // MODEL MANAGEMENT // ============================================================ +/** + * Build FastEmbed installation card UI with GPU mode options + * @param {Array} gpuDevices - List of detected GPU devices + */ +function buildFastEmbedInstallCardUI(gpuDevices) { + gpuDevices = gpuDevices || []; + + // Build GPU devices info section + var gpuInfoHtml = ''; + if (gpuDevices.length > 0) { + gpuInfoHtml = + '
' + + '
' + + '' + + (t('codexlens.detectedGpus') || 'Detected GPUs') + ':' + + '
' + + '
'; + + gpuDevices.forEach(function(device) { + var typeIcon = device.type === 'integrated' ? 'cpu' : 'zap'; + var typeClass = device.type === 'integrated' ? 'text-muted-foreground' : 'text-green-500'; + gpuInfoHtml += + '
' + + '' + + '' + escapeHtml(device.name) + '' + + '(' + (device.type === 'integrated' ? 'Integrated' : 'Discrete') + ')' + + '
'; + }); + + gpuInfoHtml += '
'; + } + + return '
' + + // Header + '
' + + '
' + + '' + + '

' + (t('codexlens.fastembedNotInstalled') || 'FastEmbed Not Installed') + '

' + + '
' + + '
' + + // Content + '
' + + '

' + + (t('codexlens.fastembedDesc') || 'FastEmbed provides local embedding models for semantic search. Select your preferred acceleration mode below.') + + '

' + + // Show detected GPUs + gpuInfoHtml + + // GPU Mode Cards + '
' + + '
' + + (t('codexlens.selectMode') || 'Select Acceleration Mode') + ':' + + '
' + + '
' + + // CPU Option Card + '' + + // DirectML Option Card + '' + + // CUDA Option Card + '' + + '
' + + '
' + + // Install Button + '' + + '
' + + '
'; +} + +/** + * Build FastEmbed status card UI (when installed) + * @param {Object} status - Semantic status object + * @param {Array} gpuDevices - List of detected GPU devices + * @param {Object} litellmStatus - LiteLLM installation status from API settings endpoint + */ +function buildFastEmbedStatusCardUI(status, gpuDevices, litellmStatus) { + gpuDevices = gpuDevices || []; + litellmStatus = litellmStatus || {}; + + // Determine accelerator info + var accelerator = status.accelerator || 'CPU'; + var acceleratorIcon = accelerator === 'CPU' ? 'cpu' : + accelerator.includes('CUDA') ? 'zap' : 'monitor'; + var acceleratorClass = accelerator === 'CPU' ? 'text-muted-foreground' : 'text-green-500'; + var acceleratorBgClass = accelerator === 'CPU' ? 'bg-muted/50' : + accelerator.includes('CUDA') ? 'bg-green-500/20' : 'bg-blue-500/20'; + + // Check if LiteLLM (ccw-litellm) is installed - use the same check as API Settings + var isLitellmInstalled = litellmStatus.installed === true; + + // Build GPU devices section with active indicator + var gpuInfoHtml = ''; + if (gpuDevices.length > 0) { + gpuInfoHtml = + '
' + + '
' + + '' + + (t('codexlens.detectedGpus') || 'Detected GPUs') + + '
' + + '
'; + + gpuDevices.forEach(function(device, index) { + var isActive = false; + // Determine if this GPU matches the active accelerator + if (accelerator === 'CUDA' && device.type === 'discrete' && device.name.toLowerCase().includes('nvidia')) { + isActive = true; + } else if (accelerator === 'DirectML' && device.type === 'discrete') { + isActive = true; + } else if (accelerator === 'CPU' && device.type === 'integrated') { + isActive = index === 0; // First integrated GPU is likely active + } + + var typeIcon = device.type === 'integrated' ? 'cpu' : 'zap'; + var activeClass = isActive ? 'border-green-500 bg-green-500/10' : 'border-border bg-muted/30'; + var activeBadge = isActive ? + '' + + (t('codexlens.active') || 'Active') + + '' : ''; + + gpuInfoHtml += + '
' + + '
' + + '' + + '
' + + '
' + escapeHtml(device.name) + '
' + + '
' + (device.type === 'integrated' ? 'Integrated' : 'Discrete') + '
' + + '
' + + '
' + + activeBadge + + '
'; + }); + + gpuInfoHtml += '
'; + } + + // Active accelerator section + var activeAcceleratorHtml = + '
' + + '
' + + '
' + + '' + + '
' + + '
' + (t('codexlens.activeAccelerator') || 'Active Accelerator') + '
' + + '
' + accelerator + '
' + + '
' + + '
' + + '
' + + '
Backend
' + + '
' + (status.backend || 'fastembed') + '
' + + '
' + + '
' + + '
'; + + return '
' + + // Header + '
' + + '
' + + '
' + + '' + + '

' + (t('codexlens.fastembedInstalled') || 'FastEmbed Installed') + '

' + + '
' + + '
' + + // LiteLLM status badge + (isLitellmInstalled ? + '' + + ' LiteLLM' + + '' : '') + + '' + + '' + + accelerator + + '' + + '
' + + '
' + + '
' + + // Content + '
' + + // Active accelerator section + activeAcceleratorHtml + + // GPU devices + gpuInfoHtml + + // Reinstall option (collapsed by default) + '
' + + '' + + '' + + (t('codexlens.reinstallOptions') || 'Reinstall Options') + + '' + + '
' + + '

' + (t('codexlens.reinstallDesc') || 'Reinstall with a different GPU mode:') + '

' + + '
' + + '' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
'; +} + +/** + * Reinstall FastEmbed with specified GPU mode + * @param {string} mode - GPU mode: cpu, directml, cuda + */ +async function reinstallFastEmbed(mode) { + if (!confirm((t('codexlens.confirmReinstall') || 'This will reinstall FastEmbed with ' + mode + ' mode. Continue?'))) { + return; + } + + var card = document.getElementById('fastembedInstallCard'); + if (!card) return; + + var modeLabels = { + cpu: 'CPU', + cuda: 'NVIDIA CUDA', + directml: 'DirectML' + }; + + // Show reinstalling state + card.innerHTML = + '
' + + '
' + + '
' + + '
' + + '

' + (t('codexlens.reinstallingFastembed') || 'Reinstalling FastEmbed...') + '

' + + '
' + + '
' + + '
' + + (t('codexlens.installingMode') || 'Installing with') + ': ' + modeLabels[mode] + + '
' + + '
'; + + try { + var response = await fetch('/api/codexlens/semantic/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gpuMode: mode }) + }); + var result = await response.json(); + + if (result.success) { + showRefreshToast((t('codexlens.fastembedReinstalled') || 'FastEmbed reinstalled') + ' (' + modeLabels[mode] + ')', 'success'); + // Reload status + loadFastEmbedInstallStatus(); + } else { + showRefreshToast((t('codexlens.fastembedInstallFailed') || 'FastEmbed reinstall failed') + ': ' + result.error, 'error'); + loadFastEmbedInstallStatus(); + } + } catch (err) { + showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error'); + loadFastEmbedInstallStatus(); + } +} + +/** + * Load FastEmbed installation status and show card + * Card is always visible - shows install UI or status UI based on state + */ +async function loadFastEmbedInstallStatus() { + console.log('[CodexLens] loadFastEmbedInstallStatus called'); + var card = document.getElementById('fastembedInstallCard'); + console.log('[CodexLens] fastembedInstallCard element:', card); + if (!card) { + console.warn('[CodexLens] fastembedInstallCard element not found!'); + return; + } + + try { + // Load semantic status, GPU list, and LiteLLM status in parallel + console.log('[CodexLens] Fetching semantic status, GPU list, and LiteLLM status...'); + var [semanticResponse, gpuResponse, litellmResponse] = await Promise.all([ + fetch('/api/codexlens/semantic/status'), + fetch('/api/codexlens/gpu/list'), + fetch('/api/litellm-api/ccw-litellm/status').catch(function() { return { ok: false }; }) + ]); + + var result = await semanticResponse.json(); + var gpuResult = await gpuResponse.json(); + var gpuDevices = gpuResult.devices || []; + + // Get LiteLLM status (same endpoint as API Settings page) + var litellmStatus = {}; + if (litellmResponse.ok) { + try { + litellmStatus = await litellmResponse.json(); + } catch (e) { + console.warn('[CodexLens] Failed to parse LiteLLM status:', e); + } + } + + console.log('[CodexLens] Semantic status:', result); + console.log('[CodexLens] GPU devices:', gpuDevices); + console.log('[CodexLens] LiteLLM status:', litellmStatus); + + if (result.available) { + // FastEmbed is installed - show status card + console.log('[CodexLens] FastEmbed available, showing status card'); + card.innerHTML = buildFastEmbedStatusCardUI(result, gpuDevices, litellmStatus); + card.classList.remove('hidden'); + if (window.lucide) lucide.createIcons(); + } else { + // FastEmbed not installed - show install card with GPU devices + console.log('[CodexLens] FastEmbed NOT available, showing install card'); + card.innerHTML = buildFastEmbedInstallCardUI(gpuDevices); + card.classList.remove('hidden'); + if (window.lucide) lucide.createIcons(); + } + } catch (err) { + // On error, show install card without GPU info + console.error('[CodexLens] Error loading FastEmbed status:', err); + card.innerHTML = buildFastEmbedInstallCardUI([]); + card.classList.remove('hidden'); + if (window.lucide) lucide.createIcons(); + } +} + +/** + * Install FastEmbed with selected GPU mode + */ +async function installFastEmbed() { + var card = document.getElementById('fastembedInstallCard'); + if (!card) return; + + // Get selected GPU mode + var selectedMode = 'cpu'; + var radios = document.querySelectorAll('input[name="fastembedMode"]'); + radios.forEach(function(radio) { + if (radio.checked) { + selectedMode = radio.value; + } + }); + + var modeLabels = { + cpu: 'CPU', + cuda: 'NVIDIA CUDA', + directml: 'DirectML' + }; + + // Show installing state in card + card.innerHTML = + '
' + + '
' + + '
' + + '
' + + '

' + (t('codexlens.installingFastembed') || 'Installing FastEmbed...') + '

' + + '
' + + '
' + + '
' + + '
' + + (t('codexlens.installingMode') || 'Installing with') + ': ' + modeLabels[selectedMode] + '' + + '
' + + '
' + + (t('codexlens.installMayTakeTime') || 'This may take several minutes. Please do not close this page.') + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + + try { + var response = await fetch('/api/codexlens/semantic/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ gpuMode: selectedMode }) + }); + var result = await response.json(); + + if (result.success) { + showRefreshToast((t('codexlens.fastembedInstalled') || 'FastEmbed installed') + ' (' + modeLabels[selectedMode] + ')', 'success'); + // Hide card and reload status + await loadFastEmbedInstallStatus(); + await loadSemanticDepsStatus(); + await loadModelList(); + } else { + showRefreshToast((t('codexlens.fastembedInstallFailed') || 'FastEmbed installation failed') + ': ' + result.error, 'error'); + await loadFastEmbedInstallStatus(); + } + } catch (err) { + showRefreshToast(t('common.error') + ': ' + err.message, 'error'); + await loadFastEmbedInstallStatus(); + } +} + /** * Copy text to clipboard */ @@ -1602,7 +2069,11 @@ async function loadModelList() { if (!result.success) { var errorMsg = result.error || ''; if (errorMsg.includes('fastembed not installed') || errorMsg.includes('Semantic')) { - html += '
' + t('codexlens.semanticNotInstalled') + '
'; + // Just show a simple message - installation UI is in the separate card above + html += '
' + + '' + + '' + (t('codexlens.installFastembedFirst') || 'Install FastEmbed above to manage local embedding models') + '' + + '
'; } else { html += '
' + escapeHtml(errorMsg || t('common.unknownError')) + '
'; } @@ -2141,19 +2612,8 @@ function switchCodexLensModelTab(tabName) { * Update model mode (Local vs API) */ function updateModelMode(mode) { - var gpuContainer = document.getElementById('gpuSelectContainer'); var modeSelect = document.getElementById('modelModeSelect'); - // Show/hide GPU selector based on mode - if (gpuContainer) { - if (mode === 'local') { - gpuContainer.classList.remove('hidden'); - loadGpuDevicesForModeSelector(); - } else { - gpuContainer.classList.add('hidden'); - } - } - // Store mode preference (will be saved when locked) if (modeSelect) { modeSelect.setAttribute('data-current-mode', mode); @@ -2165,6 +2625,7 @@ function updateModelMode(mode) { */ async function loadGpuDevicesForModeSelector() { var gpuSelect = document.getElementById('gpuDeviceSelect'); + var gpuSection = document.getElementById('gpuConfigSection'); if (!gpuSelect) return; try { @@ -2172,19 +2633,28 @@ async function loadGpuDevicesForModeSelector() { if (!response.ok) { console.warn('[CodexLens] GPU list endpoint returned:', response.status); gpuSelect.innerHTML = ''; + // Hide section if no GPU devices available + if (gpuSection) gpuSection.classList.add('hidden'); return; } var result = await response.json(); var html = ''; - if (result.devices && result.devices.length > 0) { + if (result.devices && result.devices.length > 1) { + // Only show section if multiple GPUs available result.devices.forEach(function(device, index) { html += ''; }); + gpuSelect.innerHTML = html; + if (gpuSection) gpuSection.classList.remove('hidden'); + } else { + // Single or no GPU - hide section + gpuSelect.innerHTML = html; + if (gpuSection) gpuSection.classList.add('hidden'); } - gpuSelect.innerHTML = html; } catch (err) { console.error('Failed to load GPU devices:', err); + if (gpuSection) gpuSection.classList.add('hidden'); } } @@ -2259,7 +2729,6 @@ async function toggleModelModeLock() { */ async function initModelModeFromConfig() { var modeSelect = document.getElementById('modelModeSelect'); - var gpuContainer = document.getElementById('gpuSelectContainer'); if (!modeSelect) return; @@ -2272,16 +2741,6 @@ async function initModelModeFromConfig() { modeSelect.value = mode; modeSelect.setAttribute('data-current-mode', mode); - - // Show GPU selector for local mode - if (gpuContainer) { - if (mode === 'local') { - gpuContainer.classList.remove('hidden'); - loadGpuDevicesForModeSelector(); - } else { - gpuContainer.classList.add('hidden'); - } - } } catch (err) { console.error('Failed to load model mode config:', err); } @@ -3112,6 +3571,9 @@ async function renderCodexLensManager() { // Wait for LiteLLM config before loading semantic deps (it may need provider info) await litellmPromise; + // Load FastEmbed installation status (show/hide install card) + loadFastEmbedInstallStatus(); + // Always load semantic deps status - it needs GPU detection and device list // which are not included in the aggregated endpoint loadSemanticDepsStatus(); @@ -3121,7 +3583,6 @@ async function renderCodexLensManager() { // Initialize model mode and semantic status badge updateSemanticStatusBadge(); - loadGpuDevicesForModeSelector(); // Initialize file watcher status initWatcherStatus(); @@ -3238,6 +3699,10 @@ function buildCodexLensManagerPage(config) { '' + // Right Column '
' + + // FastEmbed Installation Card (shown when not installed) + '' + // Combined: Semantic Status + Model Management with Tabs '
' + // Compact Header with Semantic Status @@ -3249,16 +3714,6 @@ function buildCodexLensManagerPage(config) { '
' + '
' + '' + - // GPU Config Section (for local mode) - '
' + - '
' + - 'GPU:' + - '' + - '
' + - '

Backend configured in Environment Variables below

' + - '
' + // Tabs for Embedding / Reranker '
' + '
' + diff --git a/ccw/src/tools/codex-lens.ts b/ccw/src/tools/codex-lens.ts index f8771c9e..f453813b 100644 --- a/ccw/src/tools/codex-lens.ts +++ b/ccw/src/tools/codex-lens.ts @@ -389,7 +389,8 @@ async function checkPythonEnvForDirectML(): Promise { try { // Get Python version and architecture in one call - const checkScript = `import sys, struct; print(f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}|{struct.calcsize('P') * 8}")`; + // Use % formatting instead of f-string to avoid Windows shell escaping issues with curly braces + const checkScript = `import sys, struct; print('%d.%d.%d|%d' % (sys.version_info.major, sys.version_info.minor, sys.version_info.micro, struct.calcsize('P') * 8))`; const result = execSync(`"${pythonPath}" -c "${checkScript}"`, { encoding: 'utf-8', timeout: 10000 }).trim(); const [version, archStr] = result.split('|'); const architecture = parseInt(archStr, 10);