import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React from "react";
import { Text, render } from "ink";
import stripAnsiLib from "strip-ansi";
import { describe, expect, it } from "vitest";
import { ChatLivePane } from "./components/ChatLivePane.js";
import { ChatLayout, ChatControls } from "./components/ChatLayout.js";
import { TerminalSizeProvider } from "./hooks/useTerminalSize.js";
import { useTheme } from "./theme/theme.js";
import { renderTranscriptItem } from "./transcript/TranscriptRenderer.js";
import { ScreenRecorder, makeRecordingStdout } from "./testing/screen-recorder.js";
const ROWS = 24;
const COLUMNS = 80;
const CONTROLS_ROWS = 4;
// Mirror useChatLayoutMeasurements: rows - controlsRows - 2 (widened cushion).
const measuredLiveAreaRows = Math.max(3, ROWS - CONTROLS_ROWS - 2);
function stripAnsi(v) {
    return v.replace(new RegExp(String.raw `\u001B\[[0-?]*[ -/]*[@-~]`, "g"), "");
}
function Harness({ liveItems, streamingText, streamingThinking = "", }) {
    const theme = useTheme();
    const renderItem = (item, index, items) => renderTranscriptItem({
        item,
        index,
        items,
        version: "0",
        currentModel: "m",
        currentProvider: "anthropic",
        displayedCwd: "/tmp",
        columns: COLUMNS,
        theme,
        renderMarkdown: true,
        measuredLiveAreaRows,
    });
    return (_jsxs(ChatLayout, { columns: COLUMNS, children: [_jsx(ChatLivePane, { liveItems: liveItems, renderItem: renderItem, isRunning: true, visibleStreamingText: streamingText, streamingThinking: streamingThinking, thinkingMs: streamingThinking ? 800 : 0, reserveStreamingSpacing: false, renderMarkdown: true, measuredLiveAreaRows: measuredLiveAreaRows, assistantMarginTop: 0, streamingContinuation: false }), _jsx(ChatControls, { controlsRef: () => { }, children: Array.from({ length: CONTROLS_ROWS }, (_, i) => (_jsxs(Text, { children: ["CONTROL_", i] }, i))) })] }));
}
function frameHeight(node) {
    let output = "";
    const stdout = {
        columns: COLUMNS,
        rows: ROWS,
        write(chunk) {
            output += chunk;
            return true;
        },
        on() { },
        off() { },
    };
    const { unmount } = render(_jsx(TerminalSizeProvider, { children: node }), {
        stdout,
        columns: COLUMNS,
        rows: ROWS,
        debug: true,
    });
    const height = stripAnsi(output).split("\n").length;
    unmount();
    return height;
}
const longText = Array.from({ length: 60 }, (_, i) => `response line ${i + 1}`).join("\n");
describe("live area clamp", () => {
    it("keeps multiple accumulated assistant blocks below the terminal height", () => {
        // The "jump on final response": Ink redraws from the top once a frame's
        // height reaches the terminal rows. Stacked finalized assistant turns must
        // stay strictly below rows so that never triggers.
        const height = frameHeight(_jsx(Harness, { liveItems: [
                { kind: "assistant", id: "a1", text: longText },
                { kind: "assistant", id: "a2", text: longText },
            ], streamingText: "" }));
        expect(height).toBeLessThan(ROWS);
    });
    it("keeps a finalized block plus in-flight streaming below the terminal height", () => {
        const height = frameHeight(_jsx(Harness, { liveItems: [{ kind: "assistant", id: "a1", text: longText }], streamingText: longText }));
        expect(height).toBeLessThan(ROWS);
    });
    it("keeps a finalized block plus streaming text and active thinking below the terminal height", () => {
        // Worst live-frame case on the native-scrollback default: a finalized turn
        // is still in the live area while the next turn streams BOTH a thinking
        // block and visible text. This is the tallest the live frame ever gets, so
        // it must still stay strictly below rows or Ink's clearTerminal redraw
        // snaps the footer to the top.
        const height = frameHeight(_jsx(Harness, { liveItems: [{ kind: "assistant", id: "a1", text: longText }], streamingText: longText, streamingThinking: Array.from({ length: 20 }, (_, i) => `thinking line ${i + 1}`).join("\n") }));
        expect(height).toBeLessThan(ROWS);
    });
    it("stays compact (no reserved blank rows) when live content is short", () => {
        const height = frameHeight(_jsx(Harness, { liveItems: [{ kind: "assistant", id: "a1", text: "short reply" }], streamingText: "" }));
        // 1 assistant row + 4 control rows, no padding up to the budget.
        expect(height).toBeLessThanOrEqual(CONTROLS_ROWS + 2);
    });
    it("reserves no blank rows above short live content (slash-command info rows)", async () => {
        // Regression: several lingering non-assistant rows (e.g. info rows printed
        // by a slash command) used to inflate the biased row ESTIMATE past the
        // budget, so the clamp fixed the live area to `budget` while the real
        // content was far shorter — bottom-anchoring it and reserving a block of
        // blank rows above the first visible row until the rows flushed to history.
        // With the estimate biased low the area stays compact, so the footer is
        // never locked too low and no blank gap appears.
        const recorder = new ScreenRecorder({ columns: COLUMNS, rows: ROWS });
        const stdout = makeRecordingStdout(recorder);
        const items = Array.from({ length: 6 }, (_, i) => ({ kind: "info", id: `i${i}`, text: `info line ${i + 1}` }));
        const instance = render(_jsx(TerminalSizeProvider, { children: _jsx(Harness, { liveItems: items, streamingText: "final short answer" }) }), {
            stdout,
            columns: COLUMNS,
            rows: ROWS,
            patchConsole: false,
            maxFps: 1000,
            anchorFrameToBottom: true,
            clipFrameToTerminalHeight: true,
        });
        // Flush any throttled/queued frame writes before reading the screen.
        await new Promise((resolve) => setTimeout(resolve, 80));
        const lines = recorder.viewportLines().map((line) => stripAnsiLib(line));
        instance.unmount();
        const firstInfoIdx = lines.findIndex((line) => line.includes("info line 1"));
        const lastContentIdx = lines.reduce((acc, line, i) => (line.trim().length > 0 ? i : acc), -1);
        expect(firstInfoIdx, "first info row visible").toBeGreaterThanOrEqual(0);
        // No run of blank rows reserved directly above the first live row.
        const blanksAbove = lines
            .slice(0, firstInfoIdx)
            .filter((line) => line.trim().length === 0).length;
        expect(blanksAbove, "no blank gap reserved above the live content").toBe(0);
        // And the controls/footer still sit at the very bottom, nothing stranded.
        expect(lines[lastContentIdx]).toContain("CONTROL_");
    });
});
//# sourceMappingURL=live-area-clamp.test.js.map