feat: Add CodexLens Manager to dashboard and enhance GPU management

- Introduced a new CodexLens Manager item in the dashboard for easier access.
- Implemented GPU management commands in the CLI, including listing available GPUs, selecting a specific GPU, and resetting to automatic detection.
- Enhanced the embedding generation process to utilize GPU resources more effectively, including batch size optimization for better performance.
- Updated the embedder to support device ID options for GPU selection, ensuring compatibility with DirectML and CUDA.
- Added detailed logging and error handling for GPU detection and selection processes.
- Updated package version to 6.2.9 and added comprehensive documentation for Codex Agent Execution Protocol.
This commit is contained in:
catlog22
2025-12-23 18:35:30 +08:00
parent 5ff2a43b70
commit 39056292b7
17 changed files with 1834 additions and 78 deletions

View File

@@ -80,6 +80,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
// API: CodexLens Index List - Get all indexed projects with details
if (pathname === '/api/codexlens/indexes') {
try {
// Check if CodexLens is installed first (without auto-installing)
const venvStatus = await checkVenvStatus();
if (!venvStatus.ready) {
res.writeHead(200, { 'Content-Type': 'application/json' });
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']);
let indexDir = '';
@@ -290,14 +297,24 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
// API: CodexLens Config - GET (Get current configuration with index count)
if (pathname === '/api/codexlens/config' && req.method === 'GET') {
try {
// Check if CodexLens is installed first (without auto-installing)
const venvStatus = await checkVenvStatus();
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 };
// If not installed, return default config without executing CodexLens
if (!venvStatus.ready) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(responseData));
return true;
}
// Fetch both config and status to merge index_count
const [configResult, statusResult] = await Promise.all([
executeCodexLens(['config', '--json']),
executeCodexLens(['status', '--json'])
]);
let responseData = { index_dir: '~/.codexlens/indexes', index_count: 0 };
// Parse config (extract JSON from output that may contain log messages)
if (configResult.success) {
try {
@@ -682,6 +699,87 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
return true;
}
// 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)
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;
}
const result = await executeCodexLens(['gpu-list', '--json']);
if (result.success) {
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 }));
}
} else {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: result.error }));
}
} catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
return true;
}
// API: Select GPU device for embedding
if (pathname === '/api/codexlens/gpu/select' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
const { device_id } = body;
if (device_id === undefined || device_id === null) {
return { success: false, error: 'device_id is required', status: 400 };
}
try {
const result = await executeCodexLens(['gpu-select', String(device_id), '--json']);
if (result.success) {
try {
const parsed = extractJSON(result.output);
return parsed;
} catch {
return { success: true, message: 'GPU selected', output: result.output };
}
} else {
return { success: false, error: result.error, status: 500 };
}
} catch (err) {
return { success: false, error: err.message, status: 500 };
}
});
return true;
}
// API: Reset GPU selection to auto-detection
if (pathname === '/api/codexlens/gpu/reset' && req.method === 'POST') {
handlePostRequest(req, res, async () => {
try {
const result = await executeCodexLens(['gpu-reset', '--json']);
if (result.success) {
try {
const parsed = extractJSON(result.output);
return parsed;
} catch {
return { success: true, message: 'GPU selection reset', output: result.output };
}
} else {
return { success: false, error: result.error, status: 500 };
}
} catch (err) {
return { success: false, error: err.message, status: 500 };
}
});
return true;
}
// API: CodexLens Semantic Search Install (with GPU mode support)
if (pathname === '/api/codexlens/semantic/install' && req.method === 'POST') {
handlePostRequest(req, res, async (body) => {
@@ -721,6 +819,13 @@ export async function handleCodexLensRoutes(ctx: RouteContext): Promise<boolean>
// API: CodexLens Model List (list available embedding models)
if (pathname === '/api/codexlens/models' && req.method === 'GET') {
try {
// Check if CodexLens is installed first (without auto-installing)
const venvStatus = await checkVenvStatus();
if (!venvStatus.ready) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: 'CodexLens not installed' }));
return true;
}
const result = await executeCodexLens(['model-list', '--json']);
if (result.success) {
try {

View File

@@ -4,9 +4,56 @@
* Aggregated status endpoint for faster dashboard loading
*/
import type { IncomingMessage, ServerResponse } from 'http';
import { existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { getCliToolsStatus } from '../../tools/cli-executor.js';
import { checkVenvStatus, checkSemanticStatus } from '../../tools/codex-lens.js';
/**
* Check CCW installation status
* Verifies that required workflow files are installed in user's home directory
*/
function checkCcwInstallStatus(): {
installed: boolean;
workflowsInstalled: boolean;
missingFiles: string[];
installPath: string;
} {
const claudeDir = join(homedir(), '.claude');
const workflowsDir = join(claudeDir, 'workflows');
// Required workflow files for full functionality
const requiredFiles = [
'chinese-response.md',
'windows-platform.md',
'cli-tools-usage.md',
'coding-philosophy.md',
'context-tools.md',
'file-modification.md'
];
const missingFiles: string[] = [];
// Check each required file
for (const file of requiredFiles) {
const filePath = join(workflowsDir, file);
if (!existsSync(filePath)) {
missingFiles.push(file);
}
}
const workflowsInstalled = existsSync(workflowsDir) && missingFiles.length === 0;
const installed = existsSync(claudeDir) && workflowsInstalled;
return {
installed,
workflowsInstalled,
missingFiles,
installPath: claudeDir
};
}
export interface RouteContext {
pathname: string;
url: URL;
@@ -27,6 +74,9 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
// API: Aggregated Status (all statuses in one call)
if (pathname === '/api/status/all') {
try {
// Check CCW installation status (sync, fast)
const ccwInstallStatus = checkCcwInstallStatus();
// Execute all status checks in parallel
const [cliStatus, codexLensStatus, semanticStatus] = await Promise.all([
getCliToolsStatus(),
@@ -39,6 +89,7 @@ export async function handleStatusRoutes(ctx: RouteContext): Promise<boolean> {
cli: cliStatus,
codexLens: codexLensStatus,
semantic: semanticStatus,
ccwInstall: ccwInstallStatus,
timestamp: new Date().toISOString()
};

View File

@@ -5,6 +5,7 @@
let cliToolStatus = { gemini: {}, qwen: {}, codex: {}, claude: {} };
let codexLensStatus = { ready: false };
let semanticStatus = { available: false };
let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
let defaultCliTool = 'gemini';
let promptConcatFormat = localStorage.getItem('ccw-prompt-format') || 'plain'; // plain, yaml, json
@@ -38,10 +39,12 @@ async function loadAllStatuses() {
cliToolStatus = data.cli || { gemini: {}, qwen: {}, codex: {}, claude: {} };
codexLensStatus = data.codexLens || { ready: false };
semanticStatus = data.semantic || { available: false };
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
// Update badges
updateCliBadge();
updateCodexLensBadge();
updateCcwInstallBadge();
return data;
} catch (err) {
@@ -187,6 +190,25 @@ function updateCodexLensBadge() {
}
}
function updateCcwInstallBadge() {
const badge = document.getElementById('badgeCcwInstall');
if (badge) {
if (ccwInstallStatus.installed) {
badge.textContent = t('status.installed');
badge.classList.add('text-success');
badge.classList.remove('text-warning', 'text-destructive');
} else if (ccwInstallStatus.workflowsInstalled === false) {
badge.textContent = t('status.incomplete');
badge.classList.add('text-warning');
badge.classList.remove('text-success', 'text-destructive');
} else {
badge.textContent = t('status.notInstalled');
badge.classList.add('text-destructive');
badge.classList.remove('text-success', 'text-warning');
}
}
}
// ========== Rendering ==========
function renderCliStatus() {
const container = document.getElementById('cli-status-panel');
@@ -310,6 +332,39 @@ function renderCliStatus() {
</div>
` : '';
// CCW Installation Status card (show warning if not fully installed)
const ccwInstallHtml = !ccwInstallStatus.installed ? `
<div class="cli-tool-card tool-ccw-install unavailable" style="border: 1px solid var(--warning); background: rgba(var(--warning-rgb), 0.05);">
<div class="cli-tool-header">
<span class="cli-tool-status status-unavailable" style="background: var(--warning);"></span>
<span class="cli-tool-name">${t('status.ccwInstall')}</span>
<span class="badge px-1.5 py-0.5 text-xs rounded bg-warning/20 text-warning">${t('status.required')}</span>
</div>
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
${t('status.ccwInstallDesc')}
</div>
<div class="cli-tool-info mt-2">
<span class="text-warning flex items-center gap-1">
<i data-lucide="alert-triangle" class="w-3 h-3"></i>
${ccwInstallStatus.missingFiles.length} ${t('status.filesMissing')}
</span>
</div>
<div class="cli-tool-actions flex flex-col gap-2 mt-3">
<div class="text-xs text-muted-foreground">
<p class="mb-1">${t('status.missingFiles')}:</p>
<ul class="list-disc list-inside text-xs opacity-70">
${ccwInstallStatus.missingFiles.slice(0, 3).map(f => `<li>${f}</li>`).join('')}
${ccwInstallStatus.missingFiles.length > 3 ? `<li>+${ccwInstallStatus.missingFiles.length - 3} more...</li>` : ''}
</ul>
</div>
<div class="bg-muted/50 rounded p-2 mt-2">
<p class="text-xs font-medium mb-1">${t('status.runToFix')}:</p>
<code class="text-xs bg-background px-2 py-1 rounded block">ccw install</code>
</div>
</div>
</div>
` : '';
// CLI Settings section
const settingsHtml = `
<div class="cli-settings-section">
@@ -392,6 +447,7 @@ function renderCliStatus() {
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
</div>
${ccwInstallHtml}
<div class="cli-tools-grid">
${toolsHtml}
${codexLensHtml}

View File

@@ -143,6 +143,12 @@ function initNavigation() {
} else {
console.error('renderCoreMemoryView not defined - please refresh the page');
}
} else if (currentView === 'codexlens-manager') {
if (typeof renderCodexLensManager === 'function') {
renderCodexLensManager();
} else {
console.error('renderCodexLensManager not defined - please refresh the page');
}
}
});
});
@@ -183,6 +189,8 @@ function updateContentTitle() {
titleEl.textContent = t('title.helpGuide');
} else if (currentView === 'core-memory') {
titleEl.textContent = t('title.coreMemory');
} else if (currentView === 'codexlens-manager') {
titleEl.textContent = t('title.codexLensManager');
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');

View File

@@ -41,6 +41,7 @@ const i18n = {
'nav.explorer': 'Explorer',
'nav.status': 'Status',
'nav.history': 'History',
'nav.codexLensManager': 'CodexLens',
'nav.memory': 'Memory',
'nav.contextMemory': 'Context',
'nav.coreMemory': 'Core Memory',
@@ -98,7 +99,8 @@ const i18n = {
'title.hookManager': 'Hook Manager',
'title.memoryModule': 'Memory Module',
'title.promptHistory': 'Prompt History',
'title.codexLensManager': 'CodexLens Manager',
// Search
'search.placeholder': 'Search...',
@@ -215,6 +217,7 @@ const i18n = {
'cli.default': 'Default',
'cli.install': 'Install',
'cli.uninstall': 'Uninstall',
'cli.openManager': 'Manager',
'cli.initIndex': 'Init Index',
'cli.geminiDesc': 'Google AI for code analysis',
'cli.qwenDesc': 'Alibaba AI assistant',
@@ -226,9 +229,11 @@ const i18n = {
// CodexLens Configuration
'codexlens.config': 'CodexLens Configuration',
'codexlens.configDesc': 'Manage code indexing, semantic search, and embedding models',
'codexlens.status': 'Status',
'codexlens.installed': 'Installed',
'codexlens.notInstalled': 'Not Installed',
'codexlens.installFirst': 'Install CodexLens to access semantic search and model management features',
'codexlens.indexes': 'Indexes',
'codexlens.currentWorkspace': 'Current Workspace',
'codexlens.indexStoragePath': 'Index Storage Path',
@@ -237,6 +242,8 @@ const i18n = {
'codexlens.newStoragePath': 'New Storage Path',
'codexlens.pathPlaceholder': 'e.g., /path/to/indexes or ~/.codexlens/indexes',
'codexlens.pathInfo': 'Supports ~ for home directory. Changes take effect immediately.',
'codexlens.pathUnchanged': 'Path unchanged',
'codexlens.pathEmpty': 'Path cannot be empty',
'codexlens.migrationRequired': 'Migration Required',
'codexlens.migrationWarning': 'After changing the path, existing indexes will need to be re-initialized for each workspace.',
'codexlens.actions': 'Actions',
@@ -244,6 +251,17 @@ const i18n = {
'codexlens.cleanCurrentWorkspace': 'Clean Current Workspace',
'codexlens.cleanAllIndexes': 'Clean All Indexes',
'codexlens.installCodexLens': 'Install CodexLens',
'codexlens.createIndex': 'Create Index',
'codexlens.embeddingModel': 'Embedding Model',
'codexlens.modelHint': 'Select embedding model for vector search (models with ✓ are installed)',
'codexlens.fullIndex': 'Full',
'codexlens.vectorIndex': 'Vector',
'codexlens.ftsIndex': 'FTS',
'codexlens.fullIndexDesc': 'FTS + Semantic search (recommended)',
'codexlens.vectorIndexDesc': 'Semantic search with embeddings only',
'codexlens.ftsIndexDesc': 'Fast full-text search only',
'codexlens.indexTypeHint': 'Full index includes FTS + semantic search. FTS only is faster but without AI-powered search.',
'codexlens.maintenance': 'Maintenance',
'codexlens.testSearch': 'Test Search',
'codexlens.testFunctionality': 'test CodexLens functionality',
'codexlens.textSearch': 'Text Search',
@@ -291,6 +309,17 @@ const i18n = {
'codexlens.cudaModeDesc': 'NVIDIA GPU (requires CUDA Toolkit)',
'common.recommended': 'Recommended',
'common.unavailable': 'Unavailable',
'common.auto': 'Auto',
// GPU Device Selection
'codexlens.selectGpuDevice': 'Select GPU Device',
'codexlens.discrete': 'Discrete',
'codexlens.integrated': 'Integrated',
'codexlens.selectingGpu': 'Selecting GPU...',
'codexlens.gpuSelected': 'GPU selected',
'codexlens.resettingGpu': 'Resetting GPU selection...',
'codexlens.gpuReset': 'GPU selection reset to auto',
'codexlens.resetToAuto': 'Reset to Auto',
'codexlens.modelManagement': 'Model Management',
'codexlens.loadingModels': 'Loading models...',
'codexlens.downloadModel': 'Download',
@@ -444,6 +473,19 @@ const i18n = {
'lang.windowsDisableSuccess': 'Windows platform guidelines disabled',
'lang.windowsEnableFailed': 'Failed to enable Windows platform guidelines',
'lang.windowsDisableFailed': 'Failed to disable Windows platform guidelines',
'lang.installRequired': 'Run "ccw install" to enable this feature',
// CCW Installation Status
'status.installed': 'Installed',
'status.incomplete': 'Incomplete',
'status.notInstalled': 'Not Installed',
'status.ccwInstall': 'CCW Workflows',
'status.ccwInstallDesc': 'Required workflow files for full functionality',
'status.required': 'Required',
'status.filesMissing': 'files missing',
'status.missingFiles': 'Missing files',
'status.runToFix': 'Run to fix',
'cli.promptFormat': 'Prompt Format',
'cli.promptFormatDesc': 'Format for multi-turn conversation concatenation',
'cli.storageBackend': 'Storage Backend',
@@ -1457,6 +1499,7 @@ const i18n = {
'nav.explorer': '文件浏览器',
'nav.status': '状态',
'nav.history': '历史',
'nav.codexLensManager': 'CodexLens',
'nav.memory': '记忆',
'nav.contextMemory': '活动',
'nav.coreMemory': '核心记忆',
@@ -1514,6 +1557,7 @@ const i18n = {
'title.hookManager': '钩子管理',
'title.memoryModule': '记忆模块',
'title.promptHistory': '提示历史',
'title.codexLensManager': 'CodexLens 管理',
// Search
'search.placeholder': '搜索...',
@@ -1631,6 +1675,7 @@ const i18n = {
'cli.default': '默认',
'cli.install': '安装',
'cli.uninstall': '卸载',
'cli.openManager': '管理',
'cli.initIndex': '初始化索引',
'cli.geminiDesc': 'Google AI 代码分析',
'cli.qwenDesc': '阿里通义 AI 助手',
@@ -1642,9 +1687,11 @@ const i18n = {
// CodexLens 配置
'codexlens.config': 'CodexLens 配置',
'codexlens.configDesc': '管理代码索引、语义搜索和嵌入模型',
'codexlens.status': '状态',
'codexlens.installed': '已安装',
'codexlens.notInstalled': '未安装',
'codexlens.installFirst': '安装 CodexLens 以访问语义搜索和模型管理功能',
'codexlens.indexes': '索引',
'codexlens.currentWorkspace': '当前工作区',
'codexlens.indexStoragePath': '索引存储路径',
@@ -1653,6 +1700,8 @@ const i18n = {
'codexlens.newStoragePath': '新存储路径',
'codexlens.pathPlaceholder': '例如:/path/to/indexes 或 ~/.codexlens/indexes',
'codexlens.pathInfo': '支持 ~ 表示用户目录。更改立即生效。',
'codexlens.pathUnchanged': '路径未变更',
'codexlens.pathEmpty': '路径不能为空',
'codexlens.migrationRequired': '需要迁移',
'codexlens.migrationWarning': '更改路径后,需要为每个工作区重新初始化索引。',
'codexlens.actions': '操作',
@@ -1660,6 +1709,17 @@ const i18n = {
'codexlens.cleanCurrentWorkspace': '清理当前工作空间',
'codexlens.cleanAllIndexes': '清理所有索引',
'codexlens.installCodexLens': '安装 CodexLens',
'codexlens.createIndex': '创建索引',
'codexlens.embeddingModel': '嵌入模型',
'codexlens.modelHint': '选择向量搜索的嵌入模型(带 ✓ 的已安装)',
'codexlens.fullIndex': '全部',
'codexlens.vectorIndex': '向量',
'codexlens.ftsIndex': 'FTS',
'codexlens.fullIndexDesc': 'FTS + 语义搜索(推荐)',
'codexlens.vectorIndexDesc': '仅语义嵌入搜索',
'codexlens.ftsIndexDesc': '仅快速全文搜索',
'codexlens.indexTypeHint': '完整索引包含 FTS + 语义搜索。仅 FTS 更快但无 AI 搜索功能。',
'codexlens.maintenance': '维护',
'codexlens.testSearch': '测试搜索',
'codexlens.testFunctionality': '测试 CodexLens 功能',
'codexlens.textSearch': '文本搜索',
@@ -1707,6 +1767,18 @@ const i18n = {
'codexlens.cudaModeDesc': 'NVIDIA GPU需要 CUDA Toolkit',
'common.recommended': '推荐',
'common.unavailable': '不可用',
'common.auto': '自动',
// GPU 设备选择
'codexlens.selectGpuDevice': '选择 GPU 设备',
'codexlens.discrete': '独立显卡',
'codexlens.integrated': '集成显卡',
'codexlens.selectingGpu': '选择 GPU 中...',
'codexlens.gpuSelected': 'GPU 已选择',
'codexlens.resettingGpu': '重置 GPU 选择中...',
'codexlens.gpuReset': 'GPU 选择已重置为自动',
'codexlens.resetToAuto': '重置为自动',
'codexlens.modelManagement': '模型管理',
'codexlens.loadingModels': '加载模型中...',
'codexlens.downloadModel': '下载',
@@ -1860,6 +1932,19 @@ const i18n = {
'lang.windowsDisableSuccess': 'Windows 平台规范已禁用',
'lang.windowsEnableFailed': '启用 Windows 平台规范失败',
'lang.windowsDisableFailed': '禁用 Windows 平台规范失败',
'lang.installRequired': '请运行 "ccw install" 以启用此功能',
// CCW 安装状态
'status.installed': '已安装',
'status.incomplete': '不完整',
'status.notInstalled': '未安装',
'status.ccwInstall': 'CCW 工作流',
'status.ccwInstallDesc': '完整功能所需的工作流文件',
'status.required': '必需',
'status.filesMissing': '个文件缺失',
'status.missingFiles': '缺失文件',
'status.runToFix': '修复命令',
'cli.promptFormat': '提示词格式',
'cli.promptFormatDesc': '多轮对话拼接格式',
'cli.storageBackend': '存储后端',

View File

@@ -9,6 +9,26 @@ var ccwEndpointTools = [];
var cliToolConfig = null; // Store loaded CLI config
var predefinedModels = {}; // Store predefined models per tool
// ========== Navigation Helpers ==========
/**
* Navigate to CodexLens Manager page
*/
function navigateToCodexLensManager() {
var navItem = document.querySelector('.nav-item[data-view="codexlens-manager"]');
if (navItem) {
navItem.click();
} else {
// Fallback: try to render directly
if (typeof renderCodexLensManager === 'function') {
currentView = 'codexlens-manager';
renderCodexLensManager();
} else {
showRefreshToast(t('common.error') + ': CodexLens Manager not available', 'error');
}
}
}
// ========== CCW Installations ==========
async function loadCcwInstallations() {
try {
@@ -314,8 +334,7 @@ async function renderCliManager() {
'<div class="cli-settings-section" id="cli-settings-section" style="margin-top: 1.5rem;"></div>' +
'<div class="cli-section" id="ccw-endpoint-tools-section" style="margin-top: 1.5rem;"></div>' +
'</div>' +
'<section id="storageCard" class="mb-6"></section>' +
'<section id="indexCard" class="mb-6"></section>';
'<section id="storageCard" class="mb-6"></section>';
// Render sub-panels
renderToolsSection();
@@ -329,11 +348,6 @@ async function renderCliManager() {
initStorageManager();
}
// Initialize index manager card
if (typeof initIndexManager === 'function') {
initIndexManager();
}
// Initialize Lucide icons
if (window.lucide) lucide.createIcons();
}
@@ -434,28 +448,22 @@ function renderToolsSection() {
'</div>';
}).join('');
// CodexLens item
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="showCodexLensConfigModal()">' +
// CodexLens item - simplified view with link to manager page
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="navigateToCodexLensManager()">' +
'<div class="tool-item-left">' +
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
'<div class="tool-item-info">' +
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span>' +
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i></div>' +
'<i data-lucide="external-link" class="w-3 h-3 tool-config-icon"></i></div>' +
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
'</div>' +
'</div>' +
'<div class="tool-item-right">' +
(codexLensStatus.ready
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
'<select id="codexlensModelSelect" class="btn-sm bg-muted border border-border rounded text-xs" onclick="event.stopPropagation()" title="' + (t('index.selectModel') || 'Select embedding model') + '">' +
buildModelSelectOptions() +
'</select>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); initCodexLensIndex(\'full\', getSelectedModel())" title="' + (t('index.fullDesc') || 'FTS + Semantic search (recommended)') + '"><i data-lucide="layers" class="w-3 h-3"></i> ' + (t('index.fullIndex') || '全部索引') + '</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'vector\', getSelectedModel())" title="' + (t('index.vectorDesc') || 'Semantic search with embeddings') + '"><i data-lucide="sparkles" class="w-3 h-3"></i> ' + (t('index.vectorIndex') || '向量索引') + '</button>' +
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex(\'normal\')" title="' + (t('index.normalDesc') || 'Fast full-text search only') + '"><i data-lucide="file-text" class="w-3 h-3"></i> ' + (t('index.normalIndex') || 'FTS索引') + '</button>' +
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()"><i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') + '</button>'
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>'
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); navigateToCodexLensManager()"><i data-lucide="settings" class="w-3 h-3"></i> ' + t('cli.openManager') + '</button>') +
'</div>' +
'</div>';
@@ -606,6 +614,16 @@ async function loadWindowsPlatformSettings() {
async function toggleChineseResponse(enabled) {
if (chineseResponseLoading) return;
// Pre-check: verify CCW workflows are installed (only when enabling)
if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) {
var missingFile = ccwInstallStatus.missingFiles.find(function(f) { return f === 'chinese-response.md'; });
if (missingFile) {
showRefreshToast(t('lang.installRequired'), 'warning');
return;
}
}
chineseResponseLoading = true;
try {
@@ -617,7 +635,14 @@ async function toggleChineseResponse(enabled) {
if (!response.ok) {
var errData = await response.json();
throw new Error(errData.error || 'Failed to update setting');
// Show specific error message from backend
var errorMsg = errData.error || 'Failed to update setting';
if (errorMsg.includes('not found')) {
showRefreshToast(t('lang.installRequired'), 'warning');
} else {
showRefreshToast((enabled ? t('lang.enableFailed') : t('lang.disableFailed')) + ': ' + errorMsg, 'error');
}
throw new Error(errorMsg);
}
var data = await response.json();
@@ -630,7 +655,7 @@ async function toggleChineseResponse(enabled) {
showRefreshToast(enabled ? t('lang.enableSuccess') : t('lang.disableSuccess'), 'success');
} catch (err) {
console.error('Failed to toggle Chinese response:', err);
showRefreshToast(enabled ? t('lang.enableFailed') : t('lang.disableFailed'), 'error');
// Error already shown in the !response.ok block
} finally {
chineseResponseLoading = false;
}
@@ -638,6 +663,16 @@ async function toggleChineseResponse(enabled) {
async function toggleWindowsPlatform(enabled) {
if (windowsPlatformLoading) return;
// Pre-check: verify CCW workflows are installed (only when enabling)
if (enabled && typeof ccwInstallStatus !== 'undefined' && !ccwInstallStatus.installed) {
var missingFile = ccwInstallStatus.missingFiles.find(function(f) { return f === 'windows-platform.md'; });
if (missingFile) {
showRefreshToast(t('lang.installRequired'), 'warning');
return;
}
}
windowsPlatformLoading = true;
try {
@@ -649,7 +684,14 @@ async function toggleWindowsPlatform(enabled) {
if (!response.ok) {
var errData = await response.json();
throw new Error(errData.error || 'Failed to update setting');
// Show specific error message from backend
var errorMsg = errData.error || 'Failed to update setting';
if (errorMsg.includes('not found')) {
showRefreshToast(t('lang.installRequired'), 'warning');
} else {
showRefreshToast((enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed')) + ': ' + errorMsg, 'error');
}
throw new Error(errorMsg);
}
var data = await response.json();
@@ -662,7 +704,7 @@ async function toggleWindowsPlatform(enabled) {
showRefreshToast(enabled ? t('lang.windowsEnableSuccess') : t('lang.windowsDisableSuccess'), 'success');
} catch (err) {
console.error('Failed to toggle Windows platform:', err);
showRefreshToast(enabled ? t('lang.windowsEnableFailed') : t('lang.windowsDisableFailed'), 'error');
// Error already shown in the !response.ok block
} finally {
windowsPlatformLoading = false;
}

View File

@@ -337,6 +337,8 @@ function initCodexLensConfigEvents(currentConfig) {
// Store detected GPU info
var detectedGpuInfo = null;
// Store available GPU devices
var availableGpuDevices = null;
/**
* Detect GPU support
@@ -363,11 +365,13 @@ async function loadSemanticDepsStatus() {
if (!container) return;
try {
// Detect GPU support in parallel
// Detect GPU support and load GPU devices in parallel
var gpuPromise = detectGpuSupport();
var gpuDevicesPromise = loadGpuDevices();
var response = await fetch('/api/codexlens/semantic/status');
var result = await response.json();
var gpuInfo = await gpuPromise;
var gpuDevices = await gpuDevicesPromise;
if (result.available) {
// Build accelerator badge
@@ -386,6 +390,9 @@ async function loadSemanticDepsStatus() {
acceleratorClass = 'bg-red-500/20 text-red-600';
}
// Build GPU device selector if multiple GPUs available
var gpuDeviceSelector = buildGpuDeviceSelector(gpuDevices);
container.innerHTML =
'<div class="space-y-2">' +
'<div class="flex items-center gap-2 text-sm">' +
@@ -402,6 +409,7 @@ async function loadSemanticDepsStatus() {
? '<span class="text-xs text-muted-foreground">' + result.providers.join(', ') + '</span>'
: '') +
'</div>' +
gpuDeviceSelector +
'</div>';
} else {
// Build GPU mode options
@@ -506,6 +514,134 @@ function getSelectedGpuMode() {
return selected ? selected.value : 'cpu';
}
/**
* Load available GPU devices
*/
async function loadGpuDevices() {
try {
var response = await fetch('/api/codexlens/gpu/list');
var result = await response.json();
if (result.success && result.result) {
availableGpuDevices = result.result;
return result.result;
}
} catch (err) {
console.error('GPU devices load failed:', err);
}
return { devices: [], selected_device_id: null };
}
/**
* Build GPU device selector HTML
*/
function buildGpuDeviceSelector(gpuDevices) {
if (!gpuDevices || !gpuDevices.devices || gpuDevices.devices.length === 0) {
return '';
}
// Only show selector if there are multiple GPUs
if (gpuDevices.devices.length < 2) {
return '';
}
var html =
'<div class="mt-3 p-3 bg-muted/30 rounded-lg border border-border">' +
'<div class="text-xs font-medium text-muted-foreground flex items-center gap-1 mb-2">' +
'<i data-lucide="cpu" class="w-3 h-3"></i>' +
(t('codexlens.selectGpuDevice') || 'Select GPU Device') +
'</div>' +
'<div class="space-y-1">';
gpuDevices.devices.forEach(function(device) {
var isSelected = device.is_selected;
var vendorIcon = device.vendor === 'nvidia' ? 'zap' : (device.vendor === 'amd' ? 'flame' : 'cpu');
var vendorColor = device.vendor === 'nvidia' ? 'text-green-500' : (device.vendor === 'amd' ? 'text-red-500' : 'text-blue-500');
var typeLabel = device.is_discrete ? (t('codexlens.discrete') || 'Discrete') : (t('codexlens.integrated') || 'Integrated');
html +=
'<label class="flex items-center gap-3 p-2 rounded border cursor-pointer hover:bg-muted/50 transition-colors ' +
(isSelected ? 'border-primary bg-primary/5' : 'border-transparent') + '">' +
'<input type="radio" name="gpuDevice" value="' + device.device_id + '" ' +
(isSelected ? 'checked' : '') +
' class="accent-primary" onchange="selectGpuDevice(' + device.device_id + ')">' +
'<div class="flex-1">' +
'<div class="flex items-center gap-2">' +
'<i data-lucide="' + vendorIcon + '" class="w-4 h-4 ' + vendorColor + '"></i>' +
'<span class="font-medium text-sm">' + device.name + '</span>' +
'</div>' +
'<div class="flex items-center gap-2 mt-0.5">' +
'<span class="text-xs text-muted-foreground">' + device.vendor.toUpperCase() + '</span>' +
'<span class="text-xs px-1.5 py-0.5 rounded ' +
(device.is_discrete ? 'bg-green-500/20 text-green-600' : 'bg-muted text-muted-foreground') + '">' +
typeLabel +
'</span>' +
(device.is_preferred ? '<span class="text-xs bg-primary/20 text-primary px-1.5 py-0.5 rounded">' + (t('common.auto') || 'Auto') + '</span>' : '') +
'</div>' +
'</div>' +
'</label>';
});
html +=
'</div>' +
'<button class="btn-xs text-muted-foreground hover:text-foreground mt-2" onclick="resetGpuDevice()">' +
'<i data-lucide="rotate-ccw" class="w-3 h-3"></i> ' + (t('codexlens.resetToAuto') || 'Reset to Auto') +
'</button>' +
'</div>';
return html;
}
/**
* Select a GPU device
*/
async function selectGpuDevice(deviceId) {
try {
showRefreshToast(t('codexlens.selectingGpu') || 'Selecting GPU...', 'info');
var response = await fetch('/api/codexlens/gpu/select', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: deviceId })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.gpuSelected') || 'GPU selected', 'success');
// Reload semantic status to reflect change
loadSemanticDepsStatus();
} else {
showRefreshToast(result.error || 'Failed to select GPU', 'error');
}
} catch (err) {
showRefreshToast(err.message, 'error');
}
}
/**
* Reset GPU device selection to auto
*/
async function resetGpuDevice() {
try {
showRefreshToast(t('codexlens.resettingGpu') || 'Resetting GPU selection...', 'info');
var response = await fetch('/api/codexlens/gpu/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.gpuReset') || 'GPU selection reset to auto', 'success');
// Reload semantic status to reflect change
loadSemanticDepsStatus();
} else {
showRefreshToast(result.error || 'Failed to reset GPU', 'error');
}
} catch (err) {
showRefreshToast(err.message, 'error');
}
}
/**
* Install semantic dependencies with GPU mode
*/
@@ -570,9 +706,7 @@ async function installSemanticDeps() {
function buildManualDownloadGuide() {
var modelData = [
{ profile: 'code', name: 'jinaai/jina-embeddings-v2-base-code', size: '~150 MB' },
{ profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' },
{ profile: 'balanced', name: 'mixedbread-ai/mxbai-embed-large-v1', size: '~600 MB' },
{ profile: 'multilingual', name: 'intfloat/multilingual-e5-large', size: '~1 GB' }
{ profile: 'fast', name: 'BAAI/bge-small-en-v1.5', size: '~80 MB' }
];
var html =
@@ -807,9 +941,7 @@ async function downloadModel(profile) {
// Get model info for size estimation
var modelSizes = {
'fast': { size: 80, time: '1-2' },
'code': { size: 150, time: '2-5' },
'multilingual': { size: 1000, time: '5-15' },
'balanced': { size: 600, time: '3-10' }
'code': { size: 150, time: '2-5' }
};
var modelInfo = modelSizes[profile] || { size: 100, time: '2-5' };
@@ -933,9 +1065,7 @@ async function downloadModel(profile) {
function showModelDownloadError(modelCard, profile, error, originalHTML) {
var modelNames = {
'fast': 'BAAI/bge-small-en-v1.5',
'code': 'jinaai/jina-embeddings-v2-base-code',
'multilingual': 'intfloat/multilingual-e5-large',
'balanced': 'mixedbread-ai/mxbai-embed-large-v1'
'code': 'jinaai/jina-embeddings-v2-base-code'
};
var modelName = modelNames[profile] || profile;
@@ -1035,7 +1165,7 @@ async function deleteModel(profile) {
/**
* Initialize CodexLens index with bottom floating progress bar
* @param {string} indexType - 'vector' (with embeddings), 'normal' (FTS only), or 'full' (FTS + Vector)
* @param {string} embeddingModel - Model profile: 'code', 'fast', 'multilingual', 'balanced'
* @param {string} embeddingModel - Model profile: 'code', 'fast'
*/
async function initCodexLensIndex(indexType, embeddingModel) {
indexType = indexType || 'vector';
@@ -1104,7 +1234,7 @@ async function initCodexLensIndex(indexType, embeddingModel) {
// Add model info for vector indexes
var modelLabel = '';
if (indexType !== 'normal') {
var modelNames = { code: 'Code', fast: 'Fast', multilingual: 'Multi', balanced: 'Balanced' };
var modelNames = { code: 'Code', fast: 'Fast' };
modelLabel = ' [' + (modelNames[embeddingModel] || embeddingModel) + ']';
}
@@ -1148,7 +1278,7 @@ async function initCodexLensIndex(indexType, embeddingModel) {
/**
* Start the indexing process
* @param {string} indexType - 'vector' or 'normal'
* @param {string} embeddingModel - Model profile: 'code', 'fast', 'multilingual', 'balanced'
* @param {string} embeddingModel - Model profile: 'code', 'fast'
*/
async function startCodexLensIndexing(indexType, embeddingModel) {
indexType = indexType || 'vector';
@@ -1727,3 +1857,546 @@ async function cleanCodexLensIndexes() {
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
}
}
// ============================================================
// CODEXLENS MANAGER PAGE (Independent View)
// ============================================================
/**
* Render CodexLens Manager as an independent page view
*/
async function renderCodexLensManager() {
var container = document.getElementById('mainContent');
if (!container) return;
// Hide stats grid and search
var statsGrid = document.getElementById('statsGrid');
var searchContainer = document.querySelector('.search-container');
if (statsGrid) statsGrid.style.display = 'none';
if (searchContainer) searchContainer.style.display = 'none';
container.innerHTML = '<div class="flex items-center justify-center py-12"><div class="animate-spin w-6 h-6 border-2 border-primary border-t-transparent rounded-full"></div><span class="ml-3">' + t('common.loading') + '</span></div>';
try {
// Load CodexLens status first to populate window.cliToolsStatus.codexlens
if (typeof loadCodexLensStatus === 'function') {
await loadCodexLensStatus();
}
var response = await fetch('/api/codexlens/config');
var config = await response.json();
container.innerHTML = buildCodexLensManagerPage(config);
if (window.lucide) lucide.createIcons();
initCodexLensManagerPageEvents(config);
loadSemanticDepsStatus();
loadModelList();
// Load index stats for the Index Manager section
if (window.cliToolsStatus?.codexlens?.installed) {
loadIndexStatsForPage();
}
} catch (err) {
container.innerHTML = '<div class="text-center py-12 text-destructive"><i data-lucide="alert-circle" class="w-8 h-8 mx-auto mb-2"></i><p>' + t('common.error') + ': ' + err.message + '</p></div>';
if (window.lucide) lucide.createIcons();
}
}
/**
* Build CodexLens Manager page content
*/
function buildCodexLensManagerPage(config) {
var indexDir = config.index_dir || '~/.codexlens/indexes';
var indexCount = config.index_count || 0;
var isInstalled = window.cliToolsStatus?.codexlens?.installed || false;
// Build model options for vector indexing
var modelOptions = buildModelSelectOptionsForPage();
return '<div class="codexlens-manager-page space-y-6">' +
// Header with status
'<div class="bg-card border border-border rounded-lg p-6">' +
'<div class="flex items-center justify-between flex-wrap gap-4">' +
'<div class="flex items-center gap-4">' +
'<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center">' +
'<i data-lucide="database" class="w-6 h-6 text-primary"></i>' +
'</div>' +
'<div>' +
'<h2 class="text-xl font-bold">' + t('codexlens.config') + '</h2>' +
'<p class="text-sm text-muted-foreground">' + t('codexlens.configDesc') + '</p>' +
'</div>' +
'</div>' +
'<div class="flex items-center gap-4">' +
(isInstalled
? '<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-success/10 text-success border border-success/20"><i data-lucide="check-circle" class="w-4 h-4"></i> ' + t('codexlens.installed') + '</span>'
: '<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-muted text-muted-foreground border border-border"><i data-lucide="circle" class="w-4 h-4"></i> ' + t('codexlens.notInstalled') + '</span>') +
'<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary/5 border border-primary/20">' +
'<span class="text-sm text-muted-foreground">' + t('codexlens.indexes') + ':</span>' +
'<span class="text-lg font-bold text-primary">' + indexCount + '</span>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
(isInstalled
? // Installed: Show full management UI
'<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">' +
// Left Column
'<div class="space-y-6">' +
// Create Index Section
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="layers" class="w-5 h-5 text-primary"></i> ' + t('codexlens.createIndex') + '</h4>' +
'<div class="space-y-4">' +
// Model selector
'<div>' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.embeddingModel') + '</label>' +
'<select id="pageModelSelect" class="w-full px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
modelOptions +
'</select>' +
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.modelHint') + '</p>' +
'</div>' +
// Index buttons - two modes: full (FTS + Vector) or FTS only
'<div class="grid grid-cols-2 gap-3">' +
'<button class="btn btn-primary flex items-center justify-center gap-2 py-3" onclick="initCodexLensIndexFromPage(\'full\')" title="' + t('codexlens.fullIndexDesc') + '">' +
'<i data-lucide="layers" class="w-4 h-4"></i>' +
'<span>' + t('codexlens.fullIndex') + '</span>' +
'</button>' +
'<button class="btn btn-outline flex items-center justify-center gap-2 py-3" onclick="initCodexLensIndexFromPage(\'normal\')" title="' + t('codexlens.ftsIndexDesc') + '">' +
'<i data-lucide="file-text" class="w-4 h-4"></i>' +
'<span>' + t('codexlens.ftsIndex') + '</span>' +
'</button>' +
'</div>' +
'<p class="text-xs text-muted-foreground">' + t('codexlens.indexTypeHint') + '</p>' +
'</div>' +
'</div>' +
// Storage Path Section
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="folder" class="w-5 h-5 text-primary"></i> ' + t('codexlens.indexStoragePath') + '</h4>' +
'<div class="space-y-3">' +
'<div>' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.currentPath') + '</label>' +
'<div class="text-sm text-muted-foreground bg-muted/50 rounded-lg px-3 py-2 font-mono border border-border truncate" title="' + indexDir + '">' + indexDir + '</div>' +
'</div>' +
'<div>' +
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.newStoragePath') + '</label>' +
'<div class="flex gap-2">' +
'<input type="text" id="indexDirInput" value="' + indexDir + '" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-foreground text-sm" />' +
'<button class="btn-sm btn-primary" id="saveIndexPathBtn"><i data-lucide="save" class="w-3.5 h-3.5"></i></button>' +
'</div>' +
'<p class="text-xs text-muted-foreground mt-1">' + t('codexlens.pathInfo') + '</p>' +
'</div>' +
'</div>' +
'</div>' +
// Maintenance Section
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="settings" class="w-5 h-5 text-primary"></i> ' + t('codexlens.maintenance') + '</h4>' +
'<div class="flex flex-wrap gap-2">' +
'<button class="btn-sm btn-outline" onclick="cleanCurrentWorkspaceIndex()"><i data-lucide="folder-x" class="w-3.5 h-3.5"></i> ' + t('codexlens.cleanCurrentWorkspace') + '</button>' +
'<button class="btn-sm btn-outline" onclick="cleanCodexLensIndexes()"><i data-lucide="trash" class="w-3.5 h-3.5"></i> ' + t('codexlens.cleanAllIndexes') + '</button>' +
'<button class="btn-sm btn-destructive" onclick="uninstallCodexLensFromManager()"><i data-lucide="trash-2" class="w-3.5 h-3.5"></i> ' + t('cli.uninstall') + '</button>' +
'</div>' +
'</div>' +
'</div>' +
// Right Column
'<div class="space-y-6">' +
// Semantic Dependencies
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="cpu" class="w-5 h-5 text-primary"></i> ' + t('codexlens.semanticDeps') + '</h4>' +
'<div id="semanticDepsStatus" class="space-y-3">' +
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
'<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div> ' + t('codexlens.checkingDeps') +
'</div>' +
'</div>' +
'</div>' +
// Model Management
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="box" class="w-5 h-5 text-primary"></i> ' + t('codexlens.modelManagement') + '</h4>' +
'<div id="modelListContainer" class="space-y-3">' +
'<div class="flex items-center gap-2 text-sm text-muted-foreground">' +
'<div class="animate-spin w-4 h-4 border-2 border-primary border-t-transparent rounded-full"></div> ' + t('codexlens.loadingModels') +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
// Index Manager Section
'<div class="bg-card border border-border rounded-lg overflow-hidden" id="indexManagerSection">' +
'<div class="bg-muted/30 border-b border-border px-4 py-3 flex items-center justify-between">' +
'<div class="flex items-center gap-2">' +
'<i data-lucide="database" class="w-4 h-4 text-primary"></i>' +
'<span class="font-medium text-foreground">' + t('index.manager') + '</span>' +
'<span class="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground" id="indexTotalSize">-</span>' +
'</div>' +
'<div class="flex items-center gap-2">' +
'<button onclick="loadIndexStatsForPage()" class="text-xs px-2 py-1 text-muted-foreground hover:text-foreground hover:bg-muted rounded transition-colors" title="' + t('common.refresh') + '">' +
'<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>' +
'</button>' +
'</div>' +
'</div>' +
'<div class="p-4">' +
'<div class="flex items-center gap-2 mb-3 text-xs text-muted-foreground">' +
'<i data-lucide="folder" class="w-3.5 h-3.5"></i>' +
'<span class="font-mono truncate" id="indexDirDisplay" title="' + indexDir + '">' + indexDir + '</span>' +
'</div>' +
'<div class="grid grid-cols-4 gap-3 mb-4">' +
'<div class="bg-muted/30 rounded-lg p-3 text-center">' +
'<div class="text-lg font-semibold text-foreground" id="indexProjectCount">-</div>' +
'<div class="text-xs text-muted-foreground">' + t('index.projects') + '</div>' +
'</div>' +
'<div class="bg-muted/30 rounded-lg p-3 text-center">' +
'<div class="text-lg font-semibold text-foreground" id="indexTotalSizeVal">-</div>' +
'<div class="text-xs text-muted-foreground">' + t('index.totalSize') + '</div>' +
'</div>' +
'<div class="bg-muted/30 rounded-lg p-3 text-center">' +
'<div class="text-lg font-semibold text-foreground" id="indexVectorCount">-</div>' +
'<div class="text-xs text-muted-foreground">' + t('index.vectorIndexes') + '</div>' +
'</div>' +
'<div class="bg-muted/30 rounded-lg p-3 text-center">' +
'<div class="text-lg font-semibold text-foreground" id="indexFtsCount">-</div>' +
'<div class="text-xs text-muted-foreground">' + t('index.ftsIndexes') + '</div>' +
'</div>' +
'</div>' +
'<div class="border border-border rounded-lg overflow-hidden">' +
'<table class="w-full text-sm">' +
'<thead class="bg-muted/50">' +
'<tr class="text-xs text-muted-foreground">' +
'<th class="py-2 px-2 text-left font-medium">' + t('index.projectId') + '</th>' +
'<th class="py-2 px-2 text-right font-medium">' + t('index.size') + '</th>' +
'<th class="py-2 px-2 text-center font-medium">' + t('index.type') + '</th>' +
'<th class="py-2 px-2 text-right font-medium">' + t('index.lastModified') + '</th>' +
'<th class="py-2 px-1 w-8"></th>' +
'</tr>' +
'</thead>' +
'<tbody id="indexTableBody">' +
'<tr><td colspan="5" class="py-4 text-center text-muted-foreground text-sm">' + t('common.loading') + '</td></tr>' +
'</tbody>' +
'</table>' +
'</div>' +
'<div class="mt-4 flex justify-end">' +
'<button onclick="cleanAllIndexesFromPage()" class="text-xs px-3 py-1.5 bg-destructive/10 text-destructive hover:bg-destructive/20 rounded transition-colors flex items-center gap-1.5">' +
'<i data-lucide="trash" class="w-3.5 h-3.5"></i>' +
t('index.cleanAll') +
'</button>' +
'</div>' +
'</div>' +
'</div>' +
// Test Search Section
'<div class="bg-card border border-border rounded-lg p-5">' +
'<h4 class="text-lg font-semibold mb-4 flex items-center gap-2"><i data-lucide="search" class="w-5 h-5 text-primary"></i> ' + t('codexlens.testSearch') + '</h4>' +
'<div class="space-y-4">' +
'<div class="flex gap-3">' +
'<select id="searchTypeSelect" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
'<option value="search">' + t('codexlens.textSearch') + '</option>' +
'<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
'<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
'</select>' +
'<select id="searchModeSelect" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm">' +
'<option value="exact">' + t('codexlens.exactMode') + '</option>' +
'<option value="fuzzy">' + t('codexlens.fuzzyMode') + '</option>' +
'<option value="hybrid">' + t('codexlens.hybridMode') + '</option>' +
'<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
'</select>' +
'</div>' +
'<div class="flex gap-3">' +
'<input type="text" id="searchQueryInput" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm" placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
'<button class="btn-sm btn-primary" id="runSearchBtn"><i data-lucide="search" class="w-3.5 h-3.5"></i> ' + t('codexlens.runSearch') + '</button>' +
'</div>' +
'<div id="searchResults" class="hidden">' +
'<div class="flex items-center justify-between mb-2">' +
'<span class="text-sm font-medium">' + t('codexlens.results') + ':</span>' +
'<span id="searchResultCount" class="text-xs text-muted-foreground"></span>' +
'</div>' +
'<pre id="searchResultContent" class="bg-muted/50 border border-border p-3 rounded-lg text-xs overflow-auto max-h-64 font-mono"></pre>' +
'</div>' +
'</div>' +
'</div>'
: // Not installed: Show install prompt
'<div class="bg-card border border-border rounded-lg p-8">' +
'<div class="text-center max-w-md mx-auto">' +
'<div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4">' +
'<i data-lucide="database" class="w-8 h-8 text-primary"></i>' +
'</div>' +
'<h3 class="text-lg font-semibold mb-2">' + t('codexlens.installCodexLens') + '</h3>' +
'<p class="text-sm text-muted-foreground mb-6">' + t('codexlens.installFirst') + '</p>' +
'<button class="btn btn-primary" onclick="installCodexLensFromManager()">' +
'<i data-lucide="download" class="w-4 h-4"></i> ' + t('codexlens.installCodexLens') +
'</button>' +
'</div>' +
'</div>'
) +
'</div>';
}
/**
* Build model select options for the page
*/
function buildModelSelectOptionsForPage() {
var installedModels = window.cliToolsStatus?.codexlens?.installedModels || [];
var allModels = window.cliToolsStatus?.codexlens?.allModels || [];
if (allModels.length === 0) {
// Fallback to default models if not loaded
return '<option value="code">code (default)</option>' +
'<option value="fast">fast</option>';
}
var options = '';
allModels.forEach(function(model) {
var isInstalled = model.installed || installedModels.includes(model.profile);
var label = model.profile + (isInstalled ? ' ✓' : '');
var selected = model.profile === 'code' ? ' selected' : '';
options += '<option value="' + model.profile + '"' + selected + '>' + label + '</option>';
});
return options;
}
/**
* Initialize index from page with selected model
*/
function initCodexLensIndexFromPage(indexType) {
var modelSelect = document.getElementById('pageModelSelect');
var selectedModel = modelSelect ? modelSelect.value : 'code';
// For FTS-only index, model is not needed
if (indexType === 'normal') {
initCodexLensIndex(indexType);
} else {
initCodexLensIndex(indexType, selectedModel);
}
}
/**
* Initialize CodexLens Manager page event handlers
*/
function initCodexLensManagerPageEvents(currentConfig) {
var saveBtn = document.getElementById('saveIndexPathBtn');
if (saveBtn) {
saveBtn.onclick = async function() {
var indexDirInput = document.getElementById('indexDirInput');
var newIndexDir = indexDirInput ? indexDirInput.value.trim() : '';
if (!newIndexDir) { showRefreshToast(t('codexlens.pathEmpty'), 'error'); return; }
if (newIndexDir === currentConfig.index_dir) { showRefreshToast(t('codexlens.pathUnchanged'), 'info'); return; }
saveBtn.disabled = true;
saveBtn.innerHTML = '<span class="animate-pulse">' + t('common.saving') + '</span>';
try {
var response = await fetch('/api/codexlens/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ index_dir: newIndexDir }) });
var result = await response.json();
if (result.success) { showRefreshToast(t('codexlens.configSaved'), 'success'); renderCodexLensManager(); }
else { showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error'); }
} catch (err) { showRefreshToast(t('common.error') + ': ' + err.message, 'error'); }
saveBtn.disabled = false;
saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
if (window.lucide) lucide.createIcons();
};
}
var runSearchBtn = document.getElementById('runSearchBtn');
if (runSearchBtn) {
runSearchBtn.onclick = async function() {
var searchType = document.getElementById('searchTypeSelect').value;
var searchMode = document.getElementById('searchModeSelect').value;
var query = document.getElementById('searchQueryInput').value.trim();
var resultsDiv = document.getElementById('searchResults');
var resultCount = document.getElementById('searchResultCount');
var resultContent = document.getElementById('searchResultContent');
if (!query) { showRefreshToast(t('codexlens.enterQuery'), 'warning'); return; }
runSearchBtn.disabled = true;
runSearchBtn.innerHTML = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
resultsDiv.classList.add('hidden');
try {
var endpoint = '/api/codexlens/' + searchType;
var params = new URLSearchParams({ query: query, limit: '20' });
if (searchType === 'search' || searchType === 'search_files') { params.append('mode', searchMode); }
var response = await fetch(endpoint + '?' + params.toString());
var result = await response.json();
if (result.success) {
var results = result.results || result.files || [];
resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount');
resultContent.textContent = JSON.stringify(results, null, 2);
resultsDiv.classList.remove('hidden');
} else {
resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError'));
resultsDiv.classList.remove('hidden');
}
} catch (err) {
resultContent.textContent = t('common.exception') + ': ' + err.message;
resultsDiv.classList.remove('hidden');
}
runSearchBtn.disabled = false;
runSearchBtn.innerHTML = '<i data-lucide="search" class="w-3.5 h-3.5"></i> ' + t('codexlens.runSearch');
if (window.lucide) lucide.createIcons();
};
}
var searchInput = document.getElementById('searchQueryInput');
if (searchInput) { searchInput.onkeypress = function(e) { if (e.key === 'Enter' && runSearchBtn) { runSearchBtn.click(); } }; }
}
/**
* Show index initialization modal
*/
function showIndexInitModal() {
// Use initCodexLensIndex with default settings
initCodexLensIndex('vector', 'code');
}
/**
* Load index stats for the CodexLens Manager page
*/
async function loadIndexStatsForPage() {
try {
var response = await fetch('/api/codexlens/indexes');
if (!response.ok) throw new Error('Failed to load index stats');
var data = await response.json();
renderIndexStatsForPage(data);
} catch (err) {
console.error('[CodexLens] Failed to load index stats:', err);
var tbody = document.getElementById('indexTableBody');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-destructive text-sm">' + err.message + '</td></tr>';
}
}
}
/**
* Render index stats in the CodexLens Manager page
*/
function renderIndexStatsForPage(data) {
var summary = data.summary || {};
var indexes = data.indexes || [];
var indexDir = data.indexDir || '';
// Update summary stats
var totalSizeEl = document.getElementById('indexTotalSize');
var projectCountEl = document.getElementById('indexProjectCount');
var totalSizeValEl = document.getElementById('indexTotalSizeVal');
var vectorCountEl = document.getElementById('indexVectorCount');
var ftsCountEl = document.getElementById('indexFtsCount');
var indexDirEl = document.getElementById('indexDirDisplay');
if (totalSizeEl) totalSizeEl.textContent = summary.totalSizeFormatted || '0 B';
if (projectCountEl) projectCountEl.textContent = summary.totalProjects || 0;
if (totalSizeValEl) totalSizeValEl.textContent = summary.totalSizeFormatted || '0 B';
if (vectorCountEl) vectorCountEl.textContent = summary.vectorIndexCount || 0;
if (ftsCountEl) ftsCountEl.textContent = summary.normalIndexCount || 0;
if (indexDirEl && indexDir) {
indexDirEl.textContent = indexDir;
indexDirEl.title = indexDir;
}
// Render table rows
var tbody = document.getElementById('indexTableBody');
if (!tbody) return;
if (indexes.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-muted-foreground text-sm">' + (t('index.noIndexes') || 'No indexes yet') + '</td></tr>';
return;
}
var rows = '';
indexes.forEach(function(idx) {
var vectorBadge = idx.hasVectorIndex
? '<span class="text-xs px-1.5 py-0.5 bg-primary/10 text-primary rounded">' + (t('index.vector') || 'Vector') + '</span>'
: '';
var normalBadge = idx.hasNormalIndex
? '<span class="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">' + (t('index.fts') || 'FTS') + '</span>'
: '';
rows += '<tr class="border-t border-border hover:bg-muted/30 transition-colors">' +
'<td class="py-2 px-2 text-foreground">' +
'<span class="font-mono text-xs truncate max-w-[250px] inline-block" title="' + escapeHtml(idx.id) + '">' + escapeHtml(idx.id) + '</span>' +
'</td>' +
'<td class="py-2 px-2 text-right text-muted-foreground">' + (idx.sizeFormatted || '-') + '</td>' +
'<td class="py-2 px-2 text-center"><div class="flex items-center justify-center gap-1">' + vectorBadge + normalBadge + '</div></td>' +
'<td class="py-2 px-2 text-right text-muted-foreground">' + formatTimeAgoSimple(idx.lastModified) + '</td>' +
'<td class="py-2 px-1 text-center">' +
'<button onclick="cleanIndexProjectFromPage(\'' + escapeHtml(idx.id) + '\')" ' +
'class="text-destructive/70 hover:text-destructive p-1 rounded hover:bg-destructive/10 transition-colors" ' +
'title="' + (t('index.cleanProject') || 'Clean Index') + '">' +
'<i data-lucide="trash-2" class="w-3.5 h-3.5"></i>' +
'</button>' +
'</td>' +
'</tr>';
});
tbody.innerHTML = rows;
if (window.lucide) lucide.createIcons();
}
/**
* Simple time ago formatter
*/
function formatTimeAgoSimple(isoString) {
if (!isoString) return t('common.never') || 'Never';
var date = new Date(isoString);
var now = new Date();
var diffMs = now - date;
var diffMins = Math.floor(diffMs / 60000);
var diffHours = Math.floor(diffMins / 60);
var diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return t('common.justNow') || 'Just now';
if (diffMins < 60) return diffMins + 'm ' + (t('common.ago') || 'ago');
if (diffHours < 24) return diffHours + 'h ' + (t('common.ago') || 'ago');
if (diffDays < 30) return diffDays + 'd ' + (t('common.ago') || 'ago');
return date.toLocaleDateString();
}
/**
* Clean a specific project's index from the page
*/
async function cleanIndexProjectFromPage(projectId) {
if (!confirm((t('index.cleanProjectConfirm') || 'Clean index for') + ' ' + projectId + '?')) {
return;
}
try {
showRefreshToast(t('index.cleaning') || 'Cleaning index...', 'info');
var response = await fetch('/api/codexlens/clean', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: projectId })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('index.cleanSuccess') || 'Index cleaned successfully', 'success');
await loadIndexStatsForPage();
} else {
showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
}
}
/**
* Clean all indexes from the page
*/
async function cleanAllIndexesFromPage() {
if (!confirm(t('index.cleanAllConfirm') || 'Are you sure you want to clean ALL indexes? This cannot be undone.')) {
return;
}
try {
showRefreshToast(t('index.cleaning') || 'Cleaning indexes...', 'info');
var response = await fetch('/api/codexlens/clean', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ all: true })
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('index.cleanAllSuccess') || 'All indexes cleaned', 'success');
await loadIndexStatsForPage();
} else {
showRefreshToast((t('index.cleanFailed') || 'Clean failed') + ': ' + result.error, 'error');
}
} catch (err) {
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
}
}

View File

@@ -331,6 +331,11 @@
<i data-lucide="history" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.history">History</span>
</li>
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="codexlens-manager" data-tooltip="CodexLens Manager">
<i data-lucide="search-code" class="nav-icon"></i>
<span class="nav-text flex-1" data-i18n="nav.codexLensManager">CodexLens</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeCodexLens">-</span>
</li>
<!-- Hidden: Code Graph Explorer (feature disabled)
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="graph-explorer" data-tooltip="Code Graph Explorer">
<i data-lucide="git-branch" class="nav-icon"></i>

View File

@@ -421,6 +421,17 @@ async function installSemantic(gpuMode: GpuMode = 'cpu'): Promise<BootstrapResul
child.on('close', (code) => {
if (code === 0) {
// IMPORTANT: fastembed installs onnxruntime (CPU) as dependency, which conflicts
// with onnxruntime-directml/gpu. Reinstall the GPU version to ensure it takes precedence.
if (gpuMode !== 'cpu') {
try {
console.log(`[CodexLens] Reinstalling ${onnxPackage} to ensure GPU provider works...`);
execSync(`"${pipPath}" install --force-reinstall ${onnxPackage}`, { stdio: 'pipe', timeout: 300000 });
console.log(`[CodexLens] ${onnxPackage} reinstalled successfully`);
} catch (e) {
console.warn(`[CodexLens] Warning: Failed to reinstall ${onnxPackage}: ${(e as Error).message}`);
}
}
console.log(`[CodexLens] Semantic dependencies installed successfully (${gpuMode} mode)`);
resolve({ success: true, message: `Installed with ${modeDescription}` });
} else {