mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
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:
764
ccw/src/templates/dashboard-js/views/claude-manager.js
Normal file
764
ccw/src/templates/dashboard-js/views/claude-manager.js
Normal file
@@ -0,0 +1,764 @@
|
||||
// CLAUDE.md Manager View
|
||||
// Three-column layout: File Tree | Viewer/Editor | Metadata & Actions
|
||||
|
||||
// ========== State Management ==========
|
||||
var claudeFilesData = {
|
||||
user: { main: null },
|
||||
project: { main: null },
|
||||
modules: [],
|
||||
summary: { totalFiles: 0, totalSize: 0 }
|
||||
};
|
||||
var selectedFile = null;
|
||||
var isEditMode = false;
|
||||
var isDirty = false;
|
||||
var fileTreeExpanded = {
|
||||
user: true,
|
||||
project: true,
|
||||
modules: {}
|
||||
};
|
||||
var searchQuery = '';
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderClaudeManager() {
|
||||
var container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and search for claude-manager view
|
||||
var statsGrid = document.getElementById('statsGrid');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
if (statsGrid) statsGrid.style.display = 'none';
|
||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="claude-manager-view loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
// Load data
|
||||
await loadClaudeFiles();
|
||||
|
||||
// Render layout
|
||||
container.innerHTML = '<div class="claude-manager-view">' +
|
||||
'<div class="claude-manager-header">' +
|
||||
'<div class="claude-manager-header-left">' +
|
||||
'<h2><i data-lucide="file-code" class="w-5 h-5"></i> ' + t('claudeManager.title') + '</h2>' +
|
||||
'<span class="file-count-badge">' + claudeFilesData.summary.totalFiles + ' ' + t('claudeManager.files') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="claude-manager-header-right">' +
|
||||
'<button class="btn btn-sm btn-primary" onclick="showCreateFileDialog()">' +
|
||||
'<i data-lucide="file-plus" class="w-4 h-4"></i> ' + t('claude.createFile') +
|
||||
'</button>' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="refreshClaudeFiles()">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' + t('common.refresh') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="claude-manager-columns">' +
|
||||
'<div class="claude-manager-column left" id="claude-file-tree"></div>' +
|
||||
'<div class="claude-manager-column center" id="claude-file-viewer"></div>' +
|
||||
'<div class="claude-manager-column right" id="claude-file-metadata"></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Render each column
|
||||
renderFileTree();
|
||||
renderFileViewer();
|
||||
renderFileMetadata();
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Data Loading ==========
|
||||
async function loadClaudeFiles() {
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/scan?path=' + encodeURIComponent(projectPath || ''));
|
||||
if (!res.ok) throw new Error('Failed to load CLAUDE.md files');
|
||||
claudeFilesData = await res.json();
|
||||
updateClaudeBadge(); // Update navigation badge
|
||||
} catch (error) {
|
||||
console.error('Error loading CLAUDE.md files:', error);
|
||||
addGlobalNotification('error', t('claudeManager.loadError'), null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshClaudeFiles() {
|
||||
await loadClaudeFiles();
|
||||
await renderClaudeManager();
|
||||
addGlobalNotification('success', t('claudeManager.refreshed'), null, 'CLAUDE.md');
|
||||
}
|
||||
|
||||
// ========== File Tree Rendering ==========
|
||||
function renderFileTree() {
|
||||
var container = document.getElementById('claude-file-tree');
|
||||
if (!container) return;
|
||||
|
||||
var html = '<div class="file-tree">' +
|
||||
// Search Box
|
||||
'<div class="file-tree-search">' +
|
||||
'<input type="text" id="fileSearchInput" placeholder="' + t('claude.searchPlaceholder') + '" ' +
|
||||
'value="' + escapeHtml(searchQuery) + '" oninput="filterFileTree(this.value)">' +
|
||||
'<i data-lucide="search" class="w-4 h-4"></i>' +
|
||||
'</div>' +
|
||||
renderClaudeFilesTree() +
|
||||
'</div>'; // end file-tree
|
||||
|
||||
container.innerHTML = html;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderClaudeFilesTree() {
|
||||
var html = '<div class="file-tree-section">' +
|
||||
'<div class="file-tree-header" onclick="toggleTreeSection(\'user\')">' +
|
||||
'<i data-lucide="' + (fileTreeExpanded.user ? 'chevron-down' : 'chevron-right') + '" class="w-4 h-4"></i>' +
|
||||
'<i data-lucide="user" class="w-4 h-4 text-orange-500"></i>' +
|
||||
'<span>' + t('claudeManager.userLevel') + '</span>' +
|
||||
'<span class="file-count">' + (claudeFilesData.user.main ? 1 : 0) + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (fileTreeExpanded.user) {
|
||||
// User CLAUDE.md (only main file, no rules)
|
||||
if (claudeFilesData.user.main) {
|
||||
html += renderFileTreeItem(claudeFilesData.user.main, 1);
|
||||
} else {
|
||||
html += '<div class="file-tree-item empty" style="padding-left: 1.5rem;">' +
|
||||
'<i data-lucide="file-x" class="w-4 h-4"></i>' +
|
||||
'<span>' + t('claudeManager.noFile') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>'; // end user section
|
||||
|
||||
// Project section
|
||||
html += '<div class="file-tree-section">' +
|
||||
'<div class="file-tree-header" onclick="toggleTreeSection(\'project\')">' +
|
||||
'<i data-lucide="' + (fileTreeExpanded.project ? 'chevron-down' : 'chevron-right') + '" class="w-4 h-4"></i>' +
|
||||
'<i data-lucide="folder" class="w-4 h-4 text-green-500"></i>' +
|
||||
'<span>' + t('claudeManager.projectLevel') + '</span>' +
|
||||
'<span class="file-count">' + (claudeFilesData.project.main ? 1 : 0) + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (fileTreeExpanded.project) {
|
||||
// Project CLAUDE.md (only main file, no rules)
|
||||
if (claudeFilesData.project.main) {
|
||||
html += renderFileTreeItem(claudeFilesData.project.main, 1);
|
||||
} else {
|
||||
html += '<div class="file-tree-item empty" style="padding-left: 1.5rem;">' +
|
||||
'<i data-lucide="file-x" class="w-4 h-4"></i>' +
|
||||
'<span>' + t('claudeManager.noFile') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
}
|
||||
|
||||
html += '</div>'; // end project section
|
||||
|
||||
// Modules section
|
||||
html += '<div class="file-tree-section">' +
|
||||
'<div class="file-tree-header">' +
|
||||
'<i data-lucide="package" class="w-4 h-4 text-blue-500"></i>' +
|
||||
'<span>' + t('claudeManager.moduleLevel') + '</span>' +
|
||||
'<span class="file-count">' + claudeFilesData.modules.length + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (claudeFilesData.modules.length > 0) {
|
||||
claudeFilesData.modules.forEach(function (file) {
|
||||
html += renderFileTreeItem(file, 1);
|
||||
});
|
||||
} else {
|
||||
html += '<div class="file-tree-item empty" style="padding-left: 1.5rem;">' +
|
||||
'<i data-lucide="file-x" class="w-4 h-4"></i>' +
|
||||
'<span>' + t('claudeManager.noModules') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '</div>'; // end modules section
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderFileTreeItem(file, indentLevel) {
|
||||
var isSelected = selectedFile && selectedFile.id === file.id;
|
||||
var indentPx = indentLevel * 1.5;
|
||||
var safeId = file.id.replace(/'/g, "'");
|
||||
|
||||
return '<div class="file-tree-item' + (isSelected ? ' selected' : '') + '" ' +
|
||||
'onclick="selectClaudeFile(\'' + safeId + '\')" ' +
|
||||
'style="padding-left: ' + indentPx + 'rem;">' +
|
||||
'<i data-lucide="file-text" class="w-4 h-4"></i>' +
|
||||
'<span class="file-name">' + escapeHtml(file.name) + '</span>' +
|
||||
(file.parentDirectory ? '<span class="file-path-hint">' + escapeHtml(file.parentDirectory) + '</span>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function toggleTreeSection(section) {
|
||||
fileTreeExpanded[section] = !fileTreeExpanded[section];
|
||||
renderFileTree();
|
||||
}
|
||||
|
||||
async function selectClaudeFile(fileId) {
|
||||
// Find file in data (only main CLAUDE.md files, no rules)
|
||||
var allFiles = [
|
||||
claudeFilesData.user.main,
|
||||
claudeFilesData.project.main,
|
||||
...claudeFilesData.modules
|
||||
].filter(function (f) { return f !== null; });
|
||||
|
||||
selectedFile = allFiles.find(function (f) { return f.id === fileId; }) || null;
|
||||
|
||||
if (selectedFile) {
|
||||
// Load full content if not already loaded
|
||||
if (!selectedFile.content) {
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/file?path=' + encodeURIComponent(selectedFile.path));
|
||||
if (res.ok) {
|
||||
var data = await res.json();
|
||||
selectedFile.content = data.content;
|
||||
selectedFile.stats = data.stats;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading file content:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderFileTree();
|
||||
renderFileViewer();
|
||||
renderFileMetadata();
|
||||
}
|
||||
|
||||
// ========== File Viewer Rendering ==========
|
||||
function renderFileViewer() {
|
||||
var container = document.getElementById('claude-file-viewer');
|
||||
if (!container) return;
|
||||
|
||||
if (!selectedFile) {
|
||||
container.innerHTML = '<div class="empty-state">' +
|
||||
'<i data-lucide="file-search" class="w-12 h-12 opacity-20"></i>' +
|
||||
'<p>' + t('claudeManager.selectFile') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '<div class="file-viewer">' +
|
||||
'<div class="file-viewer-header">' +
|
||||
'<h3>' + escapeHtml(selectedFile.name) + '</h3>' +
|
||||
'<div class="file-viewer-actions">' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="copyFileContent()" title="' + t('claude.copyContent') + '">' +
|
||||
'<i data-lucide="copy" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'<button class="btn btn-sm btn-secondary" onclick="toggleEditMode()" title="' + t('common.edit') + '">' +
|
||||
'<i data-lucide="' + (isEditMode ? 'eye' : 'edit-2') + '" class="w-4 h-4"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="file-viewer-content">' +
|
||||
(isEditMode ? renderEditor() : renderMarkdownContent(selectedFile.content || '')) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderMarkdownContent(content) {
|
||||
// Check if marked.js is available for enhanced rendering
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
tables: true,
|
||||
smartLists: true,
|
||||
highlight: function(code, lang) {
|
||||
// Check if highlight.js or Prism is available
|
||||
if (typeof hljs !== 'undefined' && lang) {
|
||||
try {
|
||||
return hljs.highlight(code, { language: lang }).value;
|
||||
} catch (e) {
|
||||
return escapeHtml(code);
|
||||
}
|
||||
} else if (typeof Prism !== 'undefined' && lang && Prism.languages[lang]) {
|
||||
return Prism.highlight(code, Prism.languages[lang], lang);
|
||||
}
|
||||
return escapeHtml(code);
|
||||
}
|
||||
});
|
||||
return '<div class="markdown-content">' + marked.parse(content) + '</div>';
|
||||
} catch (e) {
|
||||
console.error('Error rendering markdown with marked.js:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Enhanced basic rendering
|
||||
var html = escapeHtml(content);
|
||||
|
||||
// Headers
|
||||
html = html
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^#### (.*$)/gim, '<h4>$1</h4>');
|
||||
|
||||
// Inline formatting
|
||||
html = html
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
||||
|
||||
// Links
|
||||
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
|
||||
// Task lists
|
||||
html = html
|
||||
.replace(/- \[ \] (.+)$/gim, '<li class="task-list-item"><input type="checkbox" disabled> $1</li>')
|
||||
.replace(/- \[x\] (.+)$/gim, '<li class="task-list-item"><input type="checkbox" disabled checked> $1</li>');
|
||||
|
||||
// Lists
|
||||
html = html.replace(/^- (.+)$/gim, '<li>$1</li>');
|
||||
html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
|
||||
|
||||
// Code blocks
|
||||
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, function(match, lang, code) {
|
||||
return '<pre><code class="language-' + (lang || 'plaintext') + '">' + code + '</code></pre>';
|
||||
});
|
||||
|
||||
// Line breaks
|
||||
html = html.replace(/\n/g, '<br>');
|
||||
|
||||
return '<div class="markdown-content">' + html + '</div>';
|
||||
}
|
||||
|
||||
function renderEditor() {
|
||||
return '<textarea id="claudeFileEditor" class="file-editor" ' +
|
||||
'oninput="markDirty()">' +
|
||||
escapeHtml(selectedFile.content || '') +
|
||||
'</textarea>';
|
||||
}
|
||||
|
||||
function toggleEditMode() {
|
||||
if (isEditMode && isDirty) {
|
||||
if (!confirm(t('claudeManager.unsavedChanges'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isEditMode = !isEditMode;
|
||||
isDirty = false;
|
||||
renderFileViewer();
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
async function saveClaudeFile() {
|
||||
if (!selectedFile || !isEditMode) return;
|
||||
|
||||
var editor = document.getElementById('claudeFileEditor');
|
||||
if (!editor) return;
|
||||
|
||||
var newContent = editor.value;
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: selectedFile.path,
|
||||
content: newContent,
|
||||
createBackup: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to save file');
|
||||
|
||||
selectedFile.content = newContent;
|
||||
selectedFile.stats = calculateFileStats(newContent);
|
||||
isDirty = false;
|
||||
|
||||
addGlobalNotification('success', t('claudeManager.saved'), null, 'CLAUDE.md');
|
||||
renderFileMetadata();
|
||||
} catch (error) {
|
||||
console.error('Error saving file:', error);
|
||||
addGlobalNotification('error', t('claudeManager.saveError'), null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
|
||||
function calculateFileStats(content) {
|
||||
var lines = content.split('\n').length;
|
||||
var words = content.split(/\s+/).filter(function (w) { return w.length > 0; }).length;
|
||||
var characters = content.length;
|
||||
return { lines: lines, words: words, characters: characters };
|
||||
}
|
||||
|
||||
// ========== File Metadata Rendering ==========
|
||||
function renderFileMetadata() {
|
||||
var container = document.getElementById('claude-file-metadata');
|
||||
if (!container) return;
|
||||
|
||||
if (!selectedFile) {
|
||||
container.innerHTML = '<div class="empty-state">' +
|
||||
'<i data-lucide="info" class="w-8 h-8 opacity-20"></i>' +
|
||||
'<p>' + t('claudeManager.noMetadata') + '</p>' +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="file-metadata">' +
|
||||
'<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.fileInfo') + '</h4>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.level') + '</span>' +
|
||||
'<span class="value">' + t('claudeManager.level_' + selectedFile.level) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.path') + '</span>' +
|
||||
'<span class="value path">' + escapeHtml(selectedFile.relativePath) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.size') + '</span>' +
|
||||
'<span class="value">' + formatFileSize(selectedFile.size) + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.modified') + '</span>' +
|
||||
'<span class="value">' + formatDate(selectedFile.lastModified) + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
if (selectedFile.stats) {
|
||||
html += '<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.statistics') + '</h4>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.lines') + '</span>' +
|
||||
'<span class="value">' + selectedFile.stats.lines + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.words') + '</span>' +
|
||||
'<span class="value">' + selectedFile.stats.words + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="metadata-item">' +
|
||||
'<span class="label">' + t('claudeManager.characters') + '</span>' +
|
||||
'<span class="value">' + selectedFile.stats.characters + '</span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
html += '<div class="metadata-section">' +
|
||||
'<h4>' + t('claudeManager.actions') + '</h4>';
|
||||
|
||||
if (isEditMode) {
|
||||
html += '<button class="btn btn-sm btn-primary full-width" onclick="saveClaudeFile()"' +
|
||||
(isDirty ? '' : ' disabled') + '>' +
|
||||
'<i data-lucide="save" class="w-4 h-4"></i> ' + t('common.save') +
|
||||
'</button>';
|
||||
html += '<button class="btn btn-sm btn-secondary full-width" onclick="toggleEditMode()">' +
|
||||
'<i data-lucide="x" class="w-4 h-4"></i> ' + t('common.cancel') +
|
||||
'</button>';
|
||||
} else {
|
||||
html += '<button class="btn btn-sm btn-secondary full-width" onclick="toggleEditMode()">' +
|
||||
'<i data-lucide="edit-2" class="w-4 h-4"></i> ' + t('common.edit') +
|
||||
'</button>';
|
||||
}
|
||||
|
||||
// Delete button (only for CLAUDE.md files, not in edit mode)
|
||||
if (!isEditMode && selectedFile.level !== 'file') {
|
||||
html += '<button class="btn btn-sm btn-danger full-width" onclick="confirmDeleteFile()">' +
|
||||
'<i data-lucide="trash-2" class="w-4 h-4"></i> ' + t('claude.deleteFile') +
|
||||
'</button>';
|
||||
}
|
||||
|
||||
html += '</div>'; // end actions section
|
||||
|
||||
// CLI Sync Panel
|
||||
html += '<div class="metadata-section cli-sync-panel">' +
|
||||
'<div class="panel-header">' +
|
||||
'<i data-lucide="sparkles" class="w-4 h-4"></i>' +
|
||||
'<span>' + (t('claude.cliSync') || 'CLI Auto-Sync') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="sync-config">' +
|
||||
'<label>' + (t('claude.tool') || 'Tool') + '</label>' +
|
||||
'<select id="cliToolSelect" class="sync-select">' +
|
||||
'<option value="gemini">Gemini</option>' +
|
||||
'<option value="qwen">Qwen</option>' +
|
||||
'</select>' +
|
||||
'<label>' + (t('claude.mode') || 'Mode') + '</label>' +
|
||||
'<select id="cliModeSelect" class="sync-select">' +
|
||||
'<option value="update">' + (t('claude.modeUpdate') || 'Update (Smart Merge)') + '</option>' +
|
||||
'<option value="generate">' + (t('claude.modeGenerate') || 'Generate (Full Replace)') + '</option>' +
|
||||
'<option value="append">' + (t('claude.modeAppend') || 'Append') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-sm btn-primary full-width sync-button" onclick="syncFileWithCLI()" id="cliSyncButton">' +
|
||||
'<i data-lucide="refresh-cw" class="w-4 h-4"></i> ' +
|
||||
(t('claude.syncButton') || 'Sync with CLI') +
|
||||
'</button>' +
|
||||
'<div id="syncProgress" class="sync-progress" style="display:none;">' +
|
||||
'<i data-lucide="loader" class="w-4 h-4"></i>' +
|
||||
'<span id="syncProgressText">' + (t('claude.syncing') || 'Analyzing...') + '</span>' +
|
||||
'</div>' +
|
||||
'</div>'; // end cli-sync-panel
|
||||
|
||||
html += '</div>'; // end file-metadata
|
||||
|
||||
container.innerHTML = html;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== CLI Sync Functions ==========
|
||||
async function syncFileWithCLI() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
var tool = document.getElementById('cliToolSelect').value;
|
||||
var mode = document.getElementById('cliModeSelect').value;
|
||||
|
||||
// Show progress
|
||||
showSyncProgress(true, tool);
|
||||
|
||||
// Disable sync button
|
||||
var syncButton = document.getElementById('cliSyncButton');
|
||||
if (syncButton) syncButton.disabled = true;
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/memory/claude/sync', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
level: selectedFile.level,
|
||||
path: selectedFile.level === 'module' ? selectedFile.path.replace(/CLAUDE\.md$/, '').replace(/\/$/, '') : undefined,
|
||||
tool: tool,
|
||||
mode: mode
|
||||
})
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Reload file content
|
||||
var fileData = await loadFileContent(selectedFile.path);
|
||||
if (fileData) {
|
||||
selectedFile = fileData;
|
||||
renderFileViewer();
|
||||
renderFileMetadata();
|
||||
}
|
||||
showClaudeNotification('success', (t('claude.syncSuccess') || 'Synced successfully').replace('{file}', selectedFile.name));
|
||||
} else {
|
||||
showClaudeNotification('error', (t('claude.syncError') || 'Sync failed').replace('{error}', result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('CLI sync error:', error);
|
||||
showClaudeNotification('error', (t('claude.syncError') || 'Sync failed').replace('{error}', error.message));
|
||||
} finally {
|
||||
showSyncProgress(false);
|
||||
if (syncButton) syncButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showSyncProgress(show, tool) {
|
||||
var progressEl = document.getElementById('syncProgress');
|
||||
var progressText = document.getElementById('syncProgressText');
|
||||
if (!progressEl) return;
|
||||
|
||||
if (show) {
|
||||
progressEl.style.display = 'flex';
|
||||
if (progressText) {
|
||||
var text = (t('claude.syncing') || 'Analyzing with {tool}...').replace('{tool}', tool || 'CLI');
|
||||
progressText.textContent = text;
|
||||
}
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} else {
|
||||
progressEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFileContent(filePath) {
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/file?path=' + encodeURIComponent(filePath));
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch (error) {
|
||||
console.error('Error loading file content:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function showClaudeNotification(type, message) {
|
||||
// Use global notification system if available
|
||||
if (typeof addGlobalNotification === 'function') {
|
||||
addGlobalNotification(type, message, null, 'CLAUDE.md');
|
||||
} else {
|
||||
// Fallback to simple alert
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Search Functions ==========
|
||||
function filterFileTree(query) {
|
||||
searchQuery = query.toLowerCase();
|
||||
renderFileTree();
|
||||
|
||||
// Add keyboard shortcut handler
|
||||
if (query && !window.claudeSearchKeyboardHandlerAdded) {
|
||||
document.addEventListener('keydown', handleSearchKeyboard);
|
||||
window.claudeSearchKeyboardHandlerAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearchKeyboard(e) {
|
||||
// Ctrl+F or Cmd+F
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
|
||||
e.preventDefault();
|
||||
var searchInput = document.getElementById('fileSearchInput');
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== File Creation Functions ==========
|
||||
function showCreateFileDialog() {
|
||||
var dialog = '<div class="modal-overlay" onclick="closeCreateDialog()">' +
|
||||
'<div class="create-dialog" onclick="event.stopPropagation()">' +
|
||||
'<h3>' + t('claude.createDialogTitle') + '</h3>' +
|
||||
'<div class="dialog-form">' +
|
||||
'<label>' + t('claude.selectLevel') + '</label>' +
|
||||
'<select id="createLevel" onchange="toggleModulePathInput(this.value)">' +
|
||||
'<option value="user">' + t('claude.levelUser') + '</option>' +
|
||||
'<option value="project">' + t('claude.levelProject') + '</option>' +
|
||||
'<option value="module">' + t('claude.levelModule') + '</option>' +
|
||||
'</select>' +
|
||||
'<label id="modulePathLabel" style="display:none;">' + t('claude.modulePath') + '</label>' +
|
||||
'<input id="modulePath" type="text" style="display:none;" placeholder="e.g., src/components">' +
|
||||
'<label>' + t('claude.selectTemplate') + '</label>' +
|
||||
'<select id="createTemplate">' +
|
||||
'<option value="default">' + t('claude.templateDefault') + '</option>' +
|
||||
'<option value="minimal">' + t('claude.templateMinimal') + '</option>' +
|
||||
'<option value="comprehensive">' + t('claude.templateComprehensive') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="dialog-buttons">' +
|
||||
'<button onclick="closeCreateDialog()" class="btn btn-sm btn-secondary">' + t('common.cancel') + '</button>' +
|
||||
'<button onclick="createNewFile()" class="btn btn-sm btn-primary">' + t('claude.createFile') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', dialog);
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeCreateDialog() {
|
||||
var overlay = document.querySelector('.modal-overlay');
|
||||
if (overlay) overlay.remove();
|
||||
}
|
||||
|
||||
function toggleModulePathInput(level) {
|
||||
var pathLabel = document.getElementById('modulePathLabel');
|
||||
var pathInput = document.getElementById('modulePath');
|
||||
|
||||
if (level === 'module') {
|
||||
pathLabel.style.display = 'block';
|
||||
pathInput.style.display = 'block';
|
||||
} else {
|
||||
pathLabel.style.display = 'none';
|
||||
pathInput.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewFile() {
|
||||
var level = document.getElementById('createLevel').value;
|
||||
var template = document.getElementById('createTemplate').value;
|
||||
var modulePath = document.getElementById('modulePath').value;
|
||||
|
||||
if (level === 'module' && !modulePath) {
|
||||
addGlobalNotification('error', t('claude.modulePathRequired') || 'Module path is required', null, 'CLAUDE.md');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
level: level,
|
||||
path: modulePath || undefined,
|
||||
template: template
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to create file');
|
||||
|
||||
var result = await res.json();
|
||||
closeCreateDialog();
|
||||
addGlobalNotification('success', t('claude.fileCreated') || 'File created successfully', null, 'CLAUDE.md');
|
||||
|
||||
// Refresh file tree
|
||||
await refreshClaudeFiles();
|
||||
} catch (error) {
|
||||
console.error('Error creating file:', error);
|
||||
addGlobalNotification('error', t('claude.createFileError') || 'Failed to create file', null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== File Deletion Functions ==========
|
||||
async function confirmDeleteFile() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
var confirmed = confirm(
|
||||
(t('claude.deleteConfirm') || 'Are you sure you want to delete {file}?').replace('{file}', selectedFile.name) + '\n\n' +
|
||||
'Path: ' + selectedFile.path + '\n\n' +
|
||||
(t('claude.deleteWarning') || 'This action cannot be undone.')
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
var res = await fetch('/api/memory/claude/file?path=' + encodeURIComponent(selectedFile.path) + '&confirm=true', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error('Failed to delete file');
|
||||
|
||||
addGlobalNotification('success', t('claude.fileDeleted') || 'File deleted successfully', null, 'CLAUDE.md');
|
||||
selectedFile = null;
|
||||
|
||||
// Refresh file tree
|
||||
await refreshClaudeFiles();
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
addGlobalNotification('error', t('claude.deleteFileError') || 'Failed to delete file', null, 'CLAUDE.md');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Copy Content Function ==========
|
||||
function copyFileContent() {
|
||||
if (!selectedFile || !selectedFile.content) return;
|
||||
|
||||
navigator.clipboard.writeText(selectedFile.content).then(function() {
|
||||
addGlobalNotification('success', t('claude.contentCopied') || 'Content copied to clipboard', null, 'CLAUDE.md');
|
||||
}).catch(function(error) {
|
||||
console.error('Error copying content:', error);
|
||||
addGlobalNotification('error', t('claude.copyError') || 'Failed to copy content', null, 'CLAUDE.md');
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Utility Functions ==========
|
||||
// Note: escapeHtml and formatDate are imported from utils.js
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// Update navigation badge with total file count
|
||||
function updateClaudeBadge() {
|
||||
var badge = document.getElementById('badgeClaude');
|
||||
if (badge && claudeFilesData && claudeFilesData.summary) {
|
||||
var total = claudeFilesData.summary.totalFiles;
|
||||
badge.textContent = total;
|
||||
}
|
||||
}
|
||||
@@ -207,32 +207,14 @@ function initToolConfigModalEvents(tool, currentConfig, models) {
|
||||
// Install/Uninstall
|
||||
var installBtn = document.getElementById('installBtn');
|
||||
if (installBtn) {
|
||||
installBtn.onclick = async function() {
|
||||
installBtn.onclick = function() {
|
||||
var status = cliToolStatus[tool] || {};
|
||||
var endpoint = status.available ? '/api/cli/uninstall' : '/api/cli/install';
|
||||
var action = status.available ? 'uninstalling' : 'installing';
|
||||
|
||||
showRefreshToast(tool.charAt(0).toUpperCase() + tool.slice(1) + ' ' + action + '...', 'info');
|
||||
closeModal();
|
||||
|
||||
try {
|
||||
var response = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tool: tool })
|
||||
});
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(result.message || (tool + ' ' + (status.available ? 'uninstalled' : 'installed')), 'success');
|
||||
await loadCliToolStatus();
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} else {
|
||||
showRefreshToast(result.error || 'Operation failed', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast('Failed: ' + err.message, 'error');
|
||||
if (status.available) {
|
||||
openCliUninstallWizard(tool);
|
||||
} else {
|
||||
openCliInstallWizard(tool);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -384,20 +366,22 @@ function renderToolsSection() {
|
||||
}).join('');
|
||||
|
||||
// CodexLens item
|
||||
var codexLensHtml = '<div class="tool-item ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '">' +
|
||||
var codexLensHtml = '<div class="tool-item clickable ' + (codexLensStatus.ready ? 'available' : 'unavailable') + '" onclick="showCodexLensConfigModal()">' +
|
||||
'<div class="tool-item-left">' +
|
||||
'<span class="tool-status-dot ' + (codexLensStatus.ready ? 'status-available' : 'status-unavailable') + '"></span>' +
|
||||
'<div class="tool-item-info">' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span></div>' +
|
||||
'<div class="tool-item-name">CodexLens <span class="tool-type-badge">Index</span>' +
|
||||
'<i data-lucide="settings" class="w-3 h-3 tool-config-icon"></i></div>' +
|
||||
'<div class="tool-item-desc">' + (codexLensStatus.ready ? t('cli.codexLensDesc') : t('cli.codexLensDescFull')) + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="tool-item-right">' +
|
||||
(codexLensStatus.ready
|
||||
? '<span class="tool-status-text success"><i data-lucide="check-circle" class="w-3.5 h-3.5"></i> v' + (codexLensStatus.version || 'installed') + '</span>' +
|
||||
'<button class="btn-sm btn-outline" onclick="initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> ' + t('cli.initIndex') + '</button>'
|
||||
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex()"><i data-lucide="database" class="w-3 h-3"></i> ' + t('cli.initIndex') + '</button>' +
|
||||
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()"><i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') + '</button>'
|
||||
: '<span class="tool-status-text muted"><i data-lucide="circle-dashed" class="w-3.5 h-3.5"></i> ' + t('cli.notInstalled') + '</span>' +
|
||||
'<button class="btn-sm btn-primary" onclick="installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
|
||||
'<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()"><i data-lucide="download" class="w-3 h-3"></i> ' + t('cli.install') + '</button>') +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
@@ -1203,3 +1187,607 @@ function handleCliExecutionError(payload) {
|
||||
|
||||
currentCliExecution = null;
|
||||
}
|
||||
|
||||
// ========== CLI Tool Install/Uninstall Wizards ==========
|
||||
function openCliInstallWizard(toolName) {
|
||||
var toolDescriptions = {
|
||||
gemini: 'Google AI for code analysis and generation',
|
||||
qwen: 'Alibaba AI assistant for coding',
|
||||
codex: 'OpenAI code generation and understanding',
|
||||
claude: 'Anthropic AI assistant'
|
||||
};
|
||||
|
||||
var toolPackages = {
|
||||
gemini: '@google/gemini-cli',
|
||||
qwen: '@qwen-code/qwen-code',
|
||||
codex: '@openai/codex',
|
||||
claude: '@anthropic-ai/claude-code'
|
||||
};
|
||||
|
||||
var modal = document.createElement('div');
|
||||
modal.id = 'cliInstallModal';
|
||||
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="download" class="w-5 h-5 text-primary"></i>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<h3 class="text-lg font-semibold">Install ' + toolName.charAt(0).toUpperCase() + toolName.slice(1) + '</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">' + (toolDescriptions[toolName] || 'CLI tool') + '</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>NPM Package:</strong> <code class="bg-muted px-1 rounded">' + (toolPackages[toolName] || toolName) + '</code></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>Global installation</strong> - Available system-wide</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>CLI commands</strong> - Accessible from terminal</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 Method</p>' +
|
||||
'<p class="mt-1">Uses <code class="bg-muted px-1 rounded">npm install -g</code></p>' +
|
||||
'<p class="mt-1">First installation may take 1-2 minutes depending on network speed.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="cliInstallProgress" 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="cliInstallStatus">Starting installation...</span>' +
|
||||
'</div>' +
|
||||
'<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div id="cliInstallProgressBar" 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="closeCliInstallWizard()">Cancel</button>' +
|
||||
'<button id="cliInstallBtn" class="btn-primary px-4 py-2" onclick="startCliInstall(\'' + toolName + '\')">' +
|
||||
'<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 closeCliInstallWizard() {
|
||||
var modal = document.getElementById('cliInstallModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function startCliInstall(toolName) {
|
||||
var progressDiv = document.getElementById('cliInstallProgress');
|
||||
var installBtn = document.getElementById('cliInstallBtn');
|
||||
var statusText = document.getElementById('cliInstallStatus');
|
||||
var progressBar = document.getElementById('cliInstallProgressBar');
|
||||
|
||||
progressDiv.classList.remove('hidden');
|
||||
installBtn.disabled = true;
|
||||
installBtn.innerHTML = '<span class="animate-pulse">Installing...</span>';
|
||||
|
||||
var stages = [
|
||||
{ progress: 20, text: 'Connecting to NPM registry...' },
|
||||
{ progress: 40, text: 'Downloading package...' },
|
||||
{ progress: 60, text: 'Installing dependencies...' },
|
||||
{ progress: 80, text: 'Setting up CLI commands...' },
|
||||
{ progress: 95, text: 'Finalizing installation...' }
|
||||
];
|
||||
|
||||
var currentStage = 0;
|
||||
var progressInterval = setInterval(function() {
|
||||
if (currentStage < stages.length) {
|
||||
statusText.textContent = stages[currentStage].text;
|
||||
progressBar.style.width = stages[currentStage].progress + '%';
|
||||
currentStage++;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/cli/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tool: toolName })
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
progressBar.style.width = '100%';
|
||||
statusText.textContent = 'Installation complete!';
|
||||
|
||||
setTimeout(function() {
|
||||
closeCliInstallWizard();
|
||||
showRefreshToast(toolName + ' installed successfully!', 'success');
|
||||
loadCliToolStatus().then(function() {
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
});
|
||||
}, 1000);
|
||||
} else {
|
||||
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) {
|
||||
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 openCliUninstallWizard(toolName) {
|
||||
var modal = document.createElement('div');
|
||||
modal.id = 'cliUninstallModal';
|
||||
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 ' + toolName.charAt(0).toUpperCase() + toolName.slice(1) + '</h3>' +
|
||||
'<p class="text-sm text-muted-foreground">Remove CLI tool from system</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>Global NPM package</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>CLI commands and executables</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>Tool configuration (if any)</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">Note</p>' +
|
||||
'<p class="text-muted-foreground">You can reinstall this tool anytime from the CLI Manager.</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="cliUninstallProgress" 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="cliUninstallStatus">Removing package...</span>' +
|
||||
'</div>' +
|
||||
'<div class="mt-2 h-2 bg-muted rounded-full overflow-hidden">' +
|
||||
'<div id="cliUninstallProgressBar" 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="closeCliUninstallWizard()">Cancel</button>' +
|
||||
'<button id="cliUninstallBtn" class="btn-destructive px-4 py-2" onclick="startCliUninstall(\'' + toolName + '\')">' +
|
||||
'<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 closeCliUninstallWizard() {
|
||||
var modal = document.getElementById('cliUninstallModal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function startCliUninstall(toolName) {
|
||||
var progressDiv = document.getElementById('cliUninstallProgress');
|
||||
var uninstallBtn = document.getElementById('cliUninstallBtn');
|
||||
var statusText = document.getElementById('cliUninstallStatus');
|
||||
var progressBar = document.getElementById('cliUninstallProgressBar');
|
||||
|
||||
progressDiv.classList.remove('hidden');
|
||||
uninstallBtn.disabled = true;
|
||||
uninstallBtn.innerHTML = '<span class="animate-pulse">Uninstalling...</span>';
|
||||
|
||||
var stages = [
|
||||
{ progress: 33, text: 'Removing package files...' },
|
||||
{ progress: 66, text: 'Cleaning up dependencies...' },
|
||||
{ progress: 90, text: 'Finalizing removal...' }
|
||||
];
|
||||
|
||||
var currentStage = 0;
|
||||
var progressInterval = setInterval(function() {
|
||||
if (currentStage < stages.length) {
|
||||
statusText.textContent = stages[currentStage].text;
|
||||
progressBar.style.width = stages[currentStage].progress + '%';
|
||||
currentStage++;
|
||||
}
|
||||
}, 500);
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/cli/uninstall', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tool: toolName })
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
progressBar.style.width = '100%';
|
||||
statusText.textContent = 'Uninstallation complete!';
|
||||
|
||||
setTimeout(function() {
|
||||
closeCliUninstallWizard();
|
||||
showRefreshToast(toolName + ' uninstalled successfully!', 'success');
|
||||
loadCliToolStatus().then(function() {
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
});
|
||||
}, 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();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== CodexLens Configuration Modal ==========
|
||||
async function showCodexLensConfigModal() {
|
||||
var loadingContent = '<div class="text-center py-8">' +
|
||||
'<div class="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full mx-auto mb-4"></div>' +
|
||||
'<p class="text-muted-foreground">' + t('codexlens.loadingConfig') + '</p>' +
|
||||
'</div>';
|
||||
|
||||
showModal(t('codexlens.config'), loadingContent, { size: 'md' });
|
||||
|
||||
try {
|
||||
// Fetch current configuration
|
||||
var response = await fetch('/api/codexlens/config');
|
||||
var config = await response.json();
|
||||
|
||||
var content = buildCodexLensConfigContent(config);
|
||||
showModal('CodexLens Configuration', content, { size: 'md' });
|
||||
|
||||
setTimeout(function() {
|
||||
initCodexLensConfigEvents(config);
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}, 100);
|
||||
} catch (err) {
|
||||
var errorContent = '<div class="bg-destructive/10 border border-destructive/20 rounded-lg p-4">' +
|
||||
'<div class="flex items-start gap-2">' +
|
||||
'<i data-lucide="alert-circle" class="w-5 h-5 text-destructive mt-0.5"></i>' +
|
||||
'<div>' +
|
||||
'<p class="font-medium text-destructive">Failed to load configuration</p>' +
|
||||
'<p class="text-sm text-muted-foreground mt-1">' + err.message + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
showModal('CodexLens Configuration', errorContent, { size: 'md' });
|
||||
}
|
||||
}
|
||||
|
||||
function buildCodexLensConfigContent(config) {
|
||||
var status = codexLensStatus || {};
|
||||
var isInstalled = status.ready;
|
||||
var indexDir = config.index_dir || '~/.codexlens/indexes';
|
||||
var currentWorkspace = config.current_workspace || 'None';
|
||||
var indexCount = config.index_count || 0;
|
||||
|
||||
return '<div class="tool-config-modal">' +
|
||||
// Status Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.status') + '</h4>' +
|
||||
'<div class="tool-config-badges">' +
|
||||
'<span class="badge ' + (isInstalled ? 'badge-success' : 'badge-muted') + '">' +
|
||||
'<i data-lucide="' + (isInstalled ? 'check-circle' : 'circle-dashed') + '" class="w-3 h-3"></i> ' +
|
||||
(isInstalled ? t('codexlens.installed') : t('codexlens.notInstalled')) +
|
||||
'</span>' +
|
||||
'<span class="badge badge-primary">' +
|
||||
'<i data-lucide="database" class="w-3 h-3"></i> ' + indexCount + ' ' + t('codexlens.indexes') +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
(currentWorkspace !== 'None'
|
||||
? '<div class="mt-3 p-3 bg-muted/30 rounded-lg">' +
|
||||
'<p class="text-sm text-muted-foreground mb-1">' + t('codexlens.currentWorkspace') + ':</p>' +
|
||||
'<p class="text-sm font-mono break-all">' + escapeHtml(currentWorkspace) + '</p>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
'</div>' +
|
||||
|
||||
// Index Storage Path Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.indexStoragePath') + ' <span class="text-muted">(' + t('codexlens.whereIndexesStored') + ')</span></h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div class="bg-muted/30 rounded-lg p-3">' +
|
||||
'<p class="text-sm text-muted-foreground mb-2">' + t('codexlens.currentPath') + ':</p>' +
|
||||
'<p class="text-sm font-mono break-all bg-background px-2 py-1 rounded border border-border">' +
|
||||
escapeHtml(indexDir) +
|
||||
'</p>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<label class="text-sm font-medium mb-2 block">' + t('codexlens.newStoragePath') + ':</label>' +
|
||||
'<input type="text" id="indexDirInput" class="tool-config-input w-full" ' +
|
||||
'placeholder="' + t('codexlens.pathPlaceholder') + '" ' +
|
||||
'value="' + escapeHtml(indexDir) + '" />' +
|
||||
'<p class="text-xs text-muted-foreground mt-2">' +
|
||||
'<i data-lucide="info" class="w-3 h-3 inline"></i> ' +
|
||||
t('codexlens.pathInfo') +
|
||||
'</p>' +
|
||||
'</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">' + t('codexlens.migrationRequired') + '</p>' +
|
||||
'<p class="text-muted-foreground mt-1">' + t('codexlens.migrationWarning') + '</p>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Actions Section
|
||||
'<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.actions') + '</h4>' +
|
||||
'<div class="tool-config-actions">' +
|
||||
(isInstalled
|
||||
? '<button class="btn-sm btn-outline" onclick="event.stopPropagation(); initCodexLensIndex()">' +
|
||||
'<i data-lucide="database" class="w-3 h-3"></i> ' + t('codexlens.initializeIndex') +
|
||||
'</button>' +
|
||||
'<button class="btn-sm btn-outline" onclick="event.stopPropagation(); cleanCodexLensIndexes()">' +
|
||||
'<i data-lucide="trash" class="w-3 h-3"></i> ' + t('codexlens.cleanAllIndexes') +
|
||||
'</button>' +
|
||||
'<button class="btn-sm btn-outline btn-danger" onclick="event.stopPropagation(); uninstallCodexLens()">' +
|
||||
'<i data-lucide="trash-2" class="w-3 h-3"></i> ' + t('cli.uninstall') +
|
||||
'</button>'
|
||||
: '<button class="btn-sm btn-primary" onclick="event.stopPropagation(); installCodexLens()">' +
|
||||
'<i data-lucide="download" class="w-3 h-3"></i> ' + t('codexlens.installCodexLens') +
|
||||
'</button>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Test Search Section
|
||||
(isInstalled
|
||||
? '<div class="tool-config-section">' +
|
||||
'<h4>' + t('codexlens.testSearch') + ' <span class="text-muted">(' + t('codexlens.testFunctionality') + ')</span></h4>' +
|
||||
'<div class="space-y-3">' +
|
||||
'<div class="flex gap-2">' +
|
||||
'<select id="searchTypeSelect" class="tool-config-select flex-1">' +
|
||||
'<option value="search">' + t('codexlens.textSearch') + '</option>' +
|
||||
'<option value="search_files">' + t('codexlens.fileSearch') + '</option>' +
|
||||
'<option value="symbol">' + t('codexlens.symbolSearch') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<input type="text" id="searchQueryInput" class="tool-config-input w-full" ' +
|
||||
'placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<button class="btn-sm btn-primary w-full" id="runSearchBtn">' +
|
||||
'<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'<div id="searchResults" 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="searchResultCount" class="text-xs text-muted-foreground"></span>' +
|
||||
'</div>' +
|
||||
'<pre id="searchResultContent" class="text-xs font-mono whitespace-pre-wrap break-all"></pre>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
: '') +
|
||||
|
||||
// Footer
|
||||
'<div class="tool-config-footer">' +
|
||||
'<button class="btn btn-outline" onclick="closeModal()">' + t('common.cancel') + '</button>' +
|
||||
'<button class="btn btn-primary" id="saveCodexLensConfigBtn">' +
|
||||
'<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function initCodexLensConfigEvents(currentConfig) {
|
||||
var saveBtn = document.getElementById('saveCodexLensConfigBtn');
|
||||
if (saveBtn) {
|
||||
saveBtn.onclick = async function() {
|
||||
var indexDirInput = document.getElementById('indexDirInput');
|
||||
var newIndexDir = indexDirInput ? indexDirInput.value.trim() : '';
|
||||
|
||||
if (!newIndexDir) {
|
||||
showRefreshToast(t('codexlens.pathEmpty'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newIndexDir === currentConfig.index_dir) {
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.innerHTML = '<span class="animate-pulse">' + t('common.saving') + '</span>';
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/codexlens/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ index_dir: newIndexDir })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(t('codexlens.configSaved'), 'success');
|
||||
closeModal();
|
||||
|
||||
// Refresh CodexLens status
|
||||
if (typeof loadCodexLensStatus === 'function') {
|
||||
await loadCodexLensStatus();
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
} else {
|
||||
showRefreshToast(t('common.saveFailed') + ': ' + result.error, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i data-lucide="save" class="w-3.5 h-3.5"></i> ' + t('codexlens.saveConfig');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Test Search Button
|
||||
var runSearchBtn = document.getElementById('runSearchBtn');
|
||||
if (runSearchBtn) {
|
||||
runSearchBtn.onclick = async function() {
|
||||
var searchType = document.getElementById('searchTypeSelect').value;
|
||||
var query = document.getElementById('searchQueryInput').value.trim();
|
||||
var resultsDiv = document.getElementById('searchResults');
|
||||
var resultCount = document.getElementById('searchResultCount');
|
||||
var resultContent = document.getElementById('searchResultContent');
|
||||
|
||||
if (!query) {
|
||||
showRefreshToast(t('codexlens.enterQuery'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
runSearchBtn.disabled = true;
|
||||
runSearchBtn.innerHTML = '<span class="animate-pulse">' + t('codexlens.searching') + '</span>';
|
||||
resultsDiv.classList.add('hidden');
|
||||
|
||||
try {
|
||||
var endpoint = '/api/codexlens/' + searchType;
|
||||
var params = new URLSearchParams({ query: query, limit: '20' });
|
||||
|
||||
var response = await fetch(endpoint + '?' + params.toString());
|
||||
var result = await response.json();
|
||||
|
||||
console.log('[CodexLens Test] Search result:', result);
|
||||
|
||||
if (result.success) {
|
||||
var results = result.results || result.files || [];
|
||||
resultCount.textContent = results.length + ' ' + t('codexlens.resultsCount');
|
||||
resultContent.textContent = JSON.stringify(results, null, 2);
|
||||
resultsDiv.classList.remove('hidden');
|
||||
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');
|
||||
}
|
||||
|
||||
runSearchBtn.disabled = false;
|
||||
runSearchBtn.innerHTML = '<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} catch (err) {
|
||||
console.error('[CodexLens Test] Error:', err);
|
||||
resultContent.textContent = t('common.exception') + ': ' + err.message;
|
||||
resultsDiv.classList.remove('hidden');
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
runSearchBtn.disabled = false;
|
||||
runSearchBtn.innerHTML = '<i data-lucide="search" class="w-3 h-3"></i> ' + t('codexlens.runSearch');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanCodexLensIndexes() {
|
||||
if (!confirm(t('codexlens.cleanConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showRefreshToast(t('codexlens.cleaning'), 'info');
|
||||
|
||||
var response = await fetch('/api/codexlens/clean', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ all: true })
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(t('codexlens.cleanSuccess'), 'success');
|
||||
|
||||
// Refresh status
|
||||
if (typeof loadCodexLensStatus === 'function') {
|
||||
await loadCodexLensStatus();
|
||||
renderToolsSection();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
} else {
|
||||
showRefreshToast(t('codexlens.cleanFailed') + ': ' + result.error, 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,18 +47,71 @@ async function renderMcpManager() {
|
||||
const projectData = mcpAllProjects[currentPath] || {};
|
||||
const projectServers = projectData.mcpServers || {};
|
||||
const disabledServers = projectData.disabledMcpServers || [];
|
||||
const hasMcpJson = projectData.hasMcpJson || false;
|
||||
const mcpJsonPath = projectData.mcpJsonPath || null;
|
||||
|
||||
// Get all available servers from all projects
|
||||
const allAvailableServers = getAllAvailableMcpServers();
|
||||
|
||||
// Separate current project servers and available servers
|
||||
const currentProjectServerNames = Object.keys(projectServers);
|
||||
// Separate servers by category:
|
||||
// 1. Project Available = Global + Project-specific (servers available to current project)
|
||||
// 2. Global Management = Global servers that can be managed
|
||||
// 3. Other Projects = Servers from other projects (can install to project or global)
|
||||
|
||||
// Separate enterprise, user, and other project servers
|
||||
const enterpriseServerEntries = Object.entries(mcpEnterpriseServers || {})
|
||||
.filter(([name]) => !currentProjectServerNames.includes(name));
|
||||
const userServerEntries = Object.entries(mcpUserServers || {})
|
||||
.filter(([name]) => !currentProjectServerNames.includes(name) && !(mcpEnterpriseServers || {})[name]);
|
||||
const currentProjectServerNames = Object.keys(projectServers);
|
||||
const globalServerNames = Object.keys(mcpUserServers || {});
|
||||
const enterpriseServerNames = Object.keys(mcpEnterpriseServers || {});
|
||||
|
||||
// Project Available MCP: servers available to current project
|
||||
// This includes: Enterprise (highest priority) + Global + Project-specific
|
||||
const projectAvailableEntries = [];
|
||||
|
||||
// Add enterprise servers first (highest priority)
|
||||
for (const [name, config] of Object.entries(mcpEnterpriseServers || {})) {
|
||||
projectAvailableEntries.push({
|
||||
name,
|
||||
config,
|
||||
source: 'enterprise',
|
||||
canRemove: false,
|
||||
canToggle: false
|
||||
});
|
||||
}
|
||||
|
||||
// Add global servers
|
||||
for (const [name, config] of Object.entries(mcpUserServers || {})) {
|
||||
if (!enterpriseServerNames.includes(name)) {
|
||||
projectAvailableEntries.push({
|
||||
name,
|
||||
config,
|
||||
source: 'global',
|
||||
canRemove: false, // Can't remove from project view, must go to global management
|
||||
canToggle: true,
|
||||
isEnabled: !disabledServers.includes(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add project-specific servers
|
||||
for (const [name, config] of Object.entries(projectServers)) {
|
||||
if (!enterpriseServerNames.includes(name) && !globalServerNames.includes(name)) {
|
||||
projectAvailableEntries.push({
|
||||
name,
|
||||
config,
|
||||
source: 'project',
|
||||
canRemove: true,
|
||||
canToggle: true,
|
||||
isEnabled: !disabledServers.includes(name)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global Management: user global servers (for management)
|
||||
const globalManagementEntries = Object.entries(mcpUserServers || {});
|
||||
|
||||
// Enterprise servers (for display only, read-only)
|
||||
const enterpriseServerEntries = Object.entries(mcpEnterpriseServers || {});
|
||||
|
||||
// Other Projects: servers from other projects (not in current project, not global)
|
||||
const otherProjectServers = Object.entries(allAvailableServers)
|
||||
.filter(([name, info]) => !currentProjectServerNames.includes(name) && !info.isGlobal);
|
||||
// Check if CCW Tools is already installed
|
||||
@@ -126,20 +179,31 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Project MCP Servers -->
|
||||
<!-- Project Available MCP Servers -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.currentProject')}</h3>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.projectAvailable')}</h3>
|
||||
<button class="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="openMcpCreateModal()">
|
||||
<span>+</span> ${t('mcp.newServer')}
|
||||
onclick="openMcpCreateModal('project')">
|
||||
<span>+</span> ${t('mcp.newProjectServer')}
|
||||
</button>
|
||||
${hasMcpJson ? `
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-success/10 text-success rounded-md border border-success/20">
|
||||
<i data-lucide="file-check" class="w-3.5 h-3.5"></i>
|
||||
.mcp.json
|
||||
</span>
|
||||
` : `
|
||||
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs bg-muted text-muted-foreground rounded-md border border-border" title="New servers will create .mcp.json">
|
||||
<i data-lucide="file-plus" class="w-3.5 h-3.5"></i>
|
||||
Will use .mcp.json
|
||||
</span>
|
||||
`}
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${currentProjectServerNames.length} ${t('mcp.serversConfigured')}</span>
|
||||
<span class="text-sm text-muted-foreground">${projectAvailableEntries.length} ${t('mcp.serversAvailable')}</span>
|
||||
</div>
|
||||
|
||||
${currentProjectServerNames.length === 0 ? `
|
||||
${projectAvailableEntries.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="plug" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('empty.noMcpServers')}</p>
|
||||
@@ -147,53 +211,43 @@ async function renderMcpManager() {
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${currentProjectServerNames.map(serverName => {
|
||||
const serverConfig = projectServers[serverName];
|
||||
const isEnabled = !disabledServers.includes(serverName);
|
||||
return renderMcpServerCard(serverName, serverConfig, isEnabled, true);
|
||||
${projectAvailableEntries.map(entry => {
|
||||
return renderProjectAvailableServerCard(entry);
|
||||
}).join('')}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Enterprise MCP Servers (Managed) -->
|
||||
${enterpriseServerEntries.length > 0 ? `
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<!-- Global Available MCP Servers -->
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="building-2" class="w-5 h-5"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">Enterprise MCP Servers</h3>
|
||||
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Managed</span>
|
||||
<i data-lucide="globe" class="w-5 h-5 text-success"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('mcp.globalAvailable')}</h3>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${enterpriseServerEntries.length} servers (read-only)</span>
|
||||
<button class="px-3 py-1.5 text-sm bg-success text-success-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1"
|
||||
onclick="openMcpCreateModal('global')">
|
||||
<span>+</span> ${t('mcp.newGlobalServer')}
|
||||
</button>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${globalManagementEntries.length} ${t('mcp.globalServersFrom')}</span>
|
||||
</div>
|
||||
|
||||
${globalManagementEntries.length === 0 ? `
|
||||
<div class="mcp-empty-state bg-card border border-border rounded-lg p-6 text-center">
|
||||
<div class="text-muted-foreground mb-3"><i data-lucide="globe" class="w-10 h-10 mx-auto"></i></div>
|
||||
<p class="text-muted-foreground">${t('empty.noGlobalMcpServers')}</p>
|
||||
<p class="text-sm text-muted-foreground mt-1">${t('empty.globalServersHint')}</p>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${enterpriseServerEntries.map(([serverName, serverConfig]) => {
|
||||
return renderEnterpriseServerCard(serverName, serverConfig);
|
||||
${globalManagementEntries.map(([serverName, serverConfig]) => {
|
||||
return renderGlobalManagementCard(serverName, serverConfig);
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- User MCP Servers -->
|
||||
${userServerEntries.length > 0 ? `
|
||||
<div class="mcp-section mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<h3 class="text-lg font-semibold text-foreground">User MCP Servers</h3>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">${userServerEntries.length} servers from ~/.claude.json</span>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-grid grid gap-3">
|
||||
${userServerEntries.map(([serverName, serverConfig]) => {
|
||||
return renderGlobalServerCard(serverName, serverConfig, 'user');
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Available MCP Servers from Other Projects -->
|
||||
<div class="mcp-section">
|
||||
@@ -238,6 +292,7 @@ async function renderMcpManager() {
|
||||
const serverNames = Object.keys(servers);
|
||||
const isCurrentProject = path === currentPath;
|
||||
const enabledCount = serverNames.filter(s => !projectDisabled.includes(s)).length;
|
||||
const projectHasMcpJson = config.hasMcpJson || false;
|
||||
|
||||
return `
|
||||
<tr class="border-b border-border last:border-b-0 ${isCurrentProject ? 'bg-primary/5' : 'hover:bg-hover/50'}">
|
||||
@@ -245,9 +300,10 @@ async function renderMcpManager() {
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span class="shrink-0">${isCurrentProject ? '<i data-lucide="map-pin" class="w-4 h-4 text-primary"></i>' : '<i data-lucide="folder" class="w-4 h-4"></i>'}</span>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-foreground truncate text-sm" title="${escapeHtml(path)}">
|
||||
${escapeHtml(path.split('\\').pop() || path)}
|
||||
${isCurrentProject ? `<span class="ml-2 text-xs text-primary font-medium">${t('mcp.current')}</span>` : ''}
|
||||
<div class="font-medium text-foreground truncate text-sm flex items-center gap-2" title="${escapeHtml(path)}">
|
||||
<span class="truncate">${escapeHtml(path.split('\\').pop() || path)}</span>
|
||||
${isCurrentProject ? `<span class="text-xs text-primary font-medium shrink-0">${t('mcp.current')}</span>` : ''}
|
||||
${projectHasMcpJson ? `<span class="shrink-0 inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-success/10 text-success rounded" title=".mcp.json detected"><i data-lucide="file-check" class="w-3 h-3"></i></span>` : ''}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground truncate">${escapeHtml(path)}</div>
|
||||
</div>
|
||||
@@ -291,25 +347,40 @@ async function renderMcpManager() {
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentProject) {
|
||||
const command = serverConfig.command || 'N/A';
|
||||
const args = serverConfig.args || [];
|
||||
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
||||
// Render card for Project Available MCP (current project can use)
|
||||
function renderProjectAvailableServerCard(entry) {
|
||||
const { name, config, source, canRemove, canToggle, isEnabled } = entry;
|
||||
const command = config.command || 'N/A';
|
||||
const args = config.args || [];
|
||||
const hasEnv = config.env && Object.keys(config.env).length > 0;
|
||||
|
||||
// Source badge
|
||||
let sourceBadge = '';
|
||||
if (source === 'enterprise') {
|
||||
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Enterprise</span>';
|
||||
} else if (source === 'global') {
|
||||
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-success/10 text-success rounded-full">Global</span>';
|
||||
} else if (source === 'project') {
|
||||
sourceBadge = '<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">Project</span>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${isEnabled ? '' : 'opacity-60'}">
|
||||
<div class="mcp-server-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${canToggle && !isEnabled ? 'opacity-60' : ''}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>${isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-success"></i>' : '<i data-lucide="x-circle" class="w-5 h-5 text-destructive"></i>'}</span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span>${canToggle && isEnabled ? '<i data-lucide="check-circle" class="w-5 h-5 text-success"></i>' : '<i data-lucide="circle" class="w-5 h-5 text-muted-foreground"></i>'}</span>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(name)}</h4>
|
||||
${sourceBadge}
|
||||
</div>
|
||||
<label class="mcp-toggle relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer"
|
||||
${isEnabled ? 'checked' : ''}
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="toggle">
|
||||
<div class="w-9 h-5 bg-hover peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-success"></div>
|
||||
</label>
|
||||
${canToggle ? `
|
||||
<label class="mcp-toggle relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" class="sr-only peer"
|
||||
${isEnabled ? 'checked' : ''}
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-action="toggle">
|
||||
<div class="w-9 h-5 bg-hover peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-success"></div>
|
||||
</label>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
@@ -326,20 +397,85 @@ function renderMcpServerCard(serverName, serverConfig, isEnabled, isInCurrentPro
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
<span class="text-xs">${Object.keys(config.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${isInCurrentProject ? `
|
||||
<div class="mt-3 pt-3 border-t border-border">
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(config))}"
|
||||
data-scope="${source === 'global' ? 'global' : 'project'}"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
${canRemove ? `
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-name="${escapeHtml(name)}"
|
||||
data-action="remove">
|
||||
${t('mcp.removeFromProject')}
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Render card for Global Management (manage global servers)
|
||||
function renderGlobalManagementCard(serverName, serverConfig) {
|
||||
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||
const args = serverConfig.args || [];
|
||||
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
||||
const serverType = serverConfig.type || 'stdio';
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-success/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="globe" class="w-5 h-5 text-success"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||
<span class="text-xs italic">${t('mcp.availableToAll')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-border flex items-center justify-between">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-scope="global"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
<button class="text-xs text-destructive hover:text-destructive/80 transition-colors"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-action="remove-global">
|
||||
${t('mcp.removeGlobal')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -373,13 +509,26 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-key="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-action="add">
|
||||
${t('mcp.add')}
|
||||
</button>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-key="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-scope="project"
|
||||
data-action="add-from-other"
|
||||
title="${t('mcp.installToProject')}">
|
||||
<i data-lucide="folder-plus" class="w-3.5 h-3.5 inline"></i>
|
||||
</button>
|
||||
<button class="px-3 py-1 text-xs bg-success text-success-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-key="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-scope="global"
|
||||
data-action="add-from-other"
|
||||
title="${t('mcp.installToGlobal')}">
|
||||
<i data-lucide="globe" class="w-3.5 h-3.5 inline"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
@@ -398,101 +547,21 @@ function renderAvailableServerCard(serverName, serverInfo) {
|
||||
${sourceProjectName ? `<span class="text-xs text-muted-foreground/70">• from ${escapeHtml(sourceProjectName)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGlobalServerCard(serverName, serverConfig, source = 'user') {
|
||||
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||
const args = serverConfig.args || [];
|
||||
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
||||
const serverType = serverConfig.type || 'stdio';
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-global bg-card border border-primary/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-primary/10 text-primary rounded-full">User</span>
|
||||
</div>
|
||||
<button class="px-3 py-1 text-xs bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
|
||||
data-server-name="${escapeHtml(serverName)}"
|
||||
data-server-config='${JSON.stringify(serverConfig).replace(/'/g, "'")}'
|
||||
data-action="add">
|
||||
${t('mcp.addToProject')}
|
||||
<div class="mt-3 pt-3 border-t border-border">
|
||||
<button class="text-xs text-primary hover:text-primary/80 transition-colors flex items-center gap-1"
|
||||
data-server-name="${escapeHtml(originalName)}"
|
||||
data-server-config="${escapeHtml(JSON.stringify(serverConfig))}"
|
||||
data-scope="project"
|
||||
data-action="copy-install-cmd">
|
||||
<i data-lucide="copy" class="w-3 h-3"></i>
|
||||
${t('mcp.copyInstallCmd')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||
<span class="text-xs italic">Available to all projects from ~/.claude.json</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEnterpriseServerCard(serverName, serverConfig) {
|
||||
const command = serverConfig.command || serverConfig.url || 'N/A';
|
||||
const args = serverConfig.args || [];
|
||||
const hasEnv = serverConfig.env && Object.keys(serverConfig.env).length > 0;
|
||||
const serverType = serverConfig.type || 'stdio';
|
||||
|
||||
return `
|
||||
<div class="mcp-server-card mcp-server-enterprise bg-card border border-warning/30 rounded-lg p-4 hover:shadow-md transition-all">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="building-2" class="w-5 h-5"></i>
|
||||
<h4 class="font-semibold text-foreground">${escapeHtml(serverName)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 bg-warning/20 text-warning rounded-full">Enterprise</span>
|
||||
<i data-lucide="lock" class="w-3 h-3 text-muted-foreground"></i>
|
||||
</div>
|
||||
<span class="px-3 py-1 text-xs bg-muted text-muted-foreground rounded cursor-not-allowed">
|
||||
Read-only
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mcp-server-details text-sm space-y-1">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">${serverType === 'stdio' ? 'cmd' : 'url'}</span>
|
||||
<span class="truncate" title="${escapeHtml(command)}">${escapeHtml(command)}</span>
|
||||
</div>
|
||||
${args.length > 0 ? `
|
||||
<div class="flex items-start gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded shrink-0">args</span>
|
||||
<span class="text-xs font-mono truncate" title="${escapeHtml(args.join(' '))}">${escapeHtml(args.slice(0, 3).join(' '))}${args.length > 3 ? '...' : ''}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
${hasEnv ? `
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<span class="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">env</span>
|
||||
<span class="text-xs">${Object.keys(serverConfig.env).length} variables</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="flex items-center gap-2 text-muted-foreground mt-1">
|
||||
<span class="text-xs italic">Managed by organization (highest priority)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function attachMcpEventListeners() {
|
||||
// Toggle switches
|
||||
@@ -504,16 +573,22 @@ function attachMcpEventListeners() {
|
||||
});
|
||||
});
|
||||
|
||||
// Add buttons - use btn.dataset instead of e.target.dataset for event bubbling safety
|
||||
document.querySelectorAll('.mcp-server-card button[data-action="add"]').forEach(btn => {
|
||||
// Add from other projects (with scope selection)
|
||||
document.querySelectorAll('.mcp-server-card button[data-action="add-from-other"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = btn.dataset.serverName;
|
||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
||||
await copyMcpServerToProject(serverName, serverConfig);
|
||||
const scope = btn.dataset.scope; // 'project' or 'global'
|
||||
|
||||
if (scope === 'global') {
|
||||
await addGlobalMcpServer(serverName, serverConfig);
|
||||
} else {
|
||||
await copyMcpServerToProject(serverName, serverConfig);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Remove buttons - use btn.dataset instead of e.target.dataset for event bubbling safety
|
||||
// Remove buttons (project-level)
|
||||
document.querySelectorAll('.mcp-server-card button[data-action="remove"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = btn.dataset.serverName;
|
||||
@@ -522,4 +597,24 @@ function attachMcpEventListeners() {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Remove buttons (global-level)
|
||||
document.querySelectorAll('.mcp-server-card button[data-action="remove-global"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = btn.dataset.serverName;
|
||||
if (confirm(t('mcp.removeGlobalConfirm', { name: serverName }))) {
|
||||
await removeGlobalMcpServer(serverName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Copy install command buttons
|
||||
document.querySelectorAll('.mcp-server-card button[data-action="copy-install-cmd"]').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
const serverName = btn.dataset.serverName;
|
||||
const serverConfig = JSON.parse(btn.dataset.serverConfig);
|
||||
const scope = btn.dataset.scope || 'project';
|
||||
await copyMcpInstallCommand(serverName, serverConfig, scope);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -335,9 +335,482 @@ function editRule(ruleName, location) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Create Rule Modal ==========
|
||||
var ruleCreateState = {
|
||||
location: 'project',
|
||||
fileName: '',
|
||||
subdirectory: '',
|
||||
isConditional: false,
|
||||
paths: [''],
|
||||
content: '',
|
||||
mode: 'input',
|
||||
generationType: 'description',
|
||||
description: '',
|
||||
extractScope: '',
|
||||
extractFocus: ''
|
||||
};
|
||||
|
||||
function openRuleCreateModal() {
|
||||
// Open create modal (to be implemented with modal)
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.createNotImplemented'), 'info');
|
||||
// Reset state
|
||||
ruleCreateState = {
|
||||
location: 'project',
|
||||
fileName: '',
|
||||
subdirectory: '',
|
||||
isConditional: false,
|
||||
paths: [''],
|
||||
content: '',
|
||||
mode: 'input',
|
||||
generationType: 'description',
|
||||
description: '',
|
||||
extractScope: '',
|
||||
extractFocus: ''
|
||||
};
|
||||
|
||||
// Create modal HTML
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="closeRuleCreateModal(event)">
|
||||
<div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-2xl max-h-[90vh] mx-4 flex flex-col" onclick="event.stopPropagation()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('rules.createRule')}</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
|
||||
onclick="closeRuleCreateModal()">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-5">
|
||||
<!-- Location Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.location')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.location === 'project' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectRuleLocation('project')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.projectRules')}</div>
|
||||
<div class="text-xs text-muted-foreground">.claude/rules/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.location === 'user' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectRuleLocation('user')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.userRules')}</div>
|
||||
<div class="text-xs text-muted-foreground">~/.claude/rules/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.createMode')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.mode === 'input' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchRuleCreateMode('input')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="edit" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.manualInput')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('rules.manualInputHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${ruleCreateState.mode === 'cli-generate' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchRuleCreateMode('cli-generate')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('rules.cliGenerate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('rules.cliGenerateHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.fileName')}</label>
|
||||
<input type="text" id="ruleFileName"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="my-rule.md"
|
||||
value="${ruleCreateState.fileName}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.fileNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Subdirectory -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.subdirectory')} <span class="text-muted-foreground">${t('common.optional')}</span></label>
|
||||
<input type="text" id="ruleSubdirectory"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="category/subcategory"
|
||||
value="${ruleCreateState.subdirectory}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.subdirectoryHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- CLI Generation Type (CLI mode only) -->
|
||||
<div id="ruleGenerationTypeSection" style="display: ${ruleCreateState.mode === 'cli-generate' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.generationType')}</label>
|
||||
<div class="flex gap-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="ruleGenType" value="description"
|
||||
class="w-4 h-4 text-primary bg-background border-border focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.generationType === 'description' ? 'checked' : ''}
|
||||
onchange="switchRuleGenerationType('description')">
|
||||
<span class="text-sm">${t('rules.fromDescription')}</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="ruleGenType" value="extract"
|
||||
class="w-4 h-4 text-primary bg-background border-border focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.generationType === 'extract' ? 'checked' : ''}
|
||||
onchange="switchRuleGenerationType('extract')">
|
||||
<span class="text-sm">${t('rules.fromCodeExtract')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Input (CLI mode, description type) -->
|
||||
<div id="ruleDescriptionSection" style="display: ${ruleCreateState.mode === 'cli-generate' && ruleCreateState.generationType === 'description' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.description')}</label>
|
||||
<textarea id="ruleDescription"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
rows="4"
|
||||
placeholder="${t('rules.descriptionPlaceholder')}">${ruleCreateState.description}</textarea>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.descriptionHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Code Extract Options (CLI mode, extract type) -->
|
||||
<div id="ruleExtractSection" style="display: ${ruleCreateState.mode === 'cli-generate' && ruleCreateState.generationType === 'extract' ? 'block' : 'none'}">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.extractScope')}</label>
|
||||
<input type="text" id="ruleExtractScope"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary font-mono"
|
||||
placeholder="src/**/*.ts"
|
||||
value="${ruleCreateState.extractScope}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.extractScopeHint')}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.extractFocus')}</label>
|
||||
<input type="text" id="ruleExtractFocus"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="naming, error-handling, state-management"
|
||||
value="${ruleCreateState.extractFocus}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.extractFocusHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conditional Rule Toggle (Manual mode only) -->
|
||||
<div id="ruleConditionalSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="ruleConditional"
|
||||
class="w-4 h-4 text-primary bg-background border-border rounded focus:ring-2 focus:ring-primary"
|
||||
${ruleCreateState.isConditional ? 'checked' : ''}
|
||||
onchange="toggleRuleConditional()">
|
||||
<span class="text-sm font-medium text-foreground">${t('rules.conditionalRule')}</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground mt-1 ml-6">${t('rules.conditionalHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Path Conditions -->
|
||||
<div id="rulePathsContainer" style="display: ${ruleCreateState.isConditional ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.pathConditions')}</label>
|
||||
<div id="rulePathsList" class="space-y-2">
|
||||
${ruleCreateState.paths.map((path, index) => `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="src/**/*.ts"
|
||||
value="${path}"
|
||||
data-index="${index}">
|
||||
${index > 0 ? `
|
||||
<button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||
onclick="removeRulePath(${index})">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<button class="mt-2 px-3 py-1.5 text-sm text-primary hover:bg-primary/10 rounded-lg transition-colors flex items-center gap-1"
|
||||
onclick="addRulePath()">
|
||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||
${t('rules.addPath')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content (Manual mode only) -->
|
||||
<div id="ruleContentSection" style="display: ${ruleCreateState.mode === 'input' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('rules.content')}</label>
|
||||
<textarea id="ruleContent"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary font-mono"
|
||||
rows="10"
|
||||
placeholder="${t('rules.contentPlaceholder')}">${ruleCreateState.content}</textarea>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('rules.contentHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
onclick="closeRuleCreateModal()">
|
||||
${t('common.cancel')}
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
onclick="createRule()">
|
||||
${t('rules.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to DOM
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.id = 'ruleCreateModal';
|
||||
modalContainer.innerHTML = modalHtml;
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeRuleCreateModal(event) {
|
||||
if (event && event.target !== event.currentTarget) return;
|
||||
const modal = document.getElementById('ruleCreateModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
function selectRuleLocation(location) {
|
||||
ruleCreateState.location = location;
|
||||
// Re-render modal
|
||||
closeRuleCreateModal();
|
||||
openRuleCreateModal();
|
||||
}
|
||||
|
||||
function toggleRuleConditional() {
|
||||
ruleCreateState.isConditional = !ruleCreateState.isConditional;
|
||||
const pathsContainer = document.getElementById('rulePathsContainer');
|
||||
if (pathsContainer) {
|
||||
pathsContainer.style.display = ruleCreateState.isConditional ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function addRulePath() {
|
||||
ruleCreateState.paths.push('');
|
||||
// Re-render paths list
|
||||
const pathsList = document.getElementById('rulePathsList');
|
||||
if (pathsList) {
|
||||
const index = ruleCreateState.paths.length - 1;
|
||||
const pathHtml = `
|
||||
<div class="flex gap-2">
|
||||
<input type="text" class="rule-path-input flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="src/**/*.ts"
|
||||
value=""
|
||||
data-index="${index}">
|
||||
<button class="px-3 py-2 text-destructive hover:bg-destructive/10 rounded-lg transition-colors"
|
||||
onclick="removeRulePath(${index})">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
pathsList.insertAdjacentHTML('beforeend', pathHtml);
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
function removeRulePath(index) {
|
||||
ruleCreateState.paths.splice(index, 1);
|
||||
// Re-render paths list
|
||||
closeRuleCreateModal();
|
||||
openRuleCreateModal();
|
||||
}
|
||||
|
||||
function switchRuleCreateMode(mode) {
|
||||
ruleCreateState.mode = mode;
|
||||
|
||||
// Toggle visibility of sections
|
||||
const generationTypeSection = document.getElementById('ruleGenerationTypeSection');
|
||||
const descriptionSection = document.getElementById('ruleDescriptionSection');
|
||||
const extractSection = document.getElementById('ruleExtractSection');
|
||||
const conditionalSection = document.getElementById('ruleConditionalSection');
|
||||
const contentSection = document.getElementById('ruleContentSection');
|
||||
|
||||
if (mode === 'cli-generate') {
|
||||
if (generationTypeSection) generationTypeSection.style.display = 'block';
|
||||
if (conditionalSection) conditionalSection.style.display = 'none';
|
||||
if (contentSection) contentSection.style.display = 'none';
|
||||
|
||||
// Show appropriate generation section
|
||||
if (ruleCreateState.generationType === 'description') {
|
||||
if (descriptionSection) descriptionSection.style.display = 'block';
|
||||
if (extractSection) extractSection.style.display = 'none';
|
||||
} else {
|
||||
if (descriptionSection) descriptionSection.style.display = 'none';
|
||||
if (extractSection) extractSection.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
if (generationTypeSection) generationTypeSection.style.display = 'none';
|
||||
if (descriptionSection) descriptionSection.style.display = 'none';
|
||||
if (extractSection) extractSection.style.display = 'none';
|
||||
if (conditionalSection) conditionalSection.style.display = 'block';
|
||||
if (contentSection) contentSection.style.display = 'block';
|
||||
}
|
||||
|
||||
// Re-render modal to update button states
|
||||
closeRuleCreateModal();
|
||||
openRuleCreateModal();
|
||||
}
|
||||
|
||||
function switchRuleGenerationType(type) {
|
||||
ruleCreateState.generationType = type;
|
||||
|
||||
// Toggle visibility of generation sections
|
||||
const descriptionSection = document.getElementById('ruleDescriptionSection');
|
||||
const extractSection = document.getElementById('ruleExtractSection');
|
||||
|
||||
if (type === 'description') {
|
||||
if (descriptionSection) descriptionSection.style.display = 'block';
|
||||
if (extractSection) extractSection.style.display = 'none';
|
||||
} else if (type === 'extract') {
|
||||
if (descriptionSection) descriptionSection.style.display = 'none';
|
||||
if (extractSection) extractSection.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function createRule() {
|
||||
const fileNameInput = document.getElementById('ruleFileName');
|
||||
const subdirectoryInput = document.getElementById('ruleSubdirectory');
|
||||
const contentInput = document.getElementById('ruleContent');
|
||||
const pathInputs = document.querySelectorAll('.rule-path-input');
|
||||
const descriptionInput = document.getElementById('ruleDescription');
|
||||
const extractScopeInput = document.getElementById('ruleExtractScope');
|
||||
const extractFocusInput = document.getElementById('ruleExtractFocus');
|
||||
|
||||
const fileName = fileNameInput ? fileNameInput.value.trim() : ruleCreateState.fileName;
|
||||
const subdirectory = subdirectoryInput ? subdirectoryInput.value.trim() : ruleCreateState.subdirectory;
|
||||
|
||||
// Validate file name
|
||||
if (!fileName) {
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.fileNameRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fileName.endsWith('.md')) {
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.fileNameMustEndMd'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare request based on mode
|
||||
let requestBody;
|
||||
|
||||
if (ruleCreateState.mode === 'cli-generate') {
|
||||
// CLI generation mode
|
||||
const description = descriptionInput ? descriptionInput.value.trim() : ruleCreateState.description;
|
||||
const extractScope = extractScopeInput ? extractScopeInput.value.trim() : ruleCreateState.extractScope;
|
||||
const extractFocus = extractFocusInput ? extractFocusInput.value.trim() : ruleCreateState.extractFocus;
|
||||
|
||||
// Validate based on generation type
|
||||
if (ruleCreateState.generationType === 'description' && !description) {
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.descriptionRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (ruleCreateState.generationType === 'extract' && !extractScope) {
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.extractScopeRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
requestBody = {
|
||||
mode: 'cli-generate',
|
||||
fileName,
|
||||
location: ruleCreateState.location,
|
||||
subdirectory: subdirectory || undefined,
|
||||
projectPath,
|
||||
generationType: ruleCreateState.generationType,
|
||||
description: ruleCreateState.generationType === 'description' ? description : undefined,
|
||||
extractScope: ruleCreateState.generationType === 'extract' ? extractScope : undefined,
|
||||
extractFocus: ruleCreateState.generationType === 'extract' ? extractFocus : undefined
|
||||
};
|
||||
|
||||
// Show progress message
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.cliGenerating'), 'info');
|
||||
}
|
||||
} else {
|
||||
// Manual input mode
|
||||
const content = contentInput ? contentInput.value.trim() : ruleCreateState.content;
|
||||
|
||||
// Collect paths from inputs
|
||||
const paths = [];
|
||||
if (ruleCreateState.isConditional && pathInputs) {
|
||||
pathInputs.forEach(input => {
|
||||
const path = input.value.trim();
|
||||
if (path) paths.push(path);
|
||||
});
|
||||
}
|
||||
|
||||
// Validate content
|
||||
if (!content) {
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.contentRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
requestBody = {
|
||||
mode: 'input',
|
||||
fileName,
|
||||
content,
|
||||
paths: paths.length > 0 ? paths : undefined,
|
||||
location: ruleCreateState.location,
|
||||
subdirectory: subdirectory || undefined,
|
||||
projectPath
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/rules/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create rule');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Close modal
|
||||
closeRuleCreateModal();
|
||||
|
||||
// Reload rules data
|
||||
await loadRulesData();
|
||||
renderRulesView();
|
||||
|
||||
// Show success message
|
||||
if (window.showToast) {
|
||||
showToast(t('rules.created', { name: result.fileName }), 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create rule:', err);
|
||||
if (window.showToast) {
|
||||
showToast(err.message || t('rules.createError'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,9 +337,471 @@ function editSkill(skillName, location) {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Create Skill Modal ==========
|
||||
var skillCreateState = {
|
||||
mode: 'import', // 'import' or 'cli-generate'
|
||||
location: 'project',
|
||||
sourcePath: '',
|
||||
customName: '',
|
||||
validationResult: null,
|
||||
// CLI Generate mode fields
|
||||
generationType: 'description', // 'description' or 'template'
|
||||
description: '',
|
||||
skillName: ''
|
||||
};
|
||||
|
||||
function openSkillCreateModal() {
|
||||
// Open create modal (to be implemented with modal)
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.createNotImplemented'), 'info');
|
||||
// Reset state
|
||||
skillCreateState = {
|
||||
mode: 'import',
|
||||
location: 'project',
|
||||
sourcePath: '',
|
||||
customName: '',
|
||||
validationResult: null,
|
||||
generationType: 'description',
|
||||
description: '',
|
||||
skillName: ''
|
||||
};
|
||||
|
||||
// Create modal HTML
|
||||
const modalHtml = `
|
||||
<div class="modal-overlay fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="closeSkillCreateModal(event)">
|
||||
<div class="modal-dialog bg-card rounded-lg shadow-lg w-full max-w-2xl mx-4" onclick="event.stopPropagation()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border">
|
||||
<h3 class="text-lg font-semibold text-foreground">${t('skills.createSkill')}</h3>
|
||||
<button class="w-8 h-8 flex items-center justify-center text-xl text-muted-foreground hover:text-foreground hover:bg-hover rounded"
|
||||
onclick="closeSkillCreateModal()">×</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-6 space-y-5">
|
||||
<!-- Location Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.location')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.location === 'project' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectSkillLocation('project')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.projectSkills')}</div>
|
||||
<div class="text-xs text-muted-foreground">.claude/skills/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="location-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.location === 'user' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="selectSkillLocation('user')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="user" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.userSkills')}</div>
|
||||
<div class="text-xs text-muted-foreground">~/.claude/skills/</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.createMode')}</label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.mode === 'import' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillCreateMode('import')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="folder-input" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.importFolder')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.importFolderHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="mode-btn px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.mode === 'cli-generate' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillCreateMode('cli-generate')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${t('skills.cliGenerate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.cliGenerateHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Mode Content -->
|
||||
<div id="skillImportMode" style="display: ${skillCreateState.mode === 'import' ? 'block' : 'none'}">
|
||||
<!-- Source Folder Path -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.sourceFolder')}</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="skillSourcePath"
|
||||
class="flex-1 px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.sourceFolderPlaceholder')}"
|
||||
value="${skillCreateState.sourcePath}">
|
||||
<button class="px-4 py-2 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors text-sm"
|
||||
onclick="browseSkillFolder()">
|
||||
<i data-lucide="folder-open" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.sourceFolderHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Custom Name -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.customName')} <span class="text-muted-foreground">${t('common.optional')}</span></label>
|
||||
<input type="text" id="skillCustomName"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.customNamePlaceholder')}"
|
||||
value="${skillCreateState.customName}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.customNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Validation Result -->
|
||||
<div id="skillValidationResult"></div>
|
||||
</div>
|
||||
|
||||
<!-- CLI Generate Mode Content -->
|
||||
<div id="skillCliGenerateMode" style="display: ${skillCreateState.mode === 'cli-generate' ? 'block' : 'none'}">
|
||||
<!-- Skill Name (Required for CLI Generate) -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.skillName')} <span class="text-destructive">*</span></label>
|
||||
<input type="text" id="skillGenerateName"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.skillNamePlaceholder')}"
|
||||
value="${skillCreateState.skillName}">
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.skillNameHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- Generation Type Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.generationType')}</label>
|
||||
<div class="flex gap-3">
|
||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all ${skillCreateState.generationType === 'description' ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/50'}"
|
||||
onclick="switchSkillGenerationType('description')">
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium text-sm">${t('skills.fromDescription')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.fromDescriptionHint')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="flex-1 px-4 py-3 text-left border-2 rounded-lg transition-all opacity-50 cursor-not-allowed"
|
||||
disabled>
|
||||
<div class="flex items-center gap-2">
|
||||
<i data-lucide="layout-template" class="w-5 h-5"></i>
|
||||
<div>
|
||||
<div class="font-medium text-sm">${t('skills.fromTemplate')}</div>
|
||||
<div class="text-xs text-muted-foreground">${t('skills.comingSoon')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Text Area (for 'description' type) -->
|
||||
<div id="skillDescriptionArea" style="display: ${skillCreateState.generationType === 'description' ? 'block' : 'none'}">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">${t('skills.descriptionLabel')} <span class="text-destructive">*</span></label>
|
||||
<textarea id="skillDescription"
|
||||
class="w-full px-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
placeholder="${t('skills.descriptionPlaceholder')}"
|
||||
rows="6">${skillCreateState.description}</textarea>
|
||||
<p class="text-xs text-muted-foreground mt-1">${t('skills.descriptionGenerateHint')}</p>
|
||||
</div>
|
||||
|
||||
<!-- CLI Generate Info -->
|
||||
<div class="p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<i data-lucide="info" class="w-4 h-4 text-blue-600 mt-0.5"></i>
|
||||
<div class="text-sm text-blue-600">
|
||||
<p class="font-medium">${t('skills.cliGenerateInfo')}</p>
|
||||
<p class="text-xs mt-1">${t('skills.cliGenerateTimeHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-3 px-6 py-4 border-t border-border">
|
||||
<button class="px-4 py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
onclick="closeSkillCreateModal()">
|
||||
${t('common.cancel')}
|
||||
</button>
|
||||
${skillCreateState.mode === 'import' ? `
|
||||
<button class="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors"
|
||||
onclick="validateSkillImport()">
|
||||
${t('skills.validate')}
|
||||
</button>
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity"
|
||||
onclick="createSkill()">
|
||||
${t('skills.import')}
|
||||
</button>
|
||||
` : `
|
||||
<button class="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="createSkill()">
|
||||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||
${t('skills.generate')}
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to DOM
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.id = 'skillCreateModal';
|
||||
modalContainer.innerHTML = modalHtml;
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function closeSkillCreateModal(event) {
|
||||
if (event && event.target !== event.currentTarget) return;
|
||||
const modal = document.getElementById('skillCreateModal');
|
||||
if (modal) modal.remove();
|
||||
}
|
||||
|
||||
function selectSkillLocation(location) {
|
||||
skillCreateState.location = location;
|
||||
// Re-render modal
|
||||
closeSkillCreateModal();
|
||||
openSkillCreateModal();
|
||||
}
|
||||
|
||||
function switchSkillCreateMode(mode) {
|
||||
skillCreateState.mode = mode;
|
||||
// Re-render modal
|
||||
closeSkillCreateModal();
|
||||
openSkillCreateModal();
|
||||
}
|
||||
|
||||
function switchSkillGenerationType(type) {
|
||||
skillCreateState.generationType = type;
|
||||
// Re-render modal
|
||||
closeSkillCreateModal();
|
||||
openSkillCreateModal();
|
||||
}
|
||||
|
||||
function browseSkillFolder() {
|
||||
// Use browser prompt for now (Phase 3 will implement file browser)
|
||||
const path = prompt(t('skills.enterFolderPath'), skillCreateState.sourcePath);
|
||||
if (path !== null) {
|
||||
skillCreateState.sourcePath = path;
|
||||
document.getElementById('skillSourcePath').value = path;
|
||||
}
|
||||
}
|
||||
|
||||
async function validateSkillImport() {
|
||||
const sourcePathInput = document.getElementById('skillSourcePath');
|
||||
const sourcePath = sourcePathInput ? sourcePathInput.value.trim() : skillCreateState.sourcePath;
|
||||
|
||||
if (!sourcePath) {
|
||||
showValidationResult({ valid: false, errors: [t('skills.sourceFolderRequired')], skillInfo: null });
|
||||
return;
|
||||
}
|
||||
|
||||
skillCreateState.sourcePath = sourcePath;
|
||||
|
||||
// Show loading state
|
||||
showValidationResult({ loading: true });
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/skills/validate-import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sourcePath })
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Validation request failed');
|
||||
|
||||
const result = await response.json();
|
||||
skillCreateState.validationResult = result;
|
||||
showValidationResult(result);
|
||||
} catch (err) {
|
||||
console.error('Failed to validate skill:', err);
|
||||
showValidationResult({ valid: false, errors: [t('skills.validationError')], skillInfo: null });
|
||||
}
|
||||
}
|
||||
|
||||
function showValidationResult(result) {
|
||||
const container = document.getElementById('skillValidationResult');
|
||||
if (!container) return;
|
||||
|
||||
if (result.loading) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center gap-2 p-3 bg-muted/50 rounded-lg">
|
||||
<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>
|
||||
<span class="text-sm text-muted-foreground">${t('skills.validating')}</span>
|
||||
</div>
|
||||
`;
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.valid) {
|
||||
container.innerHTML = `
|
||||
<div class="p-4 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 text-green-600 mb-2">
|
||||
<i data-lucide="check-circle" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${t('skills.validSkill')}</span>
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div><span class="text-muted-foreground">${t('skills.name')}:</span> <span class="font-medium">${escapeHtml(result.skillInfo.name)}</span></div>
|
||||
<div><span class="text-muted-foreground">${t('skills.description')}:</span> <span>${escapeHtml(result.skillInfo.description)}</span></div>
|
||||
${result.skillInfo.version ? `<div><span class="text-muted-foreground">${t('skills.version')}:</span> <span>${escapeHtml(result.skillInfo.version)}</span></div>` : ''}
|
||||
${result.skillInfo.supportingFiles && result.skillInfo.supportingFiles.length > 0 ? `<div><span class="text-muted-foreground">${t('skills.supportingFiles')}:</span> <span>${result.skillInfo.supportingFiles.length} ${t('skills.files')}</span></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
|
||||
<div class="flex items-center gap-2 text-destructive mb-2">
|
||||
<i data-lucide="x-circle" class="w-5 h-5"></i>
|
||||
<span class="font-medium">${t('skills.invalidSkill')}</span>
|
||||
</div>
|
||||
<ul class="space-y-1 text-sm">
|
||||
${result.errors.map(error => `<li class="text-destructive">• ${escapeHtml(error)}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
async function createSkill() {
|
||||
if (skillCreateState.mode === 'import') {
|
||||
// Import Mode Logic
|
||||
const sourcePathInput = document.getElementById('skillSourcePath');
|
||||
const customNameInput = document.getElementById('skillCustomName');
|
||||
|
||||
const sourcePath = sourcePathInput ? sourcePathInput.value.trim() : skillCreateState.sourcePath;
|
||||
const customName = customNameInput ? customNameInput.value.trim() : skillCreateState.customName;
|
||||
|
||||
if (!sourcePath) {
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.sourceFolderRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate first if not already validated
|
||||
if (!skillCreateState.validationResult || !skillCreateState.validationResult.valid) {
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.validateFirst'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/skills/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
mode: 'import',
|
||||
location: skillCreateState.location,
|
||||
sourcePath,
|
||||
skillName: customName || undefined,
|
||||
projectPath
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create skill');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Close modal
|
||||
closeSkillCreateModal();
|
||||
|
||||
// Reload skills data
|
||||
await loadSkillsData();
|
||||
renderSkillsView();
|
||||
|
||||
// Show success message
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.created', { name: result.skillName }), 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create skill:', err);
|
||||
if (window.showToast) {
|
||||
showToast(err.message || t('skills.createError'), 'error');
|
||||
}
|
||||
}
|
||||
} else if (skillCreateState.mode === 'cli-generate') {
|
||||
// CLI Generate Mode Logic
|
||||
const skillNameInput = document.getElementById('skillGenerateName');
|
||||
const descriptionInput = document.getElementById('skillDescription');
|
||||
|
||||
const skillName = skillNameInput ? skillNameInput.value.trim() : skillCreateState.skillName;
|
||||
const description = descriptionInput ? descriptionInput.value.trim() : skillCreateState.description;
|
||||
|
||||
// Validation
|
||||
if (!skillName) {
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.skillNameRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (skillCreateState.generationType === 'description' && !description) {
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.descriptionRequired'), 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show generating progress toast
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.generating'), 'info');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/skills/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
mode: 'cli-generate',
|
||||
location: skillCreateState.location,
|
||||
generationType: skillCreateState.generationType,
|
||||
skillName,
|
||||
description: skillCreateState.generationType === 'description' ? description : undefined,
|
||||
projectPath
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to generate skill');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Close modal
|
||||
closeSkillCreateModal();
|
||||
|
||||
// Reload skills data
|
||||
await loadSkillsData();
|
||||
renderSkillsView();
|
||||
|
||||
// Show success message
|
||||
if (window.showToast) {
|
||||
showToast(t('skills.generated', { name: result.skillName }), 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to generate skill:', err);
|
||||
if (window.showToast) {
|
||||
showToast(err.message || t('skills.generateError'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user