Add comprehensive code review specifications and templates

- Introduced best practices requirements specification covering code quality, performance, maintainability, error handling, and documentation standards.
- Established quality standards with overall quality metrics and mandatory checks for security, code quality, performance, and maintainability.
- Created security requirements specification aligned with OWASP Top 10 and CWE Top 25, detailing checks and patterns for common vulnerabilities.
- Developed templates for documenting best practice findings, security findings, and generating reports, including structured markdown and JSON formats.
- Updated dependencies in the project, ensuring compatibility and stability.
- Added test files and README documentation for vector indexing tests.
This commit is contained in:
catlog22
2026-01-06 23:11:15 +08:00
parent 02d66325a0
commit ef770ff29b
32 changed files with 4530 additions and 164 deletions

View File

@@ -299,10 +299,30 @@
color: hsl(38 92% 50%);
}
.icon-btn.favorite-active svg {
stroke: hsl(38 92% 50%);
fill: hsl(38 92% 50% / 0.2);
}
.icon-btn.favorite-active:hover {
color: hsl(38 92% 40%);
}
.icon-btn.favorite-active:hover svg {
stroke: hsl(38 92% 40%);
fill: hsl(38 92% 40% / 0.3);
}
/* Favorite star icon in memory-id */
.favorite-star {
color: hsl(38 92% 50%);
}
.favorite-star svg {
stroke: hsl(38 92% 50%);
fill: hsl(38 92% 50% / 0.2);
}
.icon-btn i {
width: 18px;
height: 18px;

View File

@@ -429,6 +429,136 @@
color: hsl(200 80% 70%);
}
/* ===== Formatted Message Types ===== */
.cli-stream-line.formatted {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 10px;
margin: 2px 0;
border-radius: 4px;
transition: all 0.15s ease;
color: hsl(0 0% 90%); /* Ensure text is visible */
}
.cli-stream-line.formatted:hover {
background: hsl(0 0% 100% / 0.05);
}
/* Message Badge */
.cli-msg-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
white-space: nowrap;
flex-shrink: 0;
}
.cli-msg-badge i,
.cli-msg-badge svg {
width: 12px;
height: 12px;
}
.cli-msg-content {
flex: 1;
word-break: break-word;
}
/* System Message */
.cli-stream-line.formatted.system {
background: hsl(210 50% 20% / 0.3);
border-left: 3px solid hsl(210 80% 55%);
}
.cli-msg-badge.cli-msg-system {
background: hsl(210 80% 55% / 0.2);
color: hsl(210 80% 70%);
}
/* Thinking Message */
.cli-stream-line.formatted.thinking {
background: hsl(280 50% 20% / 0.3);
border-left: 3px solid hsl(280 70% 65%);
font-style: italic;
}
.cli-msg-badge.cli-msg-thinking {
background: hsl(280 70% 65% / 0.2);
color: hsl(280 70% 75%);
}
/* Response Message */
.cli-stream-line.formatted.response {
background: hsl(145 40% 18% / 0.3);
border-left: 3px solid hsl(145 60% 50%);
}
.cli-msg-badge.cli-msg-response {
background: hsl(145 60% 50% / 0.2);
color: hsl(145 60% 65%);
}
/* Result Message */
.cli-stream-line.formatted.result {
background: hsl(160 50% 18% / 0.4);
border-left: 3px solid hsl(160 80% 45%);
}
.cli-msg-badge.cli-msg-result {
background: hsl(160 80% 45% / 0.25);
color: hsl(160 80% 60%);
}
/* Error Message */
.cli-stream-line.formatted.error {
background: hsl(0 50% 20% / 0.4);
border-left: 3px solid hsl(0 70% 55%);
}
.cli-msg-badge.cli-msg-error {
background: hsl(0 70% 55% / 0.25);
color: hsl(0 70% 70%);
}
/* Warning Message */
.cli-stream-line.formatted.warning {
background: hsl(45 60% 18% / 0.4);
border-left: 3px solid hsl(45 80% 55%);
}
.cli-msg-badge.cli-msg-warning {
background: hsl(45 80% 55% / 0.25);
color: hsl(45 80% 65%);
}
/* Info Message */
.cli-stream-line.formatted.info {
background: hsl(200 50% 18% / 0.3);
border-left: 3px solid hsl(200 70% 60%);
}
.cli-msg-badge.cli-msg-info {
background: hsl(200 70% 60% / 0.2);
color: hsl(200 70% 70%);
}
/* Inline Code */
.cli-inline-code {
padding: 1px 5px;
background: hsl(0 0% 25%);
border-radius: 3px;
font-family: var(--font-mono, 'Consolas', 'Monaco', 'Courier New', monospace);
font-size: 0.85em;
color: hsl(45 80% 70%);
}
/* JSON/Code syntax coloring in output */
.cli-stream-line .json-key {
color: hsl(200 80% 70%);

View File

@@ -269,6 +269,106 @@ function handleCliStreamError(payload) {
updateStreamBadge();
}
// ===== Message Type Parsing =====
const MESSAGE_TYPE_PATTERNS = {
system: /^\[系统\]/,
thinking: /^\[思考\]/,
response: /^\[响应\]/,
result: /^\[结果\]/,
error: /^\[错误\]/,
warning: /^\[警告\]/,
info: /^\[信息\]/
};
const MESSAGE_TYPE_ICONS = {
system: 'settings',
thinking: 'brain',
response: 'message-circle',
result: 'check-circle',
error: 'alert-circle',
warning: 'alert-triangle',
info: 'info'
};
const MESSAGE_TYPE_LABELS = {
system: '系统',
thinking: '思考',
response: '响应',
result: '结果',
error: '错误',
warning: '警告',
info: '信息'
};
/**
* Parse message content to extract type and clean content
* @param {string} content - Raw message content
* @returns {{ type: string, label: string, content: string, hasPrefix: boolean }}
*/
function parseMessageType(content) {
for (const [type, pattern] of Object.entries(MESSAGE_TYPE_PATTERNS)) {
if (pattern.test(content)) {
return {
type,
label: MESSAGE_TYPE_LABELS[type],
content: content.replace(pattern, '').trim(),
hasPrefix: true
};
}
}
return {
type: 'default',
label: '',
content: content,
hasPrefix: false
};
}
/**
* Render a formatted message line with type badge
* @param {Object} line - Line object with type and content
* @param {string} searchFilter - Current search filter
* @returns {string} - HTML string
*/
function renderFormattedLine(line, searchFilter) {
const parsed = parseMessageType(line.content);
let content = escapeHtml(parsed.content);
// Apply search highlighting
if (searchFilter && searchFilter.trim()) {
const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
content = content.replace(searchRegex, '<mark class="cli-stream-highlight">$1</mark>');
}
// Format code blocks
content = formatCodeBlocks(content);
// Format inline code
content = content.replace(/`([^`]+)`/g, '<code class="cli-inline-code">$1</code>');
// Build type badge if has prefix
const typeBadge = parsed.hasPrefix ?
`<span class="cli-msg-badge cli-msg-${parsed.type}">
<i data-lucide="${MESSAGE_TYPE_ICONS[parsed.type] || 'circle'}"></i>
<span>${parsed.label}</span>
</span>` : '';
// Determine line class based on original type and parsed type
const lineClass = parsed.hasPrefix ? `cli-stream-line formatted ${parsed.type}` :
`cli-stream-line ${line.type}`;
return `<div class="${lineClass}">${typeBadge}<span class="cli-msg-content">${content}</span></div>`;
}
/**
* Format code blocks in content
*/
function formatCodeBlocks(content) {
// Handle multi-line code blocks (already escaped)
// Just apply styling class for now
return content;
}
// ===== UI Rendering =====
function renderStreamTabs() {
const tabsContainer = document.getElementById('cliStreamTabs');
@@ -351,16 +451,15 @@ function renderStreamContent(executionId) {
);
}
// Render output lines with search highlighting
contentContainer.innerHTML = filteredOutput.map(line => {
let content = escapeHtml(line.content);
// Highlight search matches
if (searchFilter.trim()) {
const searchRegex = new RegExp(`(${escapeRegex(searchFilter)})`, 'gi');
content = content.replace(searchRegex, '<mark class="cli-stream-highlight">$1</mark>');
}
return `<div class="cli-stream-line ${line.type}">${content}</div>`;
}).join('');
// Render output lines with formatted styling
contentContainer.innerHTML = filteredOutput.map(line =>
renderFormattedLine(line, searchFilter)
).join('');
// Initialize Lucide icons for message badges
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { class: 'cli-msg-icon' } });
}
// Show filter result count if filtering
if (searchFilter.trim() && filteredOutput.length !== exec.output.length) {

View File

@@ -298,6 +298,8 @@ const i18n = {
'codexlens.configuredInApiSettings': 'Configured in API Settings',
'codexlens.commonModels': 'Common Models',
'codexlens.selectApiModel': 'Select API model...',
'codexlens.selectLocalModel': 'Select local model...',
'codexlens.noConfiguredModels': 'No models configured in API Settings',
'codexlens.autoDownloadHint': 'Models are auto-downloaded on first use',
'codexlens.embeddingBackend': 'Embedding Backend',
'codexlens.localFastembed': 'Local (FastEmbed)',
@@ -2305,6 +2307,8 @@ const i18n = {
'codexlens.configuredInApiSettings': '已在 API 设置中配置',
'codexlens.commonModels': '常用模型',
'codexlens.selectApiModel': '选择 API 模型...',
'codexlens.selectLocalModel': '选择本地模型...',
'codexlens.noConfiguredModels': '未在 API 设置中配置模型',
'codexlens.autoDownloadHint': '模型会在首次使用时自动下载',
'codexlens.embeddingBackend': '嵌入后端',
'codexlens.localFastembed': '本地 (FastEmbed)',

View File

@@ -147,14 +147,40 @@ function buildCodexLensConfigContent(config) {
'</div>' +
'</div>' +
// Quick Actions
// Index Operations - 4 buttons grid
'<div class="space-y-2">' +
'<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">Quick Actions</h4>' +
'<div class="grid grid-cols-2 gap-2">' +
(isInstalled
? '<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-primary/30 bg-primary/5 text-primary hover:bg-primary/10 transition-colors" onclick="initCodexLensIndex()">' +
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> Update Index' +
'<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">' + (t('codexlens.indexOperations') || 'Index Operations') + '</h4>' +
(isInstalled
? '<div class="grid grid-cols-2 gap-2">' +
// FTS Full Index
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-blue-500/30 bg-blue-500/5 text-blue-600 hover:bg-blue-500/10 transition-colors" onclick="runFtsFullIndex()" title="' + (t('codexlens.ftsFullIndexDesc') || 'Rebuild full-text search index') + '">' +
'<i data-lucide="file-text" class="w-4 h-4"></i> FTS ' + (t('codexlens.fullIndex') || 'Full') +
'</button>' +
// FTS Incremental
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-blue-500/30 bg-background text-blue-600 hover:bg-blue-500/5 transition-colors" onclick="runFtsIncrementalUpdate()" title="' + (t('codexlens.ftsIncrementalDesc') || 'Update FTS index for changed files') + '">' +
'<i data-lucide="file-plus" class="w-4 h-4"></i> FTS ' + (t('codexlens.incremental') || 'Incremental') +
'</button>' +
// Vector Full Index
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-purple-500/30 bg-purple-500/5 text-purple-600 hover:bg-purple-500/10 transition-colors" onclick="runVectorFullIndex()" title="' + (t('codexlens.vectorFullIndexDesc') || 'Generate all embeddings') + '">' +
'<i data-lucide="brain" class="w-4 h-4"></i> Vector ' + (t('codexlens.fullIndex') || 'Full') +
'</button>' +
// Vector Incremental
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-purple-500/30 bg-background text-purple-600 hover:bg-purple-500/5 transition-colors" onclick="runVectorIncrementalUpdate()" title="' + (t('codexlens.vectorIncrementalDesc') || 'Generate embeddings for new files only') + '">' +
'<i data-lucide="brain" class="w-4 h-4"></i> Vector ' + (t('codexlens.incremental') || 'Incremental') +
'</button>' +
'</div>'
: '<div class="grid grid-cols-2 gap-2">' +
'<button class="col-span-2 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" onclick="installCodexLensFromManager()">' +
'<i data-lucide="download" class="w-4 h-4"></i> Install CodexLens' +
'</button>' +
'</div>') +
'</div>' +
// Quick Actions
'<div class="space-y-2 mt-3">' +
'<h4 class="text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2">' + (t('codexlens.quickActions') || 'Quick Actions') + '</h4>' +
(isInstalled
? '<div class="grid grid-cols-2 gap-2">' +
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted/50 transition-colors" onclick="showWatcherControlModal()">' +
'<i data-lucide="eye" class="w-4 h-4"></i> File Watcher' +
'</button>' +
@@ -163,11 +189,9 @@ function buildCodexLensConfigContent(config) {
'</button>' +
'<button class="flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg border border-border bg-background hover:bg-muted/50 transition-colors" onclick="cleanCurrentWorkspaceIndex()">' +
'<i data-lucide="eraser" class="w-4 h-4"></i> Clean Workspace' +
'</button>'
: '<button class="col-span-2 flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" onclick="installCodexLensFromManager()">' +
'<i data-lucide="download" class="w-4 h-4"></i> Install CodexLens' +
'</button>') +
'</div>' +
'</button>' +
'</div>'
: '') +
'</div>' +
'</div>' +
@@ -684,9 +708,10 @@ var ENV_VAR_GROUPS = {
{ group: 'Jina', items: ['jina-embeddings-v3', 'jina-embeddings-v2-base-en', 'jina-embeddings-v2-base-zh'] }
]
},
'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'; } }
'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'] === 'local'; } },
'CODEXLENS_EMBEDDING_POOL_ENABLED': { label: 'High Availability', type: 'select', options: ['true', 'false'], default: 'false', settingsPath: 'embedding.pool_enabled', showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'api'; } },
'CODEXLENS_EMBEDDING_STRATEGY': { label: 'Load Balance Strategy', type: 'select', options: ['round_robin', 'latency_aware', 'weighted_random'], default: 'latency_aware', settingsPath: 'embedding.strategy', showWhen: function(env) { return env['CODEXLENS_EMBEDDING_BACKEND'] === 'api' && env['CODEXLENS_EMBEDDING_POOL_ENABLED'] === 'true'; } },
'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'] === 'api' && env['CODEXLENS_EMBEDDING_POOL_ENABLED'] === 'true'; } }
}
},
reranker: {
@@ -711,7 +736,10 @@ var ENV_VAR_GROUPS = {
{ 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 }
'CODEXLENS_RERANKER_TOP_K': { label: 'Top K Results', type: 'number', placeholder: '50', default: '50', settingsPath: 'reranker.top_k', min: 5, max: 200 },
'CODEXLENS_RERANKER_POOL_ENABLED': { label: 'High Availability', type: 'select', options: ['true', 'false'], default: 'false', settingsPath: 'reranker.pool_enabled', showWhen: function(env) { return env['CODEXLENS_RERANKER_BACKEND'] === 'api'; } },
'CODEXLENS_RERANKER_STRATEGY': { label: 'Load Balance Strategy', type: 'select', options: ['round_robin', 'latency_aware', 'weighted_random'], default: 'latency_aware', settingsPath: 'reranker.strategy', showWhen: function(env) { return env['CODEXLENS_RERANKER_BACKEND'] === 'api' && env['CODEXLENS_RERANKER_POOL_ENABLED'] === 'true'; } },
'CODEXLENS_RERANKER_COOLDOWN': { label: 'Rate Limit Cooldown (s)', type: 'number', placeholder: '60', default: '60', settingsPath: 'reranker.cooldown', min: 0, max: 300, showWhen: function(env) { return env['CODEXLENS_RERANKER_BACKEND'] === 'api' && env['CODEXLENS_RERANKER_POOL_ENABLED'] === 'true'; } }
}
},
concurrency: {
@@ -730,15 +758,6 @@ var ENV_VAR_GROUPS = {
'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: {
labelKey: 'codexlens.envGroup.llm',
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 }
}
}
};
@@ -859,12 +878,11 @@ 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;
}
// Check variable-level showWhen condition - render but hide if condition is false
var shouldShow = !config.showWhen || config.showWhen(env);
var hiddenStyle = shouldShow ? '' : ' style="display:none"';
// Priority: env file > settings.json > hardcoded default
var value = env[key] || settings[key] || config.default || '';
@@ -874,7 +892,7 @@ async function loadEnvVariables() {
if (key === 'CODEXLENS_EMBEDDING_BACKEND' || key === 'CODEXLENS_RERANKER_BACKEND') {
onchangeHandler = ' onchange="updateModelOptionsOnBackendChange(\'' + key + '\', this.value)"';
}
html += '<div class="flex items-center gap-2">' +
html += '<div class="flex items-center gap-2"' + hiddenStyle + '>' +
'<label class="text-xs text-muted-foreground w-28 flex-shrink-0">' + escapeHtml(config.label) + '</label>' +
'<select class="tool-config-input flex-1 text-xs py-1" data-env-key="' + escapeHtml(key) + '"' + onchangeHandler + '>';
config.options.forEach(function(opt) {
@@ -897,7 +915,7 @@ async function loadEnvVariables() {
// Fallback preset list for API models
var apiModelList = config.apiModels || [];
html += '<div class="flex items-center gap-2">' +
html += '<div class="flex items-center gap-2"' + hiddenStyle + '>' +
'<label class="text-xs text-muted-foreground w-28 flex-shrink-0" title="' + escapeHtml(key) + '">' + escapeHtml(config.label) + '</label>' +
'<div class="relative flex-1">' +
'<input type="text" class="tool-config-input w-full text-xs py-1 pr-6" ' +
@@ -908,7 +926,8 @@ async function loadEnvVariables() {
'<datalist id="' + datalistId + '">';
if (isApiBackend) {
// For API backend: show configured models from API settings first
// For API backend: show ONLY configured models from API settings
// (don't show unconfigured preset models - they won't work without configuration)
if (configuredModels.length > 0) {
html += '<option value="" disabled>-- ' + (t('codexlens.configuredModels') || 'Configured in API Settings') + ' --</option>';
configuredModels.forEach(function(model) {
@@ -918,19 +937,8 @@ async function loadEnvVariables() {
(providers ? ' (' + escapeHtml(providers) + ')' : '') +
'</option>';
});
}
// Then show common API models as suggestions
if (apiModelList.length > 0) {
html += '<option value="" disabled>-- ' + (t('codexlens.commonModels') || 'Common Models') + ' --</option>';
apiModelList.forEach(function(group) {
group.items.forEach(function(model) {
// Skip if already in configured list
var exists = configuredModels.some(function(m) { return m.modelId === model; });
if (!exists) {
html += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
}
});
});
} else {
html += '<option value="" disabled>-- ' + (t('codexlens.noConfiguredModels') || 'No models configured in API Settings') + ' --</option>';
}
} else {
// For local backend (fastembed): show actually downloaded models
@@ -959,7 +967,7 @@ async function loadEnvVariables() {
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"' + hiddenStyle + '>' +
'<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" ' +
'data-env-key="' + escapeHtml(key) + '" value="' + escapeHtml(value) + '" placeholder="' + escapeHtml(config.placeholder || '') + '"' + extraAttrs + ' />' +
@@ -1021,7 +1029,8 @@ async function loadEnvVariables() {
var optionsHtml = '';
if (isApiBackend) {
// For API backend: show configured models from API settings first
// For API backend: show ONLY configured models from API settings
// (don't show unconfigured preset models - they won't work without configuration)
if (apiConfiguredModels.length > 0) {
optionsHtml += '<option value="" disabled>-- ' + (t('codexlens.configuredModels') || 'Configured in API Settings') + ' --</option>';
apiConfiguredModels.forEach(function(model) {
@@ -1031,18 +1040,8 @@ async function loadEnvVariables() {
(providers ? ' (' + escapeHtml(providers) + ')' : '') +
'</option>';
});
}
// Then show common API models as suggestions
if (apiModelList.length > 0) {
optionsHtml += '<option value="" disabled>-- ' + (t('codexlens.commonModels') || 'Common Models') + ' --</option>';
apiModelList.forEach(function(group) {
group.items.forEach(function(model) {
var exists = apiConfiguredModels.some(function(m) { return m.modelId === model; });
if (!exists) {
optionsHtml += '<option value="' + escapeHtml(model) + '">' + escapeHtml(group.group) + ': ' + escapeHtml(model) + '</option>';
}
});
});
} else {
optionsHtml += '<option value="" disabled>-- ' + (t('codexlens.noConfiguredModels') || 'No models configured in API Settings') + ' --</option>';
}
} else {
// For local backend: show actually downloaded models
@@ -1070,9 +1069,65 @@ async function loadEnvVariables() {
}
}
}
// Update visibility of dependent fields based on new backend value
var prefix = isEmbedding ? 'CODEXLENS_EMBEDDING_' : 'CODEXLENS_RERANKER_';
var gpuField = document.querySelector('[data-env-key="' + prefix + 'USE_GPU"]');
var poolField = document.querySelector('[data-env-key="' + prefix + 'POOL_ENABLED"]');
var strategyField = document.querySelector('[data-env-key="' + prefix + 'STRATEGY"]');
var cooldownField = document.querySelector('[data-env-key="' + prefix + 'COOLDOWN"]');
// GPU only for local backend
if (gpuField) {
var gpuRow = gpuField.closest('.flex.items-center');
if (gpuRow) gpuRow.style.display = isApiBackend ? 'none' : '';
}
// Pool, Strategy, Cooldown only for API backend
if (poolField) {
var poolRow = poolField.closest('.flex.items-center');
if (poolRow) poolRow.style.display = isApiBackend ? '' : 'none';
// Reset pool value when switching to local
if (!isApiBackend) poolField.value = 'false';
}
// Strategy and Cooldown depend on pool being enabled
var poolEnabled = poolField && poolField.value === 'true';
if (strategyField) {
var strategyRow = strategyField.closest('.flex.items-center');
if (strategyRow) strategyRow.style.display = (isApiBackend && poolEnabled) ? '' : 'none';
}
if (cooldownField) {
var cooldownRow = cooldownField.closest('.flex.items-center');
if (cooldownRow) cooldownRow.style.display = (isApiBackend && poolEnabled) ? '' : 'none';
}
// Note: No auto-save here - user must click Save button
});
});
// Add change handler for pool_enabled selects to show/hide strategy and cooldown
var poolSelects = container.querySelectorAll('select[data-env-key*="POOL_ENABLED"]');
poolSelects.forEach(function(select) {
select.addEventListener('change', function() {
var poolKey = select.getAttribute('data-env-key');
var poolEnabled = select.value === 'true';
var isEmbedding = poolKey.indexOf('EMBEDDING') !== -1;
var prefix = isEmbedding ? 'CODEXLENS_EMBEDDING_' : 'CODEXLENS_RERANKER_';
var strategyField = document.querySelector('[data-env-key="' + prefix + 'STRATEGY"]');
var cooldownField = document.querySelector('[data-env-key="' + prefix + 'COOLDOWN"]');
if (strategyField) {
var strategyRow = strategyField.closest('.flex.items-center');
if (strategyRow) strategyRow.style.display = poolEnabled ? '' : 'none';
}
if (cooldownField) {
var cooldownRow = cooldownField.closest('.flex.items-center');
if (cooldownRow) cooldownRow.style.display = poolEnabled ? '' : 'none';
}
});
});
} catch (err) {
container.innerHTML = '<div class="text-xs text-error">' + escapeHtml(err.message) + '</div>';
}
@@ -2213,6 +2268,9 @@ async function loadModelList() {
'<div class="flex items-center gap-2">' +
statusIcon +
'<span class="text-sm font-medium">' + model.profile + '</span>' +
'<button class="text-muted-foreground hover:text-foreground p-0.5" onclick="copyToClipboard(\'' + escapeHtml(model.model_name) + '\')" title="' + escapeHtml(model.model_name) + '">' +
'<i data-lucide="copy" class="w-3 h-3"></i>' +
'</button>' +
'<span class="text-xs text-muted-foreground">' + model.dimensions + 'd</span>' +
'</div>' +
'<div class="flex items-center gap-3">' +
@@ -2491,6 +2549,9 @@ async function loadRerankerModelList() {
'<div class="flex items-center gap-2">' +
statusIcon +
'<span class="text-sm font-medium">' + model.id + recBadge + '</span>' +
'<button class="text-muted-foreground hover:text-foreground p-0.5" onclick="copyToClipboard(\'' + escapeHtml(model.name) + '\')" title="' + escapeHtml(model.name) + '">' +
'<i data-lucide="copy" class="w-3 h-3"></i>' +
'</button>' +
'<span class="text-xs text-muted-foreground">' + model.desc + '</span>' +
'</div>' +
'<div class="flex items-center gap-3">' +
@@ -2901,12 +2962,14 @@ async function updateSemanticStatusBadge() {
* @param {string} embeddingModel - Model profile: 'code', 'fast'
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
* @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1)
* @param {boolean} incremental - Incremental mode: true=skip unchanged, false=full rebuild (default: false)
*/
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, maxWorkers) {
async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, maxWorkers, incremental) {
indexType = indexType || 'vector';
embeddingModel = embeddingModel || 'code';
embeddingBackend = embeddingBackend || 'fastembed';
maxWorkers = maxWorkers || 1;
incremental = incremental !== undefined ? incremental : false; // Default: full rebuild
// For vector/full index with local backend, check if semantic dependencies are available
// LiteLLM backend uses remote embeddings and does not require fastembed/ONNX deps.
@@ -3011,7 +3074,7 @@ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, m
var apiIndexType = (indexType === 'full') ? 'vector' : indexType;
// Start indexing with specified type and model
startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend, maxWorkers);
startCodexLensIndexing(apiIndexType, embeddingModel, embeddingBackend, maxWorkers, incremental);
}
/**
@@ -3020,12 +3083,14 @@ async function initCodexLensIndex(indexType, embeddingModel, embeddingBackend, m
* @param {string} embeddingModel - Model profile: 'code', 'fast'
* @param {string} embeddingBackend - Backend: 'fastembed' (local) or 'litellm' (API)
* @param {number} maxWorkers - Max concurrent API calls for embedding generation (default: 1)
* @param {boolean} incremental - Incremental mode (default: false for full rebuild)
*/
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend, maxWorkers) {
async function startCodexLensIndexing(indexType, embeddingModel, embeddingBackend, maxWorkers, incremental) {
indexType = indexType || 'vector';
embeddingModel = embeddingModel || 'code';
embeddingBackend = embeddingBackend || 'fastembed';
maxWorkers = maxWorkers || 1;
incremental = incremental !== undefined ? incremental : false; // Default: full rebuild
var statusText = document.getElementById('codexlensIndexStatus');
var progressBar = document.getElementById('codexlensIndexProgressBar');
var percentText = document.getElementById('codexlensIndexPercent');
@@ -3057,11 +3122,11 @@ async function startCodexLensIndexing(indexType, embeddingModel, embeddingBacken
}
try {
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend, 'maxWorkers:', maxWorkers);
console.log('[CodexLens] Starting index for:', projectPath, 'type:', indexType, 'model:', embeddingModel, 'backend:', embeddingBackend, 'maxWorkers:', maxWorkers, 'incremental:', incremental);
var response = await fetch('/api/codexlens/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend, maxWorkers: maxWorkers })
body: JSON.stringify({ path: projectPath, indexType: indexType, embeddingModel: embeddingModel, embeddingBackend: embeddingBackend, maxWorkers: maxWorkers, incremental: incremental })
});
var result = await response.json();
@@ -4165,6 +4230,121 @@ function initCodexLensIndexFromPage(indexType) {
}
}
// ============================================================
// INDEX OPERATIONS - 4 Button Functions
// ============================================================
/**
* Run FTS full index (rebuild full-text search index)
* Creates FTS index without embeddings
*/
window.runFtsFullIndex = async function runFtsFullIndex() {
showRefreshToast(t('codexlens.startingFtsFullIndex') || 'Starting FTS full index...', 'info');
// FTS only, no embeddings, full rebuild (incremental=false)
initCodexLensIndex('normal', null, 'fastembed', 1, false);
}
/**
* Run FTS incremental update
* Updates FTS index for changed files only
*/
window.runFtsIncrementalUpdate = async function runFtsIncrementalUpdate() {
var projectPath = window.CCW_PROJECT_ROOT || '.';
showRefreshToast(t('codexlens.startingFtsIncremental') || 'Starting FTS incremental update...', 'info');
try {
// Use index update endpoint for FTS incremental
var response = await fetch('/api/codexlens/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: projectPath,
indexType: 'normal', // FTS only
incremental: true
})
});
var result = await response.json();
if (result.success) {
showRefreshToast(t('codexlens.ftsIncrementalComplete') || 'FTS incremental update completed', 'success');
renderCodexLensManager();
} else {
showRefreshToast((t('codexlens.ftsIncrementalFailed') || 'FTS incremental failed') + ': ' + (result.error || 'Unknown error'), 'error');
}
} catch (err) {
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
}
}
/**
* Run Vector full index (generate all embeddings)
* Generates embeddings for all files
*/
window.runVectorFullIndex = async function runVectorFullIndex() {
showRefreshToast(t('codexlens.startingVectorFullIndex') || 'Starting Vector full index...', 'info');
try {
// Fetch env settings to get the configured embedding model
var envResponse = await fetch('/api/codexlens/env');
var envData = await envResponse.json();
var embeddingModel = envData.CODEXLENS_EMBEDDING_MODEL || envData.LITELLM_EMBEDDING_MODEL || 'code';
// Use litellm backend with env-configured model, full rebuild (incremental=false)
initCodexLensIndex('vector', embeddingModel, 'litellm', 4, false);
} catch (err) {
// Fallback to default model if env fetch fails
initCodexLensIndex('vector', 'code', 'litellm', 4, false);
}
}
/**
* Run Vector incremental update
* Generates embeddings for new/changed files only
*/
window.runVectorIncrementalUpdate = async function runVectorIncrementalUpdate() {
var projectPath = window.CCW_PROJECT_ROOT || '.';
showRefreshToast(t('codexlens.startingVectorIncremental') || 'Starting Vector incremental update...', 'info');
try {
// Fetch env settings to get the configured embedding model
var envResponse = await fetch('/api/codexlens/env');
var envData = await envResponse.json();
var embeddingModel = envData.CODEXLENS_EMBEDDING_MODEL || envData.LITELLM_EMBEDDING_MODEL || null;
// Use embeddings endpoint for vector incremental
var requestBody = {
path: projectPath,
incremental: true, // Only new/changed files
backend: 'litellm',
maxWorkers: 4
};
// Add model if configured in env
if (embeddingModel) {
requestBody.model = embeddingModel;
}
var response = await fetch('/api/codexlens/embeddings/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
var result = await response.json();
if (result.success) {
var stats = result.result || {};
var msg = (t('codexlens.vectorIncrementalComplete') || 'Vector incremental completed') +
(stats.chunks_created ? ': ' + stats.chunks_created + ' chunks' : '');
showRefreshToast(msg, 'success');
renderCodexLensManager();
} else {
showRefreshToast((t('codexlens.vectorIncrementalFailed') || 'Vector incremental failed') + ': ' + (result.error || 'Unknown error'), 'error');
}
} catch (err) {
showRefreshToast((t('common.error') || 'Error') + ': ' + err.message, 'error');
}
}
/**
* Run incremental update on the current workspace index
*/

View File

@@ -228,15 +228,31 @@ function renderMemoryCard(memory) {
const updatedDate = memory.updated_at ? new Date(memory.updated_at).toLocaleString() : createdDate;
const isArchived = memory.archived || false;
const metadata = memory.metadata || {};
// Parse metadata - it may be double-encoded JSON string from the backend
let metadata = {};
if (memory.metadata) {
try {
let parsed = typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata;
// Handle double-encoded JSON (string within string)
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
metadata = parsed;
console.log('[DEBUG] Memory', memory.id, 'metadata parsed:', metadata, 'favorite:', metadata.favorite);
} catch (e) {
console.warn('Failed to parse memory metadata:', e);
}
}
const tags = metadata.tags || [];
const priority = metadata.priority || 'medium';
const isFavorite = metadata.favorite === true;
console.log('[DEBUG] Memory', memory.id, 'isFavorite:', isFavorite);
return `
<div class="memory-card ${isArchived ? 'archived' : ''}" data-memory-id="${memory.id}" onclick="viewMemoryDetail('${memory.id}')">
<div class="memory-card-header">
<div class="memory-id">
${metadata.favorite ? '<i data-lucide="star"></i>' : ''}
${isFavorite ? '<i data-lucide="star" class="favorite-star"></i>' : ''}
<span>${memory.id}</span>
${isArchived ? `<span class="badge badge-archived">${t('common.archived')}</span>` : ''}
${priority !== 'medium' ? `<span class="badge badge-priority-${priority}">${priority}</span>` : ''}
@@ -245,7 +261,7 @@ function renderMemoryCard(memory) {
<button class="icon-btn" onclick="editMemory('${memory.id}')" title="${t('common.edit')}">
<i data-lucide="edit"></i>
</button>
<button class="icon-btn ${metadata.favorite ? 'favorite-active' : ''}" onclick="toggleFavorite('${memory.id}')" title="${t('coreMemory.toggleFavorite') || 'Toggle Favorite'}">
<button class="icon-btn ${isFavorite ? 'favorite-active' : ''}" onclick="toggleFavorite('${memory.id}')" title="${t('coreMemory.toggleFavorite') || 'Toggle Favorite'}">
<i data-lucide="star"></i>
</button>
${!isArchived
@@ -312,7 +328,8 @@ function renderMemoryCard(memory) {
// API Functions
async function fetchCoreMemories(archived = false) {
try {
const response = await fetch(`/api/core-memory/memories?path=${encodeURIComponent(projectPath)}&archived=${archived}`);
// Add timestamp to prevent browser caching
const response = await fetch(`/api/core-memory/memories?path=${encodeURIComponent(projectPath)}&archived=${archived}&_t=${Date.now()}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data.memories || [];
@@ -325,7 +342,8 @@ async function fetchCoreMemories(archived = false) {
async function fetchMemoryById(memoryId) {
try {
const response = await fetch(`/api/core-memory/memories/${memoryId}?path=${encodeURIComponent(projectPath)}`);
// Add timestamp to prevent browser caching
const response = await fetch(`/api/core-memory/memories/${memoryId}?path=${encodeURIComponent(projectPath)}&_t=${Date.now()}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
return data.memory || null;
@@ -356,7 +374,9 @@ async function editMemory(memoryId) {
document.getElementById('memoryModalTitle').textContent = t('coreMemory.edit');
document.getElementById('memoryContent').value = memory.content || '';
document.getElementById('memorySummary').value = memory.summary || '';
document.getElementById('memoryMetadata').value = memory.metadata ? JSON.stringify(memory.metadata, null, 2) : '';
document.getElementById('memoryMetadata').value = memory.metadata
? (typeof memory.metadata === 'string' ? memory.metadata : JSON.stringify(memory.metadata, null, 2))
: '';
modal.dataset.editId = memoryId;
modal.style.display = 'flex';
lucide.createIcons();
@@ -523,13 +543,23 @@ async function viewMemoryDetail(memoryId) {
<pre class="detail-code">${escapeHtml(memory.content)}</pre>
</div>
${memory.metadata && Object.keys(memory.metadata).length > 0
? `<div class="detail-section">
${(() => {
if (!memory.metadata) return '';
try {
let metadataObj = typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata;
// Handle double-encoded JSON
if (typeof metadataObj === 'string') {
metadataObj = JSON.parse(metadataObj);
}
if (Object.keys(metadataObj).length === 0) return '';
return `<div class="detail-section">
<h3>${t('coreMemory.metadata')}</h3>
<pre class="detail-code">${escapeHtml(JSON.stringify(memory.metadata, null, 2))}</pre>
</div>`
: ''
}
<pre class="detail-code">${escapeHtml(JSON.stringify(metadataObj, null, 2))}</pre>
</div>`;
} catch (e) {
return '';
}
})()}
${memory.raw_output
? `<div class="detail-section">
@@ -644,7 +674,19 @@ function showClustersView() {
// Favorites Functions
async function refreshFavorites() {
const allMemories = await fetchCoreMemories(false);
const favorites = allMemories.filter(m => m.metadata && m.metadata.favorite);
const favorites = allMemories.filter(m => {
if (!m.metadata) return false;
try {
let parsed = typeof m.metadata === 'string' ? JSON.parse(m.metadata) : m.metadata;
// Handle double-encoded JSON
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
return parsed.favorite === true;
} catch (e) {
return false;
}
});
const countEl = document.getElementById('totalFavoritesCount');
const gridEl = document.getElementById('favoritesGridContent');
@@ -670,7 +712,7 @@ async function refreshFavorites() {
async function showMemoryRelations(memoryId) {
try {
// Fetch all clusters
const response = await fetch(`/api/core-memory/clusters?path=${encodeURIComponent(projectPath)}`);
const response = await fetch(`/api/core-memory/clusters?path=${encodeURIComponent(projectPath)}&_t=${Date.now()}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
@@ -679,7 +721,7 @@ async function showMemoryRelations(memoryId) {
// Find clusters containing this memory
const relatedClusters = [];
for (const cluster of clusters) {
const detailRes = await fetch(`/api/core-memory/clusters/${cluster.id}?path=${encodeURIComponent(projectPath)}`);
const detailRes = await fetch(`/api/core-memory/clusters/${cluster.id}?path=${encodeURIComponent(projectPath)}&_t=${Date.now()}`);
if (detailRes.ok) {
const detail = await detailRes.json();
const members = detail.members || [];
@@ -749,7 +791,20 @@ async function toggleFavorite(memoryId) {
const memory = await fetchMemoryById(memoryId);
if (!memory) return;
const metadata = memory.metadata || {};
// Parse metadata - it may be double-encoded JSON string from the backend
let metadata = {};
if (memory.metadata) {
try {
let parsed = typeof memory.metadata === 'string' ? JSON.parse(memory.metadata) : memory.metadata;
// Handle double-encoded JSON
if (typeof parsed === 'string') {
parsed = JSON.parse(parsed);
}
metadata = parsed;
} catch (e) {
console.warn('Failed to parse memory metadata:', e);
}
}
metadata.favorite = !metadata.favorite;
const response = await fetch('/api/core-memory/memories', {