From 0f33444eef19bb4727bc3f6392f58d8409cf08bf Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Thu, 12 Feb 2026 15:37:02 -0500 Subject: [PATCH] fix: undo breaking Vue node image preview reactivity (#8839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary restoreOutputs was assigning the same object reference to both app.nodeOutputs and the Pinia reactive ref. This caused subsequent writes via setOutputsByLocatorId to mutate the reactive proxy's target through the raw reference before the proxy write, making Vue detect no change and skip reactivity updates permanently. Shallow-copy the outputs when assigning to the reactive ref so the proxy target remains a separate object from app.nodeOutputs. ## Screenshots before https://github.com/user-attachments/assets/98f2b17c-87b9-41e7-9caa-238e36c3c032 after https://github.com/user-attachments/assets/cb6e1d25-bd2e-41ed-a536-7b8250f858ec ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8839-fix-undo-breaking-Vue-node-image-preview-reactivity-3056d73d365081d2a1c7d4d9553f30e0) by [Unito](https://www.unito.io) --- src/stores/imagePreviewStore.test.ts | 41 ++++++++++++++++++++++++++++ src/stores/imagePreviewStore.ts | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/stores/imagePreviewStore.test.ts b/src/stores/imagePreviewStore.test.ts index b9a4ce2402..c4d0e08711 100644 --- a/src/stores/imagePreviewStore.test.ts +++ b/src/stores/imagePreviewStore.test.ts @@ -87,6 +87,47 @@ describe('imagePreviewStore setNodeOutputsByExecutionId with merge', () => { }) }) +describe('imagePreviewStore restoreOutputs', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) + vi.clearAllMocks() + app.nodeOutputs = {} + app.nodePreviewImages = {} + }) + + it('should keep reactivity after restoreOutputs followed by setNodeOutputsByExecutionId', () => { + const store = useNodeOutputStore() + + // Simulate execution: set outputs for node "4" (e.g., PreviewImage) + const executionOutput = createMockOutputs([ + { filename: 'ComfyUI_00001.png', subfolder: '', type: 'temp' } + ]) + const savedOutputs: Record = { + '4': executionOutput + } + + // Simulate undo: restoreOutputs makes app.nodeOutputs and the ref + // share the same underlying object if not handled correctly. + store.restoreOutputs(savedOutputs) + + expect(store.nodeOutputs['4']).toStrictEqual(executionOutput) + expect(store.nodeOutputs['3']).toBeUndefined() + + // Simulate widget callback setting outputs for node "3" (e.g., LoadImage) + const widgetOutput = createMockOutputs([ + { filename: 'example.png', subfolder: '', type: 'input' } + ]) + store.setNodeOutputsByExecutionId('3', widgetOutput) + + // The reactive store must reflect the new output. + // Before the fix, the raw write to app.nodeOutputs would mutate the + // proxy's target before the proxy write, causing Vue to skip the + // reactivity update. + expect(store.nodeOutputs['3']).toStrictEqual(widgetOutput) + expect(app.nodeOutputs['3']).toStrictEqual(widgetOutput) + }) +}) + describe('imagePreviewStore getPreviewParam', () => { beforeEach(() => { setActivePinia(createTestingPinia({ stubActions: false })) diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index 269a6234bb..93c7c8e793 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -365,7 +365,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { outputs: Record ) { app.nodeOutputs = outputs - nodeOutputs.value = outputs + nodeOutputs.value = { ...outputs } } function updateNodeImages(node: LGraphNode) {