From 299386aa0318a183a66cf2b253c6f5d8ff008b15 Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Wed, 28 Jan 2026 23:24:53 -0800 Subject: [PATCH 1/7] fix: suppress link rendering during slot sync after graph reconfigure After graph reconfiguration (e.g., undo/redo), there's a timing gap where canvas renders links before Vue components have mounted and synced slot positions to layoutStore. This adds a pendingSlotSync flag to layoutStore that: - Is set to true at start of graph.onConfigure() - Is cleared after flushScheduledSlotLayoutSync() completes - Causes drawConnections() to skip link rendering when true This prevents links from rendering with stale/missing positions during the brief window between configure() completing and Vue components syncing. Amp-Thread-ID: https://ampcode.com/threads/T-019c084c-49dd-764b-8125-8938c42612c8 Co-authored-by: Amp --- src/lib/litegraph/src/LGraphCanvas.ts | 3 +++ src/renderer/core/layout/store/layoutStore.ts | 14 ++++++++++++++ .../vueNodes/composables/useSlotElementTracking.ts | 10 ++++++++-- src/scripts/app.ts | 13 +++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 37c0950a234..a56627064f1 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -5614,6 +5614,9 @@ export class LGraphCanvas implements CustomEventDispatcher this.renderedPaths.clear() if (this.links_render_mode === LinkRenderType.HIDDEN_LINK) return + // Skip link rendering while waiting for slot positions to sync after reconfigure + if (LiteGraph.vueNodesMode && layoutStore.pendingSlotSync) return + const { graph, subgraph } = this if (!graph) throw new NullGraphError() diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index 786be06256e..ed928bb00bd 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -141,6 +141,20 @@ class LayoutStoreImpl implements LayoutStore { // Vue resizing state to prevent drag from activating during resize public isResizingVueNodes = ref(false) + /** + * Flag indicating slot positions are pending sync after graph reconfiguration. + * When true, link rendering should be skipped to avoid drawing with stale positions. + */ + private _pendingSlotSync = false + + get pendingSlotSync(): boolean { + return this._pendingSlotSync + } + + setPendingSlotSync(value: boolean): void { + this._pendingSlotSync = value + } + constructor() { // Initialize Yjs data structures this.ynodes = this.ydoc.getMap('nodes') diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts index c92fc8c9218..9f355f9fd0a 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -31,13 +31,19 @@ function scheduleSlotLayoutSync(nodeId: string) { raf.schedule() } -function flushScheduledSlotLayoutSync() { - if (pendingNodes.size === 0) return +export function flushScheduledSlotLayoutSync() { + if (pendingNodes.size === 0) { + // Even if no pending nodes, clear the flag (e.g., graph with no nodes) + layoutStore.setPendingSlotSync(false) + return + } const conv = useSharedCanvasPositionConversion() for (const nodeId of Array.from(pendingNodes)) { pendingNodes.delete(nodeId) syncNodeSlotLayoutsFromDOM(nodeId, conv) } + // Clear the pending sync flag - slots are now synced + layoutStore.setPendingSlotSync(false) } export function syncNodeSlotLayoutsFromDOM( diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 796965b27d5..b5ac60349c5 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -5,6 +5,8 @@ import { reactive, unref } from 'vue' import { shallowRef } from 'vue' import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { flushScheduledSlotLayoutSync } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking' import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget' import { st, t } from '@/i18n' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' @@ -730,6 +732,11 @@ export class ComfyApp { private addAfterConfigureHandler(graph: LGraph) { const { onConfigure } = graph graph.onConfigure = function (...args) { + // Set pending sync flag to suppress link rendering until slots are synced + if (LiteGraph.vueNodesMode) { + layoutStore.setPendingSlotSync(true) + } + fixLinkInputSlots(this) // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config @@ -740,6 +747,12 @@ export class ComfyApp { // Fire after onConfigure, used by primitives to generate widget using input nodes config triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured') + // Flush pending slot layout syncs to fix link alignment after undo/redo + if (LiteGraph.vueNodesMode) { + flushScheduledSlotLayoutSync() + app.canvas?.setDirty(true, true) + } + return r } } From bf21a0550b1568f377912d221953890f458a292b Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Thu, 29 Jan 2026 16:29:11 -0800 Subject: [PATCH 2/7] fix: clear reroute cache when skipping link render during slot sync Clears #visibleReroutes before the early return to prevent stale reroute entries from affecting hit-testing while pendingSlotSync is true. Addresses CodeRabbit review feedback. Amp-Thread-ID: https://ampcode.com/threads/T-019c0be1-0c2e-752d-b793-c6687e36c61c Co-authored-by: Amp --- src/lib/litegraph/src/LGraphCanvas.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index a56627064f1..705c36ba35b 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -5615,7 +5615,10 @@ export class LGraphCanvas implements CustomEventDispatcher if (this.links_render_mode === LinkRenderType.HIDDEN_LINK) return // Skip link rendering while waiting for slot positions to sync after reconfigure - if (LiteGraph.vueNodesMode && layoutStore.pendingSlotSync) return + if (LiteGraph.vueNodesMode && layoutStore.pendingSlotSync) { + this.#visibleReroutes.clear() + return + } const { graph, subgraph } = this if (!graph) throw new NullGraphError() From 2e1845bb81a1ef587fe1c0df05eac098b566e555 Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Thu, 29 Jan 2026 16:29:31 -0800 Subject: [PATCH 3/7] fix: handle race condition with late-mounting Vue components Re-asserts pendingSlotSync flag in scheduleSlotLayoutSync() so that Vue components mounting after the synchronous flushScheduledSlotLayoutSync() call in onConfigure will keep link rendering suppressed until their slots are synced. Only clears the flag when there are actual nodes to process, allowing the RAF callback to properly sync late mounts. Addresses review feedback from benceruleanlu. Amp-Thread-ID: https://ampcode.com/threads/T-019c0be1-0c2e-752d-b793-c6687e36c61c Co-authored-by: Amp --- .../vueNodes/composables/useSlotElementTracking.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts index 9f355f9fd0a..c416fb2f592 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -28,13 +28,17 @@ const raf = createRafBatch(() => { function scheduleSlotLayoutSync(nodeId: string) { pendingNodes.add(nodeId) + // Re-assert pending flag for late mounts (Vue components mounting after + // flushScheduledSlotLayoutSync was called synchronously in onConfigure) + layoutStore.setPendingSlotSync(true) raf.schedule() } export function flushScheduledSlotLayoutSync() { if (pendingNodes.size === 0) { - // Even if no pending nodes, clear the flag (e.g., graph with no nodes) - layoutStore.setPendingSlotSync(false) + // No pending nodes - don't clear the flag here as late mounts may still + // call scheduleSlotLayoutSync. The RAF callback will clear it after + // processing actual nodes. return } const conv = useSharedCanvasPositionConversion() From c2d548bfcdd632fbe5bc5a2d9c2ad3519be6cd90 Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Thu, 29 Jan 2026 16:30:27 -0800 Subject: [PATCH 4/7] fix: wrap onConfigure handler in try/finally for error resilience Ensures pendingSlotSync flag is cleared even if an error is thrown during graph configuration. This prevents links from being permanently hidden if onConfigure or its callbacks throw an exception. Addresses review feedback from benceruleanlu. Amp-Thread-ID: https://ampcode.com/threads/T-019c0be1-0c2e-752d-b793-c6687e36c61c Co-authored-by: Amp --- src/scripts/app.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index b5ac60349c5..5e82a333c9f 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -737,23 +737,26 @@ export class ComfyApp { layoutStore.setPendingSlotSync(true) } - fixLinkInputSlots(this) + try { + fixLinkInputSlots(this) - // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config - triggerCallbackOnAllNodes(this, 'onGraphConfigured') + // Fire callbacks before the onConfigure, this is used by widget inputs to setup the config + triggerCallbackOnAllNodes(this, 'onGraphConfigured') - const r = onConfigure?.apply(this, args) + const r = onConfigure?.apply(this, args) - // Fire after onConfigure, used by primitives to generate widget using input nodes config - triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured') + // Fire after onConfigure, used by primitives to generate widget using input nodes config + triggerCallbackOnAllNodes(this, 'onAfterGraphConfigured') - // Flush pending slot layout syncs to fix link alignment after undo/redo - if (LiteGraph.vueNodesMode) { - flushScheduledSlotLayoutSync() - app.canvas?.setDirty(true, true) + return r + } finally { + // Flush pending slot layout syncs to fix link alignment after undo/redo + // Using finally ensures links aren't permanently suppressed if an error occurs + if (LiteGraph.vueNodesMode) { + flushScheduledSlotLayoutSync() + app.canvas?.setDirty(true, true) + } } - - return r } } From 0e97abca7b40e42aefb6b9694807142b48eb44cc Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Thu, 29 Jan 2026 17:14:45 -0800 Subject: [PATCH 5/7] fix: clear pendingSlotSync in early return path for workflows without Vue nodes When no Vue nodes exist, pendingNodes stays empty but pendingSlotSync was set to true in app.ts. Without clearing the flag in the early return path, link rendering would be permanently suppressed for non-Vue-node workflows. Amp-Thread-ID: https://ampcode.com/threads/T-019c0c55-76a9-7128-88ff-f9c7b428a2a3 Co-authored-by: Amp --- .../vueNodes/composables/useSlotElementTracking.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts index c416fb2f592..cae48c79da3 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -36,9 +36,9 @@ function scheduleSlotLayoutSync(nodeId: string) { export function flushScheduledSlotLayoutSync() { if (pendingNodes.size === 0) { - // No pending nodes - don't clear the flag here as late mounts may still - // call scheduleSlotLayoutSync. The RAF callback will clear it after - // processing actual nodes. + // No pending nodes and no RAF scheduled - clear the flag to avoid + // permanently suppressing link rendering (e.g., workflows without Vue nodes) + layoutStore.setPendingSlotSync(false) return } const conv = useSharedCanvasPositionConversion() From 312d0aeae4f625f279a5527eaa7793e0bc3e77c2 Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Thu, 29 Jan 2026 17:38:54 -0800 Subject: [PATCH 6/7] fix: trigger canvas redraw after clearing pendingSlotSync flag After clearing the pendingSlotSync flag, the canvas needs to be marked dirty to trigger a redraw. Without this, links wouldn't appear until user interaction because the canvas didn't know it needed to re-render. Amp-Thread-ID: https://ampcode.com/threads/T-019c0c55-76a9-7128-88ff-f9c7b428a2a3 Co-authored-by: Amp --- .../extensions/vueNodes/composables/useSlotElementTracking.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts index cae48c79da3..dc54d49ca9a 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -12,6 +12,7 @@ import { useSharedCanvasPositionConversion } from '@/composables/element/useCanv import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { app } from '@/scripts/app' import type { SlotLayout } from '@/renderer/core/layout/types' import { isPointEqual, @@ -39,6 +40,7 @@ export function flushScheduledSlotLayoutSync() { // No pending nodes and no RAF scheduled - clear the flag to avoid // permanently suppressing link rendering (e.g., workflows without Vue nodes) layoutStore.setPendingSlotSync(false) + app.canvas?.setDirty(true, true) return } const conv = useSharedCanvasPositionConversion() @@ -48,6 +50,8 @@ export function flushScheduledSlotLayoutSync() { } // Clear the pending sync flag - slots are now synced layoutStore.setPendingSlotSync(false) + // Trigger canvas redraw now that links can render with correct positions + app.canvas?.setDirty(true, true) } export function syncNodeSlotLayoutsFromDOM( From 14d5bbe3b71f2e321dea3f006bc326e7aae8ff36 Mon Sep 17 00:00:00 2001 From: Subagent 5 Date: Thu, 29 Jan 2026 18:01:03 -0800 Subject: [PATCH 7/7] fix: only clear pendingSlotSync early for empty graphs When the synchronous flush is called with no pending nodes, we now check if the graph has any nodes. If it does, we keep the flag set to allow Vue components to mount and re-assert it. If the graph is empty (no nodes), we clear the flag since no Vue components will mount. This fixes the regression where links rendered with stale positions because the flag was cleared before Vue components had a chance to sync. Amp-Thread-ID: https://ampcode.com/threads/T-019c0c55-76a9-7128-88ff-f9c7b428a2a3 Co-authored-by: Amp --- .../vueNodes/composables/useSlotElementTracking.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts index dc54d49ca9a..05bbd643464 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -37,8 +37,15 @@ function scheduleSlotLayoutSync(nodeId: string) { export function flushScheduledSlotLayoutSync() { if (pendingNodes.size === 0) { - // No pending nodes and no RAF scheduled - clear the flag to avoid - // permanently suppressing link rendering (e.g., workflows without Vue nodes) + // No pending nodes - check if we should wait for Vue components to mount + const graph = app.canvas?.graph + const hasNodes = graph && graph._nodes && graph._nodes.length > 0 + if (hasNodes) { + // Graph has nodes but Vue hasn't mounted them yet - keep flag set + // so late mounts can re-assert it via scheduleSlotLayoutSync() + return + } + // No nodes in graph - safe to clear the flag (no Vue components will mount) layoutStore.setPendingSlotSync(false) app.canvas?.setDirty(true, true) return