import { newMultiBar } from './cli-progress-factory.js'; /** * Base class for progress trackers, handling common logic for time, tokens, estimation, and multibar management. */ export class BaseProgressTracker { constructor(options = {}) { this.numUnits = options.numUnits || 1; this.unitName = options.unitName || 'unit'; // e.g., 'task', 'subtask' this.startTime = null; this.completedUnits = 0; this.tokensIn = 0; this.tokensOut = 0; this.isEstimate = true; // For token display // Time estimation properties this.bestAvgTimePerUnit = null; this.lastEstimateTime = null; this.lastEstimateSeconds = 0; // UI components this.multibar = null; this.timeTokensBar = null; this.progressBar = null; this._timerInterval = null; // State flags this.isStarted = false; this.isFinished = false; // Allow subclasses to define custom properties this._initializeCustomProperties(options); } /** * Protected method for subclasses to initialize custom properties. * @protected */ _initializeCustomProperties(options) { // Subclasses can override this } /** * Get the pluralized form of the unit name for safe property keys. * @returns {string} Pluralized unit name */ get unitNamePlural() { return `${this.unitName}s`; } start() { if (this.isStarted || this.isFinished) return; this.isStarted = true; this.startTime = Date.now(); this.multibar = newMultiBar(); // Create time/tokens bar using subclass-provided format this.timeTokensBar = this.multibar.create( 1, 0, {}, { format: this._getTimeTokensBarFormat(), barsize: 1, hideCursor: true, clearOnComplete: false } ); // Create main progress bar using subclass-provided format this.progressBar = this.multibar.create( this.numUnits, 0, {}, { format: this._getProgressBarFormat(), barCompleteChar: '\u2588', barIncompleteChar: '\u2591' } ); this._updateTimeTokensBar(); this.progressBar.update(0, { [this.unitNamePlural]: `0/${this.numUnits}` }); // Start timer this._timerInterval = setInterval(() => this._updateTimeTokensBar(), 1000); // Allow subclasses to add custom bars or setup this._setupCustomUI(); } /** * Protected method for subclasses to add custom UI elements after start. * @protected */ _setupCustomUI() { // Subclasses can override this } /** * Protected method to get the format for the time/tokens bar. * @protected * @returns {string} Format string for the time/tokens bar. */ _getTimeTokensBarFormat() { return `{clock} {elapsed} | Tokens (I/O): {in}/{out} | Est: {remaining}`; } /** * Protected method to get the format for the main progress bar. * @protected * @returns {string} Format string for the progress bar. */ _getProgressBarFormat() { return `${this.unitName.charAt(0).toUpperCase() + this.unitName.slice(1)}s {${this.unitNamePlural}} |{bar}| {percentage}%`; } updateTokens(tokensIn, tokensOut, isEstimate = false) { this.tokensIn = tokensIn || 0; this.tokensOut = tokensOut || 0; this.isEstimate = isEstimate; this._updateTimeTokensBar(); } _updateTimeTokensBar() { if (!this.timeTokensBar || this.isFinished) return; const elapsed = this._formatElapsedTime(); const remaining = this._estimateRemainingTime(); const tokensLabel = this.isEstimate ? '~ Tokens (I/O)' : 'Tokens (I/O)'; this.timeTokensBar.update(1, { clock: '⏱️', elapsed, in: this.tokensIn, out: this.tokensOut, remaining, tokensLabel, // Subclasses can add more payload here via override ...this._getCustomTimeTokensPayload() }); } /** * Protected method for subclasses to provide custom payload for time/tokens bar. * @protected * @returns {Object} Custom payload object. */ _getCustomTimeTokensPayload() { return {}; } _formatElapsedTime() { if (!this.startTime) return '0m 00s'; const seconds = Math.floor((Date.now() - this.startTime) / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}m ${remainingSeconds.toString().padStart(2, '0')}s`; } _estimateRemainingTime() { const progress = this._getProgressFraction(); if (progress >= 1) return '~0s'; const now = Date.now(); const elapsed = (now - this.startTime) / 1000; if (progress === 0) return '~calculating...'; const avgTimePerUnit = elapsed / progress; if ( this.bestAvgTimePerUnit === null || avgTimePerUnit < this.bestAvgTimePerUnit ) { this.bestAvgTimePerUnit = avgTimePerUnit; } const remainingUnits = this.numUnits * (1 - progress); let estimatedSeconds = Math.ceil(remainingUnits * this.bestAvgTimePerUnit); // Stabilization logic if (this.lastEstimateTime) { const elapsedSinceEstimate = Math.floor( (now - this.lastEstimateTime) / 1000 ); const countdownSeconds = Math.max( 0, this.lastEstimateSeconds - elapsedSinceEstimate ); if (countdownSeconds === 0) return '~0s'; estimatedSeconds = Math.min(estimatedSeconds, countdownSeconds); } this.lastEstimateTime = now; this.lastEstimateSeconds = estimatedSeconds; return `~${this._formatDuration(estimatedSeconds)}`; } /** * Protected method for subclasses to calculate current progress fraction (0-1). * Defaults to simple completedUnits / numUnits. * @protected * @returns {number} Progress fraction (can be fractional for subtasks). */ _getProgressFraction() { return this.completedUnits / this.numUnits; } _formatDuration(seconds) { if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes < 60) { return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`; } const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; return `${hours}h ${remainingMinutes}m`; } getElapsedTime() { return this.startTime ? Date.now() - this.startTime : 0; } stop() { if (this.isFinished) return; this.isFinished = true; if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } if (this.multibar) { this._updateTimeTokensBar(); this.multibar.stop(); } // Ensure cleanup is called to prevent memory leaks this.cleanup(); } getSummary() { return { completedUnits: this.completedUnits, elapsedTime: this.getElapsedTime() // Subclasses should extend this }; } /** * Cleanup method to ensure proper resource disposal and prevent memory leaks. * Should be called when the progress tracker is no longer needed. */ cleanup() { // Stop any active timers if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } // Stop and clear multibar if (this.multibar) { try { this.multibar.stop(); } catch (error) { // Ignore errors during cleanup } this.multibar = null; } // Clear progress bar references this.timeTokensBar = null; this.progressBar = null; // Reset state this.isStarted = false; this.isFinished = true; // Allow subclasses to perform custom cleanup this._performCustomCleanup(); } /** * Protected method for subclasses to perform custom cleanup. * @protected */ _performCustomCleanup() { // Subclasses can override this } }