fix: resolve infinite loop in character visuals asset search and improve reference handling

This commit is contained in:
vegu-ai-tools
2026-04-25 19:00:55 +03:00
parent 6e6ceb5da3
commit dab547304e
3 changed files with 67 additions and 92 deletions

View File

@@ -36,6 +36,7 @@
- "Advance Time: Fixed toolbar time advancement being completely non-functional due to a missing websocket handler. Also fixed invalid ISO 8601 duration strings for the 1 week and 2 weeks options."
- "Prompts UI: Fixed the template preview CodeMirror editor being clipped at the bottom across the Active, group, and LLM prompt template tabs, so the bottom scrollbar is fully visible."
- "Node Graph: Fixed `data/MakeDict` and `data/MakeList` leaking their initial value as a shared mutable reference across graph executions. Downstream in-place mutations (e.g., writing into a dict fed into SetState) would persist on the node and appear on the next run as stale data. Both nodes now deep-copy their configured default before emitting it."
- "Character Visuals: Fixed an infinite loop in the cover image and portrait tabs that spammed `scene_assets/search` requests at the backend whenever a character had no scene assets to fall back on as references. The reference-asset search now runs at most once per character, breaking the watcher feedback loop that previously kept firing throughout the time the Generate dialog was open."
- "macOS Modifiers: Ctrl+click affordances throughout the UI now also accept Cmd (Meta) on macOS, where the OS reserves Ctrl+click for the system context menu. Tooltips, hints, and docs label the modifier as Cmd on Mac and Ctrl elsewhere. Affects regenerate, scene tools (narrator/director/actor/creative), world state manager, contextual generate, message asset image, autocomplete (Ctrl+Enter), and history navigation. (#261)"
improvements:
- "System Prompt Override Indicators: The system prompt override list now shows a pencil icon next to entries that have an active override, making it easy to see which prompts have been customized."

View File

@@ -532,7 +532,12 @@ export default {
},
watch: {
character: {
handler(newVal) {
handler(newVal, oldVal) {
// Must precede checkReferenceAssets() — the assets watcher (immediate: true)
// will also call it and relies on the gate already being reset.
if (newVal?.name !== oldVal?.name) {
this.hasCheckedReferences = false;
}
// Update local reactive references to default and current avatar
this.defaultAvatarId = newVal?.avatar || null;
this.currentAvatarId = newVal?.current_avatar || null;
@@ -684,50 +689,28 @@ export default {
this.userChangedReference = false;
this.hasCheckedReferences = true;
} else {
// No local assets found, try searching
// No local assets found
this.referenceAssetIds = [];
this.selectedReferenceAssetId = null;
this.referenceSelectionReason = null;
this.userChangedReference = false;
// Search for CHARACTER_PORTRAIT assets that can be used as references
this.getWebsocket().send(JSON.stringify({
type: 'scene_assets',
action: 'search',
vis_type: targetVisType,
character_name: this.character.name,
reference_vis_types: [targetVisType],
}));
// Search once per character. Mark as checked BEFORE sending so re-entry
// (e.g. via watchers firing while waiting for the response) doesn't spam
// the backend with identical search requests.
if (!this.hasCheckedReferences) {
this.hasCheckedReferences = true;
this.getWebsocket().send(JSON.stringify({
type: 'scene_assets',
action: 'search',
vis_type: targetVisType,
character_name: this.character.name,
reference_vis_types: [targetVisType],
}));
}
}
},
setReferenceAsset(assetId, reason) {
// Legacy method for backward compatibility with search results
// This sets a single asset, but we'll convert to ordered list
const asset = this.anyCharacterAssets.find(a => a.id === assetId);
if (asset) {
const targetVisType = 'CHARACTER_PORTRAIT';
const coverImageId = this.character?.cover_image;
const { selectedId, orderedIds } = computeCharacterReferenceOptions(
targetVisType,
this.anyCharacterAssets,
assetId,
this.assets, // same-vis-type assets
coverImageId // fallback
);
this.referenceAssetIds = orderedIds;
this.selectedReferenceAssetId = selectedId || assetId;
this.referenceSelectionReason = reason ? { reason } : null;
this.userChangedReference = false;
} else {
this.referenceAssetIds = [assetId];
this.selectedReferenceAssetId = assetId;
this.referenceSelectionReason = reason ? { reason } : null;
this.userChangedReference = false;
}
this.hasCheckedReferences = true;
},
onReferenceSelectionChange(newId) {
// Track that user manually changed the selection
if (newId !== this.selectedReferenceAssetId && this.referenceSelectionReason) {
@@ -911,18 +894,22 @@ export default {
// Handle asset search results
if (data.type === 'asset_search_results') {
if (data.character_name === this.character?.name &&
if (data.character_name === this.character?.name &&
data.vis_type === 'CHARACTER_PORTRAIT') {
const assetIds = data.asset_ids || [];
// Use explicit reference assets if found from search
// Apply results directly. Calling checkReferenceAssets() here would
// re-run the same logic, find no local assets, and fire another
// search — looping until the dialog is closed. Any local-asset
// changes that arrive after this request are picked up by the
// assets / character.cover_image / character.avatar watchers.
if (assetIds.length > 0) {
// Recompute with search results included
this.checkReferenceAssets();
} else {
// Fallback to local assets if search returned no explicit references
this.checkReferenceAssets();
this.referenceAssetIds = assetIds;
this.selectedReferenceAssetId = assetIds[0];
this.referenceSelectionReason = { reason: 'Found via asset search' };
this.userChangedReference = false;
}
this.hasCheckedReferences = true;
}
}

View File

@@ -498,7 +498,12 @@ export default {
},
watch: {
character: {
handler(newVal) {
handler(newVal, oldVal) {
// Must precede checkReferenceAssets() — the assets watcher (immediate: true)
// will also call it and relies on the gate already being reset.
if (newVal?.name !== oldVal?.name) {
this.hasCheckedReferences = false;
}
// Set selection to current cover image when character changes
const coverImageId = newVal?.cover_image || null;
this.selectedAssetId = coverImageId;
@@ -613,50 +618,28 @@ export default {
this.userChangedReference = false;
this.hasCheckedReferences = true;
} else {
// No local assets found, try searching
// No local assets found
this.referenceAssetIds = [];
this.selectedReferenceAssetId = null;
this.referenceSelectionReason = null;
this.userChangedReference = false;
// Search for CHARACTER_CARD assets that can be used as references
this.getWebsocket().send(JSON.stringify({
type: 'scene_assets',
action: 'search',
vis_type: targetVisType,
character_name: this.character.name,
reference_vis_types: [targetVisType],
}));
// Search once per character. Mark as checked BEFORE sending so re-entry
// (e.g. via watchers firing while waiting for the response) doesn't spam
// the backend with identical search requests.
if (!this.hasCheckedReferences) {
this.hasCheckedReferences = true;
this.getWebsocket().send(JSON.stringify({
type: 'scene_assets',
action: 'search',
vis_type: targetVisType,
character_name: this.character.name,
reference_vis_types: [targetVisType],
}));
}
}
},
setReferenceAsset(assetId, reason) {
// Legacy method for backward compatibility with search results
// This sets a single asset, but we'll convert to ordered list
const asset = this.anyCharacterAssets.find(a => a.id === assetId);
if (asset) {
const targetVisType = 'CHARACTER_CARD';
const avatarId = this.character?.avatar;
const { selectedId, orderedIds } = computeCharacterReferenceOptions(
targetVisType,
this.anyCharacterAssets,
assetId,
this.assets, // same-vis-type assets
avatarId // fallback
);
this.referenceAssetIds = orderedIds;
this.selectedReferenceAssetId = selectedId || assetId;
this.referenceSelectionReason = reason ? { reason } : null;
this.userChangedReference = false;
} else {
this.referenceAssetIds = [assetId];
this.selectedReferenceAssetId = assetId;
this.referenceSelectionReason = reason ? { reason } : null;
this.userChangedReference = false;
}
this.hasCheckedReferences = true;
},
onReferenceSelectionChange(newId) {
// Track that user manually changed the selection
if (newId !== this.selectedReferenceAssetId && this.referenceSelectionReason) {
@@ -824,18 +807,22 @@ export default {
// Handle asset search results
if (data.type === 'asset_search_results') {
if (data.character_name === this.character?.name &&
if (data.character_name === this.character?.name &&
data.vis_type === 'CHARACTER_CARD') {
const assetIds = data.asset_ids || [];
// Use explicit reference assets if found from search
// Apply results directly. Calling checkReferenceAssets() here would
// re-run the same logic, find no local assets, and fire another
// search — looping until the dialog is closed. Any local-asset
// changes that arrive after this request are picked up by the
// assets / character.cover_image / character.avatar watchers.
if (assetIds.length > 0) {
// Recompute with search results included
this.checkReferenceAssets();
} else {
// Fallback to local assets if search returned no explicit references
this.checkReferenceAssets();
this.referenceAssetIds = assetIds;
this.selectedReferenceAssetId = assetIds[0];
this.referenceSelectionReason = { reason: 'Found via asset search' };
this.userChangedReference = false;
}
this.hasCheckedReferences = true;
}
}