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:
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Chapter Checkpoint Module
|
||||
* Allows users to mark messages as "chapter start" points to filter context
|
||||
* Uses SillyTavern's /hide and /unhide commands to exclude messages from context
|
||||
*/
|
||||
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat_metadata, saveChatDebounced } from '../../../../../../../script.js';
|
||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||
|
||||
// Track the message range that is currently hidden
|
||||
let currentlyHiddenRange = null;
|
||||
|
||||
// Debounce restore to prevent loops
|
||||
let isRestoring = false;
|
||||
let restoreTimeout = null;
|
||||
|
||||
/**
|
||||
* Gets the current chapter checkpoint message ID for the active chat
|
||||
* @returns {number|null} Message ID of the checkpoint, or null if none set
|
||||
*/
|
||||
export function getChapterCheckpoint() {
|
||||
const context = getContext();
|
||||
if (!context || !chat_metadata) return null;
|
||||
|
||||
return chat_metadata.rpg_companion_chapter_checkpoint || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a message as the chapter checkpoint
|
||||
* Automatically clears any previous checkpoint (only one checkpoint allowed at a time)
|
||||
* Hides all messages before the checkpoint
|
||||
* @param {number} messageId - The chat message index to set as checkpoint
|
||||
* @returns {Promise<boolean>} True if successful
|
||||
*/
|
||||
export async function setChapterCheckpoint(messageId) {
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
|
||||
if (!chat || messageId < 0 || messageId >= chat.length) {
|
||||
console.error('[RPG Companion] Invalid message ID for checkpoint:', messageId);
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousCheckpoint = chat_metadata.rpg_companion_chapter_checkpoint;
|
||||
|
||||
// If moving checkpoint, unhide the old range first
|
||||
if (previousCheckpoint !== null && previousCheckpoint !== undefined && previousCheckpoint !== messageId && currentlyHiddenRange !== null) {
|
||||
const { start, end } = currentlyHiddenRange;
|
||||
await executeSlashCommandsOnChatInput(`/unhide ${start}-${end}`, { quiet: true });
|
||||
console.log(`[RPG Companion] Unhid previous range: ${start}-${end}`);
|
||||
}
|
||||
|
||||
// Store in chat metadata (this automatically overrides any previous checkpoint)
|
||||
chat_metadata.rpg_companion_chapter_checkpoint = messageId;
|
||||
saveChatDebounced();
|
||||
|
||||
// Hide all messages before the checkpoint
|
||||
if (messageId > 0) {
|
||||
const rangeEnd = messageId - 1;
|
||||
await executeSlashCommandsOnChatInput(`/hide 0-${rangeEnd}`, { quiet: true });
|
||||
currentlyHiddenRange = { start: 0, end: rangeEnd };
|
||||
console.log(`[RPG Companion] Hidden messages 0-${rangeEnd} (checkpoint at ${messageId})`);
|
||||
}
|
||||
|
||||
if (previousCheckpoint !== null && previousCheckpoint !== undefined && previousCheckpoint !== messageId) {
|
||||
console.log(`[RPG Companion] Chapter checkpoint moved from message ${previousCheckpoint} to ${messageId}`);
|
||||
} else {
|
||||
console.log('[RPG Companion] Chapter checkpoint set at message', messageId);
|
||||
}
|
||||
|
||||
// Emit event for UI updates
|
||||
if (typeof document !== 'undefined') {
|
||||
const event = new CustomEvent('rpg-companion-checkpoint-changed', {
|
||||
detail: { messageId, previousCheckpoint }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the chapter checkpoint and unhides all hidden messages
|
||||
*/
|
||||
export async function clearChapterCheckpoint() {
|
||||
if (!chat_metadata) return;
|
||||
|
||||
// Unhide any hidden messages
|
||||
if (currentlyHiddenRange !== null) {
|
||||
const { start, end } = currentlyHiddenRange;
|
||||
await executeSlashCommandsOnChatInput(`/unhide ${start}-${end}`, { quiet: true });
|
||||
console.log(`[RPG Companion] Unhid messages ${start}-${end}`);
|
||||
currentlyHiddenRange = null;
|
||||
}
|
||||
|
||||
delete chat_metadata.rpg_companion_chapter_checkpoint;
|
||||
saveChatDebounced();
|
||||
|
||||
console.log('[RPG Companion] Chapter checkpoint cleared');
|
||||
|
||||
// Emit event for UI updates
|
||||
if (typeof document !== 'undefined') {
|
||||
const event = new CustomEvent('rpg-companion-checkpoint-changed', {
|
||||
detail: { messageId: null }
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a message is the current checkpoint
|
||||
* @param {number} messageId - The message index to check
|
||||
* @returns {boolean} True if this is the checkpoint message
|
||||
*/
|
||||
export function isCheckpointMessage(messageId) {
|
||||
const checkpointId = getChapterCheckpoint();
|
||||
return checkpointId === messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores checkpoint state after page reload or generation events
|
||||
* Checks if a checkpoint exists and re-applies the /hide command
|
||||
* Debounced to prevent loops when called from multiple events
|
||||
*/
|
||||
export async function restoreCheckpointOnLoad() {
|
||||
// Prevent concurrent executions
|
||||
if (isRestoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any pending timeout
|
||||
if (restoreTimeout) {
|
||||
clearTimeout(restoreTimeout);
|
||||
}
|
||||
|
||||
// Debounce: wait 100ms before actually restoring
|
||||
return new Promise((resolve) => {
|
||||
restoreTimeout = setTimeout(async () => {
|
||||
isRestoring = true;
|
||||
try {
|
||||
const checkpointId = getChapterCheckpoint();
|
||||
|
||||
if (checkpointId !== null && checkpointId !== undefined && checkpointId > 0) {
|
||||
const context = getContext();
|
||||
const chat = context.chat;
|
||||
|
||||
if (chat && checkpointId < chat.length) {
|
||||
const rangeEnd = checkpointId - 1;
|
||||
|
||||
// Check if messages are already hidden
|
||||
let needsRestore = false;
|
||||
let hiddenCount = 0;
|
||||
let visibleCount = 0;
|
||||
for (let i = 0; i <= rangeEnd; i++) {
|
||||
if (chat[i]) {
|
||||
if (chat[i].is_system) {
|
||||
hiddenCount++;
|
||||
} else {
|
||||
visibleCount++;
|
||||
needsRestore = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRestore) {
|
||||
await executeSlashCommandsOnChatInput(`/hide 0-${rangeEnd}`, { quiet: true });
|
||||
currentlyHiddenRange = { start: 0, end: rangeEnd };
|
||||
console.log(`[RPG Companion] Restored checkpoint: Hidden messages 0-${rangeEnd}`);
|
||||
} else {
|
||||
currentlyHiddenRange = { start: 0, end: rangeEnd };
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isRestoring = false;
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user