Enhance progress bars with status breakdown, improve readability, optimize display width, and update changeset
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
// Determine color based on percentage
|
||||
let barColor;
|
||||
// Calculate how many characters to fill for "true completion"
|
||||
const trueCompletedFilled = Math.round(percent * length / 100);
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user