fix: UI list and show (#1210)
This commit is contained in:
@@ -6,15 +6,10 @@
|
||||
"main": "./dist/index.js",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": ["dist", "README.md"],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check src",
|
||||
"format": "biome format --write src",
|
||||
@@ -48,5 +43,10 @@
|
||||
},
|
||||
"keywords": ["task-master", "cli", "task-management", "productivity"],
|
||||
"author": "",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@ 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,
|
||||
displayRecommendedNextTask,
|
||||
getTaskDescription,
|
||||
displaySuggestedNextSteps,
|
||||
type NextTaskInfo
|
||||
} from '../ui/index.js';
|
||||
|
||||
/**
|
||||
* Options interface for the list command
|
||||
@@ -245,19 +257,16 @@ 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})` : ''}`);
|
||||
// Get file path for display
|
||||
const filePath = this.tmCore ? `.taskmaster/tasks/tasks.json` : undefined;
|
||||
|
||||
// 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)}`);
|
||||
// Display header without banner (banner already shown by main CLI)
|
||||
displayHeader({
|
||||
tag: tag || 'master',
|
||||
filePath: filePath
|
||||
});
|
||||
|
||||
// No tasks message
|
||||
if (tasks.length === 0) {
|
||||
@@ -265,21 +274,50 @@ export class ListTasksCommand extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
// Task table
|
||||
console.log(chalk.blue.bold(`\n📋 Tasks (${tasks.length}):\n`));
|
||||
// Calculate statistics
|
||||
const taskStats = calculateTaskStatistics(tasks);
|
||||
const subtaskStats = calculateSubtaskStatistics(tasks);
|
||||
const depStats = calculateDependencyStatistics(tasks);
|
||||
const priorityBreakdown = getPriorityBreakdown(tasks);
|
||||
|
||||
// Find next task following the same logic as findNextTask
|
||||
const nextTask = this.findNextTask(tasks);
|
||||
|
||||
// Display dashboard boxes
|
||||
displayDashboards(
|
||||
taskStats,
|
||||
subtaskStats,
|
||||
priorityBreakdown,
|
||||
depStats,
|
||||
nextTask
|
||||
);
|
||||
|
||||
// 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
|
||||
})
|
||||
);
|
||||
|
||||
// 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)}`);
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,6 +327,128 @@ export class ListTasksCommand extends Command {
|
||||
this.lastResult = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next task to work on
|
||||
* Implements the same logic as scripts/modules/task-manager/find-next-task.js
|
||||
*/
|
||||
private findNextTask(tasks: Task[]): NextTaskInfo | undefined {
|
||||
const priorityValues: Record<string, number> = {
|
||||
critical: 4,
|
||||
high: 3,
|
||||
medium: 2,
|
||||
low: 1
|
||||
};
|
||||
|
||||
// Build set of completed task IDs (including subtasks)
|
||||
const completedIds = new Set<string>();
|
||||
tasks.forEach((t) => {
|
||||
if (t.status === 'done' || t.status === 'completed') {
|
||||
completedIds.add(String(t.id));
|
||||
}
|
||||
if (t.subtasks) {
|
||||
t.subtasks.forEach((st) => {
|
||||
if (st.status === 'done' || st.status === 'completed') {
|
||||
completedIds.add(`${t.id}.${st.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
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 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))
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (candidateSubtasks.length > 0) {
|
||||
// Sort by priority, then by dependencies count, then by ID
|
||||
candidateSubtasks.sort((a, b) => {
|
||||
const pa = priorityValues[a.priority || 'medium'] ?? 2;
|
||||
const pb = priorityValues[b.priority || 'medium'] ?? 2;
|
||||
if (pb !== pa) return pb - pa;
|
||||
|
||||
const depCountA = a.dependencies?.length || 0;
|
||||
const depCountB = b.dependencies?.length || 0;
|
||||
if (depCountA !== depCountB) return depCountA - depCountB;
|
||||
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
return candidateSubtasks[0];
|
||||
}
|
||||
|
||||
// Fall back to finding eligible top-level tasks
|
||||
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)));
|
||||
|
||||
return depsSatisfied;
|
||||
});
|
||||
|
||||
if (eligibleTasks.length === 0) return undefined;
|
||||
|
||||
// Sort eligible tasks
|
||||
eligibleTasks.sort((a, b) => {
|
||||
// Priority (higher first)
|
||||
const pa = priorityValues[a.priority || 'medium'] ?? 2;
|
||||
const pb = priorityValues[b.priority || 'medium'] ?? 2;
|
||||
if (pb !== pa) return pb - pa;
|
||||
|
||||
// Dependencies count (fewer first)
|
||||
const depCountA = a.dependencies?.length || 0;
|
||||
const depCountB = b.dependencies?.length || 0;
|
||||
if (depCountA !== depCountB) return depCountA - depCountB;
|
||||
|
||||
// ID (lower first)
|
||||
return Number(a.id) - Number(b.id);
|
||||
});
|
||||
|
||||
const nextTask = eligibleTasks[0];
|
||||
return {
|
||||
id: nextTask.id,
|
||||
title: nextTask.title,
|
||||
priority: nextTask.priority,
|
||||
dependencies: nextTask.dependencies?.map((d) => String(d))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last result (for programmatic usage)
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,14 @@ import boxen from 'boxen';
|
||||
import { createTaskMasterCore, type Task, type TaskMasterCore } from '@tm/core';
|
||||
import type { StorageType } from '@tm/core/types';
|
||||
import * as ui from '../utils/ui.js';
|
||||
import {
|
||||
displayTaskHeader,
|
||||
displayTaskProperties,
|
||||
displayImplementationDetails,
|
||||
displayTestStrategy,
|
||||
displaySubtasks,
|
||||
displaySuggestedActions
|
||||
} from '../ui/components/task-detail.component.js';
|
||||
|
||||
/**
|
||||
* Options interface for the show command
|
||||
@@ -258,46 +266,26 @@ export class ShowCommand extends Command {
|
||||
|
||||
const task = result.task;
|
||||
|
||||
// Header
|
||||
console.log(
|
||||
boxen(chalk.white.bold(`Task #${task.id} - ${task.title}`), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1 }
|
||||
})
|
||||
);
|
||||
// Display header with tag
|
||||
displayTaskHeader(task.id, task.title);
|
||||
|
||||
// Task details
|
||||
console.log(
|
||||
`\n${chalk.blue.bold('Status:')} ${ui.getStatusWithColor(task.status)}`
|
||||
);
|
||||
console.log(
|
||||
`${chalk.blue.bold('Priority:')} ${ui.getPriorityWithColor(task.priority)}`
|
||||
);
|
||||
|
||||
if (task.description) {
|
||||
console.log(`\n${chalk.blue.bold('Description:')}`);
|
||||
console.log(task.description);
|
||||
}
|
||||
// Display task properties in table format
|
||||
displayTaskProperties(task);
|
||||
|
||||
// Display implementation details if available
|
||||
if (task.details) {
|
||||
console.log(`\n${chalk.blue.bold('Details:')}`);
|
||||
console.log(task.details);
|
||||
console.log(); // Empty line for spacing
|
||||
displayImplementationDetails(task.details);
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
console.log(`\n${chalk.blue.bold('Dependencies:')}`);
|
||||
task.dependencies.forEach((dep) => {
|
||||
console.log(` - ${chalk.cyan(dep)}`);
|
||||
});
|
||||
// Display test strategy if available
|
||||
if ('testStrategy' in task && task.testStrategy) {
|
||||
console.log(); // Empty line for spacing
|
||||
displayTestStrategy(task.testStrategy as string);
|
||||
}
|
||||
|
||||
// Subtasks
|
||||
// Display subtasks if available
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
console.log(`\n${chalk.blue.bold('Subtasks:')}`);
|
||||
|
||||
// Filter subtasks by status if provided
|
||||
const filteredSubtasks = options.status
|
||||
? task.subtasks.filter((sub) => sub.status === options.status)
|
||||
@@ -308,23 +296,12 @@ export class ShowCommand extends Command {
|
||||
chalk.gray(` No subtasks with status '${options.status}'`)
|
||||
);
|
||||
} else {
|
||||
filteredSubtasks.forEach((subtask) => {
|
||||
console.log(
|
||||
` ${chalk.cyan(`${task.id}.${subtask.id}`)} ${ui.getStatusWithColor(subtask.status)} ${subtask.title}`
|
||||
);
|
||||
if (subtask.description) {
|
||||
console.log(` ${chalk.gray(subtask.description)}`);
|
||||
}
|
||||
});
|
||||
displaySubtasks(filteredSubtasks, task.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (task.testStrategy) {
|
||||
console.log(`\n${chalk.blue.bold('Test Strategy:')}`);
|
||||
console.log(task.testStrategy);
|
||||
}
|
||||
|
||||
console.log(`\n${chalk.gray('Storage: ' + result.storageType)}`);
|
||||
// Display suggested actions
|
||||
displaySuggestedActions(task.id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
567
apps/cli/src/ui/components/dashboard.component.ts
Normal file
567
apps/cli/src/ui/components/dashboard.component.ts
Normal file
@@ -0,0 +1,567 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status breakdown for progress bars
|
||||
*/
|
||||
export interface StatusBreakdown {
|
||||
'in-progress'?: number;
|
||||
pending?: number;
|
||||
blocked?: number;
|
||||
deferred?: number;
|
||||
cancelled?: number;
|
||||
review?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a progress bar with color-coded status segments
|
||||
*/
|
||||
function createProgressBar(
|
||||
completionPercentage: number,
|
||||
width: number = 30,
|
||||
statusBreakdown?: StatusBreakdown
|
||||
): string {
|
||||
// If no breakdown provided, use simple green bar
|
||||
if (!statusBreakdown) {
|
||||
const filled = Math.round((completionPercentage / 100) * width);
|
||||
const empty = width - filled;
|
||||
return chalk.green('█').repeat(filled) + chalk.gray('░').repeat(empty);
|
||||
}
|
||||
|
||||
// Build the bar with different colored sections
|
||||
// Order matches the status display: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
|
||||
let bar = '';
|
||||
let charsUsed = 0;
|
||||
|
||||
// 1. Green filled blocks for completed tasks (done)
|
||||
const completedChars = Math.round((completionPercentage / 100) * width);
|
||||
if (completedChars > 0) {
|
||||
bar += chalk.green('█').repeat(completedChars);
|
||||
charsUsed += completedChars;
|
||||
}
|
||||
|
||||
// 2. Gray filled blocks for cancelled (won't be done)
|
||||
if (statusBreakdown.cancelled && charsUsed < width) {
|
||||
const cancelledChars = Math.round(
|
||||
(statusBreakdown.cancelled / 100) * width
|
||||
);
|
||||
const actualChars = Math.min(cancelledChars, width - charsUsed);
|
||||
if (actualChars > 0) {
|
||||
bar += chalk.gray('█').repeat(actualChars);
|
||||
charsUsed += actualChars;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Gray filled blocks for deferred (won't be done now)
|
||||
if (statusBreakdown.deferred && charsUsed < width) {
|
||||
const deferredChars = Math.round((statusBreakdown.deferred / 100) * width);
|
||||
const actualChars = Math.min(deferredChars, width - charsUsed);
|
||||
if (actualChars > 0) {
|
||||
bar += chalk.gray('█').repeat(actualChars);
|
||||
charsUsed += actualChars;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Blue filled blocks for in-progress (actively working)
|
||||
if (statusBreakdown['in-progress'] && charsUsed < width) {
|
||||
const inProgressChars = Math.round(
|
||||
(statusBreakdown['in-progress'] / 100) * width
|
||||
);
|
||||
const actualChars = Math.min(inProgressChars, width - charsUsed);
|
||||
if (actualChars > 0) {
|
||||
bar += chalk.blue('█').repeat(actualChars);
|
||||
charsUsed += actualChars;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Magenta empty blocks for review (almost done)
|
||||
if (statusBreakdown.review && charsUsed < width) {
|
||||
const reviewChars = Math.round((statusBreakdown.review / 100) * width);
|
||||
const actualChars = Math.min(reviewChars, width - charsUsed);
|
||||
if (actualChars > 0) {
|
||||
bar += chalk.magenta('░').repeat(actualChars);
|
||||
charsUsed += actualChars;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Yellow empty blocks for pending (ready to start)
|
||||
if (statusBreakdown.pending && charsUsed < width) {
|
||||
const pendingChars = Math.round((statusBreakdown.pending / 100) * width);
|
||||
const actualChars = Math.min(pendingChars, width - charsUsed);
|
||||
if (actualChars > 0) {
|
||||
bar += chalk.yellow('░').repeat(actualChars);
|
||||
charsUsed += actualChars;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Red empty blocks for blocked (can't start yet)
|
||||
if (statusBreakdown.blocked && charsUsed < width) {
|
||||
const blockedChars = Math.round((statusBreakdown.blocked / 100) * width);
|
||||
const actualChars = Math.min(blockedChars, width - charsUsed);
|
||||
if (actualChars > 0) {
|
||||
bar += chalk.red('░').repeat(actualChars);
|
||||
charsUsed += actualChars;
|
||||
}
|
||||
}
|
||||
|
||||
// Fill any remaining space with gray empty yellow blocks
|
||||
if (charsUsed < width) {
|
||||
bar += chalk.yellow('░').repeat(width - charsUsed);
|
||||
}
|
||||
|
||||
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<string, number> = {};
|
||||
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<TaskPriority, number> {
|
||||
const breakdown: Record<TaskPriority, number> = {
|
||||
critical: 0,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0
|
||||
};
|
||||
|
||||
tasks.forEach((task) => {
|
||||
const priority = task.priority || 'medium';
|
||||
breakdown[priority]++;
|
||||
});
|
||||
|
||||
return breakdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate status breakdown as percentages
|
||||
*/
|
||||
function calculateStatusBreakdown(stats: TaskStatistics): StatusBreakdown {
|
||||
if (stats.total === 0) return {};
|
||||
|
||||
return {
|
||||
'in-progress': (stats.inProgress / stats.total) * 100,
|
||||
pending: (stats.pending / stats.total) * 100,
|
||||
blocked: (stats.blocked / stats.total) * 100,
|
||||
deferred: (stats.deferred / stats.total) * 100,
|
||||
cancelled: (stats.cancelled / stats.total) * 100,
|
||||
review: ((stats.review || 0) / stats.total) * 100
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format status counts in the correct order with colors
|
||||
* @param stats - The statistics object containing counts
|
||||
* @param isSubtask - Whether this is for subtasks (affects "Done" vs "Completed" label)
|
||||
*/
|
||||
function formatStatusLine(
|
||||
stats: TaskStatistics,
|
||||
isSubtask: boolean = false
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Order: Done, Cancelled, Deferred, In Progress, Review, Pending, Blocked
|
||||
if (isSubtask) {
|
||||
parts.push(`Completed: ${chalk.green(`${stats.done}/${stats.total}`)}`);
|
||||
} else {
|
||||
parts.push(`Done: ${chalk.green(stats.done)}`);
|
||||
}
|
||||
|
||||
parts.push(`Cancelled: ${chalk.gray(stats.cancelled)}`);
|
||||
parts.push(`Deferred: ${chalk.gray(stats.deferred)}`);
|
||||
|
||||
// Add line break for second row
|
||||
const firstLine = parts.join(' ');
|
||||
parts.length = 0;
|
||||
|
||||
parts.push(`In Progress: ${chalk.blue(stats.inProgress)}`);
|
||||
parts.push(`Review: ${chalk.magenta(stats.review || 0)}`);
|
||||
parts.push(`Pending: ${chalk.yellow(stats.pending)}`);
|
||||
parts.push(`Blocked: ${chalk.red(stats.blocked)}`);
|
||||
|
||||
const secondLine = parts.join(' ');
|
||||
|
||||
return firstLine + '\n' + secondLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the project dashboard box
|
||||
*/
|
||||
export function displayProjectDashboard(
|
||||
taskStats: TaskStatistics,
|
||||
subtaskStats: TaskStatistics,
|
||||
priorityBreakdown: Record<TaskPriority, number>
|
||||
): string {
|
||||
// Calculate status breakdowns using the helper function
|
||||
const taskStatusBreakdown = calculateStatusBreakdown(taskStats);
|
||||
const subtaskStatusBreakdown = calculateStatusBreakdown(subtaskStats);
|
||||
|
||||
// Create progress bars with the breakdowns
|
||||
const taskProgressBar = createProgressBar(
|
||||
taskStats.completionPercentage,
|
||||
30,
|
||||
taskStatusBreakdown
|
||||
);
|
||||
const subtaskProgressBar = createProgressBar(
|
||||
subtaskStats.completionPercentage,
|
||||
30,
|
||||
subtaskStatusBreakdown
|
||||
);
|
||||
|
||||
const taskPercentage = `${taskStats.completionPercentage}% ${taskStats.done}/${taskStats.total}`;
|
||||
const subtaskPercentage = `${subtaskStats.completionPercentage}% ${subtaskStats.done}/${subtaskStats.total}`;
|
||||
|
||||
const content =
|
||||
chalk.white.bold('Project Dashboard') +
|
||||
'\n' +
|
||||
`Tasks Progress: ${taskProgressBar} ${chalk.yellow(taskPercentage)}\n` +
|
||||
formatStatusLine(taskStats, false) +
|
||||
'\n\n' +
|
||||
`Subtasks Progress: ${subtaskProgressBar} ${chalk.cyan(subtaskPercentage)}\n` +
|
||||
formatStatusLine(subtaskStats, true) +
|
||||
'\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<TaskPriority, number>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
47
apps/cli/src/ui/components/header.component.ts
Normal file
47
apps/cli/src/ui/components/header.component.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @fileoverview Task Master header component
|
||||
* Displays the banner, version, project info, and file path
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import figlet from 'figlet';
|
||||
import gradient from 'gradient-string';
|
||||
|
||||
/**
|
||||
* Header configuration options
|
||||
*/
|
||||
export interface HeaderOptions {
|
||||
title?: string;
|
||||
tag?: string;
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the Task Master header with project info
|
||||
*/
|
||||
export function displayHeader(options: HeaderOptions = {}): void {
|
||||
const { filePath, tag } = options;
|
||||
|
||||
// Display tag and file path info
|
||||
if (tag) {
|
||||
let tagInfo = '';
|
||||
|
||||
if (tag && tag !== 'master') {
|
||||
tagInfo = `🏷 tag: ${chalk.cyan(tag)}`;
|
||||
} else {
|
||||
tagInfo = `🏷 tag: ${chalk.cyan('master')}`;
|
||||
}
|
||||
|
||||
console.log(tagInfo);
|
||||
|
||||
if (filePath) {
|
||||
// Convert to absolute path if it's relative
|
||||
const absolutePath = filePath.startsWith('/')
|
||||
? filePath
|
||||
: `${process.cwd()}/${filePath}`;
|
||||
console.log(`Listing tasks from: ${chalk.dim(absolutePath)}`);
|
||||
}
|
||||
|
||||
console.log(); // Empty line for spacing
|
||||
}
|
||||
}
|
||||
9
apps/cli/src/ui/components/index.ts
Normal file
9
apps/cli/src/ui/components/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @fileoverview UI components exports
|
||||
*/
|
||||
|
||||
export * from './header.component.js';
|
||||
export * from './dashboard.component.js';
|
||||
export * from './next-task.component.js';
|
||||
export * from './suggested-steps.component.js';
|
||||
export * from './task-detail.component.js';
|
||||
134
apps/cli/src/ui/components/next-task.component.ts
Normal file
134
apps/cli/src/ui/components/next-task.component.ts
Normal 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;
|
||||
}
|
||||
31
apps/cli/src/ui/components/suggested-steps.component.ts
Normal file
31
apps/cli/src/ui/components/suggested-steps.component.ts
Normal 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
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
264
apps/cli/src/ui/components/task-detail.component.ts
Normal file
264
apps/cli/src/ui/components/task-detail.component.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* @fileoverview Task detail component for show command
|
||||
* Displays detailed task information in a structured format
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { marked, MarkedExtension } from 'marked';
|
||||
import { markedTerminal } from 'marked-terminal';
|
||||
import type { Task } from '@tm/core/types';
|
||||
import { getStatusWithColor, getPriorityWithColor } from '../../utils/ui.js';
|
||||
|
||||
// Configure marked to use terminal renderer with subtle colors
|
||||
marked.use(
|
||||
markedTerminal({
|
||||
// More subtle colors that match the overall design
|
||||
code: (code: string) => {
|
||||
// Custom code block handler to preserve formatting
|
||||
return code
|
||||
.split('\n')
|
||||
.map((line) => ' ' + chalk.cyan(line))
|
||||
.join('\n');
|
||||
},
|
||||
blockquote: chalk.gray.italic,
|
||||
html: chalk.gray,
|
||||
heading: chalk.white.bold, // White bold for headings
|
||||
hr: chalk.gray,
|
||||
listitem: chalk.white, // White for list items
|
||||
paragraph: chalk.white, // White for paragraphs (default text color)
|
||||
strong: chalk.white.bold, // White bold for strong text
|
||||
em: chalk.white.italic, // White italic for emphasis
|
||||
codespan: chalk.cyan, // Cyan for inline code (no background)
|
||||
del: chalk.dim.strikethrough,
|
||||
link: chalk.blue,
|
||||
href: chalk.blue.underline,
|
||||
// Add more explicit code block handling
|
||||
showSectionPrefix: false,
|
||||
unescape: true,
|
||||
emoji: false,
|
||||
// Try to preserve whitespace in code blocks
|
||||
tab: 4,
|
||||
width: 120
|
||||
}) as MarkedExtension
|
||||
);
|
||||
|
||||
// Also set marked options to preserve whitespace
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Display the task header with tag
|
||||
*/
|
||||
export function displayTaskHeader(
|
||||
taskId: string | number,
|
||||
title: string
|
||||
): void {
|
||||
// Display task header box
|
||||
console.log(
|
||||
boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display task properties in a table format
|
||||
*/
|
||||
export function displayTaskProperties(task: Task): void {
|
||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||
// Create table for task properties - simple 2-column layout
|
||||
const table = new Table({
|
||||
head: [],
|
||||
style: {
|
||||
head: [],
|
||||
border: ['grey']
|
||||
},
|
||||
colWidths: [
|
||||
Math.floor(terminalWidth * 0.2),
|
||||
Math.floor(terminalWidth * 0.8)
|
||||
],
|
||||
wordWrap: true
|
||||
});
|
||||
|
||||
const deps =
|
||||
task.dependencies && task.dependencies.length > 0
|
||||
? task.dependencies.map((d) => String(d)).join(', ')
|
||||
: 'None';
|
||||
|
||||
// Build the left column (labels) and right column (values)
|
||||
const labels = [
|
||||
chalk.cyan('ID:'),
|
||||
chalk.cyan('Title:'),
|
||||
chalk.cyan('Status:'),
|
||||
chalk.cyan('Priority:'),
|
||||
chalk.cyan('Dependencies:'),
|
||||
chalk.cyan('Complexity:'),
|
||||
chalk.cyan('Description:')
|
||||
].join('\n');
|
||||
|
||||
const values = [
|
||||
String(task.id),
|
||||
task.title,
|
||||
getStatusWithColor(task.status),
|
||||
getPriorityWithColor(task.priority),
|
||||
deps,
|
||||
'N/A',
|
||||
task.description || ''
|
||||
].join('\n');
|
||||
|
||||
table.push([labels, values]);
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display implementation details in a box
|
||||
*/
|
||||
export function displayImplementationDetails(details: string): void {
|
||||
// Handle all escaped characters properly
|
||||
const cleanDetails = details
|
||||
.replace(/\\n/g, '\n') // Convert \n to actual newlines
|
||||
.replace(/\\t/g, '\t') // Convert \t to actual tabs
|
||||
.replace(/\\"/g, '"') // Convert \" to actual quotes
|
||||
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
|
||||
|
||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||
|
||||
// Parse markdown to terminal-friendly format
|
||||
const markdownResult = marked(cleanDetails);
|
||||
const formattedDetails =
|
||||
typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
|
||||
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
|
||||
{
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'cyan', // Changed to cyan to match the original
|
||||
width: terminalWidth // Fixed width to match the original
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display test strategy in a box
|
||||
*/
|
||||
export function displayTestStrategy(testStrategy: string): void {
|
||||
// Handle all escaped characters properly (same as implementation details)
|
||||
const cleanStrategy = testStrategy
|
||||
.replace(/\\n/g, '\n') // Convert \n to actual newlines
|
||||
.replace(/\\t/g, '\t') // Convert \t to actual tabs
|
||||
.replace(/\\"/g, '"') // Convert \" to actual quotes
|
||||
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
|
||||
|
||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||
|
||||
// Parse markdown to terminal-friendly format (same as implementation details)
|
||||
const markdownResult = marked(cleanStrategy);
|
||||
const formattedStrategy =
|
||||
typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
|
||||
|
||||
console.log(
|
||||
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
|
||||
padding: 1,
|
||||
borderStyle: 'round',
|
||||
borderColor: 'cyan', // Changed to cyan to match implementation details
|
||||
width: terminalWidth
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display subtasks in a table format
|
||||
*/
|
||||
export function displaySubtasks(
|
||||
subtasks: Array<{
|
||||
id: string | number;
|
||||
title: string;
|
||||
status: any;
|
||||
description?: string;
|
||||
dependencies?: string[];
|
||||
}>,
|
||||
parentId: string | number
|
||||
): void {
|
||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||
// Display subtasks header
|
||||
console.log(
|
||||
boxen(chalk.magenta.bold('Subtasks'), {
|
||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||
borderColor: 'magenta',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, bottom: 0 }
|
||||
})
|
||||
);
|
||||
|
||||
// Create subtasks table
|
||||
const table = new Table({
|
||||
head: [
|
||||
chalk.magenta.bold('ID'),
|
||||
chalk.magenta.bold('Status'),
|
||||
chalk.magenta.bold('Title'),
|
||||
chalk.magenta.bold('Deps')
|
||||
],
|
||||
style: {
|
||||
head: [],
|
||||
border: ['grey']
|
||||
},
|
||||
colWidths: [
|
||||
Math.floor(terminalWidth * 0.1),
|
||||
Math.floor(terminalWidth * 0.15),
|
||||
Math.floor(terminalWidth * 0.6),
|
||||
Math.floor(terminalWidth * 0.15)
|
||||
],
|
||||
wordWrap: true
|
||||
});
|
||||
|
||||
subtasks.forEach((subtask) => {
|
||||
const subtaskId = `${parentId}.${subtask.id}`;
|
||||
|
||||
// Format dependencies
|
||||
const deps =
|
||||
subtask.dependencies && subtask.dependencies.length > 0
|
||||
? subtask.dependencies.join(', ')
|
||||
: 'None';
|
||||
|
||||
table.push([
|
||||
subtaskId,
|
||||
getStatusWithColor(subtask.status),
|
||||
subtask.title,
|
||||
deps
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display suggested actions
|
||||
*/
|
||||
export function displaySuggestedActions(taskId: string | number): void {
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.white.bold('Suggested Actions:') +
|
||||
'\n\n' +
|
||||
`${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
|
||||
`${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
|
||||
`${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
|
||||
{
|
||||
padding: 1,
|
||||
margin: { top: 1 },
|
||||
borderStyle: 'round',
|
||||
borderColor: 'green',
|
||||
width: process.stdout.columns * 0.95 || 100
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
9
apps/cli/src/ui/index.ts
Normal file
9
apps/cli/src/ui/index.ts
Normal file
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user