import { STEERING_PREFIX } from "./steering.js";
import { AUTOPILOT_INJECTION_PREAMBLE } from "./autopilot-cycle.js";
/**
 * Move a transcript anchor (`afterMessageCount`) from the pre-compaction
 * message list onto the compacted one.
 *
 * Compaction folds the leading `summarizedCount` non-system messages into a
 * `prefixCount`-message summary block and keeps the rest verbatim. Anchors
 * inside the collapsed region land right after the summary (their surrounding
 * conversation no longer exists); anchors in the retained tail shift by the
 * difference. Without this, re-persisted markers keep indices from a much
 * longer transcript — they then replay far too late, or past the end, which is
 * what bunches old Ken bubbles and error rows at the bottom on resume.
 */
export function remapAnchorForCompaction(anchor, remap) {
    const moved = anchor <= remap.summarizedCount
        ? Math.min(anchor, remap.prefixCount)
        : anchor - remap.summarizedCount + remap.prefixCount;
    // The retained tail can be shorter than the collapse implies (tool-pairing
    // repair, trailing-assistant pop), so clamp to the real end of the new list.
    return Math.min(moved, remap.newNonSystemCount);
}
/**
 * Drop the read-only file-order position before writing a marker back to disk.
 * It describes where a marker sat in the file it was READ from, so persisting
 * it into a rewritten file would bake in a position that no longer applies —
 * the next load recomputes it anyway.
 */
export function stripRecordedPosition(payload) {
    const { recordedAfterMessageCount: _ignored, ...rest } = payload;
    return rest;
}
function markerKey(marker) {
    return JSON.stringify({
        phase: marker.phase,
        afterMessageCount: marker.afterMessageCount,
        reason: marker.reason ?? null,
        body: marker.body ?? null,
    });
}
export function autopilotMarkerCopySeed(marker) {
    return `${marker.phase}\0${marker.afterMessageCount}\0${marker.reason ?? ""}\0${marker.body ?? ""}`;
}
/**
 * Resolve the replay position for one persisted marker.
 *
 * A marker's anchor counts messages that were ALREADY on disk when it was
 * recorded, and markers are appended after those messages — so a correctly
 * written marker can never sit at a file position lower than its own anchor.
 * File order is therefore a hard ceiling, and an anchor above it is provably
 * stale. Two historical paths produced exactly that:
 *
 *  - compaction re-persisted markers into the continuation file while they
 *    still carried indices from the much longer pre-compaction transcript;
 *  - older sessions anchored against the in-memory message list, which runs
 *    ahead of the file whenever a run fails before its messages are flushed.
 *
 * Both make markers replay far too late — bunched at the bottom of a reopened
 * session, or dropped entirely when the anchor overshoots the end. Clamping to
 * the file position fixes both. The clamp only ever moves an anchor DOWN;
 * markers written before their messages landed (fire-and-forget persistence)
 * legitimately sit below their file position and are left alone.
 *
 * `ceilingOffset` covers markers deliberately anchored ahead of the file — the
 * user_hint that attaches to the user message a prompt is about to push.
 * Returns null when even the clamped anchor falls outside the transcript.
 */
function resolveAnchor(marker, maxAfterMessageCount, ceilingOffset = 0) {
    const ceiling = marker.recordedAfterMessageCount === undefined
        ? Number.POSITIVE_INFINITY
        : marker.recordedAfterMessageCount + ceilingOffset;
    const anchor = Math.min(marker.afterMessageCount, ceiling);
    return anchor <= maxAfterMessageCount ? anchor : null;
}
/**
 * Normalize persisted autopilot markers for transcript replay.
 *
 * Stale anchors are pulled back to where the marker was actually written (see
 * {@link resolveAnchor}); anything still outside the restored transcript is
 * dropped rather than replayed at EOF, which is what bunched old Ken all-clear
 * bubbles at the bottom. Exact duplicate payloads from old rewrite/re-persist
 * paths are deduped.
 */
export function normalizeAutopilotMarkersForHistory(markers, maxAfterMessageCount) {
    const seen = new Set();
    const normalized = [];
    for (const marker of markers) {
        const anchor = resolveAnchor(marker, maxAfterMessageCount);
        if (anchor === null)
            continue;
        const key = markerKey(marker);
        if (seen.has(key))
            continue;
        seen.add(key);
        // The copy seed stays tied to the PERSISTED anchor so the all-clear wording
        // a resumed session shows matches the one the live run broadcast.
        normalized.push({
            ...marker,
            afterMessageCount: anchor,
            copySeed: autopilotMarkerCopySeed(marker),
        });
    }
    return normalized;
}
/**
 * Normalize persisted app transcript markers for replay: drop markers whose
 * anchor points beyond the restored message list (stale after compaction) and
 * dedupe exact payloads (old rewrite paths could re-append). Mirrors
 * {@link normalizeAutopilotMarkersForHistory}.
 */
export function normalizeAppMarkersForHistory(markers, maxAfterMessageCount) {
    const seen = new Set();
    const normalized = [];
    for (const marker of markers) {
        // user_hint is anchored +1 on purpose so it decorates the user row a prompt
        // is about to add; everything else must not exceed its file position.
        const anchor = resolveAnchor(marker, maxAfterMessageCount, marker.kind === "user_hint" ? 1 : 0);
        if (anchor === null)
            continue;
        const key = JSON.stringify({
            kind: marker.kind,
            afterMessageCount: marker.afterMessageCount,
            data: marker.data,
        });
        if (seen.has(key))
            continue;
        seen.add(key);
        normalized.push(anchor === marker.afterMessageCount ? marker : { ...marker, afterMessageCount: anchor });
    }
    return normalized;
}
/**
 * Normalize persisted Ken turns for replay. Unlike autopilot/app markers, Ken
 * turns carry real conversation — an out-of-range anchor (stale after
 * compaction) is CLAMPED to the last message instead of dropped, so the
 * exchange still renders (at the end, in order) rather than vanishing. Exact
 * duplicate payloads (old rewrite/re-persist paths) are deduped.
 */
export function normalizeKenTurnsForHistory(turns, maxAfterMessageCount) {
    const seen = new Set();
    const normalized = [];
    for (const turn of turns) {
        const key = JSON.stringify({ q: turn.question, r: turn.reply, a: turn.afterMessageCount });
        if (seen.has(key))
            continue;
        seen.add(key);
        // File order first, EOF clamp only as a last resort — clamping is exactly
        // what stacked resumed Ken exchanges at the bottom of the transcript.
        const anchor = resolveAnchor(turn, maxAfterMessageCount) ?? maxAfterMessageCount;
        normalized.push(anchor === turn.afterMessageCount ? turn : { ...turn, afterMessageCount: anchor });
    }
    return normalized;
}
// ── User-row reconstruction ────────────────────────────────────────
// The persisted user message is what the MODEL saw — steering wrapper, saved-
// path notes, attached-files block. The live bubble showed only the typed
// text, so resume must strip the machine framing back out.
/** Separator AgentSession.buildAttachmentParts uses before the file-notes
 *  block appended to a user message's leading text part. */
const ATTACHED_FILES_HEADER = "Attached files (inspect with your tools):";
/** Attachment-note text parts injected alongside the typed text. These never
 *  appeared in the live user bubble. */
const ATTACHMENT_NOTE_PATTERNS = [
    /^\[Image saved at .+\]$/s,
    /^The user attached a video at .+/s,
    /^\[User attached a video file .+\]$/s,
];
function stripSteering(text) {
    return text.startsWith(STEERING_PREFIX) ? text.slice(STEERING_PREFIX.length) : text;
}
/** Strip the autopilot situational-awareness preamble that the sidecar prepends
 *  to every autopilot-injected build-session run (see frameAutopilotInjection).
 *  The live transcript shows the clean body via the autopilot "prompted" marker;
 *  on resume the raw session message must render the same clean instruction, not
 *  the machine-facing preamble. */
function stripAutopilotPreamble(text) {
    return hasAutopilotPreamble(text)
        ? text.slice(AUTOPILOT_INJECTION_PREAMBLE.length).trimStart()
        : text;
}
/** Whether a persisted user message was injected by autopilot rather than typed
 *  by a human. frameAutopilotInjection adds this preamble to every injected
 *  build run, and nothing else produces it. */
function hasAutopilotPreamble(text) {
    return text.startsWith(AUTOPILOT_INJECTION_PREAMBLE);
}
function stripAttachedFilesBlock(text) {
    if (text.startsWith(ATTACHED_FILES_HEADER))
        return "";
    const idx = text.indexOf(`\n\n${ATTACHED_FILES_HEADER}`);
    return idx === -1 ? text : text.slice(0, idx);
}
/** Rebuild the live user bubble from a persisted user message's content. */
export function restoreUserRow(content) {
    if (typeof content === "string") {
        const unsteered = stripSteering(content);
        return {
            text: stripAttachedFilesBlock(stripAutopilotPreamble(unsteered)).trim(),
            images: [],
            videoWarning: false,
            autopilotInjected: hasAutopilotPreamble(unsteered),
        };
    }
    const images = [];
    const textParts = [];
    let videoWarning = false;
    let autopilotInjected = false;
    for (const c of content) {
        if (c.type === "image") {
            images.push(`data:${c.mediaType};base64,${c.data}`);
            continue;
        }
        if (c.type !== "text" || typeof c.text !== "string")
            continue;
        const unsteered = stripSteering(c.text);
        if (hasAutopilotPreamble(unsteered))
            autopilotInjected = true;
        const stripped = stripAutopilotPreamble(unsteered);
        if (stripped.startsWith("[User attached a video file"))
            videoWarning = true;
        if (ATTACHMENT_NOTE_PATTERNS.some((re) => re.test(stripped)))
            continue;
        const cleaned = stripAttachedFilesBlock(stripped);
        if (cleaned.trim())
            textParts.push(cleaned);
    }
    return { text: textParts.join("\n\n").trim(), images, videoWarning, autopilotInjected };
}
// ── Slash-command reconstruction ──────────────────────────────
// A `/name` command is expanded before it reaches the model, so the persisted
// user message is the FULL template body — not the short chip the user saw.
/** Separator AgentSession.prompt() inserts between a command's prompt body and
 *  the user's trailing args. Must stay in sync with the expansion there. */
const COMMAND_ARGS_SEP = "\n\n## User Instructions\n\n";
/**
 * Reverse a prompt-template command's expansion by matching the restored body
 * against the known templates.
 *
 * Best-effort only: it works while a template is byte-identical to the one that
 * produced the message, and templates drift (edited `.gg/commands/*.md`,
 * reworded built-ins, app-vs-CLI phrasing). Prefer the invocation recorded at
 * send time — see {@link resolveRestoredCommand}. Returns null when the text
 * isn't a known command body (an ordinary user message).
 */
export function detectPromptCommand(text, candidates) {
    for (const c of candidates) {
        if (!c.prompt)
            continue;
        if (text === c.prompt)
            return `/${c.name}`;
        if (text.startsWith(c.prompt + COMMAND_ARGS_SEP)) {
            const args = text.slice(c.prompt.length + COMMAND_ARGS_SEP.length).trim();
            return args ? `/${c.name} ${args}` : `/${c.name}`;
        }
    }
    return null;
}
/**
 * The `/name [args]` chip a restored user row should show, or null for an
 * ordinary message.
 *
 * The invocation persisted with the prompt wins: it's exactly what the user
 * typed and survives any later template edit. Sessions recorded before that was
 * persisted fall back to matching the expanded body, which is why an edited
 * command used to resume as its raw multi-KB template.
 */
export function resolveRestoredCommand(persistedCommand, text, candidates) {
    if (typeof persistedCommand === "string" && persistedCommand.trim()) {
        return persistedCommand.trim();
    }
    return detectPromptCommand(text, candidates);
}
/**
 * Split a persisted assistant message into per-bubble texts. Live streaming
 * ends the assistant bubble at every server_tool_call (see useAgentEvents'
 * server_tool_call case), so pre- and post-tool text render as separate rows.
 * Persisted content keeps that structure as separate text blocks — emit one
 * text per block instead of gluing them into a single row on resume.
 */
export function restoreAssistantTexts(content) {
    if (typeof content === "string")
        return content.trim() ? [content] : [];
    return content.flatMap((c) => c.type === "text" && typeof c.text === "string" && c.text.trim() ? [c.text] : []);
}
//# sourceMappingURL=session-history.js.map