/**
 * SF Dashboard Overlay
 *
 * Full-screen overlay showing autonomous mode progress: milestone/slice/task
 * breakdown, current unit, completed units, timing, and activity log.
 * Toggled with Ctrl+Alt+G (⌃⌥G on macOS), Ctrl+Shift+G fallback,
 * or opened from /status.
 */
import {
	Key,
	matchesKey,
	truncateToWidth,
	visibleWidth,
} from "@singularity-forge/tui";
import {
	centerLine,
	fitColumns,
	formatDuration,
	joinColumns,
	padRight,
	STATUS_COLOR,
	STATUS_GLYPH,
} from "../shared/mod.js";
import { getAutoDashboardData } from "./auto.js";
import { estimateTimeRemaining } from "./auto-dashboard.js";
import { runEnvironmentChecks } from "./doctor-environment.js";
import { loadFile } from "./files.js";
import {
	aggregateByModel,
	aggregateByPhase,
	aggregateBySlice,
	aggregateCacheHitRate,
	formatCost,
	formatCostProjection,
	formatTokenCount,
	getLedger,
	getProjectTotals,
} from "./metrics.js";
import { resolveMilestoneFile } from "./paths.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import { computeProgressScore } from "./progress-score.js";
import { getMilestoneSlices, getSliceTasks, isDbAvailable } from "./sf-db.js";
import { formattedShortcutPair } from "./shortcut-defs.js";
import { deriveState } from "./state.js";
import {
	getWorkerBatches,
	hasActiveWorkers,
} from "./subagent/worker-registry.js";
import { writeUokDiagnostics } from "./uok/diagnostic-synthesis.js";
import { getActiveWorktreeName } from "./worktree-command.js";

function unitLabel(type) {
	switch (type) {
		case "discuss-milestone":
		case "discuss-slice":
			return "Discuss";
		case "research-milestone":
			return "Research";
		case "plan-milestone":
			return "Plan";
		case "research-slice":
			return "Research";
		case "plan-slice":
			return "Plan";
		case "execute-task":
			return "Execute";
		case "complete-slice":
			return "Complete";
		case "reassess-roadmap":
			return "Reassess";
		case "triage-captures":
			return "Triage";
		case "quick-task":
			return "Quick Task";
		case "replan-slice":
			return "Replan";
		case "custom-step":
			return "Workflow Step";
		default:
			return type;
	}
}
export class SFDashboardOverlay {
	tui;
	theme;
	onClose;
	cachedWidth;
	cachedLines;
	refreshTimer;
	scrollOffset = 0;
	dashData;
	milestoneData = null;
	uokDiagnostics = null;
	loading = true;
	loadedDashboardIdentity;
	refreshInFlight = null;
	disposed = false;
	resizeHandler = null;
	constructor(tui, theme, onClose) {
		this.tui = tui;
		this.theme = theme;
		this.onClose = onClose;
		this.dashData = getAutoDashboardData();
		// Invalidate cache on terminal resize
		this.resizeHandler = () => {
			if (this.disposed) return;
			this.invalidate();
			this.tui.requestRender();
		};
		process.stdout.on("resize", this.resizeHandler);
		this.scheduleRefresh(true);
		this.refreshTimer = setInterval(() => {
			this.scheduleRefresh();
		}, 10_000);
	}
	scheduleRefresh(initial = false) {
		if (this.refreshInFlight || this.disposed) return;
		this.refreshInFlight = this.refreshDashboard(initial).finally(() => {
			this.refreshInFlight = null;
		});
	}
	computeDashboardIdentity(dashData) {
		const base = dashData.basePath || process.cwd();
		const currentUnit = dashData.currentUnit
			? `${dashData.currentUnit.type}:${dashData.currentUnit.id}:${dashData.currentUnit.startedAt}`
			: "-";
		return [
			base,
			dashData.active ? "1" : "0",
			dashData.paused ? "1" : "0",
			currentUnit,
		].join("|");
	}
	async refreshDashboard(initial = false) {
		if (this.disposed) return;
		this.dashData = getAutoDashboardData();
		const nextIdentity = this.computeDashboardIdentity(this.dashData);
		if (initial || nextIdentity !== this.loadedDashboardIdentity) {
			const loaded = await this.loadData();
			if (this.disposed) return;
			if (loaded) {
				this.loadedDashboardIdentity = nextIdentity;
			}
		}
		if (initial) {
			this.loading = false;
		}
		this.invalidate();
		this.tui.requestRender();
	}
	async loadData() {
		const base = this.dashData.basePath || process.cwd();
		try {
			try {
				this.uokDiagnostics = writeUokDiagnostics(base);
			} catch {
				this.uokDiagnostics = null;
			}
			const state = await deriveState(base);
			if (!state.activeMilestone) {
				this.milestoneData = null;
				return true;
			}
			const mid = state.activeMilestone.id;
			const view = {
				id: mid,
				title: state.activeMilestone.title,
				slices: [],
				phase: state.phase,
				progress: {
					milestones: {
						total: state.progress?.milestones.total ?? state.registry.length,
						done:
							state.progress?.milestones.done ??
							state.registry.filter((entry) => entry.status === "complete")
								.length,
					},
				},
			};
			const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
			const _roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
			let normSlices = [];
			if (isDbAvailable()) {
				normSlices = getMilestoneSlices(mid).map((s) => ({
					id: s.id,
					done: s.status === "complete",
					title: s.title,
					risk: s.risk || "medium",
				}));
			}
			for (const s of normSlices) {
				const sliceView = {
					id: s.id,
					title: s.title,
					done: s.done,
					risk: s.risk,
					active: state.activeSlice?.id === s.id,
					tasks: [],
				};
				if (sliceView.active) {
					// Normalize tasks from DB
					if (isDbAvailable()) {
						const dbTasks = getSliceTasks(mid, s.id);
						sliceView.taskProgress = {
							done: dbTasks.filter(
								(t) => t.status === "complete" || t.status === "done",
							).length,
							total: dbTasks.length,
						};
						for (const t of dbTasks) {
							sliceView.tasks.push({
								id: t.id,
								title: t.title,
								done: t.status === "complete" || t.status === "done",
								active: state.activeTask?.id === t.id,
							});
						}
					}
				}
				view.slices.push(sliceView);
			}
			this.milestoneData = view;
			return true;
		} catch {
			// Don't crash the overlay
			return false;
		}
	}
	handleInput(data) {
		if (
			matchesKey(data, Key.escape) ||
			matchesKey(data, Key.ctrl("c")) ||
			matchesKey(data, Key.ctrlAlt("g")) ||
			matchesKey(data, Key.ctrlShift("g"))
		) {
			this.dispose();
			this.onClose();
			return;
		}
		if (matchesKey(data, Key.down) || matchesKey(data, "j")) {
			this.scrollOffset++;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (matchesKey(data, Key.up) || matchesKey(data, "k")) {
			this.scrollOffset = Math.max(0, this.scrollOffset - 1);
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (data === "g") {
			this.scrollOffset = 0;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
		if (data === "G") {
			this.scrollOffset = 999;
			this.invalidate();
			this.tui.requestRender();
			return;
		}
	}
	render(width) {
		if (this.cachedLines && this.cachedWidth === width) {
			return this.cachedLines;
		}
		const content = this.buildContentLines(width);
		const viewportHeight = Math.max(
			5,
			process.stdout.rows ? process.stdout.rows - 8 : 24,
		);
		const chromeHeight = 2;
		const visibleContentRows = Math.max(1, viewportHeight - chromeHeight);
		const maxScroll = Math.max(0, content.length - visibleContentRows);
		this.scrollOffset = Math.min(this.scrollOffset, maxScroll);
		const visibleContent = content.slice(
			this.scrollOffset,
			this.scrollOffset + visibleContentRows,
		);
		const lines = this.wrapInBox(visibleContent, width);
		this.cachedWidth = width;
		this.cachedLines = lines;
		return lines;
	}
	wrapInBox(inner, width) {
		const th = this.theme;
		const border = (s) => th.fg("borderAccent", s);
		const innerWidth = width - 4;
		const lines = [];
		lines.push(border("╭" + "─".repeat(width - 2) + "╮"));
		for (const line of inner) {
			const truncated = truncateToWidth(line, innerWidth);
			const padWidth = Math.max(0, innerWidth - visibleWidth(truncated));
			lines.push(
				border("│") +
					" " +
					truncated +
					" ".repeat(padWidth) +
					" " +
					border("│"),
			);
		}
		lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
		return lines;
	}
	buildContentLines(width) {
		const th = this.theme;
		const shellWidth = width - 4;
		const contentWidth = Math.min(shellWidth, 128);
		const sidePad = Math.max(0, Math.floor((shellWidth - contentWidth) / 2));
		const leftMargin = " ".repeat(sidePad);
		const lines = [];
		const row = (content = "") => {
			const truncated = truncateToWidth(content, contentWidth);
			return leftMargin + padRight(truncated, contentWidth);
		};
		const blank = () => row("");
		const hr = () => row(th.fg("dim", "─".repeat(contentWidth)));
		const centered = (content) => row(centerLine(content, contentWidth));
		const title = th.fg("accent", th.bold("SF Dashboard"));
		const isRemote = !!this.dashData.remoteSession;
		const status = this.dashData.active
			? `${Date.now() % 2000 < 1000 ? th.fg("success", "●") : th.fg("dim", "○")} ${th.fg("success", "AUTO")}`
			: this.dashData.paused
				? th.fg("warning", "⏸ PAUSED")
				: isRemote
					? `${Date.now() % 2000 < 1000 ? th.fg("success", "●") : th.fg("dim", "○")} ${th.fg("success", "AUTO")} ${th.fg("dim", `(PID ${this.dashData.remoteSession.pid})`)}`
					: th.fg("dim", "idle");
		const worktreeName = getActiveWorktreeName();
		const worktreeTag = worktreeName
			? `  ${th.fg("warning", `⎇ ${worktreeName}`)}`
			: "";
		let elapsedParts = "";
		if (this.dashData.active || this.dashData.paused) {
			// Guard: skip display when elapsed is zero or unreasonably large (>30 days)
			const elapsed = this.dashData.elapsed;
			elapsedParts =
				elapsed > 0 && elapsed < 30 * 24 * 3600_000
					? th.fg("dim", formatDuration(elapsed))
					: "";
			const eta = estimateTimeRemaining();
			if (eta) elapsedParts += th.fg("dim", `  ·  ${eta}`);
		} else if (isRemote) {
			elapsedParts = th.fg(
				"dim",
				`since ${this.dashData.remoteSession.startedAt.replace("T", " ").slice(0, 19)}`,
			);
		}
		lines.push(
			row(
				joinColumns(
					`${title}  ${status}${worktreeTag}`,
					elapsedParts,
					contentWidth,
				),
			),
		);
		// Progress score — traffic light indicator (#1221)
		if (this.dashData.active || this.dashData.paused) {
			const progressScore = computeProgressScore();
			const progressIcon =
				progressScore.level === "green"
					? th.fg("success", "●")
					: progressScore.level === "yellow"
						? th.fg("warning", "●")
						: th.fg("error", "●");
			lines.push(
				row(`${progressIcon} ${th.fg("text", progressScore.summary)}`),
			);
			// Show signal details when degraded — real-time visibility into what doctor found
			if (progressScore.level !== "green" && progressScore.signals.length > 0) {
				for (const signal of progressScore.signals) {
					const prefix =
						signal.kind === "positive"
							? th.fg("success", "  ✓")
							: signal.kind === "negative"
								? th.fg("error", "  ✗")
								: th.fg("dim", "  ·");
					lines.push(row(`${prefix} ${th.fg("dim", signal.label)}`));
				}
			}
		}
		if (this.uokDiagnostics) {
			const diagnosticColor =
				this.uokDiagnostics.verdict === "degraded"
					? "error"
					: this.uokDiagnostics.verdict === "attention"
						? "warning"
						: "dim";
			const issue = this.uokDiagnostics.issues?.[0]?.code;
			const label = [
				`UOK ${this.uokDiagnostics.verdict}`,
				this.uokDiagnostics.classification,
				issue,
			]
				.filter(Boolean)
				.join(" · ");
			lines.push(row(th.fg(diagnosticColor, label)));
		}
		lines.push(blank());
		if (this.dashData.currentUnit) {
			const cu = this.dashData.currentUnit;
			const currentElapsed = th.fg(
				"dim",
				formatDuration(Date.now() - cu.startedAt),
			);
			lines.push(
				row(
					joinColumns(
						`${th.fg("text", "Now")}: ${th.fg("accent", unitLabel(cu.type))} ${th.fg("text", cu.id)}`,
						currentElapsed,
						contentWidth,
					),
				),
			);
			lines.push(blank());
		} else if (this.dashData.paused) {
			lines.push(row(th.fg("dim", "/autonomous to resume")));
			lines.push(blank());
		} else if (isRemote) {
			const rs = this.dashData.remoteSession;
			const unitDisplay =
				rs.unitType === "starting" || rs.unitType === "resuming"
					? rs.unitType
					: `${unitLabel(rs.unitType)} ${rs.unitId}`;
			lines.push(row(th.fg("text", `Remote session: ${unitDisplay}`)));
			lines.push(blank());
		} else {
			lines.push(row(th.fg("dim", "No unit running · /autonomous to start")));
			lines.push(blank());
		}
		// Parallel workers section — shows active subagent sessions
		if (hasActiveWorkers()) {
			lines.push(hr());
			lines.push(row(th.fg("text", th.bold("Parallel Workers"))));
			lines.push(blank());
			const batches = getWorkerBatches();
			for (const [batchId, workers] of batches) {
				const _running = workers.filter((w) => w.status === "running").length;
				const done = workers.filter((w) => w.status === "completed").length;
				const failed = workers.filter((w) => w.status === "failed").length;
				const total = workers[0]?.batchSize ?? workers.length;
				lines.push(
					row(
						joinColumns(
							`  ${th.fg("accent", "⟐")} ${th.fg("text", `Batch ${batchId.slice(0, 8)}`)}`,
							th.fg("dim", `${done + failed}/${total} done`),
							contentWidth,
						),
					),
				);
				for (const w of workers) {
					const icon =
						w.status === "running"
							? th.fg("accent", "▸")
							: w.status === "completed"
								? th.fg("success", "✓")
								: th.fg("error", "✗");
					const elapsed = th.fg(
						"dim",
						formatDuration(Date.now() - w.startedAt),
					);
					const taskPreview = truncateToWidth(
						w.task,
						Math.max(20, contentWidth - 30),
					);
					lines.push(
						row(
							joinColumns(
								`    ${icon} ${th.fg("text", w.agent)} ${th.fg("dim", taskPreview)}`,
								elapsed,
								contentWidth,
							),
						),
					);
				}
			}
			lines.push(blank());
		}
		// Pending captures badge — only shown when captures are waiting for triage
		if (this.dashData.pendingCaptureCount > 0) {
			const count = this.dashData.pendingCaptureCount;
			lines.push(
				row(
					th.fg(
						"warning",
						`📌 ${count} pending capture${count === 1 ? "" : "s"} awaiting triage`,
					),
				),
			);
			lines.push(blank());
		}
		if (this.loading) {
			lines.push(centered(th.fg("dim", "Loading dashboard…")));
			return lines;
		}
		if (this.milestoneData) {
			const mv = this.milestoneData;
			lines.push(row(th.fg("text", th.bold(`${mv.id}: ${mv.title}`))));
			lines.push(blank());
			const totalSlices = mv.slices.length;
			const doneSlices = mv.slices.filter((s) => s.done).length;
			const totalMilestones = mv.progress.milestones.total;
			const doneMilestones = mv.progress.milestones.done;
			const activeSlice = mv.slices.find((s) => s.active);
			lines.push(blank());
			if (activeSlice?.taskProgress) {
				lines.push(
					row(
						this.renderProgressRow(
							"Tasks",
							activeSlice.taskProgress.done,
							activeSlice.taskProgress.total,
							"accent",
							contentWidth,
						),
					),
				);
			}
			lines.push(
				row(
					this.renderProgressRow(
						"Slices",
						doneSlices,
						totalSlices,
						"success",
						contentWidth,
					),
				),
			);
			lines.push(
				row(
					this.renderProgressRow(
						"Milestones",
						doneMilestones,
						totalMilestones,
						"warning",
						contentWidth,
					),
				),
			);
			lines.push(blank());
			for (const s of mv.slices) {
				const sliceStatus = s.done ? "done" : s.active ? "active" : "pending";
				const icon = th.fg(
					STATUS_COLOR[sliceStatus],
					STATUS_GLYPH[sliceStatus],
				);
				const titleColor = s.active ? "accent" : s.done ? "muted" : "dim";
				const titleText = th.fg(titleColor, `${s.id}: ${s.title}`);
				const risk = th.fg("dim", s.risk);
				lines.push(
					row(joinColumns(`  ${icon} ${titleText}`, risk, contentWidth)),
				);
				if (s.active && s.tasks.length > 0) {
					for (const t of s.tasks) {
						const taskStatus = t.done
							? "done"
							: t.active
								? "active"
								: "pending";
						const tIcon = th.fg(
							STATUS_COLOR[taskStatus],
							STATUS_GLYPH[taskStatus],
						);
						const tColor = t.active ? "warning" : t.done ? "muted" : "dim";
						const tTitle = th.fg(tColor, `${t.id}: ${t.title}`);
						lines.push(
							row(
								`      ${tIcon} ${truncateToWidth(tTitle, contentWidth - 6)}`,
							),
						);
					}
				}
			}
		} else {
			lines.push(centered(th.fg("dim", "No active milestone.")));
		}
		const ledger = getLedger();
		if (ledger && ledger.units.length > 0) {
			const totals = getProjectTotals(ledger.units);
			lines.push(blank());
			lines.push(hr());
			lines.push(row(th.fg("text", th.bold("Cost & Usage"))));
			lines.push(blank());
			// Show cost or request count (for copilot/subscription users where cost is 0)
			const costOrReqs =
				totals.cost > 0
					? `${th.fg("warning", formatCost(totals.cost))} total`
					: `${th.fg("text", String(totals.apiRequests))} requests`;
			lines.push(
				row(
					fitColumns(
						[
							costOrReqs,
							`${th.fg("text", formatTokenCount(totals.tokens.total))} tokens`,
							`${th.fg("text", String(totals.toolCalls))} tools`,
							`${th.fg("text", String(totals.units))} units`,
						],
						contentWidth,
						`  ${th.fg("dim", "·")}  `,
					),
				),
			);
			lines.push(
				row(
					fitColumns(
						[
							`${th.fg("dim", "in:")} ${th.fg("text", formatTokenCount(totals.tokens.input))}`,
							`${th.fg("dim", "out:")} ${th.fg("text", formatTokenCount(totals.tokens.output))}`,
							`${th.fg("dim", "cache-r:")} ${th.fg("text", formatTokenCount(totals.tokens.cacheRead))}`,
							`${th.fg("dim", "cache-w:")} ${th.fg("text", formatTokenCount(totals.tokens.cacheWrite))}`,
						],
						contentWidth,
						"  ",
					),
				),
			);
			// Budget aggregate line — only when data exists
			if (
				totals.totalTruncationSections > 0 ||
				totals.continueHereFiredCount > 0
			) {
				const budgetParts = [];
				if (totals.totalTruncationSections > 0) {
					budgetParts.push(
						th.fg(
							"warning",
							`${totals.totalTruncationSections} sections truncated`,
						),
					);
				}
				if (totals.continueHereFiredCount > 0) {
					budgetParts.push(
						th.fg(
							"error",
							`${totals.continueHereFiredCount} continue-here fired`,
						),
					);
				}
				lines.push(row(budgetParts.join(`  ${th.fg("dim", "·")}  `)));
			}
			const phases = aggregateByPhase(ledger.units);
			if (phases.length > 0) {
				lines.push(blank());
				lines.push(row(th.fg("dim", "By Phase")));
				for (const p of phases) {
					const pct =
						totals.cost > 0 ? Math.round((p.cost / totals.cost) * 100) : 0;
					const left = `  ${th.fg("text", p.phase.padEnd(14))}${th.fg("warning", formatCost(p.cost).padStart(8))}`;
					const right = th.fg(
						"dim",
						`${String(pct).padStart(3)}%  ${formatTokenCount(p.tokens.total)} tok  ${p.units} units`,
					);
					lines.push(row(joinColumns(left, right, contentWidth)));
				}
			}
			const slices = aggregateBySlice(ledger.units);
			if (slices.length > 0) {
				lines.push(blank());
				lines.push(row(th.fg("dim", "By Slice")));
				for (const s of slices) {
					const pct =
						totals.cost > 0 ? Math.round((s.cost / totals.cost) * 100) : 0;
					const left = `  ${th.fg("text", s.sliceId.padEnd(14))}${th.fg("warning", formatCost(s.cost).padStart(8))}`;
					const right = th.fg(
						"dim",
						`${String(pct).padStart(3)}%  ${formatTokenCount(s.tokens.total)} tok  ${formatDuration(s.duration)}`,
					);
					lines.push(row(joinColumns(left, right, contentWidth)));
				}
			}
			// Cost projection — only when active milestone data is available
			if (this.milestoneData) {
				const mv = this.milestoneData;
				const msTotalSlices = mv.slices.length;
				const msDoneSlices = mv.slices.filter((s) => s.done).length;
				const remainingCount = msTotalSlices - msDoneSlices;
				const overlayPrefs = loadEffectiveSFPreferences()?.preferences;
				const projLines = formatCostProjection(
					slices,
					remainingCount,
					overlayPrefs?.budget_ceiling,
				);
				if (projLines.length > 0) {
					lines.push(blank());
					for (const line of projLines) {
						const colored = line.toLowerCase().includes("ceiling")
							? th.fg("warning", line)
							: th.fg("dim", line);
						lines.push(row(colored));
					}
				}
			}
			const models = aggregateByModel(ledger.units);
			if (models.length >= 1) {
				lines.push(blank());
				lines.push(row(th.fg("dim", "By Model")));
				for (const m of models) {
					const pct =
						totals.cost > 0 ? Math.round((m.cost / totals.cost) * 100) : 0;
					const modelName = truncateToWidth(m.model, 38);
					const ctxWindow =
						m.contextWindowTokens !== undefined
							? th.fg("dim", ` [${formatTokenCount(m.contextWindowTokens)}]`)
							: "";
					const left = `  ${th.fg("text", modelName.padEnd(38))}${th.fg("warning", formatCost(m.cost).padStart(8))}`;
					const right =
						th.fg("dim", `${String(pct).padStart(3)}%  ${m.units} units`) +
						ctxWindow;
					lines.push(row(joinColumns(left, right, contentWidth)));
				}
			}
			lines.push(blank());
			lines.push(
				row(
					`${th.fg("dim", "avg/unit:")} ${th.fg("text", formatCost(totals.cost / totals.units))}  ${th.fg("dim", "·")}  ${th.fg("text", formatTokenCount(Math.round(totals.tokens.total / totals.units)))} tokens`,
				),
			);
			// Cache hit rate
			const cacheRate = aggregateCacheHitRate();
			if (cacheRate > 0) {
				lines.push(
					row(
						`${th.fg("dim", "cache hit rate:")} ${th.fg("text", `${cacheRate}%`)}`,
					),
				);
			}
			if (
				this.dashData.rtkEnabled &&
				this.dashData.rtkSavings &&
				this.dashData.rtkSavings.commands > 0
			) {
				const rtk = this.dashData.rtkSavings;
				lines.push(
					row(
						`${th.fg("dim", "rtk saved:")} ${th.fg("text", formatTokenCount(rtk.savedTokens))} ${th.fg("dim", `(${Math.round(rtk.savingsPct)}% · ${rtk.commands} cmd${rtk.commands === 1 ? "" : "s"})`)}`,
					),
				);
			}
		}
		// UOK Health section — aligns with headless status output
		if (this.uokDiagnostics && this.uokDiagnostics.issues.length > 0) {
			lines.push(blank());
			lines.push(hr());
			lines.push(row(th.fg("text", th.bold("UOK Health"))));
			lines.push(blank());
			// Compact summary line matching headless format
			lines.push(
				row(
					th.fg(
						this.uokDiagnostics.verdict === "degraded"
							? "error"
							: this.uokDiagnostics.verdict === "attention"
								? "warning"
								: "dim",
						`Verdict: ${this.uokDiagnostics.verdict} (${this.uokDiagnostics.classification})`,
					),
				),
			);
			lines.push(blank());
			// Issue list
			for (const issue of this.uokDiagnostics.issues) {
				const icon =
					issue.severity === "error"
						? th.fg("error", "✗")
						: th.fg("warning", "⚠");
				lines.push(row(`  ${icon} ${th.fg("text", issue.code)}`));
				lines.push(row(th.fg("dim", `     ${issue.message}`)));
			}
			// Recommendations
			if (this.uokDiagnostics.recommendations.length > 0) {
				lines.push(blank());
				for (const rec of this.uokDiagnostics.recommendations) {
					lines.push(row(th.fg("dim", `  → ${rec}`)));
				}
			}
			// Signals table
			if (this.uokDiagnostics.signals) {
				lines.push(blank());
				lines.push(row(th.fg("dim", "Signals:")));
				for (const [key, value] of Object.entries(
					this.uokDiagnostics.signals,
				)) {
					const signalColor =
						value === "ok" ||
						value === "active" ||
						value === "consistent" ||
						value === "clear"
							? "success"
							: value === "unknown"
								? "dim"
								: "warning";
					lines.push(
						row(
							`  ${th.fg(signalColor, "●")} ${th.fg("text", key)}: ${th.fg(signalColor, String(value))}`,
						),
					);
				}
			}
		}
		// Environment health section (#1221) — only show issues
		const envResults = runEnvironmentChecks(
			this.dashData.basePath || process.cwd(),
		);
		const envIssues = envResults.filter((r) => r.status !== "ok");
		if (envIssues.length > 0) {
			lines.push(blank());
			lines.push(hr());
			lines.push(row(th.fg("text", th.bold("Environment"))));
			lines.push(blank());
			for (const r of envIssues) {
				const icon =
					r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
				lines.push(row(`  ${icon} ${th.fg("text", r.message)}`));
				if (r.detail) {
					lines.push(row(th.fg("dim", `     ${r.detail}`)));
				}
			}
		}
		lines.push(blank());
		lines.push(hr());
		lines.push(
			centered(
				th.fg(
					"dim",
					`↑↓ scroll · g/G top/end · Esc/${formattedShortcutPair("dashboard")} close`,
				),
			),
		);
		return lines;
	}
	renderProgressRow(label, done, total, color, width) {
		const th = this.theme;
		const pct = total > 0 ? Math.round((done / total) * 100) : 0;
		const labelWidth = 12;
		const rightWidth = 14;
		const gap = 2;
		const labelText = truncateToWidth(label, labelWidth, "").padEnd(labelWidth);
		const ratioText = `${done}/${total}`;
		const rightText = `${String(pct).padStart(3)}%  ${ratioText.padStart(rightWidth - 5)}`;
		const barWidth = Math.max(12, width - labelWidth - rightWidth - gap * 2);
		const filled = total > 0 ? Math.round((done / total) * barWidth) : 0;
		const bar =
			th.fg(color, "█".repeat(filled)) +
			th.fg("dim", "░".repeat(Math.max(0, barWidth - filled)));
		return `${th.fg("dim", labelText)}${" ".repeat(gap)}${bar}${" ".repeat(gap)}${th.fg("dim", rightText)}`;
	}
	invalidate() {
		this.cachedWidth = undefined;
		this.cachedLines = undefined;
	}
	dispose() {
		this.disposed = true;
		clearInterval(this.refreshTimer);
		if (this.resizeHandler) {
			process.stdout.removeListener("resize", this.resizeHandler);
			this.resizeHandler = null;
		}
	}
}
