Skip to main content
Glama
task-detail.component.ts9.83 kB
/** * @fileoverview Task detail component for show command * Displays detailed task information in a structured format */ import type { StorageType, Subtask, Task } from '@tm/core'; import boxen from 'boxen'; import chalk from 'chalk'; import Table from 'cli-table3'; import { MarkedExtension, marked } from 'marked'; import { markedTerminal } from 'marked-terminal'; import TurndownService from 'turndown'; import { getComplexityWithColor, getPriorityWithColor, getStatusWithColor } from '../../utils/ui.js'; // Initialize turndown for HTML to Markdown conversion const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', bulletListMarker: '-' }); /** * Convert HTML content to Markdown, then render for terminal * Handles tiptap HTML from Hamster gracefully */ export function renderContent(content: string): string { if (!content) return ''; // Clean up escape characters first - order matters: handle escaped backslashes first let cleaned = content .replace(/\\\\/g, '\\') .replace(/\\n/g, '\n') .replace(/\\t/g, '\t') .replace(/\\"/g, '"'); // Check if content has HTML tags - if so, convert to markdown first if (/<[^>]+>/.test(cleaned)) { cleaned = turndownService.turndown(cleaned); } // Render markdown to terminal const result = marked(cleaned); return typeof result === 'string' ? result.trim() : cleaned; } // 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, // Any remaining HTML will be grayed out (should be rare after turndown) 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 | Subtask, originalTaskId?: string ): 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'; // Use originalTaskId if provided (for subtasks like "104.1") const displayId = originalTaskId || String(task.id); // Render description with markdown/HTML support (handles tiptap HTML from Hamster) const renderedDescription = renderContent(task.description || ''); // 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 = [ displayId, task.title, getStatusWithColor(task.status), getPriorityWithColor(task.priority), deps, typeof task.complexity === 'number' ? getComplexityWithColor(task.complexity) : chalk.gray('N/A'), renderedDescription ].join('\n'); table.push([labels, values]); console.log(table.toString()); } /** * Display implementation details in a box */ export function displayImplementationDetails(details: string): void { const terminalWidth = process.stdout.columns * 0.95 || 100; const formattedDetails = renderContent(details); console.log( boxen( chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails, { padding: 1, borderStyle: 'round', borderColor: 'cyan', width: terminalWidth } ) ); } /** * Display test strategy in a box */ export function displayTestStrategy(testStrategy: string): void { const terminalWidth = process.stdout.columns * 0.95 || 100; const formattedStrategy = renderContent(testStrategy); console.log( boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, { padding: 1, borderStyle: 'round', borderColor: 'cyan', width: terminalWidth }) ); } /** * Display subtasks in a table format */ export function displaySubtasks( subtasks: Array<{ id: string | number; title: string; status: any; description?: string; dependencies?: string[]; }>, parentTaskId?: string | number, storageType?: Exclude<StorageType, 'auto'> ): 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) => { // Format subtask ID based on storage type: // - File storage: Show parent prefix (e.g., 10.1, 10.2) // - API storage: Show subtask ID only (e.g., 1, 2) const subtaskId = storageType === 'file' && parentTaskId ? `${parentTaskId}.${subtask.id}` : String(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 | Subtask, options?: { statusFilter?: string; showSuggestedActions?: boolean; customHeader?: string; headerColor?: string; originalTaskId?: string; storageType?: Exclude<StorageType, 'auto'>; } ): void { const { statusFilter, showSuggestedActions = false, customHeader, headerColor = 'blue', originalTaskId, storageType } = 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 { // Use originalTaskId if provided (for subtasks like "104.1") const displayId = originalTaskId || task.id; displayTaskHeader(displayId, task.title); } // Display task properties in table format displayTaskProperties(task, originalTaskId); // 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, storageType); } } // Display suggested actions if requested if (showSuggestedActions) { console.log(); // Empty line for spacing const actionTaskId = originalTaskId || task.id; displaySuggestedActions(actionTaskId); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server