-
Notifications
You must be signed in to change notification settings - Fork 420
Allow graph navigation by browser forward/backward #6811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
🎭 Playwright Test Results❌ Some tests failed ⏰ Completed at: 11/26/2025, 06:39:56 PM UTC 📊 Test Reports by Browser
🎉 Click on the links above to view detailed test results for each browser configuration. |
🎨 Storybook Build Status✅ Build completed successfully! ⏰ Completed at: 11/26/2025, 06:21:34 PM UTC 🔗 Links🎉 Your Storybook is ready for review! |
Bundle Size ReportSummary
Category Glance Per-category breakdownApp Entry Points — 3.18 MB (baseline 3.18 MB) • 🔴 +2.23 kBMain entry bundles and manifests
Status: 3 added / 3 removed Graph Workspace — 946 kB (baseline 946 kB) • 🟢 -21 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 6.54 kB (baseline 6.54 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 1 added / 1 removed Panels & Settings — 298 kB (baseline 298 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 6 added / 6 removed UI Components — 139 kB (baseline 139 kB) • 🔴 +35 BReusable component library chunks
Status: 9 added / 9 removed Data & Services — 12.5 kB (baseline 12.5 kB) • ⚪ 0 BStores, services, APIs, and repositories
Status: 3 added / 3 removed Utilities & Hooks — 2.94 kB (baseline 2.94 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 1 added / 1 removed Vendor & Third-Party — 8.56 MB (baseline 8.56 MB) • 🔴 +1.42 kBExternal libraries and shared vendor chunks
Status: 5 added / 5 removed Other — 3.84 MB (baseline 3.84 MB) • 🔴 +175 BBundles that do not match a named category
Status: 23 added / 23 removed |
| //Allow navigation with forward/back buttons | ||
| //TODO: Extend for dialogues? | ||
| //TODO: force update widget.promoted | ||
| function onHashChange() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about on page reload? I.e., initial load
|
Would consider using https://vueuse.org/router/useRouteHash/ |
Already got pinged by Alex for this and made the change. Will push once I fix your suggestion of making navigation work on initial load. |
📝 WalkthroughWalkthroughAdds hash-based URL navigation for subgraphs with back/forward support and workflow integration; exposes and calls updateHash after graph load; removes Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Router
participant App
participant SubgraphNav as Store
participant Workflow
rect rgb(235,245,255)
Note over Browser,App: App starts and loads graph
App->>SubgraphNav: loadGraphData() completes
App->>SubgraphNav: updateHash()
SubgraphNav->>Router: read current route hash
SubgraphNav->>SubgraphNav: set initialLoad = true
end
rect rgb(235,255,235)
Note over Browser,Router: External hash change (back/forward)
Browser->>Router: URL hash changes
Router->>SubgraphNav: notify routeHash change
alt blockHashUpdate is false
SubgraphNav->>SubgraphNav: navigateToHash()
alt target subgraph missing
SubgraphNav->>Workflow: request open workflow containing target
Workflow-->>SubgraphNav: workflow opened
SubgraphNav->>SubgraphNav: set active subgraph
end
end
end
rect rgb(255,245,235)
Note over App,SubgraphNav: Internal navigation (user action)
SubgraphNav->>SubgraphNav: set blockHashUpdate = true
SubgraphNav->>SubgraphNav: update navigation stack / active subgraph
SubgraphNav->>Router: push/replace URL hash via updateHash()
SubgraphNav->>SubgraphNav: set blockHashUpdate = false
end
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
src/scripts/app.ts (1)
60-60: New subgraphNavigationStore usage introduces a circular import but is currently safeImporting
useSubgraphNavigationStorehere whilesrc/stores/subgraphNavigationStore.tsalready imports{ app }creates a module cycle, but sinceuseSubgraphNavigationStore().updateHash()is only called from theComfyAppinstance method (afterappis constructed), this shouldn’t break initialization.If we ever start using
useSubgraphNavigationStore()at module top‑level elsewhere, it’d be worth revisiting this and possibly extracting hash-sync concerns into a smaller helper to avoid tighter coupling betweenapp.tsand the store. For now, the placement ofupdateHash()afterafterLoadNewGraph()looks appropriate and keeps the URL in sync with the loaded graph.Also applies to: 1309-1310
browser_tests/fixtures/ws.ts (1)
30-36: Hash stripping for WebSocket URL is correct; consider simplifying URL constructionDropping the fragment via
window.location.toString().split('#')[0]avoids producing invalid URLs like#graphIdwsonce graph IDs live in the hash, which is the right direction here.If you want this fixture to be more explicit and robust, you could construct the WS URL directly from
window.location.origin, clearing search/hash instead of relying on string concat:- if (!url) { - // If no URL specified, use page URL - const u = new URL(window.location.toString().split('#')[0]) - u.protocol = 'ws:' - u.pathname = '/' - url = u.toString() + 'ws' - } + if (!url) { + // If no URL specified, connect to `${origin}/ws` + const u = new URL(window.location.origin) + u.protocol = 'ws:' + u.pathname = '/ws' + u.search = '' + u.hash = '' + url = u.toString() + }Not mandatory, but it avoids odd query strings like
?foo=barwsif tests ever run with query parameters.src/stores/subgraphNavigationStore.ts (2)
161-227: Hash navigation logic: fix operator precedence, initial-load race, and hash normalizationThe overall approach (using
useRouteHash, syncing canvas graph ↔ URL hash, and guarding withblockHashUpdate/initialLoad) looks solid, but there are a few concrete issues and edge cases worth tightening:
Operator precedence bug in
router.replace(...)(Line 210)router.replace('#' + window.location.hash.slice(1) || app.graph.id)Because
+has higher precedence than||, this always evaluates as:router.replace(('#' + window.location.hash.slice(1)) || app.graph.id)so the fallback to
app.graph.idnever runs. On an initial load with no hash, this produces'#'instead of'#<graphId>', and later thenewId === (currentId || app.graph.id)check prevents correcting it.Suggestion:
if (!routeHash.value) {router.replace('#' + window.location.hash.slice(1) || app.graph.id)
if (!routeHash.value) {const raw = window.location.hash.slice(1) || app.graph.idrouter.replace({ hash: `#${raw}` }) }Using the object form also aligns better with vue-router’s documented API.
Potential race on initial load when hash targets another workflow (Lines 211–216)
In theinitialLoadbranch:initialLoad = false navigateToHash(routeHash.value) const graph = canvasStore.getCanvas().graph if (isSubgraph(graph)) workflowStore.activeSubgraph = graph
navigateToHashisasync(itawaitsuseWorkflowService().openWorkflow(workflow)), but it’s not awaited here. If the hash points into a different workflow,graphcan still be the old workflow’s root when you read it, soactiveSubgraphmay never be updated for the target subgraph.Two options to avoid the race:
- Make
updateHashasync and awaitnavigateToHashin the initial-load branch, or- Move the
activeSubgraphupdate intonavigateToHashaftercanvas.setGraph(targetGraph)in both code paths (same-workflow and cross-workflow), so it always runs once navigation completes.Either approach keeps
workflowStore.activeSubgraphconsistent with the visible graph.Hash normalization in
navigateToHash(Line 169)const locatorId = newHash?.slice(1) ?? root.idThis assumes
newHashalways includes a leading#. Today that’s probably true for youruseRouteHash()+ vue-router setup, but ifuseRouteHashever returns a bare id (or its behavior changes), you’d silently drop the first character.A more defensive version that works with both
'#id'and'id':
- async function navigateToHash(newHash: string | undefined | null) {
const root = app.graphconst locatorId = newHash?.slice(1) ?? root.id
- async function navigateToHash(newHash: string | undefined | null) {
const root = app.graphconst locatorId = newHash? newHash.startsWith('#')? newHash.slice(1): newHash: root.id
Optional: guard against missing canvas (Lines 171, 214, 218)
BothnavigateToHashandupdateHashcallcanvasStore.getCanvas()and immediately dereference.graph. If this store is ever instantiated before the canvas is ready (or in tests), that could throw. A simple guard would make it safer:const canvas = canvasStore.getCanvas() if (!canvas) returnNot strictly required if you know the store is only used after setup, but low-cost defensive coding.
235-236: ExposingupdateHashfrom the store is useful; consider documenting its intended call sitesReturning
updateHashhere to be called fromapp.loadGraphDatais a good way to handle the “workflow load doesn’t emitcanvasStore.currentGraphchanges” gap.Given it has some internal state (
blockHashUpdate,initialLoad), it may be worth a brief JSDoc comment near the function saying it’s meant to be invoked:
- After a new graph is loaded (e.g.,
afterLoadNewGraph), and- Not during internal hash-driven navigation (where
blockHashUpdatesuppresses it).That will help future maintainers avoid calling it in the wrong phase and accidentally fighting the router/watchers.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
browser_tests/fixtures/ws.ts(1 hunks)package.json(1 hunks)src/components/breadcrumb/SubgraphBreadcrumbItem.vue(0 hunks)src/scripts/app.ts(2 hunks)src/stores/subgraphNavigationStore.ts(3 hunks)
💤 Files with no reviewable changes (1)
- src/components/breadcrumb/SubgraphBreadcrumbItem.vue
🧰 Additional context used
🧬 Code graph analysis (2)
src/scripts/app.ts (1)
src/stores/subgraphNavigationStore.ts (1)
useSubgraphNavigationStore(21-239)
src/stores/subgraphNavigationStore.ts (5)
src/scripts/app.ts (2)
app(1710-1710)graph(161-163)src/platform/workflow/management/stores/workflowStore.ts (1)
activeState(60-62)src/lib/litegraph/src/LGraph.ts (1)
subgraphs(383-385)src/platform/workflow/core/services/workflowService.ts (1)
useWorkflowService(22-413)src/utils/typeGuardUtil.ts (1)
isSubgraph(18-20)
🪛 ESLint
src/stores/subgraphNavigationStore.ts
[error] 4-4: Unable to resolve path to module '@vueuse/router'.
(import-x/no-unresolved)
[error] 8-8: Unable to resolve path to module '@/platform/workflow/management/stores/workflowStore'.
(import-x/no-unresolved)
[error] 9-9: Unable to resolve path to module '@/platform/workflow/core/services/workflowService'.
(import-x/no-unresolved)
[error] 10-10: Unable to resolve path to module '@/renderer/core/canvas/canvasStore'.
(import-x/no-unresolved)
[error] 11-11: Unable to resolve path to module '@/router'.
(import-x/no-unresolved)
[error] 12-12: Unable to resolve path to module '@/scripts/app'.
(import-x/no-unresolved)
[error] 13-13: Unable to resolve path to module '@/utils/graphTraversalUtil'.
(import-x/no-unresolved)
[error] 14-14: Unable to resolve path to module '@/utils/typeGuardUtil'.
(import-x/no-unresolved)
🔇 Additional comments (1)
package.json (1)
152-155: Ensure @vueuse/router version is compatible with your existing VueUse / router stackAdding
@vueuse/routeras^14.0.0looks fine, but since@vueuse/coreandvue-routerare pulled in via thecatalog:indirection, please double‑check that:
- The
@vueuse/routerversion you get is intended to pair with the@vueuse/coreversion from your catalog.- The workspace lockfile has been updated (
pnpm install) so tools like eslint/import‑x no longer reportno-unresolvedon@vueuse/router.
| @@ -1,2 +1,2 @@ | |||
| <template> | |||
| <a | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a RouterLink?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From quick testing, it wouldn't be a quick substitution. It's causing errors on click about missing state.
Perhaps we move in the opposite direction and replace the with a
| //Allow navigation with forward/back buttons | ||
| //TODO: Extend for dialogues? | ||
| //TODO: force update widget.promoted | ||
| async function navigateToHash(newHash: string | undefined | null) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
e4774ca to
eb362ab
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/stores/subgraphNavigationStore.ts (1)
215-215: Consider alternative to direct store mutation.Line 215 directly mutates
workflowStore.activeSubgraphduring initial load. While this works, it couples navigation logic with store state management. Consider whether the existing watchers could handle this case, or if a dedicated method on the workflow store would be cleaner.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
browser_tests/fixtures/ws.ts(1 hunks)package.json(1 hunks)src/components/breadcrumb/SubgraphBreadcrumbItem.vue(0 hunks)src/scripts/app.ts(2 hunks)src/stores/subgraphNavigationStore.ts(3 hunks)
💤 Files with no reviewable changes (1)
- src/components/breadcrumb/SubgraphBreadcrumbItem.vue
🚧 Files skipped from review as they are similar to previous changes (2)
- browser_tests/fixtures/ws.ts
- src/scripts/app.ts
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{vue,ts,tsx}: Leverage VueUse functions for performance-enhancing utilities
Use vue-i18n in Composition API for any string literals and place new translation entries in src/locales/en/main.json
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (.cursorrules)
Use es-toolkit for utility functions
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use TypeScript for type safety
**/*.{ts,tsx}: Never useanytype - use proper TypeScript types
Never useas anytype assertions - fix the underlying type issue
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,js,vue}
📄 CodeRabbit inference engine (.cursorrules)
Implement proper error handling in components and services
**/*.{ts,tsx,js,vue}: Use 2-space indentation, single quotes, no semicolons, and maintain 80-character line width as configured in.prettierrc
Organize imports by sorting and grouping by plugin, and runpnpm formatbefore committing
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.{vue,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/**/*.{vue,ts}: Leverage VueUse functions for performance-enhancing styles
Implement proper error handling
Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/**/*.ts: Use es-toolkit for utility functions
Use TypeScript for type safety
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,js,jsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
Use camelCase for variable and setting names in TypeScript/Vue files
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,vue}: Useconst settingStore = useSettingStore()andsettingStore.get('Comfy.SomeSetting')to retrieve settings in TypeScript/Vue files
Useawait settingStore.set('Comfy.SomeSetting', newValue)to update settings in TypeScript/Vue files
Check server capabilities usingapi.serverSupportsFeature('feature_name')before using enhanced features
Useapi.getServerFeature('config_name', defaultValue)to retrieve server feature configurationEnforce ESLint rules for Vue + TypeScript including: no floating promises, no unused imports, and i18n raw text restrictions in templates
Files:
src/stores/subgraphNavigationStore.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Define dynamic setting defaults using runtime context with functions in settings configuration
UsedefaultsByInstallVersionproperty for gradual feature rollout based on version in settings configuration
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (src/CLAUDE.md)
src/**/*.{ts,tsx,vue}: Sanitize HTML with DOMPurify to prevent XSS attacks
Avoid using @ts-expect-error; use proper TypeScript types instead
Use es-toolkit for utility functions instead of other utility libraries
Implement proper TypeScript types throughout the codebase
Files:
src/stores/subgraphNavigationStore.ts
src/**/stores/**/*.{ts,tsx}
📄 CodeRabbit inference engine (src/CLAUDE.md)
src/**/stores/**/*.{ts,tsx}: Maintain clear public interfaces and restrict extension access in stores
Use TypeScript for type safety in state management stores
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (src/CLAUDE.md)
Follow Vue 3 composition API style guide
Files:
src/stores/subgraphNavigationStore.ts
**/stores/*Store.ts
📄 CodeRabbit inference engine (AGENTS.md)
Name Pinia stores with the
*Store.tssuffix
Files:
src/stores/subgraphNavigationStore.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.vue : Utilize Vue 3's Teleport component when needed
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.vue : Utilize Vue 3's Teleport component when needed
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.{vue,ts,tsx} : Leverage VueUse functions for performance-enhancing utilities
📚 Learning: 2025-11-24T19:46:52.279Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.{vue,ts,tsx} : Leverage VueUse functions for performance-enhancing utilities
Applied to files:
src/stores/subgraphNavigationStore.tspackage.json
📚 Learning: 2025-11-24T19:46:52.279Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.vue : Utilize ref and reactive for reactive state in Vue 3
Applied to files:
src/stores/subgraphNavigationStore.ts
📚 Learning: 2025-11-24T19:47:02.860Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.{vue,ts} : Leverage VueUse functions for performance-enhancing styles
Applied to files:
package.json
📚 Learning: 2025-11-24T19:47:34.324Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:47:34.324Z
Learning: Applies to src/**/*.{ts,tsx,vue} : Use es-toolkit for utility functions instead of other utility libraries
Applied to files:
package.json
🧬 Code graph analysis (1)
src/stores/subgraphNavigationStore.ts (3)
src/scripts/app.ts (2)
app(1710-1710)graph(161-163)src/lib/litegraph/src/LGraph.ts (1)
subgraphs(383-385)src/utils/typeGuardUtil.ts (1)
isSubgraph(18-20)
🪛 GitHub Actions: CI: Lint Format
src/stores/subgraphNavigationStore.ts
[error] 210-210: lint: no-floating-promises: Promises must be awaited. Command 'pnpm lint:fix' failed with exit code 1.
[error] 213-213: lint: no-floating-promises: Promises must be awaited. Command 'pnpm lint:fix' failed with exit code 1.
[error] 221-221: lint: no-floating-promises: Promises must be awaited. Command 'pnpm lint:fix' failed with exit code 1.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: deploy-and-comment
- GitHub Check: setup
- GitHub Check: test
- GitHub Check: collect
🔇 Additional comments (5)
src/stores/subgraphNavigationStore.ts (4)
4-4: LGTM! Import additions support hash-based navigation.The new imports (
useRouteHash,useWorkflowService,router,isSubgraph) are necessary for the hash-based navigation feature and align with the VueUse utilities guideline.Also applies to: 9-9, 11-11, 14-14
161-163: LGTM! State variables support navigation flow control.The reactive
routeHashreference and guard flags (blockHashUpdate,initialLoad) are appropriate for managing hash synchronization and preventing recursive updates.
225-226: Watchers will handle async functions correctly.The watchers create bidirectional synchronization between the route hash and the canvas graph. Since
updateHashandnavigateToHashwill be async (after fixing the floating promises), Vue's watchers will handle the promises appropriately.
235-235: Public API change confirmed:updateHashexposed at line 235, and floating promise exists requiring async refactor.The review comment is accurate. The store exposes
updateHashas a public API, and the function contains a floating promise at line 213 wherenavigateToHash(routeHash.value)is called withoutawait. MakingupdateHashasync to fix this floating promise would require:
- The app.ts caller (line 1309) to await the call
- The watch callback at line 225 to handle async properly (watch doesn't naturally support async callbacks returning promises)
Verify that the PR addresses the floating promise by either:
- Making
updateHashasync and awaitingnavigateToHash, then updating both callers- Or keeping
updateHashsynchronous and handling the async operation differently (e.g., via a separate effect watcher)package.json (1)
156-156: Version 14.0.0 is valid and the latest stable release; no deprecation or known vulnerabilities detected.The package
@vueuse/[email protected]is confirmed to exist on npm and is marked as the latest stable version. It is not deprecated, and no security-related issues are indicated. The version specification^14.0.0is appropriate and safe.
| 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) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -100Repository: 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 -20Repository: 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.tsRepository: 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 -150Repository: 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 -10Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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 3Repository: 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 -100Repository: 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 -30Repository: 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.tsRepository: 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:
-
canvas.setGraph()does NOT return a Promise: The method signature inLGraphCanvas.ts(line 1850) issetGraph(newGraph: LGraph | Subgraph): void. There is no missingawait— the method is synchronous and awaiting is unnecessary and incorrect. -
The actual error handling issue is different:
navigateToHashis anasyncfunction, but its callers do not await it (line 213 in direct call, line 226 in the watcher). Theconsole.erroron 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/stores/subgraphNavigationStore.ts (2)
168-177: Consider clarifying empty hash handling.Line 170 uses
?? root.idwhich only handlesnull/undefined, not empty string. IfnewHashis"",slice(1)returns"", and the nullish coalescing won't apply. The logic on line 174 compensates withlocatorId || root.id, but using|| root.iddirectly on line 170 would be clearer and more direct.- const locatorId = newHash?.slice(1) ?? root.id + const locatorId = newHash?.slice(1) || root.id
207-224: Complex initial-load coordination could benefit from documentation.The
initialLoadflag orchestrates navigation betweenupdateHash,navigateToHash, and two watchers (lines 227-228). While the guards (blockHashUpdate, early returns) prevent infinite loops, the multi-step initialization flow when a hash is present at page load is intricate. Additional inline comments explaining the expected call sequence would improve maintainability.For example, adding a comment before line 213:
+ // On initial page load with a hash: navigate to target graph, sync activeSubgraph, then return + // to avoid double-processing via the routeHash watcher } else if (initialLoad) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/scripts/app.ts(2 hunks)src/stores/subgraphNavigationStore.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/scripts/app.ts
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{vue,ts,tsx}: Leverage VueUse functions for performance-enhancing utilities
Use vue-i18n in Composition API for any string literals and place new translation entries in src/locales/en/main.json
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (.cursorrules)
Use es-toolkit for utility functions
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
Use TypeScript for type safety
**/*.{ts,tsx}: Never useanytype - use proper TypeScript types
Never useas anytype assertions - fix the underlying type issue
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,js,vue}
📄 CodeRabbit inference engine (.cursorrules)
Implement proper error handling in components and services
**/*.{ts,tsx,js,vue}: Use 2-space indentation, single quotes, no semicolons, and maintain 80-character line width as configured in.prettierrc
Organize imports by sorting and grouping by plugin, and runpnpm formatbefore committing
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.{vue,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/**/*.{vue,ts}: Leverage VueUse functions for performance-enhancing styles
Implement proper error handling
Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
src/**/*.ts: Use es-toolkit for utility functions
Use TypeScript for type safety
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,js,jsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
Use camelCase for variable and setting names in TypeScript/Vue files
Files:
src/stores/subgraphNavigationStore.ts
**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,vue}: Useconst settingStore = useSettingStore()andsettingStore.get('Comfy.SomeSetting')to retrieve settings in TypeScript/Vue files
Useawait settingStore.set('Comfy.SomeSetting', newValue)to update settings in TypeScript/Vue files
Check server capabilities usingapi.serverSupportsFeature('feature_name')before using enhanced features
Useapi.getServerFeature('config_name', defaultValue)to retrieve server feature configurationEnforce ESLint rules for Vue + TypeScript including: no floating promises, no unused imports, and i18n raw text restrictions in templates
Files:
src/stores/subgraphNavigationStore.ts
**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.ts: Define dynamic setting defaults using runtime context with functions in settings configuration
UsedefaultsByInstallVersionproperty for gradual feature rollout based on version in settings configuration
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (src/CLAUDE.md)
src/**/*.{ts,tsx,vue}: Sanitize HTML with DOMPurify to prevent XSS attacks
Avoid using @ts-expect-error; use proper TypeScript types instead
Use es-toolkit for utility functions instead of other utility libraries
Implement proper TypeScript types throughout the codebase
Files:
src/stores/subgraphNavigationStore.ts
src/**/stores/**/*.{ts,tsx}
📄 CodeRabbit inference engine (src/CLAUDE.md)
src/**/stores/**/*.{ts,tsx}: Maintain clear public interfaces and restrict extension access in stores
Use TypeScript for type safety in state management stores
Files:
src/stores/subgraphNavigationStore.ts
src/**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (src/CLAUDE.md)
Follow Vue 3 composition API style guide
Files:
src/stores/subgraphNavigationStore.ts
**/stores/*Store.ts
📄 CodeRabbit inference engine (AGENTS.md)
Name Pinia stores with the
*Store.tssuffix
Files:
src/stores/subgraphNavigationStore.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.vue : Utilize Vue 3's Teleport component when needed
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-24T19:47:02.860Z
Learning: Applies to src/**/*.vue : Utilize Vue 3's Teleport component when needed
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.{vue,ts,tsx} : Leverage VueUse functions for performance-enhancing utilities
📚 Learning: 2025-11-24T19:47:56.371Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/lib/litegraph/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:47:56.371Z
Learning: Applies to src/lib/litegraph/**/*.{test,spec}.{ts,tsx} : Use provided test helpers `createTestSubgraph` and `createTestSubgraphNode` from `./fixtures/subgraphHelpers` for consistent subgraph test setup
Applied to files:
src/stores/subgraphNavigationStore.ts
📚 Learning: 2025-11-24T19:47:34.324Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:47:34.324Z
Learning: Applies to src/**/stores/**/*.{ts,tsx} : Maintain clear public interfaces and restrict extension access in stores
Applied to files:
src/stores/subgraphNavigationStore.ts
📚 Learning: 2025-11-24T19:47:56.371Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/lib/litegraph/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:47:56.371Z
Learning: Applies to src/lib/litegraph/**/*.{js,ts,jsx,tsx} : Do not replace `&&=` or `||=` with `=` when there is no reason to do so. If you do find a reason to remove either `&&=` or `||=`, leave a comment explaining why the removal occurred
Applied to files:
src/stores/subgraphNavigationStore.ts
📚 Learning: 2025-11-24T19:46:52.279Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.{vue,ts,tsx} : Leverage VueUse functions for performance-enhancing utilities
Applied to files:
src/stores/subgraphNavigationStore.ts
📚 Learning: 2025-11-24T19:46:52.279Z
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: .cursorrules:0-0
Timestamp: 2025-11-24T19:46:52.279Z
Learning: Applies to **/*.vue : Utilize ref and reactive for reactive state in Vue 3
Applied to files:
src/stores/subgraphNavigationStore.ts
🧬 Code graph analysis (1)
src/stores/subgraphNavigationStore.ts (5)
src/scripts/app.ts (2)
app(1710-1710)graph(161-163)src/platform/workflow/management/stores/workflowStore.ts (1)
activeState(60-62)src/lib/litegraph/src/LGraph.ts (1)
subgraphs(383-385)src/platform/workflow/core/services/workflowService.ts (1)
useWorkflowService(22-413)src/utils/typeGuardUtil.ts (1)
isSubgraph(18-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: deploy-and-comment
- GitHub Check: setup
- GitHub Check: lint-and-format
- GitHub Check: test
- GitHub Check: collect
🔇 Additional comments (5)
src/stores/subgraphNavigationStore.ts (5)
4-4: LGTM: VueUse integration follows guidelines.The addition of
useRouteHashfrom@vueuse/routeraligns with the project's preference for VueUse utilities. The new imports support the hash-based navigation feature appropriately.Also applies to: 9-9, 11-11, 14-14
161-163: Well-structured state management for navigation coordination.The state variables correctly use
useRouteHash()for reactive URL sync and employ flags (blockHashUpdate,initialLoad) to prevent recursive updates during complex navigation scenarios like workflow switching.
179-205: Cross-workflow navigation logic is well-implemented.The search across open workflows with
blockHashUpdateguard correctly prevents hash recursion during workflow loading. The approach preserves browser forward/back history as intended by the feature.
227-228: Bidirectional watcher coordination is correctly guarded.The watchers establish two-way sync between
currentGraphandrouteHash, with proper guards (blockHashUpdate, early returns) preventing infinite loops. Not awaiting the async callbacks is acceptable for Vue watchers in this coordination pattern.
237-237: Public API exposure is justified.Exposing
updateHashis necessary becausecanvasStore.currentGraphdoesn't trigger duringapp.loadGraphData, requiring manual invocation to sync the hash after workflow loads. This aligns with the PR objectives.
graph.idas location hashwindow.onhashchangeto navigate to the targetgraph.ideither in the current, or any other loaded workflow.canvasStore.currentGraphdoes not trigger whenapp.loadGraphDatais called. A trigger could be forced here, but I'm concerned about side effects. InsteadupdateHashis manually called.Code search shows that there are no current custom nodes using
onhashchange┆Issue is synchronized with this Notion page by Unito