From 81f4d084b01e57fcd9077088b4f976000bf1fc3b Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 4 Jan 2026 21:04:28 +0800 Subject: [PATCH] feat: add navigation status routes and update badge aggregation logic --- ccw/src/core/routes/codexlens-routes.ts | 102 +++++++- ccw/src/core/routes/nav-status-routes.ts | 240 ++++++++++++++++++ ccw/src/core/server.ts | 6 + ccw/src/templates/dashboard-js/api.js | 5 + .../dashboard-js/components/navigation.js | 45 ++++ ccw/src/templates/dashboard-js/main.js | 5 + .../dashboard-js/views/codexlens-manager.js | 184 ++++++++------ 7 files changed, 506 insertions(+), 81 deletions(-) create mode 100644 ccw/src/core/routes/nav-status-routes.ts diff --git a/ccw/src/core/routes/codexlens-routes.ts b/ccw/src/core/routes/codexlens-routes.ts index 8691b122..b59868c4 100644 --- a/ccw/src/core/routes/codexlens-routes.ts +++ b/ccw/src/core/routes/codexlens-routes.ts @@ -1785,18 +1785,65 @@ except Exception as e: // Map settings to env var format for defaults const settingsDefaults: Record = {}; + + // Embedding settings if (settings.embedding?.backend) { settingsDefaults['CODEXLENS_EMBEDDING_BACKEND'] = settings.embedding.backend; } if (settings.embedding?.model) { settingsDefaults['CODEXLENS_EMBEDDING_MODEL'] = settings.embedding.model; + settingsDefaults['LITELLM_EMBEDDING_MODEL'] = settings.embedding.model; } + if (settings.embedding?.use_gpu !== undefined) { + settingsDefaults['CODEXLENS_USE_GPU'] = String(settings.embedding.use_gpu); + } + if (settings.embedding?.strategy) { + settingsDefaults['CODEXLENS_EMBEDDING_STRATEGY'] = settings.embedding.strategy; + } + if (settings.embedding?.cooldown !== undefined) { + settingsDefaults['CODEXLENS_EMBEDDING_COOLDOWN'] = String(settings.embedding.cooldown); + } + + // Reranker settings if (settings.reranker?.backend) { - // Map 'api' to 'litellm' for UI consistency - settingsDefaults['CODEXLENS_RERANKER_BACKEND'] = settings.reranker.backend === 'api' ? 'litellm' : settings.reranker.backend; + settingsDefaults['CODEXLENS_RERANKER_BACKEND'] = settings.reranker.backend; } if (settings.reranker?.model) { settingsDefaults['CODEXLENS_RERANKER_MODEL'] = settings.reranker.model; + settingsDefaults['LITELLM_RERANKER_MODEL'] = settings.reranker.model; + } + if (settings.reranker?.enabled !== undefined) { + settingsDefaults['CODEXLENS_RERANKER_ENABLED'] = String(settings.reranker.enabled); + } + if (settings.reranker?.top_k !== undefined) { + settingsDefaults['CODEXLENS_RERANKER_TOP_K'] = String(settings.reranker.top_k); + } + + // API/Concurrency settings + if (settings.api?.max_workers !== undefined) { + settingsDefaults['CODEXLENS_API_MAX_WORKERS'] = String(settings.api.max_workers); + } + if (settings.api?.batch_size !== undefined) { + settingsDefaults['CODEXLENS_API_BATCH_SIZE'] = String(settings.api.batch_size); + } + + // Cascade search settings + if (settings.cascade?.strategy) { + settingsDefaults['CODEXLENS_CASCADE_STRATEGY'] = settings.cascade.strategy; + } + if (settings.cascade?.coarse_k !== undefined) { + settingsDefaults['CODEXLENS_CASCADE_COARSE_K'] = String(settings.cascade.coarse_k); + } + if (settings.cascade?.fine_k !== undefined) { + settingsDefaults['CODEXLENS_CASCADE_FINE_K'] = String(settings.cascade.fine_k); + } + + // LLM settings + if (settings.llm?.enabled !== undefined) { + settingsDefaults['CODEXLENS_LLM_ENABLED'] = String(settings.llm.enabled); + } + if (settings.llm?.batch_size !== undefined) { + settingsDefaults['CODEXLENS_LLM_BATCH_SIZE'] = String(settings.llm.batch_size); } res.writeHead(200, { 'Content-Type': 'application/json' }); @@ -1956,10 +2003,57 @@ except Exception as e: await writeFile(envPath, lines.join('\n'), 'utf-8'); + // Also update settings.json with mapped values + const settingsPath = join(homedir(), '.codexlens', 'settings.json'); + let settings: Record = {}; + try { + const settingsContent = await readFile(settingsPath, 'utf-8'); + settings = JSON.parse(settingsContent); + } catch (e) { + // File doesn't exist, create default structure + settings = { embedding: {}, reranker: {}, api: {}, cascade: {}, llm: {} }; + } + + // Map env vars to settings.json structure + const envToSettings: Record any }> = { + 'CODEXLENS_EMBEDDING_BACKEND': { path: ['embedding', 'backend'] }, + 'CODEXLENS_EMBEDDING_MODEL': { path: ['embedding', 'model'] }, + 'CODEXLENS_USE_GPU': { path: ['embedding', 'use_gpu'], transform: v => v === 'true' }, + 'CODEXLENS_EMBEDDING_STRATEGY': { path: ['embedding', 'strategy'] }, + 'CODEXLENS_EMBEDDING_COOLDOWN': { path: ['embedding', 'cooldown'], transform: v => parseFloat(v) }, + 'CODEXLENS_RERANKER_BACKEND': { path: ['reranker', 'backend'] }, + 'CODEXLENS_RERANKER_MODEL': { path: ['reranker', 'model'] }, + 'CODEXLENS_RERANKER_ENABLED': { path: ['reranker', 'enabled'], transform: v => v === 'true' }, + 'CODEXLENS_RERANKER_TOP_K': { path: ['reranker', 'top_k'], transform: v => parseInt(v, 10) }, + 'CODEXLENS_API_MAX_WORKERS': { path: ['api', 'max_workers'], transform: v => parseInt(v, 10) }, + 'CODEXLENS_API_BATCH_SIZE': { path: ['api', 'batch_size'], transform: v => parseInt(v, 10) }, + 'CODEXLENS_CASCADE_STRATEGY': { path: ['cascade', 'strategy'] }, + 'CODEXLENS_CASCADE_COARSE_K': { path: ['cascade', 'coarse_k'], transform: v => parseInt(v, 10) }, + 'CODEXLENS_CASCADE_FINE_K': { path: ['cascade', 'fine_k'], transform: v => parseInt(v, 10) }, + 'CODEXLENS_LLM_ENABLED': { path: ['llm', 'enabled'], transform: v => v === 'true' }, + 'CODEXLENS_LLM_BATCH_SIZE': { path: ['llm', 'batch_size'], transform: v => parseInt(v, 10) }, + 'LITELLM_EMBEDDING_MODEL': { path: ['embedding', 'model'] }, + 'LITELLM_RERANKER_MODEL': { path: ['reranker', 'model'] } + }; + + // Apply env vars to settings + for (const [envKey, value] of Object.entries(env)) { + const mapping = envToSettings[envKey]; + if (mapping && value) { + const [section, key] = mapping.path; + if (!settings[section]) settings[section] = {}; + settings[section][key] = mapping.transform ? mapping.transform(value) : value; + } + } + + // Write updated settings + await writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8'); + return { success: true, - message: 'Environment configuration saved', - path: envPath + message: 'Environment and settings configuration saved', + path: envPath, + settingsPath }; } catch (err) { return { success: false, error: err.message, status: 500 }; diff --git a/ccw/src/core/routes/nav-status-routes.ts b/ccw/src/core/routes/nav-status-routes.ts new file mode 100644 index 00000000..cb6bd3a7 --- /dev/null +++ b/ccw/src/core/routes/nav-status-routes.ts @@ -0,0 +1,240 @@ +// @ts-nocheck +/** + * Navigation Status Routes Module + * Aggregated status endpoint for navigation bar badge updates + * + * API Endpoints: + * - GET /api/nav-status - Get aggregated navigation bar status (counts for all badges) + */ +import type { IncomingMessage, ServerResponse } from 'http'; +import { existsSync, readFileSync, readdirSync } from 'fs'; +import { join } from 'path'; +import { homedir } from 'os'; + +export interface RouteContext { + pathname: string; + url: URL; + req: IncomingMessage; + res: ServerResponse; + initialPath: string; +} + +// ========== Count Helper Functions ========== + +/** + * Count issues from JSONL file + */ +function countIssues(projectPath: string): number { + const issuesPath = join(projectPath, '.workflow', 'issues', 'issues.jsonl'); + if (!existsSync(issuesPath)) return 0; + try { + const content = readFileSync(issuesPath, 'utf8'); + return content.split('\n').filter(line => line.trim()).length; + } catch { + return 0; + } +} + +/** + * Count discoveries from index or directory scan + */ +function countDiscoveries(projectPath: string): number { + const discoveriesDir = join(projectPath, '.workflow', 'issues', 'discoveries'); + const indexPath = join(discoveriesDir, 'index.json'); + + // Try index.json first + if (existsSync(indexPath)) { + try { + const index = JSON.parse(readFileSync(indexPath, 'utf8')); + return index.discoveries?.length || 0; + } catch { /* fall through */ } + } + + // Fallback: scan directory + if (!existsSync(discoveriesDir)) return 0; + try { + const entries = readdirSync(discoveriesDir, { withFileTypes: true }); + return entries.filter(e => e.isDirectory() && e.name.startsWith('DSC-')).length; + } catch { + return 0; + } +} + +/** + * Count skills from project and user directories + */ +function countSkills(projectPath: string): { project: number; user: number; total: number } { + let project = 0, user = 0; + + // Project skills + const projectSkillsDir = join(projectPath, '.claude', 'skills'); + if (existsSync(projectSkillsDir)) { + try { + const entries = readdirSync(projectSkillsDir, { withFileTypes: true }); + project = entries.filter(e => + e.isDirectory() && existsSync(join(projectSkillsDir, e.name, 'SKILL.md')) + ).length; + } catch { /* ignore */ } + } + + // User skills + const userSkillsDir = join(homedir(), '.claude', 'skills'); + if (existsSync(userSkillsDir)) { + try { + const entries = readdirSync(userSkillsDir, { withFileTypes: true }); + user = entries.filter(e => + e.isDirectory() && existsSync(join(userSkillsDir, e.name, 'SKILL.md')) + ).length; + } catch { /* ignore */ } + } + + return { project, user, total: project + user }; +} + +/** + * Recursively count rules in a directory + */ +function countRulesInDir(dirPath: string): number { + if (!existsSync(dirPath)) return 0; + let count = 0; + try { + const entries = readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile() && entry.name.endsWith('.md')) { + count++; + } else if (entry.isDirectory()) { + count += countRulesInDir(join(dirPath, entry.name)); + } + } + } catch { /* ignore */ } + return count; +} + +/** + * Count rules from project and user directories + */ +function countRules(projectPath: string): { project: number; user: number; total: number } { + const project = countRulesInDir(join(projectPath, '.claude', 'rules')); + const user = countRulesInDir(join(homedir(), '.claude', 'rules')); + return { project, user, total: project + user }; +} + +/** + * Count CLAUDE.md files + */ +function countClaudeFiles(projectPath: string): number { + let count = 0; + const EXCLUDES = ['.git', 'node_modules', 'dist', 'build', '.venv', 'venv', '__pycache__', 'coverage', '.workflow']; + + // User main + if (existsSync(join(homedir(), '.claude', 'CLAUDE.md'))) count++; + + // Project main + if (existsSync(join(projectPath, '.claude', 'CLAUDE.md'))) count++; + + // Root CLAUDE.md + if (existsSync(join(projectPath, 'CLAUDE.md'))) count++; + + // Module-level (scan project subdirectories for CLAUDE.md files) + function scanDir(dir: string, depth: number = 0) { + if (depth > 3) return; // Limit recursion depth + try { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory() && !EXCLUDES.includes(entry.name) && !entry.name.startsWith('.')) { + const subDir = join(dir, entry.name); + if (existsSync(join(subDir, 'CLAUDE.md'))) count++; + scanDir(subDir, depth + 1); + } + } + } catch { /* ignore */ } + } + + scanDir(projectPath); + return count; +} + +/** + * Count hooks from settings object + */ +function countHooksFromSettings(settings: any): number { + if (!settings?.hooks) return 0; + let count = 0; + for (const event of Object.keys(settings.hooks)) { + const hookList = settings.hooks[event]; + count += Array.isArray(hookList) ? hookList.length : 1; + } + return count; +} + +/** + * Count hooks from global and project settings + */ +function countHooks(projectPath: string): { global: number; project: number; total: number } { + let global = 0, project = 0; + + // Global settings + const globalSettingsPath = join(homedir(), '.claude', 'settings.json'); + if (existsSync(globalSettingsPath)) { + try { + const settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); + global = countHooksFromSettings(settings); + } catch { /* ignore */ } + } + + // Project settings + const projectSettingsPath = join(projectPath, '.claude', 'settings.json'); + if (existsSync(projectSettingsPath)) { + try { + const settings = JSON.parse(readFileSync(projectSettingsPath, 'utf8')); + project = countHooksFromSettings(settings); + } catch { /* ignore */ } + } + + return { global, project, total: global + project }; +} + +// ========== Route Handler ========== + +export async function handleNavStatusRoutes(ctx: RouteContext): Promise { + const { pathname, url, res, initialPath } = ctx; + + // GET /api/nav-status - Aggregated navigation badge status + if (pathname === '/api/nav-status' && ctx.req.method === 'GET') { + try { + const projectPath = url.searchParams.get('path') || initialPath; + + // Execute all counts (synchronous file reads wrapped in Promise.resolve for consistency) + const [issues, discoveries, skills, rules, claude, hooks] = await Promise.all([ + Promise.resolve(countIssues(projectPath)), + Promise.resolve(countDiscoveries(projectPath)), + Promise.resolve(countSkills(projectPath)), + Promise.resolve(countRules(projectPath)), + Promise.resolve(countClaudeFiles(projectPath)), + Promise.resolve(countHooks(projectPath)) + ]); + + const response = { + issues: { count: issues }, + discoveries: { count: discoveries }, + skills: { count: skills.total, project: skills.project, user: skills.user }, + rules: { count: rules.total, project: rules.project, user: rules.user }, + claude: { count: claude }, + hooks: { count: hooks.total, global: hooks.global, project: hooks.project }, + timestamp: new Date().toISOString() + }; + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(response)); + return true; + } catch (error) { + console.error('[Nav Status] Error:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (error as Error).message })); + return true; + } + } + + return false; +} diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index e5994294..97fa59a9 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -26,6 +26,7 @@ import { handleClaudeRoutes } from './routes/claude-routes.js'; import { handleHelpRoutes } from './routes/help-routes.js'; import { handleLiteLLMRoutes } from './routes/litellm-routes.js'; import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js'; +import { handleNavStatusRoutes } from './routes/nav-status-routes.js'; // Import WebSocket handling import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js'; @@ -287,6 +288,11 @@ export async function startServer(options: ServerOptions = {}): Promise { await switchToPath(initialPath); + // Update all navigation badges after initial load + if (typeof updateAllNavigationBadges === 'function') { + updateAllNavigationBadges(); + } + // Clean up URL after loading (remove query param) if (urlPath && window.history.replaceState) { window.history.replaceState({}, '', window.location.pathname); diff --git a/ccw/src/templates/dashboard-js/views/codexlens-manager.js b/ccw/src/templates/dashboard-js/views/codexlens-manager.js index 2ca92752..74dfaabb 100644 --- a/ccw/src/templates/dashboard-js/views/codexlens-manager.js +++ b/ccw/src/templates/dashboard-js/views/codexlens-manager.js @@ -656,36 +656,24 @@ window.getModelLockState = getModelLockState; // ============================================================ // Environment variable groups for organized display +// Maps to settings.json structure in ~/.codexlens/settings.json +// Embedding and Reranker are configured separately var ENV_VAR_GROUPS = { - backend: { - label: 'Backend Selection', - icon: 'toggle-left', + embedding: { + label: 'Embedding Configuration', + icon: 'box', vars: { - 'CODEXLENS_EMBEDDING_BACKEND': { label: 'Embedding Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed' }, - 'CODEXLENS_RERANKER_BACKEND': { label: 'Reranker Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed' } - } - }, - local: { - label: 'Local Model Settings', - icon: 'hard-drive', - showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] !== 'litellm' || env['CODEXLENS_RERANKER_BACKEND'] !== 'litellm'; }, - vars: { - 'CODEXLENS_EMBEDDING_MODEL': { label: 'Embedding Model', placeholder: 'fast (code, base, minilm, multilingual, balanced)', default: 'fast' }, - 'CODEXLENS_RERANKER_MODEL': { label: 'Reranker Model', placeholder: 'Xenova/ms-marco-MiniLM-L-6-v2', default: 'Xenova/ms-marco-MiniLM-L-6-v2' } - } - }, - api: { - label: 'API Settings (LiteLLM)', - icon: 'cloud', - showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm' || env['CODEXLENS_RERANKER_BACKEND'] === 'litellm'; }, - vars: { - 'LITELLM_API_KEY': { label: 'API Key', placeholder: 'sk-...', type: 'password' }, - 'LITELLM_API_BASE': { label: 'API Base URL', placeholder: 'https://api.openai.com/v1' }, - 'LITELLM_EMBEDDING_MODEL': { - label: 'Embedding Model', + 'CODEXLENS_EMBEDDING_BACKEND': { label: 'Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed', settingsPath: 'embedding.backend' }, + 'CODEXLENS_EMBEDDING_MODEL': { + label: 'Model', type: 'model-select', placeholder: 'Select or enter model...', - models: [ + default: 'fast', + settingsPath: 'embedding.model', + localModels: [ + { group: 'FastEmbed Profiles', items: ['fast', 'code', 'base', 'minilm', 'multilingual', 'balanced'] } + ], + apiModels: [ { group: 'OpenAI', items: ['text-embedding-3-small', 'text-embedding-3-large', 'text-embedding-ada-002'] }, { group: 'Cohere', items: ['embed-english-v3.0', 'embed-multilingual-v3.0', 'embed-english-light-v3.0'] }, { group: 'Voyage', items: ['voyage-3', 'voyage-3-lite', 'voyage-code-3', 'voyage-multilingual-2'] }, @@ -693,17 +681,69 @@ var ENV_VAR_GROUPS = { { group: 'Jina', items: ['jina-embeddings-v3', 'jina-embeddings-v2-base-en', 'jina-embeddings-v2-base-zh'] } ] }, - 'LITELLM_RERANKER_MODEL': { - label: 'Reranker Model', + 'CODEXLENS_USE_GPU': { label: 'Use GPU', type: 'select', options: ['true', 'false'], default: 'true', settingsPath: 'embedding.use_gpu', showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] !== 'litellm'; } }, + 'CODEXLENS_EMBEDDING_STRATEGY': { label: 'Load Balance', type: 'select', options: ['round_robin', 'latency_aware', 'weighted_random'], default: 'latency_aware', settingsPath: 'embedding.strategy', showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm'; } }, + 'CODEXLENS_EMBEDDING_COOLDOWN': { label: 'Rate Limit Cooldown (s)', type: 'number', placeholder: '60', default: '60', settingsPath: 'embedding.cooldown', min: 0, max: 300, showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm'; } } + } + }, + reranker: { + label: 'Reranker Configuration', + icon: 'arrow-up-down', + vars: { + 'CODEXLENS_RERANKER_ENABLED': { label: 'Enabled', type: 'select', options: ['true', 'false'], default: 'true', settingsPath: 'reranker.enabled' }, + 'CODEXLENS_RERANKER_BACKEND': { label: 'Backend', type: 'select', options: ['fastembed', 'onnx', 'api', 'litellm'], default: 'fastembed', settingsPath: 'reranker.backend' }, + 'CODEXLENS_RERANKER_MODEL': { + label: 'Model', type: 'model-select', placeholder: 'Select or enter model...', - models: [ + default: 'Xenova/ms-marco-MiniLM-L-6-v2', + settingsPath: 'reranker.model', + localModels: [ + { group: 'FastEmbed/ONNX', items: ['Xenova/ms-marco-MiniLM-L-6-v2', 'cross-encoder/ms-marco-MiniLM-L-6-v2', 'BAAI/bge-reranker-base'] } + ], + apiModels: [ { group: 'Cohere', items: ['rerank-english-v3.0', 'rerank-multilingual-v3.0', 'rerank-english-v2.0'] }, { group: 'Voyage', items: ['rerank-2', 'rerank-2-lite', 'rerank-1'] }, { group: 'SiliconFlow', items: ['BAAI/bge-reranker-v2-m3', 'BAAI/bge-reranker-large', 'BAAI/bge-reranker-base'] }, { group: 'Jina', items: ['jina-reranker-v2-base-multilingual', 'jina-reranker-v1-base-en'] } ] - } + }, + 'CODEXLENS_RERANKER_TOP_K': { label: 'Top K Results', type: 'number', placeholder: '50', default: '50', settingsPath: 'reranker.top_k', min: 5, max: 200 } + } + }, + apiCredentials: { + label: 'API Credentials', + icon: 'key', + showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'litellm' || env['CODEXLENS_RERANKER_BACKEND'] === 'litellm' || env['CODEXLENS_RERANKER_BACKEND'] === 'api'; }, + vars: { + 'LITELLM_API_KEY': { label: 'API Key', placeholder: 'sk-...', type: 'password' }, + 'LITELLM_API_BASE': { label: 'API Base URL', placeholder: 'https://api.openai.com/v1' } + } + }, + concurrency: { + label: 'Concurrency Settings', + icon: 'cpu', + vars: { + 'CODEXLENS_API_MAX_WORKERS': { label: 'Max Workers', type: 'number', placeholder: '4', default: '4', settingsPath: 'api.max_workers', min: 1, max: 32 }, + 'CODEXLENS_API_BATCH_SIZE': { label: 'Batch Size', type: 'number', placeholder: '8', default: '8', settingsPath: 'api.batch_size', min: 1, max: 64 } + } + }, + cascade: { + label: 'Cascade Search Settings', + icon: 'git-branch', + vars: { + 'CODEXLENS_CASCADE_STRATEGY': { label: 'Search Strategy', type: 'select', options: ['binary', 'hybrid', 'binary_rerank', 'dense_rerank'], default: 'dense_rerank', settingsPath: 'cascade.strategy' }, + 'CODEXLENS_CASCADE_COARSE_K': { label: 'Coarse K (1st stage)', type: 'number', placeholder: '100', default: '100', settingsPath: 'cascade.coarse_k', min: 10, max: 500 }, + 'CODEXLENS_CASCADE_FINE_K': { label: 'Fine K (final)', type: 'number', placeholder: '10', default: '10', settingsPath: 'cascade.fine_k', min: 1, max: 100 } + } + }, + llm: { + label: 'LLM Features', + icon: 'sparkles', + collapsed: true, + vars: { + 'CODEXLENS_LLM_ENABLED': { label: 'Enable LLM', type: 'select', options: ['true', 'false'], default: 'false', settingsPath: 'llm.enabled' }, + 'CODEXLENS_LLM_BATCH_SIZE': { label: 'Batch Size', type: 'number', placeholder: '5', default: '5', settingsPath: 'llm.batch_size', min: 1, max: 20 } } } }; @@ -784,6 +824,12 @@ async function loadEnvVariables() { for (var key in group.vars) { var config = group.vars[key]; + + // Check variable-level showWhen condition + if (config.showWhen && !config.showWhen(env)) { + continue; + } + // Priority: env file > settings.json > hardcoded default var value = env[key] || settings[key] || config.default || ''; @@ -797,9 +843,16 @@ async function loadEnvVariables() { html += ''; } else if (config.type === 'model-select') { // Model selector with grouped options and custom input support + // Supports localModels/apiModels based on backend type var datalistId = 'models-' + key.replace(/_/g, '-').toLowerCase(); - var isEmbeddingModel = key === 'LITELLM_EMBEDDING_MODEL'; - var isRerankerModel = key === 'LITELLM_RERANKER_MODEL'; + var isEmbedding = key.indexOf('EMBEDDING') !== -1; + 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 || []); + var configuredModels = isEmbedding ? configuredEmbeddingModels : configuredRerankerModels; html += '
' + '' + @@ -811,68 +864,45 @@ async function loadEnvVariables() { '
' + ''; - // For embedding models: use configured models from API settings first - if (isEmbeddingModel && configuredEmbeddingModels.length > 0) { + // For API backend: show configured models from API settings first + if (isApiBackend && configuredModels.length > 0) { html += ''; - configuredEmbeddingModels.forEach(function(model) { + configuredModels.forEach(function(model) { var providers = model.providers ? model.providers.join(', ') : ''; html += ''; }); - // Add separator and fallback options - if (config.models) { + if (modelList.length > 0) { html += ''; - config.models.forEach(function(group) { - group.items.forEach(function(model) { - // Skip if already in configured list - var exists = configuredEmbeddingModels.some(function(m) { return m.modelId === model; }); - if (!exists) { - html += ''; - } - }); - }); } - } else if (isRerankerModel && configuredRerankerModels.length > 0) { - // For reranker models: use configured models from API settings first - html += ''; - configuredRerankerModels.forEach(function(model) { - var providers = model.providers ? model.providers.join(', ') : ''; - html += ''; - }); - // Add separator and fallback options - if (config.models) { - html += ''; - config.models.forEach(function(group) { - group.items.forEach(function(model) { - // Skip if already in configured list - var exists = configuredRerankerModels.some(function(m) { return m.modelId === model; }); - if (!exists) { - html += ''; - } - }); - }); - } - } else if (config.models) { - // Fallback: use static model list - config.models.forEach(function(group) { - group.items.forEach(function(model) { - 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 { var inputType = config.type || 'text'; + var extraAttrs = ''; + if (config.type === 'number') { + if (config.min !== undefined) extraAttrs += ' min="' + config.min + '"'; + if (config.max !== undefined) extraAttrs += ' max="' + config.max + '"'; + extraAttrs += ' step="1"'; + } html += '
' + '' + '' + + 'data-env-key="' + escapeHtml(key) + '" value="' + escapeHtml(value) + '" placeholder="' + escapeHtml(config.placeholder || '') + '"' + extraAttrs + ' />' + '
'; } }