feat: CLI & MCP progress tracking for parse-prd command (#1048)
* initial cutover * update log to debug * update tracker to pass units * update test to match new base tracker format * add streamTextService mocks * remove unused imports * Ensure the CLI waits for async main() completion * refactor to reduce code duplication * update comment * reuse function * ensure targetTag is defined in streaming mode * avoid throwing inside process.exit spy * check for null * remove reference to generate * fix formatting * fix textStream assignment * ensure no division by 0 * fix jest chalk mocks * refactor for maintainability * Improve bar chart calculation logic for consistent visual representation * use custom streaming error types; fix mocks * Update streamText extraction in parse-prd.js to match actual service response * remove check - doesn't belong here * update mocks * remove streaming test that wasn't really doing anything * add comment * make parsing logic more DRY * fix formatting * Fix textStream extraction to match actual service response * fix mock * Add a cleanup method to ensure proper resource disposal and prevent memory leaks * debounce progress updates to reduce UI flicker during rapid updates * Implement timeout protection for streaming operations (60-second timeout) with automatic fallback to non-streaming mode. * clear timeout properly * Add a maximum buffer size limit (1MB) to prevent unbounded memory growth with very large streaming responses. * fix formatting * remove duplicate mock * better docs * fix formatting * sanitize the dynamic property name * Fix incorrect remaining progress calculation * Use onError callback instead of console.warn * Remove unused chalk import * Add missing custom validator in fallback parsing configuration * add custom validator parameter in fallback parsing * chore: fix package-lock.json * chore: large code refactor * chore: increase timeout from 1 minute to 3 minutes * fix: refactor and fix streaming * Merge remote-tracking branch 'origin/next' into joedanz/parse-prd-progress * fix: cleanup and fix unit tests * chore: fix unit tests * chore: fix format * chore: run format * chore: fix weird CI unit test error * chore: fix format --------- Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
273
src/ui/indicators.js
Normal file
273
src/ui/indicators.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* indicators.js
|
||||
* UI functions for displaying priority and complexity indicators in different contexts
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { TASK_PRIORITY_OPTIONS } from '../constants/task-priority.js';
|
||||
|
||||
// Extract priority values for cleaner object keys
|
||||
const [HIGH, MEDIUM, LOW] = TASK_PRIORITY_OPTIONS;
|
||||
|
||||
// Cache for generated indicators
|
||||
const INDICATOR_CACHE = new Map();
|
||||
|
||||
/**
|
||||
* Base configuration for indicator systems
|
||||
*/
|
||||
class IndicatorConfig {
|
||||
constructor(name, levels, colors, thresholds = null) {
|
||||
this.name = name;
|
||||
this.levels = levels;
|
||||
this.colors = colors;
|
||||
this.thresholds = thresholds;
|
||||
}
|
||||
|
||||
getColor(level) {
|
||||
return this.colors[level] || chalk.gray;
|
||||
}
|
||||
|
||||
getLevelFromScore(score) {
|
||||
if (!this.thresholds) {
|
||||
throw new Error(`${this.name} does not support score-based levels`);
|
||||
}
|
||||
|
||||
if (score >= 7) return this.levels[0]; // high
|
||||
if (score <= 3) return this.levels[2]; // low
|
||||
return this.levels[1]; // medium
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual style definitions
|
||||
*/
|
||||
const VISUAL_STYLES = {
|
||||
cli: {
|
||||
filled: '●', // ●
|
||||
empty: '○' // ○
|
||||
},
|
||||
statusBar: {
|
||||
high: '⋮', // ⋮
|
||||
medium: ':', // :
|
||||
low: '.' // .
|
||||
},
|
||||
mcp: {
|
||||
high: '🔴', // 🔴
|
||||
medium: '🟠', // 🟠
|
||||
low: '🟢' // 🟢
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Priority configuration
|
||||
*/
|
||||
const PRIORITY_CONFIG = new IndicatorConfig('priority', [HIGH, MEDIUM, LOW], {
|
||||
[HIGH]: chalk.hex('#CC0000'),
|
||||
[MEDIUM]: chalk.hex('#FF8800'),
|
||||
[LOW]: chalk.yellow
|
||||
});
|
||||
|
||||
/**
|
||||
* Generates CLI indicator with intensity
|
||||
*/
|
||||
function generateCliIndicator(intensity, color) {
|
||||
const filled = VISUAL_STYLES.cli.filled;
|
||||
const empty = VISUAL_STYLES.cli.empty;
|
||||
|
||||
let indicator = '';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (i < intensity) {
|
||||
indicator += color(filled);
|
||||
} else {
|
||||
indicator += chalk.white(empty);
|
||||
}
|
||||
}
|
||||
return indicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intensity level from priority/complexity level
|
||||
*/
|
||||
function getIntensityFromLevel(level, levels) {
|
||||
const index = levels.indexOf(level);
|
||||
return 3 - index; // high=3, medium=2, low=1
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic cached indicator getter
|
||||
* @param {string} cacheKey - Cache key for the indicators
|
||||
* @param {Function} generator - Function to generate the indicators
|
||||
* @returns {Object} Cached or newly generated indicators
|
||||
*/
|
||||
function getCachedIndicators(cacheKey, generator) {
|
||||
if (INDICATOR_CACHE.has(cacheKey)) {
|
||||
return INDICATOR_CACHE.get(cacheKey);
|
||||
}
|
||||
|
||||
const indicators = generator();
|
||||
INDICATOR_CACHE.set(cacheKey, indicators);
|
||||
return indicators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators for MCP context (single emojis)
|
||||
* @returns {Object} Priority to emoji mapping
|
||||
*/
|
||||
export function getMcpPriorityIndicators() {
|
||||
return getCachedIndicators('mcp-priority-all', () => ({
|
||||
[HIGH]: VISUAL_STYLES.mcp.high,
|
||||
[MEDIUM]: VISUAL_STYLES.mcp.medium,
|
||||
[LOW]: VISUAL_STYLES.mcp.low
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators for CLI context (colored dots with visual hierarchy)
|
||||
* @returns {Object} Priority to colored dot string mapping
|
||||
*/
|
||||
export function getCliPriorityIndicators() {
|
||||
return getCachedIndicators('cli-priority-all', () => {
|
||||
const indicators = {};
|
||||
PRIORITY_CONFIG.levels.forEach((level) => {
|
||||
const intensity = getIntensityFromLevel(level, PRIORITY_CONFIG.levels);
|
||||
const color = PRIORITY_CONFIG.getColor(level);
|
||||
indicators[level] = generateCliIndicator(intensity, color);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators for status bars (simplified single character versions)
|
||||
* @returns {Object} Priority to single character indicator mapping
|
||||
*/
|
||||
export function getStatusBarPriorityIndicators() {
|
||||
return getCachedIndicators('statusbar-priority-all', () => {
|
||||
const indicators = {};
|
||||
PRIORITY_CONFIG.levels.forEach((level, index) => {
|
||||
const style =
|
||||
index === 0
|
||||
? VISUAL_STYLES.statusBar.high
|
||||
: index === 1
|
||||
? VISUAL_STYLES.statusBar.medium
|
||||
: VISUAL_STYLES.statusBar.low;
|
||||
const color = PRIORITY_CONFIG.getColor(level);
|
||||
indicators[level] = color(style);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority colors for consistent styling
|
||||
* @returns {Object} Priority to chalk color function mapping
|
||||
*/
|
||||
export function getPriorityColors() {
|
||||
return {
|
||||
[HIGH]: PRIORITY_CONFIG.colors[HIGH],
|
||||
[MEDIUM]: PRIORITY_CONFIG.colors[MEDIUM],
|
||||
[LOW]: PRIORITY_CONFIG.colors[LOW]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get priority indicators based on context
|
||||
* @param {boolean} isMcp - Whether this is for MCP context (true) or CLI context (false)
|
||||
* @returns {Object} Priority to indicator mapping
|
||||
*/
|
||||
export function getPriorityIndicators(isMcp = false) {
|
||||
return isMcp ? getMcpPriorityIndicators() : getCliPriorityIndicators();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific priority indicator
|
||||
* @param {string} priority - The priority level ('high', 'medium', 'low')
|
||||
* @param {boolean} isMcp - Whether this is for MCP context
|
||||
* @returns {string} The indicator string for the priority
|
||||
*/
|
||||
export function getPriorityIndicator(priority, isMcp = false) {
|
||||
const indicators = getPriorityIndicators(isMcp);
|
||||
return indicators[priority] || indicators[MEDIUM];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Complexity Indicators
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Complexity configuration
|
||||
*/
|
||||
const COMPLEXITY_CONFIG = new IndicatorConfig(
|
||||
'complexity',
|
||||
['high', 'medium', 'low'],
|
||||
{
|
||||
high: chalk.hex('#CC0000'),
|
||||
medium: chalk.hex('#FF8800'),
|
||||
low: chalk.green
|
||||
},
|
||||
{
|
||||
high: (score) => score >= 7,
|
||||
medium: (score) => score >= 4 && score <= 6,
|
||||
low: (score) => score <= 3
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Get complexity indicators for CLI context (colored dots with visual hierarchy)
|
||||
* Complexity scores: 1-3 (low), 4-6 (medium), 7-10 (high)
|
||||
* @returns {Object} Complexity level to colored dot string mapping
|
||||
*/
|
||||
export function getCliComplexityIndicators() {
|
||||
return getCachedIndicators('cli-complexity-all', () => {
|
||||
const indicators = {};
|
||||
COMPLEXITY_CONFIG.levels.forEach((level) => {
|
||||
const intensity = getIntensityFromLevel(level, COMPLEXITY_CONFIG.levels);
|
||||
const color = COMPLEXITY_CONFIG.getColor(level);
|
||||
indicators[level] = generateCliIndicator(intensity, color);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity indicators for status bars (simplified single character versions)
|
||||
* @returns {Object} Complexity level to single character indicator mapping
|
||||
*/
|
||||
export function getStatusBarComplexityIndicators() {
|
||||
return getCachedIndicators('statusbar-complexity-all', () => {
|
||||
const indicators = {};
|
||||
COMPLEXITY_CONFIG.levels.forEach((level, index) => {
|
||||
const style =
|
||||
index === 0
|
||||
? VISUAL_STYLES.statusBar.high
|
||||
: index === 1
|
||||
? VISUAL_STYLES.statusBar.medium
|
||||
: VISUAL_STYLES.statusBar.low;
|
||||
const color = COMPLEXITY_CONFIG.getColor(level);
|
||||
indicators[level] = color(style);
|
||||
});
|
||||
return indicators;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complexity colors for consistent styling
|
||||
* @returns {Object} Complexity level to chalk color function mapping
|
||||
*/
|
||||
export function getComplexityColors() {
|
||||
return { ...COMPLEXITY_CONFIG.colors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific complexity indicator based on score
|
||||
* @param {number} score - The complexity score (1-10)
|
||||
* @param {boolean} statusBar - Whether to return status bar version (single char)
|
||||
* @returns {string} The indicator string for the complexity level
|
||||
*/
|
||||
export function getComplexityIndicator(score, statusBar = false) {
|
||||
const level = COMPLEXITY_CONFIG.getLevelFromScore(score);
|
||||
const indicators = statusBar
|
||||
? getStatusBarComplexityIndicators()
|
||||
: getCliComplexityIndicators();
|
||||
return indicators[level];
|
||||
}
|
||||
477
src/ui/parse-prd.js
Normal file
477
src/ui/parse-prd.js
Normal file
@@ -0,0 +1,477 @@
|
||||
/**
|
||||
* parse-prd.js
|
||||
* UI functions specifically for PRD parsing operations
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import Table from 'cli-table3';
|
||||
import { formatElapsedTime } from '../utils/format.js';
|
||||
|
||||
// Constants
|
||||
const CONSTANTS = {
|
||||
BAR_WIDTH: 40,
|
||||
TABLE_COL_WIDTHS: [28, 50],
|
||||
DEFAULT_MODEL: 'Default',
|
||||
DEFAULT_TEMPERATURE: 0.7
|
||||
};
|
||||
|
||||
const PRIORITIES = {
|
||||
HIGH: 'high',
|
||||
MEDIUM: 'medium',
|
||||
LOW: 'low'
|
||||
};
|
||||
|
||||
const PRIORITY_COLORS = {
|
||||
[PRIORITIES.HIGH]: '#CC0000',
|
||||
[PRIORITIES.MEDIUM]: '#FF8800',
|
||||
[PRIORITIES.LOW]: '#FFCC00'
|
||||
};
|
||||
|
||||
// Reusable box styles
|
||||
const BOX_STYLES = {
|
||||
main: {
|
||||
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
||||
margin: { top: 0, bottom: 0 },
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round'
|
||||
},
|
||||
summary: {
|
||||
padding: { top: 1, right: 1, bottom: 1, left: 1 },
|
||||
borderColor: 'blue',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, right: 1, bottom: 1, left: 0 }
|
||||
},
|
||||
warning: {
|
||||
padding: 1,
|
||||
borderColor: 'yellow',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, bottom: 1 }
|
||||
},
|
||||
nextSteps: {
|
||||
padding: 1,
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round',
|
||||
margin: { top: 1, right: 0, bottom: 1, left: 0 }
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for building main message content
|
||||
* @param {Object} params - Message parameters
|
||||
* @param {string} params.prdFilePath - Path to the PRD file
|
||||
* @param {string} params.outputPath - Path where tasks will be saved
|
||||
* @param {number} params.numTasks - Number of tasks to generate
|
||||
* @param {string} params.model - AI model name
|
||||
* @param {number} params.temperature - AI temperature setting
|
||||
* @param {boolean} params.append - Whether appending to existing tasks
|
||||
* @param {boolean} params.research - Whether research mode is enabled
|
||||
* @returns {string} The formatted message content
|
||||
*/
|
||||
function buildMainMessage({
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
model,
|
||||
temperature,
|
||||
append,
|
||||
research
|
||||
}) {
|
||||
const actionVerb = append ? 'Appending' : 'Generating';
|
||||
|
||||
let modelLine = `Model: ${model} | Temperature: ${temperature}`;
|
||||
if (research) {
|
||||
modelLine += ` | ${chalk.cyan.bold('🔬 Research Mode')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
chalk.bold(`🤖 Parsing PRD and ${actionVerb} Tasks`) +
|
||||
'\n' +
|
||||
chalk.dim(modelLine) +
|
||||
'\n\n' +
|
||||
chalk.blue(`Input: ${prdFilePath}`) +
|
||||
'\n' +
|
||||
chalk.blue(`Output: ${outputPath}`) +
|
||||
'\n' +
|
||||
chalk.blue(`Tasks to ${append ? 'Append' : 'Generate'}: ${numTasks}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for displaying the main message box
|
||||
* @param {string} message - The message content to display in the box
|
||||
*/
|
||||
function displayMainMessageBox(message) {
|
||||
console.log(boxen(message, BOX_STYLES.main));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for displaying append mode notice
|
||||
* @param {number} existingTasksCount - Number of existing tasks
|
||||
* @param {number} nextId - Next ID to be used
|
||||
*/
|
||||
function displayAppendModeNotice(existingTasksCount, nextId) {
|
||||
console.log(
|
||||
chalk.yellow.bold('📝 Append mode') +
|
||||
` - Adding to ${existingTasksCount} existing tasks (next ID: ${nextId})`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for force mode messages
|
||||
* @param {boolean} append - Whether in append mode
|
||||
* @returns {string} The formatted force mode message
|
||||
*/
|
||||
function createForceMessage(append) {
|
||||
const baseMessage = chalk.red.bold('⚠️ Force flag enabled');
|
||||
return append
|
||||
? `${baseMessage} - Will overwrite if conflicts occur`
|
||||
: `${baseMessage} - Overwriting existing tasks`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the start of PRD parsing with a boxen announcement
|
||||
* @param {Object} options - Options for PRD parsing start
|
||||
* @param {string} options.prdFilePath - Path to the PRD file being parsed
|
||||
* @param {string} options.outputPath - Path where the tasks will be saved
|
||||
* @param {number} options.numTasks - Number of tasks to generate
|
||||
* @param {string} [options.model] - AI model name
|
||||
* @param {number} [options.temperature] - AI temperature setting
|
||||
* @param {boolean} [options.append=false] - Whether to append to existing tasks
|
||||
* @param {boolean} [options.research=false] - Whether research mode is enabled
|
||||
* @param {boolean} [options.force=false] - Whether force mode is enabled
|
||||
* @param {Array} [options.existingTasks=[]] - Existing tasks array
|
||||
* @param {number} [options.nextId=1] - Next ID to be used
|
||||
*/
|
||||
function displayParsePrdStart({
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
model = CONSTANTS.DEFAULT_MODEL,
|
||||
temperature = CONSTANTS.DEFAULT_TEMPERATURE,
|
||||
append = false,
|
||||
research = false,
|
||||
force = false,
|
||||
existingTasks = [],
|
||||
nextId = 1
|
||||
}) {
|
||||
// Input validation
|
||||
if (
|
||||
!prdFilePath ||
|
||||
typeof prdFilePath !== 'string' ||
|
||||
prdFilePath.trim() === ''
|
||||
) {
|
||||
throw new Error('prdFilePath is required and must be a non-empty string');
|
||||
}
|
||||
if (
|
||||
!outputPath ||
|
||||
typeof outputPath !== 'string' ||
|
||||
outputPath.trim() === ''
|
||||
) {
|
||||
throw new Error('outputPath is required and must be a non-empty string');
|
||||
}
|
||||
|
||||
// Build and display the main message box
|
||||
const message = buildMainMessage({
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
model,
|
||||
temperature,
|
||||
append,
|
||||
research
|
||||
});
|
||||
displayMainMessageBox(message);
|
||||
|
||||
// Display append/force notices beneath the boxen if either flag is set
|
||||
if (append || force) {
|
||||
// Add append mode details if enabled
|
||||
if (append) {
|
||||
displayAppendModeNotice(existingTasks.length, nextId);
|
||||
}
|
||||
|
||||
// Add force mode details if enabled
|
||||
if (force) {
|
||||
console.log(createForceMessage(append));
|
||||
}
|
||||
|
||||
// Add a blank line after notices for spacing
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate priority statistics
|
||||
* @param {Object} taskPriorities - Priority counts object
|
||||
* @param {number} totalTasks - Total number of tasks
|
||||
* @returns {Object} Priority statistics with counts and percentages
|
||||
*/
|
||||
function calculatePriorityStats(taskPriorities, totalTasks) {
|
||||
const stats = {};
|
||||
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
const count = taskPriorities[priority] || 0;
|
||||
stats[priority] = {
|
||||
count,
|
||||
percentage: totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0
|
||||
};
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bar character distribution for priorities
|
||||
* @param {Object} priorityStats - Priority statistics
|
||||
* @param {number} totalTasks - Total number of tasks
|
||||
* @returns {Object} Character counts for each priority
|
||||
*/
|
||||
function calculateBarDistribution(priorityStats, totalTasks) {
|
||||
const barWidth = CONSTANTS.BAR_WIDTH;
|
||||
const distribution = {};
|
||||
|
||||
if (totalTasks === 0) {
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
distribution[priority] = 0;
|
||||
});
|
||||
return distribution;
|
||||
}
|
||||
|
||||
// Calculate raw proportions
|
||||
const rawChars = {};
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
rawChars[priority] =
|
||||
(priorityStats[priority].count / totalTasks) * barWidth;
|
||||
});
|
||||
|
||||
// Initial distribution - floor values
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
distribution[priority] = Math.floor(rawChars[priority]);
|
||||
});
|
||||
|
||||
// Ensure non-zero priorities get at least 1 character
|
||||
Object.values(PRIORITIES).forEach((priority) => {
|
||||
if (priorityStats[priority].count > 0 && distribution[priority] === 0) {
|
||||
distribution[priority] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Distribute remaining characters based on decimal parts
|
||||
const currentTotal = Object.values(distribution).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
const remainingChars = barWidth - currentTotal;
|
||||
|
||||
if (remainingChars > 0) {
|
||||
const decimals = Object.values(PRIORITIES)
|
||||
.map((priority) => ({
|
||||
priority,
|
||||
decimal: rawChars[priority] - Math.floor(rawChars[priority])
|
||||
}))
|
||||
.sort((a, b) => b.decimal - a.decimal);
|
||||
|
||||
for (let i = 0; i < remainingChars && i < decimals.length; i++) {
|
||||
distribution[decimals[i].priority]++;
|
||||
}
|
||||
}
|
||||
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create priority distribution bar visual
|
||||
* @param {Object} barDistribution - Character distribution for priorities
|
||||
* @returns {string} Visual bar string
|
||||
*/
|
||||
function createPriorityBar(barDistribution) {
|
||||
let bar = '';
|
||||
|
||||
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])(
|
||||
'█'.repeat(barDistribution[PRIORITIES.HIGH])
|
||||
);
|
||||
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])(
|
||||
'█'.repeat(barDistribution[PRIORITIES.MEDIUM])
|
||||
);
|
||||
bar += chalk.yellow('█'.repeat(barDistribution[PRIORITIES.LOW]));
|
||||
|
||||
const totalChars = Object.values(barDistribution).reduce(
|
||||
(sum, val) => sum + val,
|
||||
0
|
||||
);
|
||||
if (totalChars < CONSTANTS.BAR_WIDTH) {
|
||||
bar += chalk.gray('░'.repeat(CONSTANTS.BAR_WIDTH - totalChars));
|
||||
}
|
||||
|
||||
return bar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build priority distribution row for table
|
||||
* @param {Object} priorityStats - Priority statistics
|
||||
* @returns {Array} Table row for priority distribution
|
||||
*/
|
||||
function buildPriorityRow(priorityStats) {
|
||||
const parts = [];
|
||||
|
||||
Object.entries(PRIORITIES).forEach(([key, priority]) => {
|
||||
const stats = priorityStats[priority];
|
||||
const color =
|
||||
priority === PRIORITIES.HIGH
|
||||
? chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])
|
||||
: priority === PRIORITIES.MEDIUM
|
||||
? chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])
|
||||
: chalk.yellow;
|
||||
|
||||
const label = key.charAt(0) + key.slice(1).toLowerCase();
|
||||
parts.push(
|
||||
`${color.bold(stats.count)} ${color(label)} (${stats.percentage}%)`
|
||||
);
|
||||
});
|
||||
|
||||
return [chalk.cyan('Priority distribution:'), parts.join(' · ')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a summary of the PRD parsing results
|
||||
* @param {Object} summary - Summary of the parsing results
|
||||
* @param {number} summary.totalTasks - Total number of tasks generated
|
||||
* @param {string} summary.prdFilePath - Path to the PRD file
|
||||
* @param {string} summary.outputPath - Path where the tasks were saved
|
||||
* @param {number} summary.elapsedTime - Total elapsed time in seconds
|
||||
* @param {Object} summary.taskPriorities - Breakdown of tasks by category/priority
|
||||
* @param {boolean} summary.usedFallback - Whether fallback parsing was used
|
||||
* @param {string} summary.actionVerb - Whether tasks were 'generated' or 'appended'
|
||||
*/
|
||||
function displayParsePrdSummary(summary) {
|
||||
const {
|
||||
totalTasks,
|
||||
taskPriorities = {},
|
||||
prdFilePath,
|
||||
outputPath,
|
||||
elapsedTime,
|
||||
usedFallback = false,
|
||||
actionVerb = 'generated'
|
||||
} = summary;
|
||||
|
||||
// Format the elapsed time
|
||||
const timeDisplay = formatElapsedTime(elapsedTime);
|
||||
|
||||
// Create a table for better alignment
|
||||
const table = new Table({
|
||||
chars: {
|
||||
top: '',
|
||||
'top-mid': '',
|
||||
'top-left': '',
|
||||
'top-right': '',
|
||||
bottom: '',
|
||||
'bottom-mid': '',
|
||||
'bottom-left': '',
|
||||
'bottom-right': '',
|
||||
left: '',
|
||||
'left-mid': '',
|
||||
mid: '',
|
||||
'mid-mid': '',
|
||||
right: '',
|
||||
'right-mid': '',
|
||||
middle: ' '
|
||||
},
|
||||
style: { border: [], 'padding-left': 2 },
|
||||
colWidths: CONSTANTS.TABLE_COL_WIDTHS
|
||||
});
|
||||
|
||||
// Basic info
|
||||
// Use the action verb to properly display if tasks were generated or appended
|
||||
table.push(
|
||||
[chalk.cyan(`Total tasks ${actionVerb}:`), chalk.bold(totalTasks)],
|
||||
[chalk.cyan('Processing time:'), chalk.bold(timeDisplay)]
|
||||
);
|
||||
|
||||
// Priority distribution if available
|
||||
if (taskPriorities && Object.keys(taskPriorities).length > 0) {
|
||||
const priorityStats = calculatePriorityStats(taskPriorities, totalTasks);
|
||||
const priorityRow = buildPriorityRow(priorityStats);
|
||||
table.push(priorityRow);
|
||||
|
||||
// Visual bar representation
|
||||
const barDistribution = calculateBarDistribution(priorityStats, totalTasks);
|
||||
const distributionBar = createPriorityBar(barDistribution);
|
||||
table.push([chalk.cyan('Distribution:'), distributionBar]);
|
||||
}
|
||||
|
||||
// Add file paths
|
||||
table.push(
|
||||
[chalk.cyan('PRD source:'), chalk.italic(prdFilePath)],
|
||||
[chalk.cyan('Tasks file:'), chalk.italic(outputPath)]
|
||||
);
|
||||
|
||||
// Add fallback parsing indicator if applicable
|
||||
if (usedFallback) {
|
||||
table.push([
|
||||
chalk.yellow('Fallback parsing:'),
|
||||
chalk.yellow('✓ Used fallback parsing')
|
||||
]);
|
||||
}
|
||||
|
||||
// Final string output with title and footer
|
||||
const output = [
|
||||
chalk.bold.underline(
|
||||
`PRD Parsing Complete - Tasks ${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)}`
|
||||
),
|
||||
'',
|
||||
table.toString()
|
||||
].join('\n');
|
||||
|
||||
// Display the summary box
|
||||
console.log(boxen(output, BOX_STYLES.summary));
|
||||
|
||||
// Show fallback parsing warning if needed
|
||||
if (usedFallback) {
|
||||
displayFallbackWarning();
|
||||
}
|
||||
|
||||
// Show next steps
|
||||
displayNextSteps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display fallback parsing warning
|
||||
*/
|
||||
function displayFallbackWarning() {
|
||||
const warningContent =
|
||||
chalk.yellow.bold('⚠️ Fallback Parsing Used') +
|
||||
'\n\n' +
|
||||
chalk.white(
|
||||
'The system used fallback parsing to complete task generation.'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white(
|
||||
'This typically happens when streaming JSON parsing is incomplete.'
|
||||
) +
|
||||
'\n' +
|
||||
chalk.white('Your tasks were successfully generated, but consider:') +
|
||||
'\n' +
|
||||
chalk.white('• Reviewing task completeness') +
|
||||
'\n' +
|
||||
chalk.white('• Checking for any missing details') +
|
||||
'\n\n' +
|
||||
chalk.white("This is normal and usually doesn't indicate any issues.");
|
||||
|
||||
console.log(boxen(warningContent, BOX_STYLES.warning));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display next steps after parsing
|
||||
*/
|
||||
function displayNextSteps() {
|
||||
const stepsContent =
|
||||
chalk.white.bold('Next Steps:') +
|
||||
'\n\n' +
|
||||
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
|
||||
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
|
||||
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master analyze-complexity')} to analyze task complexity`;
|
||||
|
||||
console.log(boxen(stepsContent, BOX_STYLES.nextSteps));
|
||||
}
|
||||
|
||||
export { displayParsePrdStart, displayParsePrdSummary, formatElapsedTime };
|
||||
Reference in New Issue
Block a user