// Rules Manager View // Manages Claude Code rules (.claude/rules/) // ========== Rules State ========== var rulesData = { projectRules: [], userRules: [] }; var selectedRule = null; var rulesLoading = false; // ========== Main Render Function ========== async function renderRulesManager() { const container = document.getElementById('mainContent'); if (!container) return; // Hide stats grid and search const statsGrid = document.getElementById('statsGrid'); const searchInput = document.getElementById('searchInput'); if (statsGrid) statsGrid.style.display = 'none'; if (searchInput) searchInput.parentElement.style.display = 'none'; // Show loading state container.innerHTML = '
' + '
' + '

' + t('common.loading') + '

' + '
'; // Load rules data await loadRulesData(); // Render the main view renderRulesView(); } async function loadRulesData() { rulesLoading = true; try { const response = await fetch('/api/rules?path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load rules'); const data = await response.json(); rulesData = { projectRules: data.projectRules || [], userRules: data.userRules || [] }; // Update badge updateRulesBadge(); } catch (err) { console.error('Failed to load rules:', err); rulesData = { projectRules: [], userRules: [] }; } finally { rulesLoading = false; } } function updateRulesBadge() { const badge = document.getElementById('badgeRules'); if (badge) { const total = rulesData.projectRules.length + rulesData.userRules.length; badge.textContent = total; } } function renderRulesView() { const container = document.getElementById('mainContent'); if (!container) return; const projectRules = rulesData.projectRules || []; const userRules = rulesData.userRules || []; container.innerHTML = `

${t('rules.title')}

${t('rules.description')}

${t('rules.projectRules')}

.claude/rules/
${projectRules.length} ${t('rules.rulesCount')}
${projectRules.length === 0 ? `

${t('rules.noProjectRules')}

${t('rules.createHint')}

` : `
${projectRules.map(rule => renderRuleCard(rule, 'project')).join('')}
`}

${t('rules.userRules')}

~/.claude/rules/
${userRules.length} ${t('rules.rulesCount')}
${userRules.length === 0 ? `

${t('rules.noUserRules')}

${t('rules.userRulesHint')}

` : `
${userRules.map(rule => renderRuleCard(rule, 'user')).join('')}
`}
${selectedRule ? renderRuleDetailPanel(selectedRule) : ''}
`; // Initialize Lucide icons if (typeof lucide !== 'undefined') lucide.createIcons(); } function renderRuleCard(rule, location) { const hasPathCondition = rule.paths && rule.paths.length > 0; const isGlobal = !hasPathCondition; const locationIcon = location === 'project' ? 'folder' : 'user'; const locationClass = location === 'project' ? 'text-success' : 'text-orange'; const locationBg = location === 'project' ? 'bg-success/10' : 'bg-orange/10'; // Get preview of content (first 100 chars) const contentPreview = rule.content ? rule.content.substring(0, 100).replace(/\n/g, ' ') + (rule.content.length > 100 ? '...' : '') : ''; return `

${escapeHtml(rule.name)}

${rule.subdirectory ? `${escapeHtml(rule.subdirectory)}/` : ''}
${isGlobal ? ` global ` : ` conditional `} ${location}
${contentPreview ? `

${escapeHtml(contentPreview)}

` : ''} ${hasPathCondition ? `
${escapeHtml(rule.paths.join(', '))}
` : ''}
`; } function renderRuleDetailPanel(rule) { const hasPathCondition = rule.paths && rule.paths.length > 0; return `

${escapeHtml(rule.name)}

${t('rules.typeLabel')}

${hasPathCondition ? ` ${t('rules.conditional')} ` : ` ${t('rules.global')} `}
${hasPathCondition ? `

${t('rules.pathConditions')}

${rule.paths.map(path => `
${escapeHtml(path)}
`).join('')}
` : ''}

${t('rules.content')}

${escapeHtml(rule.content || '')}

${t('rules.filePath')}

${escapeHtml(rule.path)}
`; } async function showRuleDetail(ruleName, location) { try { const response = await fetch('/api/rules/' + encodeURIComponent(ruleName) + '?location=' + location + '&path=' + encodeURIComponent(projectPath)); if (!response.ok) throw new Error('Failed to load rule detail'); const data = await response.json(); selectedRule = data.rule; renderRulesView(); } catch (err) { console.error('Failed to load rule detail:', err); if (window.showToast) { showToast(t('rules.loadError'), 'error'); } } } function closeRuleDetail() { selectedRule = null; renderRulesView(); } async function deleteRule(ruleName, location) { if (!confirm(t('rules.deleteConfirm', { name: ruleName }))) return; try { const response = await fetch('/api/rules/' + encodeURIComponent(ruleName), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ location, projectPath }) }); if (!response.ok) throw new Error('Failed to delete rule'); selectedRule = null; await loadRulesData(); renderRulesView(); if (window.showToast) { showToast(t('rules.deleted'), 'success'); } } catch (err) { console.error('Failed to delete rule:', err); if (window.showToast) { showToast(t('rules.deleteError'), 'error'); } } } function editRule(ruleName, location) { // Open edit modal (to be implemented with modal) if (window.showToast) { showToast(t('rules.editNotImplemented'), 'info'); } } // ========== Create Rule Modal ========== var ruleCreateState = { location: 'project', fileName: '', subdirectory: '', isConditional: false, paths: [''], content: '', mode: 'input', generationType: 'description', description: '', extractScope: '', extractFocus: '' }; function openRuleCreateModal() { // Reset state ruleCreateState = { location: 'project', fileName: '', subdirectory: '', isConditional: false, paths: [''], content: '', mode: 'input', generationType: 'description', description: '', extractScope: '', extractFocus: '' }; // Create modal HTML const modalHtml = ` `; // 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; // Update button styles without re-rendering modal const buttons = document.querySelectorAll('.location-btn'); buttons.forEach(btn => { const isProject = btn.querySelector('.font-medium')?.textContent?.includes(t('rules.projectRules')); const isUser = btn.querySelector('.font-medium')?.textContent?.includes(t('rules.userRules')); if ((isProject && location === 'project') || (isUser && location === 'user')) { btn.classList.remove('border-border', 'hover:border-primary/50'); btn.classList.add('border-primary', 'bg-primary/10'); } else { btn.classList.remove('border-primary', 'bg-primary/10'); btn.classList.add('border-border', 'hover:border-primary/50'); } }); } 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 = `
`; pathsList.insertAdjacentHTML('beforeend', pathHtml); if (typeof lucide !== 'undefined') lucide.createIcons(); } } function removeRulePath(index) { ruleCreateState.paths.splice(index, 1); // Re-render paths list without closing modal const pathsList = document.getElementById('rulePathsList'); if (pathsList) { pathsList.innerHTML = ruleCreateState.paths.map((path, idx) => `
${idx > 0 ? ` ` : ''}
`).join(''); if (typeof lucide !== 'undefined') lucide.createIcons(); } } 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'; } // Update mode button styles without re-rendering const modeButtons = document.querySelectorAll('#ruleCreateModal .mode-btn'); modeButtons.forEach(btn => { const btnText = btn.querySelector('.font-medium')?.textContent || ''; const isInput = btnText.includes(t('rules.manualInput')); const isCliGenerate = btnText.includes(t('rules.cliGenerate')); if ((isInput && mode === 'input') || (isCliGenerate && mode === 'cli-generate')) { btn.classList.remove('border-border', 'hover:border-primary/50'); btn.classList.add('border-primary', 'bg-primary/10'); } else { btn.classList.remove('border-primary', 'bg-primary/10'); btn.classList.add('border-border', 'hover:border-primary/50'); } }); } 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'); } } }