Merge upstream/main into doom-lite-expression-sync-v2

This commit is contained in:
Tremendoussly
2026-04-14 16:21:07 +02:00
38 changed files with 1922 additions and 477 deletions
+207 -35
View File
@@ -20,9 +20,21 @@ import {
setIsAwaitingNewMessage,
updateLastGeneratedData,
updateCommittedTrackerData,
$musicPlayerContainer
$musicPlayerContainer,
incrementSeparateGenerationId
} from '../../core/state.js';
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getMessageSwipeTrackerData, getCurrentMessageSwipeTrackerData, restoreLatestTrackerStateFromChat, setMessageSwipeTrackerData } from '../../core/persistence.js';
import {
saveChatData,
loadChatData,
autoSwitchPresetForEntity,
getMessageSwipeTrackerData,
getCurrentMessageSwipeTrackerData,
restoreLatestTrackerStateFromChat,
setMessageSwipeTrackerData,
getSwipeData,
commitTrackerDataFromPriorMessage,
inheritSwipeDataFromPriorMessage
} from '../../core/persistence.js';
import { i18n } from '../../core/i18n.js';
// Generation & Parsing
@@ -53,6 +65,45 @@ import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
let chatStateRehydrateRunId = 0;
/**
* Reads the swipe store of the last assistant message in `currentChat` and
* writes its data into `lastGeneratedData`, including syncing stat bars via
* `parseUserStats`. If no assistant message exists, or none has stored swipe
* data, `lastGeneratedData` is left unchanged.
*
* Use this wherever the displayed tracker state must be re-derived from the
* authoritative swipe store rather than from chat_metadata (e.g. after a
* CHAT_CHANGED caused by branching, or after a message deletion).
*
* @param {Array} currentChat - Live chat array from getContext().chat
* @returns {boolean} True if swipe data was found and applied
*/
function syncLastGeneratedDataFromSwipeStore(currentChat) {
for (let i = currentChat.length - 1; i >= 0; i--) {
const msg = currentChat[i];
if (!msg.is_user && !msg.is_system) {
const swipeId = msg.swipe_id || 0;
const swipeData = getSwipeData(msg, swipeId);
if (swipeData) {
lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null;
// Normalize characterThoughts to string (backward compat with old object format).
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
} else {
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
}
if (swipeData.userStats) {
parseUserStats(swipeData.userStats);
}
return true;
}
return false; // Last assistant message exists but has no swipe data yet
}
}
return false; // No assistant messages in chat
}
/**
* Commits the tracker data from the last assistant message to be used as source for next generation.
* This should be called when the user has replied to a message, ensuring all swipes of the next
@@ -67,22 +118,28 @@ export function commitTrackerData() {
// Find the last assistant message
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message.is_user) {
if (!message.is_user && !message.is_system) {
// Found last assistant message - commit its tracker data
if (message.extra && message.extra.rpg_companion_swipes) {
const swipeId = message.swipe_id || 0;
const swipeData = message.extra.rpg_companion_swipes[swipeId];
const swipeId = message.swipe_id || 0;
const swipeData = getSwipeData(message, swipeId);
if (swipeData) {
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
committedTrackerData.userStats = swipeData.userStats || null;
committedTrackerData.infoBox = swipeData.infoBox || null;
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
if (swipeData) {
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
committedTrackerData.userStats = swipeData.userStats || null;
committedTrackerData.infoBox = swipeData.infoBox || null;
const rawCharacterThoughts = swipeData.characterThoughts;
if (rawCharacterThoughts == null) {
committedTrackerData.characterThoughts = null;
} else if (typeof rawCharacterThoughts === 'object') {
committedTrackerData.characterThoughts = JSON.stringify(rawCharacterThoughts);
} else {
// console.log('[RPG Companion] No swipe data found for swipe', swipeId);
committedTrackerData.characterThoughts = String(rawCharacterThoughts);
}
} else {
// console.log('[RPG Companion] No RPG data found in last assistant message');
// No saved swipe data — treat as empty (e.g. first message, no prior generation)
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
break;
}
@@ -351,17 +408,6 @@ export function onMessageSent() {
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
// The RPG data comes embedded in the main response
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
// For separate mode with auto-update disabled, commit displayed tracker
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
committedTrackerData.userStats = lastGeneratedData.userStats;
committedTrackerData.infoBox = lastGeneratedData.infoBox;
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed displayed tracker (auto-update disabled)');
}
}
}
/**
@@ -499,13 +545,26 @@ export async function onMessageReceived(data) {
// Just render the music player
renderMusicPlayer($musicPlayerContainer[0]);
}
// When auto-update is disabled, no tracker API call will run for this message.
// Inherit the prior assistant message's tracker data into this swipe slot so that
// commitTrackerDataFromPriorMessage can find a valid state next turn instead of nulling everything.
// Inheritance does not overwrite existing data, so it's safe to call even if the condition misses an edge case.
if (!extensionSettings.autoUpdate || !isAwaitingNewMessage) {
inheritSwipeDataFromPriorMessage(lastMessage, chat.length - 1);
}
}
// Trigger auto-update if enabled (for both separate and external modes)
// Only trigger if this is a newly generated message, not loading chat history
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
// Capture the current generation ID before the async gap so that any
// message deletion (or a newer generation) that increments the counter
// while the 500ms timer or the API call is in-flight will cause
// updateRPGData to discard its result rather than stomping the UI.
const genId = incrementSeparateGenerationId();
setTimeout(async () => {
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, genId);
// Update FAB widgets and strip widgets after separate/external mode update completes
setFabLoadingState(false);
updateFabWidgets();
@@ -562,6 +621,22 @@ export function onCharacterChanged() {
// Load chat-specific data when switching chats
loadChatData();
// chat_metadata may not reflect the actual chat tail for branches, so
// loadChatData() may have just restored stale data from the parent chat.
// Override lastGeneratedData from the swipe store of the last assistant message.
// The message objects in the branch already carry their full swipe stores, making this authoritative.
// If no swipe data exists (e.g. branching at message 0, or a chat with no generations yet),
// null out lastGeneratedData and committedTrackerData so we don't display stale values from the parent chat.
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(getContext().chat);
if (!hadSwipeData) {
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
}
// Don't call commitTrackerData() here - it would overwrite the loaded committedTrackerData
// with data from the last message, which may be null/empty. The loaded committedTrackerData
// already contains the committed state from when we last left this chat.
@@ -609,6 +684,9 @@ export function onMessageSwiped(messageIndex) {
// This is a NEW swipe that will trigger generation
setLastActionWasSwipe(true);
setIsAwaitingNewMessage(true);
// Immediately commit context from the prior assistant message (N-1) so generation
// uses the world state before this message, not the last-viewed sibling swipe.
commitTrackerDataFromPriorMessage(messageIndex);
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
} else {
// This is navigating to an EXISTING swipe - don't change the flag
@@ -621,11 +699,10 @@ export function onMessageSwiped(messageIndex) {
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
// IMPORTANT: onMessageSwiped is for DISPLAY only!
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
// It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check
// Load saved swipe data for the active swipe only.
// Using the current-swipe helper here avoids falling back to another
// stored swipe payload and showing stale tracker state.
if (swipeData) {
// Load swipe data into lastGeneratedData for display (both modes)
lastGeneratedData.userStats = swipeData.userStats || null;
lastGeneratedData.infoBox = swipeData.infoBox || null;
@@ -637,13 +714,12 @@ export function onMessageSwiped(messageIndex) {
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
}
// DON'T parse user stats when loading swipe data
// This would overwrite manually edited fields (like Conditions) with old swipe data
// The lastGeneratedData is loaded for display purposes only
// parseUserStats() updates extensionSettings.userStats which should only be modified
// by new generations or manual edits, not by swipe navigation
// Sync extensionSettings.userStats so stat bars reflect this swipe
if (swipeData.userStats) {
parseUserStats(swipeData.userStats);
}
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
// console.log('[RPG Companion] 🔄 Loaded swipe data for swipe:', currentSwipeId);
} else {
// console.log('[RPG Companion] ️ No stored data for swipe:', currentSwipeId);
}
@@ -656,6 +732,10 @@ export function onMessageSwiped(messageIndex) {
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update widget strips with the newly loaded swipe data
updateFabWidgets();
updateStripWidgets();
// Update chat thought overlays
updateChatThoughts();
}
@@ -665,10 +745,102 @@ export function onMessageDeleted() {
return;
}
// Invalidate any pending or in-flight separate-mode generation so
// its result is not applied to the (now-changed) chat tail.
incrementSeparateGenerationId();
const currentChat = getContext().chat || [];
let lastAssistantIndex = -1;
for (let i = currentChat.length - 1; i >= 0; i--) {
if (!currentChat[i].is_user && !currentChat[i].is_system) {
lastAssistantIndex = i;
break;
}
}
syncDisplayedTrackerStateFromChat();
// After the display state has been rebuilt, restore generation context from
// the assistant message immediately before the new tail message so the next
// generation uses the correct N-1 tracker state.
if (lastAssistantIndex !== -1) {
commitTrackerDataFromPriorMessage(lastAssistantIndex);
}
saveChatData();
}
/**
* Event handler for when a message is deleted.
* Re-syncs lastGeneratedData, committedTrackerData, and all UI panels to the
* new last assistant message's active swipe — or clears everything if no
* assistant messages remain.
*/
/* Removed during merge resolution.
return;
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted');
// Invalidate any pending or in-flight separate-mode generation so
// its result is not applied to the (now-changed) chat tail.
incrementSeparateGenerationId();
const currentChat = getContext().chat;
// Walk backward to find the new last assistant message.
let lastAssistantIndex = -1;
for (let i = currentChat.length - 1; i >= 0; i--) {
if (!currentChat[i].is_user && !currentChat[i].is_system) {
lastAssistantIndex = i;
break;
}
}
if (lastAssistantIndex === -1) {
// No assistant messages remain — clear all state.
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
// console.log('[RPG Companion] 🗑️ No assistant messages remain — cleared all tracker state.');
} else {
// Restore display state from the new tail message's active swipe.
// If the message has no swipe data yet, null the fields so we
// don't show stale data from the deleted message.
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(currentChat);
if (!hadSwipeData) {
lastGeneratedData.userStats = null;
lastGeneratedData.infoBox = null;
lastGeneratedData.characterThoughts = null;
committedTrackerData.userStats = null;
committedTrackerData.infoBox = null;
committedTrackerData.characterThoughts = null;
// console.log('[RPG Companion] 🗑️ No swipe data for last assistant message — cleared display state.');
}
// Commit context from the message *before* the new tail assistant message,
// so any subsequent generation uses the correct N-1 world state.
commitTrackerDataFromPriorMessage(lastAssistantIndex);
}
// Re-render all panels.
renderUserStats();
renderInfoBox();
renderThoughts();
renderInventory();
renderQuests();
renderMusicPlayer($musicPlayerContainer[0]);
// Update widget strips.
updateFabWidgets();
updateStripWidgets();
// Persist updated state.
saveChatData();
*/
/**
* Update the persona avatar image when user switches personas
*/