From 059ce5e716692d829b58ed98e8e8c9ecb2d16bcd Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Mon, 31 Mar 2025 15:35:48 -0400 Subject: [PATCH] Enhance progress bars with status breakdown, improve readability, optimize display width, and update changeset --- .changeset/two-bats-smoke.md | 5 + scripts/modules/task-manager.js | 51 ++++++-- scripts/modules/ui.js | 205 ++++++++++++++++++++++++++++---- tasks/task_023.txt | 2 +- tasks/tasks.json | 2 +- 5 files changed, 234 insertions(+), 31 deletions(-) diff --git a/.changeset/two-bats-smoke.md b/.changeset/two-bats-smoke.md index cb38a808..89160b31 100644 --- a/.changeset/two-bats-smoke.md +++ b/.changeset/two-bats-smoke.md @@ -25,3 +25,8 @@ - Enhance task show view with a color-coded progress bar for visualizing subtask completion percentage - Add "cancelled" status to UI module status configurations for marking tasks as cancelled without deletion - Improve MCP server resource documentation with comprehensive implementation examples and best practices +- Enhance progress bars with status breakdown visualization showing proportional sections for different task statuses +- Add improved status tracking for both tasks and subtasks with detailed counts by status +- Optimize progress bar display with width constraints to prevent UI overflow on smaller terminals +- Improve status counts display with clear text labels beside status icons for better readability +- Treat deferred and cancelled tasks as effectively complete for progress calculation while maintaining visual distinction diff --git a/scripts/modules/task-manager.js b/scripts/modules/task-manager.js index 3df5a44c..9abb1f37 100644 --- a/scripts/modules/task-manager.js +++ b/scripts/modules/task-manager.js @@ -1014,22 +1014,33 @@ function listTasks(tasksPath, statusFilter, withSubtasks = false, outputFormat = task.status === 'done' || task.status === 'completed').length; const completionPercentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; - // Count statuses + // Count statuses for tasks const doneCount = completedTasks; const inProgressCount = data.tasks.filter(task => task.status === 'in-progress').length; const pendingCount = data.tasks.filter(task => task.status === 'pending').length; const blockedCount = data.tasks.filter(task => task.status === 'blocked').length; const deferredCount = data.tasks.filter(task => task.status === 'deferred').length; + const cancelledCount = data.tasks.filter(task => task.status === 'cancelled').length; - // Count subtasks + // Count subtasks and their statuses let totalSubtasks = 0; let completedSubtasks = 0; + let inProgressSubtasks = 0; + let pendingSubtasks = 0; + let blockedSubtasks = 0; + let deferredSubtasks = 0; + let cancelledSubtasks = 0; data.tasks.forEach(task => { if (task.subtasks && task.subtasks.length > 0) { totalSubtasks += task.subtasks.length; completedSubtasks += task.subtasks.filter(st => st.status === 'done' || st.status === 'completed').length; + inProgressSubtasks += task.subtasks.filter(st => st.status === 'in-progress').length; + pendingSubtasks += task.subtasks.filter(st => st.status === 'pending').length; + blockedSubtasks += task.subtasks.filter(st => st.status === 'blocked').length; + deferredSubtasks += task.subtasks.filter(st => st.status === 'deferred').length; + cancelledSubtasks += task.subtasks.filter(st => st.status === 'cancelled').length; } }); @@ -1064,10 +1075,16 @@ function listTasks(tasksPath, statusFilter, withSubtasks = false, outputFormat = pending: pendingCount, blocked: blockedCount, deferred: deferredCount, + cancelled: cancelledCount, completionPercentage, subtasks: { total: totalSubtasks, completed: completedSubtasks, + inProgress: inProgressSubtasks, + pending: pendingSubtasks, + blocked: blockedSubtasks, + deferred: deferredSubtasks, + cancelled: cancelledSubtasks, completionPercentage: subtaskCompletionPercentage } } @@ -1076,9 +1093,26 @@ function listTasks(tasksPath, statusFilter, withSubtasks = false, outputFormat = // ... existing code for text output ... - // Create progress bars - const taskProgressBar = createProgressBar(completionPercentage, 30); - const subtaskProgressBar = createProgressBar(subtaskCompletionPercentage, 30); + // Calculate status breakdowns as percentages of total + const taskStatusBreakdown = { + 'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0, + 'pending': totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0, + 'blocked': totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0, + 'deferred': totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0, + 'cancelled': totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0 + }; + + const subtaskStatusBreakdown = { + 'in-progress': totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0, + 'pending': totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0, + 'blocked': totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0, + 'deferred': totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0, + 'cancelled': totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0 + }; + + // Create progress bars with status breakdowns + const taskProgressBar = createProgressBar(completionPercentage, 30, taskStatusBreakdown); + const subtaskProgressBar = createProgressBar(subtaskCompletionPercentage, 30, subtaskStatusBreakdown); // Calculate dependency statistics const completedTaskIds = new Set(data.tasks.filter(t => @@ -1163,9 +1197,9 @@ function listTasks(tasksPath, statusFilter, withSubtasks = false, outputFormat = const projectDashboardContent = chalk.white.bold('Project Dashboard') + '\n' + `Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` + - `Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)}\n\n` + + `Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` + `Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` + - `Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} Remaining: ${chalk.yellow(totalSubtasks - completedSubtasks)}\n\n` + + `Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` + chalk.cyan.bold('Priority Breakdown:') + '\n' + `${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter(t => t.priority === 'high').length}\n` + `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter(t => t.priority === 'medium').length}\n` + @@ -1454,7 +1488,8 @@ function listTasks(tasksPath, statusFilter, withSubtasks = false, outputFormat = 'pending': chalk.yellow, 'in-progress': chalk.blue, 'deferred': chalk.gray, - 'blocked': chalk.red + 'blocked': chalk.red, + 'cancelled': chalk.gray }; const statusColor = statusColors[status.toLowerCase()] || chalk.white; return `${chalk.cyan(`${nextTask.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`; diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index 7e85cc0b..974d3cb8 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -79,34 +79,112 @@ function stopLoadingIndicator(spinner) { } /** - * Create a progress bar using ASCII characters - * @param {number} percent - Progress percentage (0-100) - * @param {number} length - Length of the progress bar in characters - * @returns {string} Formatted progress bar + * Create a colored progress bar + * @param {number} percent - The completion percentage + * @param {number} length - The total length of the progress bar in characters + * @param {Object} statusBreakdown - Optional breakdown of non-complete statuses (e.g., {pending: 20, 'in-progress': 10}) + * @returns {string} The formatted progress bar */ -function createProgressBar(percent, length = 30) { - const filled = Math.round(percent * length / 100); - const empty = length - filled; +function createProgressBar(percent, length = 30, statusBreakdown = null) { + // Adjust the percent to treat deferred and cancelled as complete + const effectivePercent = statusBreakdown ? + Math.min(100, percent + (statusBreakdown.deferred || 0) + (statusBreakdown.cancelled || 0)) : + percent; + + // Calculate how many characters to fill for "true completion" + const trueCompletedFilled = Math.round(percent * length / 100); - // Determine color based on percentage - let barColor; + // Calculate how many characters to fill for "effective completion" (including deferred/cancelled) + const effectiveCompletedFilled = Math.round(effectivePercent * length / 100); + + // The "deferred/cancelled" section (difference between true and effective) + const deferredCancelledFilled = effectiveCompletedFilled - trueCompletedFilled; + + // Set the empty section (remaining after effective completion) + const empty = length - effectiveCompletedFilled; + + // Determine color based on percentage for the completed section + let completedColor; if (percent < 25) { - barColor = chalk.red; + completedColor = chalk.red; } else if (percent < 50) { - barColor = chalk.hex('#FFA500'); // Orange + completedColor = chalk.hex('#FFA500'); // Orange } else if (percent < 75) { - barColor = chalk.yellow; + completedColor = chalk.yellow; } else if (percent < 100) { - barColor = chalk.green; + completedColor = chalk.green; } else { - barColor = chalk.hex('#006400'); // Dark green + completedColor = chalk.hex('#006400'); // Dark green } - const filledBar = barColor('█'.repeat(filled)); - const emptyBar = chalk.gray('░'.repeat(empty)); + // Create colored sections + const completedSection = completedColor('█'.repeat(trueCompletedFilled)); - // Use the same color for the percentage text - return `${filledBar}${emptyBar} ${barColor(`${percent.toFixed(0)}%`)}`; + // Gray section for deferred/cancelled items + const deferredCancelledSection = chalk.gray('█'.repeat(deferredCancelledFilled)); + + // If we have a status breakdown, create a multi-colored remaining section + let remainingSection = ''; + + if (statusBreakdown && empty > 0) { + // Status colors (matching the statusConfig colors in getStatusWithColor) + const statusColors = { + 'pending': chalk.yellow, + 'in-progress': chalk.hex('#FFA500'), // Orange + 'blocked': chalk.red, + 'review': chalk.magenta, + // Deferred and cancelled are treated as part of the completed section + }; + + // Calculate proportions for each status + const totalRemaining = Object.entries(statusBreakdown) + .filter(([status]) => !['deferred', 'cancelled', 'done', 'completed'].includes(status)) + .reduce((sum, [_, val]) => sum + val, 0); + + // If no remaining tasks with tracked statuses, just use gray + if (totalRemaining <= 0) { + remainingSection = chalk.gray('░'.repeat(empty)); + } else { + // Track how many characters we've added + let addedChars = 0; + + // Add each status section proportionally + for (const [status, percentage] of Object.entries(statusBreakdown)) { + // Skip statuses that are considered complete + if (['deferred', 'cancelled', 'done', 'completed'].includes(status)) continue; + + // Calculate how many characters this status should fill + const statusChars = Math.round((percentage / totalRemaining) * empty); + + // Make sure we don't exceed the total length due to rounding + const actualChars = Math.min(statusChars, empty - addedChars); + + // Add colored section for this status + const colorFn = statusColors[status] || chalk.gray; + remainingSection += colorFn('░'.repeat(actualChars)); + + addedChars += actualChars; + } + + // If we have any remaining space due to rounding, fill with gray + if (addedChars < empty) { + remainingSection += chalk.gray('░'.repeat(empty - addedChars)); + } + } + } else { + // Default to gray for the empty section if no breakdown provided + remainingSection = chalk.gray('░'.repeat(empty)); + } + + // Effective percentage text color should reflect the highest category + const percentTextColor = percent === 100 ? + chalk.hex('#006400') : // Dark green for 100% + (effectivePercent === 100 ? + chalk.gray : // Gray for 100% with deferred/cancelled + completedColor); // Otherwise match the completed color + + // Build the complete progress bar + return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`; } /** @@ -711,6 +789,61 @@ async function displayTaskById(tasksPath, taskId) { { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } } )); + // Calculate and display subtask completion progress + if (task.subtasks && task.subtasks.length > 0) { + const totalSubtasks = task.subtasks.length; + const completedSubtasks = task.subtasks.filter(st => + st.status === 'done' || st.status === 'completed' + ).length; + + // Count other statuses for the subtasks + const inProgressSubtasks = task.subtasks.filter(st => st.status === 'in-progress').length; + const pendingSubtasks = task.subtasks.filter(st => st.status === 'pending').length; + const blockedSubtasks = task.subtasks.filter(st => st.status === 'blocked').length; + const deferredSubtasks = task.subtasks.filter(st => st.status === 'deferred').length; + const cancelledSubtasks = task.subtasks.filter(st => st.status === 'cancelled').length; + + // Calculate status breakdown as percentages + const statusBreakdown = { + 'in-progress': (inProgressSubtasks / totalSubtasks) * 100, + 'pending': (pendingSubtasks / totalSubtasks) * 100, + 'blocked': (blockedSubtasks / totalSubtasks) * 100, + 'deferred': (deferredSubtasks / totalSubtasks) * 100, + 'cancelled': (cancelledSubtasks / totalSubtasks) * 100 + }; + + const completionPercentage = (completedSubtasks / totalSubtasks) * 100; + + // Calculate appropriate progress bar length based on terminal width + // Subtract padding (2), borders (2), and the percentage text (~5) + const availableWidth = process.stdout.columns || 80; // Default to 80 if can't detect + const boxPadding = 2; // 1 on each side + const boxBorders = 2; // 1 on each side + const percentTextLength = 5; // ~5 chars for " 100%" + // Reduce the length by adjusting the subtraction value from 20 to 35 + const progressBarLength = Math.max(20, Math.min(60, availableWidth - boxPadding - boxBorders - percentTextLength - 35)); // Min 20, Max 60 + + // Status counts for display + const statusCounts = + `${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` + + `${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`; + + console.log(boxen( + chalk.white.bold('Subtask Progress:') + '\n\n' + + `${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + + `${statusCounts}\n` + + `${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 1, bottom: 0 }, + width: Math.min(availableWidth - 10, 100), // Add width constraint to limit the box width + textAlignment: 'left' + } + )); + } + return; } @@ -875,6 +1008,22 @@ async function displayTaskById(tasksPath, taskId) { st.status === 'done' || st.status === 'completed' ).length; + // Count other statuses for the subtasks + const inProgressSubtasks = task.subtasks.filter(st => st.status === 'in-progress').length; + const pendingSubtasks = task.subtasks.filter(st => st.status === 'pending').length; + const blockedSubtasks = task.subtasks.filter(st => st.status === 'blocked').length; + const deferredSubtasks = task.subtasks.filter(st => st.status === 'deferred').length; + const cancelledSubtasks = task.subtasks.filter(st => st.status === 'cancelled').length; + + // Calculate status breakdown as percentages + const statusBreakdown = { + 'in-progress': (inProgressSubtasks / totalSubtasks) * 100, + 'pending': (pendingSubtasks / totalSubtasks) * 100, + 'blocked': (blockedSubtasks / totalSubtasks) * 100, + 'deferred': (deferredSubtasks / totalSubtasks) * 100, + 'cancelled': (cancelledSubtasks / totalSubtasks) * 100 + }; + const completionPercentage = (completedSubtasks / totalSubtasks) * 100; // Calculate appropriate progress bar length based on terminal width @@ -883,13 +1032,27 @@ async function displayTaskById(tasksPath, taskId) { const boxPadding = 2; // 1 on each side const boxBorders = 2; // 1 on each side const percentTextLength = 5; // ~5 chars for " 100%" - const progressBarLength = Math.max(30, availableWidth - boxPadding - boxBorders - percentTextLength - 20); // Minimum 30 chars + // Reduce the length by adjusting the subtraction value from 20 to 35 + const progressBarLength = Math.max(20, Math.min(60, availableWidth - boxPadding - boxBorders - percentTextLength - 35)); // Min 20, Max 60 + + // Status counts for display + const statusCounts = + `${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` + + `${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`; console.log(boxen( chalk.white.bold('Subtask Progress:') + '\n\n' + `${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + - `${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength)}`, - { padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } } + `${statusCounts}\n` + + `${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 1, bottom: 0 }, + width: Math.min(availableWidth - 10, 100), // Add width constraint to limit the box width + textAlignment: 'left' + } )); } } else { diff --git a/tasks/task_023.txt b/tasks/task_023.txt index f59291ad..07bd4db6 100644 --- a/tasks/task_023.txt +++ b/tasks/task_023.txt @@ -983,7 +983,7 @@ Analyze and refactor the project root handling mechanism to ensure consistent fi ### Details: -## 44. Implement init MCP command [done] +## 44. Implement init MCP command [deferred] ### Dependencies: None ### Description: Create MCP tool implementation for the init command ### Details: diff --git a/tasks/tasks.json b/tasks/tasks.json index bec2b4be..b1963df4 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -1759,7 +1759,7 @@ "title": "Implement init MCP command", "description": "Create MCP tool implementation for the init command", "details": "", - "status": "done", + "status": "deferred", "dependencies": [], "parentTaskId": 23 }