/** * Mission Control TUI Progress Bar * * Bottom bar displaying: * - Visual progress bar using block characters * - Percentage completion * - Task count (completed/total) * * Block characters for progress bar: * - Full: \u2588 (█) * - Empty: \u2591 (░) */ import type { Theme } from "@mariozechner/pi-coding-agent"; import type { Run } from "../state.js"; import { truncateToWidth } from "@mariozechner/pi-tui"; // Block characters for progress bar const BLOCK_FULL = "\u2588"; // █ const BLOCK_EMPTY = "\u2591"; // ░ export interface ProgressBarProps { run: Run | null; statusMessage: string; } /** * Count completed and total tasks across all phases */ function countTasks(run: Run): { completed: number; total: number; phaseCompleted: number; phaseTotal: number } { let completed = 0; let total = 0; let phaseCompleted = 0; let phaseTotal = run.phases.length; for (const phase of run.phases) { if (phase.status === "done") { phaseCompleted++; } for (const task of phase.tasks) { if (task.status === "removed") continue; total++; if (task.status === "done") { completed++; } } } return { completed, total, phaseCompleted, phaseTotal }; } /** * Render a progress bar string */ function renderProgressBar(percent: number, width: number): string { if (width < 3) return ""; const filled = Math.round((percent / 100) * width); const empty = width - filled; return BLOCK_FULL.repeat(filled) + BLOCK_EMPTY.repeat(empty); } /** * Render the progress bar component */ export function renderProgressBarComponent( width: number, props: ProgressBarProps, theme: Theme ): string[] { const { run } = props; const lines: string[] = []; if (!run || run.phases.length === 0) { const suffix = "[0/0 Tasks]"; const progressBar = renderProgressBar(0, Math.max(0, width - suffix.length - 4)); lines.push(truncateToWidth(theme.fg("muted", `[${progressBar}] ${suffix}`), width)); return lines; } // Count tasks const { completed, total } = countTasks(run); const percent = total > 0 ? Math.round((completed / total) * 100) : 0; const suffix = `[${completed}/${total} Tasks]`; const barWidth = Math.max(0, width - suffix.length - 4); const progressBar = renderProgressBar(percent, barWidth); lines.push(truncateToWidth(theme.fg("accent", `[${progressBar}] ${suffix}`), width)); return lines; } /** * Progress bar component class */ export class ProgressBarComponent { private props: ProgressBarProps; private cachedWidth?: number; private cachedLines?: string[]; constructor(run: Run | null = null, statusMessage = "") { this.props = { run, statusMessage }; } update(run: Run | null, statusMessage?: string): void { this.props.run = run; if (statusMessage !== undefined) { this.props.statusMessage = statusMessage; } this.invalidate(); } updateStatus(message: string): void { this.props.statusMessage = message; this.invalidate(); } getCompletionPercent(): number { const { run } = this.props; if (!run) return 0; let completed = 0; let total = 0; for (const phase of run.phases) { for (const task of phase.tasks) { if (task.status === "removed") continue; total++; if (task.status === "done") { completed++; } } } return total > 0 ? Math.round((completed / total) * 100) : 0; } render(width: number, theme: Theme): string[] { if (this.cachedLines && this.cachedWidth === width) { return this.cachedLines; } this.cachedLines = renderProgressBarComponent(width, this.props, theme); this.cachedWidth = width; return this.cachedLines; } invalidate(): void { this.cachedWidth = undefined; this.cachedLines = undefined; } }