fix: bring back the original list tasks command

This commit is contained in:
Ralph Khreish
2025-09-13 11:16:38 -07:00
parent cd2b4c9d56
commit 6b73c42dbd
6 changed files with 291 additions and 86 deletions

View File

@@ -24,6 +24,9 @@ import {
calculateSubtaskStatistics, calculateSubtaskStatistics,
calculateDependencyStatistics, calculateDependencyStatistics,
getPriorityBreakdown, getPriorityBreakdown,
displayRecommendedNextTask,
getTaskDescription,
displaySuggestedNextSteps,
type NextTaskInfo type NextTaskInfo
} from '../ui/index.js'; } from '../ui/index.js';
@@ -284,16 +287,40 @@ export class ListTasksCommand extends Command {
const nextTask = this.findNextTask(tasks); const nextTask = this.findNextTask(tasks);
// Display dashboard boxes // Display dashboard boxes
displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask); displayDashboards(
taskStats,
subtaskStats,
priorityBreakdown,
depStats,
nextTask
);
// Task table // Task table - no title, just show the table directly
console.log(chalk.blue.bold(`\n📋 Tasks (${tasks.length}):\n`));
console.log( console.log(
ui.createTaskTable(tasks, { ui.createTaskTable(tasks, {
showSubtasks: withSubtasks, showSubtasks: withSubtasks,
showDependencies: true showDependencies: true,
showComplexity: true // Enable complexity column
}) })
); );
// Display recommended next task section immediately after table
if (nextTask) {
// Find the full task object to get description
const fullTask = tasks.find(t => String(t.id) === String(nextTask.id));
const description = fullTask ? getTaskDescription(fullTask) : undefined;
displayRecommendedNextTask({
...nextTask,
status: 'pending', // Next task is typically pending
description
});
} else {
displayRecommendedNextTask(undefined);
}
// Display suggested next steps at the end
displaySuggestedNextSteps();
} }
/** /**
@@ -317,12 +344,12 @@ export class ListTasksCommand extends Command {
// Build set of completed task IDs (including subtasks) // Build set of completed task IDs (including subtasks)
const completedIds = new Set<string>(); const completedIds = new Set<string>();
tasks.forEach(t => { tasks.forEach((t) => {
if (t.status === 'done' || t.status === 'completed') { if (t.status === 'done' || t.status === 'completed') {
completedIds.add(String(t.id)); completedIds.add(String(t.id));
} }
if (t.subtasks) { if (t.subtasks) {
t.subtasks.forEach(st => { t.subtasks.forEach((st) => {
if (st.status === 'done' || st.status === 'completed') { if (st.status === 'done' || st.status === 'completed') {
completedIds.add(`${t.id}.${st.id}`); completedIds.add(`${t.id}.${st.id}`);
} }
@@ -334,14 +361,17 @@ export class ListTasksCommand extends Command {
const candidateSubtasks: NextTaskInfo[] = []; const candidateSubtasks: NextTaskInfo[] = [];
tasks tasks
.filter(t => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0) .filter(
.forEach(parent => { (t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0
parent.subtasks!.forEach(st => { )
.forEach((parent) => {
parent.subtasks!.forEach((st) => {
const stStatus = (st.status || 'pending').toLowerCase(); const stStatus = (st.status || 'pending').toLowerCase();
if (stStatus !== 'pending' && stStatus !== 'in-progress') return; if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
// Check if dependencies are satisfied // Check if dependencies are satisfied
const fullDeps = st.dependencies?.map(d => { const fullDeps =
st.dependencies?.map((d) => {
// Handle both numeric and string IDs // Handle both numeric and string IDs
if (typeof d === 'string' && d.includes('.')) { if (typeof d === 'string' && d.includes('.')) {
return d; return d;
@@ -349,15 +379,16 @@ export class ListTasksCommand extends Command {
return `${parent.id}.${d}`; return `${parent.id}.${d}`;
}) ?? []; }) ?? [];
const depsSatisfied = fullDeps.length === 0 || const depsSatisfied =
fullDeps.every(depId => completedIds.has(String(depId))); fullDeps.length === 0 ||
fullDeps.every((depId) => completedIds.has(String(depId)));
if (depsSatisfied) { if (depsSatisfied) {
candidateSubtasks.push({ candidateSubtasks.push({
id: `${parent.id}.${st.id}`, id: `${parent.id}.${st.id}`,
title: st.title || `Subtask ${st.id}`, title: st.title || `Subtask ${st.id}`,
priority: st.priority || parent.priority || 'medium', priority: st.priority || parent.priority || 'medium',
dependencies: fullDeps.map(d => String(d)) dependencies: fullDeps.map((d) => String(d))
}); });
} }
}); });
@@ -380,15 +411,16 @@ export class ListTasksCommand extends Command {
} }
// Fall back to finding eligible top-level tasks // Fall back to finding eligible top-level tasks
const eligibleTasks = tasks.filter(task => { const eligibleTasks = tasks.filter((task) => {
// Skip non-eligible statuses // Skip non-eligible statuses
const status = (task.status || 'pending').toLowerCase(); const status = (task.status || 'pending').toLowerCase();
if (status !== 'pending' && status !== 'in-progress') return false; if (status !== 'pending' && status !== 'in-progress') return false;
// Check dependencies // Check dependencies
const deps = task.dependencies || []; const deps = task.dependencies || [];
const depsSatisfied = deps.length === 0 || const depsSatisfied =
deps.every(depId => completedIds.has(String(depId))); deps.length === 0 ||
deps.every((depId) => completedIds.has(String(depId)));
return depsSatisfied; return depsSatisfied;
}); });
@@ -416,7 +448,7 @@ export class ListTasksCommand extends Command {
id: nextTask.id, id: nextTask.id,
title: nextTask.title, title: nextTask.title,
priority: nextTask.priority, priority: nextTask.priority,
dependencies: nextTask.dependencies?.map(d => String(d)) dependencies: nextTask.dependencies?.map((d) => String(d))
}; };
} }

View File

@@ -4,3 +4,5 @@
export * from './header.component.js'; export * from './header.component.js';
export * from './dashboard.component.js'; export * from './dashboard.component.js';
export * from './next-task.component.js';
export * from './suggested-steps.component.js';

View File

@@ -0,0 +1,134 @@
/**
* @fileoverview Next task recommendation component
* Displays detailed information about the recommended next task
*/
import chalk from 'chalk';
import boxen from 'boxen';
import type { Task } from '@tm/core/types';
/**
* Next task display options
*/
export interface NextTaskDisplayOptions {
id: string | number;
title: string;
priority?: string;
status?: string;
dependencies?: (string | number)[];
description?: string;
}
/**
* Display the recommended next task section
*/
export function displayRecommendedNextTask(
task: NextTaskDisplayOptions | undefined
): void {
if (!task) {
// If no task available, show a message
console.log(
boxen(
chalk.yellow(
'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.'
),
{
padding: 1,
borderStyle: 'round',
borderColor: 'yellow',
title: '⚠ NO TASKS AVAILABLE ⚠',
titleAlignment: 'center'
}
)
);
return;
}
// Build the content for the next task box
const content = [];
// Task header with ID and title
content.push(
`🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}`
);
content.push('');
// Priority and Status line
const statusLine = [];
if (task.priority) {
const priorityColor =
task.priority === 'high'
? chalk.red
: task.priority === 'medium'
? chalk.yellow
: chalk.gray;
statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`);
}
if (task.status) {
const statusDisplay =
task.status === 'pending'
? chalk.yellow('○ pending')
: task.status === 'in-progress'
? chalk.blue('▶ in-progress')
: chalk.gray(task.status);
statusLine.push(`Status: ${statusDisplay}`);
}
content.push(statusLine.join(' '));
// Dependencies
const depsDisplay =
!task.dependencies || task.dependencies.length === 0
? chalk.gray('None')
: chalk.cyan(task.dependencies.join(', '));
content.push(`Dependencies: ${depsDisplay}`);
// Description if available
if (task.description) {
content.push('');
content.push(`Description: ${chalk.white(task.description)}`);
}
// Action commands
content.push('');
content.push(
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}`
);
content.push(
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}`
);
// Display in a styled box with orange border
console.log(
boxen(content.join('\n'), {
padding: 1,
margin: { top: 1, bottom: 1 },
borderStyle: 'round',
borderColor: '#FFA500', // Orange color
title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'),
titleAlignment: 'center',
width: process.stdout.columns * 0.97,
fullscreen: false
})
);
}
/**
* Get task description from the full task object
*/
export function getTaskDescription(task: Task): string | undefined {
// Try to get description from the task
// This could be from task.description or the first line of task.details
if ('description' in task && task.description) {
return task.description as string;
}
if ('details' in task && task.details) {
// Take first sentence or line from details
const details = task.details as string;
const firstLine = details.split('\n')[0];
const firstSentence = firstLine.split('.')[0];
return firstSentence;
}
return undefined;
}

View File

@@ -0,0 +1,31 @@
/**
* @fileoverview Suggested next steps component
* Displays helpful command suggestions at the end of the list
*/
import chalk from 'chalk';
import boxen from 'boxen';
/**
* Display suggested next steps section
*/
export function displaySuggestedNextSteps(): void {
const steps = [
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next`,
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`
];
console.log(
boxen(
chalk.white.bold('Suggested Next Steps:') + '\n\n' + steps.join('\n'),
{
padding: 1,
margin: { top: 0, bottom: 1 },
borderStyle: 'round',
borderColor: 'gray',
width: process.stdout.columns * 0.97
}
)
);
}

View File

@@ -18,19 +18,39 @@ export function getStatusWithColor(
const statusConfig = { const statusConfig = {
done: { done: {
color: chalk.green, color: chalk.green,
icon: String.fromCharCode(8730), icon: '✓',
tableIcon: String.fromCharCode(8730) tableIcon: '✓'
}, // √ },
pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' }, pending: {
color: chalk.yellow,
icon: '○',
tableIcon: '○'
},
'in-progress': { 'in-progress': {
color: chalk.hex('#FFA500'), color: chalk.hex('#FFA500'),
icon: String.fromCharCode(9654), icon: '▶',
tableIcon: '>' tableIcon: ''
}, // ▶ },
deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' }, deferred: {
blocked: { color: chalk.red, icon: '!', tableIcon: '!' }, color: chalk.gray,
review: { color: chalk.magenta, icon: '?', tableIcon: '?' }, icon: 'x',
cancelled: { color: chalk.gray, icon: 'X', tableIcon: 'X' } tableIcon: 'x'
},
review: {
color: chalk.magenta,
icon: '?',
tableIcon: '?'
},
cancelled: {
color: chalk.gray,
icon: 'x',
tableIcon: 'x'
},
blocked: {
color: chalk.red,
icon: '!',
tableIcon: '!'
}
}; };
const config = statusConfig[status] || { const config = statusConfig[status] || {
@@ -39,18 +59,7 @@ export function getStatusWithColor(
tableIcon: 'X' tableIcon: 'X'
}; };
// Use simple ASCII characters for stable display const icon = forTable ? config.tableIcon : config.icon;
const simpleIcons = {
done: String.fromCharCode(8730), // √
pending: 'o',
'in-progress': '>',
deferred: 'x',
blocked: '!',
review: '?',
cancelled: 'X'
};
const icon = forTable ? simpleIcons[status] || 'X' : config.icon;
return config.color(`${icon} ${status}`); return config.color(`${icon} ${status}`);
} }
@@ -245,10 +254,24 @@ export function createTaskTable(
} = options || {}; } = options || {};
// Calculate dynamic column widths based on terminal width // Calculate dynamic column widths based on terminal width
const terminalWidth = process.stdout.columns || 100; const terminalWidth = process.stdout.columns * 0.9 || 100;
// Adjust column widths to better match the original layout
const baseColWidths = showComplexity const baseColWidths = showComplexity
? [8, Math.floor(terminalWidth * 0.35), 18, 12, 15, 12] // ID, Title, Status, Priority, Dependencies, Complexity ? [
: [8, Math.floor(terminalWidth * 0.4), 18, 12, 20]; // ID, Title, Status, Priority, Dependencies Math.floor(terminalWidth * 0.06),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.12)
] // ID, Title, Status, Priority, Dependencies, Complexity
: [
Math.floor(terminalWidth * 0.08),
Math.floor(terminalWidth * 0.4),
Math.floor(terminalWidth * 0.18),
Math.floor(terminalWidth * 0.12),
Math.floor(terminalWidth * 0.2)
]; // ID, Title, Status, Priority, Dependencies
const headers = [ const headers = [
chalk.blue.bold('ID'), chalk.blue.bold('ID'),
@@ -284,11 +307,19 @@ export function createTaskTable(
]; ];
if (showDependencies) { if (showDependencies) {
row.push(formatDependenciesWithStatus(task.dependencies, tasks)); // For table display, show simple format without status icons
if (!task.dependencies || task.dependencies.length === 0) {
row.push(chalk.gray('None'));
} else {
row.push(
chalk.cyan(task.dependencies.map((d) => String(d)).join(', '))
);
}
} }
if (showComplexity && 'complexity' in task) { if (showComplexity) {
row.push(getComplexityWithColor(task.complexity as number | string)); // Show N/A if no complexity score
row.push(chalk.gray('N/A'));
} }
table.push(row); table.push(row);

View File

@@ -1,7 +1,4 @@
import fs from 'fs'; import packageJson from '../../package.json' with { type: 'json' };
import path from 'path';
import { fileURLToPath } from 'url';
import { log } from '../../scripts/modules/utils.js';
/** /**
* Reads the version from the nearest package.json relative to this file. * Reads the version from the nearest package.json relative to this file.
@@ -9,27 +6,5 @@ import { log } from '../../scripts/modules/utils.js';
* @returns {string} The version string or 'unknown'. * @returns {string} The version string or 'unknown'.
*/ */
export function getTaskMasterVersion() { export function getTaskMasterVersion() {
let version = 'unknown'; return packageJson.version || 'unknown';
try {
// Get the directory of the current module (getPackageVersion.js)
const currentModuleFilename = fileURLToPath(import.meta.url);
const currentModuleDirname = path.dirname(currentModuleFilename);
// Construct the path to package.json relative to this file (../../package.json)
const packageJsonPath = path.join(
currentModuleDirname,
'..',
'..',
'package.json'
);
if (fs.existsSync(packageJsonPath)) {
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
version = packageJson.version;
}
} catch (error) {
// Silently fall back to default version
log('warn', 'Could not read own package.json for version info.', error);
}
return version;
} }