"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.runRepl = runRepl;
/**
 * REPL main loop.
 *
 * Design notes:
 *   - Single long-lived CliAnalyzeService + renderer across turns — the
 *     trace processor shell only starts once.
 *   - Readline is paused during a turn so prompt-echo and event-stream
 *     output don't interleave. We don't need `rl.pause()`'s full contract
 *     (kernel-level buffering) — a boolean guard that drops newline
 *     events is enough.
 *   - Ctrl+C is a two-press exit: first press warns, second (within 1.5s)
 *     calls process.exit. While a turn is running, the second press is
 *     the only escape — we can't cleanly abort the Claude SDK from here.
 *   - Trailing `\` on a line is a continuation marker, so users can paste
 *     multi-line questions without a special /multi mode.
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const readline = __importStar(require("readline"));
const paths_1 = require("../io/paths");
const turnRunner_1 = require("../services/turnRunner");
const sessionStore_1 = require("../io/sessionStore");
const openFile_1 = require("../io/openFile");
const slashCommands_1 = require("./slashCommands");
const constants_1 = require("../constants");
const runtimeGuard_1 = require("../services/runtimeGuard");
/** Ctrl+C double-press window. Matches common CLI tools (bash, python, Claude Code). */
const CTRL_C_DOUBLE_PRESS_MS = 1500;
/**
 * Start the REPL. Resolves when the user exits (via /exit or double Ctrl+C).
 * Caller is responsible for calling `service.shutdown()` afterwards.
 */
async function runRepl(ctx, initialResumeId) {
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout,
        terminal: true,
        // Prevent readline's own "press Ctrl+C to quit" behavior — we own that.
        historySize: 200,
    });
    let currentSession = null;
    // turnInProgress is a plain boolean rather than a state machine because Node's
    // event loop guarantees the read-set sequence below is atomic in practice:
    // the rl.on('line') handler sets `turnInProgress = true` synchronously
    // *before* its first await, so any subsequent `line` event queued by readline
    // observes true and exits via the early-return guard. Reviewers have flagged
    // this twice as a possible race — it isn't, given single-threaded JS.
    let turnInProgress = false;
    let lastCtrlC = 0;
    let continuationBuffer = '';
    // If the user passed --resume <id> on the command line, pre-load it so the
    // first prompt already knows about the session.
    if (initialResumeId) {
        const loaded = tryLoadSession(ctx.paths, initialResumeId);
        if (loaded) {
            currentSession = loaded;
            console.log(`(resumed session ${initialResumeId})`);
        }
        else {
            console.error(`(warn: --resume ${initialResumeId} — no such session)`);
        }
    }
    printBanner(currentSession);
    // Ctrl+C handling. We don't use rl's default 'SIGINT' handler because
    // it exits on first press; we want the two-press convention.
    rl.on('SIGINT', () => {
        const now = Date.now();
        if (now - lastCtrlC < CTRL_C_DOUBLE_PRESS_MS) {
            console.log('\nexiting.');
            rl.close();
            process.exit(130); // 128 + SIGINT
        }
        lastCtrlC = now;
        // Drop any pending continuation in both branches — leaving buffered lines
        // around across a Ctrl+C would splice them onto whatever the user types next,
        // which is always surprising.
        continuationBuffer = '';
        if (turnInProgress) {
            console.log('\n(turn in progress — Ctrl+C again within 1.5s to force-exit)');
        }
        else {
            console.log('\n(press Ctrl+C again within 1.5s to exit, or /exit)');
            rl.line = '';
            rl.cursor = 0;
            rl.prompt();
        }
    });
    // Main input loop. Readline is push-driven; we wrap the whole thing in a
    // Promise that only resolves when rl closes.
    await new Promise((resolve) => {
        rl.on('close', () => resolve());
        rl.on('line', async (raw) => {
            // Trailing-backslash continuation. Must come before the slash parser —
            // otherwise a multiline /ask body gets truncated at the first newline.
            // A bare `\` on an otherwise empty line is treated as "cancel continuation"
            // so a stuck user can escape without Ctrl+C.
            if (raw.endsWith('\\')) {
                if (raw.trim() === '\\') {
                    continuationBuffer = '';
                    rl.setPrompt(promptString(currentSession));
                    rl.prompt();
                    return;
                }
                continuationBuffer += `${raw.slice(0, -1)}\n`;
                rl.setPrompt('... ');
                rl.prompt();
                return;
            }
            const full = continuationBuffer + raw;
            continuationBuffer = '';
            // Guard against re-entrant turn dispatch. Shouldn't happen while rl
            // is fully synchronous, but cheap insurance.
            if (turnInProgress) {
                console.log('(turn in progress — please wait)');
                return;
            }
            const cmd = (0, slashCommands_1.parseSlashCommand)(full);
            try {
                switch (cmd.kind) {
                    case 'noop':
                        break;
                    case 'help':
                        console.log(slashCommands_1.SLASH_HELP);
                        break;
                    case 'clear':
                        // ANSI: clear screen + scrollback + home cursor.
                        process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
                        break;
                    case 'exit':
                        rl.close();
                        return;
                    case 'unknown':
                        console.log(`Unknown command: /${cmd.command}. Type /help for the list.`);
                        break;
                    case 'usage':
                        console.log(`Usage: ${cmd.hint}`);
                        break;
                    case 'focus':
                        printFocus(currentSession);
                        break;
                    case 'report':
                        handleReport(currentSession, cmd.open);
                        break;
                    case 'load':
                        turnInProgress = true;
                        currentSession = await handleLoad(ctx, cmd.path);
                        turnInProgress = false;
                        break;
                    case 'ask':
                        if (!currentSession) {
                            console.log('(no session — /load <trace> or /resume <id> first)');
                            break;
                        }
                        turnInProgress = true;
                        currentSession = await handleAsk(ctx, currentSession, cmd.query);
                        turnInProgress = false;
                        break;
                    case 'resume':
                        turnInProgress = true;
                        currentSession = await handleResume(ctx, cmd.sessionId);
                        turnInProgress = false;
                        break;
                }
            }
            catch (err) {
                turnInProgress = false;
                ctx.renderer.printError(err.message);
            }
            rl.setPrompt(promptString(currentSession));
            rl.prompt();
        });
        rl.setPrompt(promptString(currentSession));
        rl.prompt();
    });
}
/** What the user sees before each line of input. Short, honest about session state. */
function promptString(cs) {
    if (!cs)
        return 'smartperfetto › ';
    // Show the short form — full id is available via /focus.
    return `smartperfetto [${cs.sessionId.slice(-6)}] › `;
}
function printBanner(cs) {
    console.log('SmartPerfetto REPL — type /help for commands, /exit (or Ctrl+C twice) to quit.');
    if (!cs)
        console.log('No session loaded yet. Use /load <trace-path> to begin.');
}
function printFocus(cs) {
    if (!cs) {
        console.log('(no session)');
        return;
    }
    console.log(`session   ${cs.sessionId}`);
    console.log(`trace     ${cs.config.tracePath}`);
    console.log(`turns     ${cs.config.turnCount}`);
    console.log(`updated   ${new Date(cs.config.lastTurnAt).toISOString()}`);
    console.log(`folder    ${cs.sp.dir}`);
}
function handleReport(cs, open) {
    if (!cs) {
        console.log('(no session)');
        return;
    }
    if (!fs.existsSync(cs.sp.report)) {
        console.log('(no report.html yet — ask a question first)');
        return;
    }
    console.log(cs.sp.report);
    if (open) {
        const r = (0, openFile_1.openPath)(cs.sp.report);
        if (!r.ok)
            console.error(`(open failed: ${r.reason})`);
    }
}
function tryLoadSession(paths, sessionId) {
    const sp = (0, paths_1.sessionPaths)(paths, sessionId);
    const { config } = (0, sessionStore_1.loadSession)(paths, sessionId);
    if (!config)
        return null;
    return { sessionId, sp, config };
}
async function handleLoad(ctx, tracePath) {
    // /load kicks off the first turn with the generic catch-all query; the user
    // can follow up with a targeted /ask for real drilling.
    (0, runtimeGuard_1.assertAnalysisRuntimeReady)();
    const expanded = expandPath(tracePath);
    const r = await (0, turnRunner_1.startSession)(ctx, { tracePath: expanded, query: constants_1.DEFAULT_ANALYSIS_QUERY });
    return refreshSession(ctx, r.sessionId);
}
async function handleAsk(ctx, cs, query) {
    (0, runtimeGuard_1.assertAnalysisRuntimeReady)({
        providerId: cs.config.providerId,
        runtimeOverride: cs.config.agentRuntimeKind,
    });
    await (0, turnRunner_1.continueSession)(ctx, { sessionId: cs.sessionId, query });
    return refreshSession(ctx, cs.sessionId);
}
async function handleResume(ctx, sessionId) {
    const loaded = tryLoadSession(ctx.paths, sessionId);
    if (!loaded) {
        console.error(`no session found at ${(0, paths_1.sessionPaths)(ctx.paths, sessionId).dir}`);
        return null;
    }
    console.log(`(switched to session ${sessionId}, turn ${loaded.config.turnCount})`);
    return loaded;
}
function refreshSession(ctx, sessionId) {
    // Re-read config from disk — turnCount/lastTurnAt/sdkSessionId just changed.
    const reloaded = tryLoadSession(ctx.paths, sessionId);
    if (!reloaded) {
        throw new Error(`internal: session ${sessionId} missing after turn commit`);
    }
    return reloaded;
}
/** Expand leading `~` and resolve to absolute. Paths with spaces survive. */
function expandPath(p) {
    if (p.startsWith('~/')) {
        return path.join(process.env.HOME || '', p.slice(2));
    }
    return path.resolve(p);
}
//# sourceMappingURL=index.js.map