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:
Spicy_Marinara
2025-12-18 01:59:14 +01:00
parent 8645bbde98
commit 3ded104218
13 changed files with 870 additions and 236 deletions
+24 -2
View File
@@ -90,6 +90,12 @@ import {
import { import {
initTrackerEditor initTrackerEditor
} from './src/systems/ui/trackerEditor.js'; } from './src/systems/ui/trackerEditor.js';
import {
initChapterCheckpointUI,
injectCheckpointButton,
updateAllCheckpointIndicators
} from './src/systems/ui/checkpointUI.js';
import { restoreCheckpointOnLoad } from './src/systems/features/chapterCheckpoint.js';
import { import {
togglePlotButtons, togglePlotButtons,
updateCollapseToggleIcon, updateCollapseToggleIcon,
@@ -129,7 +135,8 @@ import {
onCharacterChanged, onCharacterChanged,
onMessageSwiped, onMessageSwiped,
updatePersonaAvatar, updatePersonaAvatar,
clearExtensionPrompts clearExtensionPrompts,
onGenerationEnded
} from './src/systems/integration/sillytavern.js'; } from './src/systems/integration/sillytavern.js';
// Old state variable declarations removed - now imported from core modules // Old state variable declarations removed - now imported from core modules
@@ -366,6 +373,11 @@ async function initUI() {
saveSettings(); saveSettings();
}); });
$('#rpg-save-tracker-history').on('change', function() {
extensionSettings.saveTrackerHistory = $(this).prop('checked');
saveSettings();
});
$('#rpg-toggle-plot-buttons').on('change', function() { $('#rpg-toggle-plot-buttons').on('change', function() {
extensionSettings.enablePlotButtons = $(this).prop('checked'); extensionSettings.enablePlotButtons = $(this).prop('checked');
// console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons); // console.log('[RPG Companion] Toggle enablePlotButtons changed to:', extensionSettings.enablePlotButtons);
@@ -478,6 +490,7 @@ async function initUI() {
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
$('#rpg-generation-mode').val(extensionSettings.generationMode); $('#rpg-generation-mode').val(extensionSettings.generationMode);
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided); $('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
$('#rpg-save-tracker-history').prop('checked', extensionSettings.saveTrackerHistory);
updatePanelVisibility(); updatePanelVisibility();
updateSectionVisibility(); updateSectionVisibility();
@@ -515,6 +528,10 @@ async function initUI() {
setupContentEditableScrolling(); setupContentEditableScrolling();
initInventoryEventListeners(); initInventoryEventListeners();
// Initialize chapter checkpoint UI
initChapterCheckpointUI();
injectCheckpointButton();
// Setup Memory Recollection button in World Info // Setup Memory Recollection button in World Info
setupMemoryRecollectionButton(); setupMemoryRecollectionButton();
@@ -683,7 +700,9 @@ jQuery(async () => {
[event_types.MESSAGE_SENT]: onMessageSent, [event_types.MESSAGE_SENT]: onMessageSent,
[event_types.GENERATION_STARTED]: onGenerationStarted, [event_types.GENERATION_STARTED]: onGenerationStarted,
[event_types.MESSAGE_RECEIVED]: onMessageReceived, [event_types.MESSAGE_RECEIVED]: onMessageReceived,
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar], [event_types.GENERATION_STOPPED]: onGenerationEnded,
[event_types.GENERATION_ENDED]: onGenerationEnded,
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad],
[event_types.MESSAGE_SWIPED]: onMessageSwiped, [event_types.MESSAGE_SWIPED]: onMessageSwiped,
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar, [event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar [event_types.SETTINGS_UPDATED]: updatePersonaAvatar
@@ -693,6 +712,9 @@ jQuery(async () => {
throw error; // This is critical - can't continue without events throw error; // This is critical - can't continue without events
} }
// Restore checkpoint state if one exists
await restoreCheckpointOnLoad();
console.log('[RPG Companion] ✅ Extension loaded successfully'); console.log('[RPG Companion] ✅ Extension loaded successfully');
} catch (error) { } catch (error) {
console.error('[RPG Companion] ❌ Critical initialization failure:', error); console.error('[RPG Companion] ❌ Critical initialization failure:', error);
+1
View File
@@ -42,6 +42,7 @@ export const defaultSettings = {
// This setting helps compatibility with other extensions like GuidedGenerations. // This setting helps compatibility with other extensions like GuidedGenerations.
skipInjectionsForGuided: 'none', skipInjectionsForGuided: 'none',
enablePlotButtons: true, // Show plot progression buttons above chat input enablePlotButtons: true, // Show plot progression buttons above chat input
saveTrackerHistory: false, // Save tracker data in chat history for each message
panelPosition: 'right', // 'left', 'right', or 'top' panelPosition: 'right', // 'left', 'right', or 'top'
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: { customColors: {
+1
View File
@@ -24,6 +24,7 @@ export let extensionSettings = {
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default) customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility) skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
enablePlotButtons: true, // Show plot progression buttons above chat input enablePlotButtons: true, // Show plot progression buttons above chat input
saveTrackerHistory: false, // Save tracker data in chat history for each message
panelPosition: 'right', // 'left', 'right', or 'top' panelPosition: 'right', // 'left', 'right', or 'top'
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
customColors: { customColors: {
+169 -165
View File
@@ -1,165 +1,169 @@
{ {
"settings.language.label": "Language", "settings.language.label": "Language",
"settings.language.option.en": "English", "settings.language.option.en": "English",
"settings.language.option.zh-tw": "繁體中文", "settings.language.option.zh-tw": "繁體中文",
"settings.extensionEnabled": "Enable RPG Companion", "settings.extensionEnabled": "Enable RPG Companion",
"settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.", "settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.",
"template.settingsTitle": "RPG Companion Settings", "template.settingsTitle": "RPG Companion Settings",
"template.settingsModal.themeTitle": "Theme", "template.settingsModal.themeTitle": "Theme",
"template.settingsModal.themeLabel": "Visual Theme:", "template.settingsModal.themeLabel": "Visual Theme:",
"template.settingsModal.themeOptions.default": "Default", "template.settingsModal.themeOptions.default": "Default",
"template.settingsModal.themeOptions.sciFi": "Sci-Fi (Synthwave)", "template.settingsModal.themeOptions.sciFi": "Sci-Fi (Synthwave)",
"template.settingsModal.themeOptions.fantasy": "Fantasy (Rustic Parchment)", "template.settingsModal.themeOptions.fantasy": "Fantasy (Rustic Parchment)",
"template.settingsModal.themeOptions.cyberpunk": "Cyberpunk (Neon Grid)", "template.settingsModal.themeOptions.cyberpunk": "Cyberpunk (Neon Grid)",
"template.settingsModal.themeOptions.custom": "Custom", "template.settingsModal.themeOptions.custom": "Custom",
"template.settingsModal.themeOptions.custom.background": "Background:", "template.settingsModal.themeOptions.custom.background": "Background:",
"template.settingsModal.themeOptions.custom.accent": "Accent:", "template.settingsModal.themeOptions.custom.accent": "Accent:",
"template.settingsModal.themeOptions.custom.text": "Text:", "template.settingsModal.themeOptions.custom.text": "Text:",
"template.settingsModal.themeOptions.custom.highlight": "Highlight:", "template.settingsModal.themeOptions.custom.highlight": "Highlight:",
"template.settingsModal.theme.statBarLow": "Stat Bar Color (Low):", "template.settingsModal.theme.statBarLow": "Stat Bar Color (Low):",
"template.settingsModal.theme.statBarLowNote": "Color when stats are at 0%", "template.settingsModal.theme.statBarLowNote": "Color when stats are at 0%",
"template.settingsModal.theme.statBarHigh": "Stat Bar Color (High):", "template.settingsModal.theme.statBarHigh": "Stat Bar Color (High):",
"template.settingsModal.theme.statBarHighNote": "Color when stats are at 100%", "template.settingsModal.theme.statBarHighNote": "Color when stats are at 100%",
"template.settingsModal.displayTitle": "Display Options", "template.settingsModal.displayTitle": "Display Options",
"template.settingsModal.displayNote": "Use the Extensions tab to enable/disable the RPG Companion extension.", "template.settingsModal.displayNote": "Use the Extensions tab to enable/disable the RPG Companion extension.",
"template.settingsModal.display.panelPosition": "Panel Position:", "template.settingsModal.display.panelPosition": "Panel Position:",
"template.settingsModal.display.panelPositionOptions.right": "Right Sidebar", "template.settingsModal.display.panelPositionOptions.right": "Right Sidebar",
"template.settingsModal.display.panelPositionOptions.left": "Left Sidebar", "template.settingsModal.display.panelPositionOptions.left": "Left Sidebar",
"template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages", "template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages",
"template.settingsModal.display.showUserStats": "Show User Stats", "template.settingsModal.display.showUserStats": "Show User Stats",
"template.settingsModal.display.showInfoBox": "Show Info Box", "template.settingsModal.display.showInfoBox": "Show Info Box",
"template.settingsModal.display.showPresentCharacters": "Show Present Characters", "template.settingsModal.display.showPresentCharacters": "Show Present Characters",
"template.settingsModal.display.showInventory": "Show Inventory", "template.settingsModal.display.showInventory": "Show Inventory",
"template.settingsModal.display.showThoughtsInChat": "Show Thoughts in Chat", "template.settingsModal.display.showThoughtsInChat": "Show Thoughts in Chat",
"template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages", "template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages",
"template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble", "template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble",
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first", "template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
"template.settingsModal.display.enableAnimations": "Enable Animations", "template.settingsModal.display.enableAnimations": "Enable Animations",
"template.settingsModal.display.enableAnimationsNote": "Smooth transitions for stats, content updates, and dice rolls", "template.settingsModal.display.enableAnimationsNote": "Smooth transitions for stats, content updates, and dice rolls",
"template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons", "template.settingsModal.display.showPlotProgressionButtons": "Show Plot Progression Buttons",
"template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts", "template.settingsModal.display.showPlotProgressionButtonsNote": "Display buttons above chat input for plot progression prompts",
"template.settingsModal.display.enableDebugMode": "Enable Debug Mode", "template.settingsModal.display.enableDebugMode": "Enable Debug Mode",
"template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.", "template.settingsModal.display.enableDebugModeNote": "Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.",
"template.settingsModal.advancedTitle": "Advanced", "template.settingsModal.advancedTitle": "Advanced",
"template.settingsModal.advanced.generationMode": "Generation Mode:", "template.settingsModal.advanced.generationMode": "Generation Mode:",
"template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation", "template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation",
"template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation", "template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation",
"template.settingsModal.advanced.generationModeNote": "Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).", "template.settingsModal.advanced.generationModeNote": "Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).",
"template.settingsModal.advanced.contextMessages": "Context Messages:", "template.settingsModal.advanced.contextMessages": "Context Messages:",
"template.settingsModal.advanced.contextMessagesNote": "Number of recent messages to include (Separate mode only)", "template.settingsModal.advanced.contextMessagesNote": "Number of recent messages to include (Separate mode only)",
"template.settingsModal.advanced.memoryBatchSize": "Memory Batch Size:", "template.settingsModal.advanced.memoryBatchSize": "Memory Batch Size:",
"template.settingsModal.advanced.memoryBatchSizeNote": "Number of messages to process per batch in Memory Recollection", "template.settingsModal.advanced.memoryBatchSizeNote": "Number of messages to process per batch in Memory Recollection",
"template.settingsModal.advanced.useSeparatePreset": "Use model connected to RPG Companion Trackers preset", "template.settingsModal.advanced.useSeparatePreset": "Use model connected to RPG Companion Trackers preset",
"template.settingsModal.advanced.useSeparatePresetNote": "Separate mode only. When enabled, tracker generation will use the model from the \"RPG Companion Trackers\" preset instead of your main API model. The preset will be switched automatically during generation and restored afterward. Select the desired model in that preset and make sure the \"Bind presets to API connections\" toggle is on (next to the import/export preset buttons).", "template.settingsModal.advanced.useSeparatePresetNote": "Separate mode only. When enabled, tracker generation will use the model from the \"RPG Companion Trackers\" preset instead of your main API model. The preset will be switched automatically during generation and restored afterward. Select the desired model in that preset and make sure the \"Bind presets to API connections\" toggle is on (next to the import/export preset buttons).",
"template.settingsModal.advanced.skipInjections": "Skip Injections during Guided Generations:", "template.settingsModal.advanced.skipInjections": "Skip Injections during Guided Generations:",
"template.settingsModal.advanced.skipInjectionsOptions.none": "Never skip", "template.settingsModal.advanced.skipInjectionsOptions.none": "Never skip",
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Only on impersonation requests", "template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Only on impersonation requests",
"template.settingsModal.advanced.skipInjectionsOptions.guided": "Always for guided or quiet prompts", "template.settingsModal.advanced.skipInjectionsOptions.guided": "Always for guided or quiet prompts",
"template.settingsModal.advanced.skipInjectionsNote": "When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.", "template.settingsModal.advanced.skipInjectionsNote": "When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.",
"template.settingsModal.advanced.customHtmlPromptTitle": "Custom HTML Prompt:", "template.settingsModal.advanced.customHtmlPromptTitle": "Custom HTML Prompt:",
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Restore Default", "template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Restore Default",
"template.settingsModal.advanced.customHtmlPromptNote": "Customize the HTML prompt injected when \"Enable Immersive HTML\" is enabled. The default prompt is shown above - you can edit it directly or replace it entirely. Click \"Restore Default\" to reset. This affects all generation modes (together, separate, and plot progression).", "template.settingsModal.advanced.customHtmlPromptNote": "Customize the HTML prompt injected when \"Enable Immersive HTML\" is enabled. The default prompt is shown above - you can edit it directly or replace it entirely. Click \"Restore Default\" to reset. This affects all generation modes (together, separate, and plot progression).",
"template.settingsModal.advanced.clearCache": "Clear Extension Cache", "template.settingsModal.advanced.clearCache": "Clear Extension Cache",
"template.settingsModal.advanced.resetFabPositions": "Reset Button Positions", "template.settingsModal.advanced.resetFabPositions": "Reset Button Positions",
"template.settingsModal.advanced.resetFabPositionsNote": "Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen.", "template.settingsModal.advanced.resetFabPositionsNote": "Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen.",
"template.trackerEditorModal.title": "Edit Trackers", "template.trackerEditorModal.title": "Edit Trackers",
"template.trackerEditorModal.tabs.userStats": "User Stats", "template.trackerEditorModal.tabs.userStats": "User Stats",
"template.trackerEditorModal.tabs.infoBox": "Info Box", "template.trackerEditorModal.tabs.infoBox": "Info Box",
"template.trackerEditorModal.tabs.presentCharacters": "Present Characters", "template.trackerEditorModal.tabs.presentCharacters": "Present Characters",
"template.trackerEditorModal.buttons.reset": "Reset to Defaults", "template.trackerEditorModal.buttons.reset": "Reset to Defaults",
"template.trackerEditorModal.buttons.cancel": "Cancel", "template.trackerEditorModal.buttons.cancel": "Cancel",
"template.trackerEditorModal.buttons.save": "Save & Apply", "template.trackerEditorModal.buttons.save": "Save & Apply",
"template.trackerEditorModal.userStatsTab.customStatsTitle": "Custom Stats", "template.trackerEditorModal.userStatsTab.customStatsTitle": "Custom Stats",
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "Add Custom Stat", "template.trackerEditorModal.userStatsTab.addCustomStatButton": "Add Custom Stat",
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG Attributes", "template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG Attributes",
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Enable RPG Attributes Section", "template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Enable RPG Attributes Section",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt", "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Always Include Attributes in Prompt",
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.", "template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "If disabled, attributes are only sent when a dice roll is active.",
"template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute", "template.trackerEditorModal.userStatsTab.addAttributeButton": "Add Attribute",
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section", "template.trackerEditorModal.userStatsTab.statusSectionTitle": "Status Section",
"template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section", "template.trackerEditorModal.userStatsTab.enableStatusSection": "Enable Status Section",
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "Show Mood Emoji", "template.trackerEditorModal.userStatsTab.showMoodEmoji": "Show Mood Emoji",
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Status Fields (comma-separated):", "template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Status Fields (comma-separated):",
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Skills Section", "template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Skills Section",
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "Enable Skills Section", "template.trackerEditorModal.userStatsTab.enableSkillsSection": "Enable Skills Section",
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Skills Label:", "template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Skills Label:",
"template.trackerEditorModal.userStatsTab.skillsListLabel": "Skills List (comma-separated):", "template.trackerEditorModal.userStatsTab.skillsListLabel": "Skills List (comma-separated):",
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "Widgets", "template.trackerEditorModal.infoBoxTab.widgetsTitle": "Widgets",
"template.trackerEditorModal.infoBoxTab.dateWidget": "Date", "template.trackerEditorModal.infoBoxTab.dateWidget": "Date",
"template.trackerEditorModal.infoBoxTab.weatherWidget": "Weather", "template.trackerEditorModal.infoBoxTab.weatherWidget": "Weather",
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "Temperature", "template.trackerEditorModal.infoBoxTab.temperatureWidget": "Temperature",
"template.trackerEditorModal.infoBoxTab.timeWidget": "Time", "template.trackerEditorModal.infoBoxTab.timeWidget": "Time",
"template.trackerEditorModal.infoBoxTab.locationWidget": "Location", "template.trackerEditorModal.infoBoxTab.locationWidget": "Location",
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Recent Events", "template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Recent Events",
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Relationship Status Fields", "template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Relationship Status Fields",
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Define relationship types with corresponding emojis shown on character portraits", "template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Define relationship types with corresponding emojis shown on character portraits",
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "New Relationship", "template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "New Relationship",
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Appearance/Demeanor Fields", "template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Appearance/Demeanor Fields",
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Fields shown below character name, separated by |", "template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Fields shown below character name, separated by |",
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Add Custom Field", "template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Add Custom Field",
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Thoughts Configuration", "template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Thoughts Configuration",
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Enable Character Thoughts", "template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Enable Character Thoughts",
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Thoughts Label:", "template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Thoughts Label:",
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:", "template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:",
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats", "template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats",
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats", "template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats",
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars)", "template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars)",
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat", "template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat",
"template.mainPanel.title": "RPG Companion", "template.mainPanel.title": "RPG Companion",
"template.mainPanel.lastRoll": "Last Roll:", "template.mainPanel.lastRoll": "Last Roll:",
"template.mainPanel.clearLastRoll": "Clear last roll", "template.mainPanel.clearLastRoll": "Clear last roll",
"template.mainPanel.enableImmersiveHtml": "Enable Immersive HTML", "template.mainPanel.enableImmersiveHtml": "Enable Immersive HTML",
"template.mainPanel.refreshRpgInfo": "Refresh RPG Info", "template.mainPanel.refreshRpgInfo": "Refresh RPG Info",
"template.mainPanel.updating": "Updating...", "template.mainPanel.updating": "Updating...",
"template.mainPanel.editTrackersButton": "Edit Trackers", "template.mainPanel.editTrackersButton": "Edit Trackers",
"template.mainPanel.settingsButton": "Settings", "template.mainPanel.settingsButton": "Settings",
"global.none": "None", "global.none": "None",
"global.add": "Add", "global.add": "Add",
"global.cancel": "Cancel", "global.cancel": "Cancel",
"global.listView": "List view", "global.listView": "List view",
"global.gridView": "Grid view", "global.gridView": "Grid view",
"global.save": "Save", "global.save": "Save",
"global.status":"Status", "global.status":"Status",
"global.inventory":"Inventory", "global.inventory":"Inventory",
"global.quests":"Quests", "global.quests":"Quests",
"global.info":"Info", "global.info":"Info",
"infobox.noData.title": "No data yet", "infobox.noData.title": "No data yet",
"infobox.noData.instruction": "Generate a new response in the roleplay or switch to \"Separate Generation\" in Settings to access and click the \"Refresh RPG Info\" button", "infobox.noData.instruction": "Generate a new response in the roleplay or switch to \"Separate Generation\" in Settings to access and click the \"Refresh RPG Info\" button",
"infobox.recentEvents.title": "Recent Events", "infobox.recentEvents.title": "Recent Events",
"infobox.recentEvents.addEventPlaceholder": "Add event...", "infobox.recentEvents.addEventPlaceholder": "Add event...",
"inventory.section.onPerson": "On Person", "inventory.section.onPerson": "On Person",
"inventory.section.stored": "Stored", "inventory.section.stored": "Stored",
"inventory.section.assets": "Assets", "inventory.section.assets": "Assets",
"inventory.onPerson.empty": "No items carried", "inventory.onPerson.empty": "No items carried",
"inventory.onPerson.title": "Items Currently Carried", "inventory.onPerson.title": "Items Currently Carried",
"inventory.onPerson.addItemButton": "Add Item", "inventory.onPerson.addItemButton": "Add Item",
"inventory.onPerson.addItemPlaceholder": "Enter item name...", "inventory.onPerson.addItemPlaceholder": "Enter item name...",
"inventory.stored.title": "Storage Locations", "inventory.stored.title": "Storage Locations",
"inventory.stored.addLocationButton": "Add Location", "inventory.stored.addLocationButton": "Add Location",
"inventory.stored.addLocationPlaceholder": "Enter location name...", "inventory.stored.addLocationPlaceholder": "Enter location name...",
"inventory.stored.saveButton": "Save", "inventory.stored.saveButton": "Save",
"inventory.stored.empty": "No storage locations yet. Click \"Add Location\" to create one.", "inventory.stored.empty": "No storage locations yet. Click \"Add Location\" to create one.",
"inventory.stored.noItems": "No items stored here", "inventory.stored.noItems": "No items stored here",
"inventory.stored.addItemToLocationPlaceholder": "Enter item name...", "inventory.stored.addItemToLocationPlaceholder": "Enter item name...",
"inventory.stored.addItemButton": "Add Item", "inventory.stored.addItemButton": "Add Item",
"inventory.stored.confirmRemoveLocationMessage": "Remove \"${location}\"? This will delete all items stored there.", "inventory.stored.confirmRemoveLocationMessage": "Remove \"${location}\"? This will delete all items stored there.",
"inventory.stored.confirmRemoveLocationConfirmButton": "Confirm", "inventory.stored.confirmRemoveLocationConfirmButton": "Confirm",
"inventory.assets.empty": "No assets owned", "inventory.assets.empty": "No assets owned",
"inventory.assets.title": "Vehicles, Property & Major Possessions", "inventory.assets.title": "Vehicles, Property & Major Possessions",
"inventory.assets.addAssetModalTitle": "Add Asset", "inventory.assets.addAssetModalTitle": "Add Asset",
"inventory.assets.addAssetButton": "Add Asset", "inventory.assets.addAssetButton": "Add Asset",
"inventory.assets.addAssetPlaceholder": "Enter asset name...", "inventory.assets.addAssetPlaceholder": "Enter asset name...",
"inventory.assets.description": "Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).", "inventory.assets.description": "Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).",
"quests.section.main": "Main Quest", "quests.section.main": "Main Quest",
"quests.section.optional": "Optional Quests", "quests.section.optional": "Optional Quests",
"quests.main.title": "Main Quests", "quests.main.title": "Main Quests",
"quests.main.addQuestButton": "Add Quest", "quests.main.addQuestButton": "Add Quest",
"quests.main.addQuestPlaceholder": "Enter main quest title...", "quests.main.addQuestPlaceholder": "Enter main quest title...",
"quests.main.empty": "No active main quests", "quests.main.empty": "No active main quests",
"quests.main.hint": "The main quest represents your primary objective in the story.", "quests.main.hint": "The main quest represents your primary objective in the story.",
"quests.optional.title": "Optional Quests", "quests.optional.title": "Optional Quests",
"quests.optional.addQuestButton": "Add Quest", "quests.optional.addQuestButton": "Add Quest",
"quests.optional.addQuestPlaceholder": "Enter optional quest title...", "quests.optional.addQuestPlaceholder": "Enter optional quest title...",
"quests.optional.empty": "No active optional quests", "quests.optional.empty": "No active optional quests",
"quests.optional.hint": "Optional quests are side objectives that complement your main story." "quests.optional.hint": "Optional quests are side objectives that complement your main story.",
} "checkpoint.setChapterStart": "Set Chapter Start",
"checkpoint.clearChapterStart": "Clear Chapter Start",
"checkpoint.indicator": "Chapter Start",
"checkpoint.tooltip": "Messages before this point are excluded from context"
}
+181
View File
@@ -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);
});
}
+52 -50
View File
@@ -144,6 +144,35 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
// Store RPG data for the last assistant message (separate mode) // Store RPG data for the last assistant message (separate mode)
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null; 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'); // 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 && !lastMessage.is_user) {
if (!lastMessage.extra) { if (!lastMessage.extra) {
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); // 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 // Save to chat metadata
saveChatData(); saveChatData();
} }
+6 -1
View File
@@ -21,6 +21,7 @@ import {
generateContextualSummary, generateContextualSummary,
DEFAULT_HTML_PROMPT DEFAULT_HTML_PROMPT
} from './promptBuilder.js'; } from './promptBuilder.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
/** /**
* Event handler for generation start. * Event handler for generation start.
@@ -29,7 +30,7 @@ import {
* @param {string} type - Event type * @param {string} type - Event type
* @param {Object} data - Event data * @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] onGenerationStarted called');
// console.log('[RPG Companion] enabled:', extensionSettings.enabled); // console.log('[RPG Companion] enabled:', extensionSettings.enabled);
// console.log('[RPG Companion] generationMode:', extensionSettings.generationMode); // 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-html', '', extension_prompt_types.IN_CHAT, 0, false);
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, 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; const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
// For SEPARATE mode only: Check if we need to commit extension data // For SEPARATE mode only: Check if we need to commit extension data
+32
View File
@@ -159,6 +159,38 @@ export function parseResponse(responseText) {
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, ''); cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '');
debugLog('[RPG Parser] Removed thinking tags, new length:', cleanedResponse.length + ' chars'); 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 // Extract code blocks
const codeBlockRegex = /```([^`]+)```/g; const codeBlockRegex = /```([^`]+)```/g;
const matches = [...cleanedResponse.matchAll(codeBlockRegex)]; const matches = [...cleanedResponse.matchAll(codeBlockRegex)];
+26 -10
View File
@@ -5,7 +5,7 @@
import { getContext } from '../../../../../../extensions.js'; import { getContext } from '../../../../../../extensions.js';
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.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'; import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
// Type imports // Type imports
@@ -25,7 +25,8 @@ async function getCharacterCardsInfo() {
// Check if in group chat // Check if in group chat
if (selected_group) { 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); const groupMembers = getGroupMembers(selected_group);
if (groupMembers && groupMembers.length > 0) { if (groupMembers && groupMembers.length > 0) {
@@ -33,13 +34,15 @@ async function getCharacterCardsInfo() {
// Filter out disabled (muted) members // Filter out disabled (muted) members
const disabledMembers = group?.disabled_members || []; const disabledMembers = group?.disabled_members || [];
console.log('[RPG Companion] 🔍 Group ID:', selected_group, '| Disabled members:', disabledMembers);
let characterIndex = 0; let characterIndex = 0;
groupMembers.forEach((member) => { groupMembers.forEach((member) => {
if (!member || !member.name) return; if (!member || !member.name) return;
// Skip muted characters // Skip muted characters - check against avatar filename
if (member.avatar && disabledMembers.includes(member.avatar)) { if (member.avatar && disabledMembers.includes(member.avatar)) {
console.log(`[RPG Companion] ❌ Skipping muted: ${member.name} (${member.avatar})`);
return; return;
} }
@@ -204,16 +207,27 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
// Only add tracker instructions if at least one tracker is enabled // Only add tracker instructions if at least one tracker is enabled
if (hasAnyTrackers) { 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 // 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 // Add format specifications for each enabled tracker
if (extensionSettings.showUserStats) { if (extensionSettings.showUserStats) {
const userStatsConfig = trackerConfig?.userStats; const userStatsConfig = trackerConfig?.userStats;
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || []; const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
instructions += '```\n'; instructions += codeBlockMarker + '\n';
instructions += `${userName}'s Stats\n`; instructions += `${userName}'s Stats\n`;
instructions += '---\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 += '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 += '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) { if (extensionSettings.showInfoBox) {
const infoBoxConfig = trackerConfig?.infoBox; const infoBoxConfig = trackerConfig?.infoBox;
const widgets = infoBoxConfig?.widgets || {}; const widgets = infoBoxConfig?.widgets || {};
instructions += '```\n'; instructions += codeBlockMarker + '\n';
instructions += 'Info Box\n'; instructions += 'Info Box\n';
instructions += '---\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 += '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) { if (extensionSettings.showCharacterThoughts) {
@@ -301,7 +315,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
const characterStats = presentCharsConfig?.characterStats; const characterStats = presentCharsConfig?.characterStats;
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || []; const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
instructions += '```\n'; instructions += codeBlockMarker + '\n';
instructions += 'Present Characters\n'; instructions += 'Present Characters\n';
instructions += '---\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 += `- … (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 // Only add continuation instruction if includeContinuation is true
@@ -560,8 +574,10 @@ export async function generateSeparateUpdatePrompt() {
content: systemMessage content: systemMessage
}); });
// /hide command automatically handles checkpoint filtering
// Add chat history as separate user/assistant messages // Add chat history as separate user/assistant messages
const recentMessages = chat.slice(-depth); const recentMessages = chat.slice(-depth);
for (const message of recentMessages) { for (const message of recentMessages) {
messages.push({ messages.push({
role: message.is_user ? 'user' : 'assistant', role: message.is_user ? 'user' : 'assistant',
+34 -8
View File
@@ -34,6 +34,10 @@ import { renderQuests } from '../rendering/quests.js';
// Utils // Utils
import { getSafeThumbnailUrl } from '../../utils/avatars.js'; import { getSafeThumbnailUrl } from '../../utils/avatars.js';
// Chapter checkpoint
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
/** /**
* Commits the tracker data from the last assistant message to be used as source for next generation. * 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 * This should be called when the user has replied to a message, ensuring all swipes of the next
@@ -158,14 +162,20 @@ export async function onMessageReceived(data) {
// Remove the tracker code blocks from the visible message // Remove the tracker code blocks from the visible message
let cleanedMessage = responseText; let cleanedMessage = responseText;
// Remove all code blocks that contain tracker data
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, ''); // Only remove trackers if saveTrackerHistory is disabled
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, ''); // When enabled, trackers are in <trackers> XML tags which SillyTavern auto-hides
cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, ''); if (!extensionSettings.saveTrackerHistory) {
// Remove any stray "---" dividers that might appear after the code blocks // Remove all code blocks that contain tracker data
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, ''); cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
// Clean up multiple consecutive newlines cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n'); cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, '');
// Remove any stray "---" dividers that might appear after the code blocks
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
// Clean up multiple consecutive newlines
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
}
// Note: <trackers> XML tags are automatically hidden by SillyTavern
// Update the message in chat history // Update the message in chat history
lastMessage.mes = cleanedMessage.trim(); lastMessage.mes = cleanedMessage.trim();
@@ -213,6 +223,9 @@ export async function onMessageReceived(data) {
setIsPlotProgression(false); setIsPlotProgression(false);
// console.log('[RPG Companion] Plot progression generation completed'); // console.log('[RPG Companion] Plot progression generation completed');
} }
// Re-apply checkpoint in case SillyTavern unhid messages during generation
await restoreCheckpointOnLoad();
} }
/** /**
@@ -243,6 +256,9 @@ export function onCharacterChanged() {
// Update chat thought overlays // Update chat thought overlays
updateChatThoughts(); updateChatThoughts();
// Update checkpoint indicators for the loaded chat
updateAllCheckpointIndicators();
} }
/** /**
@@ -362,3 +378,13 @@ export function clearExtensionPrompts() {
// Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option // Note: rpg-companion-plot is not cleared here since it's passed via quiet_prompt option
// console.log('[RPG Companion] Cleared all extension prompts'); // console.log('[RPG Companion] Cleared all extension prompts');
} }
/**
* Event handler for when generation stops or ends
* Re-applies checkpoint if SillyTavern unhid messages
*/
export async function onGenerationEnded() {
// SillyTavern may auto-unhide messages when generation stops
// Re-apply checkpoint if one exists
await restoreCheckpointOnLoad();
}
+285
View File
@@ -0,0 +1,285 @@
/**
* Chapter Checkpoint UI Module
* Adds UI elements for chapter checkpoint functionality
*/
import { getContext } from '../../../../../../extensions.js';
import { i18n } from '../../core/i18n.js';
import {
setChapterCheckpoint,
clearChapterCheckpoint,
isCheckpointMessage
} from '../features/chapterCheckpoint.js';
/**
* Adds the chapter checkpoint button to a message's extra menu
* @param {number} messageId - The message index
* @param {HTMLElement} menu - The message menu element
*/
export function addCheckpointButtonToMessage(messageId, menu) {
if (!menu) return;
const isCheckpoint = isCheckpointMessage(messageId);
// Create the menu item
const menuItem = document.createElement('div');
menuItem.className = 'extraMesButtonsHint list-group-item flex-container flexGap5';
const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
menuItem.setAttribute('data-i18n', translationKey);
menuItem.title = isCheckpoint
? 'Clear Chapter Start'
: 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones';
// Icon only (no text label)
const icon = document.createElement('i');
icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
icon.style.color = isCheckpoint ? '#4a9eff' : '';
menuItem.appendChild(icon);
// Click handler
menuItem.addEventListener('click', (e) => {
e.stopPropagation();
const wasCheckpoint = isCheckpointMessage(messageId);
if (wasCheckpoint) {
clearChapterCheckpoint();
} else {
setChapterCheckpoint(messageId);
}
// Update this button immediately
const newIsCheckpoint = isCheckpointMessage(messageId);
icon.className = newIsCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
icon.style.color = newIsCheckpoint ? '#4a9eff' : '';
menuItem.title = newIsCheckpoint
? 'Clear Chapter Start'
: 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones';
const newTranslationKey = newIsCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
menuItem.setAttribute('data-i18n', newTranslationKey);
// Update indicators in all messages
updateAllCheckpointIndicators();
});
return menuItem;
}
/**
* Adds visual indicators to messages that are checkpoints
* @param {number} messageId - The message index
* @param {HTMLElement} messageBlock - The message DOM element
*/
export function addCheckpointIndicator(messageId, messageBlock) {
if (!messageBlock) return;
const isCheckpoint = isCheckpointMessage(messageId);
// Remove existing indicator if present
const existingIndicator = messageBlock.querySelector('.rpg-checkpoint-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
if (!isCheckpoint) return;
// Add checkpoint indicator
const indicator = document.createElement('div');
indicator.className = 'rpg-checkpoint-indicator';
const indicatorText = i18n.getTranslation('checkpoint.indicator') || 'Chapter Start';
const tooltipText = i18n.getTranslation('checkpoint.tooltip') || 'Messages before this point are excluded from context';
indicator.innerHTML = `
<i class="fa-solid fa-bookmark"></i>
<span>${indicatorText}</span>
`;
indicator.title = tooltipText;
// Insert at the beginning of the message
const mesText = messageBlock.querySelector('.mes_text');
if (mesText && mesText.parentNode) {
mesText.parentNode.insertBefore(indicator, mesText);
}
}
/**
* Updates checkpoint indicators for all messages
*/
export function updateAllCheckpointIndicators() {
const context = getContext();
const chat = context.chat;
if (!chat) return;
// Clear all processed flags so buttons can be updated
document.querySelectorAll('.extraMesButtons[data-checkpoint-processed]').forEach(menu => {
delete menu.dataset.checkpointProcessed;
});
// Update all message blocks
const messageBlocks = document.querySelectorAll('.mes');
messageBlocks.forEach((block) => {
// Get the actual message ID from the mesid attribute
const messageId = Number(block.getAttribute('mesid'));
if (isNaN(messageId)) return;
addCheckpointIndicator(messageId, block);
// Also update any open menus for this message
const menu = block.querySelector('.extraMesButtons');
if (menu) {
updateCheckpointButtonInMenu(menu, messageId);
}
});
}
/**
* Initializes the chapter checkpoint UI
*/
export function initChapterCheckpointUI() {
// Listen for checkpoint changes
document.addEventListener('rpg-companion-checkpoint-changed', () => {
updateAllCheckpointIndicators();
});
// Listen for chat changes to update indicators
const context = getContext();
if (context && context.eventSource) {
// Update checkpoint indicators when messages are rendered
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE &&
node.classList && node.classList.contains('mes')) {
shouldUpdate = true;
}
});
});
if (shouldUpdate) {
// Debounce updates to avoid excessive re-rendering
clearTimeout(window.rpgCheckpointUpdateTimeout);
window.rpgCheckpointUpdateTimeout = setTimeout(() => {
updateAllCheckpointIndicators();
}, 100);
}
});
const chatContainer = document.getElementById('chat');
if (chatContainer) {
observer.observe(chatContainer, {
childList: true,
subtree: false
});
}
}
// Update indicators on initialization
updateAllCheckpointIndicators();
}
/**
* Injects checkpoint button into message menus
* This should be called when SillyTavern renders message menus
*/
export function injectCheckpointButton() {
// Direct approach: Hook into when extraMesButtons elements appear or are populated
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Check for added nodes
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if extraMesButtons container was added
if (node.classList && node.classList.contains('extraMesButtons')) {
processExtraMesButtons(node);
}
// Also check if extraMesButtons exists within added subtree
if (node.querySelector) {
const extraButtons = node.querySelectorAll('.extraMesButtons');
extraButtons.forEach(processExtraMesButtons);
}
}
});
// Check if nodes were added TO an extraMesButtons container
if (mutation.target && mutation.target.classList &&
mutation.target.classList.contains('extraMesButtons')) {
processExtraMesButtons(mutation.target);
}
});
});
// Observe the chat container
const chatContainer = document.getElementById('chat');
if (chatContainer) {
observer.observe(chatContainer, {
childList: true,
subtree: true
});
// Process any existing menus on initialization
const existingMenus = chatContainer.querySelectorAll('.extraMesButtons');
existingMenus.forEach(processExtraMesButtons);
}
}
/**
* Process an extraMesButtons container to add checkpoint button
* @param {HTMLElement} menu - The extraMesButtons container
*/
function processExtraMesButtons(menu) {
if (!menu) return;
// Find the message block
const messageBlock = menu.closest('.mes');
if (!messageBlock) return;
// Get the message ID from the mesid attribute (SillyTavern's standard way)
const messageId = Number(messageBlock.getAttribute('mesid'));
if (isNaN(messageId)) return;
// Check if button already exists
if (!menu.dataset.checkpointProcessed) {
// Mark as processed
menu.dataset.checkpointProcessed = 'true';
// Add checkpoint button
const checkpointBtn = addCheckpointButtonToMessage(messageId, menu);
if (checkpointBtn) {
checkpointBtn.classList.add('rpg-checkpoint-button');
menu.appendChild(checkpointBtn);
}
}
}
/**
* Update the checkpoint button in an existing menu
* @param {HTMLElement} menu - The extraMesButtons container
* @param {number} messageId - The message index
*/
function updateCheckpointButtonInMenu(menu, messageId) {
if (!menu) return;
const existingButton = menu.querySelector('.rpg-checkpoint-button');
if (!existingButton) return;
const isCheckpoint = isCheckpointMessage(messageId);
// Update icon
const icon = existingButton.querySelector('i');
if (icon) {
icon.className = isCheckpoint ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark';
icon.style.color = isCheckpoint ? '#4a9eff' : '';
}
// Update tooltip
existingButton.title = isCheckpoint
? 'Clear Chapter Start'
: 'Set Chapter Start: When bookmarked, this message will count as the first message in the chat history, skipping earlier ones';
const translationKey = isCheckpoint ? 'checkpoint.clearChapterStart' : 'checkpoint.setChapterStart';
existingButton.setAttribute('data-i18n', translationKey);
}
+51
View File
@@ -6637,3 +6637,54 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
font-size: clamp(14px, 3vw, 18px) !important; font-size: clamp(14px, 3vw, 18px) !important;
} }
} }
/* ============================================
CHAPTER CHECKPOINT STYLES
============================================ */
/* Checkpoint indicator in messages */
.rpg-checkpoint-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
margin-bottom: 12px;
background: linear-gradient(135deg, rgba(74, 158, 255, 0.15) 0%, rgba(74, 158, 255, 0.05) 100%);
border-left: 4px solid #4a9eff;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
color: #4a9eff;
animation: checkpoint-fade-in 0.3s ease-out;
}
.rpg-checkpoint-indicator i {
font-size: 16px;
}
@keyframes checkpoint-fade-in {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Optional: Add a subtle glow effect for checkpoint messages */
.mes:has(.rpg-checkpoint-indicator) {
position: relative;
}
.mes:has(.rpg-checkpoint-indicator)::before {
content: '';
position: absolute;
left: -4px;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, #4a9eff 0%, transparent 100%);
opacity: 0.5;
}
+8
View File
@@ -267,6 +267,14 @@
When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions. When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.
</small> </small>
<label class="checkbox_label" style="margin-top: 16px;">
<input type="checkbox" id="rpg-save-tracker-history" />
<span data-i18n-key="template.settingsModal.advanced.saveTrackerHistory">Save Tracker History in Chat</span>
</label>
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;" data-i18n-key="template.settingsModal.advanced.saveTrackerHistoryNote">
When enabled, tracker data is saved in chat history for each message. In Together mode, trackers appear in &lt;trackers&gt; XML tags (hidden from display). In Separate mode, tracker data is stored in message metadata. When disabled, only the most recent trackers are kept.
</small>
<!-- Custom HTML Prompt Editor --> <!-- Custom HTML Prompt Editor -->
<div class="rpg-setting-row" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--rpg-border);"> <div class="rpg-setting-row" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--rpg-border);">
<label for="rpg-custom-html-prompt" style="display: block; margin-bottom: 8px; font-weight: 600;" data-i18n-key="template.settingsModal.advanced.customHtmlPromptTitle"> <label for="rpg-custom-html-prompt" style="display: block; margin-bottom: 8px; font-weight: 600;" data-i18n-key="template.settingsModal.advanced.customHtmlPromptTitle">