First attempt at adding expression sync

This commit is contained in:
Tremendoussly
2026-03-08 22:29:14 +01:00
parent 2f98686e60
commit c73c0c2bb6
12 changed files with 803 additions and 1 deletions
+2
View File
@@ -30,6 +30,8 @@ export const defaultSettings = {
showInfoBox: true,
showCharacterThoughts: true,
showAlternatePresentCharactersPanel: false,
syncExpressionsToPresentCharacters: false,
hideDefaultExpressionDisplay: false,
showInventory: true, // Show inventory section (v2 system)
showQuests: true, // Show quests section
showLockIcons: true, // Show lock/unlock icons on tracker items
+21
View File
@@ -9,10 +9,13 @@ import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
syncedExpressionPortraits,
setExtensionSettings,
updateExtensionSettings,
setLastGeneratedData,
setCommittedTrackerData,
setSyncedExpressionPortraits,
clearSyncedExpressionPortraits,
FEATURE_FLAGS
} from './state.js';
import { migrateInventory } from '../utils/migration.js';
@@ -381,6 +384,16 @@ export function loadSettings() {
settingsChanged = true;
}
if (extensionSettings.syncExpressionsToPresentCharacters === undefined) {
extensionSettings.syncExpressionsToPresentCharacters = false;
settingsChanged = true;
}
if (extensionSettings.hideDefaultExpressionDisplay === undefined) {
extensionSettings.hideDefaultExpressionDisplay = false;
settingsChanged = true;
}
// Save migrated settings
if (settingsChanged) {
saveSettings();
@@ -465,6 +478,7 @@ export function saveChatData() {
quests: extensionSettings.quests,
lastGeneratedData: lastGeneratedData,
committedTrackerData: committedTrackerData,
syncedExpressionPortraits: syncedExpressionPortraits,
timestamp: Date.now()
};
@@ -548,6 +562,7 @@ export function loadChatData() {
infoBox: null,
characterThoughts: null
});
clearSyncedExpressionPortraits();
}
// Restore stats
@@ -596,6 +611,12 @@ export function loadChatData() {
// console.log('[RPG Companion] ⚠️ No lastGeneratedData found in save');
}
if (savedData?.syncedExpressionPortraits && typeof savedData.syncedExpressionPortraits === 'object') {
setSyncedExpressionPortraits(savedData.syncedExpressionPortraits);
} else {
clearSyncedExpressionPortraits();
}
// Migrate inventory in chat data if feature flag enabled
if (FEATURE_FLAGS.useNewInventory && extensionSettings.userStats.inventory) {
const migrationResult = migrateInventory(extensionSettings.userStats.inventory);
+36
View File
@@ -19,6 +19,8 @@ export let extensionSettings = {
showInfoBox: true,
showCharacterThoughts: true,
showAlternatePresentCharactersPanel: false,
syncExpressionsToPresentCharacters: false,
hideDefaultExpressionDisplay: false,
showInventory: true, // Show inventory section (v2 system)
showQuests: true, // Show quests section
showThoughtsInChat: true, // Show thoughts overlay in chat
@@ -361,6 +363,40 @@ export function clearSessionAvatarPrompts() {
sessionAvatarPrompts = {};
}
/**
* Per-chat storage for synced Character Expressions portraits.
* Maps normalized character names to the last captured expression image URL.
*/
export let syncedExpressionPortraits = {};
export function setSyncedExpressionPortrait(characterName, src) {
if (!characterName || !src) {
return;
}
syncedExpressionPortraits[characterName] = src;
}
export function removeSyncedExpressionPortrait(characterName) {
if (!characterName) {
return;
}
delete syncedExpressionPortraits[characterName];
}
export function setSyncedExpressionPortraits(portraits) {
syncedExpressionPortraits = portraits && typeof portraits === 'object' ? { ...portraits } : {};
}
export function getSyncedExpressionPortrait(characterName) {
return syncedExpressionPortraits[characterName] || null;
}
export function clearSyncedExpressionPortraits() {
syncedExpressionPortraits = {};
}
/**
* Tracks whether the last action was a swipe (for separate mode)
* Used to determine whether to commit lastGeneratedData to committedTrackerData
+4
View File
@@ -36,6 +36,10 @@
"template.settingsModal.display.showPresentCharactersNote": "Display character portraits with their current thoughts and status.",
"template.settingsModal.display.showBelowChatPresentCharacters": "Show Below-Chat Present Characters",
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Display a compact Present Characters panel below the chat.",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
"template.settingsModal.display.narratorMode": "Narrator Mode",
"template.settingsModal.display.narratorModeNote": "Use character card as narrator. Infer characters from context instead of using fixed character references.",
"template.settingsModal.display.showInventory": "Show Inventory",
+4
View File
@@ -37,6 +37,10 @@
"template.settingsModal.display.showPresentCharactersNote": "Afficher les portraits des personnages avec leurs pensées actuelles et leur statut.",
"template.settingsModal.display.showBelowChatPresentCharacters": "Afficher les personnages sous le chat",
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Afficher un panneau compact des personnages présents sous le chat.",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
"template.settingsModal.display.narratorMode": "Mode Narrateur",
"template.settingsModal.display.narratorModeNote": "Utiliser la carte de personnage comme narrateur. Déduire les personnages du contexte au lieu d'utiliser des références de personnages fixes.",
"template.settingsModal.display.showInventory": "Afficher Inventaire",
+4
View File
@@ -36,6 +36,10 @@
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
"template.settingsModal.display.showBelowChatPresentCharacters": "Показывать персонажей под чатом",
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Показывать компактную панель персонажей под чатом.",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
"template.settingsModal.display.narratorMode": "Режим расказчика",
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
"template.settingsModal.display.showInventory": "Показывать инвентарь",
+4
View File
@@ -32,6 +32,10 @@
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
"template.settingsModal.display.showBelowChatPresentCharacters": "顯示聊天下方的在場角色",
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方顯示精簡的在場角色面板。",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressions": "Sync Expressions in Below-Chat Panel",
"template.settingsModal.display.syncBelowChatPresentCharactersExpressionsNote": "Use each character's current SillyTavern expression portrait in the below-chat Present Characters panel.",
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
"template.settingsModal.display.showInventory": "顯示物品欄",
"template.settingsModal.display.showLockIcons": "顯示鎖定/解鎖追蹤器",
"template.settingsModal.display.showLockIconsNote": "在追蹤器項目上顯示鎖定/解鎖圖示,以防止 AI 修改它們。",
+632
View File
@@ -0,0 +1,632 @@
/**
* Character Expressions -> below-chat Present Characters portrait sync.
*
* Mirrors SillyTavern's currently displayed Character Expressions image into
* the alternate Present Characters panel, persisting the last known
* expression for each character until they speak again.
*/
import { chat } from '../../../../../../../script.js';
import {
extensionSettings,
syncedExpressionPortraits,
setSyncedExpressionPortrait,
getSyncedExpressionPortrait,
removeSyncedExpressionPortrait
} from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js';
import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js';
let expressionContainerObserver = null;
let expressionImageObserver = null;
let observedExpressionImage = null;
let pendingSpeakerName = null;
let pendingSpeakerBaselineSignature = null;
let pendingSpeakerQueuedAt = 0;
let lastCapturedExpressionSrc = null;
let scheduledCaptureTimers = [];
let hiddenExpressionStyleElement = null;
let pendingCaptureRequestId = 0;
function normalizeName(name) {
return String(name || '').trim().toLowerCase();
}
function normalizeExpressionSrc(src) {
return String(src || '').trim();
}
function resolveExpressionUrl(src) {
const normalized = normalizeExpressionSrc(src);
if (!normalized) {
return null;
}
try {
return new URL(normalized, window.location.href);
} catch {
return null;
}
}
function isDocumentLikeUrl(src) {
const candidate = resolveExpressionUrl(src);
if (!candidate) {
return false;
}
const current = new URL(window.location.href);
return candidate.origin === current.origin
&& candidate.pathname === current.pathname
&& candidate.search === current.search;
}
function isUsableExpressionSrc(src) {
const normalized = normalizeExpressionSrc(src);
if (!normalized) {
return false;
}
const lower = normalized.toLowerCase();
if (lower.includes('/img/default-expressions/') || lower.includes('/default-expressions/')) {
return false;
}
if (isDocumentLikeUrl(normalized)) {
return false;
}
return true;
}
function purgeInvalidSyncedExpressionPortraits() {
let changed = false;
for (const [storedName, src] of Object.entries(syncedExpressionPortraits)) {
if (!isUsableExpressionSrc(src)) {
removeSyncedExpressionPortrait(storedName);
changed = true;
}
}
if (changed) {
saveChatData();
}
return changed;
}
function namesMatch(a, b) {
const left = normalizeName(a);
const right = normalizeName(b);
if (!left || !right) {
return false;
}
return left === right || left.startsWith(right + ' ') || right.startsWith(left + ' ');
}
export function getExpressionPortraitForCharacter(characterName) {
const target = normalizeName(characterName);
if (!target) {
return null;
}
const exact = getSyncedExpressionPortrait(target);
if (isUsableExpressionSrc(exact)) {
return exact;
}
for (const [storedName, src] of Object.entries(syncedExpressionPortraits)) {
if (namesMatch(storedName, target) && isUsableExpressionSrc(src)) {
return src;
}
}
return null;
}
function getLatestAssistantSpeakerName() {
for (let i = chat.length - 1; i >= 0; i--) {
const message = chat[i];
if (!message || message.is_user || message.is_system) {
continue;
}
return message.name || null;
}
return null;
}
function shouldHideNativeExpressionDisplay() {
return extensionSettings.enabled === true && extensionSettings.hideDefaultExpressionDisplay === true;
}
function shouldRunExpressionObservers() {
return extensionSettings.enabled === true && (
extensionSettings.syncExpressionsToPresentCharacters === true
|| extensionSettings.hideDefaultExpressionDisplay === true
);
}
function isExpressionContainerNode(node) {
if (!(node instanceof Element)) {
return false;
}
return !!node.closest('#expression-wrapper, #expression-holder, .expression-holder, [data-expression-container], #visual-novel-wrapper');
}
function getExpressionImageState(img) {
if (!(img instanceof HTMLImageElement)) {
return null;
}
const rawSrc = normalizeExpressionSrc(img.getAttribute('src'));
const resolvedSrc = normalizeExpressionSrc(img.currentSrc || img.src || '');
const src = rawSrc || '';
const spriteFolderName = String(img.getAttribute('data-sprite-folder-name') || '').trim();
const spriteFileName = String(img.getAttribute('data-sprite-filename') || '').trim();
const expression = String(img.getAttribute('data-expression') || img.getAttribute('title') || '').trim();
const isDefault = img.classList.contains('default')
|| rawSrc.toLowerCase().includes('/img/default-expressions/')
|| resolvedSrc.toLowerCase().includes('/img/default-expressions/');
return {
src,
resolvedSrc,
spriteFolderName,
spriteFileName,
expression,
isDefault,
signature: JSON.stringify({
src,
resolvedSrc,
spriteFolderName,
spriteFileName,
expression,
isDefault
})
};
}
function hasMeaningfulMetadataValue(value) {
const normalized = String(value || '').trim().toLowerCase();
return Boolean(normalized && normalized !== 'null' && normalized !== 'undefined');
}
function looksLikeFallbackExpressionAsset(state) {
if (!state) {
return true;
}
const combined = [state.src, state.spriteFolderName, state.spriteFileName, state.expression]
.map(value => String(value || '').trim().toLowerCase())
.join(' ');
return [
'/img/default-expressions/',
'/default-expressions/',
'/emote/',
'/emotes/',
'/emoji/',
'/emotion/',
'/emotions/',
' default ',
' fallback ',
' placeholder '
].some(fragment => combined.includes(fragment.trim()));
}
function hasRealSyncedSprite(state) {
if (!state) {
return false;
}
if (!state.src || state.isDefault) {
return false;
}
if (!isUsableExpressionSrc(state.src)) {
return false;
}
if (!hasMeaningfulMetadataValue(state.spriteFolderName)) {
return false;
}
if (!hasMeaningfulMetadataValue(state.spriteFileName)) {
return false;
}
if (!hasMeaningfulMetadataValue(state.expression)) {
return false;
}
if (looksLikeFallbackExpressionAsset(state)) {
return false;
}
return true;
}
function isProbablyExpressionImage(img) {
if (!(img instanceof HTMLImageElement)) {
return false;
}
const state = getExpressionImageState(img);
if (!state?.src) {
return false;
}
const hasExpressionClass = img.classList.contains('expression') || img.id === 'expression-image';
const hasExpressionMetadata = Boolean(state.expression || state.spriteFolderName || state.spriteFileName);
if (!hasExpressionClass && !hasExpressionMetadata && !isExpressionContainerNode(img)) {
return false;
}
return true;
}
function getPreferredVisualNovelImage(speakerName) {
const target = normalizeName(speakerName);
const candidates = Array.from(document.querySelectorAll('#visual-novel-wrapper .expression-holder img'))
.filter(node => isProbablyExpressionImage(node) && hasRealSyncedSprite(getExpressionImageState(node)));
if (!candidates.length) {
return null;
}
if (!target) {
return candidates.find(node => node.offsetParent !== null) || candidates[0] || null;
}
const exactMatch = candidates.find(node => {
const state = getExpressionImageState(node);
const folderRoot = String(state?.spriteFolderName || '').split('/')[0];
return namesMatch(folderRoot, target);
});
if (exactMatch) {
return exactMatch;
}
return candidates.find(node => node.offsetParent !== null) || candidates[0] || null;
}
function findExpressionImageElement(speakerName = null) {
if (observedExpressionImage && observedExpressionImage.isConnected && isProbablyExpressionImage(observedExpressionImage)) {
return observedExpressionImage;
}
const preferredSelectors = [
'#expression-wrapper img.expression',
'#expression-holder > img.expression',
'#expression-image'
];
for (const selector of preferredSelectors) {
const nodes = Array.from(document.querySelectorAll(selector));
const visibleMatch = nodes.find(node => isProbablyExpressionImage(node)
&& hasRealSyncedSprite(getExpressionImageState(node))
&& node.offsetParent !== null);
if (visibleMatch) {
return visibleMatch;
}
const anyRealMatch = nodes.find(node =>
isProbablyExpressionImage(node) && hasRealSyncedSprite(getExpressionImageState(node)));
if (anyRealMatch) {
return anyRealMatch;
}
}
const visualNovelMatch = getPreferredVisualNovelImage(speakerName);
if (visualNovelMatch) {
return visualNovelMatch;
}
const allImages = Array.from(document.querySelectorAll('img.expression, #visual-novel-wrapper .expression-holder img'));
return allImages.find(node => isProbablyExpressionImage(node) && hasRealSyncedSprite(getExpressionImageState(node))) || null;
}
function refreshExpressionConsumers() {
renderAlternatePresentCharacters({ useCommittedFallback: true });
}
function getHideStyleCss() {
return `
#expression-image,
#expression-holder,
.expression-holder,
[data-expression-container],
#expression-image img,
#expression-holder img,
.expression-holder img,
[data-expression-container] img {
position: absolute !important;
left: -10000px !important;
top: 0 !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
visibility: hidden !important;
}
`;
}
function hideNativeExpressionDisplay() {
if (hiddenExpressionStyleElement?.isConnected) {
return;
}
const styleElement = document.createElement('style');
styleElement.id = 'rpg-hidden-native-expression-display-style';
styleElement.textContent = getHideStyleCss();
document.head.appendChild(styleElement);
hiddenExpressionStyleElement = styleElement;
}
function showNativeExpressionDisplay() {
if (hiddenExpressionStyleElement?.isConnected) {
hiddenExpressionStyleElement.remove();
} else {
document.getElementById('rpg-hidden-native-expression-display-style')?.remove();
}
hiddenExpressionStyleElement = null;
}
function syncNativeExpressionDisplayVisibility() {
if (shouldHideNativeExpressionDisplay()) {
hideNativeExpressionDisplay();
} else {
showNativeExpressionDisplay();
}
}
function teardownExpressionObservers() {
if (expressionContainerObserver) {
expressionContainerObserver.disconnect();
expressionContainerObserver = null;
}
if (expressionImageObserver) {
expressionImageObserver.disconnect();
expressionImageObserver = null;
}
observedExpressionImage = null;
}
function captureExpressionForSpeaker(speakerName, expectedRequestId = null) {
if (!extensionSettings.enabled || !extensionSettings.syncExpressionsToPresentCharacters) {
return false;
}
if (expectedRequestId !== null && expectedRequestId !== pendingCaptureRequestId) {
return false;
}
const name = normalizeName(speakerName || pendingSpeakerName || getLatestAssistantSpeakerName());
if (!name) {
return false;
}
const previous = getSyncedExpressionPortrait(name);
const img = findExpressionImageElement(name);
const state = getExpressionImageState(img);
if (!hasRealSyncedSprite(state)) {
const elapsed = pendingSpeakerQueuedAt ? (Date.now() - pendingSpeakerQueuedAt) : 0;
if (previous && elapsed >= 1200) {
removeSyncedExpressionPortrait(name);
saveChatData();
refreshExpressionConsumers();
}
return false;
}
pendingSpeakerName = name;
// After a speaker switch, SillyTavern may briefly keep showing the previous
// speaker's expression. Wait for the widget to actually change before storing.
if (pendingSpeakerBaselineSignature && state.signature === pendingSpeakerBaselineSignature && previous !== state.src) {
return false;
}
if (previous === state.src && lastCapturedExpressionSrc === state.src) {
return true;
}
lastCapturedExpressionSrc = state.src;
pendingSpeakerBaselineSignature = null;
setSyncedExpressionPortrait(name, state.src);
saveChatData();
refreshExpressionConsumers();
return true;
}
function observeExpressionImage(img) {
if (!shouldRunExpressionObservers()) {
return;
}
if (!img || observedExpressionImage === img) {
return;
}
if (expressionImageObserver) {
expressionImageObserver.disconnect();
}
observedExpressionImage = img;
expressionImageObserver = new MutationObserver(() => {
captureExpressionForSpeaker(pendingSpeakerName, pendingCaptureRequestId);
});
expressionImageObserver.observe(img, {
attributes: true,
attributeFilter: ['src', 'class', 'style', 'title', 'data-expression', 'data-sprite-folder-name', 'data-sprite-filename']
});
}
function ensureExpressionObservers() {
syncNativeExpressionDisplayVisibility();
if (!shouldRunExpressionObservers()) {
teardownExpressionObservers();
return;
}
const currentImg = findExpressionImageElement(pendingSpeakerName);
if (currentImg) {
observeExpressionImage(currentImg);
} else if (expressionImageObserver) {
expressionImageObserver.disconnect();
expressionImageObserver = null;
observedExpressionImage = null;
}
if (expressionContainerObserver) {
return;
}
expressionContainerObserver = new MutationObserver(() => {
if (!shouldRunExpressionObservers()) {
teardownExpressionObservers();
syncNativeExpressionDisplayVisibility();
return;
}
const img = findExpressionImageElement(pendingSpeakerName);
if (img) {
observeExpressionImage(img);
captureExpressionForSpeaker(pendingSpeakerName, pendingCaptureRequestId);
} else if (expressionImageObserver) {
expressionImageObserver.disconnect();
expressionImageObserver = null;
observedExpressionImage = null;
}
syncNativeExpressionDisplayVisibility();
});
expressionContainerObserver.observe(document.body, {
childList: true,
subtree: true
});
}
function clearScheduledCaptures() {
for (const timer of scheduledCaptureTimers) {
clearTimeout(timer);
}
scheduledCaptureTimers = [];
}
export function queueExpressionCaptureForSpeaker(speakerName) {
if (!extensionSettings.enabled || !extensionSettings.syncExpressionsToPresentCharacters) {
return;
}
pendingSpeakerName = normalizeName(speakerName || getLatestAssistantSpeakerName());
if (!pendingSpeakerName) {
return;
}
const currentImg = findExpressionImageElement(pendingSpeakerName);
const currentState = getExpressionImageState(currentImg);
pendingSpeakerBaselineSignature = currentState?.signature || null;
pendingSpeakerQueuedAt = Date.now();
pendingCaptureRequestId += 1;
const requestId = pendingCaptureRequestId;
ensureExpressionObservers();
clearScheduledCaptures();
for (const delay of [50, 200, 500, 900, 1500, 2200]) {
const timer = setTimeout(() => captureExpressionForSpeaker(pendingSpeakerName, requestId), delay);
scheduledCaptureTimers.push(timer);
}
}
export function syncExpressionFromLatestMessage() {
if (!extensionSettings.enabled || !extensionSettings.syncExpressionsToPresentCharacters) {
return;
}
queueExpressionCaptureForSpeaker(getLatestAssistantSpeakerName());
}
export function initExpressionSync() {
if (purgeInvalidSyncedExpressionPortraits()) {
refreshExpressionConsumers();
}
ensureExpressionObservers();
if (extensionSettings.syncExpressionsToPresentCharacters) {
syncExpressionFromLatestMessage();
}
}
export function onExpressionSyncChatChanged() {
if (!extensionSettings.enabled) {
showNativeExpressionDisplay();
return;
}
const purged = purgeInvalidSyncedExpressionPortraits();
if (purged) {
refreshExpressionConsumers();
}
const retryDelays = [0, 80, 220, 500];
for (const delay of retryDelays) {
setTimeout(() => {
ensureExpressionObservers();
syncNativeExpressionDisplayVisibility();
if (extensionSettings.syncExpressionsToPresentCharacters) {
syncExpressionFromLatestMessage();
} else {
refreshExpressionConsumers();
}
}, delay);
}
}
export function onExpressionSyncSettingChanged(enabled) {
if (enabled) {
const purged = purgeInvalidSyncedExpressionPortraits();
initExpressionSync();
if (!purged) {
refreshExpressionConsumers();
}
syncExpressionFromLatestMessage();
return;
}
ensureExpressionObservers();
clearScheduledCaptures();
pendingCaptureRequestId += 1;
pendingSpeakerName = null;
pendingSpeakerBaselineSignature = null;
pendingSpeakerQueuedAt = 0;
lastCapturedExpressionSrc = null;
refreshExpressionConsumers();
}
export function onHideDefaultExpressionDisplaySettingChanged(enabled) {
extensionSettings.hideDefaultExpressionDisplay = enabled === true;
ensureExpressionObservers();
syncNativeExpressionDisplayVisibility();
setTimeout(() => syncNativeExpressionDisplayVisibility(), 0);
setTimeout(() => syncNativeExpressionDisplayVisibility(), 120);
}
export function clearExpressionSyncCache() {
clearScheduledCaptures();
pendingCaptureRequestId += 1;
pendingSpeakerName = null;
pendingSpeakerBaselineSignature = null;
pendingSpeakerQueuedAt = 0;
lastCapturedExpressionSrc = null;
teardownExpressionObservers();
showNativeExpressionDisplay();
}
+4 -1
View File
@@ -1,5 +1,6 @@
import { extensionSettings } from '../../core/state.js';
import { i18n } from '../../core/i18n.js';
import { getExpressionPortraitForCharacter } from '../integration/expressionSync.js';
import {
getPresentCharactersTrackerData,
parsePresentCharacters,
@@ -132,7 +133,9 @@ export function renderAlternatePresentCharacters({ useCommittedFallback = true }
`;
for (const character of presentCharacters) {
const portrait = resolvePresentCharacterPortrait(character.name);
const portrait = (extensionSettings.syncExpressionsToPresentCharacters
? getExpressionPortraitForCharacter(character.name)
: null) || resolvePresentCharacterPortrait(character.name);
const name = escapeHtml(character.name || '');
html += `
+2
View File
@@ -11,6 +11,7 @@ import {
$infoBoxContainer,
$thoughtsContainer,
$userStatsContainer,
clearSyncedExpressionPortraits,
setPendingDiceRoll,
getPendingDiceRoll,
clearSessionAvatarPrompts
@@ -370,6 +371,7 @@ export function setupSettingsPopup() {
// Clear session avatar prompts
clearSessionAvatarPrompts();
clearSyncedExpressionPortraits();
// Clear chat metadata immediately (don't wait for debounced save)
const context = getContext();