UNPKG

conductor-tasks

Version:

Task Manager for AI Development

385 lines 18.5 kB
import chalk from 'chalk'; import { TaskPriority, TaskStatus } from '../core/types.js'; export class TaskVisualizer { constructor(taskManager) { this.taskManager = taskManager; } displayKanbanBoard(options = {}) { const { showPriority = true, showComplexity = true, filterPriority, filterTag, compact = false } = options; let tasks = this.taskManager.getTasks(); if (filterPriority) { tasks = tasks.filter(task => task.priority === filterPriority); } if (filterTag) { tasks = tasks.filter(task => task.tags.includes(filterTag)); } const tasksByStatus = new Map(); Object.values(TaskStatus).forEach(status => { tasksByStatus.set(status, []); }); tasks.forEach(task => { const statusTasks = tasksByStatus.get(task.status) || []; statusTasks.push(task); }); tasksByStatus.forEach((statusTasks, status) => { tasksByStatus.set(status, statusTasks.sort((a, b) => { const priorityOrder = { [TaskPriority.CRITICAL]: 0, [TaskPriority.HIGH]: 1, [TaskPriority.MEDIUM]: 2, [TaskPriority.LOW]: 3, [TaskPriority.BACKLOG]: 4 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; })); }); const terminalWidth = process.stdout.columns || 100; const numColumns = Object.keys(TaskStatus).length; const columnWidth = Math.floor((terminalWidth - (numColumns * 2)) / numColumns); const totalTasks = tasks.length; const doneTasks = tasksByStatus.get(TaskStatus.DONE)?.length || 0; const progressPercentage = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 100) : 0; let output = ''; output += '\n' + chalk.bold.blue('╔═══════════════════════════════════════════════════════════════╗') + '\n'; output += chalk.bold.blue('║ ') + chalk.bold.white('TASK BOARD') + chalk.bold.blue(' '.repeat(terminalWidth - 13) + '║') + '\n'; output += chalk.bold.blue('╚═══════════════════════════════════════════════════════════════╝') + '\n\n'; output += chalk.white(`Total Tasks: ${totalTasks} | Progress: ${progressPercentage}% complete\n\n`); const headers = Object.values(TaskStatus).map(status => { const count = tasksByStatus.get(status)?.length || 0; return `${this.formatStatusText(status)} (${count})`; }); headers.forEach((header, index) => { const paddedHeader = this.padCenter(header, columnWidth); output += chalk.bold.white(paddedHeader); if (index < headers.length - 1) { output += ' │ '; } }); output += '\n' + '─'.repeat(terminalWidth) + '\n'; const maxTasks = Math.max(...Array.from(tasksByStatus.values()).map(tasks => tasks.length), 1); const maxTasksToShow = Math.min(maxTasks, 20); for (let i = 0; i < maxTasksToShow; i++) { Object.values(TaskStatus).forEach((status, statusIndex) => { const statusTasks = tasksByStatus.get(status) || []; const task = statusTasks[i]; if (task) { let taskDisplay = compact ? this.formatTaskCompact(task, { showPriority, showComplexity }) : this.formatTaskEnhanced(task, { showPriority, showComplexity, columnWidth }); output += taskDisplay; } else { output += ' '.repeat(columnWidth); } if (statusIndex < Object.values(TaskStatus).length - 1) { output += ' │ '; } }); output += '\n'; if (i < maxTasksToShow - 1) { Object.values(TaskStatus).forEach((_, statusIndex) => { output += '─'.repeat(columnWidth); if (statusIndex < Object.values(TaskStatus).length - 1) { output += '─┼─'; } }); output += '\n'; } } const hiddenTasks = maxTasks - maxTasksToShow; if (hiddenTasks > 0) { output += '\n' + chalk.italic.gray(`... and ${hiddenTasks} more tasks not shown. Use filters to narrow results.`) + '\n'; } output += '═'.repeat(terminalWidth) + '\n'; if (showPriority) { output += chalk.bold('\nPriority: '); output += chalk.red('● CRITICAL ') + chalk.yellow('● HIGH ') + chalk.blue('● MEDIUM ') + chalk.green('● LOW ') + chalk.gray('● BACKLOG\n'); } if (filterPriority || filterTag) { output += chalk.italic('\nActive Filters: '); if (filterPriority) output += chalk.white(`Priority=${filterPriority} `); if (filterTag) output += chalk.white(`Tag=${filterTag} `); output += '\n'; } return output; } displayDependencyTree(rootTaskId) { if (rootTaskId) { const depTree = this.taskManager.getTaskDependencyTree?.(rootTaskId); if (!depTree) { console.log(chalk.red(`Task with ID ${rootTaskId} not found`)); return; } console.log(chalk.bold.blue(`\n=== Dependency Tree for "${depTree.task.title}" ===\n`)); this.printTaskWithDependencies(depTree.task, depTree.dependencies, '', true); } else { const allTasks = this.taskManager.getTasks(); const dependentTasks = new Set(); allTasks.forEach(task => { task.dependencies.forEach(depId => { dependentTasks.add(depId); }); }); const topLevelTasks = allTasks.filter(task => !dependentTasks.has(task.id)); console.log(chalk.bold.blue('\n=== Task Dependency Trees ===\n')); if (topLevelTasks.length === 0) { console.log(chalk.yellow('No top-level tasks found. You might have circular dependencies.')); } else { topLevelTasks.forEach((task, index) => { const depTree = this.taskManager.getTaskDependencyTree?.(task.id); if (depTree) { this.printTaskWithDependencies(depTree.task, depTree.dependencies, '', index === topLevelTasks.length - 1); } if (index < topLevelTasks.length - 1) { console.log(); } }); } } console.log(); } displayDashboard() { const allTasks = this.taskManager.getTasks(); const totalTasks = allTasks.length; let output = ''; if (totalTasks === 0) { return chalk.yellow('\nNo tasks found. Create tasks with the "create-task" command.\n'); } const tasksByStatus = new Map(); Object.values(TaskStatus).forEach(status => { tasksByStatus.set(status, 0); }); allTasks.forEach(task => { const count = tasksByStatus.get(task.status) || 0; tasksByStatus.set(task.status, count + 1); }); const tasksByPriority = new Map(); Object.values(TaskPriority).forEach(priority => { tasksByPriority.set(priority, 0); }); allTasks.forEach(task => { const count = tasksByPriority.get(task.priority) || 0; tasksByPriority.set(task.priority, count + 1); }); const tasksNeedingAttention = this.taskManager.getTasksNeedingAttention?.() || []; const recentTasks = [...allTasks] .sort((a, b) => b.updatedAt - a.updatedAt) .slice(0, 5); const tagCounts = new Map(); allTasks.forEach(task => { task.tags.forEach(tag => { tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); }); }); const topTags = Array.from(tagCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5); const completedTasks = tasksByStatus.get(TaskStatus.DONE) || 0; const progressPercentage = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; const progressBarWidth = 50; const filledWidth = Math.round((progressPercentage / 100) * progressBarWidth); const progressBar = '[' + '█'.repeat(filledWidth) + '·'.repeat(progressBarWidth - filledWidth) + ']'; output += '\n' + chalk.bold.blue('╔══════════════════════════════════════════════════════════╗') + '\n'; output += chalk.bold.blue('║ ') + chalk.bold.white('PROJECT DASHBOARD') + chalk.bold.blue(' '.repeat(31) + '║') + '\n'; output += chalk.bold.blue('╚══════════════════════════════════════════════════════════╝') + '\n\n'; output += chalk.bold.white('📊 Project Overview') + '\n'; output += chalk.white('─'.repeat(80)) + '\n'; output += chalk.white(`Total Tasks: ${totalTasks}`) + '\n'; output += chalk.white(`Progress: ${progressPercentage}% complete`) + '\n'; output += chalk.white(`${progressBar} ${progressPercentage}%`) + '\n\n'; output += chalk.bold.white('📋 Task Status') + '\n'; output += chalk.white('─'.repeat(80)) + '\n'; tasksByStatus.forEach((count, status) => { const percentage = totalTasks > 0 ? ((count / totalTasks) * 100).toFixed(1) : '0.0'; const barWidth = Math.round((count / totalTasks) * 40); const statusColor = this.getStatusColor(status); const bar = '█'.repeat(barWidth); output += `${statusColor(status.padEnd(12))}: ${count.toString().padStart(3)} (${percentage.padStart(4)}%) ${chalk.white(bar)}\n`; }); output += '\n'; output += chalk.bold.white('🔥 Task Priority') + '\n'; output += chalk.white('─'.repeat(80)) + '\n'; tasksByPriority.forEach((count, priority) => { const percentage = totalTasks > 0 ? ((count / totalTasks) * 100).toFixed(1) : '0.0'; const barWidth = Math.round((count / totalTasks) * 40); const priorityColor = this.getPriorityColor(priority); const bar = '█'.repeat(barWidth); output += `${priorityColor(priority.padEnd(12))}: ${count.toString().padStart(3)} (${percentage.padStart(4)}%) ${chalk.white(bar)}\n`; }); output += '\n'; if (topTags.length > 0) { output += chalk.bold.white('🏷️ Top Tags') + '\n'; output += chalk.white('─'.repeat(80)) + '\n'; topTags.forEach(([tag, count]) => { const percentage = totalTasks > 0 ? ((count / totalTasks) * 100).toFixed(1) : '0.0'; output += `${chalk.cyan(tag.padEnd(15))}: ${count.toString().padStart(3)} (${percentage.padStart(4)}%)\n`; }); output += '\n'; } if (tasksNeedingAttention.length > 0) { output += chalk.bold.yellow('⚠️ Tasks Needing Attention') + '\n'; output += chalk.white('─'.repeat(80)) + '\n'; tasksNeedingAttention.slice(0, 5).forEach((task, index) => { const statusColor = this.getStatusColor(task.status); const priorityColor = this.getPriorityColor(task.priority); output += `${index + 1}. ${chalk.bold.white(task.title)}\n`; output += ` ID: ${chalk.gray(task.id)} | ${statusColor(task.status)} | ${priorityColor(task.priority)}\n`; if (task.tags.length > 0) { output += ` Tags: ${task.tags.map(tag => chalk.cyan(tag)).join(', ')}\n`; } if (index < tasksNeedingAttention.length - 1) { output += '\n'; } }); if (tasksNeedingAttention.length > 5) { output += chalk.gray(`\n...and ${tasksNeedingAttention.length - 5} more tasks needing attention.\n`); } output += '\n'; } if (recentTasks.length > 0) { output += chalk.bold.white('🕒 Recent Activity') + '\n'; output += chalk.white('─'.repeat(80)) + '\n'; recentTasks.forEach((task, index) => { const statusColor = this.getStatusColor(task.status); const date = new Date(task.updatedAt).toLocaleString(); output += `${chalk.bold.white(task.title)} ${chalk.gray(`(${date})`)}\n`; output += ` ID: ${chalk.gray(task.id)} | ${statusColor(task.status)} | Updated: ${task.updatedAt !== task.createdAt ? 'Yes' : 'No'}\n`; if (index < recentTasks.length - 1) { output += '\n'; } }); } return output; } printTaskWithDependencies(task, dependencies, prefix, isLast) { const branch = isLast ? '└── ' : '├── '; const newPrefix = prefix + (isLast ? ' ' : '│ '); const statusColor = this.getStatusColor(task.status); const priorityColor = this.getPriorityColor(task.priority); console.log(`${prefix}${branch}${chalk.bold(task.title)} ` + `[${statusColor(task.status)}, ${priorityColor(task.priority)}]`); dependencies.forEach((depTask, index) => { const depIsLast = index === dependencies.length - 1; this.printTaskWithDependencies(depTask, this.taskManager.getTaskDependencyTree?.(depTask.id)?.dependencies || [], newPrefix, depIsLast); }); } formatTaskCompact(task, options) { const { showPriority, showComplexity } = options; const statusColor = this.getStatusColor(task.status); const priorityColor = this.getPriorityColor(task.priority); let formattedTask = chalk.bold(task.title); const indicators = []; if (showPriority) { indicators.push(priorityColor(`[${task.priority.charAt(0).toUpperCase()}]`)); } if (showComplexity) { indicators.push(chalk.gray(`[C${task.complexity}]`)); } if (indicators.length > 0) { formattedTask += ' ' + indicators.join(' '); } return formattedTask; } formatTaskEnhanced(task, options) { const { showPriority, showComplexity, columnWidth } = options; const maxTitleLength = columnWidth - 6; let title = task.title; if (title.length > maxTitleLength) { title = title.substring(0, maxTitleLength - 3) + '...'; } let taskDisplay = ''; if (showPriority) { taskDisplay += this.getPriorityIndicator(task.priority) + ' '; } taskDisplay += chalk.bold(title); if (showComplexity && task.complexity) { const complexityIndicator = this.getComplexityIndicator(task.complexity); taskDisplay += ' ' + complexityIndicator; } const shortId = task.id.split('-').pop() || task.id; taskDisplay += ' ' + chalk.gray(`#${shortId}`); if (taskDisplay.length > columnWidth) { taskDisplay = taskDisplay.substring(0, columnWidth - 3) + '...'; } else { taskDisplay = taskDisplay.padEnd(columnWidth); } return taskDisplay; } getPriorityIndicator(priority) { switch (priority) { case TaskPriority.CRITICAL: return chalk.bgRed.white('!'); case TaskPriority.HIGH: return chalk.red('●'); case TaskPriority.MEDIUM: return chalk.yellow('●'); case TaskPriority.LOW: return chalk.blue('●'); case TaskPriority.BACKLOG: return chalk.gray('●'); default: return ' '; } } getComplexityIndicator(complexity) { if (complexity <= 3) { return chalk.green(`⦿${complexity}`); } else if (complexity <= 6) { return chalk.yellow(`⦿${complexity}`); } else { return chalk.red(`⦿${complexity}`); } } formatStatusText(status) { const statusColor = this.getStatusColor(status); return statusColor(status.toUpperCase()); } getStatusColor(status) { switch (status) { case TaskStatus.DONE: return chalk.green; case TaskStatus.IN_PROGRESS: return chalk.blue; case TaskStatus.REVIEW: return chalk.cyan; case TaskStatus.TODO: return chalk.yellow; case TaskStatus.BLOCKED: return chalk.red; case TaskStatus.BACKLOG: default: return chalk.gray; } } getPriorityColor(priority) { switch (priority) { case TaskPriority.CRITICAL: return chalk.red.bold; case TaskPriority.HIGH: return chalk.red; case TaskPriority.MEDIUM: return chalk.yellow; case TaskPriority.LOW: return chalk.green; case TaskPriority.BACKLOG: default: return chalk.gray; } } padCenter(text, width) { const padding = width - text.length; if (padding <= 0) return text; const leftPadding = Math.floor(padding / 2); const rightPadding = padding - leftPadding; return ' '.repeat(leftPadding) + text + ' '.repeat(rightPadding); } } //# sourceMappingURL=visualizer.js.map