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

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

View File

@@ -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()">&times;</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');
}
}
}