import { canonicalizeImagePaths } from './media-path.js';
import { cleanUserText, isAssistantReflectionText, isPassiveStatusChatLine, parseUserImagePaths, stripJsonlRedactionArtifacts, } from './message-filter.js';
function normalizeUserChatMessage(m) {
    const images = canonicalizeImagePaths([
        ...(m.images ?? []),
        ...parseUserImagePaths(m.text ?? ''),
    ]);
    const text = cleanUserText(m.text ?? '');
    return {
        ...m,
        text,
        images: images.length ? images : undefined,
    };
}
function normalizeMessage(m) {
    return m.role === 'user' ? normalizeUserChatMessage(m) : m;
}
function normalizeAll(messages) {
    return messages.map(normalizeMessage);
}
export function compareUserText(text) {
    return cleanUserText(text).replace(/\s+/g, ' ').trim();
}
function userImagePaths(m) {
    return canonicalizeImagePaths([
        ...(m.images ?? []),
        ...parseUserImagePaths(m.text ?? ''),
    ]);
}
function userImageSetsEquivalent(a, b) {
    const pa = userImagePaths(a);
    const pb = userImagePaths(b);
    if (!pa.length || !pb.length)
        return false;
    if (pa.length !== pb.length)
        return false;
    const setB = new Set(pb);
    return pa.every((p) => setB.has(p));
}
function sharedTextPrefixLen(a, b) {
    const n = Math.min(a.length, b.length);
    let i = 0;
    while (i < n && a[i] === b[i])
        i++;
    return i;
}
/** Same user intent when JSONL cites `docs/regression-agent/…` and DOM shows `PROMPT.md`. */
function isRegressionRerunUserRequest(text) {
    const n = compareUserText(text).toLowerCase();
    if (!n.includes('повторн') && !/regress/i.test(n))
        return false;
    return /prompt\.md|regression-agent|testcases\.md|docs\/regress|prompt\./i.test(n);
}
export function userTextsEquivalent(ta, tb) {
    const a = compareUserText(ta);
    const b = compareUserText(tb);
    if (!a || !b)
        return !a && !b;
    if (a === b)
        return true;
    if (isRegressionRerunUserRequest(a) &&
        isRegressionRerunUserRequest(b) &&
        sharedTextPrefixLen(a, b) >= 36) {
        return true;
    }
    const short = a.length <= b.length ? a : b;
    const long = a.length <= b.length ? b : a;
    if (short.length < 12)
        return false;
    if (long.includes(short)) {
        // Новый user_query, цитирующий старый внутри длинного текста — не дедупить.
        if (long.length > short.length * 1.2 &&
            /<user_query>/i.test(long) &&
            !long.startsWith(short) &&
            !long.endsWith(short)) {
            return false;
        }
        if (long.endsWith(short) || long.startsWith(short))
            return true;
        if (short.length / long.length >= 0.4)
            return true;
    }
    return false;
}
export function userMessagesEquivalent(a, b) {
    if (a.role !== 'user' || b.role !== 'user')
        return false;
    const ta = compareUserText(a.text ?? '');
    const tb = compareUserText(b.text ?? '');
    if (ta && tb && userTextsEquivalent(ta, tb))
        return true;
    if (userImageSetsEquivalent(a, b))
        return true;
    const pa = userImagePaths(a);
    const pb = userImagePaths(b);
    if (ta && tb && pa.length > 0 && pb.length > 0) {
        const setB = new Set(pb);
        if (pa.every((p) => setB.has(p)))
            return true;
    }
    return false;
}
function pickPreferredUserMessage(a, b, preferB) {
    const na = normalizeUserChatMessage(a);
    const nb = normalizeUserChatMessage(b);
    const score = (m) => {
        let s = 0;
        const nImg = m.images?.length ?? 0;
        if (nImg)
            s += 20 + nImg;
        if (!/<image_files>/i.test(m.text ?? ''))
            s += 40;
        if ((m.text ?? '').trim())
            s += 5;
        if (!m.id.startsWith('hist-'))
            s += 8;
        return s;
    };
    const sa = score(na);
    const sb = score(nb);
    let pick = sa > sb ? na : sb > sa ? nb : preferB ? nb : na;
    const other = pick === na ? nb : na;
    if ((pick.text?.length ?? 0) < (other.text?.length ?? 0))
        pick = other;
    const images = canonicalizeImagePaths([
        ...(pick.images ?? []),
        ...(other.images ?? []),
        ...parseUserImagePaths(pick.text ?? ''),
        ...parseUserImagePaths(other.text ?? ''),
    ]);
    const flatPick = Math.min(a.flatIndex ?? Number.MAX_SAFE_INTEGER, b.flatIndex ?? Number.MAX_SAFE_INTEGER);
    const id = !pick.id.startsWith('hist-')
        ? pick.id
        : !other.id.startsWith('hist-')
            ? other.id
            : pick.id;
    return {
        ...pick,
        id,
        images: images.length ? images : undefined,
        flatIndex: Number.isFinite(flatPick) ? flatPick : pick.flatIndex ?? other.flatIndex,
    };
}
/** DOM textContent vs JSONL markdown — same turn, different surface form. */
function normalizeAssistantCompareText(text) {
    return text
        .replace(/[\u2013\u2014\u2212]/g, '-')
        .replace(/\*\*/g, '')
        .replace(/`+/g, '')
        .replace(/^#+\s*/gm, '')
        .replace(/\|/g, ' ')
        .replace(/[-]{3,}/g, ' ')
        .replace(/\s+/g, ' ')
        .trim()
        .toLowerCase();
}
function wordBagOverlapRatio(textA, textB) {
    const wa = new Set((textA.match(/[\p{L}\p{N}]{3,}/gu) ?? []).map((w) => w.toLowerCase()));
    const wb = new Set((textB.match(/[\p{L}\p{N}]{3,}/gu) ?? []).map((w) => w.toLowerCase()));
    if (!wa.size || !wb.size)
        return 0;
    let shared = 0;
    for (const w of wa) {
        if (wb.has(w))
            shared++;
    }
    return shared / Math.min(wa.size, wb.size);
}
function assistantTextsEquivalent(a, b) {
    if (a.role !== 'assistant' || b.role !== 'assistant')
        return false;
    const ta = normalizeAssistantCompareText(a.text ?? '');
    const tb = normalizeAssistantCompareText(b.text ?? '');
    if (!ta || !tb)
        return false;
    if (ta === tb)
        return true;
    const short = ta.length <= tb.length ? ta : tb;
    const long = ta.length <= tb.length ? tb : ta;
    if (short.length < 20)
        return false;
    if (!long.includes(short))
        return false;
    if (long.startsWith(short) && short.length >= 32)
        return true;
    return short.length / Math.min(long.length, 1200) >= 0.35;
}
/** Same idea as `/debug/chats` compare — DOM textContent vs JSONL markdown. */
function compactOverlayMatchText(text, role) {
    const base = role === 'assistant' ? normalizeAssistantCompareText(text) : compareUserText(text);
    return base.replace(/\s/g, '');
}
function overlaySurfaceText(m) {
    const t = (m.text ?? '').trim();
    if (t)
        return t;
    return (m.html ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}
function compactTextsMatchArchiveOverlay(archive, overlay) {
    const role = archive.role;
    const ta = compactOverlayMatchText(overlaySurfaceText(archive), role);
    const tb = compactOverlayMatchText(overlaySurfaceText(overlay), role);
    if (!ta || !tb)
        return false;
    if (ta === tb)
        return true;
    const short = ta.length <= tb.length ? ta : tb;
    const long = ta.length <= tb.length ? tb : ta;
    if (short.length < 24)
        return false;
    if (long.startsWith(short))
        return true;
    if (long.includes(short) && short.length / Math.min(long.length, 1600) >= 0.28) {
        return true;
    }
    return false;
}
function assistantArchiveCoversOverlay(archive, overlay) {
    if (assistantTextsEquivalent(archive, overlay) || messagesEquivalent(archive, overlay)) {
        return true;
    }
    if (compactTextsMatchArchiveOverlay(archive, overlay))
        return true;
    const ta = normalizeAssistantCompareText(overlaySurfaceText(archive));
    const tb = normalizeAssistantCompareText(overlaySurfaceText(overlay));
    if (!ta || !tb)
        return false;
    const head = 72;
    if (ta.slice(0, head) === tb.slice(0, head) && head >= 40)
        return true;
    const short = ta.length <= tb.length ? ta : tb;
    const long = ta.length <= tb.length ? tb : ta;
    if (short.length >= 48 && long.includes(short))
        return true;
    // DOM textContent vs JSONL markdown: таблицы/тире дают 1–2 отличия на тысячи символов.
    if (ta.length >= 120 && tb.length >= 120) {
        if (wordBagOverlapRatio(ta, tb) >= 0.88)
            return true;
        if (wordBagOverlapRatio(ta.slice(0, 420), tb.slice(0, 420)) >= 0.92 &&
            ta.slice(0, 56) === tb.slice(0, 56)) {
            return true;
        }
    }
    return false;
}
/**
 * JSONL `flatIndex` = file line number (~1..N); Cursor DOM = `data-flat-index` (often 1000+).
 * When DOM is ahead of the archive tail, fuzzy cover hid live rows (debug: NEEDS_DOM_OVERLAY, overlay 0).
 */
export const JSONL_DOM_ORDER_GAP = 32;
export function maxArchiveOrderKey(archive) {
    let max = 0;
    for (const m of archive)
        max = Math.max(max, messageOrderKey(m));
    return max;
}
function isCursorDomOverlayRow(overlay) {
    const id = overlay.id ?? '';
    if (id.startsWith('hist-'))
        return false;
    const flat = overlay.flatIndex;
    return flat != null && Number.isFinite(flat) && flat >= 64;
}
export function isDomOrderAheadOfJsonlArchive(overlay, maxArchiveKey) {
    if (!isCursorDomOverlayRow(overlay))
        return false;
    return (overlay.flatIndex ?? 0) > maxArchiveKey + JSONL_DOM_ORDER_GAP;
}
/** Exact / near-exact match only — no word-bag (safe when DOM timeline > JSONL tail). */
export function archiveCoversOverlayStrict(archive, overlay) {
    if (archive.role !== overlay.role)
        return false;
    if (archive.role === 'user') {
        if (compactTextsMatchArchiveOverlay(archive, overlay))
            return true;
        return userMessagesEquivalent(archive, overlay);
    }
    if (archive.role === 'assistant') {
        return (assistantTextsEquivalent(archive, overlay) ||
            messagesEquivalent(archive, overlay) ||
            compactTextsMatchArchiveOverlay(archive, overlay));
    }
    return messagesEquivalent(archive, overlay);
}
/** Fuzzy match (markdown vs DOM textContent, word-bag, prefixes). */
function archiveCoversOverlayFuzzy(archive, overlay) {
    if (archive.role !== overlay.role)
        return false;
    if (archive.role === 'user') {
        if (userMessagesEquivalent(archive, overlay))
            return true;
        if (compactTextsMatchArchiveOverlay(archive, overlay))
            return true;
        const a = compareUserText(overlaySurfaceText(archive));
        const b = compareUserText(overlaySurfaceText(overlay));
        if (!a || !b)
            return false;
        if (a === b)
            return true;
        const short = a.length <= b.length ? a : b;
        const long = a.length <= b.length ? b : a;
        return short.length >= 24 && long.includes(short);
    }
    if (archive.role === 'assistant') {
        return assistantArchiveCoversOverlay(archive, overlay);
    }
    return messagesEquivalent(archive, overlay);
}
/**
 * When DOM flat-index is ahead of JSONL line keys, allow fuzzy dedupe once JSONL caught up,
 * but keep overlay if DOM body is still growing (streaming).
 */
const DOM_AHEAD_STREAM_MARGIN = { assistant: 48, user: 24 };
function overlayStillStreamingPastArchive(archive, overlay) {
    const aLen = (archive.text ?? '').trim().length;
    const oLen = (overlay.text ?? '').trim().length;
    if (!aLen || !oLen)
        return false;
    const margin = overlay.role === 'user' ? DOM_AHEAD_STREAM_MARGIN.user : DOM_AHEAD_STREAM_MARGIN.assistant;
    return oLen > aLen + margin;
}
/**
 * Viewport rows for the live tail: not yet represented in JSONL archive.
 * Uses `archiveCoversOverlay` (strict / fuzzy / streaming) — same rule for idle and generating.
 */
export function selectDomOverlayFromViewport(viewport, archive) {
    if (!viewport.length)
        return [];
    if (!archive.length)
        return viewport;
    const maxArchiveKey = maxArchiveOrderKey(archive);
    return viewport.filter((d) => !archive.some((h) => archiveCoversOverlay(h, d, { maxArchiveKey })));
}
/** JSONL archive covers a DOM overlay row (DOM may be a truncated viewport). */
export function archiveCoversOverlay(archive, overlay, opts) {
    if (archiveCoversOverlayStrict(archive, overlay))
        return true;
    const maxKey = opts?.maxArchiveKey;
    if (maxKey != null && isDomOrderAheadOfJsonlArchive(overlay, maxKey)) {
        if (!archiveCoversOverlayFuzzy(archive, overlay))
            return false;
        if (overlayStillStreamingPastArchive(archive, overlay))
            return false;
        return true;
    }
    return archiveCoversOverlayFuzzy(archive, overlay);
}
function normalizeTodoKey(text) {
    return text.trim().replace(/\s+/g, ' ').toLowerCase();
}
function mergePlanBlocks(a, b) {
    if (!a)
        return b;
    if (!b)
        return a;
    const statusRank = (s) => s === 'completed' ? 3 : s === 'in_progress' ? 2 : s === 'cancelled' ? 1 : 0;
    const byKey = new Map();
    for (const t of [...a.todos, ...b.todos]) {
        const key = t.id || normalizeTodoKey(t.text);
        const prev = byKey.get(key);
        if (!prev || statusRank(t.status) > statusRank(prev.status))
            byKey.set(key, t);
    }
    const mergedTodos = [...byKey.values()];
    const completedCount = mergedTodos.filter((t) => t.status === 'completed').length;
    return {
        buildTitle: b.buildTitle || a.buildTitle,
        todos: mergedTodos,
        completedCount: Math.max(a.completedCount ?? 0, b.completedCount ?? 0, completedCount),
        totalCount: Math.max(a.totalCount ?? 0, b.totalCount ?? 0, mergedTodos.length),
    };
}
/** Apply live composer todo statuses to archived CreatePlan cards (JSONL has no status). */
export function enrichPlanBlocksFromTodos(messages, liveTodos) {
    if (!liveTodos.length)
        return messages;
    return messages.map((m) => {
        if (!m.planBlock?.todos.length)
            return m;
        const todos = m.planBlock.todos.map((pt) => {
            const live = liveTodos.find((lt) => (pt.id && lt.id === pt.id) ||
                normalizeTodoKey(lt.text) === normalizeTodoKey(pt.text) ||
                normalizeTodoKey(lt.text).startsWith(normalizeTodoKey(pt.text).slice(0, 48)));
            return live ? { ...pt, status: live.status } : pt;
        });
        const completedCount = todos.filter((t) => t.status === 'completed').length;
        return {
            ...m,
            planBlock: {
                ...m.planBlock,
                todos,
                completedCount,
                totalCount: m.planBlock.totalCount ?? todos.length,
            },
        };
    });
}
function messageKey(m) {
    const n = normalizeMessage(m);
    if (n.role === 'user') {
        return `user:${compareUserText(n.text ?? '')}:${(n.images ?? []).join('|')}`;
    }
    const html = n.html?.trim() ? '1' : '0';
    const planSig = n.planBlock
        ? `plan:${n.planBlock.buildTitle}:${n.planBlock.todos.map((t) => t.id).join(',')}`
        : '';
    return `${n.role}:${n.text.trim().replace(/\s+/g, ' ')}:${(n.images ?? []).join('|')}:${html}:${planSig}`;
}
export function messagesEquivalent(a, b) {
    if (a.role === 'user' && b.role === 'user')
        return userMessagesEquivalent(a, b);
    if (a.role === 'assistant' && b.role === 'assistant') {
        return assistantTextsEquivalent(a, b) || messageKey(a) === messageKey(b);
    }
    return messageKey(a) === messageKey(b);
}
function mergeTimelineFields(prev, next, pick) {
    const indices = [prev.flatIndex, next.flatIndex, pick.flatIndex].filter((n) => n != null && Number.isFinite(n));
    const domSeqs = [prev.domSeq, next.domSeq, pick.domSeq].filter((n) => n != null && Number.isFinite(n));
    return {
        ...pick,
        flatIndex: indices.length ? Math.min(...indices) : pick.flatIndex,
        domSeq: domSeqs.length ? Math.min(...domSeqs) : pick.domSeq,
    };
}
export function pickPreferredMessage(prev, next, preferNext) {
    if (prev.role === 'user' && next.role === 'user') {
        return pickPreferredUserMessage(prev, next, preferNext);
    }
    if (prev.role !== 'assistant' || next.role !== 'assistant') {
        const pick = preferNext ? next : prev;
        return mergeTimelineFields(prev, next, pick);
    }
    const score = (m) => {
        let s = 0;
        if (m.html?.trim())
            s += 30;
        if (m.planBlock?.todos.length)
            s += 15;
        s += Math.min(m.text?.length ?? 0, 5000) / 100;
        if (!m.id.startsWith('hist-'))
            s += 5;
        return s;
    };
    const sp = score(prev);
    const sn = score(next);
    let pick;
    if (sn > sp)
        pick = next;
    else if (sp > sn)
        pick = prev;
    else
        pick = preferNext ? next : prev;
    const merged = mergeTimelineFields(prev, next, pick);
    const planBlock = mergePlanBlocks(prev.planBlock, next.planBlock);
    return planBlock ? { ...merged, planBlock } : merged;
}
export function messageOrderKey(m) {
    if (m.flatIndex != null && Number.isFinite(m.flatIndex))
        return m.flatIndex;
    if (m.domSeq != null && Number.isFinite(m.domSeq))
        return m.domSeq;
    const hist = /^hist-(\d+)/.exec(m.id ?? '');
    if (hist)
        return Number(hist[1]);
    return 0;
}
/** DOM flat-index vs JSONL line ts — see app `anchorDomOverlaySortKeys`. */
export function anchorDomOverlaySortKeys(history, live) {
    if (!live.length)
        return live;
    let maxKey = -1;
    for (const m of history) {
        maxKey = Math.max(maxKey, messageOrderKey(m));
    }
    const sortedLive = sortMessagesChronologically(live);
    return sortedLive.map((m, i) => ({
        ...m,
        flatIndex: maxKey + 1 + i,
    }));
}
export function sortMessagesChronologically(messages) {
    return [...messages].sort((a, b) => {
        const ka = messageOrderKey(a);
        const kb = messageOrderKey(b);
        if (ka !== kb)
            return ka - kb;
        const da = a.domSeq ?? 0;
        const db = b.domSeq ?? 0;
        if (da !== db)
            return da - db;
        return a.id.localeCompare(b.id);
    });
}
function dedupeUserMessages(messages) {
    const sorted = sortMessagesChronologically(normalizeAll(messages));
    const out = [];
    for (const m of sorted) {
        if (m.role !== 'user') {
            out.push(m);
            continue;
        }
        const idx = out.findIndex((x) => x.role === 'user' && userMessagesEquivalent(x, m));
        if (idx >= 0) {
            out[idx] = pickPreferredUserMessage(out[idx], m, false);
        }
        else {
            out.push(normalizeUserChatMessage(m));
        }
    }
    return out;
}
/** JSONL+DOM compose order — do not re-sort by flatIndex before dedupe. */
export function dedupeUsersInComposeOrder(messages) {
    const out = [];
    for (const m of normalizeAll(messages)) {
        if (m.role !== 'user') {
            out.push(m);
            continue;
        }
        const idx = out.findIndex((x) => x.role === 'user' && userMessagesEquivalent(x, m));
        if (idx >= 0) {
            out[idx] = pickPreferredUserMessage(out[idx], m, false);
        }
        else {
            out.push(normalizeUserChatMessage(m));
        }
    }
    return out;
}
function assistantTailMatchesLongBody(tail, body) {
    const a = tail.trim();
    const b = body.trim();
    if (!a || !b || b.length <= a.length)
        return false;
    if (b.includes(a))
        return true;
    const words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
    const wa = words(a);
    const wb = words(b);
    let shared = 0;
    while (shared < wa.length && shared < wb.length && wa[shared] === wb[shared]) {
        shared++;
    }
    return shared >= 3 && b.length > a.length * 1.15;
}
/** Cursor may render one assistant turn as A₁, user, A₂ (tail + body); restore A₂ before user. */
function fixAssistantFragmentSandwich(messages) {
    const sorted = sortMessagesChronologically(normalizeAll(messages));
    const out = [];
    let i = 0;
    while (i < sorted.length) {
        if (i + 2 < sorted.length &&
            sorted[i].role === 'assistant' &&
            sorted[i + 1].role === 'user' &&
            sorted[i + 2].role === 'assistant') {
            const a1 = sorted[i];
            const u = sorted[i + 1];
            const a2 = sorted[i + 2];
            const t1 = (a1.text ?? '').trim();
            const t2 = (a2.text ?? '').trim();
            if (assistantTailMatchesLongBody(t1, t2)) {
                out.push(a2, u);
                i += 3;
                continue;
            }
        }
        out.push(sorted[i]);
        i++;
    }
    return out;
}
function assistantOneSupersedesOther(prev, next) {
    const prevPlan = (prev.planBlock?.todos.length ?? 0) > 0;
    const nextPlan = (next.planBlock?.todos.length ?? 0) > 0;
    if (prevPlan !== nextPlan)
        return false;
    if (assistantTextsEquivalent(prev, next))
        return true;
    const ta = (prev.text ?? '').trim();
    const tb = (next.text ?? '').trim();
    if (!ta || !tb)
        return false;
    if (assistantTailMatchesLongBody(ta, tb))
        return true;
    if (assistantTailMatchesLongBody(tb, ta))
        return true;
    const short = ta.length <= tb.length ? ta : tb;
    const long = ta.length <= tb.length ? tb : ta;
    if (short.length >= 40 && long.includes(short))
        return true;
    return false;
}
function collapseAssistantBursts(messages) {
    const sorted = sortMessagesChronologically(normalizeAll(messages));
    const out = [];
    let run = [];
    const flush = () => {
        if (!run.length)
            return;
        const visible = run.filter((m) => (m.planBlock?.todos.length ?? 0) > 0 ||
            !isAssistantReflectionText(m.text ?? ''));
        const pool = visible.length ? visible : run.filter((m) => (m.planBlock?.todos.length ?? 0) > 0);
        const merged = [];
        for (const m of pool) {
            const last = merged[merged.length - 1];
            if (last && assistantOneSupersedesOther(last, m)) {
                merged[merged.length - 1] = pickPreferredMessage(last, m, true);
            }
            else {
                merged.push(m);
            }
        }
        out.push(...merged);
        run = [];
    };
    for (const m of sorted) {
        if (m.role === 'assistant')
            run.push(m);
        else {
            flush();
            out.push(m);
        }
    }
    flush();
    return out;
}
export function prepareChatMessagesForDisplay(messages) {
    return collapseAssistantBursts(dedupeUserMessages(fixAssistantFragmentSandwich(messages)));
}
/** Tail overlap for JSONL incremental merge — user rows only on exact text (duplicate line in file). */
function tailOverlapForMerge(prev, incoming) {
    const max = Math.min(prev.length, incoming.length, 40);
    let overlap = 0;
    for (let n = 1; n <= max; n++) {
        const a = prev.slice(-n);
        const b = incoming.slice(0, n);
        if (a.every((m, i) => {
            const o = b[i];
            if (m.role === 'user' && o.role === 'user') {
                return compareUserText(m.text ?? '') === compareUserText(o.text ?? '');
            }
            return messagesEquivalent(m, o);
        })) {
            overlap = n;
        }
    }
    return overlap;
}
function tailOverlap(history, live) {
    const max = Math.min(history.length, live.length, 40);
    let overlap = 0;
    for (let n = 1; n <= max; n++) {
        const a = history.slice(-n);
        const b = live.slice(0, n);
        if (a.every((m, i) => messagesEquivalent(m, b[i])))
            overlap = n;
    }
    return overlap;
}
function mergeOverlappingBoundary(prev, incoming, overlap) {
    let picked = prev[prev.length - overlap];
    for (let i = 0; i < overlap; i++) {
        picked = pickPreferredMessage(picked, incoming[i], true);
    }
    return picked;
}
function upsertLiveMessage(merged, m, live, overlap, preferNext) {
    const tailWindow = Math.max(live.length - overlap, 0) + 6;
    const searchFrom = Math.max(0, merged.length - tailWindow);
    let dupIdx = -1;
    for (let j = merged.length - 1; j >= searchFrom; j--) {
        if (messagesEquivalent(merged[j], m)) {
            dupIdx = j;
            break;
        }
    }
    if (dupIdx >= 0)
        merged[dupIdx] = pickPreferredMessage(merged[dupIdx], m, preferNext);
    else
        merged.push(m);
}
export function mergeHistoryTail(prev, incoming) {
    if (!incoming.length)
        return prepareChatMessagesForDisplay(prev);
    if (!prev.length)
        return prepareChatMessagesForDisplay(incoming);
    const prevN = normalizeAll(prev);
    const inc = normalizeAll(incoming);
    const overlap = tailOverlapForMerge(prevN, inc);
    const prefix = prevN.slice(0, Math.max(0, prevN.length - overlap));
    const merged = [...prefix];
    if (overlap > 0) {
        upsertLiveMessage(merged, mergeOverlappingBoundary(prevN, inc, overlap), inc, overlap, true);
    }
    for (const m of inc.slice(overlap)) {
        upsertLiveMessage(merged, m, inc, overlap, true);
    }
    return prepareChatMessagesForDisplay(merged);
}
/** Document order inside one DOM extract — never Cursor viewport flatIndex alone. */
export function sortByDomDocumentOrder(messages) {
    return [...messages].sort((a, b) => {
        const da = a.domSeq ?? 0;
        const db = b.domSeq ?? 0;
        if (da !== db)
            return da - db;
        return (a.flatIndex ?? 0) - (b.flatIndex ?? 0);
    });
}
/** Force overlay keys above archive so any sort by flatIndex stays below JSONL tail. */
export function stampOverlayAfterArchive(archiveFloor, overlay) {
    let seq = Math.max(archiveFloor, 0);
    return overlay.map((m) => {
        seq += 1;
        return { ...m, flatIndex: seq };
    });
}
export function archiveOrderFloor(jsonlHistory, rawJsonlRows) {
    let floor = Math.max(rawJsonlRows, 0);
    for (const m of jsonlHistory) {
        floor = Math.max(floor, messageOrderKey(m));
    }
    return floor;
}
/** Dedupe/collapse overlay only; order = domSeq (then stamped flatIndex). */
export function prepareDomOverlayForDisplay(overlay) {
    if (!overlay.length)
        return [];
    return collapseAssistantBursts(dedupeUserMessages(sortByDomDocumentOrder(overlay)));
}
/**
 * Lenta for app: JSONL archive first, DOM overlay append-only.
 * Order = array order here; app must not re-sort by flatIndex across the join.
 */
export function composeDisplayLenta(jsonlHistory, domOverlay, liveTodos) {
    const base = !domOverlay.length
        ? [...jsonlHistory]
        : !jsonlHistory.length
            ? [...domOverlay]
            : dedupeUsersInComposeOrder([...jsonlHistory, ...domOverlay]);
    return liveTodos?.length ? enrichPlanBlocksFromTodos(base, liveTodos) : base;
}
/** Dev: warn if composed lenta inverts archive keys. */
export function assertMonotonicLenta(messages, label) {
    if (process.env.LENTA_ORDER_ASSERT !== '1')
        return;
    for (let i = 1; i < messages.length; i++) {
        const prev = messageOrderKey(messages[i - 1]);
        const cur = messageOrderKey(messages[i]);
        if (cur < prev) {
            console.warn(`[lenta-order] inversion ${label} i=${i} ${prev}>${cur}`);
        }
    }
}
/** @deprecated Use `composeDisplayLenta` — kept for tests/tools. */
export function mergeDomWithHistory(history, live) {
    const hist = prepareChatMessagesForDisplay(dedupeUserMessages(history));
    const collapsedLive = prepareChatMessagesForDisplay(collapseAssistantBursts(live));
    return composeDisplayLenta(hist, collapsedLive);
}
export function historyRowsToChat(rows) {
    return rows.map((m, i) => {
        const msg = {
            id: `hist-${m.ts ?? i}`,
            role: m.role,
            text: stripJsonlRedactionArtifacts(m.text),
            html: m.html,
            planBlock: m.planBlock,
            images: m.images?.length ? m.images : undefined,
            flatIndex: m.ts ?? i,
        };
        return m.role === 'user' ? normalizeUserChatMessage(msg) : msg;
    });
}
/** DOM sometimes attaches thumbnails to assistant rows; JSONL only has user images. */
function reassignAssistantImagesToUsers(messages) {
    const orphan = [];
    const stripped = messages.map((m) => {
        if (m.role !== 'assistant' || !(m.images?.length ?? 0))
            return m;
        orphan.push(...m.images);
        return { ...m, images: undefined };
    });
    if (!orphan.length)
        return stripped;
    const extra = canonicalizeImagePaths(orphan);
    for (let i = stripped.length - 1; i >= 0; i--) {
        const m = stripped[i];
        if (m.role !== 'user')
            continue;
        const merged = canonicalizeImagePaths([...userImagePaths(m), ...extra]);
        stripped[i] = normalizeUserChatMessage({
            ...m,
            images: merged.length ? merged : undefined,
        });
        break;
    }
    return stripped;
}
export function filterClientDisplayList(messages) {
    return reassignAssistantImagesToUsers(messages).filter((m) => {
        const text = stripJsonlRedactionArtifacts(m.text ?? '');
        const hasPlan = (m.planBlock?.todos.length ?? 0) > 0;
        if (!text && !(m.images?.length ?? 0) && !hasPlan)
            return false;
        if (m.role === 'assistant' && !hasPlan && isAssistantReflectionText(text))
            return false;
        if (m.role === 'assistant' && isPassiveStatusChatLine(text))
            return false;
        if (m.role === 'user') {
            return Boolean(text.trim()) || (m.images?.length ?? 0) > 0;
        }
        return true;
    });
}
export function userMessageCoversExisting(existing, sentText) {
    if (existing.role !== 'user')
        return false;
    return userTextsEquivalent(existing.text ?? '', sentText);
}
