mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: auto-update developmentIndex on session archive (closes #58)
- Add updateDevelopmentIndex() function to session-manager.ts - Auto-append entry to developmentIndex when archiving sessions - Add timeline view toggle for Development History section - Support both 'archivedAt' and 'date' field names for compatibility - Add dynamic calculation for statistics (Total Features, Last Updated) - Add CSS styles for timeline view
This commit is contained in:
@@ -300,3 +300,112 @@ body {
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
Development Index Timeline Styles
|
||||
=================================== */
|
||||
|
||||
/* View toggle buttons */
|
||||
.dev-view-btn {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dev-view-btn.active {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.dev-view-btn:hover:not(.active) {
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
/* Development Timeline */
|
||||
.dev-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.dev-timeline-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dev-timeline-marker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
.dev-timeline-dot {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dev-timeline-dot.bg-primary {
|
||||
background: hsl(var(--primary));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dev-timeline-dot.bg-success {
|
||||
background: hsl(var(--success));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dev-timeline-dot.bg-destructive {
|
||||
background: hsl(var(--destructive));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dev-timeline-dot.bg-warning {
|
||||
background: hsl(var(--warning));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.dev-timeline-dot.bg-muted {
|
||||
background: hsl(var(--muted-foreground));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dev-timeline-dot i {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
|
||||
.dev-timeline-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
min-height: 1rem;
|
||||
background: hsl(var(--border));
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.dev-timeline-content {
|
||||
flex: 1;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dev-timeline-content:hover {
|
||||
border-color: hsl(var(--primary) / 0.3);
|
||||
box-shadow: 0 2px 8px hsl(var(--foreground) / 0.05);
|
||||
}
|
||||
|
||||
/* Hidden utility class */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -180,16 +180,16 @@ function renderProjectOverview() {
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="text-center p-4 bg-background rounded-lg">
|
||||
<div class="text-3xl font-bold text-primary mb-1">${project.statistics.total_features || 0}</div>
|
||||
<div class="text-3xl font-bold text-primary mb-1">${calculateTotalFeatures(project.developmentIndex)}</div>
|
||||
<div class="text-sm text-muted-foreground">Total Features</div>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-background rounded-lg">
|
||||
<div class="text-3xl font-bold text-success mb-1">${project.statistics.total_sessions || 0}</div>
|
||||
<div class="text-3xl font-bold text-success mb-1">${workflowData.statistics?.totalSessions || 0}</div>
|
||||
<div class="text-sm text-muted-foreground">Total Sessions</div>
|
||||
</div>
|
||||
<div class="text-center p-4 bg-background rounded-lg">
|
||||
<div class="text-sm text-muted-foreground mb-1">Last Updated</div>
|
||||
<div class="text-sm font-medium text-foreground">${formatDate(project.statistics.last_updated)}</div>
|
||||
<div class="text-sm font-medium text-foreground">${formatDate(getLastUpdatedDate(project.developmentIndex))}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,55 +203,171 @@ function renderDevelopmentIndex(devIndex) {
|
||||
if (!devIndex) return '<p class="text-muted-foreground text-sm">No development history available</p>';
|
||||
|
||||
const categories = [
|
||||
{ key: 'feature', label: 'Features', icon: '<i data-lucide="sparkles" class="w-4 h-4 inline"></i>', badgeClass: 'bg-primary-light text-primary' },
|
||||
{ key: 'enhancement', label: 'Enhancements', icon: '<i data-lucide="zap" class="w-4 h-4 inline"></i>', badgeClass: 'bg-success-light text-success' },
|
||||
{ key: 'bugfix', label: 'Bug Fixes', icon: '<i data-lucide="bug" class="w-4 h-4 inline"></i>', badgeClass: 'bg-destructive/10 text-destructive' },
|
||||
{ key: 'refactor', label: 'Refactorings', icon: '<i data-lucide="wrench" class="w-4 h-4 inline"></i>', badgeClass: 'bg-warning-light text-warning' },
|
||||
{ key: 'docs', label: 'Documentation', icon: '<i data-lucide="book-open" class="w-4 h-4 inline"></i>', badgeClass: 'bg-muted text-muted-foreground' }
|
||||
{ key: 'feature', label: 'Features', icon: '<i data-lucide="sparkles" class="w-4 h-4 inline"></i>', badgeClass: 'bg-primary-light text-primary', color: 'primary' },
|
||||
{ key: 'enhancement', label: 'Enhancements', icon: '<i data-lucide="zap" class="w-4 h-4 inline"></i>', badgeClass: 'bg-success-light text-success', color: 'success' },
|
||||
{ key: 'bugfix', label: 'Bug Fixes', icon: '<i data-lucide="bug" class="w-4 h-4 inline"></i>', badgeClass: 'bg-destructive/10 text-destructive', color: 'destructive' },
|
||||
{ key: 'refactor', label: 'Refactorings', icon: '<i data-lucide="wrench" class="w-4 h-4 inline"></i>', badgeClass: 'bg-warning-light text-warning', color: 'warning' },
|
||||
{ key: 'docs', label: 'Documentation', icon: '<i data-lucide="book-open" class="w-4 h-4 inline"></i>', badgeClass: 'bg-muted text-muted-foreground', color: 'muted' }
|
||||
];
|
||||
|
||||
const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0);
|
||||
// Calculate totals from developmentIndex (dynamic calculation)
|
||||
const totals = categories.reduce((acc, cat) => {
|
||||
acc[cat.key] = (devIndex[cat.key] || []).length;
|
||||
return acc;
|
||||
}, {});
|
||||
const totalEntries = Object.values(totals).reduce((sum, count) => sum + count, 0);
|
||||
|
||||
if (totalEntries === 0) {
|
||||
return '<p class="text-muted-foreground text-sm">No development history entries</p>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="space-y-4">
|
||||
${categories.map(cat => {
|
||||
const entries = devIndex[cat.key] || [];
|
||||
if (entries.length === 0) return '';
|
||||
// Collect all entries with type info for timeline view
|
||||
const allEntries = [];
|
||||
categories.forEach(cat => {
|
||||
(devIndex[cat.key] || []).forEach(entry => {
|
||||
allEntries.push({
|
||||
...entry,
|
||||
type: cat.key,
|
||||
typeLabel: cat.label,
|
||||
typeIcon: cat.icon,
|
||||
typeBadgeClass: cat.badgeClass,
|
||||
typeColor: cat.color,
|
||||
// Support both 'archivedAt' and 'date' field names
|
||||
sortDate: entry.archivedAt || entry.date || entry.implemented_at || ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<span>${cat.icon}</span>
|
||||
<span>${cat.label}</span>
|
||||
<span class="text-xs px-2 py-0.5 ${cat.badgeClass} rounded-full">${entries.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${entries.slice(0, 5).map(entry => `
|
||||
<div class="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
|
||||
<span class="text-xs text-muted-foreground">${formatDate(entry.date)}</span>
|
||||
</div>
|
||||
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
|
||||
${entry.status ? `<span class="px-2 py-0.5 ${entry.status === 'completed' ? 'bg-success-light text-success' : 'bg-warning-light text-warning'} rounded">${escapeHtml(entry.status)}</span>` : ''}
|
||||
</div>
|
||||
// Sort by date descending (newest first)
|
||||
allEntries.sort((a, b) => {
|
||||
const dateA = new Date(a.sortDate || 0).getTime();
|
||||
const dateB = new Date(b.sortDate || 0).getTime();
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
return `
|
||||
<!-- View Toggle & Stats Summary -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
${categories.map(cat => {
|
||||
const count = totals[cat.key];
|
||||
if (count === 0) return '';
|
||||
return `<span class="text-xs px-2 py-1 ${cat.badgeClass} rounded-full flex items-center gap-1">${cat.icon} ${count}</span>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button id="viewCategoryBtn" onclick="toggleDevIndexView('category')" class="px-3 py-1.5 text-xs rounded-lg border border-border bg-background hover:bg-muted transition-colors dev-view-btn active">
|
||||
<i data-lucide="layout-grid" class="w-3.5 h-3.5 inline mr-1"></i>Categories
|
||||
</button>
|
||||
<button id="viewTimelineBtn" onclick="toggleDevIndexView('timeline')" class="px-3 py-1.5 text-xs rounded-lg border border-border bg-background hover:bg-muted transition-colors dev-view-btn">
|
||||
<i data-lucide="git-commit-horizontal" class="w-3.5 h-3.5 inline mr-1"></i>Timeline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category View (default) -->
|
||||
<div id="devIndexCategoryView" class="dev-index-view">
|
||||
<div class="space-y-4">
|
||||
${categories.map(cat => {
|
||||
const entries = devIndex[cat.key] || [];
|
||||
if (entries.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<span>${cat.icon}</span>
|
||||
<span>${cat.label}</span>
|
||||
<span class="text-xs px-2 py-0.5 ${cat.badgeClass} rounded-full">${entries.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${entries.slice(0, 5).map(entry => renderDevIndexEntry(entry, cat)).join('')}
|
||||
${entries.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${entries.length - 5} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline View -->
|
||||
<div id="devIndexTimelineView" class="dev-index-view hidden">
|
||||
<div class="dev-timeline">
|
||||
${allEntries.slice(0, 20).map((entry, index) => `
|
||||
<div class="dev-timeline-item">
|
||||
<div class="dev-timeline-marker">
|
||||
<div class="dev-timeline-dot bg-${entry.typeColor}" title="${entry.typeLabel}">
|
||||
${entry.typeIcon}
|
||||
</div>
|
||||
${index < Math.min(allEntries.length, 20) - 1 ? '<div class="dev-timeline-line"></div>' : ''}
|
||||
</div>
|
||||
<div class="dev-timeline-content">
|
||||
<div class="flex items-start justify-between gap-2 mb-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-0.5 ${entry.typeBadgeClass} rounded">${entry.typeLabel}</span>
|
||||
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
|
||||
</div>
|
||||
`).join('')}
|
||||
${entries.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${entries.length - 5} more</div>` : ''}
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap">${formatDate(entry.sortDate)}</span>
|
||||
</div>
|
||||
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
${entry.sessionId ? `<span class="px-2 py-0.5 bg-muted rounded font-mono">${escapeHtml(entry.sessionId)}</span>` : ''}
|
||||
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
|
||||
${entry.tags && entry.tags.length > 0 ? entry.tags.slice(0, 3).map(tag => `<span class="px-2 py-0.5 bg-accent rounded">${escapeHtml(tag)}</span>`).join('') : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
`).join('')}
|
||||
${allEntries.length > 20 ? `<div class="text-sm text-muted-foreground text-center py-4">... and ${allEntries.length - 20} more entries</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Helper function to render a single development index entry
|
||||
function renderDevIndexEntry(entry, cat) {
|
||||
// Support both 'archivedAt' and 'date' field names
|
||||
const entryDate = entry.archivedAt || entry.date || entry.implemented_at || '';
|
||||
|
||||
return `
|
||||
<div class="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
|
||||
<span class="text-xs text-muted-foreground">${formatDate(entryDate)}</span>
|
||||
</div>
|
||||
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
|
||||
<div class="flex items-center gap-2 text-xs flex-wrap">
|
||||
${entry.sessionId ? `<span class="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">${escapeHtml(entry.sessionId)}</span>` : ''}
|
||||
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
|
||||
${entry.status ? `<span class="px-2 py-0.5 ${entry.status === 'completed' ? 'bg-success-light text-success' : 'bg-warning-light text-warning'} rounded">${escapeHtml(entry.status)}</span>` : ''}
|
||||
${entry.tags && entry.tags.length > 0 ? entry.tags.slice(0, 2).map(tag => `<span class="px-2 py-0.5 bg-accent rounded">${escapeHtml(tag)}</span>`).join('') : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Toggle between category and timeline views
|
||||
function toggleDevIndexView(view) {
|
||||
const categoryView = document.getElementById('devIndexCategoryView');
|
||||
const timelineView = document.getElementById('devIndexTimelineView');
|
||||
const categoryBtn = document.getElementById('viewCategoryBtn');
|
||||
const timelineBtn = document.getElementById('viewTimelineBtn');
|
||||
|
||||
if (view === 'timeline') {
|
||||
categoryView.classList.add('hidden');
|
||||
timelineView.classList.remove('hidden');
|
||||
categoryBtn.classList.remove('active');
|
||||
timelineBtn.classList.add('active');
|
||||
} else {
|
||||
categoryView.classList.remove('hidden');
|
||||
timelineView.classList.add('hidden');
|
||||
categoryBtn.classList.add('active');
|
||||
timelineBtn.classList.remove('active');
|
||||
}
|
||||
|
||||
// Reinitialize icons for the newly visible view
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
}
|
||||
|
||||
function renderProjectGuidelines(guidelines) {
|
||||
if (!guidelines) {
|
||||
return `
|
||||
@@ -401,3 +517,32 @@ function renderLearnings(learnings) {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Calculate total features from developmentIndex (dynamic calculation)
|
||||
function calculateTotalFeatures(devIndex) {
|
||||
if (!devIndex) return 0;
|
||||
const categories = ['feature', 'enhancement', 'bugfix', 'refactor', 'docs'];
|
||||
return categories.reduce((sum, cat) => sum + (devIndex[cat]?.length || 0), 0);
|
||||
}
|
||||
|
||||
// Get the most recent date from developmentIndex entries
|
||||
function getLastUpdatedDate(devIndex) {
|
||||
if (!devIndex) return null;
|
||||
|
||||
const categories = ['feature', 'enhancement', 'bugfix', 'refactor', 'docs'];
|
||||
let latestDate = null;
|
||||
|
||||
categories.forEach(cat => {
|
||||
(devIndex[cat] || []).forEach(entry => {
|
||||
const entryDate = entry.archivedAt || entry.date || entry.implemented_at;
|
||||
if (entryDate) {
|
||||
const date = new Date(entryDate);
|
||||
if (!latestDate || date > latestDate) {
|
||||
latestDate = date;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return latestDate ? latestDate.toISOString() : null;
|
||||
}
|
||||
|
||||
@@ -737,6 +737,11 @@ function executeArchive(params: Params): any {
|
||||
}
|
||||
}
|
||||
|
||||
// Update development index with archived session info
|
||||
if (sessionMetadata) {
|
||||
updateDevelopmentIndex(sessionMetadata);
|
||||
}
|
||||
|
||||
return {
|
||||
operation: 'archive',
|
||||
session_id,
|
||||
@@ -903,6 +908,72 @@ function executeStats(params: Params): any {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the project's development index when a session is archived.
|
||||
* Simplified: only appends entries, does NOT manage statistics.
|
||||
* Dashboard aggregator handles dynamic calculation.
|
||||
*/
|
||||
function updateDevelopmentIndex(sessionMetadata: any): void {
|
||||
if (!sessionMetadata || !sessionMetadata.session_id) {
|
||||
console.warn('Skipping development index update due to missing session metadata.');
|
||||
return;
|
||||
}
|
||||
|
||||
const root = findWorkflowRoot();
|
||||
const projectTechFile = join(root, WORKFLOW_BASE, 'project-tech.json');
|
||||
|
||||
if (!existsSync(projectTechFile)) {
|
||||
console.warn(`Skipping development index update: ${projectTechFile} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectData = readJsonFile(projectTechFile);
|
||||
|
||||
// Ensure development_index exists
|
||||
if (!projectData.development_index) {
|
||||
projectData.development_index = { feature: [], enhancement: [], bugfix: [], refactor: [], docs: [] };
|
||||
}
|
||||
|
||||
// Type inference from description
|
||||
const description = (sessionMetadata.description || '').toLowerCase();
|
||||
let devType: 'feature' | 'enhancement' | 'bugfix' | 'refactor' | 'docs' = 'enhancement';
|
||||
|
||||
if (sessionMetadata.type === 'docs') {
|
||||
devType = 'docs';
|
||||
} else if (/\b(fix|bug|resolve)\b/.test(description)) {
|
||||
devType = 'bugfix';
|
||||
} else if (/\b(feature|implement|add|create)\b/.test(description)) {
|
||||
devType = 'feature';
|
||||
} else if (/\b(refactor|restructure|cleanup)\b/.test(description)) {
|
||||
devType = 'refactor';
|
||||
}
|
||||
|
||||
const entry = {
|
||||
title: sessionMetadata.description || sessionMetadata.project || sessionMetadata.session_id,
|
||||
sessionId: sessionMetadata.session_id,
|
||||
type: devType,
|
||||
tags: sessionMetadata.tags || [],
|
||||
archivedAt: sessionMetadata.archived_at || new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Append to correct category
|
||||
if (!projectData.development_index[devType]) {
|
||||
projectData.development_index[devType] = [];
|
||||
}
|
||||
projectData.development_index[devType].push(entry);
|
||||
|
||||
// CRITICAL: Do NOT touch projectData.statistics
|
||||
// Dashboard aggregator handles dynamic calculation
|
||||
|
||||
writeJsonFile(projectTechFile, projectData);
|
||||
console.log(`Development index updated for session: ${sessionMetadata.session_id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to update development index: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Main Execute Function
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user