feat: Implement CLAUDE.md Manager View with file tree, viewer, and metadata actions

- Added main JavaScript functionality for CLAUDE.md management including file loading, rendering, and editing capabilities.
- Created a test HTML file to validate the functionality of the CLAUDE.md manager.
- Introduced CLI generation examples and documentation for rules creation via CLI.
- Enhanced error handling and notifications for file operations.
This commit is contained in:
catlog22
2025-12-14 23:08:36 +08:00
parent 0529b57694
commit d91477ad80
30 changed files with 7961 additions and 298 deletions

View File

@@ -184,6 +184,9 @@ function renderCliStatus() {
</button>`
: `<button class="btn-sm btn-outline flex items-center gap-1" onclick="initCodexLensIndex()">
<i data-lucide="database" class="w-3 h-3"></i> Init Index
</button>
<button class="btn-sm btn-outline flex items-center gap-1" onclick="uninstallCodexLens()">
<i data-lucide="trash-2" class="w-3 h-3"></i> Uninstall
</button>`
}
</div>
@@ -379,8 +382,121 @@ async function refreshAllCliStatus() {
renderCliStatus();
}
async function installCodexLens() {
showRefreshToast('Installing CodexLens...', 'info');
function installCodexLens() {
openCodexLensInstallWizard();
}
function openCodexLensInstallWizard() {
const modal = document.createElement('div');
modal.id = 'codexlensInstallModal';
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i data-lucide="database" class="w-5 h-5 text-primary"></i>
</div>
<div>
<h3 class="text-lg font-semibold">Install CodexLens</h3>
<p class="text-sm text-muted-foreground">Python-based code indexing engine</p>
</div>
</div>
<div class="space-y-4">
<div class="bg-muted/50 rounded-lg p-4">
<h4 class="font-medium mb-2">What will be installed:</h4>
<ul class="text-sm space-y-2 text-muted-foreground">
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>Python virtual environment</strong> - Isolated Python environment</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>CodexLens package</strong> - Code indexing and search engine</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-4 h-4 text-success mt-0.5"></i>
<span><strong>SQLite FTS5</strong> - Full-text search database</span>
</li>
</ul>
</div>
<div class="bg-primary/5 border border-primary/20 rounded-lg p-3">
<div class="flex items-start gap-2">
<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>
<div class="text-sm text-muted-foreground">
<p class="font-medium text-foreground">Installation Location</p>
<p class="mt-1"><code class="bg-muted px-1 rounded">~/.codexlens/venv</code></p>
<p class="mt-1">First installation may take 2-3 minutes to download and setup Python packages.</p>
</div>
</div>
</div>
<div id="codexlensInstallProgress" class="hidden">
<div class="flex items-center gap-3">
<div class="animate-spin w-5 h-5 border-2 border-primary border-t-transparent rounded-full"></div>
<span class="text-sm" id="codexlensInstallStatus">Starting installation...</span>
</div>
<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">
<div id="codexlensProgressBar" class="h-full bg-primary transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">
<button class="btn-outline px-4 py-2" onclick="closeCodexLensInstallWizard()">Cancel</button>
<button id="codexlensInstallBtn" class="btn-primary px-4 py-2" onclick="startCodexLensInstall()">
<i data-lucide="download" class="w-4 h-4 mr-2"></i>
Install Now
</button>
</div>
</div>
`;
document.body.appendChild(modal);
if (window.lucide) {
lucide.createIcons();
}
}
function closeCodexLensInstallWizard() {
const modal = document.getElementById('codexlensInstallModal');
if (modal) {
modal.remove();
}
}
async function startCodexLensInstall() {
const progressDiv = document.getElementById('codexlensInstallProgress');
const installBtn = document.getElementById('codexlensInstallBtn');
const statusText = document.getElementById('codexlensInstallStatus');
const progressBar = document.getElementById('codexlensProgressBar');
// Show progress, disable button
progressDiv.classList.remove('hidden');
installBtn.disabled = true;
installBtn.innerHTML = '<span class="animate-pulse">Installing...</span>';
// Simulate progress stages
const stages = [
{ progress: 10, text: 'Creating virtual environment...' },
{ progress: 30, text: 'Installing pip packages...' },
{ progress: 50, text: 'Installing CodexLens package...' },
{ progress: 70, text: 'Setting up Python dependencies...' },
{ progress: 90, text: 'Finalizing installation...' }
];
let currentStage = 0;
const progressInterval = setInterval(() => {
if (currentStage < stages.length) {
statusText.textContent = stages[currentStage].text;
progressBar.style.width = `${stages[currentStage].progress}%`;
currentStage++;
}
}, 1500);
try {
const response = await fetch('/api/codexlens/bootstrap', {
@@ -389,40 +505,288 @@ async function installCodexLens() {
body: JSON.stringify({})
});
clearInterval(progressInterval);
const result = await response.json();
if (result.success) {
showRefreshToast('CodexLens installed successfully!', 'success');
await loadCodexLensStatus();
renderCliStatus();
progressBar.style.width = '100%';
statusText.textContent = 'Installation complete!';
setTimeout(() => {
closeCodexLensInstallWizard();
showRefreshToast('CodexLens installed successfully!', 'success');
loadCodexLensStatus().then(() => renderCliStatus());
}, 1000);
} else {
showRefreshToast(`Install failed: ${result.error}`, 'error');
statusText.textContent = `Error: ${result.error}`;
progressBar.classList.add('bg-destructive');
installBtn.disabled = false;
installBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
if (window.lucide) lucide.createIcons();
}
} catch (err) {
showRefreshToast(`Install error: ${err.message}`, 'error');
clearInterval(progressInterval);
statusText.textContent = `Error: ${err.message}`;
progressBar.classList.add('bg-destructive');
installBtn.disabled = false;
installBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
if (window.lucide) lucide.createIcons();
}
}
function uninstallCodexLens() {
openCodexLensUninstallWizard();
}
function openCodexLensUninstallWizard() {
const modal = document.createElement('div');
modal.id = 'codexlensUninstallModal';
modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
modal.innerHTML = `
<div class="bg-card rounded-lg shadow-xl w-full max-w-md mx-4 overflow-hidden">
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-destructive/10 flex items-center justify-center">
<i data-lucide="trash-2" class="w-5 h-5 text-destructive"></i>
</div>
<div>
<h3 class="text-lg font-semibold">Uninstall CodexLens</h3>
<p class="text-sm text-muted-foreground">Remove CodexLens and all data</p>
</div>
</div>
<div class="space-y-4">
<div class="bg-destructive/5 border border-destructive/20 rounded-lg p-4">
<h4 class="font-medium text-destructive mb-2">What will be removed:</h4>
<ul class="text-sm space-y-2 text-muted-foreground">
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>
<span>Virtual environment at <code class="bg-muted px-1 rounded">~/.codexlens/venv</code></span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>
<span>All CodexLens indexed data and databases</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-4 h-4 text-destructive mt-0.5"></i>
<span>Configuration and semantic search models</span>
</li>
</ul>
</div>
<div class="bg-warning/10 border border-warning/20 rounded-lg p-3">
<div class="flex items-start gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4 text-warning mt-0.5"></i>
<div class="text-sm">
<p class="font-medium text-warning">Warning</p>
<p class="text-muted-foreground">This action cannot be undone. All indexed code data will be permanently deleted.</p>
</div>
</div>
</div>
<div id="codexlensUninstallProgress" class="hidden">
<div class="flex items-center gap-3">
<div class="animate-spin w-5 h-5 border-2 border-destructive border-t-transparent rounded-full"></div>
<span class="text-sm" id="codexlensUninstallStatus">Removing files...</span>
</div>
<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">
<div id="codexlensUninstallProgressBar" class="h-full bg-destructive transition-all duration-300" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">
<button class="btn-outline px-4 py-2" onclick="closeCodexLensUninstallWizard()">Cancel</button>
<button id="codexlensUninstallBtn" class="btn-destructive px-4 py-2" onclick="startCodexLensUninstall()">
<i data-lucide="trash-2" class="w-4 h-4 mr-2"></i>
Uninstall
</button>
</div>
</div>
`;
document.body.appendChild(modal);
if (window.lucide) {
lucide.createIcons();
}
}
function closeCodexLensUninstallWizard() {
const modal = document.getElementById('codexlensUninstallModal');
if (modal) {
modal.remove();
}
}
async function startCodexLensUninstall() {
const progressDiv = document.getElementById('codexlensUninstallProgress');
const uninstallBtn = document.getElementById('codexlensUninstallBtn');
const statusText = document.getElementById('codexlensUninstallStatus');
const progressBar = document.getElementById('codexlensUninstallProgressBar');
// Show progress, disable button
progressDiv.classList.remove('hidden');
uninstallBtn.disabled = true;
uninstallBtn.innerHTML = '<span class="animate-pulse">Uninstalling...</span>';
// Simulate progress stages
const stages = [
{ progress: 25, text: 'Removing virtual environment...' },
{ progress: 50, text: 'Deleting indexed data...' },
{ progress: 75, text: 'Cleaning up configuration...' },
{ progress: 90, text: 'Finalizing removal...' }
];
let currentStage = 0;
const progressInterval = setInterval(() => {
if (currentStage < stages.length) {
statusText.textContent = stages[currentStage].text;
progressBar.style.width = `${stages[currentStage].progress}%`;
currentStage++;
}
}, 500);
try {
const response = await fetch('/api/codexlens/uninstall', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
clearInterval(progressInterval);
const result = await response.json();
if (result.success) {
progressBar.style.width = '100%';
statusText.textContent = 'Uninstallation complete!';
setTimeout(() => {
closeCodexLensUninstallWizard();
showRefreshToast('CodexLens uninstalled successfully!', 'success');
loadCodexLensStatus().then(() => renderCliStatus());
}, 1000);
} else {
statusText.textContent = `Error: ${result.error}`;
progressBar.classList.remove('bg-destructive');
progressBar.classList.add('bg-destructive');
uninstallBtn.disabled = false;
uninstallBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
if (window.lucide) lucide.createIcons();
}
} catch (err) {
clearInterval(progressInterval);
statusText.textContent = `Error: ${err.message}`;
progressBar.classList.remove('bg-destructive');
progressBar.classList.add('bg-destructive');
uninstallBtn.disabled = false;
uninstallBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4 mr-2"></i> Retry';
if (window.lucide) lucide.createIcons();
}
}
async function initCodexLensIndex() {
// Get current workspace path from multiple sources
let targetPath = null;
// Helper function to check if path is valid
const isValidPath = (path) => {
return path && typeof path === 'string' && path.length > 0 &&
(path.includes('/') || path.includes('\\')) &&
!path.startsWith('{{') && !path.endsWith('}}');
};
console.log('[CodexLens] Attempting to get project path...');
// Try 1: Global projectPath variable
if (isValidPath(projectPath)) {
targetPath = projectPath;
console.log('[CodexLens] ✓ Using global projectPath:', targetPath);
}
// Try 2: Get from workflowData
if (!targetPath && typeof workflowData !== 'undefined' && workflowData && isValidPath(workflowData.projectPath)) {
targetPath = workflowData.projectPath;
console.log('[CodexLens] ✓ Using workflowData.projectPath:', targetPath);
}
// Try 3: Get from current path display element
if (!targetPath) {
const currentPathEl = document.getElementById('currentPath');
if (currentPathEl && currentPathEl.textContent) {
const pathText = currentPathEl.textContent.trim();
if (isValidPath(pathText)) {
targetPath = pathText;
console.log('[CodexLens] ✓ Using currentPath element text:', targetPath);
}
}
}
// Final validation
if (!targetPath) {
showRefreshToast('Error: No workspace loaded. Please open a workspace first.', 'error');
console.error('[CodexLens] No valid project path available');
console.error('[CodexLens] Attempted sources: projectPath:', projectPath, 'workflowData:', workflowData);
return;
}
showRefreshToast('Initializing CodexLens index...', 'info');
console.log('[CodexLens] Initializing index for path:', targetPath);
try {
const response = await fetch('/api/codexlens/init', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: projectPath })
body: JSON.stringify({ path: targetPath })
});
const result = await response.json();
console.log('[CodexLens] Init result:', result);
if (result.success) {
const data = result.result?.result || result.result || result;
let data = null;
// Try to parse nested JSON in output field
if (result.output && typeof result.output === 'string') {
try {
// Extract JSON from output (it may contain other text before the JSON)
const jsonMatch = result.output.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
data = parsed.result || parsed;
console.log('[CodexLens] Parsed from output:', data);
}
} catch (e) {
console.warn('[CodexLens] Failed to parse output as JSON:', e);
}
}
// Fallback to direct result field
if (!data) {
data = result.result?.result || result.result || result;
}
const files = data.files_indexed || 0;
const dirs = data.dirs_indexed || 0;
const symbols = data.symbols_indexed || 0;
showRefreshToast(`Index created: ${files} files, ${symbols} symbols`, 'success');
console.log('[CodexLens] Parsed data:', { files, dirs, symbols });
if (files === 0 && dirs === 0) {
showRefreshToast(`Warning: No files indexed. Path: ${targetPath}`, 'warning');
console.warn('[CodexLens] No files indexed. Full data:', data);
} else {
showRefreshToast(`Index created: ${files} files, ${dirs} directories`, 'success');
console.log('[CodexLens] Index created successfully');
}
} else {
showRefreshToast(`Init failed: ${result.error}`, 'error');
console.error('[CodexLens] Init error:', result.error);
}
} catch (err) {
showRefreshToast(`Init error: ${err.message}`, 'error');
console.error('[CodexLens] Exception:', err);
}
}
@@ -591,7 +955,7 @@ function openSemanticSettingsModal() {
tool.charAt(0).toUpperCase() + tool.slice(1) + '</option>';
}).join('');
const fallbackOptions = '<option value="">None</option>' + availableTools.map(function(tool) {
const fallbackOptions = '<option value="">' + t('semantic.none') + '</option>' + availableTools.map(function(tool) {
return '<option value="' + tool + '"' + (llmEnhancementSettings.fallbackTool === tool ? ' selected' : '') + '>' +
tool.charAt(0).toUpperCase() + tool.slice(1) + '</option>';
}).join('');
@@ -607,16 +971,16 @@ function openSemanticSettingsModal() {
'<i data-lucide="sparkles" class="w-5 h-5 text-primary"></i>' +
'</div>' +
'<div>' +
'<h3 class="text-lg font-semibold">Semantic Search Settings</h3>' +
'<p class="text-sm text-muted-foreground">Configure LLM enhancement for semantic indexing</p>' +
'<h3 class="text-lg font-semibold">' + t('semantic.settings') + '</h3>' +
'<p class="text-sm text-muted-foreground">' + t('semantic.configDesc') + '</p>' +
'</div>' +
'</div>' +
'<div class="space-y-4">' +
'<div class="flex items-center justify-between p-4 bg-muted/50 rounded-lg">' +
'<div>' +
'<h4 class="font-medium flex items-center gap-2">' +
'<i data-lucide="brain" class="w-4 h-4"></i>LLM Enhancement</h4>' +
'<p class="text-sm text-muted-foreground mt-1">Use LLM to generate code summaries for better semantic search</p>' +
'<i data-lucide="brain" class="w-4 h-4"></i>' + t('semantic.llmEnhancement') + '</h4>' +
'<p class="text-sm text-muted-foreground mt-1">' + t('semantic.llmDesc') + '</p>' +
'</div>' +
'<label class="cli-toggle">' +
'<input type="checkbox" id="llmEnhancementToggle" ' + (llmEnhancementSettings.enabled ? 'checked' : '') +
@@ -628,29 +992,29 @@ function openSemanticSettingsModal() {
'<div class="grid grid-cols-2 gap-4">' +
'<div>' +
'<label class="block text-sm font-medium mb-2">' +
'<i data-lucide="cpu" class="w-3 h-3 inline mr-1"></i>Primary LLM Tool</label>' +
'<i data-lucide="cpu" class="w-3 h-3 inline mr-1"></i>' + t('semantic.primaryTool') + '</label>' +
'<select class="cli-setting-select w-full" id="llmToolSelect" onchange="updateLlmTool(this.value)" ' + disabled + '>' + toolOptions + '</select>' +
'</div>' +
'<div>' +
'<label class="block text-sm font-medium mb-2">' +
'<i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1"></i>Fallback Tool</label>' +
'<i data-lucide="refresh-cw" class="w-3 h-3 inline mr-1"></i>' + t('semantic.fallbackTool') + '</label>' +
'<select class="cli-setting-select w-full" id="llmFallbackSelect" onchange="updateLlmFallback(this.value)" ' + disabled + '>' + fallbackOptions + '</select>' +
'</div>' +
'</div>' +
'<div class="grid grid-cols-2 gap-4">' +
'<div>' +
'<label class="block text-sm font-medium mb-2">' +
'<i data-lucide="layers" class="w-3 h-3 inline mr-1"></i>Batch Size</label>' +
'<i data-lucide="layers" class="w-3 h-3 inline mr-1"></i>' + t('semantic.batchSize') + '</label>' +
'<select class="cli-setting-select w-full" id="llmBatchSelect" onchange="updateLlmBatchSize(this.value)" ' + disabled + '>' +
'<option value="1"' + (llmEnhancementSettings.batchSize === 1 ? ' selected' : '') + '>1 file</option>' +
'<option value="3"' + (llmEnhancementSettings.batchSize === 3 ? ' selected' : '') + '>3 files</option>' +
'<option value="5"' + (llmEnhancementSettings.batchSize === 5 ? ' selected' : '') + '>5 files</option>' +
'<option value="10"' + (llmEnhancementSettings.batchSize === 10 ? ' selected' : '') + '>10 files</option>' +
'<option value="1"' + (llmEnhancementSettings.batchSize === 1 ? ' selected' : '') + '>1 ' + t('semantic.file') + '</option>' +
'<option value="3"' + (llmEnhancementSettings.batchSize === 3 ? ' selected' : '') + '>3 ' + t('semantic.files') + '</option>' +
'<option value="5"' + (llmEnhancementSettings.batchSize === 5 ? ' selected' : '') + '>5 ' + t('semantic.files') + '</option>' +
'<option value="10"' + (llmEnhancementSettings.batchSize === 10 ? ' selected' : '') + '>10 ' + t('semantic.files') + '</option>' +
'</select>' +
'</div>' +
'<div>' +
'<label class="block text-sm font-medium mb-2">' +
'<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>Timeout</label>' +
'<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>' + t('semantic.timeout') + '</label>' +
'<select class="cli-setting-select w-full" id="llmTimeoutSelect" onchange="updateLlmTimeout(this.value)" ' + disabled + '>' +
'<option value="60000"' + (llmEnhancementSettings.timeoutMs === 60000 ? ' selected' : '') + '>1 min</option>' +
'<option value="180000"' + (llmEnhancementSettings.timeoutMs === 180000 ? ' selected' : '') + '>3 min</option>' +
@@ -664,26 +1028,110 @@ function openSemanticSettingsModal() {
'<div class="flex items-start gap-2">' +
'<i data-lucide="info" class="w-4 h-4 text-primary mt-0.5"></i>' +
'<div class="text-sm text-muted-foreground">' +
'<p>LLM enhancement generates code summaries and keywords for each file, improving semantic search accuracy.</p>' +
'<p class="mt-1">Run <code class="bg-muted px-1 rounded">codex-lens enhance</code> after enabling to process existing files.</p>' +
'<p>' + t('semantic.enhanceInfo') + '</p>' +
'<p class="mt-1">' + t('semantic.enhanceCommand') + ' <code class="bg-muted px-1 rounded">codex-lens enhance</code> ' + t('semantic.enhanceAfterEnable') + '</p>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="flex gap-2 pt-2">' +
'<button class="btn-sm btn-outline flex items-center gap-1 flex-1" onclick="runEnhanceCommand()" ' + disabled + '>' +
'<i data-lucide="zap" class="w-3 h-3"></i>Run Enhance Now</button>' +
'<i data-lucide="zap" class="w-3 h-3"></i>' + t('semantic.runEnhanceNow') + '</button>' +
'<button class="btn-sm btn-outline flex items-center gap-1 flex-1" onclick="viewEnhanceStatus()">' +
'<i data-lucide="bar-chart-2" class="w-3 h-3"></i>View Status</button>' +
'<i data-lucide="bar-chart-2" class="w-3 h-3"></i>' + t('semantic.viewStatus') + '</button>' +
'</div>' +
'<div class="border-t border-border my-4"></div>' +
'<div>' +
'<h4 class="font-medium mb-3 flex items-center gap-2">' +
'<i data-lucide="search" class="w-4 h-4"></i>' + t('semantic.testSearch') + '</h4>' +
'<div class="space-y-3">' +
'<div>' +
'<input type="text" id="semanticSearchInput" class="tool-config-input w-full" ' +
'placeholder="' + t('semantic.searchPlaceholder') + '" />' +
'</div>' +
'<div>' +
'<button class="btn-sm btn-primary w-full" id="runSemanticSearchBtn">' +
'<i data-lucide="search" class="w-3 h-3"></i> ' + t('semantic.runSearch') +
'</button>' +
'</div>' +
'<div id="semanticSearchResults" class="hidden">' +
'<div class="bg-muted/30 rounded-lg p-3 max-h-64 overflow-y-auto">' +
'<div class="flex items-center justify-between mb-2">' +
'<p class="text-sm font-medium">' + t('codexlens.results') + ':</p>' +
'<span id="semanticResultCount" class="text-xs text-muted-foreground"></span>' +
'</div>' +
'<pre id="semanticResultContent" class="text-xs font-mono whitespace-pre-wrap break-all"></pre>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="border-t border-border p-4 flex justify-end gap-3 bg-muted/30">' +
'<button class="btn-outline px-4 py-2" onclick="closeSemanticSettingsModal()">Close</button>' +
'<button class="btn-outline px-4 py-2" onclick="closeSemanticSettingsModal()">' + t('semantic.close') + '</button>' +
'</div>' +
'</div>';
document.body.appendChild(modal);
// Add semantic search button handler
setTimeout(function() {
var runSemanticSearchBtn = document.getElementById('runSemanticSearchBtn');
if (runSemanticSearchBtn) {
runSemanticSearchBtn.onclick = async function() {
var query = document.getElementById('semanticSearchInput').value.trim();
var resultsDiv = document.getElementById('semanticSearchResults');
var resultCount = document.getElementById('semanticResultCount');
var resultContent = document.getElementById('semanticResultContent');
if (!query) {
showRefreshToast(t('codexlens.enterQuery'), 'warning');
return;
}
runSemanticSearchBtn.disabled = true;
runSemanticSearchBtn.innerHTML = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
resultsDiv.classList.add('hidden');
try {
var params = new URLSearchParams({
query: query,
mode: 'semantic',
limit: '10'
});
var response = await fetch('/api/codexlens/search?' + params.toString());
var result = await response.json();
console.log('[Semantic Search Test] Result:', result);
if (result.success) {
var results = result.results || [];
resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount');
resultContent.textContent = JSON.stringify(results, null, 2);
resultsDiv.classList.remove('hidden');
showRefreshToast(t('codexlens.searchCompleted') + ': ' + results.length + ' ' + t('codexlens.resultsCount'), 'success');
} else {
resultContent.textContent = t('common.error') + ': ' + (result.error || t('common.unknownError'));
resultsDiv.classList.remove('hidden');
showRefreshToast(t('codexlens.searchFailed') + ': ' + result.error, 'error');
}
runSemanticSearchBtn.disabled = false;
runSemanticSearchBtn.innerHTML = '<i data-lucide="search" class="w-3 h-3"></i> ' + t('semantic.runSearch');
if (window.lucide) lucide.createIcons();
} catch (err) {
console.error('[Semantic Search Test] Error:', err);
resultContent.textContent = t('common.exception') + ': ' + err.message;
resultsDiv.classList.remove('hidden');
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
runSemanticSearchBtn.disabled = false;
runSemanticSearchBtn.innerHTML = '<i data-lucide="search" class="w-3 h-3"></i> ' + t('semantic.runSearch');
if (window.lucide) lucide.createIcons();
}
};
}
}, 100);
var handleEscape = function(e) {
if (e.key === 'Escape') {
closeSemanticSettingsModal();
@@ -713,37 +1161,37 @@ function toggleLlmEnhancement(enabled) {
}
renderCliStatus();
showRefreshToast('LLM Enhancement ' + (enabled ? 'enabled' : 'disabled'), 'success');
showRefreshToast(t('semantic.llmEnhancement') + ' ' + (enabled ? t('semantic.enabled') : t('semantic.disabled')), 'success');
}
function updateLlmTool(tool) {
llmEnhancementSettings.tool = tool;
localStorage.setItem('ccw-llm-enhancement-tool', tool);
showRefreshToast('Primary LLM tool set to ' + tool, 'success');
showRefreshToast(t('semantic.toolSetTo') + ' ' + tool, 'success');
}
function updateLlmFallback(tool) {
llmEnhancementSettings.fallbackTool = tool;
localStorage.setItem('ccw-llm-enhancement-fallback', tool);
showRefreshToast('Fallback tool set to ' + (tool || 'none'), 'success');
showRefreshToast(t('semantic.fallbackSetTo') + ' ' + (tool || t('semantic.none')), 'success');
}
function updateLlmBatchSize(size) {
llmEnhancementSettings.batchSize = parseInt(size, 10);
localStorage.setItem('ccw-llm-enhancement-batch-size', size);
showRefreshToast('Batch size set to ' + size + ' files', 'success');
showRefreshToast(t('semantic.batchSetTo') + ' ' + size + ' ' + t('semantic.files'), 'success');
}
function updateLlmTimeout(ms) {
llmEnhancementSettings.timeoutMs = parseInt(ms, 10);
localStorage.setItem('ccw-llm-enhancement-timeout', ms);
var mins = parseInt(ms, 10) / 60000;
showRefreshToast('Timeout set to ' + mins + ' minute' + (mins > 1 ? 's' : ''), 'success');
showRefreshToast(t('semantic.timeoutSetTo') + ' ' + mins + ' ' + (mins > 1 ? t('semantic.minutes') : t('semantic.minute')), 'success');
}
async function runEnhanceCommand() {
if (!llmEnhancementSettings.enabled) {
showRefreshToast('Please enable LLM Enhancement first', 'warning');
showRefreshToast(t('semantic.enableFirst'), 'warning');
return;
}

View File

@@ -142,6 +142,59 @@ async function removeMcpServerFromProject(serverName) {
}
}
async function addGlobalMcpServer(serverName, serverConfig) {
try {
const response = await fetch('/api/mcp-add-global-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: serverName,
serverConfig: serverConfig
})
});
if (!response.ok) throw new Error('Failed to add global MCP server');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`Global MCP server "${serverName}" added`, 'success');
}
return result;
} catch (err) {
console.error('Failed to add global MCP server:', err);
showRefreshToast(`Failed to add global MCP server: ${err.message}`, 'error');
return null;
}
}
async function removeGlobalMcpServer(serverName) {
try {
const response = await fetch('/api/mcp-remove-global-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: serverName
})
});
if (!response.ok) throw new Error('Failed to remove global MCP server');
const result = await response.json();
if (result.success) {
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`Global MCP server "${serverName}" removed`, 'success');
}
return result;
} catch (err) {
console.error('Failed to remove global MCP server:', err);
showRefreshToast(`Failed to remove global MCP server: ${err.message}`, 'error');
return null;
}
}
// ========== Badge Update ==========
function updateMcpBadge() {
const badge = document.getElementById('badgeMcpServers');
@@ -260,8 +313,40 @@ function isServerInCurrentProject(serverName) {
return serverName in servers;
}
// Generate install command for MCP server
function generateMcpInstallCommand(serverName, serverConfig, scope = 'project') {
const command = serverConfig.command || '';
const args = serverConfig.args || [];
// Check if it's an npx-based package
if (command === 'npx' && args.length > 0) {
const packageName = args[0];
// Check if it's a scoped package or standard package
if (packageName.startsWith('@') || packageName.includes('/')) {
const scopeFlag = scope === 'global' ? ' --global' : '';
return `claude mcp add ${packageName}${scopeFlag}`;
}
}
// For custom servers, return JSON configuration
const scopeFlag = scope === 'global' ? ' --global' : '';
return `claude mcp add ${serverName}${scopeFlag}`;
}
// Copy install command to clipboard
async function copyMcpInstallCommand(serverName, serverConfig, scope = 'project') {
try {
const command = generateMcpInstallCommand(serverName, serverConfig, scope);
await navigator.clipboard.writeText(command);
showRefreshToast(t('mcp.installCmdCopied'), 'success');
} catch (error) {
console.error('Failed to copy install command:', error);
showRefreshToast(t('mcp.installCmdFailed'), 'error');
}
}
// ========== MCP Create Modal ==========
function openMcpCreateModal() {
function openMcpCreateModal(scope = 'project') {
const modal = document.getElementById('mcpCreateModal');
if (modal) {
modal.classList.remove('hidden');
@@ -276,6 +361,11 @@ function openMcpCreateModal() {
// Clear JSON input
document.getElementById('mcpServerJson').value = '';
document.getElementById('mcpJsonPreview').classList.add('hidden');
// Set scope (global or project)
const scopeSelect = document.getElementById('mcpServerScope');
if (scopeSelect) {
scopeSelect.value = scope;
}
// Focus on name input
document.getElementById('mcpServerName').focus();
// Setup JSON input listener
@@ -427,6 +517,8 @@ async function submitMcpCreateFromForm() {
const command = document.getElementById('mcpServerCommand').value.trim();
const argsText = document.getElementById('mcpServerArgs').value.trim();
const envText = document.getElementById('mcpServerEnv').value.trim();
const scopeSelect = document.getElementById('mcpServerScope');
const scope = scopeSelect ? scopeSelect.value : 'project';
// Validate required fields
if (!name) {
@@ -471,7 +563,7 @@ async function submitMcpCreateFromForm() {
serverConfig.env = env;
}
await createMcpServerWithConfig(name, serverConfig);
await createMcpServerWithConfig(name, serverConfig, scope);
}
async function submitMcpCreateFromJson() {
@@ -550,18 +642,30 @@ async function submitMcpCreateFromJson() {
}
}
async function createMcpServerWithConfig(name, serverConfig) {
async function createMcpServerWithConfig(name, serverConfig, scope = 'project') {
// Submit to API
try {
const response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: name,
serverConfig: serverConfig
})
});
let response;
if (scope === 'global') {
response = await fetch('/api/mcp-add-global-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
serverName: name,
serverConfig: serverConfig
})
});
} else {
response = await fetch('/api/mcp-copy-server', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectPath: projectPath,
serverName: name,
serverConfig: serverConfig
})
});
}
if (!response.ok) throw new Error('Failed to create MCP server');
@@ -570,7 +674,8 @@ async function createMcpServerWithConfig(name, serverConfig) {
closeMcpCreateModal();
await loadMcpConfig();
renderMcpManager();
showRefreshToast(`MCP server "${name}" created successfully`, 'success');
const scopeLabel = scope === 'global' ? 'global' : 'project';
showRefreshToast(`MCP server "${name}" created in ${scopeLabel} scope`, 'success');
} else {
showRefreshToast(result.error || 'Failed to create MCP server', 'error');
}

View File

@@ -112,6 +112,8 @@ function initNavigation() {
renderSkillsManager();
} else if (currentView === 'rules-manager') {
renderRulesManager();
} else if (currentView === 'claude-manager') {
renderClaudeManager();
}
});
});
@@ -144,6 +146,8 @@ function updateContentTitle() {
titleEl.textContent = t('title.skillsManager');
} else if (currentView === 'rules-manager') {
titleEl.textContent = t('title.rulesManager');
} else if (currentView === 'claude-manager') {
titleEl.textContent = t('title.claudeManager');
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');

View File

@@ -214,13 +214,13 @@ function handleNotification(data) {
if (typeof handleMemoryUpdated === 'function') {
handleMemoryUpdated(payload);
}
// Force refresh of memory view if active
if (getCurrentView && getCurrentView() === 'memory') {
if (typeof loadMemoryStats === 'function') {
loadMemoryStats().then(function() {
if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
});
}
// Force refresh of memory view
if (typeof loadMemoryStats === 'function') {
loadMemoryStats().then(function() {
if (typeof renderHotspotsColumn === 'function') renderHotspotsColumn();
}).catch(function(err) {
console.error('[Memory] Failed to refresh stats:', err);
});
}
break;
@@ -254,15 +254,142 @@ function handleNotification(data) {
'Memory'
);
}
// Refresh Active Memory status if on memory view
if (getCurrentView && getCurrentView() === 'memory') {
if (typeof loadActiveMemoryStatus === 'function') {
loadActiveMemoryStatus();
}
// Refresh Active Memory status
if (typeof loadActiveMemoryStatus === 'function') {
loadActiveMemoryStatus().catch(function(err) {
console.error('[Active Memory] Failed to refresh status:', err);
});
}
console.log('[Active Memory] Sync completed:', payload);
break;
case 'CLAUDE_FILE_SYNCED':
// Handle CLAUDE.md file sync completion
if (typeof addGlobalNotification === 'function') {
const { path, level, tool, mode } = payload;
const fileName = path.split(/[/\\]/).pop();
addGlobalNotification(
'success',
`${fileName} synced`,
{
'Level': level,
'Tool': tool,
'Mode': mode,
'Time': new Date(payload.timestamp).toLocaleTimeString()
},
'CLAUDE.md'
);
}
// Refresh file list
if (typeof loadClaudeFiles === 'function') {
loadClaudeFiles().then(() => {
// Re-render the view to show updated content
if (typeof renderClaudeManager === 'function') {
renderClaudeManager();
}
}).catch(err => console.error('[CLAUDE.md] Failed to refresh files:', err));
}
console.log('[CLAUDE.md] Sync completed:', payload);
break;
case 'CLI_TOOL_INSTALLED':
// Handle CLI tool installation completion
if (typeof addGlobalNotification === 'function') {
const { tool } = payload;
addGlobalNotification(
'success',
`${tool} installed successfully`,
{
'Tool': tool,
'Time': new Date(payload.timestamp).toLocaleTimeString()
},
'CLI Tools'
);
}
// Refresh CLI manager
if (typeof loadCliToolStatus === 'function') {
loadCliToolStatus().then(() => {
if (typeof renderToolsSection === 'function') {
renderToolsSection();
}
}).catch(err => console.error('[CLI Tools] Failed to refresh status:', err));
}
console.log('[CLI Tools] Installation completed:', payload);
break;
case 'CLI_TOOL_UNINSTALLED':
// Handle CLI tool uninstallation completion
if (typeof addGlobalNotification === 'function') {
const { tool } = payload;
addGlobalNotification(
'success',
`${tool} uninstalled successfully`,
{
'Tool': tool,
'Time': new Date(payload.timestamp).toLocaleTimeString()
},
'CLI Tools'
);
}
// Refresh CLI manager
if (typeof loadCliToolStatus === 'function') {
loadCliToolStatus().then(() => {
if (typeof renderToolsSection === 'function') {
renderToolsSection();
}
}).catch(err => console.error('[CLI Tools] Failed to refresh status:', err));
}
console.log('[CLI Tools] Uninstallation completed:', payload);
break;
case 'CODEXLENS_INSTALLED':
// Handle CodexLens installation completion
if (typeof addGlobalNotification === 'function') {
const { version } = payload;
addGlobalNotification(
'success',
`CodexLens installed successfully`,
{
'Version': version || 'latest',
'Time': new Date(payload.timestamp).toLocaleTimeString()
},
'CodexLens'
);
}
// Refresh CLI status if active
if (typeof loadCodexLensStatus === 'function') {
loadCodexLensStatus().then(() => {
if (typeof renderCliStatus === 'function') {
renderCliStatus();
}
});
}
console.log('[CodexLens] Installation completed:', payload);
break;
case 'CODEXLENS_UNINSTALLED':
// Handle CodexLens uninstallation completion
if (typeof addGlobalNotification === 'function') {
addGlobalNotification(
'success',
`CodexLens uninstalled successfully`,
{
'Time': new Date(payload.timestamp).toLocaleTimeString()
},
'CodexLens'
);
}
// Refresh CLI status if active
if (typeof loadCodexLensStatus === 'function') {
loadCodexLensStatus().then(() => {
if (typeof renderCliStatus === 'function') {
renderCliStatus();
}
});
}
console.log('[CodexLens] Uninstallation completed:', payload);
break;
default:
console.log('[WS] Unknown notification type:', type);
}