Co-authored-by: Max Tuzzolino <maxtuzz@Maxs-MacBook-Pro.local> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Max Tuzzolino <max.tuzsmith@gmail.com> Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
336 lines
8.8 KiB
TypeScript
336 lines
8.8 KiB
TypeScript
/**
|
|
* @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
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Display complete task details - used by both show and start commands
|
|
*/
|
|
export function displayTaskDetails(
|
|
task: Task,
|
|
options?: {
|
|
statusFilter?: string;
|
|
showSuggestedActions?: boolean;
|
|
customHeader?: string;
|
|
headerColor?: string;
|
|
}
|
|
): void {
|
|
const {
|
|
statusFilter,
|
|
showSuggestedActions = false,
|
|
customHeader,
|
|
headerColor = 'blue'
|
|
} = options || {};
|
|
|
|
// Display header - either custom or default
|
|
if (customHeader) {
|
|
console.log(
|
|
boxen(chalk.white.bold(customHeader), {
|
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
borderColor: headerColor,
|
|
borderStyle: 'round',
|
|
margin: { top: 1 }
|
|
})
|
|
);
|
|
} else {
|
|
displayTaskHeader(task.id, task.title);
|
|
}
|
|
|
|
// Display task properties in table format
|
|
displayTaskProperties(task);
|
|
|
|
// Display implementation details if available
|
|
if (task.details) {
|
|
console.log(); // Empty line for spacing
|
|
displayImplementationDetails(task.details);
|
|
}
|
|
|
|
// Display test strategy if available
|
|
if ('testStrategy' in task && task.testStrategy) {
|
|
console.log(); // Empty line for spacing
|
|
displayTestStrategy(task.testStrategy as string);
|
|
}
|
|
|
|
// Display subtasks if available
|
|
if (task.subtasks && task.subtasks.length > 0) {
|
|
// Filter subtasks by status if provided
|
|
const filteredSubtasks = statusFilter
|
|
? task.subtasks.filter((sub) => sub.status === statusFilter)
|
|
: task.subtasks;
|
|
|
|
if (filteredSubtasks.length === 0 && statusFilter) {
|
|
console.log(); // Empty line for spacing
|
|
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
|
} else if (filteredSubtasks.length > 0) {
|
|
console.log(); // Empty line for spacing
|
|
displaySubtasks(filteredSubtasks, task.id);
|
|
}
|
|
}
|
|
|
|
// Display suggested actions if requested
|
|
if (showSuggestedActions) {
|
|
console.log(); // Empty line for spacing
|
|
displaySuggestedActions(task.id);
|
|
}
|
|
}
|