Compare commits

...

4 Commits

Author SHA1 Message Date
Alexander Brown
5f0afff729 fix(GraphCanvas): add ping-pong guard to palette setting watcher
Phase 4: Reactivity Fixes - prevent circular trigger when activePaletteId watcher sets Comfy.ColorPalette setting which would trigger the palette watcher back

Amp-Thread-ID: https://ampcode.com/threads/T-019bf966-9b22-70af-a5be-1c9c2deb3d1e
Co-authored-by: Amp <amp@ampcode.com>
2026-01-27 12:59:24 -08:00
Alexander Brown
3a2dbe68a3 refactor(GraphCanvas): Phase 3 - code organization fixes
- Consolidate two Vue node lifecycle reset watchers into one
- Remove duplicate useVueFeatureFlags() call
- Cache store references at top level instead of calling inside callbacks

Amp-Thread-ID: https://ampcode.com/threads/T-019bf963-9130-77df-bacc-7d4b1c5cae31
Co-authored-by: Amp <amp@ampcode.com>
2026-01-27 12:59:21 -08:00
Alexander Brown
bbec515a7b fix: GraphCanvas performance fixes (Phase 2)
- Remove deep: true from progress watcher

- Hoist useWorkflowStore() outside loop

- Add run-id race guards to async watchers (palette, background, locale)

Amp-Thread-ID: https://ampcode.com/threads/T-019bf961-0a09-71f3-93ac-8166b25ded66
Co-authored-by: Amp <amp@ampcode.com>
2026-01-27 12:55:04 -08:00
Alexander Brown
14f11cd51e refactor(GraphCanvas): Phase 1 - critical fixes
- Fix dead isNativeWindow template condition (was impossible branch)

- Add async cancellation guards to onMounted

- Add cleanup for onSelectionChange in onUnmounted

Amp-Thread-ID: https://ampcode.com/threads/T-019bf956-6121-720f-a40b-a0388bf7fb40
Co-authored-by: Amp <amp@ampcode.com>
2026-01-27 12:52:50 -08:00

View File

@@ -4,15 +4,15 @@
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
<template v-if="showUI" #workflow-tabs>
<!-- Native drag area for Electron (when tabs are NOT in topbar) -->
<div
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div
v-if="workflowTabsPosition === 'Topbar'"
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
>
<!-- Native drag area for Electron -->
<div
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div
class="flex h-full items-center border-b border-interface-stroke bg-comfy-menu-bg shadow-interface"
>
@@ -225,13 +225,11 @@ const handleVueNodeLifecycleReset = async () => {
}
}
watch(() => canvasStore.currentGraph, handleVueNodeLifecycleReset)
watch(
() => canvasStore.isInSubgraph,
async (newValue, oldValue) => {
if (oldValue && !newValue) {
useWorkflowStore().updateActiveGraph()
() => [canvasStore.currentGraph, canvasStore.isInSubgraph] as const,
async ([_graph, isInSubgraph], [_prevGraph, wasInSubgraph]) => {
if (wasInSubgraph && !isInSubgraph) {
workflowStore.updateActiveGraph()
}
await handleVueNodeLifecycleReset()
}
@@ -294,15 +292,18 @@ watch(
}
)
let paletteWatcherRunId = 0
watch(
[() => canvasStore.canvas, () => settingStore.get('Comfy.ColorPalette')],
async ([canvas, currentPaletteId]) => {
if (!canvas) return
const runId = ++paletteWatcherRunId
await colorPaletteService.loadColorPalette(currentPaletteId)
if (runId !== paletteWatcherRunId) return
}
)
let backgroundWatcherRunId = 0
watch(
() => settingStore.get('Comfy.Canvas.BackgroundImage'),
async () => {
@@ -310,8 +311,10 @@ watch(
const currentPaletteId = colorPaletteStore.activePaletteId
if (!currentPaletteId) return
const runId = ++backgroundWatcherRunId
// Reload color palette to apply background image
await colorPaletteService.loadColorPalette(currentPaletteId)
if (runId !== backgroundWatcherRunId) return
// Mark background canvas as dirty
canvasStore.canvas.setDirty(false, true)
}
@@ -319,7 +322,10 @@ watch(
watch(
() => colorPaletteStore.activePaletteId,
async (newValue) => {
await settingStore.set('Comfy.ColorPalette', newValue)
// Guard against ping-pong: only set if value actually differs
if (newValue && settingStore.get('Comfy.ColorPalette') !== newValue) {
await settingStore.set('Comfy.ColorPalette', newValue)
}
}
)
@@ -330,7 +336,7 @@ watch(
([nodeLocationProgressStates, canvas]) => {
if (!canvas?.graph) return
for (const node of canvas.graph.nodes) {
const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(node.id)
const nodeLocatorId = workflowStore.nodeIdToNodeLocatorId(node.id)
const progressState = nodeLocationProgressStates[nodeLocatorId]
if (progressState && progressState.state === 'running') {
node.progress = progressState.value / progressState.max
@@ -341,8 +347,7 @@ watch(
// Force canvas redraw to ensure progress updates are visible
canvas.setDirty(true, false)
},
{ deep: true }
}
)
// Update node slot errors for LiteGraph nodes
@@ -396,6 +401,7 @@ useEventListener(
const comfyAppReady = ref(false)
const workflowPersistence = useWorkflowPersistence()
const commandStore = useCommandStore()
const { flags } = useFeatureFlags()
// Set up invite loader during setup phase so useRoute/useRouter work correctly
const inviteUrlLoader = isCloud ? useInviteUrlLoader() : null
@@ -410,20 +416,26 @@ usePaste()
useWorkflowAutoSave()
// Start watching for locale change after the initial value is loaded.
let localeWatcherRunId = 0
watch(
() => settingStore.get('Comfy.Locale'),
async (_newLocale, oldLocale) => {
if (!oldLocale) return
const runId = ++localeWatcherRunId
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
if (runId !== localeWatcherRunId) return
await Promise.all([
until(() => isSettingsReady.value || !!settingsError.value).toBe(true),
until(() => isI18nReady.value || !!i18nError.value).toBe(true)
])
if (runId !== localeWatcherRunId) return
if (settingsError.value || i18nError.value) {
console.warn(
'Somehow the Locale setting was changed while the settings or i18n had a setup error'
)
}
await useCommandStore().execute('Comfy.RefreshNodeDefinitions')
await commandStore.execute('Comfy.RefreshNodeDefinitions')
if (runId !== localeWatcherRunId) return
await useWorkflowService().reloadCurrentWorkflow()
}
)
@@ -435,6 +447,10 @@ useEventListener(
}
)
let disposed = false
let prevOnSelectionChange: typeof comfyApp.canvas.onSelectionChange | null =
null
onMounted(async () => {
comfyApp.vueAppReady = true
workspaceStore.spinner = true
@@ -443,6 +459,7 @@ onMounted(async () => {
ChangeTracker.init()
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
if (disposed) return
if (settingsError.value) {
if (settingsError.value instanceof UnauthorizedError) {
@@ -463,6 +480,8 @@ onMounted(async () => {
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
newUserService().initializeIfNewUser(settingStore)
])
if (disposed) return
if (i18nError.value) {
console.warn(
'[GraphCanvas] Failed to load custom nodes i18n:',
@@ -472,6 +491,8 @@ onMounted(async () => {
// @ts-expect-error fixme ts strict error
await comfyApp.setup(canvasRef.value)
if (disposed) return
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false
@@ -484,6 +505,7 @@ onMounted(async () => {
vueNodeLifecycle.setupEmptyGraphListener()
prevOnSelectionChange = comfyApp.canvas.onSelectionChange
comfyApp.canvas.onSelectionChange = useChainCallback(
comfyApp.canvas.onSelectionChange,
() => canvasStore.updateSelectedItems()
@@ -496,10 +518,13 @@ onMounted(async () => {
// Restore saved workflow and workflow tabs state
await workflowPersistence.initializeWorkflow()
if (disposed) return
workflowPersistence.restoreWorkflowTabsState()
// Load template from URL if present
await workflowPersistence.loadTemplateFromUrlIfPresent()
if (disposed) return
// Accept workspace invite from URL if present (e.g., ?invite=TOKEN)
// Uses watch because feature flags load asynchronously - flag may be false initially
@@ -527,6 +552,10 @@ onMounted(async () => {
})
onUnmounted(() => {
disposed = true
vueNodeLifecycle.cleanup()
if (prevOnSelectionChange && comfyApp.canvas) {
comfyApp.canvas.onSelectionChange = prevOnSelectionChange
}
})
</script>