diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index b9b97dfd..cb6fbead 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -24,6 +24,9 @@ import { calculateSubtaskStatistics, calculateDependencyStatistics, getPriorityBreakdown, + displayRecommendedNextTask, + getTaskDescription, + displaySuggestedNextSteps, type NextTaskInfo } from '../ui/index.js'; @@ -258,7 +261,7 @@ export class ListTasksCommand extends Command { // Get file path for display const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined; - + // Display header with Task Master banner displayHeader({ version: '0.26.0', // You may want to get this dynamically @@ -284,16 +287,40 @@ export class ListTasksCommand extends Command { const nextTask = this.findNextTask(tasks); // Display dashboard boxes - displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask); + displayDashboards( + taskStats, + subtaskStats, + priorityBreakdown, + depStats, + nextTask + ); - // Task table - console.log(chalk.blue.bold(`\nšŸ“‹ Tasks (${tasks.length}):\n`)); + // Task table - no title, just show the table directly console.log( ui.createTaskTable(tasks, { 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(); } /** @@ -308,21 +335,21 @@ export class ListTasksCommand extends Command { * Implements the same logic as scripts/modules/task-manager/find-next-task.js */ private findNextTask(tasks: Task[]): NextTaskInfo | undefined { - const priorityValues: Record = { + const priorityValues: Record = { critical: 4, - high: 3, - medium: 2, - low: 1 + high: 3, + medium: 2, + low: 1 }; // Build set of completed task IDs (including subtasks) const completedIds = new Set(); - tasks.forEach(t => { + tasks.forEach((t) => { if (t.status === 'done' || t.status === 'completed') { completedIds.add(String(t.id)); } if (t.subtasks) { - t.subtasks.forEach(st => { + t.subtasks.forEach((st) => { if (st.status === 'done' || st.status === 'completed') { completedIds.add(`${t.id}.${st.id}`); } @@ -332,32 +359,36 @@ export class ListTasksCommand extends Command { // First, look for eligible subtasks in in-progress parent tasks const candidateSubtasks: NextTaskInfo[] = []; - + tasks - .filter(t => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0) - .forEach(parent => { - parent.subtasks!.forEach(st => { + .filter( + (t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0 + ) + .forEach((parent) => { + parent.subtasks!.forEach((st) => { const stStatus = (st.status || 'pending').toLowerCase(); if (stStatus !== 'pending' && stStatus !== 'in-progress') return; // Check if dependencies are satisfied - const fullDeps = st.dependencies?.map(d => { - // Handle both numeric and string IDs - if (typeof d === 'string' && d.includes('.')) { - return d; - } - return `${parent.id}.${d}`; - }) ?? []; + const fullDeps = + st.dependencies?.map((d) => { + // Handle both numeric and string IDs + if (typeof d === 'string' && d.includes('.')) { + return d; + } + return `${parent.id}.${d}`; + }) ?? []; - const depsSatisfied = fullDeps.length === 0 || - fullDeps.every(depId => completedIds.has(String(depId))); + const depsSatisfied = + fullDeps.length === 0 || + fullDeps.every((depId) => completedIds.has(String(depId))); if (depsSatisfied) { candidateSubtasks.push({ id: `${parent.id}.${st.id}`, title: st.title || `Subtask ${st.id}`, 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 - const eligibleTasks = tasks.filter(task => { + const eligibleTasks = tasks.filter((task) => { // Skip non-eligible statuses const status = (task.status || 'pending').toLowerCase(); if (status !== 'pending' && status !== 'in-progress') return false; // Check dependencies const deps = task.dependencies || []; - const depsSatisfied = deps.length === 0 || - deps.every(depId => completedIds.has(String(depId))); + const depsSatisfied = + deps.length === 0 || + deps.every((depId) => completedIds.has(String(depId))); return depsSatisfied; }); @@ -416,7 +448,7 @@ export class ListTasksCommand extends Command { id: nextTask.id, title: nextTask.title, priority: nextTask.priority, - dependencies: nextTask.dependencies?.map(d => String(d)) + dependencies: nextTask.dependencies?.map((d) => String(d)) }; } diff --git a/apps/cli/src/ui/components/index.ts b/apps/cli/src/ui/components/index.ts index 27f59c5c..268a96a1 100644 --- a/apps/cli/src/ui/components/index.ts +++ b/apps/cli/src/ui/components/index.ts @@ -3,4 +3,6 @@ */ export * from './header.component.js'; -export * from './dashboard.component.js'; \ No newline at end of file +export * from './dashboard.component.js'; +export * from './next-task.component.js'; +export * from './suggested-steps.component.js'; \ No newline at end of file diff --git a/apps/cli/src/ui/components/next-task.component.ts b/apps/cli/src/ui/components/next-task.component.ts new file mode 100644 index 00000000..beb5d7cc --- /dev/null +++ b/apps/cli/src/ui/components/next-task.component.ts @@ -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; +} diff --git a/apps/cli/src/ui/components/suggested-steps.component.ts b/apps/cli/src/ui/components/suggested-steps.component.ts new file mode 100644 index 00000000..66e5eb19 --- /dev/null +++ b/apps/cli/src/ui/components/suggested-steps.component.ts @@ -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=')} to break down a task into subtasks`, + `${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --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 + } + ) + ); +} diff --git a/apps/cli/src/utils/ui.ts b/apps/cli/src/utils/ui.ts index 4912a519..7ddbd34e 100644 --- a/apps/cli/src/utils/ui.ts +++ b/apps/cli/src/utils/ui.ts @@ -18,19 +18,39 @@ export function getStatusWithColor( const statusConfig = { done: { color: chalk.green, - icon: String.fromCharCode(8730), - tableIcon: String.fromCharCode(8730) - }, // √ - pending: { color: chalk.yellow, icon: 'o', tableIcon: 'o' }, + icon: 'āœ“', + tableIcon: 'āœ“' + }, + pending: { + color: chalk.yellow, + icon: 'ā—‹', + tableIcon: 'ā—‹' + }, 'in-progress': { color: chalk.hex('#FFA500'), - icon: String.fromCharCode(9654), - tableIcon: '>' - }, // ā–¶ - deferred: { color: chalk.gray, icon: 'x', tableIcon: 'x' }, - blocked: { color: chalk.red, icon: '!', tableIcon: '!' }, - review: { color: chalk.magenta, icon: '?', tableIcon: '?' }, - cancelled: { color: chalk.gray, icon: 'X', tableIcon: 'X' } + icon: 'ā–¶', + tableIcon: 'ā–¶' + }, + deferred: { + color: chalk.gray, + icon: '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] || { @@ -39,18 +59,7 @@ export function getStatusWithColor( tableIcon: 'X' }; - // Use simple ASCII characters for stable display - const simpleIcons = { - done: String.fromCharCode(8730), // √ - pending: 'o', - 'in-progress': '>', - deferred: 'x', - blocked: '!', - review: '?', - cancelled: 'X' - }; - - const icon = forTable ? simpleIcons[status] || 'X' : config.icon; + const icon = forTable ? config.tableIcon : config.icon; return config.color(`${icon} ${status}`); } @@ -245,10 +254,24 @@ export function createTaskTable( } = options || {}; // 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 - ? [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 = [ chalk.blue.bold('ID'), @@ -284,11 +307,19 @@ export function createTaskTable( ]; 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) { - row.push(getComplexityWithColor(task.complexity as number | string)); + if (showComplexity) { + // Show N/A if no complexity score + row.push(chalk.gray('N/A')); } table.push(row); diff --git a/src/utils/getVersion.js b/src/utils/getVersion.js index 55a64f40..ffbd1f5e 100644 --- a/src/utils/getVersion.js +++ b/src/utils/getVersion.js @@ -1,7 +1,4 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { log } from '../../scripts/modules/utils.js'; +import packageJson from '../../package.json' with { type: 'json' }; /** * 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'. */ export function getTaskMasterVersion() { - let 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; + return packageJson.version || 'unknown'; }