import { matchesKey, Container, Spacer, } from "@earendil-works/pi-tui";
export function addChatTranscriptEntry(container, component, role) {
    if (needsLeadingSpacer(role) && container.children.length > 0) {
        container.addChild(new Spacer(1));
    }
    container.addChild(component);
}
function needsLeadingSpacer(role) {
    return (role === "user" ||
        role === "custom" ||
        role === "notice" ||
        role === "system" ||
        role === "summary");
}
/**
 * Reusable pi chat transcript scaffold for extension surfaces.
 *
 * This intentionally mirrors InteractiveMode.addMessageToChat spacing without
 * coupling consumers to a full AgentSession. Extension UIs can bring their own
 * message model while still rendering inside the same Container/Spacer rhythm
 * as the main chat.
 */
export class ChatTranscriptComponent {
    constructor(entries, renderEntry) {
        this.entries = entries;
        this.renderEntry = renderEntry;
    }
    render(width) {
        const container = new Container();
        for (const entry of this.entries) {
            addChatTranscriptEntry(container, this.renderEntry(entry), entry.role);
        }
        return container.render(width);
    }
    invalidate() { }
}
const DEFAULT_SCROLL_STEP_ROWS = 4;
/**
 * Sticky-bottom, scrollable viewport for chat-like component stacks.
 *
 * Pi's main interactive chat gets terminal scrollback for free. Extension
 * overlays render into a fixed rectangle, so they need an explicit viewport
 * with the same sticky-bottom default plus keyboard and mouse history controls.
 */
export class ScrollableComponentViewport {
    constructor() {
        this.components = [];
        this.visibleRows = 1;
        this.scrollFromBottom = 0;
        this.lastLineCount = 0;
        this.lastWidth = 0;
        this.maxScroll = 0;
    }
    setComponents(components) {
        this.components = components;
    }
    setVisibleRows(rows) {
        this.visibleRows = Math.max(1, Math.floor(rows));
        this.clampScroll();
    }
    getScrollFromBottom() {
        return this.scrollFromBottom;
    }
    getMaxScroll() {
        return this.maxScroll;
    }
    scrollToBottom() {
        this.scrollFromBottom = 0;
    }
    scrollToTop() {
        this.scrollFromBottom = this.maxScroll;
    }
    scrollBy(deltaRows) {
        // Positive deltas move toward newer content; negative deltas move up
        // into older history. Store the offset from the sticky bottom so new
        // streaming output can keep following when the offset is zero.
        this.scrollFromBottom -= deltaRows;
        this.clampScroll();
    }
    handleInput(data) {
        const wheelDeltaRows = mouseWheelDeltaRows(data);
        if (wheelDeltaRows !== 0) {
            this.scrollBy(wheelDeltaRows);
            return true;
        }
        if (isMouseSequence(data))
            return true;
        if (matchesKey(data, "pageUp")) {
            this.scrollBy(-this.pageSize());
            return true;
        }
        if (matchesKey(data, "pageDown")) {
            this.scrollBy(this.pageSize());
            return true;
        }
        if (matchesKey(data, "home")) {
            this.scrollToTop();
            return true;
        }
        if (matchesKey(data, "end")) {
            this.scrollToBottom();
            return true;
        }
        return false;
    }
    render(width) {
        const allLines = this.components.flatMap((component) => component.render(width));
        const maxScroll = Math.max(0, allLines.length - this.visibleRows);
        if (this.scrollFromBottom > 0 && this.lastWidth === width && allLines.length > this.lastLineCount) {
            this.scrollFromBottom += allLines.length - this.lastLineCount;
        }
        this.lastLineCount = allLines.length;
        this.lastWidth = width;
        this.maxScroll = maxScroll;
        this.clampScroll();
        const start = Math.max(0, maxScroll - this.scrollFromBottom);
        const visible = allLines.slice(start, start + this.visibleRows);
        while (visible.length < this.visibleRows)
            visible.push(" ".repeat(width));
        return visible;
    }
    invalidate() {
        for (const component of this.components)
            component.invalidate();
    }
    pageSize() {
        return Math.max(4, this.visibleRows - 2);
    }
    clampScroll() {
        this.scrollFromBottom = Math.max(0, Math.min(this.maxScroll, this.scrollFromBottom));
    }
}
export class ScrollableChatTranscriptComponent {
    constructor(entries, renderEntry) {
        this.viewport = new ScrollableComponentViewport();
        this.transcript = new ChatTranscriptComponent(entries, renderEntry);
        this.viewport.setComponents([this.transcript]);
    }
    setVisibleRows(rows) {
        this.viewport.setVisibleRows(rows);
    }
    handleInput(data) {
        return this.viewport.handleInput(data);
    }
    render(width) {
        return this.viewport.render(width);
    }
    invalidate() {
        this.viewport.invalidate();
    }
    getScrollFromBottom() {
        return this.viewport.getScrollFromBottom();
    }
    getMaxScroll() {
        return this.viewport.getMaxScroll();
    }
    scrollToBottom() {
        this.viewport.scrollToBottom();
    }
}
function mouseWheelDeltaRows(data) {
    const sgr = data.match(/^\x1b\[<(\d+);\d+;\d+M$/);
    if (sgr)
        return wheelDeltaForButtonCode(Number.parseInt(sgr[1], 10));
    if (data.startsWith("\x1b[M") && data.length >= 6) {
        return wheelDeltaForButtonCode(data.charCodeAt(3) - 32);
    }
    return 0;
}
function wheelDeltaForButtonCode(code) {
    if ((code & 64) === 0)
        return 0;
    const direction = code & 3;
    if (direction === 0)
        return -DEFAULT_SCROLL_STEP_ROWS;
    if (direction === 1)
        return DEFAULT_SCROLL_STEP_ROWS;
    return 0;
}
function isMouseSequence(data) {
    return /^\x1b\[<\d+;\d+;\d+[mM]$/.test(data) || data.startsWith("\x1b[M");
}
//# sourceMappingURL=chat-transcript.js.map