fix: preserve input asset previews across execution updates (#9123)

## Summary

Fix input asset previews (images/videos) disappearing from
LoadImage/LoadVideo nodes after execution and a browser tab switch.

## Changes

- **What**: Guard `setOutputsByLocatorId` in `imagePreviewStore` to
preserve existing input-type preview images (`type: 'input'`) when the
incoming execution output has no images. Execution outputs with actual
images still overwrite as expected.

## Review Focus

- The guard only applies when existing output is an input preview (`type
=== 'input'` for all images) AND incoming output has no images — this is
the exact scenario where execution clobbers upload widget previews.
- Root cause: execution results from the backend overwrite the upload
widget's synthetic preview for LoadImage/LoadVideo nodes (which produce
no output images). Combined with the deferred resize-observer
re-observation from PR #8805, returning to a hidden tab reads the
now-empty store entry.
This commit is contained in:
Christian Byrne
2026-03-12 08:50:14 -07:00
committed by GitHub
parent 39ce4a23cc
commit 8b53d5c807
2 changed files with 140 additions and 0 deletions

View File

@@ -126,6 +126,22 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
})
}
/**
* Check if an output contains input-type preview images (from upload widgets).
* These are synthetic previews set by LoadImage/LoadVideo widgets, not
* execution results from the backend.
*/
function isInputPreviewOutput(
output: ExecutedWsMessage['output'] | ResultItem | undefined
): boolean {
const images = (output as ExecutedWsMessage['output'] | undefined)?.images
return (
Array.isArray(images) &&
images.length > 0 &&
images.every((i) => i?.type === 'input')
)
}
/**
* Internal function to set outputs by NodeLocatorId.
* Handles the merge logic when needed.
@@ -140,6 +156,26 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
// (e.g., two LoadImage nodes selecting the same image)
if (outputs == null) return
// Preserve input preview images (from upload widgets) when execution
// sends outputs with no images. Without this guard, execution results
// overwrite the upload widget's preview, causing LoadImage/LoadVideo
// nodes to lose their preview after execution + tab switch.
// Note: intentional preview clears go through setNodeOutputs (widget
// path), not setNodeOutputsByExecutionId, so this guard does not
// interfere with user-initiated clears.
const incomingImages = (outputs as ExecutedWsMessage['output']).images
const hasIncomingImages =
Array.isArray(incomingImages) && incomingImages.length > 0
if (
!hasIncomingImages &&
isInputPreviewOutput(app.nodeOutputs[nodeLocatorId])
) {
outputs = {
...outputs,
images: app.nodeOutputs[nodeLocatorId].images
}
}
if (options.merge) {
const existingOutput = app.nodeOutputs[nodeLocatorId]
if (existingOutput && outputs) {