diff --git a/hide-engine.js b/hide-engine.js index ba5e7c5..79e7289 100644 --- a/hide-engine.js +++ b/hide-engine.js @@ -1,11 +1,15 @@ -// ST-BME: 隐藏旧楼层引擎 -// 通过临时把旧楼层标记为 is_system=true,让宿主主回复与 ST-BME 自己的聊天读取一起跳过这些楼层。 +// ST-BME: old-message hide engine +// Uses the host's native /hide and /unhide slash commands instead of +// mutating chat messages into is_system=true. const hideState = { managedChatRef: null, - hiddenIndices: new Set(), + managedChatKey: null, + managedSystemIndices: new Set(), + hiddenRangeEnd: -1, lastProcessedLength: 0, scheduledTimer: null, + operationVersion: 0, }; function getTimerApi(runtime = {}) { @@ -21,22 +25,61 @@ function getTimerApi(runtime = {}) { }; } -function getJquery(runtime = {}) { - if (typeof runtime.$ === "function") return runtime.$; - if (typeof globalThis.$ === "function") return globalThis.$; - return null; -} - -function getCurrentChat(runtime = {}) { +function getCurrentContext(runtime = {}) { try { - const context = - typeof runtime.getContext === "function" ? runtime.getContext() : null; - return Array.isArray(context?.chat) ? context.chat : null; + return typeof runtime.getContext === "function" ? runtime.getContext() : null; } catch { return null; } } +function getCurrentChatInfo(runtime = {}) { + const context = getCurrentContext(runtime); + return { + chat: Array.isArray(context?.chat) ? context.chat : null, + chatId: + context?.chatId != null && context.chatId !== "" + ? String(context.chatId) + : null, + }; +} + +function getCurrentChatKey(runtime = {}) { + const { chat, chatId } = getCurrentChatInfo(runtime); + if (chatId) return chatId; + return Array.isArray(chat) ? chat : null; +} + +function getSlashExecutor(runtime = {}) { + if (typeof runtime.executeSlashCommands === "function") { + return runtime.executeSlashCommands.bind(runtime); + } + + const context = getCurrentContext(runtime); + if (typeof context?.executeSlashCommands === "function") { + return context.executeSlashCommands.bind(context); + } + + if (typeof globalThis.executeSlashCommands === "function") { + return globalThis.executeSlashCommands.bind(globalThis); + } + + if (typeof globalThis.executeSlashCommandsOnChatInput === "function") { + return globalThis.executeSlashCommandsOnChatInput.bind(globalThis); + } + + return null; +} + +async function executeSlashCommand(command, runtime = {}) { + const executor = getSlashExecutor(runtime); + if (!executor) { + throw new Error("executeSlashCommands is not available"); + } + + return await executor(command, true); +} + function normalizeHideSettings(settings = {}) { return { enabled: Boolean(settings.enabled), @@ -55,9 +98,19 @@ function normalizeHideSettings(settings = {}) { }; } +function getJquery(runtime = {}) { + if (typeof runtime.$ === "function") return runtime.$; + if (typeof globalThis.$ === "function") return globalThis.$; + return null; +} + function syncSystemAttribute(chat, indices = [], value = "true", runtime = {}) { - if (!Array.isArray(indices) || indices.length === 0) return; - if (getCurrentChat(runtime) !== chat) return; + if (!Array.isArray(chat) || !Array.isArray(indices) || indices.length === 0) { + return; + } + + const currentChat = getCurrentChatInfo(runtime).chat; + if (currentChat !== chat) return; const jq = getJquery(runtime); if (!jq) return; @@ -67,177 +120,267 @@ function syncSystemAttribute(chat, indices = [], value = "true", runtime = {}) { jq(selector).attr("is_system", value); } -function unhideTrackedChat(chat, runtime = {}) { - if (!Array.isArray(chat) || hideState.hiddenIndices.size === 0) { - return { shownCount: 0 }; - } - - const toShow = []; - for (const index of hideState.hiddenIndices) { - const message = chat[index]; - if (!message || message.is_system !== true) continue; - message.is_system = false; - toShow.push(index); - } - - syncSystemAttribute(chat, toShow, "false", runtime); - return { shownCount: toShow.length }; -} - -function swapManagedChat(nextChat, runtime = {}) { - const previousChat = hideState.managedChatRef; - if (previousChat && previousChat !== nextChat) { - unhideTrackedChat(previousChat, runtime); - hideState.hiddenIndices.clear(); - hideState.lastProcessedLength = 0; - } - hideState.managedChatRef = nextChat; -} - -export function runFullHideCheck(settings = {}, runtime = {}) { - const normalized = normalizeHideSettings(settings); - const chat = getCurrentChat(runtime); - if (!chat || chat.length === 0) { - resetHideState(runtime); - return { - active: false, - hiddenCount: 0, - shownCount: 0, - managedCount: 0, - chatLength: 0, - }; - } - - swapManagedChat(chat, runtime); - - if (!normalized.enabled || normalized.hideLastN <= 0) { - const { shownCount } = unhideTrackedChat(chat, runtime); - hideState.hiddenIndices.clear(); - hideState.lastProcessedLength = chat.length; - return { - active: false, - hiddenCount: 0, - shownCount, - managedCount: 0, - chatLength: chat.length, - }; +function calcHideRange(chatLength, hideLastN) { + if (!Number.isFinite(chatLength) || chatLength <= 0 || hideLastN <= 0) { + return null; } const visibleStart = - normalized.hideLastN >= chat.length - ? 0 - : Math.max(0, chat.length - normalized.hideLastN); - const desiredHiddenIndices = new Set(); - const managedHiddenIndices = new Set(); - const toHide = []; - const toShow = []; - - for (let index = 0; index < chat.length; index++) { - const message = chat[index]; - if (!message) continue; - - const shouldHide = index < visibleStart; - const isHidden = message.is_system === true; - const wasHiddenByBme = hideState.hiddenIndices.has(index); - - if (shouldHide) { - desiredHiddenIndices.add(index); - if (wasHiddenByBme || !isHidden) { - managedHiddenIndices.add(index); - } - if (!isHidden) { - message.is_system = true; - toHide.push(index); - } - continue; - } - - if (isHidden && wasHiddenByBme) { - message.is_system = false; - toShow.push(index); - } - } - - syncSystemAttribute(chat, [...desiredHiddenIndices], "true", runtime); - syncSystemAttribute(chat, toShow, "false", runtime); - - hideState.hiddenIndices = managedHiddenIndices; - hideState.lastProcessedLength = chat.length; + hideLastN >= chatLength ? 0 : Math.max(0, chatLength - hideLastN); + const hideEnd = visibleStart - 1; + if (hideEnd < 0) return null; return { - active: true, - hiddenCount: toHide.length, - shownCount: toShow.length, - managedCount: managedHiddenIndices.size, - chatLength: chat.length, + start: 0, + end: hideEnd, }; } -export function runIncrementalHideCheck(settings = {}, runtime = {}) { +function beginOperation() { + hideState.operationVersion += 1; + return hideState.operationVersion; +} + +function isOperationCurrent(version) { + return version === hideState.operationVersion; +} + +function clearScheduledTimer(runtime = {}) { + const timers = getTimerApi(runtime); + if (hideState.scheduledTimer) { + timers.clearTimeout(hideState.scheduledTimer); + hideState.scheduledTimer = null; + } +} + +function clearManagedState() { + hideState.managedChatRef = null; + hideState.managedChatKey = null; + hideState.managedSystemIndices.clear(); + hideState.hiddenRangeEnd = -1; + hideState.lastProcessedLength = 0; +} + +function restoreManagedSystemFlags(chat, runtime = {}) { + if (!Array.isArray(chat) || hideState.managedSystemIndices.size === 0) { + hideState.managedSystemIndices.clear(); + return 0; + } + + const restored = []; + for (const index of hideState.managedSystemIndices) { + const message = chat[index]; + if (!message || message.is_system !== true) continue; + message.is_system = false; + restored.push(index); + } + + syncSystemAttribute(chat, restored, "false", runtime); + hideState.managedSystemIndices.clear(); + return restored.length; +} + +function markManagedSystemRange(chat, start, end, runtime = {}) { + if (!Array.isArray(chat) || start > end) return 0; + + const marked = []; + for (let index = start; index <= end && index < chat.length; index++) { + const message = chat[index]; + if (!message || message.is_system === true) continue; + message.is_system = true; + hideState.managedSystemIndices.add(index); + marked.push(index); + } + + syncSystemAttribute(chat, marked, "true", runtime); + return marked.length; +} + +function adoptManagedChat(chat, chatKey, runtime = {}) { + const previousChat = hideState.managedChatRef; + if (previousChat && previousChat !== chat) { + restoreManagedSystemFlags(previousChat, runtime); + hideState.hiddenRangeEnd = -1; + hideState.lastProcessedLength = 0; + } + + hideState.managedChatRef = chat; + hideState.managedChatKey = chatKey; +} + +function buildResult({ + active = false, + hiddenCount = 0, + shownCount = 0, + chatLength = 0, + incremental = false, + stale = false, +} = {}) { + return { + active, + hiddenCount, + shownCount, + managedCount: active ? hiddenCount : 0, + chatLength, + incremental, + stale, + }; +} + +async function unhideCurrentRange(runtime = {}, version = null) { + const { chat } = getCurrentChatInfo(runtime); + const chatLength = Array.isArray(chat) ? chat.length : 0; + const previousEnd = Math.min(hideState.hiddenRangeEnd, chatLength - 1); + if (previousEnd < 0) { + return { shownCount: 0, chatLength }; + } + + await executeSlashCommand(`/unhide 0-${previousEnd}`, runtime); + if (!isOperationCurrent(version ?? hideState.operationVersion)) { + return { shownCount: 0, chatLength, stale: true }; + } + + return { shownCount: previousEnd + 1, chatLength }; +} + +async function runHideApply(settings = {}, runtime = {}, options = {}) { const normalized = normalizeHideSettings(settings); - const chat = getCurrentChat(runtime); - if (!chat || chat.length === 0) { - resetHideState(runtime); - return { - active: false, + const { incrementalPreferred = false, version = beginOperation() } = options; + const chatInfo = getCurrentChatInfo(runtime); + const chat = chatInfo.chat; + const chatLength = Array.isArray(chat) ? chat.length : 0; + + if (!chat || chatLength === 0) { + clearManagedState(); + return buildResult(); + } + + const chatKey = getCurrentChatKey(runtime); + const previousChatKey = hideState.managedChatKey; + const sameChat = + previousChatKey !== null && chatKey !== null && previousChatKey === chatKey; + const previousHiddenEnd = sameChat ? hideState.hiddenRangeEnd : -1; + const previousLength = sameChat ? hideState.lastProcessedLength : 0; + adoptManagedChat(chat, chatKey, runtime); + hideState.lastProcessedLength = chatLength; + + if (!normalized.enabled || normalized.hideLastN <= 0) { + if (previousHiddenEnd >= 0) { + const { shownCount } = await unhideCurrentRange(runtime, version); + if (!isOperationCurrent(version)) { + return buildResult({ chatLength, shownCount, stale: true }); + } + restoreManagedSystemFlags(chat, runtime); + hideState.hiddenRangeEnd = -1; + return buildResult({ chatLength, shownCount }); + } + + restoreManagedSystemFlags(chat, runtime); + hideState.hiddenRangeEnd = -1; + return buildResult({ chatLength }); + } + + const nextRange = calcHideRange(chatLength, normalized.hideLastN); + if (!nextRange) { + if (previousHiddenEnd >= 0) { + const { shownCount } = await unhideCurrentRange(runtime, version); + if (!isOperationCurrent(version)) { + return buildResult({ chatLength, shownCount, stale: true }); + } + restoreManagedSystemFlags(chat, runtime); + hideState.hiddenRangeEnd = -1; + return buildResult({ chatLength, shownCount }); + } + + restoreManagedSystemFlags(chat, runtime); + hideState.hiddenRangeEnd = -1; + return buildResult({ + active: true, hiddenCount: 0, - shownCount: 0, - managedCount: 0, - chatLength: 0, - incremental: false, - }; + chatLength, + }); } if ( - hideState.managedChatRef !== chat || - !normalized.enabled || - normalized.hideLastN <= 0 + incrementalPreferred && + sameChat && + previousHiddenEnd >= 0 && + chatLength > previousLength && + previousLength > 0 ) { - return { - ...runFullHideCheck(normalized, runtime), - incremental: false, - }; - } + const previousRange = calcHideRange(previousLength, normalized.hideLastN); + const canExtendOnly = + previousRange && + previousRange.end === previousHiddenEnd && + nextRange.end >= previousHiddenEnd; + if (canExtendOnly && nextRange.end > previousHiddenEnd) { + const start = previousHiddenEnd + 1; + const end = nextRange.end; + await executeSlashCommand(`/hide ${start}-${end}`, runtime); + if (!isOperationCurrent(version)) { + return buildResult({ chatLength, stale: true }); + } - const chatLength = chat.length; - const previousLength = hideState.lastProcessedLength; - if (chatLength <= previousLength) { - return { - ...runFullHideCheck(normalized, runtime), - incremental: false, - }; - } - - const previousVisibleStart = - previousLength > 0 ? Math.max(0, previousLength - normalized.hideLastN) : 0; - const nextVisibleStart = Math.max(0, chatLength - normalized.hideLastN); - const toHide = []; - - if (nextVisibleStart > previousVisibleStart) { - for (let index = previousVisibleStart; index < nextVisibleStart; index++) { - const message = chat[index]; - if (!message || message.is_system === true) continue; - message.is_system = true; - hideState.hiddenIndices.add(index); - toHide.push(index); + markManagedSystemRange(chat, start, end, runtime); + hideState.hiddenRangeEnd = end; + return buildResult({ + active: true, + hiddenCount: end + 1, + shownCount: 0, + chatLength, + incremental: true, + }); } } - syncSystemAttribute(chat, toHide, "true", runtime); + let shownCount = 0; + if (previousHiddenEnd >= 0) { + const unhideResult = await unhideCurrentRange(runtime, version); + if (!isOperationCurrent(version)) { + return buildResult({ + chatLength, + shownCount: unhideResult.shownCount ?? 0, + stale: true, + }); + } + shownCount = unhideResult.shownCount ?? 0; + } + restoreManagedSystemFlags(chat, runtime); + + await executeSlashCommand(`/hide ${nextRange.start}-${nextRange.end}`, runtime); + if (!isOperationCurrent(version)) { + return buildResult({ chatLength, shownCount, stale: true }); + } + + markManagedSystemRange(chat, nextRange.start, nextRange.end, runtime); + hideState.hiddenRangeEnd = nextRange.end; hideState.lastProcessedLength = chatLength; - return { + return buildResult({ active: true, - hiddenCount: toHide.length, - shownCount: 0, - managedCount: hideState.hiddenIndices.size, + hiddenCount: nextRange.end + 1, + shownCount, chatLength, - incremental: true, - }; + incremental: false, + }); } -export function applyHideSettings(settings = {}, runtime = {}) { - return runFullHideCheck(settings, runtime); +export async function runFullHideCheck(settings = {}, runtime = {}) { + return await runHideApply(settings, runtime, { + incrementalPreferred: false, + version: beginOperation(), + }); +} + +export async function runIncrementalHideCheck(settings = {}, runtime = {}) { + return await runHideApply(settings, runtime, { + incrementalPreferred: true, + version: beginOperation(), + }); +} + +export async function applyHideSettings(settings = {}, runtime = {}) { + return await runFullHideCheck(settings, runtime); } export function scheduleHideSettingsApply( @@ -245,58 +388,56 @@ export function scheduleHideSettingsApply( runtime = {}, delayMs = 120, ) { - const timers = getTimerApi(runtime); - if (hideState.scheduledTimer) { - timers.clearTimeout(hideState.scheduledTimer); - hideState.scheduledTimer = null; - } + clearScheduledTimer(runtime); + const timers = getTimerApi(runtime); const snapshot = normalizeHideSettings(settings); hideState.scheduledTimer = timers.setTimeout(() => { hideState.scheduledTimer = null; - applyHideSettings(snapshot, runtime); + void applyHideSettings(snapshot, runtime).catch((error) => { + console.warn?.("[ST-BME] scheduled hide apply failed", error); + }); }, Math.max(0, Math.trunc(Number(delayMs) || 0))); } -export function unhideAll(runtime = {}) { - const timers = getTimerApi(runtime); - if (hideState.scheduledTimer) { - timers.clearTimeout(hideState.scheduledTimer); - hideState.scheduledTimer = null; +export async function unhideAll(runtime = {}) { + clearScheduledTimer(runtime); + const version = beginOperation(); + const chatInfo = getCurrentChatInfo(runtime); + const chatLength = Array.isArray(chatInfo.chat) ? chatInfo.chat.length : 0; + + if (chatLength === 0 || hideState.hiddenRangeEnd < 0) { + hideState.lastProcessedLength = chatLength; + hideState.hiddenRangeEnd = -1; + hideState.managedChatKey = getCurrentChatKey(runtime); + return buildResult({ chatLength }); } - const managedChat = hideState.managedChatRef; - const { shownCount } = unhideTrackedChat(managedChat, runtime); - hideState.hiddenIndices.clear(); - hideState.lastProcessedLength = Array.isArray(managedChat) - ? managedChat.length - : 0; + const { shownCount } = await unhideCurrentRange(runtime, version); + if (!isOperationCurrent(version)) { + return buildResult({ chatLength, shownCount, stale: true }); + } - return { - active: false, - shownCount, - managedCount: 0, - }; + restoreManagedSystemFlags(chatInfo.chat, runtime); + hideState.hiddenRangeEnd = -1; + hideState.lastProcessedLength = chatLength; + hideState.managedChatRef = chatInfo.chat; + hideState.managedChatKey = getCurrentChatKey(runtime); + + return buildResult({ chatLength, shownCount }); } export function resetHideState(runtime = {}) { - const timers = getTimerApi(runtime); - if (hideState.scheduledTimer) { - timers.clearTimeout(hideState.scheduledTimer); - hideState.scheduledTimer = null; - } - - const managedChat = hideState.managedChatRef; - unhideTrackedChat(managedChat, runtime); - hideState.managedChatRef = null; - hideState.hiddenIndices.clear(); - hideState.lastProcessedLength = 0; + clearScheduledTimer(runtime); + beginOperation(); + restoreManagedSystemFlags(hideState.managedChatRef, runtime); + clearManagedState(); } export function getHideStateSnapshot() { return { - hasManagedChat: Boolean(hideState.managedChatRef), - managedHiddenCount: hideState.hiddenIndices.size, + hasManagedChat: hideState.managedChatRef !== null, + managedHiddenCount: hideState.hiddenRangeEnd >= 0 ? hideState.hiddenRangeEnd + 1 : 0, lastProcessedLength: hideState.lastProcessedLength, scheduled: Boolean(hideState.scheduledTimer), }; diff --git a/index.js b/index.js index 07041ca..954ee7b 100644 --- a/index.js +++ b/index.js @@ -2287,9 +2287,9 @@ function getHideRuntimeAdapters() { }; } -function applyMessageHideNow(reason = "manual-apply") { +async function applyMessageHideNow(reason = "manual-apply") { try { - const result = applyHideSettings( + const result = await applyHideSettings( getMessageHideSettings(), getHideRuntimeAdapters(), ); @@ -2316,9 +2316,9 @@ function scheduleMessageHideApply(reason = "scheduled", delayMs = 120) { } } -function runIncrementalMessageHide(reason = "incremental") { +async function runIncrementalMessageHide(reason = "incremental") { try { - const result = runIncrementalHideCheck( + const result = await runIncrementalHideCheck( getMessageHideSettings(), getHideRuntimeAdapters(), ); @@ -2344,9 +2344,9 @@ function clearMessageHideState(reason = "reset") { } } -function clearAllHiddenMessages(reason = "manual-clear") { +async function clearAllHiddenMessages(reason = "manual-clear") { try { - const result = unhideAll(getHideRuntimeAdapters()); + const result = await unhideAll(getHideRuntimeAdapters()); console.log("[ST-BME] 已取消全部旧楼层隐藏:", reason, result); return result; } catch (error) { @@ -4899,7 +4899,7 @@ function updateModuleSettings(patch = {}) { if (Object.keys(patch).some((key) => messageHideKeys.has(key))) { const hideSettings = getMessageHideSettings(settings); if (!hideSettings.enabled || hideSettings.hide_last_n <= 0) { - clearAllHiddenMessages("settings-updated-disable"); + void clearAllHiddenMessages("settings-updated-disable"); } else { scheduleMessageHideApply("settings-updated", 30); } @@ -8273,7 +8273,7 @@ function onMessageReceived(messageId = null, type = "") { hideSettings?.hide_last_n > 0 && typeof runIncrementalMessageHide === "function" ) { - runIncrementalMessageHide("message-received"); + void runIncrementalMessageHide("message-received"); } return result; diff --git a/tests/hide-engine.mjs b/tests/hide-engine.mjs index d87c43e..f7e024e 100644 --- a/tests/hide-engine.mjs +++ b/tests/hide-engine.mjs @@ -8,13 +8,17 @@ import { unhideAll, } from "../hide-engine.js"; -function createRuntime(chat) { +function createRuntime(chat, chatId = "chat-a") { + const commands = []; const domWrites = []; return { chat, + chatId, + commands, domWrites, - getContext() { - return { chat: this.chat }; + async executeSlashCommands(command) { + commands.push(command); + return ""; }, $(selector) { return { @@ -23,62 +27,115 @@ function createRuntime(chat) { }, }; }, + getContext() { + return { + chat: this.chat, + chatId: this.chatId, + executeSlashCommands: this.executeSlashCommands.bind(this), + }; + }, }; } -function testApplyAndUnhidePreservesOriginalSystemMessages() { +async function testApplyUsesNativeHide() { + resetHideState(); const chat = [ - { mes: "原系统", is_system: true }, - { mes: "用户1", is_user: true, is_system: false }, - { mes: "助手1", is_user: false, is_system: false }, - { mes: "用户2", is_user: true, is_system: false }, - { mes: "助手2", is_user: false, is_system: false }, + { mes: "user-1", is_user: true, is_system: false }, + { mes: "assistant-1", is_user: false, is_system: false }, + { mes: "user-2", is_user: true, is_system: false }, + { mes: "assistant-2", is_user: false, is_system: false }, + { mes: "user-3", is_user: true, is_system: false }, ]; const runtime = createRuntime(chat); - const applyResult = applyHideSettings( + const result = await applyHideSettings( { enabled: true, hide_last_n: 2 }, runtime, ); - assert.equal(applyResult.active, true); + + assert.equal(result.active, true); + assert.equal(result.hiddenCount, 3); + assert.deepEqual(runtime.commands, ["/hide 0-2"]); assert.equal(chat[0].is_system, true); assert.equal(chat[1].is_system, true); assert.equal(chat[2].is_system, true); assert.equal(chat[3].is_system, false); - assert.equal(chat[4].is_system, false); - assert.equal(applyResult.managedCount, 2); - - const unhideResult = unhideAll(runtime); - assert.equal(unhideResult.active, false); - assert.equal(chat[0].is_system, true, "原系统消息不应被恢复"); - assert.equal(chat[1].is_system, false); - assert.equal(chat[2].is_system, false); + assert.deepEqual(getHideStateSnapshot(), { + hasManagedChat: true, + managedHiddenCount: 3, + lastProcessedLength: 5, + scheduled: false, + }); } -function testResetRestoresPreviousManagedChat() { - const oldChat = [ - { mes: "用户1", is_user: true, is_system: false }, - { mes: "助手1", is_user: false, is_system: false }, - { mes: "用户2", is_user: true, is_system: false }, - { mes: "助手2", is_user: false, is_system: false }, +async function testDisableUnhidesManagedRange() { + resetHideState(); + const chat = [ + { mes: "system", is_user: false, is_system: true }, + { mes: "assistant-1", is_user: false, is_system: false }, + { mes: "user-2", is_user: true, is_system: false }, + { mes: "assistant-2", is_user: false, is_system: false }, ]; - const newChat = [ - { mes: "新用户", is_user: true, is_system: false }, - { mes: "新助手", is_user: false, is_system: false }, + const runtime = createRuntime(chat); + + await applyHideSettings({ enabled: true, hide_last_n: 1 }, runtime); + runtime.commands.length = 0; + + const result = await unhideAll(runtime); + assert.equal(result.active, false); + assert.equal(result.shownCount, 3); + assert.deepEqual(runtime.commands, ["/unhide 0-2"]); + assert.equal(chat[0].is_system, true); + assert.equal(chat[1].is_system, false); + assert.equal(chat[2].is_system, false); + assert.equal(getHideStateSnapshot().managedHiddenCount, 0); +} + +async function testIncrementalOnlyHidesOverflowDelta() { + resetHideState(); + const chat = [ + { mes: "user-1", is_user: true, is_system: false }, + { mes: "assistant-1", is_user: false, is_system: false }, + { mes: "user-2", is_user: true, is_system: false }, ]; - const runtime = createRuntime(oldChat); + const runtime = createRuntime(chat); - applyHideSettings({ enabled: true, hide_last_n: 1 }, runtime); - assert.equal(oldChat[0].is_system, true); - assert.equal(oldChat[1].is_system, true); - assert.equal(oldChat[2].is_system, true); + await applyHideSettings({ enabled: true, hide_last_n: 2 }, runtime); + runtime.commands.length = 0; + + chat.push({ mes: "assistant-2", is_user: false, is_system: false }); + const result = await runIncrementalHideCheck( + { enabled: true, hide_last_n: 2 }, + runtime, + ); + + assert.equal(result.incremental, true); + assert.equal(result.hiddenCount, 2); + assert.deepEqual(runtime.commands, ["/hide 1-1"]); + assert.equal(chat[0].is_system, true); + assert.equal(chat[1].is_system, true); + assert.equal(chat[2].is_system, false); + assert.equal(chat[3].is_system, false); + assert.equal(getHideStateSnapshot().managedHiddenCount, 2); +} + +async function testResetClearsStateWithoutIssuingCommands() { + resetHideState(); + const chat = [ + { mes: "user-1", is_user: true, is_system: false }, + { mes: "assistant-1", is_user: false, is_system: false }, + { mes: "user-2", is_user: true, is_system: false }, + ]; + const runtime = createRuntime(chat); + + await applyHideSettings({ enabled: true, hide_last_n: 1 }, runtime); + runtime.commands.length = 0; - runtime.chat = newChat; resetHideState(runtime); - assert.equal(oldChat[0].is_system, false); - assert.equal(oldChat[1].is_system, false); - assert.equal(oldChat[2].is_system, false); + assert.deepEqual(runtime.commands, []); + assert.equal(chat[0].is_system, false); + assert.equal(chat[1].is_system, false); assert.deepEqual(getHideStateSnapshot(), { hasManagedChat: false, managedHiddenCount: 0, @@ -87,33 +144,9 @@ function testResetRestoresPreviousManagedChat() { }); } -function testIncrementalHideOnlyHidesNewOverflowMessages() { - const chat = [ - { mes: "用户1", is_user: true, is_system: false }, - { mes: "助手1", is_user: false, is_system: false }, - { mes: "用户2", is_user: true, is_system: false }, - ]; - const runtime = createRuntime(chat); - - applyHideSettings({ enabled: true, hide_last_n: 2 }, runtime); - assert.equal(chat[0].is_system, true); - assert.equal(chat[1].is_system, false); - assert.equal(chat[2].is_system, false); - - chat.push({ mes: "助手2", is_user: false, is_system: false }); - const result = runIncrementalHideCheck( - { enabled: true, hide_last_n: 2 }, - runtime, - ); - assert.equal(result.incremental, true); - assert.equal(result.hiddenCount, 1); - assert.equal(chat[1].is_system, true); - assert.equal(chat[2].is_system, false); - assert.equal(chat[3].is_system, false); -} - -testApplyAndUnhidePreservesOriginalSystemMessages(); -testResetRestoresPreviousManagedChat(); -testIncrementalHideOnlyHidesNewOverflowMessages(); +await testApplyUsesNativeHide(); +await testDisableUnhidesManagedRange(); +await testIncrementalOnlyHidesOverflowDelta(); +await testResetClearsStateWithoutIssuingCommands(); console.log("hide-engine tests passed");