UNPKG

@pimzino/claude-code-spec-workflow

Version:

Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent orchestration, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have

410 lines (346 loc) 13.6 kB
// Multi-project dashboard app using shared components PetiteVue.createApp({ // Extend the base state from shared components ...window.DashboardShared.BaseAppState, // Multi dashboard specific state projects: [], selectedProject: null, selectedSpec: null, activeTasks: [], activeTab: 'active', // 'active' or 'projects' connected: false, ws: null, username: 'User', expandedRequirements: {}, expandedDesigns: {}, // Computed properties get activeSessionCount() { return this.projects.filter((p) => p.hasActiveSession).length; }, // Initialize the dashboard async init() { console.log('Multi-project dashboard initializing...'); this.initTheme(); this.setupKeyboardHandlers(); this.connectWebSocket(); }, // Connect to WebSocket for real-time updates connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); this.connected = true; }; this.ws.onmessage = (event) => { const message = JSON.parse(event.data); console.log('WebSocket message:', message); this.handleWebSocketMessage(message); }; this.ws.onclose = () => { console.log('WebSocket disconnected'); this.connected = false; setTimeout(() => this.connectWebSocket(), 2000); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; }, // Handle incoming WebSocket messages handleWebSocketMessage(message) { switch (message.type) { case 'initial': this.projects = Array.isArray(message.data) ? this.normalizeProjects(message.data) : []; this.activeTasks = Array.isArray(message.activeTasks) ? message.activeTasks : []; this.username = message.username || 'User'; console.log( `Received initial data: ${this.projects.length} projects, ${this.activeTasks.length} active tasks` ); // Sort projects: active first, then by activity this.sortProjects(); // Select first project if none selected if (!this.selectedProject && this.projects.length > 0 && this.activeTab === 'projects') { this.selectedProject = this.projects[0]; } break; case 'new-project': // Add new project const newProject = this.normalizeProjects([message.data])[0]; if (!this.projects.find((p) => p.path === newProject.path)) { this.projects.push(newProject); this.sortProjects(); } console.log(`New project appeared: ${message.data.name}`); break; case 'remove-project': // Remove project this.projects = this.projects.filter((p) => p.path !== message.data.path); // If we were viewing the removed project, clear selection if (this.selectedProject?.path === message.data.path) { this.selectedProject = this.projects.length > 0 ? this.projects[0] : null; } break; case 'project-update': const event = message.data; const projectPath = message.projectPath; // Find and update the project const projectIndex = this.projects.findIndex((p) => p.path === projectPath); if (projectIndex >= 0) { const project = this.projects[projectIndex]; switch (event.type) { case 'spec-update': if (project.specs) { const specIndex = project.specs.findIndex((s) => s.name === event.data.name); if (specIndex >= 0) { project.specs[specIndex] = event.data; } else { project.specs.push(event.data); } } break; case 'status-update': project.hasActiveSession = event.data.hasActiveSession; project.lastActivity = event.data.lastActivity; project.isClaudeActive = event.data.isClaudeActive; break; case 'git-update': project.gitBranch = event.data.branch; project.gitCommit = event.data.commit; break; case 'steering-update': project.steering = event.data; break; } this.sortProjects(); } break; case 'active-tasks-update': this.activeTasks = Array.isArray(message.data) ? message.data : []; console.log(`Active tasks updated: ${this.activeTasks.length} tasks`, this.activeTasks); break; case 'bug-update': const bugEvent = message.data; const bugProjectPath = message.projectPath; // Find the project const bugProjectIndex = this.projects.findIndex((p) => p.path === bugProjectPath); if (bugProjectIndex >= 0) { const project = this.projects[bugProjectIndex]; if (!project.bugs) { project.bugs = []; } if (bugEvent.data) { // Add or update bug const bugIndex = project.bugs.findIndex((b) => b.name === bugEvent.data.name); if (bugIndex >= 0) { project.bugs[bugIndex] = bugEvent.data; } else { project.bugs.push(bugEvent.data); } } else if (bugEvent.type === 'removed') { // Remove bug project.bugs = project.bugs.filter((b) => b.name !== bugEvent.bug); } } break; default: console.log('Unknown message type:', message.type); } }, // Switch between tabs switchTab(tab) { this.activeTab = tab; }, // Select a project from active task selectProjectFromTask(projectPath, specName) { const project = this.projects.find((p) => p.path === projectPath); if (project) { this.selectedProject = project; this.activeTab = 'projects'; // Find and expand the spec if (project.specs) { const spec = project.specs.find((s) => s.name === specName); if (spec) { this.selectedSpec = spec; } } } }, // Project-specific methods getSpecsInProgress(project) { if (!project.specs) return 0; return project.specs.filter((s) => s.status === 'in-progress').length; }, getSpecsCompleted(project) { if (!project.specs) return 0; return project.specs.filter((s) => s.status === 'completed').length; }, getTotalTasks(project) { if (!project.specs) return 0; return project.specs.reduce((total, spec) => { return total + (spec.tasks?.total || 0); }, 0); }, getOpenSpecsCount(project) { if (!project.specs) return 0; return project.specs.filter((s) => s.status !== 'completed').length; }, getOpenBugsCount(project) { if (!project.bugs) return 0; return project.bugs.filter((b) => b.status !== 'resolved').length; }, getBugsInProgress(project) { if (!project.bugs) return 0; return project.bugs.filter((b) => ['analyzing', 'fixing', 'verifying'].includes(b.status)).length; }, getBugsResolved(project) { if (!project.bugs) return 0; return project.bugs.filter((b) => b.status === 'resolved').length; }, // Sort projects: active sessions first, then by last activity sortProjects() { this.projects.sort((a, b) => { // Active sessions come first if (a.hasActiveSession && !b.hasActiveSession) return -1; if (!a.hasActiveSession && b.hasActiveSession) return 1; // Then sort by last activity const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0); const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0); return dateB - dateA; }); }, // Active task helpers getTaskNumber(activeTask) { const spec = this.projects .find((p) => p.path === activeTask.projectPath) ?.specs?.find((s) => s.name === activeTask.specName); if (!spec?.tasks?.taskList) return 1; const taskIndex = spec.tasks.taskList.findIndex((t) => t.id === activeTask.task.id); return taskIndex >= 0 ? taskIndex + 1 : 1; }, getSpecTaskCount(activeTask) { const spec = this.projects .find((p) => p.path === activeTask.projectPath) ?.specs?.find((s) => s.name === activeTask.specName); return spec?.tasks?.total || 0; }, getSpecProgress(activeTask) { const spec = this.projects .find((p) => p.path === activeTask.projectPath) ?.specs?.find((s) => s.name === activeTask.specName); if (!spec?.tasks) return 0; return (spec.tasks.completed / spec.tasks.total) * 100; }, getNextTask(activeTask) { const spec = this.projects .find((p) => p.path === activeTask.projectPath) ?.specs?.find((s) => s.name === activeTask.specName); if (!spec?.tasks?.taskList) return null; const currentIndex = spec.tasks.taskList.findIndex((t) => t.id === activeTask.task.id); if (currentIndex >= 0 && currentIndex < spec.tasks.taskList.length - 1) { const nextTask = spec.tasks.taskList[currentIndex + 1]; if (!nextTask.completed) return nextTask; } return spec.tasks.taskList.find((t) => !t.completed && t.id !== activeTask.task.id); }, // Requirements expand/collapse toggleRequirementsExpanded(specName) { if (this.expandedRequirements[specName]) { delete this.expandedRequirements[specName]; } else { this.expandedRequirements[specName] = true; } }, isRequirementsExpanded(specName) { return !!this.expandedRequirements[specName]; }, // Design expand/collapse toggleDesignExpanded(specName) { if (this.expandedDesigns[specName]) { delete this.expandedDesigns[specName]; } else { this.expandedDesigns[specName] = true; } }, isDesignExpanded(specName) { return !!this.expandedDesigns[specName]; }, // Normalize project data normalizeProjects(projects) { return projects.map((project) => { if (project.specs) { project.specs = project.specs.map((spec) => { // Initialize completed tasks as collapsed by default if (spec.tasks && this.getCompletedTaskCount(spec) > 0) { this.collapsedCompletedTasks[spec.name] = true; } // Ensure requirements content is properly handled if (spec.requirements && spec.requirements.content) { // Parse requirements if needed if (typeof spec.requirements.content === 'string') { try { spec.requirements.content = JSON.parse(spec.requirements.content); } catch (e) { console.warn('Failed to parse requirements content:', e); } } } // Ensure design content is properly parsed if (spec.design && spec.design.codeReuseContent) { if (typeof spec.design.codeReuseContent === 'string') { try { spec.design.codeReuseContent = JSON.parse(spec.design.codeReuseContent); } catch (e) { console.warn('Failed to parse design content:', e); } } } return spec; }); } return project; }); }, // Override viewMarkdown to include projectPath async viewMarkdown(specName, docType) { if (!this.selectedProject) return; return window.DashboardShared.BaseAppState.viewMarkdown.call( this, specName, docType, this.selectedProject.path ); }, // View bug markdown async viewBugMarkdown(projectPath, bugName, docType) { this.markdownPreview.loading = true; this.markdownPreview.show = true; this.markdownPreview.title = `${bugName} - ${docType}`; this.markdownPreview.content = 'Loading...'; try { const encodedPath = encodeURIComponent(projectPath); const response = await fetch(`/api/projects/${encodedPath}/bugs/${bugName}/${docType}`); if (!response.ok) throw new Error('Failed to fetch document'); const data = await response.json(); const html = marked.parse(data.content); this.markdownPreview.content = html; } catch (error) { console.error('Error loading bug markdown:', error); this.markdownPreview.content = '<p class="text-red-500">Error loading document</p>'; } finally { this.markdownPreview.loading = false; } }, // Get current task for a spec getCurrentTask(spec) { if (!spec.tasks || !spec.tasks.taskList || !spec.tasks.inProgress) return null; return spec.tasks.taskList.find(task => task.id === spec.tasks.inProgress); }, // Scroll to a specific requirement scrollToRequirement(specName, requirementId) { const element = document.getElementById(`${specName}-req-${requirementId}`); if (element) { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); } } }).mount('#app');