/**
 * @module        workerDashboard
 * @description   Worker 仪表盘命令，展示所有 Worker 槽位的详细运行状态和输出
 *
 * @author        eddy
 * @organization  wykj
 * @ownership     wykj/eddy
 *
 * @created       2026-03-24
 * @updated       2026-04-03
 *
 * @role          command
 * @layer         command
 * @boundedContext pipeline
 *
 * @trigger       sps worker dashboard <project> [--json] [--watch]
 * @inputs        项目名、JSON 输出标志、watch 模式
 * @outputs       Worker 状态仪表盘（含会话、输出流和运行时信息）
 * @workflow      1. 加载项目状态 → 2. 收集 Worker 运行时快照 → 3. 渲染仪表盘视图
 */
import { execFileSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { resolve } from 'node:path';
import { ProjectContext } from '../core/context.js';
import { loadRuntimeSnapshot } from '../core/runtimeSnapshot.js';
import { isPersistedSessionAlive, isProcessAlive, } from '../core/sessionLiveness.js';
// ACP SDK handles all worker interaction
import { readState } from '../core/state.js';
import { summarizeWorkerRuntime } from '../core/workerRuntimeSummary.js';
import { tailFile } from '../providers/outputParser.js';
import { renderClaudeStreamLines } from '../providers/streamRenderer.js';
const HOME = process.env.HOME || '/home/coral';
// ── ANSI helpers ──────────────────────────────────────────────────────
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
const DIM = '\x1b[2m';
const FG = {
    red: '\x1b[31m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
    magenta: '\x1b[35m',
    cyan: '\x1b[36m',
    white: '\x1b[37m',
    gray: '\x1b[90m',
};
const _BG = {
    black: '\x1b[40m',
};
const STRIP_ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07|\x1b[()][AB012]|\x1b\[[?]?[0-9;]*[hlm]/g;
function statusColor(status) {
    switch (status) {
        case 'active': return FG.green;
        case 'idle': return FG.gray;
        case 'releasing': return FG.yellow;
        default: return FG.white;
    }
}
function statusIcon(status) {
    switch (status) {
        case 'active': return '●';
        case 'idle': return '○';
        case 'releasing': return '◐';
        default: return '?';
    }
}
function sessionStatusIcon(alive) {
    return alive ? `${FG.green}▶${RESET}` : `${FG.red}■${RESET}`;
}
// ── Strip ANSI escape codes for width calculations ────────────────────
function stripAnsi(s) {
    return s.replace(STRIP_ANSI_RE, '');
}
function visibleLength(s) {
    return stripAnsi(s).length;
}
// ── Pad / truncate to visible width ──────────────────────────────────
function padOrTruncate(s, width) {
    const vLen = visibleLength(s);
    if (vLen > width) {
        // Truncate: walk character by character, tracking visible length
        let visible = 0;
        let result = '';
        let inEscape = false;
        for (const ch of s) {
            if (ch === '\x1b') {
                inEscape = true;
                result += ch;
                continue;
            }
            if (inEscape) {
                result += ch;
                if (/[a-zA-Z]/.test(ch))
                    inEscape = false;
                continue;
            }
            if (visible >= width - 1) {
                result += '…';
                break;
            }
            result += ch;
            visible++;
        }
        return result;
    }
    return s + ' '.repeat(width - vLen);
}
// ── Discover all SPS projects from ~/.coral/projects/ ────────────────
function discoverProjects() {
    const projectsDir = resolve(HOME, '.coral', 'projects');
    if (!existsSync(projectsDir))
        return [];
    try {
        return readdirSync(projectsDir, { withFileTypes: true })
            .filter(d => d.isDirectory() && existsSync(resolve(projectsDir, d.name, 'conf')))
            .map(d => d.name)
            .sort();
    }
    catch {
        return [];
    }
}
async function loadSnapshots(projects) {
    const snapshots = new Map();
    for (const projectName of projects) {
        try {
            snapshots.set(projectName, await loadRuntimeSnapshot(projectName, { projectWhenTickStopped: false }));
        }
        catch {
            // Read-only dashboard snapshots are best effort.
        }
    }
    return snapshots;
}
function shortenPath(path) {
    if (!path)
        return '';
    return path.startsWith(HOME) ? `~${path.slice(HOME.length)}` : path;
}
function cleanScreenLines(text) {
    const lines = stripAnsi(text)
        .replace(/\r/g, '\n')
        .split('\n')
        .map(line => line.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').replace(/\s+/g, ' ').trim())
        .filter(Boolean);
    const deduped = [];
    for (const line of lines) {
        if (deduped[deduped.length - 1] !== line)
            deduped.push(line);
    }
    return deduped;
}
function extractClaudeScreenFacts(lines) {
    const facts = {};
    for (const line of lines) {
        if (!facts.prompt) {
            const match = line.match(/^[›❯>]\s+(.+)$/);
            if (match)
                facts.prompt = match[1].trim();
        }
        if (!facts.status) {
            if (/trust this folder/i.test(line)) {
                facts.status = 'Trust confirmation required';
            }
            else if (/Enter to confirm|Esc to cancel/i.test(line)) {
                facts.status = 'Confirmation required';
            }
        }
    }
    return facts;
}
function getProcessMetrics(pid) {
    if (!pid || pid <= 0)
        return null;
    try {
        const out = execFileSync('ps', ['-p', String(pid), '-o', 'pid=,state=,%cpu=,etime='], {
            encoding: 'utf-8',
            timeout: 5_000,
            stdio: ['ignore', 'pipe', 'pipe'],
        }).trim();
        if (!out)
            return null;
        const match = out.match(/^\s*(\d+)\s+(\S+)\s+([\d.]+)\s+(.+)\s*$/);
        if (!match)
            return null;
        return {
            pid: Number(match[1]),
            state: match[2],
            cpu: Number(match[3]),
            elapsed: match[4].trim(),
        };
    }
    catch {
        return null;
    }
}
function latestWorkerLogAgeSec(ctx, _slotName) {
    try {
        const entries = readdirSync(ctx.paths.logsDir)
            .filter(name => name.includes('-acp-') &&
            name.endsWith('.log'))
            .map(name => resolve(ctx.paths.logsDir, name));
        if (entries.length === 0)
            return null;
        entries.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
        const mtimeMs = statSync(entries[0]).mtimeMs;
        return Math.max(0, Math.floor((Date.now() - mtimeMs) / 1000));
    }
    catch {
        return null;
    }
}
function analyzeACPSession(ctx, slotName, slot, session, sessionAlive) {
    const rawLines = cleanScreenLines(session?.lastPaneText || '');
    const facts = extractClaudeScreenFacts(rawLines);
    const process = getProcessMetrics(session?.pid ?? slot.pid);
    const lastOutputAgeSec = latestWorkerLogAgeSec(ctx, slotName);
    let stalledReason = null;
    const runStatus = session?.currentRun?.status || slot.remoteStatus || 'unknown';
    if (session?.stalledReason) {
        stalledReason = session.stalledReason;
    }
    if (!stalledReason &&
        sessionAlive &&
        runStatus === 'stalled_submit') {
        stalledReason = session?.stalledReason || 'prompt submission stalled';
    }
    return {
        screenStatus: facts.status,
        promptText: facts.prompt,
        process,
        lastOutputAgeSec,
        stalledReason,
    };
}
/** Read last N lines from the most recent ACP log file for this session. */
function tailAcpLogFile(ctx, sessionName, n) {
    if (!sessionName)
        return [];
    try {
        const entries = readdirSync(ctx.paths.logsDir)
            .filter(name => name.includes('-acp-') && name.endsWith('.log'))
            .map(name => resolve(ctx.paths.logsDir, name))
            .sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
        if (entries.length === 0)
            return [];
        const content = readFileSync(entries[0], 'utf-8');
        return content.split('\n').filter(l => l.length > 0).slice(-n);
    }
    catch {
        return [];
    }
}
function buildACPPanelLines(ctx, slotName, slot, session, sessionAlive) {
    const lines = [];
    const tool = session?.tool || slot.agent || 'worker';
    const runStatus = session?.currentRun?.status || slot.remoteStatus || 'unknown';
    const sessionState = session?.sessionState || slot.sessionState || 'offline';
    const worktree = shortenPath(slot.worktree || session?.cwd || '');
    // For ACP-SDK: prefer log file tail over lastPaneText (more real-time)
    const isAcpSdk = slot.transport === 'acp-sdk' || slot.mode === 'acp-sdk';
    const paneText = session?.lastPaneText || '';
    const rawLines = isAcpSdk && !paneText
        ? tailAcpLogFile(ctx, session?.sessionName, 20)
        : cleanScreenLines(paneText);
    const _facts = extractClaudeScreenFacts(rawLines);
    const diagnostics = analyzeACPSession(ctx, slotName, slot, session, sessionAlive);
    lines.push(`${BOLD}tool:${RESET} ${tool}`);
    lines.push(`${BOLD}session:${RESET} ${sessionState} / ${runStatus}`);
    lines.push(`${BOLD}cwd:${RESET} ${worktree || '(unknown)'}`);
    if (diagnostics.process) {
        lines.push(`${BOLD}proc:${RESET} pid ${diagnostics.process.pid} · ${diagnostics.process.state} · cpu ${diagnostics.process.cpu.toFixed(1)}% · etime ${diagnostics.process.elapsed}`);
    }
    if (diagnostics.lastOutputAgeSec != null) {
        lines.push(`${BOLD}output:${RESET} last worker output ${formatDuration(diagnostics.lastOutputAgeSec)} ago`);
    }
    if (diagnostics.stalledReason) {
        lines.push(`${FG.yellow}${BOLD}health:${RESET}${FG.yellow} ${diagnostics.stalledReason}${RESET}`);
    }
    if (session?.pendingInput) {
        const danger = session.pendingInput.dangerous ? ' (dangerous)' : '';
        lines.push(`${FG.yellow}${BOLD}waiting:${RESET}${FG.yellow} ${session.pendingInput.type}${danger}${RESET}`);
        lines.push(`${DIM}${session.pendingInput.prompt}${RESET}`);
        return lines;
    }
    if (diagnostics.screenStatus) {
        lines.push(`${BOLD}screen:${RESET} ${diagnostics.screenStatus}`);
    }
    if (diagnostics.promptText) {
        lines.push(`${DIM}${diagnostics.promptText}${RESET}`);
    }
    else if (session?.currentRun?.promptPreview) {
        // For ACP-SDK mode, promptPreview is the full prompt (not useful for display)
        const isAcp = slot.transport === 'acp-sdk' || slot.mode === 'acp-sdk';
        if (!isAcp) {
            lines.push(`${DIM}${session.currentRun.promptPreview}${RESET}`);
        }
    }
    const liveTail = rawLines
        .filter(line => line !== diagnostics.screenStatus && line !== diagnostics.promptText)
        .slice(-4);
    if (liveTail.length === 0 && (slot.transport === 'acp-sdk' || slot.mode === 'acp-sdk')) {
        lines.push(`${DIM}(waiting for agent output...)${RESET}`);
    }
    for (const line of liveTail) {
        lines.push(`${DIM}${line}${RESET}`);
    }
    return lines;
}
function buildPrintPanelLines(_slot, rawLines) {
    const lines = rawLines
        .map(line => line.replace(/[\u0000-\u001F\u007F-\u009F]/g, '').trim())
        .filter(Boolean)
        .slice(-8);
    return lines.length > 0 ? lines : ['(worker running, no output yet)'];
}
function collectPanels(projects, snapshots) {
    const panels = [];
    for (const projectName of projects) {
        const snapshot = snapshots.get(projectName);
        if (!snapshot)
            continue;
        const { ctx, state } = snapshot;
        for (const [slotName, slot] of Object.entries(state.workers)) {
            const isPrintMode = slot.mode === 'print';
            const isAcpMode = slot.mode === 'acp' || slot.mode === 'acp-sdk' || slot.transport === 'acp-sdk';
            let sessionAlive;
            let paneLines;
            if (isAcpMode) {
                const session = state.sessions[slotName];
                const runStatus = session?.currentRun?.status || slot.remoteStatus || 'unknown';
                sessionAlive = isPersistedSessionAlive(slot, session);
                paneLines = buildACPPanelLines(ctx, slotName, slot, session, sessionAlive);
                if (paneLines.length === 0) {
                    paneLines = [`(acp ${slot.agent || 'worker'} ${sessionAlive ? 'connected' : 'offline'})`, `run: ${runStatus}`];
                }
            }
            else if (isPrintMode) {
                // Print mode: check PID liveness + tail output file
                sessionAlive = !!(slot.pid && slot.pid > 0 && isProcessAlive(slot.pid));
                if (slot.outputFile) {
                    const rawLines = tailFile(slot.outputFile, 40).split('\n');
                    // Render JSONL into human-readable lines
                    const rendered = renderClaudeStreamLines(rawLines);
                    paneLines = buildPrintPanelLines(slot, rendered.length > 0 ? rendered : rawLines);
                }
                else {
                    paneLines = sessionAlive ? ['(worker running, no output yet)'] : ['(no output file)'];
                }
            }
            else {
                // Fallback: no live output
                sessionAlive = false;
                paneLines = ['(no output)'];
            }
            // Skip idle slots with no live session/process
            if (slot.status === 'idle' && !sessionAlive)
                continue;
            // Skip "active" slots where the worker PID is actually dead (stale state)
            if (slot.status === 'active' && !sessionAlive)
                continue;
            // Show "merging" slots (no live output but useful to see in dashboard)
            // Show "resolving" slots (resume worker may have output)
            panels.push({
                projectName,
                slotName,
                slot,
                sessionAlive,
                paneLines,
            });
        }
    }
    return panels;
}
// ── Render dashboard ─────────────────────────────────────────────────
function renderHeader(termWidth) {
    const title = ' SPS Worker Dashboard ';
    const now = new Date().toLocaleTimeString();
    const rightInfo = `${DIM}${now}  q=quit  r=refresh  :=respond${RESET}`;
    const rightLen = visibleLength(rightInfo);
    const leftPad = Math.max(0, Math.floor((termWidth - title.length) / 2));
    return [
        `${BOLD}${FG.cyan}${'─'.repeat(termWidth)}${RESET}`,
        `${' '.repeat(leftPad)}${BOLD}${FG.cyan}${title}${RESET}${' '.repeat(Math.max(0, termWidth - leftPad - title.length - rightLen))}${rightInfo}`,
        `${BOLD}${FG.cyan}${'─'.repeat(termWidth)}${RESET}`,
    ];
}
function renderPanel(panel, panelWidth, panelHeight) {
    const lines = [];
    const innerWidth = panelWidth - 2; // borders
    // ── Header bar ──
    const sIcon = statusIcon(panel.slot.status);
    const sColor = statusColor(panel.slot.status);
    const aliveIcon = sessionStatusIcon(panel.sessionAlive);
    const seqInfo = panel.slot.seq !== null ? ` seq:${panel.slot.seq}` : '';
    const branchInfo = panel.slot.branch ? ` ${DIM}${panel.slot.branch}${RESET}` : '';
    const headerText = `${sColor}${sIcon} ${BOLD}${panel.projectName}/${panel.slotName}${RESET}${seqInfo} ${aliveIcon}${branchInfo}`;
    const _headerLine = ` ${padOrTruncate(headerText, innerWidth)} `;
    // ── Time info ──
    const elapsed = panel.slot.claimedAt
        ? formatElapsed(new Date(panel.slot.claimedAt))
        : '';
    const heartbeat = panel.slot.lastHeartbeat
        ? `hb: ${formatElapsed(new Date(panel.slot.lastHeartbeat))} ago`
        : '';
    const modeInfo = panel.slot.mode === 'print'
        ? `pid:${panel.slot.pid || '?'}${panel.slot.exitCode != null ? ` exit:${panel.slot.exitCode}` : ''}`
        : (panel.slot.mode === 'acp' || panel.slot.mode === 'acp-sdk')
            ? `${panel.slot.mode}:${panel.slot.sessionState || 'unknown'}${panel.slot.remoteStatus ? `/${panel.slot.remoteStatus}` : ''}`
            : '';
    const timeLine = elapsed || heartbeat || modeInfo
        ? ` ${DIM}${[elapsed, heartbeat, modeInfo].filter(Boolean).join(' │ ')}${RESET}`
        : '';
    // Top border
    lines.push(`${FG.gray}┌${'─'.repeat(panelWidth - 2)}┐${RESET}`);
    lines.push(`${FG.gray}│${RESET}${padOrTruncate(headerText, innerWidth)}${FG.gray}│${RESET}`);
    if (timeLine) {
        lines.push(`${FG.gray}│${RESET}${padOrTruncate(timeLine, innerWidth)}${FG.gray}│${RESET}`);
    }
    lines.push(`${FG.gray}├${'─'.repeat(panelWidth - 2)}┤${RESET}`);
    // ── Pane content ──
    const contentHeight = panelHeight - lines.length - 1; // -1 for bottom border
    const trimmedLines = panel.paneLines
        .filter(l => l.trim() !== '') // skip empty lines
        .slice(-contentHeight); // show last N lines
    for (let i = 0; i < contentHeight; i++) {
        const raw = trimmedLines[i] ?? '';
        const content = padOrTruncate(raw, innerWidth);
        lines.push(`${FG.gray}│${RESET}${content}${FG.gray}│${RESET}`);
    }
    // Bottom border
    lines.push(`${FG.gray}└${'─'.repeat(panelWidth - 2)}┘${RESET}`);
    return lines;
}
function renderIdleSummary(projects, termWidth, snapshots) {
    const lines = [];
    for (const projectName of projects) {
        void termWidth;
        const snapshot = snapshots.get(projectName);
        if (!snapshot)
            continue;
        const { state } = snapshot;
        const summary = summarizeWorkerRuntime(state);
        const activeCards = Object.keys(state.activeCards).length;
        const extraParts = [];
        if (summary.merging > 0)
            extraParts.push(`${FG.yellow}${summary.merging} merging${RESET}`);
        if (summary.stale > 0)
            extraParts.push(`${FG.yellow}${summary.stale} stale${RESET}`);
        const extraStr = extraParts.length > 0 ? ` / ${extraParts.join(' / ')}` : '';
        lines.push(`  ${BOLD}${projectName}${RESET}: ${FG.green}${summary.active} active${RESET} / ${FG.gray}${summary.idle} idle${RESET}${extraStr} / ${summary.total} total  │  ${FG.cyan}${activeCards} cards${RESET}`);
    }
    return lines;
}
function renderEmptyState(_termWidth) {
    return [
        '',
        `${DIM}  No active workers found.${RESET}`,
        `${DIM}  All worker slots are idle across all projects.${RESET}`,
        '',
        `${DIM}  Tip: Start a pipeline with ${RESET}sps tick <project>${DIM} to launch workers.${RESET}`,
        '',
    ];
}
function formatElapsed(from) {
    const ms = Date.now() - from.getTime();
    if (ms < 0)
        return '';
    const secs = Math.floor(ms / 1000);
    if (secs < 60)
        return `${secs}s`;
    const mins = Math.floor(secs / 60);
    if (mins < 60)
        return `${mins}m${secs % 60}s`;
    const hours = Math.floor(mins / 60);
    return `${hours}h${mins % 60}m`;
}
function formatDuration(totalSecs) {
    if (totalSecs < 60)
        return `${totalSecs}s`;
    const mins = Math.floor(totalSecs / 60);
    if (mins < 60)
        return `${mins}m${totalSecs % 60}s`;
    const hours = Math.floor(mins / 60);
    return `${hours}h${mins % 60}m`;
}
function getTerminalSize() {
    return {
        cols: process.stdout.columns || 120,
        rows: process.stdout.rows || 40,
    };
}
function renderDashboard(projects, snapshots) {
    const { cols: termWidth, rows: termHeight } = getTerminalSize();
    const output = [];
    // Header
    output.push(...renderHeader(termWidth));
    // Summary line
    output.push('');
    output.push(...renderIdleSummary(projects, termWidth, snapshots));
    output.push('');
    // Collect active panels
    const panels = collectPanels(projects, snapshots);
    if (panels.length === 0) {
        output.push(...renderEmptyState(termWidth));
        return output.join('\n');
    }
    // Calculate grid layout
    const gridCols = panels.length <= 2 ? panels.length : panels.length <= 4 ? 2 : 3;
    const gridRows = Math.ceil(panels.length / gridCols);
    const panelWidth = Math.floor((termWidth - 1) / gridCols); // -1 for spacing
    const usedHeaderRows = output.length;
    const availableRows = termHeight - usedHeaderRows - 1; // -1 for safety
    const panelHeight = Math.max(6, Math.floor(availableRows / gridRows));
    // Render panels row by row
    for (let row = 0; row < gridRows; row++) {
        const rowPanels = [];
        for (let col = 0; col < gridCols; col++) {
            const idx = row * gridCols + col;
            if (idx < panels.length) {
                rowPanels.push(renderPanel(panels[idx], panelWidth, panelHeight));
            }
            else {
                // Empty filler
                rowPanels.push(Array(panelHeight + 2).fill(' '.repeat(panelWidth)));
            }
        }
        // Merge columns side by side
        const maxLines = Math.max(...rowPanels.map(p => p.length));
        for (let line = 0; line < maxLines; line++) {
            const merged = rowPanels
                .map(p => p[line] ?? ' '.repeat(panelWidth))
                .join(' ');
            output.push(merged);
        }
    }
    // Truncate to terminal height to prevent scrolling
    if (output.length > termHeight) {
        output.length = termHeight;
    }
    return output.join('\n');
}
function buildJsonOutput(projects, snapshots) {
    const result = {
        timestamp: new Date().toISOString(),
        projects: [],
    };
    for (const projectName of projects) {
        const snapshot = snapshots.get(projectName);
        if (!snapshot)
            continue;
        const { state } = snapshot;
        const workers = [];
        for (const [slotName, slot] of Object.entries(state.workers)) {
            const isPrintMode = slot.mode === 'print';
            const isAcpMode = slot.mode === 'acp' || slot.mode === 'acp-sdk' || slot.transport === 'acp-sdk';
            const acpSession = state.sessions[slotName];
            const sessionAlive = isAcpMode
                ? isPersistedSessionAlive(slot, acpSession)
                : isPrintMode
                    ? !!(slot.pid && slot.pid > 0 && isProcessAlive(slot.pid))
                    : false;
            const panePreview = isAcpMode
                ? buildACPPanelLines(snapshot.ctx, slotName, slot, acpSession, sessionAlive).join(' | ')
                : '';
            const diagnostics = isAcpMode
                ? analyzeACPSession(snapshot.ctx, slotName, slot, acpSession, sessionAlive)
                : null;
            const projectedStatus = isAcpMode && !sessionAlive && slot.status !== 'idle'
                ? 'stale'
                : slot.status;
            workers.push({
                slot: slotName,
                status: projectedStatus,
                seq: slot.seq,
                branch: slot.branch,
                sessionAlive,
                claimedAt: slot.claimedAt,
                lastHeartbeat: slot.lastHeartbeat,
                processCpu: diagnostics?.process?.cpu ?? null,
                processState: diagnostics?.process?.state ?? null,
                lastOutputAgeSec: diagnostics?.lastOutputAgeSec ?? null,
                stalledReason: diagnostics?.stalledReason ?? null,
                panePreview,
            });
        }
        result.projects.push({
            name: projectName,
            workers,
            activeCards: state.activeCards,
        });
    }
    return result;
}
function collectPendingInputs(projects) {
    const items = [];
    for (const projectName of projects) {
        let ctx;
        try {
            ctx = ProjectContext.load(projectName);
        }
        catch {
            continue;
        }
        const state = readState(ctx.paths.stateFile, ctx.maxWorkers);
        const transport = ctx.config.raw.WORKER_TRANSPORT || 'acp';
        for (const [slotName, session] of Object.entries(state.sessions)) {
            if (!session.pendingInput)
                continue;
            items.push({
                project: projectName,
                slot: slotName,
                prompt: session.pendingInput.prompt,
                options: session.pendingInput.options,
                dangerous: session.pendingInput.dangerous,
                transport,
                sessionName: session.sessionName || `sps-acp-${projectName}-${slotName}`,
            });
        }
    }
    return items;
}
function sendResponse(item, response) {
    try {
        // Send response via ACP runtime
        const ctx = ProjectContext.load(item.project);
        const { createAgentRuntime } = require('../providers/registry.js');
        const runtime = createAgentRuntime(ctx);
        // Fire and forget — dashboard is synchronous UI
        runtime.resumeRun(item.slot, response).catch(() => { });
        return true;
    }
    catch {
        return false;
    }
}
// ── Live mode (watch) ────────────────────────────────────────────────
async function runLive(projects, intervalMs) {
    // Switch to alternate screen buffer + hide cursor
    process.stdout.write('\x1b[?1049h\x1b[?25l');
    let inputMode = false;
    let inputBuffer = '';
    let statusMessage = '';
    const cleanup = () => {
        process.stdout.write('\x1b[?25h'); // show cursor
        process.stdout.write('\x1b[?1049l'); // switch back to main screen
        process.stdout.write('\x1b[0m'); // reset colors
        process.exit(0);
    };
    process.on('SIGINT', cleanup);
    process.on('SIGTERM', cleanup);
    const draw = async () => {
        const snapshots = await loadSnapshots(projects);
        const { cols: termWidth } = getTerminalSize();
        const screen = renderDashboard(projects, snapshots);
        process.stdout.write('\x1b[H\x1b[J');
        process.stdout.write(screen);
        // Render pending list + input bar at bottom
        const pending = collectPendingInputs(projects);
        if (pending.length > 0) {
            process.stdout.write(`\n${FG.yellow}  Pending confirmations:${RESET}\n`);
            pending.forEach((p, i) => {
                const danger = p.dangerous ? `${FG.red} DANGEROUS${RESET}` : '';
                const opts = p.options ? ` [${p.options.map((o, j) => `${j + 1}=${o}`).join(', ')}]` : '';
                process.stdout.write(`  ${BOLD}${i + 1}.${RESET} ${p.project}/${p.slot}${danger}: ${p.prompt}${DIM}${opts}${RESET}\n`);
            });
        }
        if (inputMode) {
            process.stdout.write(`\n${FG.cyan}  > respond ${RESET}${inputBuffer}\x1b[?25h`); // show cursor in input mode
        }
        else if (statusMessage) {
            process.stdout.write(`\n  ${statusMessage}\n`);
        }
        else {
            const hint = pending.length > 0
                ? `${DIM}  :=respond  r=refresh  q=quit${RESET}`
                : `${DIM}  r=refresh  q=quit${RESET}`;
            process.stdout.write(`\n${hint}\n`);
        }
    };
    if (process.stdin.isTTY) {
        process.stdin.setRawMode(true);
        process.stdin.resume();
        process.stdin.setEncoding('utf-8');
        process.stdin.on('data', (key) => {
            if (inputMode) {
                if (key === '\x1b') {
                    // Escape — cancel input
                    inputMode = false;
                    inputBuffer = '';
                    process.stdout.write('\x1b[?25l'); // hide cursor
                    void draw();
                    return;
                }
                if (key === '\r') {
                    // Enter — execute command
                    inputMode = false;
                    process.stdout.write('\x1b[?25l'); // hide cursor
                    const cmd = inputBuffer.trim();
                    inputBuffer = '';
                    // Parse: "<slot> <response>" or "<number> <response>"
                    const parts = cmd.split(/\s+/);
                    if (parts.length >= 2) {
                        const pending = collectPendingInputs(projects);
                        const target = parts[0];
                        const response = parts.slice(1).join(' ');
                        // Try by index (1-based)
                        const idx = parseInt(target, 10);
                        let item;
                        if (!Number.isNaN(idx) && idx >= 1 && idx <= pending.length) {
                            item = pending[idx - 1];
                        }
                        else {
                            // Try by slot name
                            item = pending.find(p => p.slot === target || p.slot === `worker-${target}`);
                        }
                        if (item) {
                            const ok = sendResponse(item, response);
                            statusMessage = ok
                                ? `${FG.green}  Sent "${response}" to ${item.project}/${item.slot}${RESET}`
                                : `${FG.red}  Failed to send to ${item.project}/${item.slot}${RESET}`;
                            // Clear status after 3s
                            setTimeout(() => { statusMessage = ''; void draw(); }, 3000);
                        }
                        else {
                            statusMessage = `${FG.red}  Target not found: ${target}${RESET}`;
                            setTimeout(() => { statusMessage = ''; void draw(); }, 3000);
                        }
                    }
                    else {
                        statusMessage = `${FG.red}  Usage: <slot-or-number> <response>${RESET}`;
                        setTimeout(() => { statusMessage = ''; void draw(); }, 3000);
                    }
                    void draw();
                    return;
                }
                if (key === '\x7f') {
                    // Backspace
                    inputBuffer = inputBuffer.slice(0, -1);
                    void draw();
                    return;
                }
                // Regular character
                if (key.length === 1 && key.charCodeAt(0) >= 32) {
                    inputBuffer += key;
                    void draw();
                    return;
                }
                return;
            }
            // Normal mode
            if (key === 'q' || key === '\x03') {
                cleanup();
            }
            if (key === 'r') {
                void draw();
            }
            if (key === ':') {
                const pending = collectPendingInputs(projects);
                if (pending.length > 0) {
                    inputMode = true;
                    inputBuffer = '';
                    void draw();
                }
            }
        });
    }
    await draw();
    setInterval(() => { void draw(); }, intervalMs);
    return new Promise(() => { });
}
// ── Main entry point ─────────────────────────────────────────────────
export async function executeWorkerDashboard(projects, flags) {
    const jsonOutput = !!flags.json;
    const watch = !flags.once; // default to live mode unless --once
    // If no projects specified, discover all
    if (projects.length === 0) {
        projects = discoverProjects();
    }
    if (projects.length === 0) {
        if (jsonOutput) {
            console.log(JSON.stringify({ timestamp: new Date().toISOString(), projects: [] }));
        }
        else {
            console.error('No projects found in ~/.coral/projects/');
        }
        process.exit(1);
    }
    const snapshots = await loadSnapshots(projects);
    if (jsonOutput) {
        console.log(JSON.stringify(buildJsonOutput(projects, snapshots), null, 2));
        return;
    }
    if (watch) {
        const intervalMs = parseInt(process.env.SPS_DASHBOARD_INTERVAL || '3000', 10);
        await runLive(projects, intervalMs);
    }
    else {
        console.log(renderDashboard(projects, snapshots));
    }
}
//# sourceMappingURL=workerDashboard.js.map