Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion browser_tests/fixtures/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const webSocketFixture = base.extend<{
function ([data, url]) {
if (!url) {
// If no URL specified, use page URL
const u = new URL(window.location.toString())
const u = new URL(window.location.toString().split('#')[0])
u.protocol = 'ws:'
u.pathname = '/'
url = u.toString() + 'ws'
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"@tiptap/starter-kit": "^2.10.4",
"@vueuse/core": "catalog:",
"@vueuse/integrations": "catalog:",
"@vueuse/router": "^14.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/xterm": "^5.5.0",
Expand Down
30 changes: 30 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/components/breadcrumb/SubgraphBreadcrumbItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
showDelay: 512
}"
draggable="false"
href="#"
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
:class="{
'flex items-center gap-1': isActive,
Expand Down
2 changes: 2 additions & 0 deletions src/scripts/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useModelStore } from '@/stores/modelStore'
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { useSubgraphStore } from '@/stores/subgraphStore'
import { useWidgetStore } from '@/stores/widgetStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
Expand Down Expand Up @@ -1305,6 +1306,7 @@ export class ComfyApp {
workflow,
this.graph.serialize() as unknown as ComfyWorkflowJSON
)
useSubgraphNavigationStore().updateHash()
requestAnimationFrame(() => {
this.graph.setDirtyCanvas(true, true)
})
Expand Down
74 changes: 71 additions & 3 deletions src/stores/subgraphNavigationStore.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import QuickLRU from '@alloc/quick-lru'
import { defineStore } from 'pinia'
import { computed, ref, shallowRef, watch } from 'vue'
import { useRouteHash } from '@vueuse/router'

import type { DragAndScaleState } from '@/lib/litegraph/src/DragAndScale'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import router from '@/router'
import { app } from '@/scripts/app'
import { findSubgraphPathById } from '@/utils/graphTraversalUtil'
import { isNonNullish } from '@/utils/typeGuardUtil'
import { isNonNullish, isSubgraph } from '@/utils/typeGuardUtil'

/**
* Stores the current subgraph navigation state; a stack representing subgraph
Expand Down Expand Up @@ -38,8 +41,6 @@ export const useSubgraphNavigationStore = defineStore(
*/
const getCurrentRootGraphId = () => {
const canvas = canvasStore.getCanvas()
if (!canvas) return 'root'

return canvas.graph?.rootGraph?.id ?? 'root'
}

Expand Down Expand Up @@ -157,6 +158,72 @@ export const useSubgraphNavigationStore = defineStore(
onNavigated(newValue, oldValue)
}
)
const routeHash = useRouteHash()
let blockHashUpdate = false
let initialLoad = true

//Allow navigation with forward/back buttons
//TODO: Extend for dialogues?
//TODO: force update widget.promoted
async function navigateToHash(newHash: string | undefined | null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're doing a lot imperatively here that could be done through Route definitions and use of the Router, but I don't know for sure how easy it would be to transition to that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that it would be particularly hard. The navigation to a new graph happens before any event listeners or watched values change. The correct router based approach would mean initiating a navigation to the new address which causes the new graph or subgraph to load.

const root = app.graph
const locatorId = newHash?.slice(1) ?? root.id
const canvas = canvasStore.getCanvas()
if (canvas.graph?.id === locatorId) return
const targetGraph =
(locatorId || root.id) !== root.id
? root.subgraphs.get(locatorId)
: root
if (targetGraph) return canvas.setGraph(targetGraph)

//Search all open workflows
for (const workflow of workflowStore.openWorkflows) {
const { activeState } = workflow
if (!activeState) continue
const subgraphs = activeState.definitions?.subgraphs ?? []
for (const graph of [activeState, ...subgraphs]) {
if (graph.id !== locatorId) continue
//This will trigger a navigation, which can break forward history
try {
blockHashUpdate = true
await useWorkflowService().openWorkflow(workflow)
} finally {
blockHashUpdate = false
}
const targetGraph =
app.graph.id === locatorId
? app.graph
: app.graph.subgraphs.get(locatorId)
if (!targetGraph) {
console.error('subgraph poofed after load?')
return
}

return canvas.setGraph(targetGraph)
}
}
}
Comment on lines +168 to +205
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Script executed:

#!/bin/bash
# Search for setGraph method definition to check if it's async
rg -n "setGraph\s*\(" --type ts --type tsx -A 5 | head -100

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stderr:

unrecognized file type: tsx

</shell_result>


Script executed:

#!/bin/bash
# Find canvas-related files
fd "canvas" --type f | grep -E "\.(ts|tsx|js)$" | head -20

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

browser_tests/tests/graphCanvasMenu.spec.ts
src/composables/element/useCanvasPositionConversion.ts
src/composables/graph/useCanvasRefresh.ts
src/composables/maskeditor/useCanvasHistory.ts
src/composables/maskeditor/useCanvasManager.ts
src/composables/maskeditor/useCanvasTools.ts
src/composables/node/useNodeCanvasImagePreview.ts
src/composables/useCanvasDrop.ts
src/lib/litegraph/src/CanvasPointer.ts
src/lib/litegraph/src/LGraphCanvas.ts
src/lib/litegraph/src/infrastructure/LGraphCanvasEventMap.ts
src/renderer/core/canvas/canvasStore.ts
src/renderer/core/canvas/interaction/canvasPointerEvent.ts
src/renderer/core/canvas/useCanvasInteractions.ts
src/renderer/extensions/minimap/minimapCanvasRenderer.ts
tests-ui/tests/composables/maskeditor/useCanvasHistory.test.ts
tests-ui/tests/composables/maskeditor/useCanvasManager.test.ts
tests-ui/tests/composables/maskeditor/useCanvasTools.test.ts
tests-ui/tests/litegraph/core/LGraphCanvas.titleButtons.test.ts
tests-ui/tests/litegraph/utils/CanvasPointer.deviceDetection.test.ts

</shell_result>


Script executed:

#!/bin/bash
# Get the file line count and read the specific function
wc -l src/stores/subgraphNavigationStore.ts
echo "---"
sed -n '168,205p' src/stores/subgraphNavigationStore.ts

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

239 src/stores/subgraphNavigationStore.ts
---
    async function navigateToHash(newHash: string | undefined | null) {
      const root = app.graph
      const locatorId = newHash?.slice(1) ?? root.id
      const canvas = canvasStore.getCanvas()
      if (canvas.graph?.id === locatorId) return
      const targetGraph =
        (locatorId || root.id) !== root.id
          ? root.subgraphs.get(locatorId)
          : root
      if (targetGraph) return canvas.setGraph(targetGraph)

      //Search all open workflows
      for (const workflow of workflowStore.openWorkflows) {
        const { activeState } = workflow
        if (!activeState) continue
        const subgraphs = activeState.definitions?.subgraphs ?? []
        for (const graph of [activeState, ...subgraphs]) {
          if (graph.id !== locatorId) continue
          //This will trigger a navigation, which can break forward history
          try {
            blockHashUpdate = true
            await useWorkflowService().openWorkflow(workflow)
          } finally {
            blockHashUpdate = false
          }
          const targetGraph =
            app.graph.id === locatorId
              ? app.graph
              : app.graph.subgraphs.get(locatorId)
          if (!targetGraph) {
            console.error('subgraph poofed after load?')
            return
          }

          return canvas.setGraph(targetGraph)
        }
      }
    }

</shell_result>


Script executed:

#!/bin/bash
# Search for setGraph implementation
ast-grep --pattern $'setGraph($$$) {
  $$$
}'

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

</shell_result>


Script executed:

#!/bin/bash
# Alternative search using rg with context
rg "setGraph" -A 10 --type ts --type tsx | grep -v node_modules | head -150

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stderr:

unrecognized file type: tsx

</shell_result>


Script executed:

#!/bin/bash
# Look for canvas store to understand setGraph
fd -e ts -e tsx | xargs rg -l "setGraph" | head -10

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

src/composables/useCoreCommands.ts
src/lib/litegraph/src/LGraphCanvas.ts
src/lib/litegraph/src/LGraph.ts
src/stores/subgraphNavigationStore.ts
src/stores/subgraphStore.ts
src/scripts/changeTracker.ts

</shell_result>


Script executed:

#!/bin/bash
# Search in canvasStore.ts for setGraph method
cat -n src/renderer/core/canvas/canvasStore.ts | grep -A 20 "setGraph"

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Also check LGraphCanvas for setGraph
cat -n src/lib/litegraph/src/LGraphCanvas.ts | grep -A 15 "setGraph"

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

  1850	  setGraph(newGraph: LGraph | Subgraph): void {
  1851	    const { graph } = this
  1852	    if (newGraph === graph) return
  1853	
  1854	    this.clear()
  1855	    newGraph.attachCanvas(this)
  1856	
  1857	    // Re-initialize link renderer with new graph
  1858	    this.linkRenderer = new LitegraphLinkAdapter(false)
  1859	
  1860	    this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
  1861	    this.#dirty()
  1862	  }
  1863	
  1864	  openSubgraph(subgraph: Subgraph, fromNode: SubgraphNode): void {
  1865	    const { graph } = this
--
  1880	    this.setGraph(subgraph)
  1881	
  1882	    this.canvas.dispatchEvent(new CustomEvent('subgraph-opened', options))
  1883	  }
  1884	
  1885	  /**
  1886	   * @returns the visually active graph (in case there are more in the stack)
  1887	   */
  1888	  getCurrentGraph(): LGraph | null {
  1889	    return this.graph
  1890	  }
  1891	
  1892	  /**
  1893	   * Finds the canvas if required, throwing on failure.
  1894	   * @param canvas Canvas element, or its element ID
  1895	   * @returns The canvas element

</shell_result>


Script executed:

#!/bin/bash
# Get the full definition by looking at canvasStore
head -100 src/renderer/core/canvas/canvasStore.ts

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

import { useEventListener, whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, markRaw, ref, shallowRef } from 'vue'
import type { Raw } from 'vue'

import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
import type {
  LGraph,
  LGraphCanvas,
  LGraphGroup,
  LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { isLGraphGroup, isLGraphNode, isReroute } from '@/utils/litegraphUtil'

export const useTitleEditorStore = defineStore('titleEditor', () => {
  const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)

  return {
    titleEditorTarget
  }
})

export const useCanvasStore = defineStore('canvas', () => {
  /**
   * The LGraphCanvas instance.
   *
   * The root LGraphCanvas object is a shallow ref.
   */
  const canvas = shallowRef<LGraphCanvas | null>(null)
  /**
   * The selected items on the canvas. All stored items are raw.
   */
  const selectedItems = ref<Raw<Positionable>[]>([])
  const updateSelectedItems = () => {
    const items = Array.from(canvas.value?.selectedItems ?? [])
    selectedItems.value = items.map((item) => markRaw(item))
  }

  // Reactive scale percentage that syncs with app.canvas.ds.scale
  const appScalePercentage = ref(100)

  const linearMode = ref(false)

  // Set up scale synchronization when canvas is available
  let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
    undefined
  const initScaleSync = () => {
    if (app.canvas?.ds) {
      // Initial sync
      originalOnChanged = app.canvas.ds.onChanged
      appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)

      // Set up continuous sync
      app.canvas.ds.onChanged = () => {
        if (app.canvas?.ds?.scale) {
          appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
        }
        // Call original handler if exists
        originalOnChanged?.(app.canvas.ds.scale, app.canvas.ds.offset)
      }
    }
  }

  const cleanupScaleSync = () => {
    if (app.canvas?.ds) {
      app.canvas.ds.onChanged = originalOnChanged
      originalOnChanged = undefined
    }
  }

  const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
  const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
  const rerouteSelected = computed(() => selectedItems.value.some(isReroute))

  const getCanvas = () => {
    if (!canvas.value) throw new Error('getCanvas: canvas is null')
    return canvas.value
  }

  /**
   * Sets the canvas zoom level from a percentage value
   * @param percentage - Zoom percentage value (1-1000, where 1000 = 1000% zoom)
   */
  const setAppZoomFromPercentage = (percentage: number) => {
    if (!app.canvas?.ds || percentage <= 0) return

    // Convert percentage to scale (1000% = 10.0 scale)
    const newScale = percentage / 100
    const ds = app.canvas.ds

    ds.changeScale(
      newScale,
      ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
    )
    app.canvas.setDirty(true, true)

    // Update reactive value immediately for UI consistency
    appScalePercentage.value = Math.round(newScale * 100)
  }

</shell_result>


Script executed:

#!/bin/bash
# Find setGraph method definition with more context
rg -n "setGraph\s*\(" -A 8 src/renderer/core/canvas/canvasStore.ts

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 1

</shell_result>


Script executed:

#!/bin/bash
# Check the entire canvasStore structure
wc -l src/renderer/core/canvas/canvasStore.ts
head -200 src/renderer/core/canvas/canvasStore.ts

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

152 src/renderer/core/canvas/canvasStore.ts
import { useEventListener, whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, markRaw, ref, shallowRef } from 'vue'
import type { Raw } from 'vue'

import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
import type {
  LGraph,
  LGraphCanvas,
  LGraphGroup,
  LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { isLGraphGroup, isLGraphNode, isReroute } from '@/utils/litegraphUtil'

export const useTitleEditorStore = defineStore('titleEditor', () => {
  const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)

  return {
    titleEditorTarget
  }
})

export const useCanvasStore = defineStore('canvas', () => {
  /**
   * The LGraphCanvas instance.
   *
   * The root LGraphCanvas object is a shallow ref.
   */
  const canvas = shallowRef<LGraphCanvas | null>(null)
  /**
   * The selected items on the canvas. All stored items are raw.
   */
  const selectedItems = ref<Raw<Positionable>[]>([])
  const updateSelectedItems = () => {
    const items = Array.from(canvas.value?.selectedItems ?? [])
    selectedItems.value = items.map((item) => markRaw(item))
  }

  // Reactive scale percentage that syncs with app.canvas.ds.scale
  const appScalePercentage = ref(100)

  const linearMode = ref(false)

  // Set up scale synchronization when canvas is available
  let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
    undefined
  const initScaleSync = () => {
    if (app.canvas?.ds) {
      // Initial sync
      originalOnChanged = app.canvas.ds.onChanged
      appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)

      // Set up continuous sync
      app.canvas.ds.onChanged = () => {
        if (app.canvas?.ds?.scale) {
          appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
        }
        // Call original handler if exists
        originalOnChanged?.(app.canvas.ds.scale, app.canvas.ds.offset)
      }
    }
  }

  const cleanupScaleSync = () => {
    if (app.canvas?.ds) {
      app.canvas.ds.onChanged = originalOnChanged
      originalOnChanged = undefined
    }
  }

  const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
  const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
  const rerouteSelected = computed(() => selectedItems.value.some(isReroute))

  const getCanvas = () => {
    if (!canvas.value) throw new Error('getCanvas: canvas is null')
    return canvas.value
  }

  /**
   * Sets the canvas zoom level from a percentage value
   * @param percentage - Zoom percentage value (1-1000, where 1000 = 1000% zoom)
   */
  const setAppZoomFromPercentage = (percentage: number) => {
    if (!app.canvas?.ds || percentage <= 0) return

    // Convert percentage to scale (1000% = 10.0 scale)
    const newScale = percentage / 100
    const ds = app.canvas.ds

    ds.changeScale(
      newScale,
      ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
    )
    app.canvas.setDirty(true, true)

    // Update reactive value immediately for UI consistency
    appScalePercentage.value = Math.round(newScale * 100)
  }

  const currentGraph = shallowRef<LGraph | null>(null)
  const isInSubgraph = ref(false)

  // Provide selection state to all Vue nodes
  const selectedNodeIds = computed(
    () =>
      new Set(
        selectedItems.value
          .filter((item) => item.id !== undefined)
          .map((item) => String(item.id))
      )
  )

  whenever(
    () => canvas.value,
    (newCanvas) => {
      useEventListener(
        newCanvas.canvas,
        'litegraph:set-graph',
        (event: CustomEvent<{ newGraph: LGraph; oldGraph: LGraph }>) => {
          const newGraph = event.detail?.newGraph ?? app.canvas?.graph // TODO: Ambiguous Graph
          currentGraph.value = newGraph
          isInSubgraph.value = Boolean(app.canvas?.subgraph)
        }
      )

      useEventListener(newCanvas.canvas, 'subgraph-opened', () => {
        isInSubgraph.value = true
      })
    },
    { immediate: true }
  )

  return {
    canvas,
    selectedItems,
    selectedNodeIds,
    nodeSelected,
    groupSelected,
    rerouteSelected,
    appScalePercentage,
    linearMode,
    updateSelectedItems,
    getCanvas,
    setAppZoomFromPercentage,
    initScaleSync,
    cleanupScaleSync,
    currentGraph,
    isInSubgraph
  }
})

</shell_result>


Script executed:

#!/bin/bash
# Find calls to navigateToHash
rg -n "navigateToHash" src/stores/subgraphNavigationStore.ts -B 3 -A 3

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

165-    //Allow navigation with forward/back buttons
166-    //TODO: Extend for dialogues?
167-    //TODO: force update widget.promoted
168:    async function navigateToHash(newHash: string | undefined | null) {
169-      const root = app.graph
170-      const locatorId = newHash?.slice(1) ?? root.id
171-      const canvas = canvasStore.getCanvas()
--
210-        router.replace('#' + (window.location.hash.slice(1) || app.graph.id))
211-      } else if (initialLoad) {
212-        initialLoad = false
213:        navigateToHash(routeHash.value)
214-        const graph = canvasStore.getCanvas().graph
215-        if (isSubgraph(graph)) workflowStore.activeSubgraph = graph
216-        return
--
223-    //update navigation hash
224-    //NOTE: Doesn't apply on workflow load
225-    watch(() => canvasStore.currentGraph, updateHash)
226:    watch(routeHash, navigateToHash)
227-
228-    return {
229-      activeSubgraph,

</shell_result>


Script executed:

#!/bin/bash
# Look for watchers or event listeners that call this function
rg -n "navigateToHash" --type ts -B 2 -A 2 | head -100

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

src/stores/subgraphNavigationStore.ts-166-    //TODO: Extend for dialogues?
src/stores/subgraphNavigationStore.ts-167-    //TODO: force update widget.promoted
src/stores/subgraphNavigationStore.ts:168:    async function navigateToHash(newHash: string | undefined | null) {
src/stores/subgraphNavigationStore.ts-169-      const root = app.graph
src/stores/subgraphNavigationStore.ts-170-      const locatorId = newHash?.slice(1) ?? root.id
--
src/stores/subgraphNavigationStore.ts-211-      } else if (initialLoad) {
src/stores/subgraphNavigationStore.ts-212-        initialLoad = false
src/stores/subgraphNavigationStore.ts:213:        navigateToHash(routeHash.value)
src/stores/subgraphNavigationStore.ts-214-        const graph = canvasStore.getCanvas().graph
src/stores/subgraphNavigationStore.ts-215-        if (isSubgraph(graph)) workflowStore.activeSubgraph = graph
--
src/stores/subgraphNavigationStore.ts-224-    //NOTE: Doesn't apply on workflow load
src/stores/subgraphNavigationStore.ts-225-    watch(() => canvasStore.currentGraph, updateHash)
src/stores/subgraphNavigationStore.ts:226:    watch(routeHash, navigateToHash)
src/stores/subgraphNavigationStore.ts-227-
src/stores/subgraphNavigationStore.ts-228-    return {

</shell_result>


Script executed:

#!/bin/bash
# Get more context on the full subgraphNavigationStore to see where navigateToHash is called
head -170 src/stores/subgraphNavigationStore.ts | tail -30

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

      const path = findSubgraphPathById(subgraph.rootGraph, subgraph.id)
      const isInReachableSubgraph = !!path
      if (isInReachableSubgraph) {
        idStack.value = [...path]
      } else {
        // Treat as if opening a new subgraph
        idStack.value = [subgraph.id]
      }

      // Always try to restore viewport for the target subgraph
      restoreViewport(subgraph.id)
    }

    // Update navigation stack when opened subgraph changes (also triggers when switching workflows)
    watch(
      () => workflowStore.activeSubgraph,
      (newValue, oldValue) => {
        onNavigated(newValue, oldValue)
      }
    )
    const routeHash = useRouteHash()
    let blockHashUpdate = false
    let initialLoad = true

    //Allow navigation with forward/back buttons
    //TODO: Extend for dialogues?
    //TODO: force update widget.promoted
    async function navigateToHash(newHash: string | undefined | null) {
      const root = app.graph
      const locatorId = newHash?.slice(1) ?? root.id

</shell_result>


Script executed:

#!/bin/bash
# Get the context around navigateToHash function definition
sed -n '150,240p' src/stores/subgraphNavigationStore.ts

Repository: Comfy-Org/ComfyUI_frontend


<shell_result>
Repository: Comfy-Org/ComfyUI_frontend
Exit code: 0

stdout:

      // Always try to restore viewport for the target subgraph
      restoreViewport(subgraph.id)
    }

    // Update navigation stack when opened subgraph changes (also triggers when switching workflows)
    watch(
      () => workflowStore.activeSubgraph,
      (newValue, oldValue) => {
        onNavigated(newValue, oldValue)
      }
    )
    const routeHash = useRouteHash()
    let blockHashUpdate = false
    let initialLoad = true

    //Allow navigation with forward/back buttons
    //TODO: Extend for dialogues?
    //TODO: force update widget.promoted
    async function navigateToHash(newHash: string | undefined | null) {
      const root = app.graph
      const locatorId = newHash?.slice(1) ?? root.id
      const canvas = canvasStore.getCanvas()
      if (canvas.graph?.id === locatorId) return
      const targetGraph =
        (locatorId || root.id) !== root.id
          ? root.subgraphs.get(locatorId)
          : root
      if (targetGraph) return canvas.setGraph(targetGraph)

      //Search all open workflows
      for (const workflow of workflowStore.openWorkflows) {
        const { activeState } = workflow
        if (!activeState) continue
        const subgraphs = activeState.definitions?.subgraphs ?? []
        for (const graph of [activeState, ...subgraphs]) {
          if (graph.id !== locatorId) continue
          //This will trigger a navigation, which can break forward history
          try {
            blockHashUpdate = true
            await useWorkflowService().openWorkflow(workflow)
          } finally {
            blockHashUpdate = false
          }
          const targetGraph =
            app.graph.id === locatorId
              ? app.graph
              : app.graph.subgraphs.get(locatorId)
          if (!targetGraph) {
            console.error('subgraph poofed after load?')
            return
          }

          return canvas.setGraph(targetGraph)
        }
      }
    }

    function updateHash() {
      if (blockHashUpdate) return
      if (!routeHash.value) {
        router.replace('#' + (window.location.hash.slice(1) || app.graph.id))
      } else if (initialLoad) {
        initialLoad = false
        navigateToHash(routeHash.value)
        const graph = canvasStore.getCanvas().graph
        if (isSubgraph(graph)) workflowStore.activeSubgraph = graph
        return
      }
      const newId = canvasStore.getCanvas().graph?.id ?? ''
      const currentId = window.location.hash.slice(1)
      if (!newId || newId === (currentId || app.graph.id)) return
      router.push('#' + newId)
    }
    //update navigation hash
    //NOTE: Doesn't apply on workflow load
    watch(() => canvasStore.currentGraph, updateHash)
    watch(routeHash, navigateToHash)

    return {
      activeSubgraph,
      navigationStack,
      restoreState,
      exportState,
      saveViewport,
      restoreViewport,
      updateHash,
      viewportCache
    }
  }
)

</shell_result>


Remove the first concern; the second concern about error handling needs refinement.

After verification, I found critical issues with the original review comment:

  1. canvas.setGraph() does NOT return a Promise: The method signature in LGraphCanvas.ts (line 1850) is setGraph(newGraph: LGraph | Subgraph): void. There is no missing await — the method is synchronous and awaiting is unnecessary and incorrect.

  2. The actual error handling issue is different: navigateToHash is an async function, but its callers do not await it (line 213 in direct call, line 226 in the watcher). The console.error on line 198 is a defensive check for an edge case ("subgraph poofed after load?") and does propagate the error by logging it before returning.

The console.error usage is reasonable for this rare edge case, though converting it to throw an error would allow callers to handle it if they awaited the function.

🤖 Prompt for AI Agents
In src/stores/subgraphNavigationStore.ts around lines 168 to 205, navigateToHash
is async but its callers are not awaiting it (calls around lines 213 and 226),
and the current in-function console.error for the "subgraph poofed after load?"
case just logs then returns, which hides failures from callers; update the
callers to await navigateToHash and handle rejections (add await and wrap in
try/catch where appropriate), and change the console.error branch to throw a
descriptive Error (or return a rejected promise) instead of only logging so the
awaiting callers can observe and handle the failure.


function updateHash() {
if (blockHashUpdate) return
if (!routeHash.value) {
router.replace('#' + (window.location.hash.slice(1) || app.graph.id))
} else if (initialLoad) {
initialLoad = false
navigateToHash(routeHash.value)
const graph = canvasStore.getCanvas().graph
if (isSubgraph(graph)) workflowStore.activeSubgraph = graph
return
}
const newId = canvasStore.getCanvas().graph?.id ?? ''
const currentId = window.location.hash.slice(1)
if (!newId || newId === (currentId || app.graph.id)) return
router.push('#' + newId)
}
//update navigation hash
//NOTE: Doesn't apply on workflow load
watch(() => canvasStore.currentGraph, updateHash)
watch(routeHash, navigateToHash)

return {
activeSubgraph,
Expand All @@ -165,6 +232,7 @@ export const useSubgraphNavigationStore = defineStore(
exportState,
saveViewport,
restoreViewport,
updateHash,
viewportCache
}
}
Expand Down
Loading