mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
feat: add navigation status routes and update badge aggregation logic
This commit is contained in:
@@ -1785,18 +1785,65 @@ except Exception as e:
|
|||||||
|
|
||||||
// Map settings to env var format for defaults
|
// Map settings to env var format for defaults
|
||||||
const settingsDefaults: Record<string, string> = {};
|
const settingsDefaults: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Embedding settings
|
||||||
if (settings.embedding?.backend) {
|
if (settings.embedding?.backend) {
|
||||||
settingsDefaults['CODEXLENS_EMBEDDING_BACKEND'] = settings.embedding.backend;
|
settingsDefaults['CODEXLENS_EMBEDDING_BACKEND'] = settings.embedding.backend;
|
||||||
}
|
}
|
||||||
if (settings.embedding?.model) {
|
if (settings.embedding?.model) {
|
||||||
settingsDefaults['CODEXLENS_EMBEDDING_MODEL'] = 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) {
|
if (settings.reranker?.backend) {
|
||||||
// Map 'api' to 'litellm' for UI consistency
|
settingsDefaults['CODEXLENS_RERANKER_BACKEND'] = settings.reranker.backend;
|
||||||
settingsDefaults['CODEXLENS_RERANKER_BACKEND'] = settings.reranker.backend === 'api' ? 'litellm' : settings.reranker.backend;
|
|
||||||
}
|
}
|
||||||
if (settings.reranker?.model) {
|
if (settings.reranker?.model) {
|
||||||
settingsDefaults['CODEXLENS_RERANKER_MODEL'] = 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' });
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
@@ -1956,10 +2003,57 @@ except Exception as e:
|
|||||||
|
|
||||||
await writeFile(envPath, lines.join('\n'), 'utf-8');
|
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<string, any> = {};
|
||||||
|
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<string, { path: string[], transform?: (v: string) => 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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: 'Environment configuration saved',
|
message: 'Environment and settings configuration saved',
|
||||||
path: envPath
|
path: envPath,
|
||||||
|
settingsPath
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: err.message, status: 500 };
|
return { success: false, error: err.message, status: 500 };
|
||||||
|
|||||||
240
ccw/src/core/routes/nav-status-routes.ts
Normal file
240
ccw/src/core/routes/nav-status-routes.ts
Normal file
@@ -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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { handleClaudeRoutes } from './routes/claude-routes.js';
|
|||||||
import { handleHelpRoutes } from './routes/help-routes.js';
|
import { handleHelpRoutes } from './routes/help-routes.js';
|
||||||
import { handleLiteLLMRoutes } from './routes/litellm-routes.js';
|
import { handleLiteLLMRoutes } from './routes/litellm-routes.js';
|
||||||
import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
|
import { handleLiteLLMApiRoutes } from './routes/litellm-api-routes.js';
|
||||||
|
import { handleNavStatusRoutes } from './routes/nav-status-routes.js';
|
||||||
|
|
||||||
// Import WebSocket handling
|
// Import WebSocket handling
|
||||||
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
|
import { handleWebSocketUpgrade, broadcastToClients } from './websocket.js';
|
||||||
@@ -287,6 +288,11 @@ export async function startServer(options: ServerOptions = {}): Promise<http.Ser
|
|||||||
if (await handleStatusRoutes(routeContext)) return;
|
if (await handleStatusRoutes(routeContext)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigation status routes (/api/nav-status) - Aggregated badge counts
|
||||||
|
if (pathname === '/api/nav-status') {
|
||||||
|
if (await handleNavStatusRoutes(routeContext)) return;
|
||||||
|
}
|
||||||
|
|
||||||
// CLI routes (/api/cli/*)
|
// CLI routes (/api/cli/*)
|
||||||
if (pathname.startsWith('/api/cli/')) {
|
if (pathname.startsWith('/api/cli/')) {
|
||||||
if (await handleCliRoutes(routeContext)) return;
|
if (await handleCliRoutes(routeContext)) return;
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ async function switchToPath(path) {
|
|||||||
document.getElementById('currentPath').textContent = projectPath;
|
document.getElementById('currentPath').textContent = projectPath;
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
refreshRecentPaths();
|
refreshRecentPaths();
|
||||||
|
|
||||||
|
// Update all navigation badges after path switch
|
||||||
|
if (typeof updateAllNavigationBadges === 'function') {
|
||||||
|
updateAllNavigationBadges();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to switch path:', err);
|
console.error('Failed to switch path:', err);
|
||||||
|
|||||||
@@ -327,6 +327,51 @@ function updateSidebarCounts(data) {
|
|||||||
if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0;
|
if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Navigation Badge Aggregation ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a single badge element by ID
|
||||||
|
* @param {string} badgeId - Element ID
|
||||||
|
* @param {number|undefined} count - Badge count value
|
||||||
|
*/
|
||||||
|
function updateBadgeById(badgeId, count) {
|
||||||
|
const badge = document.getElementById(badgeId);
|
||||||
|
if (badge && count !== undefined) {
|
||||||
|
badge.textContent = count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and update all navigation badges at once
|
||||||
|
* Called on dashboard initialization and path switch
|
||||||
|
*/
|
||||||
|
async function updateAllNavigationBadges() {
|
||||||
|
if (!window.SERVER_MODE) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/nav-status?path=' + encodeURIComponent(projectPath));
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('[Nav Status] Failed to fetch:', response.status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await response.json();
|
||||||
|
|
||||||
|
// Update each badge
|
||||||
|
updateBadgeById('badgeIssues', status.issues?.count);
|
||||||
|
updateBadgeById('badgeDiscovery', status.discoveries?.count);
|
||||||
|
updateBadgeById('badgeSkills', status.skills?.count);
|
||||||
|
updateBadgeById('badgeRules', status.rules?.count);
|
||||||
|
updateBadgeById('badgeClaude', status.claude?.count);
|
||||||
|
updateBadgeById('badgeHooks', status.hooks?.count);
|
||||||
|
|
||||||
|
console.log('[Nav Status] Badges updated:', status);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Nav Status] Error fetching status:', err);
|
||||||
|
// Graceful degradation - badges will update when user visits each page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showRefreshToast(message, type) {
|
function showRefreshToast(message, type) {
|
||||||
// Remove existing toast
|
// Remove existing toast
|
||||||
const existing = document.querySelector('.status-toast');
|
const existing = document.querySelector('.status-toast');
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
|
|
||||||
await switchToPath(initialPath);
|
await switchToPath(initialPath);
|
||||||
|
|
||||||
|
// Update all navigation badges after initial load
|
||||||
|
if (typeof updateAllNavigationBadges === 'function') {
|
||||||
|
updateAllNavigationBadges();
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up URL after loading (remove query param)
|
// Clean up URL after loading (remove query param)
|
||||||
if (urlPath && window.history.replaceState) {
|
if (urlPath && window.history.replaceState) {
|
||||||
window.history.replaceState({}, '', window.location.pathname);
|
window.history.replaceState({}, '', window.location.pathname);
|
||||||
|
|||||||
@@ -656,36 +656,24 @@ window.getModelLockState = getModelLockState;
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Environment variable groups for organized display
|
// 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 = {
|
var ENV_VAR_GROUPS = {
|
||||||
backend: {
|
embedding: {
|
||||||
label: 'Backend Selection',
|
label: 'Embedding Configuration',
|
||||||
icon: 'toggle-left',
|
icon: 'box',
|
||||||
vars: {
|
vars: {
|
||||||
'CODEXLENS_EMBEDDING_BACKEND': { label: 'Embedding Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed' },
|
'CODEXLENS_EMBEDDING_BACKEND': { label: 'Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed', settingsPath: 'embedding.backend' },
|
||||||
'CODEXLENS_RERANKER_BACKEND': { label: 'Reranker Backend', type: 'select', options: ['fastembed', 'litellm'], default: 'fastembed' }
|
'CODEXLENS_EMBEDDING_MODEL': {
|
||||||
}
|
label: 'Model',
|
||||||
},
|
|
||||||
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',
|
|
||||||
type: 'model-select',
|
type: 'model-select',
|
||||||
placeholder: 'Select or enter model...',
|
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: '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: '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'] },
|
{ 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'] }
|
{ group: 'Jina', items: ['jina-embeddings-v3', 'jina-embeddings-v2-base-en', 'jina-embeddings-v2-base-zh'] }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
'LITELLM_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'; } },
|
||||||
label: 'Reranker Model',
|
'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',
|
type: 'model-select',
|
||||||
placeholder: 'Select or enter model...',
|
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: '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: '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: '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'] }
|
{ 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) {
|
for (var key in group.vars) {
|
||||||
var config = group.vars[key];
|
var config = group.vars[key];
|
||||||
|
|
||||||
|
// Check variable-level showWhen condition
|
||||||
|
if (config.showWhen && !config.showWhen(env)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Priority: env file > settings.json > hardcoded default
|
// Priority: env file > settings.json > hardcoded default
|
||||||
var value = env[key] || settings[key] || config.default || '';
|
var value = env[key] || settings[key] || config.default || '';
|
||||||
|
|
||||||
@@ -797,9 +843,16 @@ async function loadEnvVariables() {
|
|||||||
html += '</select></div>';
|
html += '</select></div>';
|
||||||
} else if (config.type === 'model-select') {
|
} else if (config.type === 'model-select') {
|
||||||
// Model selector with grouped options and custom input support
|
// Model selector with grouped options and custom input support
|
||||||
|
// Supports localModels/apiModels based on backend type
|
||||||
var datalistId = 'models-' + key.replace(/_/g, '-').toLowerCase();
|
var datalistId = 'models-' + key.replace(/_/g, '-').toLowerCase();
|
||||||
var isEmbeddingModel = key === 'LITELLM_EMBEDDING_MODEL';
|
var isEmbedding = key.indexOf('EMBEDDING') !== -1;
|
||||||
var isRerankerModel = key === 'LITELLM_RERANKER_MODEL';
|
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 += '<div class="flex items-center gap-2">' +
|
html += '<div class="flex items-center gap-2">' +
|
||||||
'<label class="text-xs text-muted-foreground w-28 flex-shrink-0" title="' + escapeHtml(key) + '">' + escapeHtml(config.label) + '</label>' +
|
'<label class="text-xs text-muted-foreground w-28 flex-shrink-0" title="' + escapeHtml(key) + '">' + escapeHtml(config.label) + '</label>' +
|
||||||
@@ -811,68 +864,45 @@ async function loadEnvVariables() {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'<datalist id="' + datalistId + '">';
|
'<datalist id="' + datalistId + '">';
|
||||||
|
|
||||||
// For embedding models: use configured models from API settings first
|
// For API backend: show configured models from API settings first
|
||||||
if (isEmbeddingModel && configuredEmbeddingModels.length > 0) {
|
if (isApiBackend && configuredModels.length > 0) {
|
||||||
html += '<option value="" disabled>-- Configured in API Settings --</option>';
|
html += '<option value="" disabled>-- Configured in API Settings --</option>';
|
||||||
configuredEmbeddingModels.forEach(function(model) {
|
configuredModels.forEach(function(model) {
|
||||||
var providers = model.providers ? model.providers.join(', ') : '';
|
var providers = model.providers ? model.providers.join(', ') : '';
|
||||||
html += '<option value="' + escapeHtml(model.modelId) + '">' +
|
html += '<option value="' + escapeHtml(model.modelId) + '">' +
|
||||||
escapeHtml(model.modelName || model.modelId) +
|
escapeHtml(model.modelName || model.modelId) +
|
||||||
(providers ? ' (' + escapeHtml(providers) + ')' : '') +
|
(providers ? ' (' + escapeHtml(providers) + ')' : '') +
|
||||||
'</option>';
|
'</option>';
|
||||||
});
|
});
|
||||||
// Add separator and fallback options
|
if (modelList.length > 0) {
|
||||||
if (config.models) {
|
|
||||||
html += '<option value="" disabled>-- Common Models --</option>';
|
html += '<option value="" disabled>-- Common Models --</option>';
|
||||||
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 += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (isRerankerModel && configuredRerankerModels.length > 0) {
|
|
||||||
// For reranker models: use configured models from API settings first
|
|
||||||
html += '<option value="" disabled>-- Configured in API Settings --</option>';
|
|
||||||
configuredRerankerModels.forEach(function(model) {
|
|
||||||
var providers = model.providers ? model.providers.join(', ') : '';
|
|
||||||
html += '<option value="' + escapeHtml(model.modelId) + '">' +
|
|
||||||
escapeHtml(model.modelName || model.modelId) +
|
|
||||||
(providers ? ' (' + escapeHtml(providers) + ')' : '') +
|
|
||||||
'</option>';
|
|
||||||
});
|
|
||||||
// Add separator and fallback options
|
|
||||||
if (config.models) {
|
|
||||||
html += '<option value="" disabled>-- Common Models --</option>';
|
|
||||||
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 += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (config.models) {
|
|
||||||
// Fallback: use static model list
|
|
||||||
config.models.forEach(function(group) {
|
|
||||||
group.items.forEach(function(model) {
|
|
||||||
html += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
html += '</datalist></div>';
|
html += '</datalist></div>';
|
||||||
} else {
|
} else {
|
||||||
var inputType = config.type || 'text';
|
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 += '<div class="flex items-center gap-2">' +
|
html += '<div class="flex items-center gap-2">' +
|
||||||
'<label class="text-xs text-muted-foreground w-28 flex-shrink-0" title="' + escapeHtml(key) + '">' + escapeHtml(config.label) + '</label>' +
|
'<label class="text-xs text-muted-foreground w-28 flex-shrink-0" title="' + escapeHtml(key) + '">' + escapeHtml(config.label) + '</label>' +
|
||||||
'<input type="' + inputType + '" class="tool-config-input flex-1 text-xs py-1" ' +
|
'<input type="' + inputType + '" class="tool-config-input flex-1 text-xs py-1" ' +
|
||||||
'data-env-key="' + escapeHtml(key) + '" value="' + escapeHtml(value) + '" placeholder="' + escapeHtml(config.placeholder || '') + '" />' +
|
'data-env-key="' + escapeHtml(key) + '" value="' + escapeHtml(value) + '" placeholder="' + escapeHtml(config.placeholder || '') + '"' + extraAttrs + ' />' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user