Files
claude-task-master/src/progress/parse-prd-tracker.js
Joe Danziger e3ed4d7c14 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>
2025-08-12 22:37:07 +02:00

222 lines
5.2 KiB
JavaScript

import chalk from 'chalk';
import { newMultiBar } from './cli-progress-factory.js';
import { BaseProgressTracker } from './base-progress-tracker.js';
import {
createProgressHeader,
createProgressRow,
createBorder
} from './tracker-ui.js';
import {
getCliPriorityIndicators,
getPriorityIndicator,
getStatusBarPriorityIndicators,
getPriorityColors
} from '../ui/indicators.js';
// Get centralized priority indicators
const PRIORITY_INDICATORS = getCliPriorityIndicators();
const PRIORITY_DOTS = getStatusBarPriorityIndicators();
const PRIORITY_COLORS = getPriorityColors();
// Constants
const CONSTANTS = {
DEBOUNCE_DELAY: 100,
MAX_TITLE_LENGTH: 57,
TRUNCATED_LENGTH: 54,
TASK_ID_PAD_START: 3,
TASK_ID_PAD_END: 4,
PRIORITY_PAD_END: 3,
VALID_PRIORITIES: ['high', 'medium', 'low'],
DEFAULT_PRIORITY: 'medium'
};
/**
* Helper class to manage update debouncing
*/
class UpdateDebouncer {
constructor(delay = CONSTANTS.DEBOUNCE_DELAY) {
this.delay = delay;
this.pendingTimeout = null;
}
debounce(callback) {
this.clear();
this.pendingTimeout = setTimeout(() => {
callback();
this.pendingTimeout = null;
}, this.delay);
}
clear() {
if (this.pendingTimeout) {
clearTimeout(this.pendingTimeout);
this.pendingTimeout = null;
}
}
hasPending() {
return this.pendingTimeout !== null;
}
}
/**
* Helper class to manage priority counts
*/
class PriorityManager {
constructor() {
this.priorities = { high: 0, medium: 0, low: 0 };
}
increment(priority) {
const normalized = this.normalize(priority);
this.priorities[normalized]++;
return normalized;
}
normalize(priority) {
const lowercased = priority
? priority.toLowerCase()
: CONSTANTS.DEFAULT_PRIORITY;
return CONSTANTS.VALID_PRIORITIES.includes(lowercased)
? lowercased
: CONSTANTS.DEFAULT_PRIORITY;
}
getCounts() {
return { ...this.priorities };
}
}
/**
* Helper class for formatting task display elements
*/
class TaskFormatter {
static formatTitle(title, taskNumber) {
if (!title) return `Task ${taskNumber}`;
return title.length > CONSTANTS.MAX_TITLE_LENGTH
? title.substring(0, CONSTANTS.TRUNCATED_LENGTH) + '...'
: title;
}
static formatPriority(priority) {
return getPriorityIndicator(priority, false).padEnd(
CONSTANTS.PRIORITY_PAD_END,
' '
);
}
static formatTaskId(taskNumber) {
return taskNumber
.toString()
.padStart(CONSTANTS.TASK_ID_PAD_START, ' ')
.padEnd(CONSTANTS.TASK_ID_PAD_END, ' ');
}
}
/**
* Tracks progress for PRD parsing operations with multibar display
*/
class ParsePrdTracker extends BaseProgressTracker {
_initializeCustomProperties(options) {
this.append = options.append;
this.priorityManager = new PriorityManager();
this.debouncer = new UpdateDebouncer();
this.headerShown = false;
}
_getTimeTokensBarFormat() {
return `{clock} {elapsed} | ${PRIORITY_DOTS.high} {high} ${PRIORITY_DOTS.medium} {medium} ${PRIORITY_DOTS.low} {low} | Tokens (I/O): {in}/{out} | Est: {remaining}`;
}
_getProgressBarFormat() {
return 'Tasks {tasks} |{bar}| {percentage}%';
}
_getCustomTimeTokensPayload() {
return this.priorityManager.getCounts();
}
addTaskLine(taskNumber, title, priority = 'medium') {
if (!this.multibar || this.isFinished) return;
this._ensureHeaderShown();
const normalizedPriority = this._updateTaskCounters(taskNumber, priority);
// Immediately update the time/tokens bar to show the new priority count
this._updateTimeTokensBar();
this.debouncer.debounce(() => {
this._updateProgressDisplay(taskNumber, title, normalizedPriority);
});
}
_ensureHeaderShown() {
if (!this.headerShown) {
this.headerShown = true;
createProgressHeader(
this.multibar,
' TASK | PRI | TITLE',
'------+-----+----------------------------------------------------------------'
);
}
}
_updateTaskCounters(taskNumber, priority) {
const normalizedPriority = this.priorityManager.increment(priority);
this.completedUnits = taskNumber;
return normalizedPriority;
}
_updateProgressDisplay(taskNumber, title, normalizedPriority) {
this.progressBar.update(this.completedUnits, {
tasks: `${this.completedUnits}/${this.numUnits}`
});
const displayTitle = TaskFormatter.formatTitle(title, taskNumber);
const priorityDisplay = TaskFormatter.formatPriority(normalizedPriority);
const taskIdCentered = TaskFormatter.formatTaskId(taskNumber);
createProgressRow(
this.multibar,
` ${taskIdCentered} | ${priorityDisplay} | {title}`,
{ title: displayTitle }
);
createBorder(
this.multibar,
'------+-----+----------------------------------------------------------------'
);
this._updateTimeTokensBar();
}
finish() {
// Flush any pending updates before finishing
if (this.debouncer.hasPending()) {
this.debouncer.clear();
this._updateTimeTokensBar();
}
this.cleanup();
super.finish();
}
/**
* Override cleanup to handle pending updates
*/
_performCustomCleanup() {
this.debouncer.clear();
}
getSummary() {
return {
...super.getSummary(),
taskPriorities: this.priorityManager.getCounts(),
actionVerb: this.append ? 'appended' : 'generated'
};
}
}
export function createParsePrdTracker(options = {}) {
return new ParsePrdTracker(options);
}