Add chapter checkpoint feature
- New feature: bookmark messages to exclude earlier history from context - Saves tokens by marking chapter start points in long chats - Uses SillyTavern's /hide and /unhide slash commands - Persists checkpoint across page reloads and generation events - UI: bookmark icon in message menus with visual indicators - Debounced restore function prevents concurrent executions - Pre-generation checkpoint application ensures messages stay hidden - Clean production-ready code with proper error handling
This commit is contained in:
@@ -144,6 +144,35 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
// Store RPG data for the last assistant message (separate mode)
|
||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||
// console.log('[RPG Companion] Last message is_user:', lastMessage ? lastMessage.is_user : 'no message');
|
||||
|
||||
// Update lastGeneratedData for display (regardless of message type)
|
||||
if (parsedData.userStats) {
|
||||
lastGeneratedData.userStats = parsedData.userStats;
|
||||
parseUserStats(parsedData.userStats);
|
||||
}
|
||||
if (parsedData.infoBox) {
|
||||
lastGeneratedData.infoBox = parsedData.infoBox;
|
||||
}
|
||||
if (parsedData.characterThoughts) {
|
||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||
}
|
||||
|
||||
// When saveTrackerHistory is enabled, store tracker data on the user's message too
|
||||
// This allows scrolling through history and seeing trackers at each point
|
||||
if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) {
|
||||
if (!lastMessage.extra) {
|
||||
lastMessage.extra = {};
|
||||
}
|
||||
lastMessage.extra.rpg_companion_data = {
|
||||
userStats: parsedData.userStats,
|
||||
infoBox: parsedData.infoBox,
|
||||
characterThoughts: parsedData.characterThoughts,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
// console.log('[RPG Companion] 💾 Stored tracker data on user message for history');
|
||||
}
|
||||
|
||||
// Also store on assistant message if present (existing behavior)
|
||||
if (lastMessage && !lastMessage.is_user) {
|
||||
if (!lastMessage.extra) {
|
||||
lastMessage.extra = {};
|
||||
@@ -160,58 +189,31 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
};
|
||||
|
||||
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
||||
|
||||
// Update lastGeneratedData for display AND future commit
|
||||
if (parsedData.userStats) {
|
||||
lastGeneratedData.userStats = parsedData.userStats;
|
||||
parseUserStats(parsedData.userStats);
|
||||
}
|
||||
if (parsedData.infoBox) {
|
||||
lastGeneratedData.infoBox = parsedData.infoBox;
|
||||
}
|
||||
if (parsedData.characterThoughts) {
|
||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||
}
|
||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Updated lastGeneratedData:', {
|
||||
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
|
||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
||||
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
|
||||
// });
|
||||
|
||||
// Only auto-commit on TRULY first generation (no committed data exists at all)
|
||||
// This prevents auto-commit after refresh when we have saved committed data
|
||||
const hasAnyCommittedContent = (
|
||||
(committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') ||
|
||||
(committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') ||
|
||||
(committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n')
|
||||
);
|
||||
|
||||
// Only commit if we have NO committed content at all (truly first time ever)
|
||||
if (!hasAnyCommittedContent) {
|
||||
committedTrackerData.userStats = parsedData.userStats;
|
||||
committedTrackerData.infoBox = parsedData.infoBox;
|
||||
committedTrackerData.characterThoughts = parsedData.characterThoughts;
|
||||
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
||||
}
|
||||
|
||||
// Render the updated data
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
} else {
|
||||
// No assistant message to attach to - just update display
|
||||
if (parsedData.userStats) {
|
||||
parseUserStats(parsedData.userStats);
|
||||
}
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
}
|
||||
|
||||
// Only commit on TRULY first generation (no committed data exists at all)
|
||||
// This prevents auto-commit after refresh when we have saved committed data
|
||||
const hasAnyCommittedContent = (
|
||||
(committedTrackerData.userStats && committedTrackerData.userStats.trim() !== '') ||
|
||||
(committedTrackerData.infoBox && committedTrackerData.infoBox.trim() !== '' && committedTrackerData.infoBox !== 'Info Box\n---\n') ||
|
||||
(committedTrackerData.characterThoughts && committedTrackerData.characterThoughts.trim() !== '' && committedTrackerData.characterThoughts !== 'Present Characters\n---\n')
|
||||
);
|
||||
|
||||
// Only commit if we have NO committed content at all (truly first time ever)
|
||||
if (!hasAnyCommittedContent) {
|
||||
committedTrackerData.userStats = parsedData.userStats;
|
||||
committedTrackerData.infoBox = parsedData.infoBox;
|
||||
committedTrackerData.characterThoughts = parsedData.characterThoughts;
|
||||
// console.log('[RPG Companion] 🔆 FIRST TIME: Auto-committed tracker data');
|
||||
}
|
||||
|
||||
// Render the updated data (outside the message check, always render)
|
||||
renderUserStats();
|
||||
renderInfoBox();
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
|
||||
// Save to chat metadata
|
||||
saveChatData();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
generateContextualSummary,
|
||||
DEFAULT_HTML_PROMPT
|
||||
} from './promptBuilder.js';
|
||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||
|
||||
/**
|
||||
* Event handler for generation start.
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
* @param {string} type - Event type
|
||||
* @param {Object} data - Event data
|
||||
*/
|
||||
export function onGenerationStarted(type, data) {
|
||||
export async function onGenerationStarted(type, data) {
|
||||
// console.log('[RPG Companion] onGenerationStarted called');
|
||||
// console.log('[RPG Companion] enabled:', extensionSettings.enabled);
|
||||
// console.log('[RPG Companion] generationMode:', extensionSettings.generationMode);
|
||||
@@ -68,6 +69,10 @@ export function onGenerationStarted(type, data) {
|
||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||
}
|
||||
|
||||
// Ensure checkpoint is applied before generation
|
||||
await restoreCheckpointOnLoad();
|
||||
|
||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||
|
||||
// For SEPARATE mode only: Check if we need to commit extension data
|
||||
|
||||
@@ -159,6 +159,38 @@ export function parseResponse(responseText) {
|
||||
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
|
||||
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars');
|
||||
|
||||
// Check if response uses XML <trackers> tags (new format)
|
||||
const xmlMatch = cleanedResponse.match(/<trackers>([\s\S]*?)<\/trackers>/i);
|
||||
if (xmlMatch) {
|
||||
debugLog('[RPG Parser] ✓ Found XML <trackers> tags, using XML parser');
|
||||
const trackersContent = xmlMatch[1].trim();
|
||||
|
||||
// Extract sections from XML content (sections are not in code blocks)
|
||||
const statsMatch = trackersContent.match(/(User )?Stats\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*(Info Box|Present Characters)|$)/i);
|
||||
if (statsMatch) {
|
||||
result.userStats = stripBrackets(statsMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Stats from XML');
|
||||
}
|
||||
|
||||
const infoBoxMatch = trackersContent.match(/Info Box\s*\n\s*---[\s\S]*?(?=\n\s*\n\s*Present Characters|$)/i);
|
||||
if (infoBoxMatch) {
|
||||
result.infoBox = stripBrackets(infoBoxMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Info Box from XML');
|
||||
}
|
||||
|
||||
const charactersMatch = trackersContent.match(/Present Characters\s*\n\s*---[\s\S]*$/i);
|
||||
if (charactersMatch) {
|
||||
result.characterThoughts = stripBrackets(charactersMatch[0].trim());
|
||||
debugLog('[RPG Parser] ✓ Extracted Present Characters from XML');
|
||||
}
|
||||
|
||||
debugLog('[RPG Parser] Parsed from XML:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fallback to markdown code block parsing (old format)
|
||||
debugLog('[RPG Parser] No XML tags found, using code block parser');
|
||||
|
||||
// Extract code blocks
|
||||
const codeBlockRegex = /```([^`]+)```/g;
|
||||
const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
||||
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
|
||||
import { selected_group, getGroupMembers, getGroupChat, groups } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
||||
|
||||
// Type imports
|
||||
@@ -25,7 +25,8 @@ async function getCharacterCardsInfo() {
|
||||
|
||||
// Check if in group chat
|
||||
if (selected_group) {
|
||||
const group = await getGroupChat(selected_group);
|
||||
// Find the current group directly from the groups array
|
||||
const group = groups.find(g => g.id === selected_group);
|
||||
const groupMembers = getGroupMembers(selected_group);
|
||||
|
||||
if (groupMembers && groupMembers.length > 0) {
|
||||
@@ -33,13 +34,15 @@ async function getCharacterCardsInfo() {
|
||||
|
||||
// Filter out disabled (muted) members
|
||||
const disabledMembers = group?.disabled_members || [];
|
||||
console.log('[RPG Companion] 🔍 Group ID:', selected_group, '| Disabled members:', disabledMembers);
|
||||
let characterIndex = 0;
|
||||
|
||||
groupMembers.forEach((member) => {
|
||||
if (!member || !member.name) return;
|
||||
|
||||
// Skip muted characters
|
||||
// Skip muted characters - check against avatar filename
|
||||
if (member.avatar && disabledMembers.includes(member.avatar)) {
|
||||
console.log(`[RPG Companion] ❌ Skipping muted: ${member.name} (${member.avatar})`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,16 +207,27 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
|
||||
// Only add tracker instructions if at least one tracker is enabled
|
||||
if (hasAnyTrackers) {
|
||||
// Determine format based on saveTrackerHistory setting
|
||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
||||
const openTag = useXmlTags ? '<trackers>\n' : '';
|
||||
const closeTag = useXmlTags ? '\n</trackers>' : '';
|
||||
const codeBlockMarker = useXmlTags ? '' : '```';
|
||||
|
||||
// Universal instruction header
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).
|
||||
if (useXmlTags) {
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in <trackers></trackers> XML tags. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).
|
||||
`;
|
||||
} else {
|
||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that ${userName} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).
|
||||
`;
|
||||
}
|
||||
|
||||
// Add format specifications for each enabled tracker
|
||||
if (extensionSettings.showUserStats) {
|
||||
const userStatsConfig = trackerConfig?.userStats;
|
||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
instructions += '```\n';
|
||||
instructions += codeBlockMarker + '\n';
|
||||
instructions += `${userName}'s Stats\n`;
|
||||
instructions += '---\n';
|
||||
|
||||
@@ -258,14 +272,14 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n';
|
||||
instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n';
|
||||
|
||||
instructions += '```\n\n';
|
||||
instructions += codeBlockMarker + '\n\n';
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox) {
|
||||
const infoBoxConfig = trackerConfig?.infoBox;
|
||||
const widgets = infoBoxConfig?.widgets || {};
|
||||
|
||||
instructions += '```\n';
|
||||
instructions += codeBlockMarker + '\n';
|
||||
instructions += 'Info Box\n';
|
||||
instructions += '---\n';
|
||||
|
||||
@@ -290,7 +304,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
|
||||
}
|
||||
|
||||
instructions += '```\n\n';
|
||||
instructions += codeBlockMarker + '\n\n';
|
||||
}
|
||||
|
||||
if (extensionSettings.showCharacterThoughts) {
|
||||
@@ -301,7 +315,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
const characterStats = presentCharsConfig?.characterStats;
|
||||
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
instructions += '```\n';
|
||||
instructions += codeBlockMarker + '\n';
|
||||
instructions += 'Present Characters\n';
|
||||
instructions += '---\n';
|
||||
|
||||
@@ -346,7 +360,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
||||
|
||||
instructions += `- … (Repeat the format above for every other present major character)\n`;
|
||||
|
||||
instructions += '```\n\n';
|
||||
instructions += codeBlockMarker + '\n\n';
|
||||
}
|
||||
|
||||
// Only add continuation instruction if includeContinuation is true
|
||||
@@ -560,8 +574,10 @@ export async function generateSeparateUpdatePrompt() {
|
||||
content: systemMessage
|
||||
});
|
||||
|
||||
// /hide command automatically handles checkpoint filtering
|
||||
// Add chat history as separate user/assistant messages
|
||||
const recentMessages = chat.slice(-depth);
|
||||
|
||||
for (const message of recentMessages) {
|
||||
messages.push({
|
||||
role: message.is_user ? 'user' : 'assistant',
|
||||
|
||||
Reference in New Issue
Block a user