/**
 * OMK Chat Cockpit — terminal rendering and layout.
 */
import { readFile, readdir, stat as fsStat } from "fs/promises";
import { getGitNumstat, getGitBranch } from "../../cockpit/git-numstat.js";
import { getLspStatus } from "../../cockpit/lsp-status.js";
import { renderRailView } from "../../cockpit/views/rail-view.js";
import { getProjectRootAsync, pathExists, getRunPath, getRunsDir, } from "../../util/fs.js";
import { buildRunViewModel, parseRunStateResult, sanitizeForDisplay, } from "../../util/run-view-model.js";
import { style, sanitizeTerminalText, getSystemUsage, } from "../../util/theme.js";
import { renderMatrixRain } from "../../brand/matrix-rain.js";
import { visibleTerminalWidth, truncateLine, padEndVisible, panel as layoutPanel, fitLines, sectionHeader, } from "../../util/terminal-layout.js";
import { getKimiUsage } from "../../kimi/usage.js";
import { listRunCandidates } from "../hud.js";
import { loadTodos } from "../../util/todo-sync.js";
import { readSessionMeta } from "../../util/session.js";
import { readEvents } from "../../util/events-logger.js";
import { PANEL_HORIZONTAL_OVERHEAD, withTimeout, truncateText, statusRank, formatElapsed, getCacheEntry, getGitChanges, getCockpitResources, getCockpitDeepSeekSnapshot, formatDeepSeekBalance, computeCockpitLayout, } from "./utils.js";
import { buildCockpitSnapshot, buildRailModel, computeDeepSeekRunUsage, computeDeepSeekRequests, } from "./telemetry.js";
import { getTerminalWidth, normalizeCockpitFrameHeight, DEFAULT_COCKPIT_HEIGHT, } from "./update-loop.js";
import { renderWorkingHud, renderSweepRule, } from "../../ui/omk-working-sweep.js";
import { sliceFromBottom } from "./scroll.js";
import { renderOmkSparkleText } from "../../ui/omk-sigil.js";
function renderCockpitPanel(lines, innerWidth) {
    const safeInnerWidth = Math.max(1, innerWidth);
    const horizontal = "━".repeat(safeInnerWidth + 2);
    const top = style.darkPurple(`┏${horizontal}┓`);
    const bottom = style.darkPurple(`┗${horizontal}┛`);
    const body = lines.map((line) => style.darkPurple("┃ ") + padEndVisible(truncateLine(line, safeInnerWidth), safeInnerWidth) + style.darkPurple(" ┃"));
    return [top, ...body, bottom].join("\n");
}
function updateRendererScrollState(args) {
    const layout = computeCockpitLayout({
        cols: args.width,
        rows: args.frameHeight,
        rightRailPinned: false,
        composerHeight: Math.max(2, args.composerRows || 2),
        workingHeight: args.workingRows,
        composerLiftRows: 0,
    });
    const viewportHeight = Math.max(1, args.viewportHeight);
    const leftWidth = Math.max(1, args.width - PANEL_HORIZONTAL_OVERHEAD);
    const previousLineCount = args.renderer.lastLeftLineCount;
    const contentY = 2;
    const transcriptY = contentY + args.stickyTopRows;
    const workingY = transcriptY + viewportHeight;
    const composerY = workingY + args.workingRows;
    args.renderer.currentLayout = {
        ...layout,
        leftPane: {
            x: 1,
            y: contentY,
            w: leftWidth,
            h: Math.max(1, args.bodyHeight),
        },
        transcript: {
            x: 1,
            y: transcriptY,
            w: leftWidth,
            h: viewportHeight,
        },
        working: {
            x: 1,
            y: workingY,
            w: leftWidth,
            h: args.workingRows,
        },
        composer: {
            x: 1,
            y: composerY,
            w: leftWidth,
            h: args.composerRows,
        },
        rightRail: null,
        footer: {
            x: 1,
            y: Math.max(1, args.frameHeight - 1),
            w: args.width,
            h: 1,
        },
    };
    args.renderer.leftTranscriptLineCount = args.lineCount;
    args.renderer.leftTranscriptHeight = viewportHeight;
    args.renderer.lastLeftLineCount = args.lineCount;
    args.renderer.lastTranscriptHeight = viewportHeight;
    if (!args.renderer.followTail && args.lineCount > previousLineCount) {
        args.renderer.leftScrollFromBottom += args.lineCount - previousLineCount;
    }
    args.renderer.onLeftContentChanged();
}
function renderStickyComposer(text, width) {
    if (text === undefined)
        return [];
    const safe = sanitizeTerminalText(text).replace(/\s+/g, " ").trim();
    const body = safe.length > 0 ? truncateText(safe, Math.max(1, width - 14)) : "ready";
    return [`${style.gray("composer")} ${style.cream("›")} ${style.cream(body)}`];
}
function todoMarker(statusValue) {
    const normalized = statusValue.toLowerCase();
    switch (normalized) {
        case "running":
        case "in_progress": return style.purpleBold("▶");
        case "done":
        case "completed": return style.mintBold("✓");
        case "failed": return style.red("✕");
        case "blocked": return style.orange("■");
        case "skipped": return style.gray("⊘");
        default: return style.gray("□");
    }
}
function gitMarker(changeStatus) {
    const normalized = changeStatus.replace(/\s/g, "");
    if (normalized === "??")
        return style.blue("?");
    if (normalized.includes("D"))
        return style.red("D");
    if (normalized.includes("A"))
        return style.mint("A");
    if (normalized.includes("R"))
        return style.purple("R");
    return style.orange("M");
}
function healthColor(health) {
    switch (health) {
        case "ok": return style.mint;
        case "warn": return style.orange;
        case "blocked": return style.orange;
        case "failed": return style.red;
        default: return style.gray;
    }
}
function workerStateColor(state) {
    switch (state) {
        case "running": return style.purpleBold;
        case "done": return style.mint;
        case "failed": return style.red;
        case "blocked": return style.orange;
        case "skipped": return style.gray;
        default: return style.gray;
    }
}
function miniProgressBar(done, total, width = 8) {
    if (total <= 0 || !Number.isFinite(total))
        return style.gray("░".repeat(width));
    const safeDone = Number.isNaN(done) ? 0 : Math.min(Math.max(0, done), total);
    const filled = Math.round((safeDone / total) * width);
    const empty = Math.max(0, width - filled);
    return style.mint("█".repeat(filled)) + style.gray("░".repeat(empty));
}
function looksLikeSecret(value) {
    if (/\b[a-f0-9]{16,}\b/i.test(value))
        return true;
    if (/(?:https?:\/\/|@)[^/\s:]+:[^/\s@]+@/.test(value))
        return true;
    if (/\bghp_[A-Za-z0-9]{36}\b/.test(value))
        return true;
    if (/\brw_[A-Za-z0-9]+\b/.test(value))
        return true;
    return false;
}
function redactResourceName(name) {
    return looksLikeSecret(name) ? "***REDACTED***" : name;
}
function formatResourceSummary(resources, maxWidth) {
    if (!resources)
        return `${style.gray("MCP")} ${style.gray("?:? connected · ? tools")}`;
    const connected = resources.mcpServers.filter((r) => r.status === "connected" || r.status == null).length;
    const connecting = resources.mcpServers.filter((r) => r.status === "connecting");
    const failed = resources.mcpServers.filter((r) => r.status === "failed");
    const toolsCount = resources.mcpServers.reduce((sum, r) => sum + (r.toolsCount ?? 0), 0);
    const statusBits = [
        `${style.mintBold(`${connected}/${resources.mcpServers.length}`)} connected`,
        `${style.mintBold(String(toolsCount))} tools`,
        connecting.length > 0 ? `connecting: ${sampleNamesPlain(connecting.map((r) => ({ ...r, name: redactResourceName(r.name) })), 3)}` : "",
        failed.length > 0 ? `failed: ${sampleNamesPlain(failed.map((r) => ({ ...r, name: redactResourceName(r.name) })), 3)}` : "",
    ].filter(Boolean);
    const sample = sampleNames([
        ...resources.mcpServers.slice(0, 2).map((r) => ({ ...r, name: redactResourceName(r.name) })),
        ...resources.skills.slice(0, 2).map((r) => ({ ...r, name: redactResourceName(r.name) })),
        ...resources.hooks.slice(0, 2).map((r) => ({ ...r, name: redactResourceName(r.name) })),
    ], 3);
    const base = `${style.gray("MCP")} ${statusBits.join(" · ")} ` +
        `mcp:${style.mintBold(String(resources.mcpServers.length))} ` +
        `skills:${style.mintBold(String(resources.skills.length))} ` +
        `hooks:${style.mintBold(String(resources.hooks.length))} ` +
        `${style.gray(`scope:${resources.scope}`)}`;
    const result = sample ? `${base} ${style.gray(truncateText(sample, maxWidth - 32))}` : base;
    return visibleTerminalWidth(result) > maxWidth ? truncateLine(result, maxWidth) : result;
}
function formatRuntimeContract(contract, maxWidth) {
    if (!contract)
        return `${style.gray("contract")} ${style.gray("--")}`;
    const base = `${style.gray("contract")} ` +
        `mcp:${style.mintBold(String(contract.mcpCount))} ` +
        `skills:${style.mintBold(String(contract.skillCount))} ` +
        `hooks:${style.mintBold(String(contract.hookCount))} ` +
        `${style.gray(`scope:${contract.scope}`)}`;
    const workerPart = contract.workerCap != null ? ` workers:${style.mintBold(String(contract.workerCap))}` : "";
    const stepsPart = contract.maxStepsPerTurn != null ? ` steps:${style.mintBold(String(contract.maxStepsPerTurn))}` : "";
    const gatesPart = contract.gateCount > 0 ? ` gates:${style.mintBold(String(contract.gateCount))}` : "";
    const result = base + workerPart + stepsPart + gatesPart;
    if (visibleTerminalWidth(result) > maxWidth) {
        return truncateLine(result, maxWidth);
    }
    return result;
}
function formatDeepSeekSummary(deepSeek, usage, requests, maxWidth) {
    if (!deepSeek)
        return `${style.gray("deepseek")} ${style.gray("checking")} use:${usage.attempts} fb:${usage.fallbackCount}`;
    const state = deepSeek.available
        ? style.mintBold("ok")
        : deepSeek.enabled && deepSeek.apiKeySet
            ? style.orange("warn")
            : style.gray("off");
    const balance = deepSeek ? formatDeepSeekBalance(deepSeek) : "n/a";
    const modelPart = formatDeepSeekModelUsage(usage);
    const reason = deepSeek.reason ? ` ${style.gray(truncateText(sanitizeForDisplay(deepSeek.reason), Math.max(8, maxWidth - 56)))}` : "";
    const running = requests.filter((r) => r.status === "running").length;
    const completed = requests.filter((r) => r.status === "completed").length;
    const failed = requests.filter((r) => r.status === "failed").length;
    const liveParts = [];
    if (running > 0)
        liveParts.push(`▶${running} running`);
    if (completed > 0)
        liveParts.push(`${completed} completed`);
    if (failed > 0)
        liveParts.push(`${failed} failed`);
    const livePart = liveParts.length > 0 ? ` ${liveParts.join(" · ")}` : "";
    return `${style.gray("DeepSeek")} ${state}${livePart} bal:${balance} use:${usage.attempts}${modelPart} ` +
        `d:${usage.directCount} a:${usage.advisoryCount} f:${usage.fallbackCount}${reason}`;
}
function sampleNames(entries, limit) {
    const names = [...new Set(entries.map((entry) => entry.name))].slice(0, limit);
    return names.length > 0 ? `[${names.join(",")}]` : "";
}
function sampleNamesPlain(entries, limit) {
    return [...new Set(entries.map((entry) => entry.name))].slice(0, limit).join(",");
}
function formatDeepSeekModelUsage(usage) {
    const tiers = Object.entries(usage.byTier)
        .sort(([a], [b]) => a.localeCompare(b))
        .map(([tier, count]) => `${tier}:${count}`);
    if (tiers.length > 0)
        return ` ${style.gray(tiers.join(","))}`;
    const models = Object.entries(usage.byModel)
        .sort(([a], [b]) => a.localeCompare(b))
        .slice(0, 2)
        .map(([model, count]) => `${model.replace(/^deepseek-v4-/, "")}:${count}`);
    return models.length > 0 ? ` ${style.gray(models.join(","))}` : "";
}
function buildFooter(targetWidth, heightMode) {
    if (targetWidth < 40) {
        return `[${style.gray("q")}]uit`;
    }
    if (targetWidth < 60) {
        return `[${style.gray("q")}]uit [${style.gray("sp")}]ause ${style.gray(`h:${heightMode}`)}`;
    }
    return `[${style.gray("h")}]istory [${style.gray("+")}/${style.gray("-")}]height [${style.gray("a")}]auto [${style.gray("space")}]pause [${style.gray("q")}]uit ${style.gray(`height:${heightMode}`)}`;
}
export async function renderCockpit(options = {}) {
    const width = getTerminalWidth(options.terminalWidth);
    const targetWidth = Math.max(1, width - PANEL_HORIZONTAL_OVERHEAD);
    const fixedFrameHeight = normalizeCockpitFrameHeight(options.height);
    const fixedBodyHeight = fixedFrameHeight != null ? Math.max(0, fixedFrameHeight - 2) : undefined;
    const root = await getProjectRootAsync();
    const now = Date.now();
    const cache = options.cache;
    const quick = options.quick ?? false;
    const showHistory = options.showHistory ?? true;
    const section = options.section ?? "all";
    const useEvents = options.events !== "off";
    const deepSeekPromise = options.deepSeekProvider
        ? options.deepSeekProvider()
        : quick
            ? Promise.resolve(cache?.deepSeek?.value ?? null)
            : (async () => {
                const cached = getCacheEntry(cache?.deepSeek, 60_000, now);
                if (cached !== undefined)
                    return cached;
                const value = await getCockpitDeepSeekSnapshot().catch(() => null);
                if (cache)
                    cache.deepSeek = { value, ts: now };
                return value;
            })();
    // ── Fetch git changes (cached) ──
    const gitChangesPromise = quick
        ? Promise.resolve(cache?.gitChanges?.value ?? [])
        : (async () => {
            const cached = getCacheEntry(cache?.gitChanges, 5000, now);
            if (cached !== undefined)
                return cached;
            const result = await getGitChanges(root);
            const value = result ?? [];
            if (cache)
                cache.gitChanges = { value, ts: now };
            return value;
        })();
    // ── Resolve latest run + state (always fast, uncached) ──
    const runsDir = getRunsDir();
    let latestRunName = options.runId ?? null;
    let stateContent = null;
    let sessionMeta = null;
    if (await pathExists(runsDir)) {
        if (!latestRunName) {
            const entries = await readdir(runsDir, { withFileTypes: true });
            const dirs = entries.filter((e) => e.isDirectory());
            const stats = await Promise.all(dirs.map(async (d) => {
                const statePath = getRunPath(d.name, "state.json");
                const [hasState, s] = await Promise.all([
                    pathExists(statePath),
                    fsStat(statePath).catch(() => null),
                ]);
                if (!hasState)
                    return null;
                return s ? { name: d.name, mtime: s.mtimeMs } : null;
            }));
            const best = stats
                .filter((s) => s !== null)
                .sort((a, b) => b.mtime - a.mtime)[0];
            latestRunName = best?.name ?? null;
        }
        if (latestRunName) {
            const [sc, sm] = await Promise.all([
                withTimeout(readFile(getRunPath(latestRunName, "state.json"), "utf-8"), 10000).catch(() => null),
                withTimeout(readSessionMeta(latestRunName), 10000).catch(() => null),
            ]);
            stateContent = sc;
            sessionMeta = sm;
        }
    }
    const eventsPromise = (async () => {
        if (!latestRunName || !useEvents)
            return [];
        const cached = getCacheEntry(cache?.events, 750, now);
        if (cached !== undefined)
            return cached;
        const value = await readEvents(getRunPath(latestRunName)).catch(() => []);
        if (cache)
            cache.events = { value, ts: now };
        return value;
    })();
    const resourcesPromise = options.resourceProvider
        ? options.resourceProvider()
        : quick
            ? Promise.resolve(cache?.resources?.value ?? null)
            : (async () => {
                const cached = getCacheEntry(cache?.resources, 30_000, now);
                if (cached !== undefined)
                    return cached;
                const value = await getCockpitResources(root, latestRunName).catch(() => null);
                if (cache)
                    cache.resources = { value, ts: now };
                return value;
            })();
    const gitChanges = (await gitChangesPromise) ?? [];
    const events = await eventsPromise;
    let vm = buildRunViewModel(null);
    let stateError = "missing";
    let parsedState = null;
    if (stateContent) {
        const result = parseRunStateResult(stateContent);
        stateError = result.error;
        parsedState = result.state;
        vm = buildRunViewModel(parsedState, { changedFiles: gitChanges.map((c) => c.path), telemetry: events });
    }
    // ── Fetch todos (cached) ──
    const todosPromise = (async () => {
        if (!latestRunName)
            return null;
        const cached = getCacheEntry(cache?.stateTodos, 750, now);
        if (cached !== undefined)
            return cached;
        const value = await loadTodos(latestRunName).catch(() => null);
        if (cache)
            cache.stateTodos = { value, ts: now };
        return value;
    })();
    // ── Fetch Kimi usage (cached) ──
    const primaryPromise = quick
        ? Promise.resolve(cache?.primaryUsage?.value ?? null)
        : (async () => {
            const cached = getCacheEntry(cache?.primaryUsage, 60000, now);
            if (cached !== undefined)
                return cached;
            try {
                const value = await getKimiUsage();
                if (cache)
                    cache.primaryUsage = { value, ts: now };
                return value;
            }
            catch {
                return null;
            }
        })();
    const [todos, primaryUsage, resources, deepSeek] = await Promise.all([
        todosPromise,
        primaryPromise,
        resourcesPromise,
        deepSeekPromise,
    ]);
    const deepSeekUsage = computeDeepSeekRunUsage(parsedState);
    const deepSeekRequests = computeDeepSeekRequests(events);
    // ── System usage (cached) ──
    let sysUsage = null;
    if (!quick) {
        const cached = getCacheEntry(cache?.systemUsage, 3000, now);
        if (cached !== undefined) {
            sysUsage = cached;
        }
        else {
            try {
                sysUsage = getSystemUsage();
                if (cache)
                    cache.systemUsage = { value: sysUsage, ts: now };
            }
            catch { /* ignore */ }
        }
    }
    else {
        sysUsage = cache?.systemUsage?.value ?? null;
    }
    // ── Normalize into snapshot ──
    const snapshot = await buildCockpitSnapshot(vm, todos, primaryUsage, resources, deepSeek, deepSeekUsage, deepSeekRequests, sysUsage, gitChanges, sessionMeta, stateError, latestRunName);
    // ── View branch ──
    const view = options.view ?? "panel";
    if (view === "rail") {
        const [numstat, branch, lspEntries] = await Promise.all([
            getGitNumstat(root),
            getGitBranch(root),
            getLspStatus(root),
        ]);
        const railModel = buildRailModel(snapshot, numstat, lspEntries, branch, root, primaryUsage);
        const railWidth = Math.max(28, Math.min(options.terminalWidth ?? 36, 56));
        return renderRailView(railModel, { width: railWidth, height: options.height });
    }
    if (view === "json") {
        return JSON.stringify(snapshot, null, 2);
    }
    // ── Build sections as separate arrays ──
    const animFrame = options.animFrame ?? 0;
    // Header (Green Rain signal + OMK control title)
    const rainWidth = Math.min(targetWidth, 60);
    const rain = renderMatrixRain(latestRunName ?? "omk", rainWidth, 3);
    const rainLines = process.stdout.isTTY
        ? rain.split("\n").map((l) => style.phosphor(l))
        : [];
    const headerLines = [
        "",
        ...rainLines,
        renderOmkSparkleText("◢█ OMK//CONTROL COCKPIT █◣", {
            frame: animFrame,
            colors: ["#00D6FF", "#f4ffff", "#ffd166", "#FF47B2", "#00FFC2"],
        }),
        style.gray("NEON GRID · GREEN RAIN · METRICS WALL"),
        style.gray("route · verify · loop · control · evidence gated"),
        "",
    ];
    // ── Sweep animation line ──
    let sweepLine = "";
    if (animFrame > 0 || process.env.OMK_ANIM !== "0") {
        const activeNode = vm.activeNode;
        const sweepState = activeNode
            ? {
                kind: vm.progress.running > 0 ? "loop" : "idle",
                label: activeNode.role || "working",
                detail: activeNode.thinking || activeNode.name || "running",
                startedAtMs: Date.now(),
            }
            : {
                kind: "idle",
                label: "idle",
                detail: "waiting for instruction",
                startedAtMs: Date.now(),
            };
        const sweepHud = renderWorkingHud({
            state: sweepState,
            frame: animFrame,
            width: Math.max(24, targetWidth),
            compact: false,
        });
        sweepLine = sweepHud;
    }
    // Info section — compact priority-ordered lines
    const infoLines = [];
    const displayRunId = latestRunName ?? "--";
    const healthStr = vm.health.toUpperCase();
    const progressStr = `${vm.progress.settled}/${vm.progress.total}`;
    infoLines.push(`${style.gray("run")} ${style.creamBold(truncateText(displayRunId, targetWidth - 6))}`);
    if (primaryUsage) {
        const account = primaryUsage.oauth.loggedIn ? primaryUsage.oauth.displayId : "/login";
        const fiveHourPercent = primaryUsage.quota.fiveHour?.remainingPercent != null
            ? Math.min(100, Math.max(0, 100 - primaryUsage.quota.fiveHour.remainingPercent))
            : null;
        const weeklyPercent = primaryUsage.quota.weekly?.remainingPercent != null
            ? Math.min(100, Math.max(0, 100 - primaryUsage.quota.weekly.remainingPercent))
            : null;
        const fiveHour = fiveHourPercent != null
            ? `${fiveHourPercent}%`
            : `${Math.round(primaryUsage.totalSecondsLast5Hours / 60)}m`;
        const weekly = weeklyPercent != null
            ? `${weeklyPercent}%`
            : `${Math.round(primaryUsage.totalSecondsWeek / 60)}m`;
        const sysPart = sysUsage
            ? `${style.gray("sys")} ${style.gray("cpu")}${style.mintBold(`${sysUsage.cpuPercent}%`)} ${style.gray("mem")}${style.mintBold(`${sysUsage.memPercent}%`)}`
            : "";
        infoLines.push(`${style.gray("primary")} ${truncateText(account, 16)} 5h:${fiveHour} wk:${weekly}  ${sysPart}`.trimEnd());
    }
    else {
        const sysPart = sysUsage
            ? `${style.gray("sys")} ${style.gray("cpu")}${style.mintBold(`${sysUsage.cpuPercent}%`)} ${style.gray("mem")}${style.mintBold(`${sysUsage.memPercent}%`)}`
            : `${style.gray("sys")} ${style.gray("--")}`;
        infoLines.push(`${style.gray("primary")} ${style.gray("unavailable")}  ${sysPart}`.trimEnd());
    }
    infoLines.push(formatDeepSeekSummary(deepSeek, deepSeekUsage, snapshot.deepSeekRequests, targetWidth));
    const mcpLines = [formatResourceSummary(resources, targetWidth)];
    if (snapshot.runtimeContract) {
        mcpLines.push(formatRuntimeContract(snapshot.runtimeContract, targetWidth));
    }
    if (snapshot.evidence.failedGates > 0 || snapshot.evidence.skippedGates > 0) {
        const gateParts = [];
        if (snapshot.evidence.failedGates > 0)
            gateParts.push(`${style.red(String(snapshot.evidence.failedGates))} failed`);
        if (snapshot.evidence.skippedGates > 0)
            gateParts.push(`${style.orange(String(snapshot.evidence.skippedGates))} skipped`);
        const evidenceSample = (vm.workers ?? [])
            .find((node) => (node.state === "failed" || node.state === "blocked") && node.lastEvidence)
            ?.lastEvidence;
        const evidenceDetail = evidenceSample
            ? ` · ${truncateText(sanitizeForDisplay(evidenceSample.message || evidenceSample.gate), targetWidth - 24)}`
            : "";
        mcpLines.push(`${style.gray("evidence")} ${gateParts.join(" · ")}${evidenceDetail}`);
    }
    const goalLineParts = [];
    if (sessionMeta?.type === "chat") {
        goalLineParts.push(`${style.gray("type")} ${style.purpleBold("💬 Chat")}`);
    }
    if (vm.goalTitle) {
        const scoreBadge = vm.goalScore != null ? style.creamBold(` ${vm.goalScore}%`) : "";
        goalLineParts.push(`${style.gray("goal")} ${truncateText(vm.goalTitle, targetWidth - 10 - sanitizeTerminalText(scoreBadge).length)}${scoreBadge}`);
    }
    if (vm.startedAt) {
        const startedMs = Date.parse(vm.startedAt);
        if (!Number.isNaN(startedMs)) {
            const duration = formatElapsed(Date.now() - startedMs);
            goalLineParts.push(`${style.gray("dur")} ${style.gray(duration)}`);
        }
    }
    if (goalLineParts.length > 0) {
        infoLines.push(goalLineParts.join("  "));
    }
    const statusParts = [];
    statusParts.push(`${style.gray("health")} ${healthColor(vm.health)(healthStr)}`);
    statusParts.push(`${style.gray("progress")} ${style.mintBold(progressStr)} ${style.gray(`settled, ${vm.progress.running} active`)}`);
    if (vm.activeNode) {
        const activeName = truncateText(vm.activeNode.name, targetWidth - 20);
        statusParts.push(`${style.gray("active")} ${style.purpleBold("▶")} ${activeName}`);
    }
    if (statusParts.length > 0) {
        infoLines.push(statusParts.join("  "));
    }
    const extraParts = [];
    if (vm.nextAction && vm.nextAction !== "Run complete" && vm.nextAction !== "Ready") {
        extraParts.push(`${style.gray("next")} ${style.cream(truncateText(vm.nextAction, targetWidth - 8))}`);
    }
    if (vm.eta) {
        const confBadge = vm.etaConfidence ? style.gray(` (${vm.etaConfidence})`) : "";
        extraParts.push(`${style.gray("ETA")} ${style.cream(vm.eta)}${confBadge}`);
    }
    if (vm.blocker) {
        const blockerText = truncateText(`${vm.blocker.reason} (${vm.blocker.nodeId})`, targetWidth - 12);
        extraParts.push(`${style.gray("blocker")} ${style.red("■")} ${blockerText}`);
    }
    if (extraParts.length > 0) {
        infoLines.push(extraParts.join("  "));
    }
    const selectedRuntime = snapshot.pulse.selectedRuntime;
    if (selectedRuntime) {
        infoLines.push(`${style.gray("runtime")} ${style.creamBold(String(selectedRuntime))}`);
    }
    // ── Worker / TODO section ──
    const workerLines = [];
    const sortedNodes = [...(vm.workers ?? [])].sort((a, b) => {
        const rank = statusRank(a.state) - statusRank(b.state);
        return rank !== 0 ? rank : a.id.localeCompare(b.id);
    });
    const sortedTodos = todos
        ? [...todos].sort((a, b) => statusRank(a.status) - statusRank(b.status))
        : null;
    const chatPrefix = sessionMeta?.type === "chat" ? "💬 " : "";
    if (sortedTodos) {
        const doneCount = sortedTodos.filter((t) => t.status === "done").length;
        const totalCount = sortedTodos.length;
        const pct = totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0;
        workerLines.push(`${sectionHeader(`${chatPrefix}TODO`)} ${miniProgressBar(doneCount, totalCount)} ${style.creamBold(`${pct}%`)} ` +
            `${style.gray(`(${doneCount}/${totalCount})`)}`);
        if (sortedTodos.length === 0) {
            workerLines.push(`  ${style.gray("TODO 없음")}`);
        }
        else {
            for (const todo of sortedTodos.slice(0, 6)) {
                const marker = todoMarker(todo.status);
                const elapsed = todo.elapsedMs ? style.gray(formatElapsed(todo.elapsedMs)) : "";
                const agentBadge = todo.agent ? style.gray(`[${truncateText(sanitizeForDisplay(todo.agent), 8)}]`) : "";
                const idleBadge = todo.agent === "chat" && todo.status === "in_progress"
                    ? style.gray("idle / waiting for input")
                    : "";
                const base = `${marker} ${agentBadge} ${truncateText(sanitizeForDisplay(todo.title), targetWidth - 14)} ${idleBadge} ${elapsed}`.replace(/\s+/g, " ").trim();
                workerLines.push(`  ${base}`);
            }
            if (sortedTodos.length > 6) {
                workerLines.push(`  ${style.gray(`… ${sortedTodos.length} total`)}`);
            }
        }
    }
    else {
        workerLines.push(`${sectionHeader(`${chatPrefix}TODO`)} ${style.gray("not recorded")}`);
    }
    if (todos && todos.length > 0) {
        workerLines.push("");
    }
    if (sortedNodes.length > 0) {
        const { done, total, running, failed, blocked, skipped, settled } = vm.progress;
        const pct = total > 0 ? Math.round((settled / total) * 100) : 0;
        workerLines.push(`${sectionHeader("AGENTS")} ${miniProgressBar(settled, total)} ${style.creamBold(`${pct}%`)} ` +
            `${style.gray(`(${running}▶ ${done}✓ ${failed}✕ ${blocked}■ ${skipped}⊘ / ${total})`)}`);
        for (const node of sortedNodes) {
            const stateLabel = workerStateColor(node.state)(node.state.toUpperCase());
            const liveBadge = node.liveStatus && node.liveStatus !== node.state
                ? style.gray(` ${node.liveStatus}`)
                : "";
            const elapsed = formatElapsed(node.elapsedMs);
            const nodeRuntime = node.selectedRuntime;
            const runtimeBadge = nodeRuntime ? style.gray(`[${truncateText(nodeRuntime, 12)}]`) : "";
            const base = `${stateLabel}${liveBadge} ${style.gray(elapsed)} ${truncateText(sanitizeForDisplay(node.label || node.id), targetWidth - 22)}${runtimeBadge ? " " + runtimeBadge : ""}`;
            if (node.state === "running") {
                const isChatInputWait = node.liveStatus === "waiting-input" || (node.id === "chat" && !node.phase && !node.thinking);
                workerLines.push(`  ${base}`);
                if (isChatInputWait) {
                    workerLines.push(`    ${style.gray("→")} ${style.gray("idle / waiting for input")}`);
                }
                else if (node.phase) {
                    const cleanPhase = sanitizeTerminalText(sanitizeForDisplay(node.phase));
                    workerLines.push(`    ${style.gray("→")} ${truncateText(cleanPhase, targetWidth - 8)}`);
                }
                else if (node.currentNode) {
                    const cleanNode = sanitizeTerminalText(sanitizeForDisplay(node.currentNode));
                    workerLines.push(`    ${style.gray("→")} ${truncateText(cleanNode, targetWidth - 8)}`);
                }
                if (!isChatInputWait && node.liveStatus === "stalled") {
                    const staleText = `stalled ${formatElapsed(node.lastHeartbeatAgeMs ?? node.lastActivityAgeMs ?? 0)}`;
                    workerLines.push(`    ${style.orange("⚠")} ${style.orange(staleText)}`);
                }
                else if (!isChatInputWait && node.lastActivityAgeMs != null && node.lastActivityAgeMs > 30_000) {
                    const staleText = `silent ${formatElapsed(node.lastActivityAgeMs)}`;
                    workerLines.push(`    ${style.orange("⚠")} ${style.orange(staleText)}`);
                }
            }
            else if ((node.state === "failed" || node.state === "blocked") && node.lastEvidence) {
                const evidence = truncateText(sanitizeForDisplay(node.lastEvidence.message || node.lastEvidence.gate), targetWidth - 10);
                const retryBadge = node.retryCount > 0 ? style.orange(` [retry ${node.retryCount}]`) : "";
                workerLines.push(`  ${base}${retryBadge}`);
                workerLines.push(`    ${style.red("→")} ${evidence}`);
            }
            else {
                workerLines.push(`  ${base}`);
            }
        }
        if (sortedNodes.length > 0) {
            workerLines.push(`  ${style.gray(`\u2026 ${sortedNodes.length} total`)}`);
        }
    }
    if (sortedNodes.length === 0) {
        workerLines.push("", `${sectionHeader("AGENTS")} ${style.gray("No parallel agents active — waiting for work")}`);
    }
    // ── Changed / History / State section ──
    const changedLines = [];
    if (stateError !== "ok") {
        changedLines.push(`${style.orangeBold("⚠ state")} ${style.orange(stateError)}`);
        const isChatRun = (latestRunName ?? "").startsWith("chat-");
        const recovery = stateError === "missing"
            ? isChatRun
                ? `omk chat --run-id ${latestRunName ?? "<id>"}`
                : `omk run --run-id ${latestRunName ?? "<id>"}`
            : stateError === "corrupt"
                ? `omk run --run-id ${latestRunName ?? "<run-id>"}`
                : `omk verify --run ${latestRunName ?? "<run-id>"}`;
        changedLines.push(`${style.gray("hint:")} ${style.cream(sanitizeForDisplay(recovery))}`);
    }
    if (gitChanges.length > 0) {
        const counts = snapshot.worktree.counts;
        const countParts = [
            counts.M > 0 ? `M:${counts.M}` : "",
            counts.A > 0 ? `A:${counts.A}` : "",
            counts.D > 0 ? `D:${counts.D}` : "",
            counts["?"] > 0 ? `?:${counts["?"]}` : "",
            counts.R > 0 ? `R:${counts.R}` : "",
        ].filter(Boolean);
        const countStr = countParts.join(" ");
        changedLines.push(sectionHeader(`Changed (${gitChanges.length})`) + (countStr ? ` ${style.gray(countStr)}` : ""));
        const maxPaths = targetWidth >= 80 && fixedBodyHeight != null && fixedBodyHeight > 24 ? 8 : 5;
        for (const path of snapshot.worktree.topPaths.slice(0, maxPaths)) {
            const change = gitChanges.find((c) => c.path === path);
            if (change) {
                changedLines.push(`  ${gitMarker(change.status)} ${truncateText(sanitizeForDisplay(change.path), targetWidth - 6)}`);
            }
        }
        if (gitChanges.length > maxPaths) {
            changedLines.push(`  ${style.gray(`\u2026 ${gitChanges.length} total`)}`);
        }
    }
    else if (stateError === "ok") {
        changedLines.push(style.mint("✓ clean worktree"));
    }
    if (!quick && showHistory && stateError === "ok") {
        const cached = getCacheEntry(cache?.history, 10000, now);
        let historyLines = [];
        if (cached !== undefined) {
            historyLines = cached;
        }
        else {
            try {
                if (await pathExists(runsDir)) {
                    const candidates = await listRunCandidates(runsDir);
                    const sorted = candidates
                        .filter((c) => c.name !== latestRunName)
                        .sort((a, b) => b.stateUpdatedAtMs - a.stateUpdatedAtMs)
                        .slice(0, 5);
                    if (sorted.length > 0) {
                        historyLines = await Promise.all(sorted.map(async (c) => {
                            let st = style.gray("?");
                            try {
                                const raw = await withTimeout(readFile(getRunPath(c.name, "state.json"), "utf-8"), 10000);
                                const parsed = parseRunStateResult(raw);
                                if (parsed.state) {
                                    const stateVm = buildRunViewModel(parsed.state);
                                    if (stateVm.health === "ok" && stateVm.progress.settled === stateVm.progress.total && stateVm.progress.total > 0) {
                                        st = style.mint("✓");
                                    }
                                    else if (stateVm.health === "failed") {
                                        st = style.red("✕");
                                    }
                                    else if (stateVm.health === "blocked") {
                                        st = style.orange("■");
                                    }
                                    else if (stateVm.progress.running > 0) {
                                        st = style.purple("▶");
                                    }
                                    else {
                                        st = style.gray("?");
                                    }
                                }
                            }
                            catch { /* ignore */ }
                            let goalTitle = "";
                            try {
                                const goalRaw = await withTimeout(readFile(getRunPath(c.name, "goal.md"), "utf-8"), 10000);
                                const firstLine = goalRaw.split(/\r?\n/)[0]?.trim() ?? "";
                                goalTitle = firstLine.replace(/^#+\s*/, "").slice(0, 24);
                            }
                            catch { /* ignore */ }
                            const date = new Date(c.stateUpdatedAtMs);
                            const dateStr = `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
                            const name = truncateText(c.name, targetWidth - 20);
                            const titlePart = goalTitle ? style.gray(`  → ${goalTitle}`) : "";
                            return `  ${st} ${name} ${style.gray(dateStr)}${titlePart}`;
                        }));
                    }
                }
            }
            catch { /* ignore */ }
            if (cache)
                cache.history = { value: historyLines, ts: now };
        }
        if (historyLines.length > 0) {
            changedLines.push(sectionHeader("History"));
            changedLines.push(...historyLines);
        }
    }
    // ── Assemble responsive rectangle ──
    const heightMode = options.height == null || fixedFrameHeight == null ? "auto" : `${fixedFrameHeight}`;
    const footerLine = buildFooter(targetWidth, heightMode);
    const agentHeaderIndex = workerLines.findIndex((line) => sanitizeTerminalText(line).includes("AGENTS"));
    const selectedWorkerLines = section === "todos"
        ? (agentHeaderIndex >= 0 ? workerLines.slice(0, agentHeaderIndex).filter((line) => line.trim() !== "") : workerLines)
        : section === "agents"
            ? (agentHeaderIndex >= 0 ? workerLines.slice(agentHeaderIndex) : workerLines)
            : section === "mcp"
                ? []
                : workerLines;
    const selectedInfoLines = section === "all" ? infoLines : [];
    const selectedMcpLines = section === "all" || section === "mcp"
        ? (resources ? mcpLines : mcpLines.slice(1))
        : [];
    const selectedChangedLines = section === "all" ? changedLines : [];
    const activePanels = [];
    if (selectedInfoLines.length > 0)
        activePanels.push({ title: "Run", lines: selectedInfoLines, key: "info" });
    if (selectedWorkerLines.length > 0)
        activePanels.push({ title: "Workers & TODO", lines: selectedWorkerLines, key: "worker" });
    if (selectedMcpLines.length > 0)
        activePanels.push({ title: "Resources", lines: selectedMcpLines, key: "mcp" });
    if (selectedChangedLines.length > 0)
        activePanels.push({ title: "Changes & History", lines: selectedChangedLines, key: "changed" });
    const stickyHeaderLines = [...headerLines];
    const stickyWorkingLines = sweepLine ? [sweepLine, renderSweepRule(targetWidth, animFrame + 7)] : [];
    const stickyComposerLines = renderStickyComposer(options.composerText, targetWidth);
    const transcriptLines = activePanels.flatMap((p) => layoutPanel(p.title, p.lines, targetWidth).split("\n"));
    const scrollableLeftLines = transcriptLines;
    const fullLeftLines = [
        ...stickyHeaderLines,
        ...scrollableLeftLines,
        ...stickyWorkingLines,
        ...stickyComposerLines,
        footerLine,
    ];
    const renderer = options.renderer;
    let body;
    if (renderer) {
        const bodyHeight = fixedBodyHeight ?? DEFAULT_COCKPIT_HEIGHT - 2;
        const frameHeight = fixedFrameHeight ?? bodyHeight + 2;
        const stickyBottomLines = [
            ...stickyWorkingLines,
            ...stickyComposerLines,
            footerLine,
        ];
        const viewportHeight = Math.max(1, bodyHeight - stickyHeaderLines.length - stickyBottomLines.length);
        updateRendererScrollState({
            renderer,
            width,
            frameHeight,
            bodyHeight,
            viewportHeight,
            lineCount: transcriptLines.length,
            stickyTopRows: stickyHeaderLines.length,
            workingRows: stickyWorkingLines.length,
            composerRows: stickyComposerLines.length,
        });
        body = [
            ...stickyHeaderLines,
            ...sliceFromBottom({
                lines: transcriptLines,
                viewportHeight,
                scrollFromBottom: renderer.leftScrollFromBottom,
            }),
            ...stickyBottomLines,
        ];
        while (body.length < bodyHeight)
            body.splice(stickyHeaderLines.length, 0, "");
        if (body.length > bodyHeight)
            body.length = bodyHeight;
    }
    else if (fixedBodyHeight != null) {
        const headerRows = headerLines.length + (sweepLine ? 2 : 0);
        const footerRows = 1;
        const panelOverheadRows = activePanels.length * 2;
        const available = Math.max(0, fixedBodyHeight - headerRows - footerRows - panelOverheadRows);
        // Priority-based budget: critical info > active agents/TODO > MCP compact > changed/history
        const infoMin = Math.min(selectedInfoLines.length, 3);
        const workerMin = Math.min(selectedWorkerLines.length, section === "all" ? 5 : 8);
        const mcpMin = Math.min(selectedMcpLines.length, 1);
        const changedMin = Math.min(selectedChangedLines.length, 1);
        let infoBudget = infoMin;
        let workerBudget = workerMin;
        let mcpBudget = mcpMin;
        let changedBudget = changedMin;
        let remaining = available - (infoBudget + workerBudget + mcpBudget + changedBudget);
        if (remaining < 0) {
            // Emergency shrink from lowest priority upward, preserving a compact MCP row when possible.
            changedBudget = Math.max(0, changedBudget + remaining);
            remaining = available - (infoBudget + workerBudget + mcpBudget + changedBudget);
            if (remaining < 0) {
                workerBudget = Math.max(0, workerBudget + remaining);
                remaining = available - (infoBudget + workerBudget + mcpBudget + changedBudget);
                if (remaining < 0) {
                    infoBudget = Math.max(selectedInfoLines.length > 0 ? 1 : 0, infoBudget + remaining);
                    remaining = available - (infoBudget + workerBudget + mcpBudget + changedBudget);
                    if (remaining < 0) {
                        mcpBudget = Math.max(0, mcpBudget + remaining);
                        remaining = 0;
                    }
                }
            }
        }
        // Distribute surplus to higher priorities
        if (remaining > 0) {
            const extraInfo = Math.min(remaining, selectedInfoLines.length - infoBudget);
            infoBudget += extraInfo;
            remaining -= extraInfo;
        }
        if (remaining > 0) {
            const extraWorker = Math.min(remaining, selectedWorkerLines.length - workerBudget);
            workerBudget += extraWorker;
            remaining -= extraWorker;
        }
        if (remaining > 0) {
            const extraMcp = Math.min(remaining, selectedMcpLines.length - mcpBudget);
            mcpBudget += extraMcp;
            remaining -= extraMcp;
        }
        if (remaining > 0) {
            changedBudget += remaining;
        }
        const panelContents = activePanels
            .map((p) => {
            const budget = p.key === "info" ? infoBudget
                : p.key === "worker" ? workerBudget
                    : p.key === "mcp" ? mcpBudget
                        : changedBudget;
            return { title: p.title, lines: fitLines(p.lines, budget) };
        })
            .filter((p) => p.lines.length > 0);
        body = [
            ...headerLines,
            ...(sweepLine ? [sweepLine, renderSweepRule(targetWidth, animFrame + 7)] : []),
            ...panelContents.flatMap((p) => layoutPanel(p.title, p.lines, targetWidth).split("\n")),
            footerLine,
        ];
        while (body.length < fixedBodyHeight)
            body.push("");
        if (body.length > fixedBodyHeight)
            body.length = fixedBodyHeight;
    }
    else {
        body = [...fullLeftLines];
        const minimumBodyHeight = DEFAULT_COCKPIT_HEIGHT - 2;
        while (body.length < minimumBodyHeight)
            body.push("");
    }
    const content = body.map((l) => truncateLine(l, targetWidth));
    return renderCockpitPanel(content, targetWidth);
}
