diff --git a/event-binding.js b/event-binding.js index d7c0253..ef3b680 100644 --- a/event-binding.js +++ b/event-binding.js @@ -175,6 +175,15 @@ export function registerCoreEventHooksController(runtime) { if (eventTypes.MESSAGE_UPDATED) { bind(eventTypes.MESSAGE_UPDATED, handlers.onMessageEdited); } + if (eventTypes.USER_MESSAGE_RENDERED) { + bind(eventTypes.USER_MESSAGE_RENDERED, handlers.onUserMessageRendered); + } + if (eventTypes.CHARACTER_MESSAGE_RENDERED) { + bind( + eventTypes.CHARACTER_MESSAGE_RENDERED, + handlers.onCharacterMessageRendered, + ); + } const nextState = { registered: true, @@ -248,6 +257,31 @@ export function onMessageSentController(runtime, messageId) { runtime.refreshPersistedRecallMessageUi?.(); } +export function onUserMessageRenderedController(runtime, messageId = null) { + // MESSAGE_SENT 早于实际 DOM 挂载;这里等宿主确认 user 楼层渲染完成后, + // 再补一次 Recall Card 刷新,避免“当前楼层没卡片,下一楼才补出来”。 + runtime.refreshPersistedRecallMessageUi?.(40); + return { + messageId: Number.isFinite(Number(messageId)) ? Number(messageId) : null, + refreshed: true, + source: "user-message-rendered", + }; +} + +export function onCharacterMessageRenderedController( + runtime, + messageId = null, + type = "", +) { + runtime.refreshPersistedRecallMessageUi?.(80); + return { + messageId: Number.isFinite(Number(messageId)) ? Number(messageId) : null, + type: String(type || ""), + refreshed: true, + source: "character-message-rendered", + }; +} + export function onGenerationStartedController( runtime, type, diff --git a/index.js b/index.js index 713c701..ff1c4eb 100644 --- a/index.js +++ b/index.js @@ -50,6 +50,7 @@ import { import { installSendIntentHooksController, onBeforeCombinePromptsController, + onCharacterMessageRenderedController, onChatChangedController, onChatLoadedController, onGenerationAfterCommandsController, @@ -59,6 +60,7 @@ import { onMessageReceivedController, onMessageSentController, onMessageSwipedController, + onUserMessageRenderedController, registerBeforeCombinePromptsController, registerCoreEventHooksController, registerGenerationAfterCommandsController, @@ -9099,6 +9101,25 @@ function onMessageSent(messageId) { return result; } +function onUserMessageRendered(messageId = null) { + return onUserMessageRenderedController( + { + refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, + }, + messageId, + ); +} + +function onCharacterMessageRendered(messageId = null, type = "") { + return onCharacterMessageRenderedController( + { + refreshPersistedRecallMessageUi: schedulePersistedRecallMessageUiRefresh, + }, + messageId, + type, + ); +} + function onMessageDeleted(chatLengthOrMessageId, meta = null) { const result = onMessageDeletedController( { @@ -9574,6 +9595,7 @@ async function onReembedDirect() { getCoreEventBindingState, handlers: { onBeforeCombinePrompts, + onCharacterMessageRendered, onChatChanged, onChatLoaded, onGenerationAfterCommands, @@ -9584,6 +9606,7 @@ async function onReembedDirect() { onMessageReceived, onMessageSent, onMessageSwiped, + onUserMessageRendered, }, registerBeforeCombinePrompts, registerGenerationAfterCommands, diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index 9108dc2..f5ba000 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -7,12 +7,14 @@ import vm from "node:vm"; import { pruneProcessedMessageHashesFromFloor } from "../chat-history.js"; import { onBeforeCombinePromptsController, + onCharacterMessageRenderedController, onChatChangedController, onGenerationAfterCommandsController, onGenerationStartedController, onMessageSentController, onMessageReceivedController, onMessageSwipedController, + onUserMessageRenderedController, registerCoreEventHooksController, } from "../event-binding.js"; import { @@ -3580,23 +3582,29 @@ async function testRegisterCoreEventHooksIsIdempotent() { CHAT_LOADED: "chat-loaded", MESSAGE_SENT: "message-sent", GENERATION_STARTED: "generation-started", + GENERATION_ENDED: "generation-ended", MESSAGE_RECEIVED: "message-received", MESSAGE_DELETED: "message-deleted", MESSAGE_EDITED: "message-edited", MESSAGE_SWIPED: "message-swiped", MESSAGE_UPDATED: "message-updated", + USER_MESSAGE_RENDERED: "user-message-rendered", + CHARACTER_MESSAGE_RENDERED: "character-message-rendered", }, handlers: { onChatChanged() {}, onChatLoaded() {}, onMessageSent() {}, onGenerationStarted() {}, + onGenerationEnded() {}, onGenerationAfterCommands() {}, onBeforeCombinePrompts() {}, onMessageReceived() {}, onMessageDeleted() {}, onMessageEdited() {}, onMessageSwiped() {}, + onUserMessageRendered() {}, + onCharacterMessageRendered() {}, }, registerGenerationAfterCommands(listener) { makeFirstRegistrations.push({ hook: "after", listener }); @@ -3620,7 +3628,7 @@ async function testRegisterCoreEventHooksIsIdempotent() { registerCoreEventHooksController(runtime); registerCoreEventHooksController(runtime); - assert.equal(eventRegistrations.length, 9); + assert.equal(eventRegistrations.length, 12); assert.equal(makeFirstRegistrations.length, 2); assert.equal(bindingState.registered, true); } @@ -3743,6 +3751,42 @@ async function testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid() { assert.equal(refreshCalls, 1); } +async function testUserMessageRenderedRefreshesRecallUiAfterRealDomRender() { + const refreshCalls = []; + + const result = onUserMessageRenderedController( + { + refreshPersistedRecallMessageUi(delayMs = 0) { + refreshCalls.push(delayMs); + }, + }, + 7, + ); + + assert.deepEqual(refreshCalls, [40]); + assert.equal(result.messageId, 7); + assert.equal(result.source, "user-message-rendered"); +} + +async function testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender() { + const refreshCalls = []; + + const result = onCharacterMessageRenderedController( + { + refreshPersistedRecallMessageUi(delayMs = 0) { + refreshCalls.push(delayMs); + }, + }, + 8, + "normal", + ); + + assert.deepEqual(refreshCalls, [80]); + assert.equal(result.messageId, 8); + assert.equal(result.type, "normal"); + assert.equal(result.source, "character-message-rendered"); +} + async function testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask() { let runExtractionCalls = 0; let refreshCalls = 0; @@ -5275,6 +5319,8 @@ await testRegisterCoreEventHooksIsIdempotent(); await testChatChangedDoesNotClearCoreEventBindings(); await testSwipeRoutesToRerollWithoutHistoryRecoveryFallback(); await testMessageSentFallsBackToLatestUserWhenHostMessageIdInvalid(); +await testUserMessageRenderedRefreshesRecallUiAfterRealDomRender(); +await testCharacterMessageRenderedRefreshesRecallUiAfterAssistantRender(); await testMessageReceivedQueuesExtractionWithoutRuntimeQueueMicrotask(); await testAutoExtractionDefersWhenGraphNotReady(); await testAutoExtractionDefersWhenAlreadyExtracting();