diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 7105daca..3520efb0 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -17,6 +17,15 @@ import { } from '@tm/core'; import type { StorageType } from '@tm/core/types'; import * as ui from '../utils/ui.js'; +import { + displayHeader, + displayDashboards, + calculateTaskStatistics, + calculateSubtaskStatistics, + calculateDependencyStatistics, + getPriorityBreakdown, + type NextTaskInfo +} from '../ui/index.js'; /** * Options interface for the list command @@ -245,19 +254,19 @@ export class ListTasksCommand extends Command { * Display in text format with tables */ private displayText(data: ListTasksResult, withSubtasks?: boolean): void { - const { tasks, total, filtered, tag, storageType } = data; + const { tasks, tag } = data; - // Header - ui.displayBanner(`Task List${tag ? ` (${tag})` : ''}`); - - // Statistics - console.log(chalk.blue.bold('\nšŸ“Š Statistics:\n')); - console.log(` Total tasks: ${chalk.cyan(total)}`); - console.log(` Filtered: ${chalk.cyan(filtered)}`); - if (tag) { - console.log(` Tag: ${chalk.cyan(tag)}`); - } - console.log(` Storage: ${chalk.cyan(storageType)}`); + // 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 + projectName: 'Taskmaster', + tag: tag || 'master', + filePath: filePath, + showBanner: true + }); // No tasks message if (tasks.length === 0) { @@ -265,6 +274,26 @@ export class ListTasksCommand extends Command { return; } + // Calculate statistics + const taskStats = calculateTaskStatistics(tasks); + const subtaskStats = calculateSubtaskStatistics(tasks); + const depStats = calculateDependencyStatistics(tasks); + const priorityBreakdown = getPriorityBreakdown(tasks); + + // Find next task (simplified for now) + const nextTask: NextTaskInfo | undefined = tasks + .filter(t => t.status === 'pending' && (!t.dependencies || t.dependencies.length === 0)) + .map(t => ({ + id: t.id, + title: t.title, + priority: t.priority, + dependencies: t.dependencies, + complexity: undefined // Add if available + }))[0]; + + // Display dashboard boxes + displayDashboards(taskStats, subtaskStats, priorityBreakdown, depStats, nextTask); + // Task table console.log(chalk.blue.bold(`\nšŸ“‹ Tasks (${tasks.length}):\n`)); console.log( @@ -273,13 +302,6 @@ export class ListTasksCommand extends Command { showDependencies: true }) ); - - // Progress bar - const completedCount = tasks.filter( - (t: Task) => t.status === 'done' - ).length; - console.log(chalk.blue.bold('\nšŸ“Š Overall Progress:\n')); - console.log(` ${ui.createProgressBar(completedCount, tasks.length)}`); } /** diff --git a/apps/cli/src/ui/components/dashboard.component.ts b/apps/cli/src/ui/components/dashboard.component.ts new file mode 100644 index 00000000..cc9c741f --- /dev/null +++ b/apps/cli/src/ui/components/dashboard.component.ts @@ -0,0 +1,381 @@ +/** + * @fileoverview Dashboard components for Task Master CLI + * Displays project statistics and dependency information + */ + +import chalk from 'chalk'; +import boxen from 'boxen'; +import type { Task, TaskPriority } from '@tm/core/types'; + +/** + * Statistics for task collection + */ +export interface TaskStatistics { + total: number; + done: number; + inProgress: number; + pending: number; + blocked: number; + deferred: number; + cancelled: number; + review?: number; + completionPercentage: number; +} + +/** + * Statistics for dependencies + */ +export interface DependencyStatistics { + tasksWithNoDeps: number; + tasksReadyToWork: number; + tasksBlockedByDeps: number; + mostDependedOnTaskId?: number; + mostDependedOnCount?: number; + avgDependenciesPerTask: number; +} + +/** + * Next task information + */ +export interface NextTaskInfo { + id: string | number; + title: string; + priority?: TaskPriority; + dependencies?: (string | number)[]; + complexity?: number | string; +} + +/** + * Create a progress bar with percentage + */ +function createProgressBar(percentage: number, width: number = 30): string { + const filled = Math.round((percentage / 100) * width); + const empty = width - filled; + + const bar = chalk.green('ā–ˆ').repeat(filled) + chalk.gray('ā–‘').repeat(empty); + return bar; +} + +/** + * Calculate task statistics from a list of tasks + */ +export function calculateTaskStatistics(tasks: Task[]): TaskStatistics { + const stats: TaskStatistics = { + total: tasks.length, + done: 0, + inProgress: 0, + pending: 0, + blocked: 0, + deferred: 0, + cancelled: 0, + review: 0, + completionPercentage: 0 + }; + + tasks.forEach(task => { + switch (task.status) { + case 'done': + stats.done++; + break; + case 'in-progress': + stats.inProgress++; + break; + case 'pending': + stats.pending++; + break; + case 'blocked': + stats.blocked++; + break; + case 'deferred': + stats.deferred++; + break; + case 'cancelled': + stats.cancelled++; + break; + case 'review': + stats.review = (stats.review || 0) + 1; + break; + } + }); + + stats.completionPercentage = stats.total > 0 + ? Math.round((stats.done / stats.total) * 100) + : 0; + + return stats; +} + +/** + * Calculate subtask statistics from tasks + */ +export function calculateSubtaskStatistics(tasks: Task[]): TaskStatistics { + const stats: TaskStatistics = { + total: 0, + done: 0, + inProgress: 0, + pending: 0, + blocked: 0, + deferred: 0, + cancelled: 0, + review: 0, + completionPercentage: 0 + }; + + tasks.forEach(task => { + if (task.subtasks && task.subtasks.length > 0) { + task.subtasks.forEach(subtask => { + stats.total++; + switch (subtask.status) { + case 'done': + stats.done++; + break; + case 'in-progress': + stats.inProgress++; + break; + case 'pending': + stats.pending++; + break; + case 'blocked': + stats.blocked++; + break; + case 'deferred': + stats.deferred++; + break; + case 'cancelled': + stats.cancelled++; + break; + case 'review': + stats.review = (stats.review || 0) + 1; + break; + } + }); + } + }); + + stats.completionPercentage = stats.total > 0 + ? Math.round((stats.done / stats.total) * 100) + : 0; + + return stats; +} + +/** + * Calculate dependency statistics + */ +export function calculateDependencyStatistics(tasks: Task[]): DependencyStatistics { + const completedTaskIds = new Set( + tasks.filter(t => t.status === 'done').map(t => t.id) + ); + + const tasksWithNoDeps = tasks.filter( + t => t.status !== 'done' && (!t.dependencies || t.dependencies.length === 0) + ).length; + + const tasksWithAllDepsSatisfied = tasks.filter( + t => t.status !== 'done' && + t.dependencies && + t.dependencies.length > 0 && + t.dependencies.every(depId => completedTaskIds.has(depId)) + ).length; + + const tasksBlockedByDeps = tasks.filter( + t => t.status !== 'done' && + t.dependencies && + t.dependencies.length > 0 && + !t.dependencies.every(depId => completedTaskIds.has(depId)) + ).length; + + // Calculate most depended-on task + const dependencyCount: Record = {}; + tasks.forEach(task => { + if (task.dependencies && task.dependencies.length > 0) { + task.dependencies.forEach(depId => { + const key = String(depId); + dependencyCount[key] = (dependencyCount[key] || 0) + 1; + }); + } + }); + + let mostDependedOnTaskId: number | undefined; + let mostDependedOnCount = 0; + + for (const [taskId, count] of Object.entries(dependencyCount)) { + if (count > mostDependedOnCount) { + mostDependedOnCount = count; + mostDependedOnTaskId = parseInt(taskId); + } + } + + // Calculate average dependencies + const totalDependencies = tasks.reduce( + (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), + 0 + ); + const avgDependenciesPerTask = tasks.length > 0 + ? totalDependencies / tasks.length + : 0; + + return { + tasksWithNoDeps, + tasksReadyToWork: tasksWithNoDeps + tasksWithAllDepsSatisfied, + tasksBlockedByDeps, + mostDependedOnTaskId, + mostDependedOnCount, + avgDependenciesPerTask + }; +} + +/** + * Get priority counts + */ +export function getPriorityBreakdown(tasks: Task[]): Record { + const breakdown: Record = { + critical: 0, + high: 0, + medium: 0, + low: 0 + }; + + tasks.forEach(task => { + const priority = task.priority || 'medium'; + breakdown[priority]++; + }); + + return breakdown; +} + +/** + * Display the project dashboard box + */ +export function displayProjectDashboard( + taskStats: TaskStatistics, + subtaskStats: TaskStatistics, + priorityBreakdown: Record +): string { + const taskProgressBar = createProgressBar(taskStats.completionPercentage); + const subtaskProgressBar = createProgressBar(subtaskStats.completionPercentage); + + const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}%`; + const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}%`; + + const content = + chalk.white.bold('Project Dashboard') + '\n' + + `Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` + + `Done: ${chalk.green(taskStats.done)} In Progress: ${chalk.blue(taskStats.inProgress)} Pending: ${chalk.yellow(taskStats.pending)} Blocked: ${chalk.red(taskStats.blocked)} Deferred: ${chalk.gray(taskStats.deferred)}\n` + + `Cancelled: ${chalk.gray(taskStats.cancelled)}\n\n` + + `Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` + + `Completed: ${chalk.green(`${subtaskStats.done}/${subtaskStats.total}`)} In Progress: ${chalk.blue(subtaskStats.inProgress)} Pending: ${chalk.yellow(subtaskStats.pending)} Blocked: ${chalk.red(subtaskStats.blocked)}\n` + + `Deferred: ${chalk.gray(subtaskStats.deferred)} Cancelled: ${chalk.gray(subtaskStats.cancelled)}\n\n` + + chalk.cyan.bold('Priority Breakdown:') + '\n' + + `${chalk.red('•')} ${chalk.white('High priority:')} ${priorityBreakdown.high}\n` + + `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${priorityBreakdown.medium}\n` + + `${chalk.green('•')} ${chalk.white('Low priority:')} ${priorityBreakdown.low}`; + + return content; +} + +/** + * Display the dependency dashboard box + */ +export function displayDependencyDashboard( + depStats: DependencyStatistics, + nextTask?: NextTaskInfo +): string { + const content = + chalk.white.bold('Dependency Status & Next Task') + '\n' + + chalk.cyan.bold('Dependency Metrics:') + '\n' + + `${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${depStats.tasksWithNoDeps}\n` + + `${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${depStats.tasksReadyToWork}\n` + + `${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${depStats.tasksBlockedByDeps}\n` + + `${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${ + depStats.mostDependedOnTaskId + ? chalk.cyan(`#${depStats.mostDependedOnTaskId} (${depStats.mostDependedOnCount} dependents)`) + : chalk.gray('None') + }\n` + + `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${depStats.avgDependenciesPerTask.toFixed(1)}\n\n` + + chalk.cyan.bold('Next Task to Work On:') + '\n' + + `ID: ${nextTask ? chalk.cyan(String(nextTask.id)) : chalk.gray('N/A')} - ${ + nextTask ? chalk.white.bold(nextTask.title) : chalk.yellow('No task available') + }\n` + + `Priority: ${nextTask?.priority || chalk.gray('N/A')} Dependencies: ${ + nextTask?.dependencies?.length ? chalk.cyan(nextTask.dependencies.join(', ')) : chalk.gray('None') + }\n` + + `Complexity: ${nextTask?.complexity || chalk.gray('N/A')}`; + + return content; +} + +/** + * Display dashboard boxes side by side or stacked + */ +export function displayDashboards( + taskStats: TaskStatistics, + subtaskStats: TaskStatistics, + priorityBreakdown: Record, + depStats: DependencyStatistics, + nextTask?: NextTaskInfo +): void { + const projectDashboardContent = displayProjectDashboard(taskStats, subtaskStats, priorityBreakdown); + const dependencyDashboardContent = displayDependencyDashboard(depStats, nextTask); + + // Get terminal width + const terminalWidth = process.stdout.columns || 80; + const minDashboardWidth = 50; + const minDependencyWidth = 50; + const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; + + // If terminal is wide enough, show side by side + if (terminalWidth >= totalMinWidth) { + const halfWidth = Math.floor(terminalWidth / 2); + const boxContentWidth = halfWidth - 4; + + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + width: boxContentWidth, + dimBorder: false + }); + + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: 'magenta', + borderStyle: 'round', + width: boxContentWidth, + dimBorder: false + }); + + // Create side-by-side layout + const dashboardLines = dashboardBox.split('\n'); + const dependencyLines = dependencyBox.split('\n'); + const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); + + const combinedLines = []; + for (let i = 0; i < maxHeight; i++) { + const dashLine = i < dashboardLines.length ? dashboardLines[i] : ''; + const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; + const paddedDashLine = dashLine.padEnd(halfWidth, ' '); + combinedLines.push(paddedDashLine + depLine); + } + + console.log(combinedLines.join('\n')); + } else { + // Show stacked vertically + const dashboardBox = boxen(projectDashboardContent, { + padding: 1, + borderColor: 'blue', + borderStyle: 'round', + margin: { top: 0, bottom: 1 } + }); + + const dependencyBox = boxen(dependencyDashboardContent, { + padding: 1, + borderColor: 'magenta', + borderStyle: 'round', + margin: { top: 0, bottom: 1 } + }); + + console.log(dashboardBox); + console.log(dependencyBox); + } +} \ No newline at end of file diff --git a/apps/cli/src/ui/components/header.component.ts b/apps/cli/src/ui/components/header.component.ts new file mode 100644 index 00000000..6423cb89 --- /dev/null +++ b/apps/cli/src/ui/components/header.component.ts @@ -0,0 +1,101 @@ +/** + * @fileoverview Task Master header component + * Displays the banner, version, project info, and file path + */ + +import chalk from 'chalk'; +import boxen from 'boxen'; +import figlet from 'figlet'; +import gradient from 'gradient-string'; + +/** + * Header configuration options + */ +export interface HeaderOptions { + title?: string; + version?: string; + projectName?: string; + tag?: string; + filePath?: string; + showBanner?: boolean; +} + +/** + * Create the Task Master ASCII art banner + */ +function createBanner(): string { + const bannerText = figlet.textSync('Task Master', { + font: 'Standard', + horizontalLayout: 'default', + verticalLayout: 'default' + }); + + // Create a cool gradient effect + const coolGradient = gradient(['#0099ff', '#00ffcc']); + return coolGradient(bannerText); +} + +/** + * Display the Task Master header with project info + */ +export function displayHeader(options: HeaderOptions = {}): void { + const { + version = '0.26.0', + projectName = 'Taskmaster', + tag, + filePath, + showBanner = true + } = options; + + // Display the ASCII banner if requested + if (showBanner) { + console.log(createBanner()); + + // Add creator credit line below the banner + console.log( + chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano') + ); + } + + // Create the version and project info box + const infoBoxContent = chalk.white( + `${chalk.bold('Version:')} ${version} ${chalk.bold('Project:')} ${projectName}` + ); + + console.log( + boxen(infoBoxContent, { + padding: { left: 1, right: 1, top: 0, bottom: 0 }, + margin: { top: 1, bottom: 1 }, + borderStyle: 'round', + borderColor: 'cyan' + }) + ); + + // Display tag and file path info + if (tag || filePath) { + let tagInfo = ''; + + if (tag && tag !== 'master') { + tagInfo = `šŸ· tag: ${chalk.cyan(tag)}`; + } else { + tagInfo = `šŸ· tag: ${chalk.cyan('master')}`; + } + + console.log(tagInfo); + + if (filePath) { + console.log( + `Listing tasks from: ${chalk.dim(filePath)}` + ); + } + + console.log(); // Empty line for spacing + } +} + +/** + * Display a simple header without the ASCII art + */ +export function displaySimpleHeader(options: HeaderOptions = {}): void { + displayHeader({ ...options, showBanner: false }); +} \ No newline at end of file diff --git a/apps/cli/src/ui/components/index.ts b/apps/cli/src/ui/components/index.ts new file mode 100644 index 00000000..27f59c5c --- /dev/null +++ b/apps/cli/src/ui/components/index.ts @@ -0,0 +1,6 @@ +/** + * @fileoverview UI components exports + */ + +export * from './header.component.js'; +export * from './dashboard.component.js'; \ No newline at end of file diff --git a/apps/cli/src/ui/index.ts b/apps/cli/src/ui/index.ts new file mode 100644 index 00000000..f345a7b4 --- /dev/null +++ b/apps/cli/src/ui/index.ts @@ -0,0 +1,9 @@ +/** + * @fileoverview Main UI exports + */ + +// Export all components +export * from './components/index.js'; + +// Re-export existing UI utilities +export * from '../utils/ui.js'; \ No newline at end of file